From 38a10c0cbf1d9fabfd2ed59146479c20e3f56aac Mon Sep 17 00:00:00 2001 From: NIK Date: Tue, 17 Mar 2026 09:58:52 +0800 Subject: [PATCH 1/7] added pos mode v1 --- .../android/app/src/main/AndroidManifest.xml | 1 + .../shared_address_action_sheet.dart | 4 +- .../lib/providers/route_intent_providers.dart | 10 ++ .../lib/providers/wallet_providers.dart | 16 ++ .../lib/services/deep_link_service.dart | 13 ++ .../lib/v2/screens/home/home_screen.dart | 49 +++++- .../lib/v2/screens/pos/pos_amount_screen.dart | 158 ++++++++++++++++++ .../lib/v2/screens/pos/pos_qr_screen.dart | 108 ++++++++++++ .../lib/v2/screens/send/send_sheet.dart | 10 +- .../v2/screens/settings/settings_screen.dart | 13 +- .../lib/src/services/settings_service.dart | 11 ++ 11 files changed, 385 insertions(+), 8 deletions(-) create mode 100644 mobile-app/lib/v2/screens/pos/pos_amount_screen.dart create mode 100644 mobile-app/lib/v2/screens/pos/pos_qr_screen.dart diff --git a/mobile-app/android/app/src/main/AndroidManifest.xml b/mobile-app/android/app/src/main/AndroidManifest.xml index b64593bf..2d6320d9 100644 --- a/mobile-app/android/app/src/main/AndroidManifest.xml +++ b/mobile-app/android/app/src/main/AndroidManifest.xml @@ -47,6 +47,7 @@ + diff --git a/mobile-app/lib/features/components/shared_address_action_sheet.dart b/mobile-app/lib/features/components/shared_address_action_sheet.dart index 1d16a9db..e40481ee 100644 --- a/mobile-app/lib/features/components/shared_address_action_sheet.dart +++ b/mobile-app/lib/features/components/shared_address_action_sheet.dart @@ -9,6 +9,7 @@ import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; +import 'package:resonance_network_wallet/v2/screens/send/send_sheet.dart'; class SharedAddressActionSheet extends StatefulWidget { final String address; @@ -56,7 +57,8 @@ class _SharedAddressActionSheetState extends State { } void _sendToAddress() { - Navigator.of(context).pushNamed('/send', arguments: widget.address); + Navigator.of(context).pop(); + showSendSheetV2(context, address: widget.address); } void _closeSheet() { diff --git a/mobile-app/lib/providers/route_intent_providers.dart b/mobile-app/lib/providers/route_intent_providers.dart index a0fe38d6..4b211420 100644 --- a/mobile-app/lib/providers/route_intent_providers.dart +++ b/mobile-app/lib/providers/route_intent_providers.dart @@ -3,3 +3,13 @@ import 'package:quantus_sdk/quantus_sdk.dart'; final transactionIntentProvider = StateProvider((_) => null); final sharedAccountIntentProvider = StateProvider((_) => null); + +class PaymentIntent { + final String to; + final String amount; + final String? ref; + + const PaymentIntent({required this.to, required this.amount, this.ref}); +} + +final paymentIntentProvider = StateProvider((_) => null); diff --git a/mobile-app/lib/providers/wallet_providers.dart b/mobile-app/lib/providers/wallet_providers.dart index 4aac4d12..3cc73900 100644 --- a/mobile-app/lib/providers/wallet_providers.dart +++ b/mobile-app/lib/providers/wallet_providers.dart @@ -152,3 +152,19 @@ class IsBalanceHiddenNotifier extends StateNotifier { state = value; } } + +final posModeProvider = StateNotifierProvider((ref) { + final settingsService = ref.watch(settingsServiceProvider); + return PosModeNotifier(settingsService); +}); + +class PosModeNotifier extends StateNotifier { + final SettingsService _settingsService; + + PosModeNotifier(this._settingsService) : super(_settingsService.isPosModeEnabled()); + + Future setPosMode(bool value) async { + await _settingsService.setPosModeEnabled(value); + state = value; + } +} diff --git a/mobile-app/lib/services/deep_link_service.dart b/mobile-app/lib/services/deep_link_service.dart index 0cae95fe..d58a8d8d 100644 --- a/mobile-app/lib/services/deep_link_service.dart +++ b/mobile-app/lib/services/deep_link_service.dart @@ -53,6 +53,19 @@ class DeepLinkService { } } + if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'pay') { + final to = uri.queryParameters['to']; + final amount = uri.queryParameters['amount']; + final ref = uri.queryParameters['ref']; + + if (to != null && to.isNotEmpty && amount != null && amount.isNotEmpty) { + _ref.read(paymentIntentProvider.notifier).state = PaymentIntent(to: to, amount: amount, ref: ref); + navigatorKey.currentState?.pushNamed('/account'); + } else { + print('Missing payment parameters'); + } + } + if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'oauth') { _ref.invalidate(accountAssociationsProvider); } diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 7f89da8c..240f8b40 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -13,6 +13,7 @@ import 'package:resonance_network_wallet/v2/screens/receive/receive_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/send/send_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_screen.dart'; import 'package:resonance_network_wallet/utils/feature_flags.dart'; +import 'package:resonance_network_wallet/v2/screens/pos/pos_amount_screen.dart'; import 'package:resonance_network_wallet/v2/screens/swap/swap_screen.dart'; import 'package:resonance_network_wallet/providers/account_id_list_cache.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; @@ -48,6 +49,15 @@ class _HomeScreenState extends ConsumerState { } void _processIntentIfAvailable() { + final payment = ref.read(paymentIntentProvider); + if (payment != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(paymentIntentProvider.notifier).state = null; + showSendSheetV2(context, address: payment.to, amount: payment.amount); + }); + return; + } + final shared = ref.read(sharedAccountIntentProvider); if (shared != null) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -67,13 +77,14 @@ class _HomeScreenState extends ConsumerState { _processIntentIfAvailable(); final isBalanceHidden = ref.watch(isBalanceHiddenProvider); + final isPosMode = ref.watch(posModeProvider); final accountAsync = ref.watch(activeAccountProvider); final balanceAsync = ref.watch(balanceProvider); final txAsync = ref.watch(activeAccountTransactionsProvider); final colors = context.colors; final text = context.themeText; - return accountAsync.when( + Widget screen = accountAsync.when( loading: () => ScaffoldBase( child: Center(child: CircularProgressIndicator(color: colors.textPrimary)), ), @@ -91,11 +102,45 @@ class _HomeScreenState extends ConsumerState { slivers: [ _buildContent(active, balanceAsync, isBalanceHidden, colors, text), ActivitySection(txAsync: txAsync, activeAccount: active.account, onRetry: _refresh), - const SizedBox(height: 58), + SizedBox(height: isPosMode ? 120 : 58), ], ); }, ); + + if (!isPosMode) return screen; + + return Stack( + children: [ + screen, + Positioned( + left: 24, + right: 24, + bottom: MediaQuery.of(context).padding.bottom + 24, + child: _buildPosButton(colors, text), + ), + ], + ); + } + + Widget _buildPosButton(AppColorsV2 colors, AppTextTheme text) { + return GestureDetector( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PosAmountScreen())), + child: Container( + height: 64, + decoration: BoxDecoration( + color: colors.accentGreen, + borderRadius: BorderRadius.circular(16), + boxShadow: [BoxShadow(color: colors.accentGreen.withValues(alpha: 0.4), blurRadius: 20, offset: const Offset(0, 4))], + ), + child: Center( + child: Text( + 'New Charge', + style: text.smallTitle?.copyWith(color: Colors.black, fontWeight: FontWeight.w700, fontSize: 20), + ), + ), + ), + ); } Widget _buildContent( diff --git a/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart new file mode 100644 index 00000000..dc8ed05e --- /dev/null +++ b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; +import 'package:resonance_network_wallet/v2/screens/pos/pos_qr_screen.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; + +class PosAmountScreen extends ConsumerStatefulWidget { + const PosAmountScreen({super.key}); + + @override + ConsumerState createState() => _PosAmountScreenState(); +} + +class _PosAmountScreenState extends ConsumerState { + String _input = '0'; + final _fmt = NumberFormattingService(); + + void _onDigit(String digit) { + setState(() { + if (_input == '0' && digit != '.') { + _input = digit; + } else if (digit == '.' && _input.contains('.')) { + return; + } else if (_input.contains('.') && _input.split('.').last.length >= 12) { + return; + } else { + _input += digit; + } + }); + } + + void _onBackspace() { + setState(() { + if (_input.length <= 1) { + _input = '0'; + } else { + _input = _input.substring(0, _input.length - 1); + } + }); + } + + void _onClear() => setState(() => _input = '0'); + + void _onCharge() { + final amount = _fmt.parseAmount(_input); + if (amount == null || amount <= BigInt.zero) return; + Navigator.push( + context, + MaterialPageRoute(builder: (_) => PosQrScreen(amount: _input)), + ); + } + + bool get _isValid { + final amount = _fmt.parseAmount(_input); + return amount != null && amount > BigInt.zero; + } + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + + return ScaffoldBase( + appBar: const V2AppBar(title: 'New Charge'), + child: Column( + children: [ + Expanded( + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + '$_input ${AppConstants.tokenSymbol}', + style: text.extraLargeTitle?.copyWith( + color: colors.textPrimary, + fontSize: 56, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + _buildKeypad(colors, text), + const SizedBox(height: 16), + _buildChargeButton(colors, text), + const SizedBox(height: 24), + ], + ), + ); + } + + Widget _buildKeypad(AppColorsV2 colors, AppTextTheme text) { + const keys = [ + ['1', '2', '3'], + ['4', '5', '6'], + ['7', '8', '9'], + ['.', '0', 'backspace'], + ]; + + return Column( + children: keys.map((row) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: row.map((key) => _buildKey(key, colors, text)).toList(), + ), + ); + }).toList(), + ); + } + + Widget _buildKey(String key, AppColorsV2 colors, AppTextTheme text) { + return Expanded( + child: GestureDetector( + onTap: () => key == 'backspace' ? _onBackspace() : _onDigit(key), + onLongPress: key == 'backspace' ? _onClear : null, + behavior: HitTestBehavior.opaque, + child: Container( + height: 60, + alignment: Alignment.center, + child: key == 'backspace' + ? Icon(Icons.backspace_outlined, color: colors.textPrimary, size: 28) + : Text( + key, + style: text.mediumTitle?.copyWith(color: colors.textPrimary, fontSize: 28), + ), + ), + ), + ); + } + + Widget _buildChargeButton(AppColorsV2 colors, AppTextTheme text) { + final disabled = !_isValid; + return GestureDetector( + onTap: disabled ? null : _onCharge, + child: Container( + width: double.infinity, + height: 58, + decoration: BoxDecoration( + color: disabled ? colors.buttonDisabled : colors.accentGreen, + borderRadius: BorderRadius.circular(14), + ), + child: Center( + child: Text( + _isValid ? 'Charge $_input ${AppConstants.tokenSymbol}' : 'Enter Amount', + style: text.smallTitle?.copyWith( + color: disabled ? colors.textTertiary : Colors.black, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ); + } +} diff --git a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart new file mode 100644 index 00000000..2855585a --- /dev/null +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/v2/components/glass_button.dart'; +import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; + +String _generateRefId() { + final now = DateTime.now().millisecondsSinceEpoch; + return now.toRadixString(36).toUpperCase(); +} + +class PosQrScreen extends ConsumerStatefulWidget { + final String amount; + const PosQrScreen({super.key, required this.amount}); + + @override + ConsumerState createState() => _PosQrScreenState(); +} + +class _PosQrScreenState extends ConsumerState { + late final String _refId; + + @override + void initState() { + super.initState(); + _refId = _generateRefId(); + } + + String _buildPaymentUrl(String accountId) { + final uri = Uri.https('www.quantus.com', '/pay', { + 'to': accountId, + 'amount': widget.amount, + 'ref': _refId, + }); + return uri.toString(); + } + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + final accountAsync = ref.watch(activeAccountProvider); + + return ScaffoldBase( + appBar: const V2AppBar(title: 'Scan to Pay'), + child: accountAsync.when( + loading: () => Center(child: CircularProgressIndicator(color: colors.textPrimary)), + error: (e, _) => Center(child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError))), + data: (active) { + if (active == null) return const Center(child: Text('No active account')); + final paymentUrl = _buildPaymentUrl(active.account.accountId); + return _buildContent(paymentUrl, colors, text); + }, + ), + ); + } + + Widget _buildContent(String paymentUrl, AppColorsV2 colors, AppTextTheme text) { + return Column( + children: [ + const Spacer(), + Text( + '${widget.amount} ${AppConstants.tokenSymbol}', + style: text.extraLargeTitle?.copyWith(color: colors.textPrimary, fontSize: 40), + ), + const SizedBox(height: 32), + _buildQrCode(paymentUrl, colors), + const SizedBox(height: 16), + Text('Ref: $_refId', style: text.detail?.copyWith(color: colors.textTertiary)), + const Spacer(), + GlassButton.simple( + label: 'New Charge', + onTap: () => Navigator.pop(context), + variant: ButtonVariant.secondary, + ), + const SizedBox(height: 16), + GlassButton.simple( + label: 'Done', + onTap: () => Navigator.of(context).popUntil((route) => route.isFirst), + variant: ButtonVariant.primary, + ), + const SizedBox(height: 24), + ], + ); + } + + Widget _buildQrCode(String data, AppColorsV2 colors) { + return Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: QrImageView( + data: data, + version: QrVersions.auto, + size: 280, + padding: const EdgeInsets.all(16), + backgroundColor: Colors.white, + eyeStyle: const QrEyeStyle(eyeShape: QrEyeShape.square, color: Colors.black), + dataModuleStyle: const QrDataModuleStyle(dataModuleShape: QrDataModuleShape.square, color: Colors.black), + ), + ), + ); + } +} diff --git a/mobile-app/lib/v2/screens/send/send_sheet.dart b/mobile-app/lib/v2/screens/send/send_sheet.dart index 0062da7b..fae5ff5e 100644 --- a/mobile-app/lib/v2/screens/send/send_sheet.dart +++ b/mobile-app/lib/v2/screens/send/send_sheet.dart @@ -19,7 +19,8 @@ enum _Step { form, confirm, sending, complete } class SendSheet extends ConsumerStatefulWidget { final String? initialAddress; - const SendSheet({super.key, this.initialAddress}); + final String? initialAmount; + const SendSheet({super.key, this.initialAddress, this.initialAmount}); @override ConsumerState createState() => _SendSheetState(); @@ -48,6 +49,9 @@ class _SendSheetState extends ConsumerState { if (widget.initialAddress != null) { _recipientController.text = widget.initialAddress!; } + if (widget.initialAmount != null) { + _amountController.text = widget.initialAmount!; + } } @override @@ -444,12 +448,12 @@ class _SendSheetState extends ConsumerState { } } -void showSendSheetV2(BuildContext context, {String? address}) { +void showSendSheetV2(BuildContext context, {String? address, String? amount}) { BottomSheetContainer.show( context, builder: (_) => Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: SendSheet(initialAddress: address), + child: SendSheet(initialAddress: address, initialAmount: amount), ), ); } diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index 63e6ea99..516d3c6a 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -13,6 +13,7 @@ import 'package:resonance_network_wallet/providers/account_associations_provider import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/providers/notification_config_provider.dart'; import 'package:resonance_network_wallet/providers/pending_transactions_provider.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/shared/utils/account_utils.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -99,6 +100,7 @@ class _SettingsScreenV2State extends ConsumerState { final colors = context.colors; final text = context.themeText; final notifConfig = ref.watch(notificationConfigProvider); + final posMode = ref.watch(posModeProvider); return ScaffoldBase( appBar: const V2AppBar(title: 'Settings'), @@ -134,8 +136,15 @@ class _SettingsScreenV2State extends ConsumerState { ]), const SizedBox(height: 40), _section('Preferences', colors, text, [ - // _chevronItem('Currency', 'USD (\$)', colors, text, onTap: () {}), - // _divider(colors), + _toggleItem( + 'POS Mode', + posMode ? 'Point of Sale Enabled' : 'Disabled', + posMode, + (v) => ref.read(posModeProvider.notifier).setPosMode(v), + colors, + text, + ), + _divider(colors), _toggleItem( 'Notifications', notifConfig.enabled ? 'Transaction Alerts Enabled' : 'Alerts Disabled', diff --git a/quantus_sdk/lib/src/services/settings_service.dart b/quantus_sdk/lib/src/services/settings_service.dart index ce2495ca..715b84ee 100644 --- a/quantus_sdk/lib/src/services/settings_service.dart +++ b/quantus_sdk/lib/src/services/settings_service.dart @@ -287,6 +287,17 @@ class SettingsService { return _prefs.getBool(_balanceHiddenKey) ?? false; } + // POS Mode Settings + static const String _posModeEnabledKey = 'pos_mode_enabled'; + + Future setPosModeEnabled(bool enabled) async { + await _prefs.setBool(_posModeEnabledKey, enabled); + } + + bool isPosModeEnabled() { + return _prefs.getBool(_posModeEnabledKey) ?? false; + } + // --- Primitive Accessors for General Use --- /// Get a boolean value from SharedPreferences From ca884f17ec0529254ccfac2c3082f5449f9166de Mon Sep 17 00:00:00 2001 From: NIK Date: Tue, 17 Mar 2026 10:02:42 +0800 Subject: [PATCH 2/7] add pos service --- mobile-app/lib/services/pos_service.dart | 40 +++++++++++++++++++ .../lib/v2/screens/pos/pos_qr_screen.dart | 40 ++++++------------- 2 files changed, 53 insertions(+), 27 deletions(-) create mode 100644 mobile-app/lib/services/pos_service.dart diff --git a/mobile-app/lib/services/pos_service.dart b/mobile-app/lib/services/pos_service.dart new file mode 100644 index 00000000..8f7284bc --- /dev/null +++ b/mobile-app/lib/services/pos_service.dart @@ -0,0 +1,40 @@ +class PosPaymentRequest { + final String paymentUrl; + final String refId; + final String amount; + + const PosPaymentRequest({ + required this.paymentUrl, + required this.refId, + required this.amount, + }); +} + +class PosService { + String generateRefId() { + final now = DateTime.now().millisecondsSinceEpoch; + return now.toRadixString(36).toUpperCase(); + } + + String buildPaymentUrl({ + required String accountId, + required String amount, + required String refId, + }) { + final uri = Uri.https('www.quantus.com', '/pay', { + 'to': accountId, + 'amount': amount, + 'ref': refId, + }); + return uri.toString(); + } + + PosPaymentRequest createPaymentRequest({ + required String accountId, + required String amount, + }) { + final refId = generateRefId(); + final url = buildPaymentUrl(accountId: accountId, amount: amount, refId: refId); + return PosPaymentRequest(paymentUrl: url, refId: refId, amount: amount); + } +} diff --git a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart index 2855585a..16888aba 100644 --- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -3,17 +3,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/services/pos_service.dart'; import 'package:resonance_network_wallet/v2/components/glass_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -String _generateRefId() { - final now = DateTime.now().millisecondsSinceEpoch; - return now.toRadixString(36).toUpperCase(); -} - class PosQrScreen extends ConsumerStatefulWidget { final String amount; const PosQrScreen({super.key, required this.amount}); @@ -23,22 +19,8 @@ class PosQrScreen extends ConsumerStatefulWidget { } class _PosQrScreenState extends ConsumerState { - late final String _refId; - - @override - void initState() { - super.initState(); - _refId = _generateRefId(); - } - - String _buildPaymentUrl(String accountId) { - final uri = Uri.https('www.quantus.com', '/pay', { - 'to': accountId, - 'amount': widget.amount, - 'ref': _refId, - }); - return uri.toString(); - } + final _posService = PosService(); + PosPaymentRequest? _request; @override Widget build(BuildContext context) { @@ -53,25 +35,29 @@ class _PosQrScreenState extends ConsumerState { error: (e, _) => Center(child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError))), data: (active) { if (active == null) return const Center(child: Text('No active account')); - final paymentUrl = _buildPaymentUrl(active.account.accountId); - return _buildContent(paymentUrl, colors, text); + _request ??= _posService.createPaymentRequest( + accountId: active.account.accountId, + amount: widget.amount, + ); + debugPrint('POS Payment URL: ${_request!.paymentUrl}'); + return _buildContent(_request!, colors, text); }, ), ); } - Widget _buildContent(String paymentUrl, AppColorsV2 colors, AppTextTheme text) { + Widget _buildContent(PosPaymentRequest request, AppColorsV2 colors, AppTextTheme text) { return Column( children: [ const Spacer(), Text( - '${widget.amount} ${AppConstants.tokenSymbol}', + '${request.amount} ${AppConstants.tokenSymbol}', style: text.extraLargeTitle?.copyWith(color: colors.textPrimary, fontSize: 40), ), const SizedBox(height: 32), - _buildQrCode(paymentUrl, colors), + _buildQrCode(request.paymentUrl, colors), const SizedBox(height: 16), - Text('Ref: $_refId', style: text.detail?.copyWith(color: colors.textTertiary)), + Text('Ref: ${request.refId}', style: text.detail?.copyWith(color: colors.textTertiary)), const Spacer(), GlassButton.simple( label: 'New Charge', From 2a222a4b498a0f02e5af1a30184d2926231ef7ea Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Mar 2026 11:45:42 +0800 Subject: [PATCH 3/7] format --- mobile-app/lib/services/pos_service.dart | 23 ++++--------------- .../lib/v2/screens/home/home_screen.dart | 4 +++- .../lib/v2/screens/pos/pos_amount_screen.dart | 10 ++------ .../lib/v2/screens/pos/pos_qr_screen.dart | 15 ++++-------- 4 files changed, 14 insertions(+), 38 deletions(-) diff --git a/mobile-app/lib/services/pos_service.dart b/mobile-app/lib/services/pos_service.dart index 8f7284bc..b005c5e2 100644 --- a/mobile-app/lib/services/pos_service.dart +++ b/mobile-app/lib/services/pos_service.dart @@ -3,11 +3,7 @@ class PosPaymentRequest { final String refId; final String amount; - const PosPaymentRequest({ - required this.paymentUrl, - required this.refId, - required this.amount, - }); + const PosPaymentRequest({required this.paymentUrl, required this.refId, required this.amount}); } class PosService { @@ -16,23 +12,12 @@ class PosService { return now.toRadixString(36).toUpperCase(); } - String buildPaymentUrl({ - required String accountId, - required String amount, - required String refId, - }) { - final uri = Uri.https('www.quantus.com', '/pay', { - 'to': accountId, - 'amount': amount, - 'ref': refId, - }); + String buildPaymentUrl({required String accountId, required String amount, required String refId}) { + final uri = Uri.https('www.quantus.com', '/pay', {'to': accountId, 'amount': amount, 'ref': refId}); return uri.toString(); } - PosPaymentRequest createPaymentRequest({ - required String accountId, - required String amount, - }) { + PosPaymentRequest createPaymentRequest({required String accountId, required String amount}) { final refId = generateRefId(); final url = buildPaymentUrl(accountId: accountId, amount: amount, refId: refId); return PosPaymentRequest(paymentUrl: url, refId: refId, amount: amount); diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 240f8b40..3ae4d648 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -131,7 +131,9 @@ class _HomeScreenState extends ConsumerState { decoration: BoxDecoration( color: colors.accentGreen, borderRadius: BorderRadius.circular(16), - boxShadow: [BoxShadow(color: colors.accentGreen.withValues(alpha: 0.4), blurRadius: 20, offset: const Offset(0, 4))], + boxShadow: [ + BoxShadow(color: colors.accentGreen.withValues(alpha: 0.4), blurRadius: 20, offset: const Offset(0, 4)), + ], ), child: Center( child: Text( diff --git a/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart index dc8ed05e..6f1843bc 100644 --- a/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart @@ -47,10 +47,7 @@ class _PosAmountScreenState extends ConsumerState { void _onCharge() { final amount = _fmt.parseAmount(_input); if (amount == null || amount <= BigInt.zero) return; - Navigator.push( - context, - MaterialPageRoute(builder: (_) => PosQrScreen(amount: _input)), - ); + Navigator.push(context, MaterialPageRoute(builder: (_) => PosQrScreen(amount: _input))); } bool get _isValid { @@ -123,10 +120,7 @@ class _PosAmountScreenState extends ConsumerState { alignment: Alignment.center, child: key == 'backspace' ? Icon(Icons.backspace_outlined, color: colors.textPrimary, size: 28) - : Text( - key, - style: text.mediumTitle?.copyWith(color: colors.textPrimary, fontSize: 28), - ), + : Text(key, style: text.mediumTitle?.copyWith(color: colors.textPrimary, fontSize: 28)), ), ), ); diff --git a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart index 16888aba..fd1d9fe7 100644 --- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -32,13 +32,12 @@ class _PosQrScreenState extends ConsumerState { appBar: const V2AppBar(title: 'Scan to Pay'), child: accountAsync.when( loading: () => Center(child: CircularProgressIndicator(color: colors.textPrimary)), - error: (e, _) => Center(child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError))), + error: (e, _) => Center( + child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError)), + ), data: (active) { if (active == null) return const Center(child: Text('No active account')); - _request ??= _posService.createPaymentRequest( - accountId: active.account.accountId, - amount: widget.amount, - ); + _request ??= _posService.createPaymentRequest(accountId: active.account.accountId, amount: widget.amount); debugPrint('POS Payment URL: ${_request!.paymentUrl}'); return _buildContent(_request!, colors, text); }, @@ -59,11 +58,7 @@ class _PosQrScreenState extends ConsumerState { const SizedBox(height: 16), Text('Ref: ${request.refId}', style: text.detail?.copyWith(color: colors.textTertiary)), const Spacer(), - GlassButton.simple( - label: 'New Charge', - onTap: () => Navigator.pop(context), - variant: ButtonVariant.secondary, - ), + GlassButton.simple(label: 'New Charge', onTap: () => Navigator.pop(context), variant: ButtonVariant.secondary), const SizedBox(height: 16), GlassButton.simple( label: 'Done', From e5fda153d28e90a6821ae83de3944b8dffb6208d Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Mar 2026 12:05:22 +0800 Subject: [PATCH 4/7] fix yellow underline --- mobile-app/lib/v2/screens/home/home_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 3ae4d648..2516699c 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -138,7 +138,7 @@ class _HomeScreenState extends ConsumerState { child: Center( child: Text( 'New Charge', - style: text.smallTitle?.copyWith(color: Colors.black, fontWeight: FontWeight.w700, fontSize: 20), + style: text.smallTitle?.copyWith(color: Colors.black, fontWeight: FontWeight.w700, fontSize: 20, decoration: TextDecoration.none), ), ), ), From 25bc0184a23455c9eeaab4d26fe67b4b89be82aa Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Mar 2026 12:11:11 +0800 Subject: [PATCH 5/7] Payment mode says 'Pay' --- mobile-app/lib/v2/screens/home/home_screen.dart | 2 +- mobile-app/lib/v2/screens/send/send_sheet.dart | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 2516699c..3fb783e4 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -53,7 +53,7 @@ class _HomeScreenState extends ConsumerState { if (payment != null) { WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(paymentIntentProvider.notifier).state = null; - showSendSheetV2(context, address: payment.to, amount: payment.amount); + showSendSheetV2(context, address: payment.to, amount: payment.amount, isPayMode: true); }); return; } diff --git a/mobile-app/lib/v2/screens/send/send_sheet.dart b/mobile-app/lib/v2/screens/send/send_sheet.dart index fae5ff5e..f37dce90 100644 --- a/mobile-app/lib/v2/screens/send/send_sheet.dart +++ b/mobile-app/lib/v2/screens/send/send_sheet.dart @@ -20,7 +20,8 @@ enum _Step { form, confirm, sending, complete } class SendSheet extends ConsumerStatefulWidget { final String? initialAddress; final String? initialAmount; - const SendSheet({super.key, this.initialAddress, this.initialAmount}); + final bool isPayMode; + const SendSheet({super.key, this.initialAddress, this.initialAmount, this.isPayMode = false}); @override ConsumerState createState() => _SendSheetState(); @@ -173,7 +174,7 @@ class _SendSheetState extends ConsumerState { final balance = ref.watch(effectiveMaxBalanceProvider); return BottomSheetContainer( - title: 'Send', + title: widget.isPayMode ? 'Pay' : 'Send', onBack: _step == _Step.confirm ? _backToForm : null, child: AnimatedSize( duration: const Duration(milliseconds: 200), @@ -408,7 +409,7 @@ class _SendSheetState extends ConsumerState { const SizedBox(height: 48), CircularProgressIndicator(color: colors.textPrimary), const SizedBox(height: 24), - Text('Sending...', style: text.smallTitle?.copyWith(color: colors.textPrimary)), + Text(widget.isPayMode ? 'Paying...' : 'Sending...', style: text.smallTitle?.copyWith(color: colors.textPrimary)), const SizedBox(height: 80), ], ); @@ -421,7 +422,7 @@ class _SendSheetState extends ConsumerState { const SizedBox(height: 48), const SuccessCheck(size: 64), const SizedBox(height: 24), - Text('Sent!', style: text.smallTitle?.copyWith(color: colors.textPrimary)), + Text(widget.isPayMode ? 'Paid!' : 'Sent!', style: text.smallTitle?.copyWith(color: colors.textPrimary)), const SizedBox(height: 8), Text( '${_fmt.formatBalance(_amount)} ${AppConstants.tokenSymbol}', @@ -448,12 +449,12 @@ class _SendSheetState extends ConsumerState { } } -void showSendSheetV2(BuildContext context, {String? address, String? amount}) { +void showSendSheetV2(BuildContext context, {String? address, String? amount, bool isPayMode = false}) { BottomSheetContainer.show( context, builder: (_) => Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: SendSheet(initialAddress: address, initialAmount: amount), + child: SendSheet(initialAddress: address, initialAmount: amount, isPayMode: isPayMode), ), ); } From 31819b7db70025a5dee64e47f6a5512166bb89fe Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Wed, 18 Mar 2026 12:11:22 +0800 Subject: [PATCH 6/7] format --- mobile-app/lib/v2/screens/home/home_screen.dart | 7 ++++++- mobile-app/lib/v2/screens/send/send_sheet.dart | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 3fb783e4..f949c001 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -138,7 +138,12 @@ class _HomeScreenState extends ConsumerState { child: Center( child: Text( 'New Charge', - style: text.smallTitle?.copyWith(color: Colors.black, fontWeight: FontWeight.w700, fontSize: 20, decoration: TextDecoration.none), + style: text.smallTitle?.copyWith( + color: Colors.black, + fontWeight: FontWeight.w700, + fontSize: 20, + decoration: TextDecoration.none, + ), ), ), ), diff --git a/mobile-app/lib/v2/screens/send/send_sheet.dart b/mobile-app/lib/v2/screens/send/send_sheet.dart index f37dce90..f946eabc 100644 --- a/mobile-app/lib/v2/screens/send/send_sheet.dart +++ b/mobile-app/lib/v2/screens/send/send_sheet.dart @@ -409,7 +409,10 @@ class _SendSheetState extends ConsumerState { const SizedBox(height: 48), CircularProgressIndicator(color: colors.textPrimary), const SizedBox(height: 24), - Text(widget.isPayMode ? 'Paying...' : 'Sending...', style: text.smallTitle?.copyWith(color: colors.textPrimary)), + Text( + widget.isPayMode ? 'Paying...' : 'Sending...', + style: text.smallTitle?.copyWith(color: colors.textPrimary), + ), const SizedBox(height: 80), ], ); From 9cca269f5f3f367203f7c12344819a2f236c4a2e Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 20 Mar 2026 11:46:48 +0800 Subject: [PATCH 7/7] use normal buttons --- .../lib/features/components/button.dart | 17 ++++++- .../lib/v2/screens/home/home_screen.dart | 31 +++-------- .../lib/v2/screens/pos/pos_amount_screen.dart | 51 ++++++++----------- 3 files changed, 44 insertions(+), 55 deletions(-) diff --git a/mobile-app/lib/features/components/button.dart b/mobile-app/lib/features/components/button.dart index 0a6901a4..28842fc3 100644 --- a/mobile-app/lib/features/components/button.dart +++ b/mobile-app/lib/features/components/button.dart @@ -3,8 +3,9 @@ import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -enum ButtonVariant { transparent, neutral, primary, success, danger, glass, glassOutline, dangerOutline } +enum ButtonVariant { transparent, neutral, primary, success, danger, glass, glassOutline, dangerOutline, accent } class Button extends StatelessWidget { final String label; @@ -34,6 +35,8 @@ class Button extends StatelessWidget { switch (variant) { case ButtonVariant.neutral: return context.themeColors.textSecondary; + case ButtonVariant.accent: + return Colors.black; default: return null; } @@ -169,6 +172,18 @@ class Button extends StatelessWidget { ); break; + case ButtonVariant.accent: + buttonWidget = Container( + width: width, + padding: padding, + decoration: ShapeDecoration( + color: disabled ? disabledBtnColor : context.colors.accentGreen, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(buttonRadius)), + ), + child: buttonContent, + ); + break; + default: buttonWidget = ClipRRect( borderRadius: BorderRadius.circular(buttonRadius), diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index f949c001..7e420105 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -4,9 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/account_gradient_image.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; import 'package:resonance_network_wallet/features/components/shared_address_action_sheet.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; -import 'package:resonance_network_wallet/v2/components/glass_button.dart'; +import 'package:resonance_network_wallet/v2/components/glass_button.dart' hide ButtonVariant; import 'package:resonance_network_wallet/v2/components/glass_icon_button.dart'; import 'package:resonance_network_wallet/v2/screens/accounts/accounts_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/receive/receive_sheet.dart'; @@ -124,29 +125,11 @@ class _HomeScreenState extends ConsumerState { } Widget _buildPosButton(AppColorsV2 colors, AppTextTheme text) { - return GestureDetector( - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PosAmountScreen())), - child: Container( - height: 64, - decoration: BoxDecoration( - color: colors.accentGreen, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow(color: colors.accentGreen.withValues(alpha: 0.4), blurRadius: 20, offset: const Offset(0, 4)), - ], - ), - child: Center( - child: Text( - 'New Charge', - style: text.smallTitle?.copyWith( - color: Colors.black, - fontWeight: FontWeight.w700, - fontSize: 20, - decoration: TextDecoration.none, - ), - ), - ), - ), + return Button( + label: 'New Charge', + variant: ButtonVariant.accent, + onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PosAmountScreen())), + textStyle: text.smallTitle?.copyWith(fontWeight: FontWeight.w700, fontSize: 20, decoration: TextDecoration.none), ); } diff --git a/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart index 6f1843bc..79e3a2a1 100644 --- a/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/features/components/button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/screens/pos/pos_qr_screen.dart'; @@ -17,18 +19,19 @@ class PosAmountScreen extends ConsumerStatefulWidget { class _PosAmountScreenState extends ConsumerState { String _input = '0'; final _fmt = NumberFormattingService(); + final _decimalFilter = DecimalInputFilter(); void _onDigit(String digit) { + final oldText = _input == '0' && digit != '.' && digit != ',' ? '' : _input; + final newText = oldText + digit; + + final oldValue = TextEditingValue(text: oldText); + final newValue = TextEditingValue(text: newText); + + final formatted = _decimalFilter.formatEditUpdate(oldValue, newValue); + setState(() { - if (_input == '0' && digit != '.') { - _input = digit; - } else if (digit == '.' && _input.contains('.')) { - return; - } else if (_input.contains('.') && _input.split('.').last.length >= 12) { - return; - } else { - _input += digit; - } + _input = formatted.text.isEmpty ? '0' : formatted.text; }); } @@ -89,11 +92,12 @@ class _PosAmountScreenState extends ConsumerState { } Widget _buildKeypad(AppColorsV2 colors, AppTextTheme text) { - const keys = [ + final decimalSeparator = NumberFormat().symbols.DECIMAL_SEP; + final keys = [ ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], - ['.', '0', 'backspace'], + [decimalSeparator, '0', 'backspace'], ]; return Column( @@ -128,25 +132,12 @@ class _PosAmountScreenState extends ConsumerState { Widget _buildChargeButton(AppColorsV2 colors, AppTextTheme text) { final disabled = !_isValid; - return GestureDetector( - onTap: disabled ? null : _onCharge, - child: Container( - width: double.infinity, - height: 58, - decoration: BoxDecoration( - color: disabled ? colors.buttonDisabled : colors.accentGreen, - borderRadius: BorderRadius.circular(14), - ), - child: Center( - child: Text( - _isValid ? 'Charge $_input ${AppConstants.tokenSymbol}' : 'Enter Amount', - style: text.smallTitle?.copyWith( - color: disabled ? colors.textTertiary : Colors.black, - fontWeight: FontWeight.w700, - ), - ), - ), - ), + return Button( + label: _isValid ? 'Charge $_input ${AppConstants.tokenSymbol}' : 'Enter Amount', + variant: ButtonVariant.accent, + isDisabled: disabled, + onPressed: _onCharge, + textStyle: text.smallTitle?.copyWith(fontWeight: FontWeight.w700), ); } }