diff --git a/mobile-app/assets/copy_icon.svg b/mobile-app/assets/copy_icon.svg
deleted file mode 100644
index 0a5caf533..000000000
--- a/mobile-app/assets/copy_icon.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/mobile-app/assets/lock_icon.svg b/mobile-app/assets/lock_icon.svg
deleted file mode 100644
index 73e51aed1..000000000
--- a/mobile-app/assets/lock_icon.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/mobile-app/lib/shared/utils/amount_input_logic.dart b/mobile-app/lib/shared/utils/amount_input_logic.dart
new file mode 100644
index 000000000..db0d6d4c3
--- /dev/null
+++ b/mobile-app/lib/shared/utils/amount_input_logic.dart
@@ -0,0 +1,88 @@
+import 'package:quantus_sdk/quantus_sdk.dart';
+import 'package:resonance_network_wallet/models/fiat_currency.dart';
+import 'package:resonance_network_wallet/services/exchange_rate_service.dart';
+
+class ToggledInputResult {
+ final String text;
+ final BigInt amount;
+
+ ToggledInputResult({required this.text, required this.amount});
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is ToggledInputResult && runtimeType == other.runtimeType && text == other.text && amount == other.amount;
+
+ @override
+ int get hashCode => text.hashCode ^ amount.hashCode;
+}
+
+class AmountInputLogic {
+ final ExchangeRateService exchangeRateService;
+ final FiatCurrency selectedFiat;
+ final LocaleNumberConfig localeConfig;
+ final NumberFormattingService formattingService;
+
+ AmountInputLogic({
+ required this.exchangeRateService,
+ required this.selectedFiat,
+ required this.localeConfig,
+ required this.formattingService,
+ });
+
+ /// Converts a raw QUAN [BigInt] to a fiat input string using the current
+ /// exchange rate and selected fiat currency, formatted for the user's locale.
+ String quanToFiatString(BigInt quanAmount) {
+ if (quanAmount == BigInt.zero) return '';
+ final fiatValue = exchangeRateService.quanRawToFiat(quanAmount, selectedFiat, AppConstants.decimals);
+ final canonical = fiatValue.toStringAsFixed(selectedFiat.decimals);
+ return localeConfig.localize(canonical, addGroupingSeparators: false);
+ }
+
+ /// Parses a locale-formatted fiat input string and returns the equivalent
+ /// raw QUAN [BigInt] scaled by [AppConstants.decimals].
+ ///
+ /// Throws [InvalidNumberInputException] when [fiatText] cannot be parsed.
+ BigInt fiatStringToQuan(String fiatText) {
+ if (fiatText.isEmpty) return BigInt.zero;
+ final fiatDecimal = localeConfig.parseDecimal(fiatText);
+ return exchangeRateService.fiatToQuanRaw(fiatDecimal, selectedFiat, AppConstants.decimals);
+ }
+
+ /// Parses a QUAN amount string.
+ BigInt parseQuanAmount(String text) {
+ if (text.isEmpty) return BigInt.zero;
+ return formattingService.parseAmount(text) ?? BigInt.zero;
+ }
+
+ /// Formats a QUAN amount for display in an input field.
+ String formatQuanAmount(BigInt amount) {
+ if (amount == BigInt.zero) return '';
+ return formattingService.formatBalance(amount, maxDecimals: AppConstants.decimals, addThousandsSeparators: false);
+ }
+
+ /// Returns the new input string and amount when toggling between QUAN and Fiat.
+ ToggledInputResult getToggledInput({required bool wasFlipped, required BigInt currentAmount}) {
+ if (wasFlipped) {
+ // Fiat -> QUAN: The user was looking at a fiat amount.
+ // We already have currentAmount which was calculated from that fiat amount.
+ final text = formatQuanAmount(currentAmount);
+ return ToggledInputResult(text: text, amount: currentAmount);
+ } else {
+ // QUAN -> Fiat: re-parse amount from the rounded fiat string so
+ // the displayed value and amount stay in sync.
+ final text = quanToFiatString(currentAmount);
+ final newAmount = currentAmount == BigInt.zero ? BigInt.zero : fiatStringToQuan(text);
+ return ToggledInputResult(text: text, amount: newAmount);
+ }
+ }
+
+ /// Handles amount change and returns the updated BigInt amount.
+ BigInt onAmountChanged({required String value, required bool isFlipped}) {
+ if (isFlipped) {
+ return fiatStringToQuan(value);
+ } else {
+ return parseQuanAmount(value);
+ }
+ }
+}
diff --git a/mobile-app/lib/v2/components/quantus_qr.dart b/mobile-app/lib/v2/components/quantus_qr.dart
new file mode 100644
index 000000000..65c96b87a
--- /dev/null
+++ b/mobile-app/lib/v2/components/quantus_qr.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+import 'package:qr_flutter/qr_flutter.dart';
+import 'package:resonance_network_wallet/v2/theme/app_colors.dart';
+
+class QuantusQr extends StatelessWidget {
+ final String accountId;
+
+ const QuantusQr({super.key, required this.accountId});
+
+ @override
+ Widget build(BuildContext context) {
+ final qrSize = 267.0;
+ final qrLogoSize = 64.0;
+
+ return Container(
+ decoration: BoxDecoration(
+ border: Border.all(color: context.colors.textTertiary, width: 1),
+ borderRadius: BorderRadius.circular(16),
+ ),
+ width: qrSize,
+ height: qrSize,
+ child: QrImageView(
+ data: accountId,
+ errorCorrectionLevel: QrErrorCorrectLevel.M,
+ embeddedImage: const AssetImage('assets/v2/uppercase_q_black_bg.png'),
+ embeddedImageStyle: QrEmbeddedImageStyle(size: Size(qrLogoSize, qrLogoSize)),
+ version: QrVersions.auto,
+ size: qrSize,
+ padding: const EdgeInsets.all(16),
+ eyeStyle: const QrEyeStyle(eyeShape: QrEyeShape.square, color: Colors.white),
+ dataModuleStyle: const QrDataModuleStyle(dataModuleShape: QrDataModuleShape.square, color: Colors.white),
+ ),
+ );
+ }
+}
diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart
index 05ba37a28..5f2cd01aa 100644
--- a/mobile-app/lib/v2/screens/home/home_screen.dart
+++ b/mobile-app/lib/v2/screens/home/home_screen.dart
@@ -88,14 +88,12 @@ class _HomeScreenState extends ConsumerState {
showSharedAddressActionSheet(context, shared);
});
- final isPosMode = ref.watch(posModeProvider);
- final balanceAsync = ref.watch(balanceProvider);
final accountAsync = ref.watch(activeAccountProvider);
final txAsync = ref.watch(activeAccountTransactionsProvider(TransactionFilter.all));
final colors = context.colors;
final text = context.themeText;
- Widget screen = accountAsync.when(
+ return accountAsync.when(
loading: () => const ScaffoldBase(mainContent: Center(child: Loader())),
error: (e, _) => ScaffoldBase(
mainContent: Center(
@@ -111,46 +109,12 @@ class _HomeScreenState extends ConsumerState {
slivers: [
_buildContent(active, colors, text),
ActivitySection(txAsync: txAsync, activeAccount: active.account, onRetry: _refresh),
- SizedBox(height: isPosMode ? 120 : 58),
+ const SizedBox(height: 58),
],
- bottomContent: balanceAsync
- .whenData(
- (balance) => balance == BigInt.zero
- ? ScaffoldBaseBottomContent(
- child: QuantusButton.simple(
- label: 'Get Testnet Tokens ↗',
- onTap: () => launchXPost(AppConstants.faucetUrl),
- ),
- )
- : null,
- )
- .value,
+ bottomContent: _buildBottomContent(),
);
},
);
-
- if (!isPosMode) return screen;
-
- return Stack(
- children: [
- screen,
- Positioned(
- left: 24,
- right: 24,
- bottom: MediaQuery.of(context).padding.bottom + 24,
- child: Material(color: Colors.transparent, child: _buildPosButton(colors, text)),
- ),
- ],
- );
- }
-
- Widget _buildPosButton(AppColorsV2 colors, AppTextTheme text) {
- return QuantusButton.simple(
- label: 'New Charge',
- variant: ButtonVariant.success,
- onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PosAmountScreen())),
- textStyle: text.smallTitle?.copyWith(fontWeight: FontWeight.w700, fontSize: 20, decoration: TextDecoration.none),
- );
}
Widget _buildContent(DisplayAccount active, AppColorsV2 colors, AppTextTheme text) {
@@ -173,6 +137,33 @@ class _HomeScreenState extends ConsumerState {
);
}
+ Widget? _buildBottomContent() {
+ final enablePos = ref.watch(posModeProvider);
+ final balanceAsync = ref.watch(balanceProvider);
+
+ if (enablePos) {
+ return ScaffoldBaseBottomContent(
+ child: QuantusButton.simple(
+ label: 'Charge',
+ onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PosAmountScreen())),
+ ),
+ );
+ }
+
+ return balanceAsync
+ .whenData(
+ (balance) => balance == BigInt.zero
+ ? ScaffoldBaseBottomContent(
+ child: QuantusButton.simple(
+ label: 'Get Testnet Tokens ↗',
+ onTap: () => launchXPost(AppConstants.faucetUrl),
+ ),
+ )
+ : null,
+ )
+ .value;
+ }
+
Widget _buildTopBar() {
final isBalanceHidden = ref.watch(isBalanceHiddenProvider);
@@ -247,43 +238,37 @@ class _HomeScreenState extends ConsumerState {
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SelectRecipientScreen())),
);
- if (!enableSwap) {
- return Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- SizedBox(width: 151, child: receiveCard),
- const SizedBox(width: 20),
- SizedBox(width: 151, child: sendCard),
- ],
- );
+ final swapCard = _actionCard(
+ iconAsset: 'assets/v2/action_swap.svg',
+ label: 'Swap',
+ onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SwapScreen())),
+ );
+
+ final List children = [];
+
+ children.add(receiveCard);
+ children.add(const SizedBox(width: 15));
+ children.add(sendCard);
+
+ if (enableSwap) {
+ children.add(const SizedBox(width: 15));
+ children.add(swapCard);
}
- return Row(
- children: [
- Expanded(child: receiveCard),
- const SizedBox(width: 15),
- Expanded(child: sendCard),
- const SizedBox(width: 15),
- Expanded(
- child: _actionCard(
- iconAsset: 'assets/v2/action_swap.svg',
- label: 'Swap',
- onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SwapScreen())),
- ),
- ),
- ],
- );
+ return Row(children: children);
}
Widget _actionCard({required String iconAsset, required String label, required VoidCallback onTap}) {
- return QuantusButton.simple(
- label: label,
- onTap: onTap,
- icon: SvgPicture.asset(iconAsset, width: 24, height: 24),
- iconPlacement: IconPlacement.top,
- padding: const EdgeInsets.all(14),
- variant: ButtonVariant.secondary,
- textStyle: context.themeText.paragraph?.copyWith(color: context.colors.textPrimary.useOpacity(0.8)),
+ return Expanded(
+ child: QuantusButton.simple(
+ label: label,
+ onTap: onTap,
+ icon: SvgPicture.asset(iconAsset, width: 24, height: 24),
+ iconPlacement: IconPlacement.top,
+ padding: const EdgeInsets.all(14),
+ variant: ButtonVariant.secondary,
+ textStyle: context.themeText.paragraph?.copyWith(color: context.colors.textPrimary.useOpacity(0.8)),
+ ),
);
}
}
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 af28d9310..6e519630d 100644
--- a/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart
+++ b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart
@@ -1,9 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
+import 'package:resonance_network_wallet/models/fiat_currency.dart';
+import 'package:resonance_network_wallet/providers/currency_display_provider.dart';
import 'package:resonance_network_wallet/providers/wallet_providers.dart';
+import 'package:resonance_network_wallet/shared/utils/amount_input_logic.dart';
+import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart';
import 'package:resonance_network_wallet/v2/components/quantus_button.dart';
+import 'package:resonance_network_wallet/v2/components/quantus_icon_button.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
+import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.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';
@@ -17,129 +23,155 @@ class PosAmountScreen extends ConsumerStatefulWidget {
}
class _PosAmountScreenState extends ConsumerState {
- String _input = '0';
- LocaleNumberConfig get _localeConfig => ref.read(localeNumberConfigProvider);
+ final _amountController = TextEditingController();
+ final _amountFocus = FocusNode();
+ BigInt _amount = BigInt.zero;
- void _onDigit(String digit) {
- final sep = _localeConfig.decimalSeparator;
- final oldText = _input == '0' && digit != sep ? '' : _input;
- final newText = oldText + digit;
+ AmountInputLogic get _amountInputLogic => AmountInputLogic(
+ exchangeRateService: ref.read(exchangeRateServiceProvider),
+ selectedFiat: ref.read(selectedFiatCurrencyProvider),
+ localeConfig: ref.read(localeNumberConfigProvider),
+ formattingService: ref.read(numberFormattingServiceProvider),
+ );
- final oldValue = TextEditingValue(text: oldText);
- final newValue = TextEditingValue(text: newText);
-
- final filter = DecimalInputFilter(localeConfig: _localeConfig);
- final formatted = filter.formatEditUpdate(oldValue, newValue);
-
- setState(() {
- _input = formatted.text.isEmpty ? '0' : formatted.text;
- });
+ @override
+ void dispose() {
+ _amountController.dispose();
+ _amountFocus.dispose();
+ super.dispose();
}
- void _onBackspace() {
- setState(() {
- if (_input.length <= 1) {
- _input = '0';
- } else {
- _input = _input.substring(0, _input.length - 1);
- }
- });
+ void _onAmountChanged(String _) {
+ final isFlipped = ref.read(isCurrencyFlippedProvider);
+ try {
+ setState(() => _amount = _amountInputLogic.onAmountChanged(value: _amountController.text, isFlipped: isFlipped));
+ } on InvalidNumberInputException catch (e, stack) {
+ debugPrint('Amount parse failed: $e\n$stack');
+ context.showErrorToaster(message: 'Please enter a valid amount');
+ return;
+ }
}
- void _onClear() => setState(() => _input = '0');
-
void _onCharge() {
- final formattingService = ref.watch(numberFormattingServiceProvider);
- final amount = formattingService.parseAmount(_input);
- if (amount == null || amount <= BigInt.zero) return;
- Navigator.push(context, MaterialPageRoute(builder: (_) => PosQrScreen(amount: _input)));
+ if (_amount <= BigInt.zero) return;
+ final quanString = _amountInputLogic.formatQuanAmount(_amount);
+ Navigator.push(context, MaterialPageRoute(builder: (_) => PosQrScreen(amount: quanString)));
}
- bool get _isValid {
- final formattingService = ref.watch(numberFormattingServiceProvider);
- final amount = formattingService.parseAmount(_input);
- return amount != null && amount > BigInt.zero;
+ Future _toggleFlip() async {
+ final wasFlipped = ref.read(isCurrencyFlippedProvider);
+ await ref.read(isCurrencyFlippedProvider.notifier).toggle();
+
+ final result = _amountInputLogic.getToggledInput(wasFlipped: wasFlipped, currentAmount: _amount);
+
+ setState(() {
+ _amountController.text = result.text;
+ _amount = result.amount;
+ });
}
+ bool get _isValid => _amount > BigInt.zero;
+
@override
Widget build(BuildContext context) {
final colors = context.colors;
final text = context.themeText;
+ final primaryAmount = ref
+ .watch(txAmountDisplayProvider)(_amount, withSignPrefix: false, isSend: true)
+ .primaryAmount;
return ScaffoldBase(
appBar: const V2AppBar(title: 'New Charge'),
- mainContent: 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),
- ],
- ),
+ mainContent: _amountCenter(colors, text),
+ bottomContent: _bottomContent(colors, text, primaryAmount),
);
}
- Widget _buildKeypad(AppColorsV2 colors, AppTextTheme text) {
- final decimalSeparator = _localeConfig.decimalSeparator;
- final keys = [
- ['1', '2', '3'],
- ['4', '5', '6'],
- ['7', '8', '9'],
- [decimalSeparator, '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 _amountCenter(AppColorsV2 colors, AppTextTheme text) {
+ final isFlipped = ref.watch(isCurrencyFlippedProvider);
+ final selectedFiat = ref.watch(selectedFiatCurrencyProvider);
+ final display = ref.watch(txAmountDisplayProvider)(
+ _amount,
+ withSignPrefix: false,
+ quanDecimals: 4,
+ isSend: true,
+ withQuanSymbol: false,
);
- }
- 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)),
+ final symbolStyle = text.transactionDetailAmountSymbol?.copyWith(color: colors.textPrimary);
+ final isPrefixFiat = isFlipped && selectedFiat.symbolPosition == SymbolPosition.prefix;
+
+ final maxDecimals = isFlipped ? selectedFiat.decimals : null;
+ final inputField = IntrinsicWidth(
+ child: TextField(
+ controller: _amountController,
+ focusNode: _amountFocus,
+ onChanged: _onAmountChanged,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
+ textAlign: isPrefixFiat ? TextAlign.left : TextAlign.right,
+ inputFormatters: [
+ DecimalInputFilter(localeConfig: ref.read(localeNumberConfigProvider), maxDecimalPlaces: maxDecimals),
+ ],
+ style: text.transactionDetailAmountPrimary?.copyWith(
+ color: _amount == BigInt.zero ? colors.textTertiary : colors.textPrimary,
+ ),
+ decoration: InputDecoration(
+ isDense: true,
+ hintText: '0',
+ hintStyle: text.transactionDetailAmountPrimary?.copyWith(color: colors.textTertiary),
),
),
);
+
+ final symbolWidget = Text(isFlipped ? selectedFiat.symbol : AppConstants.tokenSymbol, style: symbolStyle);
+
+ final List primaryRowChildren = isPrefixFiat
+ ? [symbolWidget, const SizedBox(width: 8), inputField]
+ : [inputField, const SizedBox(width: 8), symbolWidget];
+
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: primaryRowChildren,
+ ),
+ ),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ '≈ ${display.secondaryAmount}',
+ style: text.paragraph?.copyWith(
+ color: colors.textTertiary,
+ fontFamily: AppTextTheme.fontFamilySecondary,
+ ),
+ ),
+ const SizedBox(width: 8),
+ QuantusIconButton.circular(
+ icon: Icons.swap_vert,
+ onTap: _toggleFlip,
+ isActive: display.isFlipped,
+ size: IconButtonSize.small,
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
}
- Widget _buildChargeButton(AppColorsV2 colors, AppTextTheme text) {
- final disabled = !_isValid;
- return QuantusButton.simple(
- label: _isValid ? 'Charge $_input ${AppConstants.tokenSymbol}' : 'Enter Amount',
- onTap: _onCharge,
- isDisabled: disabled,
- textStyle: text.smallTitle?.copyWith(fontWeight: FontWeight.w700),
+ Widget _bottomContent(AppColorsV2 colors, AppTextTheme text, String amountDisplay) {
+ final label = _amount > BigInt.zero ? 'Charge $amountDisplay' : 'Enter Amount';
+
+ return ScaffoldBaseBottomContent(
+ child: QuantusButton.simple(label: label, onTap: _onCharge, isDisabled: !_isValid),
);
}
}
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 8aa367f95..ef3aa7d86 100644
--- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart
+++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart
@@ -2,15 +2,19 @@ import 'dart:async';
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/providers/currency_display_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/services/pending_transaction_polling_service.dart';
import 'package:resonance_network_wallet/services/pos_service.dart';
+import 'package:resonance_network_wallet/shared/utils/open_external_url.dart';
import 'package:resonance_network_wallet/v2/components/loader.dart';
import 'package:resonance_network_wallet/v2/components/quantus_button.dart';
+import 'package:resonance_network_wallet/v2/components/quantus_icon_button.dart';
+import 'package:resonance_network_wallet/v2/components/quantus_qr.dart';
+import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart';
import 'package:resonance_network_wallet/services/tx_watch_service.dart';
import 'package:resonance_network_wallet/v2/screens/pos/pos_amount_screen.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
@@ -34,6 +38,8 @@ class _PosQrScreenState extends ConsumerState {
Timer? _startTimer;
Timer? _timeoutTimer;
TxWatchTransfer? _paidTransfer;
+ DateTime? _paidAt;
+ String? _senderCheckphrase;
bool _watching = false;
String? _watchError;
bool get _isPaid => _paidTransfer != null;
@@ -87,7 +93,13 @@ class _PosQrScreenState extends ConsumerState {
);
ref.read(pendingTransactionsProvider.notifier).add(pendingTx);
ref.read(pendingTransactionPollingServiceProvider).startPolling(pendingTx);
- if (mounted) setState(() => _paidTransfer = tx);
+ if (mounted) {
+ setState(() {
+ _paidTransfer = tx;
+ _paidAt = DateTime.now();
+ });
+ _loadSenderCheckphrase(tx.from);
+ }
},
onError: (e) {
_txWatch.dispose();
@@ -112,6 +124,12 @@ class _PosQrScreenState extends ConsumerState {
});
}
+ Future _loadSenderCheckphrase(String address) async {
+ final checksumService = ref.read(humanReadableChecksumServiceProvider);
+ final checkphrase = await checksumService.getHumanReadableName(address);
+ if (mounted) setState(() => _senderCheckphrase = checkphrase);
+ }
+
@override
void dispose() {
_startTimer?.cancel();
@@ -120,11 +138,33 @@ class _PosQrScreenState extends ConsumerState {
super.dispose();
}
+ void _toggleFlip() {
+ ref.read(isCurrencyFlippedProvider.notifier).toggle();
+ }
+
+ void _done() {
+ Navigator.of(context).popUntil((route) => route.isFirst);
+ }
+
+ void _newCharge() {
+ Navigator.of(context).popUntil((route) => route.isFirst);
+ Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PosAmountScreen()));
+ }
+
+ void _openExplorer() {
+ final txHash = _paidTransfer?.txHash;
+ if (txHash == null) return;
+ openUrl('${AppConstants.explorerEndpoint}/immediate-transactions/$txHash');
+ }
+
@override
Widget build(BuildContext context) {
final colors = context.colors;
final text = context.themeText;
final accountAsync = ref.watch(activeAccountProvider);
+ final formattingService = ref.watch(numberFormattingServiceProvider);
+ final planck = formattingService.parseAmount(widget.amount) ?? BigInt.zero;
+ final display = ref.watch(txAmountDisplayProvider)(planck, withSignPrefix: false, isSend: false, quanDecimals: 4);
return ScaffoldBase(
appBar: V2AppBar(title: _isPaid ? 'Payment Received' : 'Scan to Pay'),
@@ -136,101 +176,235 @@ class _PosQrScreenState extends ConsumerState {
data: (active) {
if (active == null) return const Center(child: Text('No active account'));
_request ??= _posService.createPaymentRequest(accountId: active.account.accountId, amount: widget.amount);
- if (_isPaid) return _buildPaidContent(colors, text);
- return _buildQrContent(_request!, colors, text);
+ if (_isPaid) return _buildPaidContent(colors, text, display.primaryAmount);
+ return _buildQrContent(_request!, colors, text, display);
},
),
+ bottomContent: ScaffoldBaseBottomContent(child: _isPaid ? _buildPaidButtons() : _buildQrButton()),
+ );
+ }
+
+ Widget _buildQrButton() {
+ return QuantusButton.simple(label: 'New Charge', onTap: _newCharge, variant: ButtonVariant.primary);
+ }
+
+ Widget _buildPaidButtons() {
+ final padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 20);
+
+ return Row(
+ spacing: 16,
+ children: [
+ Expanded(
+ child: QuantusButton.simple(padding: padding, label: 'Done', onTap: _done, variant: ButtonVariant.secondary),
+ ),
+ Expanded(
+ child: QuantusButton.simple(
+ padding: padding,
+ label: 'New Charge',
+ onTap: _newCharge,
+ variant: ButtonVariant.primary,
+ ),
+ ),
+ ],
);
}
- Widget _buildPaidContent(AppColorsV2 colors, AppTextTheme text) {
+ Widget _buildPaidContent(AppColorsV2 colors, AppTextTheme text, String amountDisplay) {
+ final transfer = _paidTransfer!;
+ final formattedAddress = AddressFormattingService.formatAddress(transfer.from.trim());
+
return Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- const Spacer(),
- Icon(Icons.check_circle_rounded, color: colors.accentGreen, size: 96),
- const SizedBox(height: 24),
- Text('Paid', style: text.extraLargeTitle?.copyWith(color: colors.accentGreen, fontSize: 48)),
- const SizedBox(height: 16),
+ const SizedBox(height: 40),
+ _buildSuccessCircle(colors),
+ const SizedBox(height: 32),
Text(
- '${widget.amount} ${AppConstants.tokenSymbol}',
- style: text.mediumTitle?.copyWith(color: colors.textSecondary),
+ '$amountDisplay received',
+ style: text.smallTitle?.copyWith(color: colors.textLightGray, fontSize: 32, fontWeight: FontWeight.w400),
+ textAlign: TextAlign.center,
),
+ const SizedBox(height: 4),
+ if (_paidAt != null)
+ Text(
+ _formatPaidAt(_paidAt!),
+ style: text.smallParagraph?.copyWith(color: colors.textTertiary, letterSpacing: 0.7),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 32),
+ _buildFromSection(colors, text, formattedAddress),
const Spacer(),
- QuantusButton.simple(label: 'Done', onTap: _newCharge, variant: ButtonVariant.primary),
- const SizedBox(height: 24),
+ _buildExplorerLink(colors, text),
+ const SizedBox(height: 16),
],
);
}
- void _newCharge() {
- Navigator.of(context).popUntil((route) => route.isFirst);
- Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PosAmountScreen()));
+ Widget _buildSuccessCircle(AppColorsV2 colors) {
+ return Container(
+ width: 78,
+ height: 78,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ border: Border.all(color: colors.success, width: 1.8),
+ ),
+ child: Center(child: Icon(Icons.check, color: colors.success, size: 32)),
+ );
}
- Widget _buildQrContent(PosPaymentRequest request, AppColorsV2 colors, AppTextTheme text) {
+ Widget _buildFromSection(AppColorsV2 colors, AppTextTheme text, String formattedAddress) {
return Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- const Spacer(),
Text(
- '${request.amount} ${AppConstants.tokenSymbol}',
- style: text.extraLargeTitle?.copyWith(color: colors.textPrimary, fontSize: 40),
+ 'From:',
+ style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500),
+ textAlign: TextAlign.center,
),
- const SizedBox(height: 32),
- _buildQrCode(request.paymentUrl, colors),
- const SizedBox(height: 12),
- Text('Ref: ${request.refId}', style: text.detail?.copyWith(color: colors.textTertiary)),
+ const SizedBox(height: 16),
+ if (_senderCheckphrase != null)
+ Text(
+ _senderCheckphrase!,
+ style: text.smallParagraph?.copyWith(color: colors.checksum),
+ textAlign: TextAlign.center,
+ )
+ else
+ Text(
+ '...',
+ style: text.smallParagraph?.copyWith(color: colors.checksum),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 4),
+ Text(
+ formattedAddress.toLowerCase(),
+ style: text.smallParagraph?.copyWith(
+ color: colors.textPrimary,
+ fontFamily: AppTextTheme.fontFamilySecondary,
+ fontWeight: FontWeight.w500,
+ height: 1.35,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ );
+ }
+
+ Widget _buildExplorerLink(AppColorsV2 colors, AppTextTheme text) {
+ return GestureDetector(
+ onTap: _openExplorer,
+ child: Container(
+ decoration: BoxDecoration(
+ border: Border(bottom: BorderSide(color: colors.textTertiary, width: 1)),
+ ),
+ padding: const EdgeInsets.only(bottom: 3),
+ child: Text('View in Explorer ↗', style: text.smallParagraph?.copyWith(color: colors.textTertiary)),
+ ),
+ );
+ }
+
+ Widget _buildQrContent(
+ PosPaymentRequest request,
+ AppColorsV2 colors,
+ AppTextTheme text,
+ CurrencyDisplayState display,
+ ) {
+ return Column(
+ children: [
+ _buildAmountSection(colors, text, display),
+ const SizedBox(height: 16),
+ QuantusQr(accountId: request.paymentUrl),
const Spacer(),
- QuantusButton.simple(label: 'New Charge', onTap: _newCharge, variant: ButtonVariant.secondary),
+ if (!_watching && _watchError != null) _buildErrorSection(colors, text),
+ if (_watching) _buildWaitingPill(colors, text),
const SizedBox(height: 16),
- _buildWaitingButton(colors, text),
- const SizedBox(height: 24),
],
);
}
- Widget _buildWaitingButton(AppColorsV2 colors, AppTextTheme text) {
- if (_watching) {
- return QuantusButton(
- variant: ButtonVariant.primary,
- onTap: () {},
- child: Row(
+ Widget _buildAmountSection(AppColorsV2 colors, AppTextTheme text, CurrencyDisplayState display) {
+ return Column(
+ children: [
+ Text(
+ display.primaryAmount,
+ style: text.totalMinedBlocks?.copyWith(color: colors.textPrimary, letterSpacing: -2.77),
+ ),
+ const SizedBox(height: 8),
+ Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const Loader(),
- const SizedBox(width: 10),
- Text('Waiting for payment', style: text.smallTitle?.copyWith(color: colors.textSecondary, fontSize: 16)),
+ Text(
+ '≈ ${display.secondaryAmount}',
+ style: text.paragraph?.copyWith(color: colors.textTertiary, fontFamily: AppTextTheme.fontFamilySecondary),
+ ),
+ const SizedBox(width: 8),
+ QuantusIconButton.circular(
+ icon: Icons.swap_vert,
+ onTap: _toggleFlip,
+ isActive: display.isFlipped,
+ size: IconButtonSize.small,
+ ),
],
),
- );
- }
+ ],
+ );
+ }
- return Column(
- children: [
- if (_watchError != null) ...[
- Text('Network Error', style: text.detail?.copyWith(color: colors.textError)),
- const SizedBox(height: 8),
- QuantusButton.simple(label: 'Try Again', onTap: _startWatching, variant: ButtonVariant.secondary),
- const SizedBox(height: 12),
+ Widget _buildWaitingPill(AppColorsV2 colors, AppTextTheme text) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 9),
+ decoration: BoxDecoration(
+ color: colors.toasterBackground,
+ border: Border.all(color: colors.toasterBorder),
+ borderRadius: BorderRadius.circular(35),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Loader(size: 14, color: colors.textMuted),
+ const SizedBox(width: 9),
+ Text('Waiting for payment', style: text.detail?.copyWith(color: colors.textMuted)),
],
- QuantusButton.simple(label: 'Done', onTap: _newCharge, variant: ButtonVariant.primary),
- ],
+ ),
);
}
- 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),
+ Widget _buildErrorSection(AppColorsV2 colors, AppTextTheme text) {
+ return Column(
+ children: [
+ Text('Network Error', style: text.detail?.copyWith(color: colors.textError)),
+ const SizedBox(height: 8),
+ QuantusButton.simple(
+ label: 'Try Again',
+ padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 8),
+ onTap: _startWatching,
+ variant: ButtonVariant.secondary,
),
- ),
+ ],
);
}
+
+ String _formatPaidAt(DateTime dt) {
+ final hour = dt.hour > 12 ? dt.hour - 12 : (dt.hour == 0 ? 12 : dt.hour);
+ final minute = dt.minute.toString().padLeft(2, '0');
+ final ampm = dt.hour >= 12 ? 'pm' : 'am';
+ final ordinal = _ordinalSuffix(dt.day);
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ final month = months[dt.month - 1];
+ final year = dt.year.toString().substring(2);
+ return "At $hour:$minute$ampm, ${dt.day}$ordinal $month'$year";
+ }
+
+ String _ordinalSuffix(int day) {
+ if (day >= 11 && day <= 13) return 'th';
+ switch (day % 10) {
+ case 1:
+ return 'st';
+ case 2:
+ return 'nd';
+ case 3:
+ return 'rd';
+ default:
+ return 'th';
+ }
+ }
}
diff --git a/mobile-app/lib/v2/screens/receive/receive_screen.dart b/mobile-app/lib/v2/screens/receive/receive_screen.dart
index 349eb02d3..6403a8d62 100644
--- a/mobile-app/lib/v2/screens/receive/receive_screen.dart
+++ b/mobile-app/lib/v2/screens/receive/receive_screen.dart
@@ -2,11 +2,11 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:qr_flutter/qr_flutter.dart';
import 'package:resonance_network_wallet/providers/wallet_providers.dart';
import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart';
import 'package:resonance_network_wallet/v2/components/address_details_card.dart';
import 'package:resonance_network_wallet/v2/components/loader.dart';
+import 'package:resonance_network_wallet/v2/components/quantus_qr.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart';
import 'package:resonance_network_wallet/v2/components/share_account_button.dart';
import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart';
@@ -148,32 +148,10 @@ class QrCodeTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final qrSize = 267.0;
- final qrLogoSize = 64.0;
-
return Expanded(
child: Column(
children: [
- Container(
- decoration: BoxDecoration(
- border: Border.all(color: context.colors.textTertiary, width: 1),
- borderRadius: BorderRadius.circular(16),
- ),
- width: qrSize,
- height: qrSize,
- child: QrImageView(
- data: accountId,
- errorCorrectionLevel: QrErrorCorrectLevel.M,
- embeddedImage: const AssetImage('assets/v2/uppercase_q_black_bg.png'),
- embeddedImageStyle: QrEmbeddedImageStyle(size: Size(qrLogoSize, qrLogoSize)),
- version: QrVersions.auto,
- size: qrSize,
- padding: const EdgeInsets.all(16),
- eyeStyle: const QrEyeStyle(eyeShape: QrEyeShape.square, color: Colors.white),
- dataModuleStyle: const QrDataModuleStyle(dataModuleShape: QrDataModuleShape.square, color: Colors.white),
- ),
- ),
-
+ QuantusQr(accountId: accountId),
const SizedBox(height: 12),
Text(
checksum,
diff --git a/mobile-app/lib/v2/screens/send/input_amount_screen.dart b/mobile-app/lib/v2/screens/send/input_amount_screen.dart
index 5d648218a..05ca1c8c6 100644
--- a/mobile-app/lib/v2/screens/send/input_amount_screen.dart
+++ b/mobile-app/lib/v2/screens/send/input_amount_screen.dart
@@ -15,6 +15,7 @@ import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_cont
import 'package:resonance_network_wallet/v2/theme/app_colors.dart';
import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart';
import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart';
+import 'package:resonance_network_wallet/shared/utils/amount_input_logic.dart';
import 'package:resonance_network_wallet/shared/utils/debouncer.dart';
import 'package:resonance_network_wallet/v2/components/loader.dart';
import 'package:resonance_network_wallet/v2/components/quantus_icon_button.dart';
@@ -52,7 +53,12 @@ class _InputAmountScreenState extends ConsumerState {
int _blockHeight = 0;
bool _isFetchingFee = true;
- LocaleNumberConfig get _localeConfig => ref.read(localeNumberConfigProvider);
+ AmountInputLogic get _amountInputLogic => AmountInputLogic(
+ exchangeRateService: ref.read(exchangeRateServiceProvider),
+ selectedFiat: ref.read(selectedFiatCurrencyProvider),
+ localeConfig: ref.read(localeNumberConfigProvider),
+ formattingService: ref.read(numberFormattingServiceProvider),
+ );
@override
void initState() {
@@ -61,16 +67,14 @@ class _InputAmountScreenState extends ConsumerState {
_amountFocus.addListener(_onAmountFocusChanged);
if (widget.initialAmount != null) {
final isFlipped = ref.read(isCurrencyFlippedProvider);
- final formattingService = ref.read(numberFormattingServiceProvider);
if (!isFlipped) {
- final parsed = formattingService.parseAmount(widget.initialAmount!);
- _amount = parsed ?? BigInt.zero;
+ _amount = _amountInputLogic.parseQuanAmount(widget.initialAmount!);
_amountController.text = widget.initialAmount!;
} else {
- final parsed = formattingService.parseAmount(widget.initialAmount!);
- if (parsed != null && parsed > BigInt.zero) {
+ final parsed = _amountInputLogic.parseQuanAmount(widget.initialAmount!);
+ if (parsed > BigInt.zero) {
_amount = parsed;
- _amountController.text = _quanToFiatString(parsed);
+ _amountController.text = _amountInputLogic.quanToFiatString(parsed);
}
}
}
@@ -118,19 +122,12 @@ class _InputAmountScreenState extends ConsumerState {
void _onAmountChanged(String _) {
final isFlipped = ref.read(isCurrencyFlippedProvider);
- if (isFlipped) {
- try {
- final convertedAmount = _fiatStringToQuan(_amountController.text);
- setState(() => _amount = convertedAmount);
- } on InvalidNumberInputException catch (e, stack) {
- debugPrint('Fiat→QUAN parse failed: $e\n$stack');
- context.showErrorToaster(message: 'Please enter a valid amount');
- return;
- }
- } else {
- final formattingService = ref.read(numberFormattingServiceProvider);
- final parsed = formattingService.parseAmount(_amountController.text);
- setState(() => _amount = parsed ?? BigInt.zero);
+ try {
+ setState(() => _amount = _amountInputLogic.onAmountChanged(value: _amountController.text, isFlipped: isFlipped));
+ } on InvalidNumberInputException catch (e, stack) {
+ debugPrint('Amount parse failed: $e\n$stack');
+ context.showErrorToaster(message: 'Please enter a valid amount');
+ return;
}
if (_amount > BigInt.zero) _feeDebouncer.run(_fetchFee);
}
@@ -185,34 +182,13 @@ class _InputAmountScreenState extends ConsumerState {
/// Converts a raw QUAN [BigInt] to a fiat input string using the current
/// exchange rate and selected fiat currency, formatted for the user's locale.
- String _quanToFiatString(BigInt quanAmount) {
- final xRate = ref.read(exchangeRateServiceProvider);
- final selectedFiat = ref.read(selectedFiatCurrencyProvider);
- final fiatValue = xRate.quanRawToFiat(quanAmount, selectedFiat, AppConstants.decimals);
- final canonical = fiatValue.toStringAsFixed(selectedFiat.decimals);
- return _localeConfig.localize(canonical, addGroupingSeparators: false);
- }
-
- /// Parses a locale-formatted fiat input string and returns the equivalent
- /// raw QUAN [BigInt] scaled by [AppConstants.decimals].
- ///
- /// Throws [InvalidNumberInputException] when [fiatText] cannot be parsed.
- BigInt _fiatStringToQuan(String fiatText) {
- if (fiatText.isEmpty) return BigInt.zero;
- final fiatDecimal = _localeConfig.parseDecimal(fiatText);
- final xRate = ref.read(exchangeRateServiceProvider);
- final selectedFiat = ref.read(selectedFiatCurrencyProvider);
- return xRate.fiatToQuanRaw(fiatDecimal, selectedFiat, AppConstants.decimals);
- }
-
void _setMax() {
final balance = ref.read(effectiveMaxBalanceProvider).value ?? BigInt.zero;
final max = SendScreenLogic.calculateMaxSendableAmount(balance: balance, networkFee: _networkFee);
final isFlipped = ref.read(isCurrencyFlippedProvider);
- final formattingService = ref.read(numberFormattingServiceProvider);
_amountController.text = isFlipped
- ? _quanToFiatString(max)
- : formattingService.formatBalance(max, maxDecimals: AppConstants.decimals, addThousandsSeparators: false);
+ ? _amountInputLogic.quanToFiatString(max)
+ : _amountInputLogic.formatQuanAmount(max);
setState(() => _amount = max);
if (max > BigInt.zero) _fetchFee();
}
@@ -220,26 +196,13 @@ class _InputAmountScreenState extends ConsumerState {
Future _toggleFlip() async {
final wasFlipped = ref.read(isCurrencyFlippedProvider);
await ref.read(isCurrencyFlippedProvider.notifier).toggle();
- final formattingService = ref.read(numberFormattingServiceProvider);
- // Anchor the amount to the current primary value before flipping.
- // If we were in fiat mode, the 'canonical' amount is the fiat value.
- // If we were in QUAN mode, the 'canonical' amount is the QUAN value.
- if (wasFlipped) {
- // Fiat -> QUAN: The user was looking at a fiat amount.
- // We already have _amount which was calculated from that fiat amount.
- // No change needed to _amount, just update the controller.
- _amountController.text = _amount == BigInt.zero
- ? ''
- : formattingService.formatBalance(_amount, maxDecimals: AppConstants.decimals, addThousandsSeparators: false);
- } else {
- // QUAN -> Fiat: re-parse _amount from the rounded fiat string so
- // the displayed value and _amount stay in sync.
- _amountController.text = _amount == BigInt.zero ? '' : _quanToFiatString(_amount);
- if (_amount != BigInt.zero) {
- _amount = _fiatStringToQuan(_amountController.text);
- }
- }
+ final result = _amountInputLogic.getToggledInput(wasFlipped: wasFlipped, currentAmount: _amount);
+
+ setState(() {
+ _amountController.text = result.text;
+ _amount = result.amount;
+ });
}
Future _openReview() async {
diff --git a/mobile-app/test/unit/amount_input_logic_test.dart b/mobile-app/test/unit/amount_input_logic_test.dart
new file mode 100644
index 000000000..85a04294e
--- /dev/null
+++ b/mobile-app/test/unit/amount_input_logic_test.dart
@@ -0,0 +1,81 @@
+import 'package:decimal/decimal.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:quantus_sdk/quantus_sdk.dart';
+import 'package:resonance_network_wallet/models/fiat_currency.dart';
+import 'package:resonance_network_wallet/services/exchange_rate_service.dart';
+import 'package:resonance_network_wallet/shared/utils/amount_input_logic.dart';
+
+void main() {
+ group('AmountInputLogic', () {
+ late ExchangeRateService exchangeRateService;
+ late LocaleNumberConfig localeConfig;
+ late NumberFormattingService formattingService;
+
+ setUp(() {
+ exchangeRateService = ExchangeRateService(rates: {'USD': Decimal.one});
+ localeConfig = LocaleNumberConfig.dotDecimal;
+ formattingService = NumberFormattingService(localeConfig: localeConfig);
+ });
+
+ AmountInputLogic createLogic({FiatCurrency? selectedFiat}) {
+ return AmountInputLogic(
+ exchangeRateService: exchangeRateService,
+ selectedFiat: selectedFiat ?? FiatCurrency.usd,
+ localeConfig: localeConfig,
+ formattingService: formattingService,
+ );
+ }
+
+ test('quanToFiatString converts correctly', () {
+ final logic = createLogic();
+ final amount = BigInt.from(1000000000000); // 1.0 QUAN
+ expect(logic.quanToFiatString(amount), '1.00');
+ });
+
+ test('fiatStringToQuan parses correctly', () {
+ final logic = createLogic();
+ final result = logic.fiatStringToQuan('1.00');
+ expect(result, BigInt.from(1000000000000));
+ });
+
+ test('getToggledInput handles QUAN -> Fiat toggle', () {
+ final logic = createLogic();
+ final amount = BigInt.from(1500000000000); // 1.5 QUAN
+ final result = logic.getToggledInput(wasFlipped: false, currentAmount: amount);
+
+ expect(result.text, '1.50');
+ expect(result.amount, BigInt.from(1500000000000));
+ });
+
+ test('getToggledInput handles Fiat -> QUAN toggle', () {
+ final logic = createLogic();
+ final amount = BigInt.from(1500000000000); // 1.5 QUAN
+ final result = logic.getToggledInput(wasFlipped: true, currentAmount: amount);
+
+ expect(result.text, '1.5');
+ expect(result.amount, amount);
+ });
+
+ test('onAmountChanged handles QUAN input', () {
+ final logic = createLogic();
+ final result = logic.onAmountChanged(value: '1.5', isFlipped: false);
+ expect(result, BigInt.from(1500000000000));
+ });
+
+ test('onAmountChanged handles Fiat input', () {
+ final logic = createLogic();
+ final result = logic.onAmountChanged(value: '1.50', isFlipped: true);
+ expect(result, BigInt.from(1500000000000));
+ });
+
+ test('quanToFiatString returns empty string for zero', () {
+ final logic = createLogic();
+ expect(logic.quanToFiatString(BigInt.zero), '');
+ });
+
+ test('formatQuanAmount returns empty string for zero', () {
+ final logic = createLogic();
+ expect(logic.formatQuanAmount(BigInt.zero), '');
+ });
+ });
+}
diff --git a/quantus_sdk/lib/src/services/settings_service.dart b/quantus_sdk/lib/src/services/settings_service.dart
index dd8032f0d..684b5c619 100644
--- a/quantus_sdk/lib/src/services/settings_service.dart
+++ b/quantus_sdk/lib/src/services/settings_service.dart
@@ -11,9 +11,7 @@ class SettingsService {
SettingsService._internal();
late SharedPreferences _prefs;
- final _secureStorage = const FlutterSecureStorage(
- mOptions: MacOsOptions(usesDataProtectionKeychain: false),
- );
+ final _secureStorage = const FlutterSecureStorage(mOptions: MacOsOptions(usesDataProtectionKeychain: false));
// New keys for multi-account support
static const String _accountsKey = 'accounts_v5';