finance_sdk 1.0.2
finance_sdk: ^1.0.2 copied to clipboard
A Flutter plugin for Firebase-based dynamic API orchestration. Fetches API configurations from Firestore and Remote Config to handle HTTP requests dynamically.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:finance_sdk/finance_sdk.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Note: Firebase is initialized internally by the plugin
// No need to call Firebase.initializeApp() here
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
String _status = 'Initializing...';
String _response = '';
Map<String, String> _availableApiKeys = {};
Map<String, String> _availableServices = {};
final _financeSdkPlugin = FinanceSdk();
final _testService = FinanceSdkTestService.instance;
final TextEditingController _apiKeyController = TextEditingController();
final TextEditingController _requestBodyController = TextEditingController();
final TextEditingController _baseUrlController = TextEditingController();
final TextEditingController _bearerTokenController = TextEditingController();
bool _isRunningTests = false;
bool _isJsonBody = true;
@override
void initState() {
super.initState();
initPlatformState();
_initializeSdk();
}
@override
void dispose() {
_apiKeyController.dispose();
_requestBodyController.dispose();
_baseUrlController.dispose();
_bearerTokenController.dispose();
super.dispose();
}
Future<void> initPlatformState() async {
String platformVersion;
try {
platformVersion =
await _financeSdkPlugin.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
Future<void> _initializeSdk() async {
try {
setState(() {
_status = 'Initializing Finance SDK...';
});
await _financeSdkPlugin.initialize();
// Get available API keys and services
final apiKeys = await _financeSdkPlugin.getAvailableApiKeys();
final services = await _financeSdkPlugin.getAvailableServices();
// Load current base URL from Remote Config or override
final baseUrlSource = _financeSdkPlugin.getBaseUrlSource();
setState(() {
_status = 'SDK initialized successfully!';
_availableApiKeys = apiKeys;
_availableServices = services;
// Show current base URL source
if (baseUrlSource == 'Local Override') {
_status = 'SDK initialized (using Local Override)';
}
});
} catch (e) {
setState(() {_status = 'Failed to initialize SDK: $e';});
}
}
Future<void> _sendRequest() async {
if (_apiKeyController.text.isEmpty || _requestBodyController.text.isEmpty) {
setState(() {
_response = 'Please enter API key and request body';
});
return;
}
try {
setState(() {
_status = 'Sending request...';
});
// Parse request body (supports JSON or key=value&key2=value2)
Map<String, dynamic> requestBody;
if (_isJsonBody) {
try {
final decoded = jsonDecode(_requestBodyController.text);
requestBody = decoded is Map<String, dynamic>
? decoded
: {'data': decoded};
} catch (_) {
requestBody = {'data': _requestBodyController.text};
}
} else {
try {
requestBody = Map<String, dynamic>.from(
Uri.splitQueryString(_requestBodyController.text)
);
} catch (_) {
requestBody = {'data': _requestBodyController.text};
}
}
final response = await _financeSdkPlugin.sendRequest(
apiKey: _apiKeyController.text,
requestBody: requestBody,
);
setState(() {
_status = 'Request completed';
_response = response.success
? 'Success: ${response.data}'
: 'Error: ${response.error}';
});
} catch (e) {
setState(() {
_status = 'Request failed';
_response = 'Error: $e';
});
}
}
Future<void> _refreshData() async {
try {
setState(() {
_status = 'Refreshing data...';
});
await _financeSdkPlugin.refreshData();
// Get updated API keys and services
final apiKeys = await _financeSdkPlugin.getAvailableApiKeys();
final services = await _financeSdkPlugin.getAvailableServices();
setState(() {
_status = 'Data refreshed successfully!';
_availableApiKeys = apiKeys;
_availableServices = services;
});
} catch (e) {
setState(() {
_status = 'Failed to refresh data: $e';
});
}
}
Future<void> _runTestGet() async {
if (_isRunningTests) return;
setState(() {
_isRunningTests = true;
_status = 'Running GET test...';
_response = '';
});
try {
final result = await _testService.testGetRequest();
setState(() {
_status = result.success ? 'GET test completed!' : 'GET test failed';
_response = result.success
? '✅ SUCCESS\n\nStatus: ${result.statusCode}\n\nResponse:\n${_formatResponse(result.data)}'
: '❌ FAILED\n\nError: ${result.error}';
_isRunningTests = false;
});
} catch (e) {
setState(() {
_status = 'GET test error';
_response = '❌ Exception: $e';
_isRunningTests = false;
});
}
}
Future<void> _runTestPost() async {
if (_isRunningTests) return;
setState(() {
_isRunningTests = true;
_status = 'Running POST test...';
_response = '';
});
try {
final result = await _testService.testPostRequest();
setState(() {
_status = result.success ? 'POST test completed!' : 'POST test failed';
_response = result.success
? '✅ SUCCESS\n\nStatus: ${result.statusCode}\n\nResponse:\n${_formatResponse(result.data)}'
: '❌ FAILED\n\nError: ${result.error}';
_isRunningTests = false;
});
} catch (e) {
setState(() {
_status = 'POST test error';
_response = '❌ Exception: $e';
_isRunningTests = false;
});
}
}
Future<void> _runAllTests() async {
if (_isRunningTests) return;
setState(() {
_isRunningTests = true;
_status = 'Running all tests...';
_response = '';
});
try {
// First verify configuration
final isConfigured = await _testService.verifyConfiguration();
if (!isConfigured) {
setState(() {
_status = 'Configuration check failed';
_response = '⚠️ Please ensure TEST_GET_REQUEST and TEST_POST_REQUEST are configured in Firestore.';
_isRunningTests = false;
});
return;
}
final results = await _testService.runAllTests();
final summary = results['summary'] as Map<String, dynamic>;
final testResults = results['results'] as Map<String, dynamic>;
final buffer = StringBuffer();
buffer.writeln('📊 TEST SUMMARY');
buffer.writeln('════════════════════════════════════════');
buffer.writeln('Total: ${summary['total']}');
buffer.writeln('✅ Passed: ${summary['passed']}');
buffer.writeln('❌ Failed: ${summary['failed']}');
buffer.writeln('');
buffer.writeln('════════════════════════════════════════');
for (final entry in testResults.entries) {
final testName = entry.key;
final result = entry.value as Map<String, dynamic>;
buffer.writeln('');
buffer.writeln('$testName Test:');
buffer.writeln(' Status: ${result['success'] == true ? '✅ PASSED' : '❌ FAILED'}');
if (result['statusCode'] != null) {
buffer.writeln(' HTTP Status: ${result['statusCode']}');
}
if (result['error'] != null) {
buffer.writeln(' Error: ${result['error']}');
}
if (result['data'] != null) {
buffer.writeln(' Response: ${_formatResponse(result['data'])}');
}
}
setState(() {
_status = 'All tests completed!';
_response = buffer.toString();
_isRunningTests = false;
});
} catch (e) {
setState(() {
_status = 'Test suite error';
_response = '❌ Exception: $e';
_isRunningTests = false;
});
}
}
String _formatResponse(dynamic data) {
try {
if (data is Map || data is List) {
const encoder = JsonEncoder.withIndent(' ');
return encoder.convert(data);
}
return data.toString();
} catch (e) {
return data.toString();
}
}
void _setBaseUrlOverride() {
final url = _baseUrlController.text.trim();
if (url.isEmpty) {
_financeSdkPlugin.clearBaseUrlOverride();
setState(() {
_status = 'Base URL override cleared (using Remote Config)';
});
} else {
_financeSdkPlugin.setBaseUrlOverride(url);
setState(() {
_status = 'Base URL override set: $url';
});
}
}
void _setBearerToken() {
final token = _bearerTokenController.text.trim();
if (token.isEmpty) {
_financeSdkPlugin.clearBearerToken();
setState(() {
_status = 'Bearer token cleared';
});
} else {
_financeSdkPlugin.setBearerToken(token);
setState(() {
_status = 'Bearer token set successfully';
});
}
}
void _copyResponse() async {
try {
await Clipboard.setData(ClipboardData(text: _response));
if (mounted) {
setState(() {
_status = 'Response copied to clipboard';
});
}
} catch (_) {}
}
void _prettifyResponse() {
try {
final trimmed = _response.trim();
String? headerless;
final marker = 'Response:\n';
final idx = trimmed.indexOf(marker);
if (idx >= 0) {
headerless = trimmed.substring(idx + marker.length);
}
String candidate = headerless ?? trimmed;
dynamic parsed;
try {
parsed = jsonDecode(candidate);
} catch (_) {
// Try to find JSON braces within the text
final start = candidate.indexOf('{');
final startArr = candidate.indexOf('[');
int s = -1;
if (start >= 0 && startArr >= 0) {
s = start < startArr ? start : startArr;
} else {
s = start >= 0 ? start : startArr;
}
if (s >= 0) {
final sub = candidate.substring(s);
parsed = jsonDecode(sub);
}
}
if (parsed != null) {
const encoder = JsonEncoder.withIndent(' ');
final pretty = encoder.convert(parsed);
if (headerless != null) {
setState(() {
_response = trimmed.substring(0, idx + marker.length) + pretty;
});
} else {
setState(() {
_response = pretty;
});
}
}
} catch (_) {}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Finance SDK Example'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Platform: $_platformVersion',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text('Status: $_status'),
],
),
),
),
const SizedBox(height: 16),
// Available API Keys
/* Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Available API Keys:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
if (_availableApiKeys.isEmpty)
const Text('No API keys available')
else
..._availableApiKeys.entries.map((entry) =>
Text('• ${entry.key}: ${entry.value}')),
],
),
),
),
const SizedBox(height: 16),
// Available Services
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Available Services:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
if (_availableServices.isEmpty)
const Text('No services available')
else
..._availableServices.entries.map((entry) =>
Text('• ${entry.key}: ${entry.value}')),
],
),
),
),
const SizedBox(height: 16),*/
// Base URL Configuration
/* Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.settings, size: 20),
const SizedBox(width: 8),
const Text(
'Base URL Configuration:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
const SizedBox(height: 8),
Text(
'Source: ${_financeSdkPlugin.getBaseUrlSource()}',
style: TextStyle(
color: _financeSdkPlugin.getBaseUrlSource() == 'Local Override'
? Colors.orange
: Colors.grey[600],
fontSize: 12,
),
),
const SizedBox(height: 12),
TextField(
controller: _baseUrlController,
decoration: InputDecoration(
labelText: 'Base URL Override (Optional)',
hintText: 'https://httpbin.org',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
),
prefixIcon: const Icon(Icons.link_rounded),
suffixIcon: (_baseUrlController.text.isNotEmpty)
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() => _baseUrlController.clear());
},
)
: null,
helperText: 'Leave empty to use Remote Config value',
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _setBaseUrlOverride,
icon: const Icon(Icons.save_rounded),
label: const Text('Set Override'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () {
_baseUrlController.clear();
_financeSdkPlugin.clearBaseUrlOverride();
setState(() {
_status = 'Base URL override cleared';
});
},
icon: const Icon(Icons.refresh_rounded),
label: const Text('Clear'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
*/
// Bearer Token Configuration
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.lock_outline, size: 20),
const SizedBox(width: 8),
const Text(
'Bearer Token Configuration:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
const SizedBox(height: 8),
Text(
'Status: ${_bearerTokenController.text.isNotEmpty ? 'Token Set' : 'No Token'}',
style: TextStyle(
color: _bearerTokenController.text.isNotEmpty
? Colors.green
: Colors.grey[600],
fontSize: 12,
),
),
const SizedBox(height: 12),
TextField(
controller: _bearerTokenController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Bearer Token (Optional)',
hintText: 'Enter your bearer token',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
),
prefixIcon: const Icon(Icons.vpn_key),
suffixIcon: (_bearerTokenController.text.isNotEmpty)
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() => _bearerTokenController.clear());
},
)
: null,
helperText: 'Token will be sent as "Authorization: Bearer <token>" in all requests',
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _setBearerToken,
icon: const Icon(Icons.save_rounded),
label: const Text('Set Token'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () {
_bearerTokenController.clear();
_financeSdkPlugin.clearBearerToken();
setState(() {
_status = 'Bearer token cleared';
});
},
icon: const Icon(Icons.refresh_rounded),
label: const Text('Clear'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// Test Buttons
/*Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'🧪 Test Requests:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _isRunningTests ? null : _runTestGet,
icon: const Icon(Icons.get_app),
label: const Text('Test GET'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _isRunningTests ? null : _runTestPost,
icon: const Icon(Icons.send),
label: const Text('Test POST'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isRunningTests ? null : _runAllTests,
icon: const Icon(Icons.play_arrow),
label: const Text('Run All Tests'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
if (_isRunningTests) ...[
const SizedBox(height: 8),
const Center(
child: CircularProgressIndicator(),
),
],
],
),
),
),
const SizedBox(height: 16),*/
// Request Form
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Send API Request:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 16),
// API Key selector + manual entry
if (_availableApiKeys.isNotEmpty) ...[
DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: 'Choose API Key',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.list_alt_rounded),
),
isExpanded: true,
items: _availableApiKeys.values
.map((k) => DropdownMenuItem<String>(
value: k,
child: Text(k),
))
.toList(),
onChanged: (val) {
if (val != null) _apiKeyController.text = val;
},
),
const SizedBox(height: 12),
],
/*TextField(
controller: _apiKeyController,
decoration: InputDecoration(
labelText: 'API Key',
hintText: 'e.g., GET_USER_DETAIL',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
),
prefixIcon: const Icon(Icons.vpn_key_rounded),
suffixIcon: (_apiKeyController.text.isNotEmpty)
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() => _apiKeyController.clear());
},
)
: null,
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),*/
// Body type toggle
Row(
children: [
const Icon(Icons.description_outlined, size: 18),
const SizedBox(width: 8),
const Text('Body as JSON'),
const Spacer(),
Switch(
value: _isJsonBody,
onChanged: (v) => setState(() => _isJsonBody = v),
),
],
),
const SizedBox(height: 8),
TextField(
controller: _requestBodyController,
decoration: InputDecoration(
labelText: _isJsonBody
? 'Request Body (JSON)'
: 'Request Body (key=value&key2=value2)',
hintText: _isJsonBody
? '{"userId":"123","name":"John"}'
: 'userId=123&name=John',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
),
),
minLines: 3,
maxLines: 8,
),
const SizedBox(height: 16),
Row(children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _sendRequest,
icon: const Icon(Icons.send_rounded),
label: const Text('Send Request'),
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 14)),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: _refreshData,
icon: const Icon(Icons.refresh_rounded),
label: const Text('Refresh Data'),
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 14)),
),
),
]),
],
),
),
),
const SizedBox(height: 16),
// Response
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Response:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
Row(
children: [
ElevatedButton.icon(
onPressed: _copyResponse,
icon: const Icon(Icons.copy),
label: const Text('Copy'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _prettifyResponse,
icon: const Icon(Icons.format_align_left),
label: const Text('Pretty'),
),
],
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
_response.isEmpty ? 'No response yet' : _response,
style: const TextStyle(fontFamily: 'monospace'),
),
),
],
),
),
),
],
),
),
),
);
}
}