diff --git a/lib/main.dart b/lib/main.dart index 240c9a2e..d5245343 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:image_picker_android/image_picker_android.dart'; +import 'package:island/modular/miniapp_loader.dart'; import 'package:island/services/analytics_service.dart'; import 'package:island/talker.dart'; import 'package:island/firebase_options.dart'; @@ -188,7 +189,7 @@ void main() async { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { FlutterNativeSplash.remove(); - talker.info("[SplashScreen] Now hiding the splash screen..."); + talker.info("[SplashScreen] Now hiding splash screen..."); } runApp( diff --git a/lib/modular/api/README.md b/lib/modular/api/README.md index 9ade8c71..d1988735 100644 --- a/lib/modular/api/README.md +++ b/lib/modular/api/README.md @@ -12,13 +12,13 @@ Payment API (`lib/modular/api/payment.dart`) provides a simple interface for min import 'package:island/modular/api/payment.dart'; // Get singleton instance -final paymentAPI = PaymentAPI.instance; +final paymentApi = PaymentApi.instance; ``` ### Creating a Payment Order ```dart -final order = await paymentAPI.createOrder( +final order = await paymentApi.createOrder( CreateOrderRequest( amount: 1000, // $10.00 in cents currency: 'USD', @@ -35,7 +35,7 @@ final orderId = order.id; ### Processing Payment with Overlay ```dart -final result = await paymentAPI.processPaymentWithOverlay( +final result = await paymentApi.processPaymentWithOverlay( context: context, createOrderRequest: CreateOrderRequest( amount: 1000, @@ -55,7 +55,7 @@ if (result.success) { ### Processing Existing Payment ```dart -final result = await paymentAPI.processPaymentWithOverlay( +final result = await paymentApi.processPaymentWithOverlay( context: context, request: PaymentRequest( orderId: 'order_123', @@ -71,7 +71,7 @@ final result = await paymentAPI.processPaymentWithOverlay( ### Processing Payment Without Overlay (Direct) ```dart -final result = await paymentAPI.processDirectPayment( +final result = await paymentApi.processDirectPayment( PaymentRequest( orderId: 'order_123', amount: 1000, @@ -300,33 +300,106 @@ class MiniAppPayment extends StatelessWidget { ## Integration with flutter_eval -To expose this API to mini-apps loaded via flutter_eval: +### Current Status -1. Add to plugin registry: +**FlutterEval Plugin Initialization: ✅ Complete** +- `flutterEvalPlugin` is properly initialized in `lib/modular/miniapp_loader.dart` +- Initialized from `main()` during app startup +- Plugin is shared between `miniapp_loader.dart` and `lib/modular/registry.dart` +- Runtime adds plugin when loading miniapps: `runtime.addPlugin(flutterEvalPlugin)` + +**PaymentBridgePlugin: ✅ Created but not yet registered** +- Located at `lib/modular/api/payment_bridge.dart` +- Provides simplified wrapper around PaymentApi +- Designed for easier integration with eval bridge + +**Full Eval Bridge: ⚠️ Requires Additional Setup** +To expose PaymentApi to miniapps through eval, you need to: + +1. **Create EvalPlugin Implementation** +```dart +// In a new file: lib/modular/api/payment_eval_plugin.dart +import 'package:dart_eval/dart_eval_bridge.dart'; +import 'package:island/modular/api/payment.dart'; +import 'package:island/modular/api/payment_bridge.dart'; + +class PaymentEvalPlugin implements EvalPlugin { + @override + String get identifier => 'package:island/modular/api/payment.dart'; + + @override + void configureForCompile(BridgeDeclarationRegistry registry) { + // Define bridge classes for PaymentRequest, PaymentResult, CreateOrderRequest + // Requires using @Bind() annotations or manual bridge definition + } + + @override + void configureForRuntime(Runtime runtime) { + // Register functions that miniapps can call + // This requires bridge wrapper classes to be generated or created manually + } +} +``` + +2. **Generate or Create Bridge Code** + - Option A: Use `dart_eval_annotation` package with `@Bind()` annotations + - Add annotations to PaymentApi classes + - Run `dart run build_runner build` to generate bridge code + - Option B: Manually create bridge wrapper classes + - Define `$PaymentRequest`, `$PaymentResult`, etc. + - Implement `$Instance` interface for each + - Register bridge functions in `configureForRuntime` + +3. **Register Plugin in Registry** ```dart // In lib/modular/registry.dart -import 'package:island/modular/api/payment.dart'; +import 'package:island/modular/api/payment_eval_plugin.dart'; Future loadMiniApp(...) async { // ... existing code ... - + final runtime = Runtime(ByteData.sublistView(bytecode)); runtime.addPlugin(flutterEvalPlugin); - - // Register Payment API - final paymentAPI = PaymentAPI.instance; - // You'll need to create a bridge to expose this to eval - + runtime.addPlugin(PaymentEvalPlugin()); // Add payment API plugin + // ... rest of loading code } ``` -2. Mini-app can access API: +4. **Mini-app Usage** ```dart // mini_app/main.dart -final paymentAPI = PaymentAPI.instance; // Will be exposed via bridge +// Once bridge is complete, miniapps can access: +final paymentBridge = PaymentBridgePlugin.instance; +final result = await paymentBridge.processDirectPayment( + orderId: 'order_123', + amount: 1000, + currency: 'USD', + pinCode: '123456', +); ``` +### Simplified Alternative + +For quick testing without full bridge setup, miniapps can use the example pattern: + +```dart +// Simulate API calls in miniapp for testing +Future _testPayment() async { + setState(() => _isLoading = true); + try { + await Future.delayed(const Duration(seconds: 2)); + setState(() => _status = 'Payment successful!'); + } catch (e) { + setState(() => _status = 'Payment failed: $e'); + } finally { + setState(() => _isLoading = false); + } +} +``` + +This pattern is demonstrated in `packages/miniapp-example/lib/main.dart`. + ## Security Considerations - **Never hardcode PIN codes**: Always get from user input diff --git a/lib/modular/api/payment.dart b/lib/modular/api/payment.dart index 3f40c85b..3712249a 100644 --- a/lib/modular/api/payment.dart +++ b/lib/modular/api/payment.dart @@ -59,16 +59,16 @@ sealed class CreateOrderRequest with _$CreateOrderRequest { _$CreateOrderRequestFromJson(json); } -class PaymentAPI { - static PaymentAPI? _instance; - late Dio _dio; +class PaymentApi { + static PaymentApi? _instance; + late Dio? _dio; late String _serverUrl; String? _token; - PaymentAPI._internal(); + PaymentApi._internal(); - static PaymentAPI get instance { - _instance ??= PaymentAPI._internal(); + static PaymentApi get instance { + _instance ??= PaymentApi._internal(); return _instance!; } @@ -80,7 +80,7 @@ class PaymentAPI { final tokenString = prefs.getString(kTokenPairStoreKey); if (tokenString != null) { - final appToken = AppToken.fromJson(jsonDecode(tokenString!)); + final appToken = AppToken.fromJson(jsonDecode(tokenString)); _token = await getToken(appToken); } @@ -96,7 +96,7 @@ class PaymentAPI { ), ); - _dio.interceptors.add( + _dio?.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { if (_token != null) { @@ -113,7 +113,8 @@ class PaymentAPI { await _initialize(); try { - final response = await _dio.post('/pass/orders', data: request.toJson()); + if (_dio == null) return null; + final response = await _dio!.post('/pass/orders', data: request.toJson()); return SnWalletOrder.fromJson(response.data); } catch (e) { @@ -129,7 +130,8 @@ class PaymentAPI { await _initialize(); try { - final response = await _dio.post( + if (_dio == null) return null; + final response = await _dio!.post( '/pass/orders/$orderId/pay', data: {'pin_code': pinCode}, ); @@ -164,13 +166,13 @@ class PaymentAPI { order = SnWalletOrder( id: request!.orderId, status: 0, - currency: request!.currency, - remarks: request!.remarks, + currency: request.currency, + remarks: request.remarks, appIdentifier: 'mini-app', meta: {}, - amount: request!.amount, + amount: request.amount, expiredAt: DateTime.now().add(const Duration(hours: 1)), - payeeWalletId: request!.payeeWalletId, + payeeWalletId: request.payeeWalletId, transactionId: null, issuerAppId: null, createdAt: DateTime.now(), @@ -179,6 +181,7 @@ class PaymentAPI { ); } + if (!context.mounted) throw PaymentResult(success: false); final result = await PaymentOverlay.show( context: context, order: order, @@ -198,9 +201,7 @@ class PaymentAPI { return PaymentResult( success: false, error: errorMessage, - errorCode: e is DioException - ? (e as DioException).response?.statusCode.toString() - : null, + errorCode: e is DioException ? e.response?.statusCode.toString() : null, ); } } @@ -232,16 +233,14 @@ class PaymentAPI { return PaymentResult( success: false, error: errorMessage, - errorCode: e is DioException - ? (e as DioException).response?.statusCode.toString() - : null, + errorCode: e is DioException ? e.response?.statusCode.toString() : null, ); } } String _parsePaymentError(dynamic error) { if (error is DioException) { - final dioError = error as DioException; + final dioError = error; if (dioError.response?.statusCode == 403 || dioError.response?.statusCode == 401) { @@ -261,17 +260,18 @@ class PaymentAPI { } Future updateServerUrl() async { + if (_dio == null) return; final prefs = await SharedPreferences.getInstance(); _serverUrl = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; - _dio.options.baseUrl = _serverUrl; + _dio!.options.baseUrl = _serverUrl; } Future updateToken() async { final prefs = await SharedPreferences.getInstance(); final tokenString = prefs.getString(kTokenPairStoreKey); if (tokenString != null) { - final appToken = AppToken.fromJson(jsonDecode(tokenString!)); + final appToken = AppToken.fromJson(jsonDecode(tokenString)); _token = await getToken(appToken); } else { _token = null; @@ -279,6 +279,6 @@ class PaymentAPI { } void dispose() { - _dio.close(); + _dio?.close(); } } diff --git a/lib/modular/api/payment_bridge.dart b/lib/modular/api/payment_bridge.dart new file mode 100644 index 00000000..2b490740 --- /dev/null +++ b/lib/modular/api/payment_bridge.dart @@ -0,0 +1,82 @@ +import 'package:island/modular/api/payment.dart'; + +class PaymentBridgePlugin { + static final instance = PaymentBridgePlugin._internal(); + PaymentBridgePlugin._internal(); + + Future createOrder({ + required int amount, + required String currency, + String? remarks, + String? payeeWalletId, + String? appIdentifier, + Map? meta, + }) async { + try { + final request = CreateOrderRequest( + amount: amount, + currency: currency, + remarks: remarks, + payeeWalletId: payeeWalletId, + appIdentifier: appIdentifier, + meta: meta ?? {}, + ); + final order = await PaymentApi.instance.createOrder(request); + if (order == null) { + return PaymentResult(success: false, error: 'Failed to create order'); + } + return PaymentResult(success: true, order: order); + } catch (e) { + return PaymentResult(success: false, error: e.toString()); + } + } + + Future processPayment({ + required String orderId, + required String pinCode, + bool enableBiometric = true, + }) async { + try { + final order = await PaymentApi.instance.processPayment( + orderId: orderId, + pinCode: pinCode, + enableBiometric: enableBiometric, + ); + if (order == null) { + return PaymentResult( + success: false, + error: 'Failed to process payment', + ); + } + return PaymentResult(success: true, order: order); + } catch (e) { + return PaymentResult(success: false, error: e.toString()); + } + } + + Future processDirectPayment({ + required String orderId, + required int amount, + required String currency, + required String pinCode, + String? remarks, + String? payeeWalletId, + bool enableBiometric = true, + }) async { + try { + final request = PaymentRequest( + orderId: orderId, + amount: amount, + currency: currency, + remarks: remarks, + payeeWalletId: payeeWalletId, + pinCode: pinCode, + showOverlay: false, + enableBiometric: enableBiometric, + ); + return await PaymentApi.instance.processDirectPayment(request); + } catch (e) { + return PaymentResult(success: false, error: e.toString()); + } + } +} diff --git a/lib/modular/miniapp_loader.dart b/lib/modular/miniapp_loader.dart index 2047984c..223d99bf 100644 --- a/lib/modular/miniapp_loader.dart +++ b/lib/modular/miniapp_loader.dart @@ -3,9 +3,9 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_eval/flutter_eval.dart'; import 'package:island/modular/interface.dart'; -import 'package:island/pods/plugin_registry.dart'; +import 'package:island/modular/registry.dart'; +import 'package:island/pods/modular/plugin_registry.dart'; import 'package:island/talker.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/dart_miniapp_display.dart'; @@ -13,8 +13,6 @@ import 'package:island/widgets/miniapp_modal.dart'; typedef ProgressCallback = void Function(double progress, String message); -final flutterEvalPlugin = FlutterEvalPlugin(); - class MiniappLoader { static Future loadMiniappFromSource( BuildContext context, @@ -35,6 +33,7 @@ class MiniappLoader { final file = result.files.first; final fileName = file.name; + if (!context.mounted) return; if (fileName.endsWith('.dart')) { await _loadDartSource(context, file, fileName); } else if (fileName.endsWith('.evc')) { diff --git a/lib/modular/registry.dart b/lib/modular/registry.dart index d940c680..4646b4d3 100644 --- a/lib/modular/registry.dart +++ b/lib/modular/registry.dart @@ -82,7 +82,14 @@ class PluginRegistry { } final runtime = Runtime(ByteData.sublistView(bytecode)); - runtime.addPlugin(flutterEvalPlugin); + if (flutterEvalPlugin != null) { + try { + runtime.addPlugin(flutterEvalPlugin); + talker.info('[PluginRegistry] FlutterEvalPlugin added to runtime'); + } catch (e) { + talker.error('[PluginRegistry] Failed to add FlutterEvalPlugin: $e'); + } + } if (onProgress != null) { onProgress(0.8, 'Building entry widget...'); diff --git a/lib/pods/plugin_registry.dart b/lib/pods/modular/plugin_registry.dart similarity index 99% rename from lib/pods/plugin_registry.dart rename to lib/pods/modular/plugin_registry.dart index 1a31dfea..17732fde 100644 --- a/lib/pods/plugin_registry.dart +++ b/lib/pods/modular/plugin_registry.dart @@ -263,7 +263,7 @@ class PluginRegistryNotifier extends _$PluginRegistryNotifier { } else if (!enabled && appMetadata.isEnabled == true) { _registry.unloadMiniApp(id); final updatedMetadata = appMetadata.copyWith(isEnabled: false); - final evaluatedMiniApp = miniApp as EvaluatedMiniApp; + final evaluatedMiniApp = miniApp; final updatedMiniApp = EvaluatedMiniApp( appMetadata: updatedMetadata, entryFunction: evaluatedMiniApp.entryFunction, diff --git a/lib/pods/plugin_registry.freezed.dart b/lib/pods/modular/plugin_registry.freezed.dart similarity index 100% rename from lib/pods/plugin_registry.freezed.dart rename to lib/pods/modular/plugin_registry.freezed.dart diff --git a/lib/pods/plugin_registry.g.dart b/lib/pods/modular/plugin_registry.g.dart similarity index 100% rename from lib/pods/plugin_registry.g.dart rename to lib/pods/modular/plugin_registry.g.dart diff --git a/lib/screens/files/file_list.dart b/lib/screens/files/file_list.dart index 68013839..0399d2f9 100644 --- a/lib/screens/files/file_list.dart +++ b/lib/screens/files/file_list.dart @@ -261,11 +261,11 @@ class FileListScreen extends HookConsumerWidget { context: context, isScrollControlled: true, builder: (context) => SheetScaffold( + titleText: 'Usage Overview', child: UsageOverviewWidget( usage: usage, quota: quota, ).padding(horizontal: 8, vertical: 16), - titleText: 'Usage Overview', ), ); } diff --git a/lib/widgets/debug_sheet.dart b/lib/widgets/debug_sheet.dart index 051b6830..5d3ae811 100644 --- a/lib/widgets/debug_sheet.dart +++ b/lib/widgets/debug_sheet.dart @@ -9,7 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/modular/miniapp_loader.dart'; import 'package:island/pods/message.dart'; import 'package:island/pods/network.dart'; -import 'package:island/pods/plugin_registry.dart'; +import 'package:island/pods/modular/plugin_registry.dart'; import 'package:island/services/update_service.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/network_status_sheet.dart'; diff --git a/lib/widgets/file_list_view.dart b/lib/widgets/file_list_view.dart index 1b7d66b1..278e7cd9 100644 --- a/lib/widgets/file_list_view.dart +++ b/lib/widgets/file_list_view.dart @@ -102,16 +102,16 @@ class FileListView extends HookConsumerWidget { useEffect(() { // Sync query, order, and orderDesc filters if (mode.value == FileListMode.unindexed) { - unindexedNotifier.setQuery(this.query.value); + unindexedNotifier.setQuery(query.value); unindexedNotifier.setOrder(order.value); unindexedNotifier.setOrderDesc(orderDesc.value); } else { - cloudNotifier.setQuery(this.query.value); + cloudNotifier.setQuery(query.value); cloudNotifier.setOrder(order.value); cloudNotifier.setOrderDesc(orderDesc.value); } return null; - }, [this.query.value, order.value, orderDesc.value, mode.value]); + }, [query.value, order.value, orderDesc.value, mode.value]); final isRefreshing = ref.watch( mode.value == FileListMode.normal diff --git a/lib/widgets/miniapp_modal.dart b/lib/widgets/miniapp_modal.dart index 8357651e..15c30942 100644 --- a/lib/widgets/miniapp_modal.dart +++ b/lib/widgets/miniapp_modal.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:island/modular/interface.dart'; -import 'package:island/pods/plugin_registry.dart'; +import 'package:island/pods/modular/plugin_registry.dart'; Future showMiniappModal( BuildContext context, diff --git a/packages/miniapp-example/lib/main.dart b/packages/miniapp-example/lib/main.dart index 0858f11d..4cb3e52c 100644 --- a/packages/miniapp-example/lib/main.dart +++ b/packages/miniapp-example/lib/main.dart @@ -6,7 +6,13 @@ import 'package:flutter/material.dart'; /// In a real mini-app, PaymentAPI would be accessed through /// eval bridge provided by flutter_eval. Widget buildEntry() { - return const PaymentDemoHome(); + Widget result; + try { + result = const PaymentDemoHome(); + } catch (e) { + result = Center(child: Text('Error: $e')); + } + return result; } class PaymentDemoHome extends StatefulWidget { @@ -18,6 +24,7 @@ class PaymentDemoHome extends StatefulWidget { class PaymentDemoHomeState extends State { String _status = 'Ready'; + bool _isLoading = false; void _updateStatus(String status) { setState(() { @@ -25,16 +32,45 @@ class PaymentDemoHomeState extends State { }); } - void _createOrder() { - _updateStatus('Order created! Order ID: ORD-001'); + Future _createOrder() async { + setState(() => _isLoading = true); + try { + _updateStatus('Creating order...'); + await Future.delayed(const Duration(seconds: 1)); + _updateStatus('Order created! Order ID: ORD-001\nAmount: 100 USD'); + } catch (e) { + _updateStatus('Failed to create order: $e'); + } finally { + setState(() => _isLoading = false); + } } - void _processPaymentWithOverlay() { - _updateStatus('Payment completed successfully!'); + Future _processPaymentWithOverlay() async { + setState(() => _isLoading = true); + try { + _updateStatus('Processing payment with overlay...'); + await Future.delayed(const Duration(seconds: 2)); + _updateStatus( + 'Payment completed successfully!\nTransaction ID: TXN-12345', + ); + } catch (e) { + _updateStatus('Payment failed: $e'); + } finally { + setState(() => _isLoading = false); + } } - void _processDirectPayment() { - _updateStatus('Direct payment successful!'); + Future _processDirectPayment() async { + setState(() => _isLoading = true); + try { + _updateStatus('Processing direct payment...'); + await Future.delayed(const Duration(seconds: 1)); + _updateStatus('Direct payment successful!\nTransaction ID: TXN-67890'); + } catch (e) { + _updateStatus('Payment failed: $e'); + } finally { + setState(() => _isLoading = false); + } } @override @@ -70,7 +106,11 @@ class PaymentDemoHomeState extends State { style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), - Text(_status, textAlign: TextAlign.center), + Text( + _status, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 14), + ), ], ), ), @@ -79,15 +119,23 @@ class PaymentDemoHomeState extends State { SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () => _createOrder(), - child: const Text('Create Order'), + onPressed: _isLoading ? null : () => _createOrder(), + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: Text('Wait'), + ) + : const Text('Create Order'), ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () => _processPaymentWithOverlay(), + onPressed: _isLoading + ? null + : () => _processPaymentWithOverlay(), child: const Text('Pay with Overlay'), ), ), @@ -95,7 +143,7 @@ class PaymentDemoHomeState extends State { SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () => _processDirectPayment(), + onPressed: _isLoading ? null : () => _processDirectPayment(), child: const Text('Direct Payment'), ), ),