From 68e0ce9cafa1849a91219fbdd512d8558a1024f5 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 12:59:55 +0800 Subject: [PATCH 01/25] feat: setup l10n and implement to auth wrapper --- mobile-app/l10n.yaml | 3 + mobile-app/lib/app.dart | 7 + mobile-app/lib/l10n/app_en.arb | 29 ++++ mobile-app/lib/l10n/app_id.arb | 8 + mobile-app/lib/l10n/app_localizations.dart | 156 ++++++++++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 27 +++ mobile-app/lib/l10n/app_localizations_id.dart | 27 +++ mobile-app/lib/providers/l10n_provider.dart | 11 ++ .../lib/v2/screens/auth/auth_wrapper.dart | 9 +- mobile-app/pubspec.lock | 5 + mobile-app/pubspec.yaml | 3 + 11 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 mobile-app/l10n.yaml create mode 100644 mobile-app/lib/l10n/app_en.arb create mode 100644 mobile-app/lib/l10n/app_id.arb create mode 100644 mobile-app/lib/l10n/app_localizations.dart create mode 100644 mobile-app/lib/l10n/app_localizations_en.dart create mode 100644 mobile-app/lib/l10n/app_localizations_id.dart create mode 100644 mobile-app/lib/providers/l10n_provider.dart diff --git a/mobile-app/l10n.yaml b/mobile-app/l10n.yaml new file mode 100644 index 00000000..4e6692e5 --- /dev/null +++ b/mobile-app/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart \ No newline at end of file diff --git a/mobile-app/lib/app.dart b/mobile-app/lib/app.dart index 08ca8f05..26422182 100644 --- a/mobile-app/lib/app.dart +++ b/mobile-app/lib/app.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/wallet_initializer.dart'; import 'package:resonance_network_wallet/v2/screens/auth/auth_wrapper.dart'; import 'package:resonance_network_wallet/v2/theme/app_theme.dart'; @@ -8,6 +9,7 @@ import 'package:resonance_network_wallet/services/notification_integration_servi import 'package:resonance_network_wallet/services/referral_service.dart'; import 'package:resonance_network_wallet/services/telemetry_navigator_observer.dart'; import 'package:resonance_network_wallet/services/deep_link_service.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'dart:io' show Platform; class ResonanceWalletApp extends ConsumerStatefulWidget { @@ -41,8 +43,13 @@ class _ResonanceWalletAppState extends ConsumerState { @override Widget build(BuildContext context) { + final locale = ref.watch(localeProvider); + return MaterialApp( title: 'Quantus Wallet', + locale: locale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, navigatorObservers: [TelemetryNavigatorObserver()], initialRoute: '/', routes: {'/': (context) => const WalletInitializer()}, diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb new file mode 100644 index 00000000..6d4b024c --- /dev/null +++ b/mobile-app/lib/l10n/app_en.arb @@ -0,0 +1,29 @@ +{ + "greeting": "Hello, {name}!", + "@greeting": { + "description": "A welcome message", + "placeholders": { + "name": { + "type": "String" + } + } + }, + + + "authUseDeviceBiometricsToUnlock": "Use device biometrics to unlock", + "@authUseDeviceBiometricsToUnlock": { + "description": "Text for the text on the lock screen when using device biometrics to unlock" + }, + "authAuthenticating": "Authenticating...", + "@authAuthenticating": { + "description": "Text for the text on the lock screen when authenticating" + }, + "authUnlockWallet": "Unlock Wallet", + "@authUnlockWallet": { + "description": "Text for the button on the lock screen to unlock the wallet" + }, + "authAuthorizationRequired": "Authorization \n Required", + "@authAuthorizationRequired": { + "description": "Text for displayed on the lock screen when authorization is required" + } + } \ No newline at end of file diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb new file mode 100644 index 00000000..ef3e3cf9 --- /dev/null +++ b/mobile-app/lib/l10n/app_id.arb @@ -0,0 +1,8 @@ +{ + "greeting": "Halo, {name}!", + + "authUseDeviceBiometricsToUnlock": "Gunakan biometrik untuk membuka perangkat", + "authAuthenticating": "Mengotentikasi...", + "authUnlockWallet": "Buka Wallet", + "authAuthorizationRequired": "Otorisasi \n Diperlukan" +} \ No newline at end of file diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart new file mode 100644 index 00000000..df775f83 --- /dev/null +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -0,0 +1,156 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_id.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [Locale('en'), Locale('id')]; + + /// A welcome message + /// + /// In en, this message translates to: + /// **'Hello, {name}!'** + String greeting(String name); + + /// Text for the text on the lock screen when using device biometrics to unlock + /// + /// In en, this message translates to: + /// **'Use device biometrics to unlock'** + String get authUseDeviceBiometricsToUnlock; + + /// Text for the text on the lock screen when authenticating + /// + /// In en, this message translates to: + /// **'Authenticating...'** + String get authAuthenticating; + + /// Text for the button on the lock screen to unlock the wallet + /// + /// In en, this message translates to: + /// **'Unlock Wallet'** + String get authUnlockWallet; + + /// Text for displayed on the lock screen when authorization is required + /// + /// In en, this message translates to: + /// **'Authorization \n Required'** + String get authAuthorizationRequired; +} + +class _AppLocalizationsDelegate extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => ['en', 'id'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'id': + return AppLocalizationsId(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart new file mode 100644 index 00000000..7d75daf6 --- /dev/null +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -0,0 +1,27 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String greeting(String name) { + return 'Hello, $name!'; + } + + @override + String get authUseDeviceBiometricsToUnlock => 'Use device biometrics to unlock'; + + @override + String get authAuthenticating => 'Authenticating...'; + + @override + String get authUnlockWallet => 'Unlock Wallet'; + + @override + String get authAuthorizationRequired => 'Authorization \n Required'; +} diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart new file mode 100644 index 00000000..e1dabd3c --- /dev/null +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -0,0 +1,27 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Indonesian (`id`). +class AppLocalizationsId extends AppLocalizations { + AppLocalizationsId([String locale = 'id']) : super(locale); + + @override + String greeting(String name) { + return 'Halo, $name!'; + } + + @override + String get authUseDeviceBiometricsToUnlock => 'Gunakan biometrik untuk membuka perangkat'; + + @override + String get authAuthenticating => 'Mengotentikasi...'; + + @override + String get authUnlockWallet => 'Buka Wallet'; + + @override + String get authAuthorizationRequired => 'Otorisasi \n Diperlukan'; +} diff --git a/mobile-app/lib/providers/l10n_provider.dart b/mobile-app/lib/providers/l10n_provider.dart new file mode 100644 index 00000000..dae74db4 --- /dev/null +++ b/mobile-app/lib/providers/l10n_provider.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; // Import your generated file + +final localeProvider = StateProvider((ref) => const Locale('en')); + +final l10nProvider = Provider((ref) { + final locale = ref.watch(localeProvider); + + return lookupAppLocalizations(locale); +}); \ No newline at end of file diff --git a/mobile-app/lib/v2/screens/auth/auth_wrapper.dart b/mobile-app/lib/v2/screens/auth/auth_wrapper.dart index 3530efb0..c9ca2f3c 100644 --- a/mobile-app/lib/v2/screens/auth/auth_wrapper.dart +++ b/mobile-app/lib/v2/screens/auth/auth_wrapper.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/local_auth_provider.dart'; import 'package:resonance_network_wallet/v2/components/base_background.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; @@ -34,6 +35,8 @@ class AuthWrapper extends ConsumerWidget { } Widget _buildLockScreen(BuildContext context, WidgetRef ref, bool isAuthenticating) { + final l10n = ref.watch(l10nProvider); + return Scaffold( backgroundColor: context.colors.background, body: BaseBackground( @@ -45,7 +48,7 @@ class AuthWrapper extends ConsumerWidget { alignment: Alignment.center, children: [ Image.asset('assets/v2/auth_wrapper_bracket.png'), - Text('Authorization \n Required', style: context.themeText.lockTitle, textAlign: TextAlign.center), + Text(l10n.authAuthorizationRequired, style: context.themeText.lockTitle, textAlign: TextAlign.center), ], ), const SizedBox(height: 60), @@ -55,7 +58,7 @@ class AuthWrapper extends ConsumerWidget { Padding( padding: EdgeInsets.symmetric(horizontal: context.themeSize.screenPadding), child: QuantusButton.simple( - label: 'Unlock Wallet', + label: l10n.authUnlockWallet, onTap: () { ref.read(localAuthProvider.notifier).authenticate(); }, @@ -64,7 +67,7 @@ class AuthWrapper extends ConsumerWidget { ), const SizedBox(height: 40), Text( - isAuthenticating ? 'Authenticating...' : 'Use device biometrics to unlock', + isAuthenticating ? l10n.authAuthenticating : l10n.authUseDeviceBiometricsToUnlock, style: context.themeText.smallParagraph?.copyWith(color: context.colors.textSecondary), textAlign: TextAlign.center, ), diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index bbdd006c..94a7724b 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -614,6 +614,11 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_native_splash: dependency: "direct main" description: diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 7dfa7e67..c589cba8 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -10,6 +10,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter url_launcher: ^6.2.1 quantus_sdk: video_player: @@ -72,6 +74,7 @@ dev_dependencies: riverpod_lint: ^2.6.5 flutter: + generate: true uses-material-design: true assets: - .env From 56434fc8b28f424fd05aed266f528eb7a7f9d6f5 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 13:15:15 +0800 Subject: [PATCH 02/25] feat: localize wallet init screen --- mobile-app/lib/l10n/app_en.arb | 20 ++++++++++--------- mobile-app/lib/l10n/app_id.arb | 4 +++- mobile-app/lib/l10n/app_localizations.dart | 18 ++++++++++++++--- mobile-app/lib/l10n/app_localizations_en.dart | 10 +++++++--- mobile-app/lib/l10n/app_localizations_id.dart | 10 +++++++--- mobile-app/lib/wallet_initializer.dart | 13 +++++++++--- 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 6d4b024c..c9529979 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -1,14 +1,16 @@ { - "greeting": "Hello, {name}!", - "@greeting": { - "description": "A welcome message", - "placeholders": { - "name": { - "type": "String" - } - } + "walletInitErrorTitle": "Wallet Error", + "@walletInitErrorTitle": { + "description": "Title for the error dialog when the wallet is not found" + }, + "walletInitErrorMessage": "Unable to find secret phrase. Please restore your wallet.", + "@walletInitErrorMessage": { + "description": "Message for the error dialog when the wallet is not found" + }, + "walletInitErrorButtonLabel": "OK", + "@walletInitErrorButtonLabel": { + "description": "Label for the button on the error dialog when the wallet is not found" }, - "authUseDeviceBiometricsToUnlock": "Use device biometrics to unlock", "@authUseDeviceBiometricsToUnlock": { diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index ef3e3cf9..a0fdfa65 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -1,5 +1,7 @@ { - "greeting": "Halo, {name}!", + "walletInitErrorTitle": "Wallet Eror", + "walletInitErrorMessage": "Gagal mencari secret phrase. Coba pulihkan wallet anda.", + "walletInitErrorButtonLabel": "OK", "authUseDeviceBiometricsToUnlock": "Gunakan biometrik untuk membuka perangkat", "authAuthenticating": "Mengotentikasi...", diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index df775f83..344e8a3d 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -92,11 +92,23 @@ abstract class AppLocalizations { /// A list of this localizations delegate's supported locales. static const List supportedLocales = [Locale('en'), Locale('id')]; - /// A welcome message + /// Title for the error dialog when the wallet is not found /// /// In en, this message translates to: - /// **'Hello, {name}!'** - String greeting(String name); + /// **'Wallet Error'** + String get walletInitErrorTitle; + + /// Message for the error dialog when the wallet is not found + /// + /// In en, this message translates to: + /// **'Unable to find secret phrase. Please restore your wallet.'** + String get walletInitErrorMessage; + + /// Label for the button on the error dialog when the wallet is not found + /// + /// In en, this message translates to: + /// **'OK'** + String get walletInitErrorButtonLabel; /// Text for the text on the lock screen when using device biometrics to unlock /// diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 7d75daf6..d4b4da95 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -9,9 +9,13 @@ class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); @override - String greeting(String name) { - return 'Hello, $name!'; - } + String get walletInitErrorTitle => 'Wallet Error'; + + @override + String get walletInitErrorMessage => 'Unable to find secret phrase. Please restore your wallet.'; + + @override + String get walletInitErrorButtonLabel => 'OK'; @override String get authUseDeviceBiometricsToUnlock => 'Use device biometrics to unlock'; diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index e1dabd3c..18cd7941 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -9,9 +9,13 @@ class AppLocalizationsId extends AppLocalizations { AppLocalizationsId([String locale = 'id']) : super(locale); @override - String greeting(String name) { - return 'Halo, $name!'; - } + String get walletInitErrorTitle => 'Wallet Eror'; + + @override + String get walletInitErrorMessage => 'Gagal mencari secret phrase. Coba pulihkan wallet anda.'; + + @override + String get walletInitErrorButtonLabel => 'OK'; @override String get authUseDeviceBiometricsToUnlock => 'Gunakan biometrik untuk membuka perangkat'; diff --git a/mobile-app/lib/wallet_initializer.dart b/mobile-app/lib/wallet_initializer.dart index 6771011c..b11f4137 100644 --- a/mobile-app/lib/wallet_initializer.dart +++ b/mobile-app/lib/wallet_initializer.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/migration_dialog.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; @@ -89,16 +90,22 @@ class WalletInitializerState extends ConsumerState { } Future _showMnemonicLostDialog() async { + final l10n = ref.watch(l10nProvider); + await BottomSheetContainer.show( context, builder: (ctx) => BottomSheetContainer( - title: 'Wallet Error', + title: l10n.walletInitErrorTitle, child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Unable to find secret phrase. Please restore your wallet.', style: ctx.themeText.smallParagraph), + Text(l10n.walletInitErrorMessage, style: ctx.themeText.smallParagraph), const SizedBox(height: 32), - QuantusButton.simple(label: 'OK', onTap: () => Navigator.pop(ctx), variant: ButtonVariant.secondary), + QuantusButton.simple( + label: l10n.walletInitErrorButtonLabel, + onTap: () => Navigator.pop(ctx), + variant: ButtonVariant.secondary, + ), ], ), ), From f1ea219f93b1308abbc4497dd962e7fbb4837034 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 16:16:25 +0800 Subject: [PATCH 03/25] feat: onboarding flow localized --- mobile-app/lib/l10n/app_en.arb | 117 +++++++++++++- mobile-app/lib/l10n/app_id.arb | 34 +++- mobile-app/lib/l10n/app_localizations.dart | 150 ++++++++++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 82 ++++++++++ mobile-app/lib/l10n/app_localizations_id.dart | 83 ++++++++++ .../v2/components/recovery_phrase_body.dart | 27 ++-- .../accounts/account_ready_screen.dart | 27 ++-- .../new_wallet_recovery_phrase_screen.dart | 15 +- .../screens/create/wallet_ready_screen.dart | 15 +- .../screens/import/import_wallet_screen.dart | 12 +- .../settings/settings_caution_scaffold.dart | 7 +- .../v2/screens/welcome/welcome_screen.dart | 14 +- 12 files changed, 545 insertions(+), 38 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index c9529979..b8092618 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -27,5 +27,120 @@ "authAuthorizationRequired": "Authorization \n Required", "@authAuthorizationRequired": { "description": "Text for displayed on the lock screen when authorization is required" + }, + + "welcomeTagline": "Quantum Secure Encrypted Money", + "@welcomeTagline": { + "description": "Tagline on the welcome screen" + }, + "welcomeCreateNewWallet": "Create New Wallet", + "@welcomeCreateNewWallet": { + "description": "Button to start creating a new wallet on the welcome screen" + }, + "welcomeImportWallet": "Import Wallet", + "@welcomeImportWallet": { + "description": "Button to import an existing wallet on the welcome screen" + }, + + "createWalletAppBarTitle": "Create Wallet", + "@createWalletAppBarTitle": { + "description": "App bar title for the create wallet flow" + }, + "createWalletCautionHeadline": "Keep your Recovery Phrase Secret", + "@createWalletCautionHeadline": { + "description": "Headline on the recovery phrase caution screen during wallet creation" + }, + "createWalletCautionBullet1": "If you lose this device, your recovery phrase is the only way back", + "@createWalletCautionBullet1": { + "description": "First bullet on the recovery phrase caution screen" + }, + "createWalletCautionBullet2": "Anyone who gets hold of it has complete control over your funds, permanently", + "@createWalletCautionBullet2": { + "description": "Second bullet on the recovery phrase caution screen" + }, + "createWalletCautionBullet3": "Write it down and keep it somewhere safe. Do not save it digitally", + "@createWalletCautionBullet3": { + "description": "Third bullet on the recovery phrase caution screen" + }, + "createWalletCautionCheckboxLabel": "I understand that anyone with my recovery phrase can access my wallet. I will store it safely.", + "@createWalletCautionCheckboxLabel": { + "description": "Checkbox label on the recovery phrase caution screen" + }, + "createWalletCautionContinue": "Continue", + "@createWalletCautionContinue": { + "description": "Continue button on the recovery phrase caution screen" + }, + "createWalletRecoveryPhraseNext": "Next", + "@createWalletRecoveryPhraseNext": { + "description": "Primary button on the new wallet recovery phrase screen" + }, + "createWalletRecoveryPhraseFailedGenerate": "Failed to generate: {error}", + "@createWalletRecoveryPhraseFailedGenerate": { + "description": "Error when mnemonic generation fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "createWalletRecoveryPhraseSaveError": "Error saving wallet: {error}", + "@createWalletRecoveryPhraseSaveError": { + "description": "Error when saving a new wallet fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + + "recoveryPhraseBodyInstructions": "Write these words down in order and keep them somewhere only you can access. Do not screenshot or copy to a notes app.", + "@recoveryPhraseBodyInstructions": { + "description": "Instructions above the recovery phrase word grid" + }, + "recoveryPhraseBodyCopy": "Copy", + "@recoveryPhraseBodyCopy": { + "description": "Copy button on the recovery phrase screen" + }, + "recoveryPhraseBodyCopiedMessage": "Recovery phrase copied to clipboard", + "@recoveryPhraseBodyCopiedMessage": { + "description": "Toast when recovery phrase is copied" + }, + + "accountReadyAccountCreated": "Account Created", + "@accountReadyAccountCreated": { + "description": "Title when a new account is created" + }, + "accountReadyWalletCreated": "Wallet Created", + "@accountReadyWalletCreated": { + "description": "Title when a new wallet is created" + }, + "accountReadyWalletImported": "Wallet Imported", + "@accountReadyWalletImported": { + "description": "Title when a wallet is imported" + }, + "accountReadyDone": "Done", + "@accountReadyDone": { + "description": "Done button on the account ready screen" + }, + + "importWalletAppBarTitle": "Import Wallet", + "@importWalletAppBarTitle": { + "description": "App bar title on the import wallet screen" + }, + "importWalletDescription": "Restore an existing wallet with your 12 or 24 words recovery phrase", + "@importWalletDescription": { + "description": "Description on the import wallet screen" + }, + "importWalletHint": "Type in or paste your recovery phrase. Separate words with spaces.", + "@importWalletHint": { + "description": "Hint for the recovery phrase text field" + }, + "importWalletButton": "Import", + "@importWalletButton": { + "description": "Import button on the import wallet screen" + }, + "importWalletValidationError": "Recovery phrase must be 12 or 24 words", + "@importWalletValidationError": { + "description": "Validation error when recovery phrase word count is invalid" } - } \ No newline at end of file + } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index a0fdfa65..310614e0 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -6,5 +6,35 @@ "authUseDeviceBiometricsToUnlock": "Gunakan biometrik untuk membuka perangkat", "authAuthenticating": "Mengotentikasi...", "authUnlockWallet": "Buka Wallet", - "authAuthorizationRequired": "Otorisasi \n Diperlukan" -} \ No newline at end of file + "authAuthorizationRequired": "Otorisasi \n Diperlukan", + + "welcomeTagline": "Uang Terenkripsi Aman Kuantum", + "welcomeCreateNewWallet": "Buat Wallet Baru", + "welcomeImportWallet": "Impor Wallet", + + "createWalletAppBarTitle": "Buat Wallet", + "createWalletCautionHeadline": "Jaga Kerahasiaan Recovery Phrase Anda", + "createWalletCautionBullet1": "Jika Anda kehilangan perangkat ini, recovery phrase adalah satu-satunya cara kembali", + "createWalletCautionBullet2": "Siapa pun yang mendapatkannya akan memiliki kendali penuh atas dana Anda, secara permanen", + "createWalletCautionBullet3": "Tuliskan dan simpan di tempat yang aman. Jangan simpan secara digital", + "createWalletCautionCheckboxLabel": "Saya memahami bahwa siapa pun yang memiliki recovery phrase saya dapat mengakses wallet saya. Saya akan menyimpannya dengan aman.", + "createWalletCautionContinue": "Lanjutkan", + "createWalletRecoveryPhraseNext": "Berikutnya", + "createWalletRecoveryPhraseFailedGenerate": "Gagal membuat: {error}", + "createWalletRecoveryPhraseSaveError": "Gagal menyimpan wallet: {error}", + + "recoveryPhraseBodyInstructions": "Tuliskan kata-kata ini secara berurutan dan simpan di tempat yang hanya Anda yang bisa akses. Jangan screenshot atau salin ke aplikasi catatan.", + "recoveryPhraseBodyCopy": "Salin", + "recoveryPhraseBodyCopiedMessage": "Recovery phrase disalin ke clipboard", + + "accountReadyAccountCreated": "Akun Dibuat", + "accountReadyWalletCreated": "Wallet Dibuat", + "accountReadyWalletImported": "Wallet Diimpor", + "accountReadyDone": "Selesai", + + "importWalletAppBarTitle": "Impor Wallet", + "importWalletDescription": "Pulihkan wallet yang ada dengan recovery phrase 12 atau 24 kata Anda", + "importWalletHint": "Ketik atau tempel recovery phrase Anda. Pisahkan kata dengan spasi.", + "importWalletButton": "Impor", + "importWalletValidationError": "Recovery phrase harus 12 atau 24 kata" +} diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 344e8a3d..dabc4a8a 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -133,6 +133,156 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Authorization \n Required'** String get authAuthorizationRequired; + + /// Tagline on the welcome screen + /// + /// In en, this message translates to: + /// **'Quantum Secure Encrypted Money'** + String get welcomeTagline; + + /// Button to start creating a new wallet on the welcome screen + /// + /// In en, this message translates to: + /// **'Create New Wallet'** + String get welcomeCreateNewWallet; + + /// Button to import an existing wallet on the welcome screen + /// + /// In en, this message translates to: + /// **'Import Wallet'** + String get welcomeImportWallet; + + /// App bar title for the create wallet flow + /// + /// In en, this message translates to: + /// **'Create Wallet'** + String get createWalletAppBarTitle; + + /// Headline on the recovery phrase caution screen during wallet creation + /// + /// In en, this message translates to: + /// **'Keep your Recovery Phrase Secret'** + String get createWalletCautionHeadline; + + /// First bullet on the recovery phrase caution screen + /// + /// In en, this message translates to: + /// **'If you lose this device, your recovery phrase is the only way back'** + String get createWalletCautionBullet1; + + /// Second bullet on the recovery phrase caution screen + /// + /// In en, this message translates to: + /// **'Anyone who gets hold of it has complete control over your funds, permanently'** + String get createWalletCautionBullet2; + + /// Third bullet on the recovery phrase caution screen + /// + /// In en, this message translates to: + /// **'Write it down and keep it somewhere safe. Do not save it digitally'** + String get createWalletCautionBullet3; + + /// Checkbox label on the recovery phrase caution screen + /// + /// In en, this message translates to: + /// **'I understand that anyone with my recovery phrase can access my wallet. I will store it safely.'** + String get createWalletCautionCheckboxLabel; + + /// Continue button on the recovery phrase caution screen + /// + /// In en, this message translates to: + /// **'Continue'** + String get createWalletCautionContinue; + + /// Primary button on the new wallet recovery phrase screen + /// + /// In en, this message translates to: + /// **'Next'** + String get createWalletRecoveryPhraseNext; + + /// Error when mnemonic generation fails + /// + /// In en, this message translates to: + /// **'Failed to generate: {error}'** + String createWalletRecoveryPhraseFailedGenerate(String error); + + /// Error when saving a new wallet fails + /// + /// In en, this message translates to: + /// **'Error saving wallet: {error}'** + String createWalletRecoveryPhraseSaveError(String error); + + /// Instructions above the recovery phrase word grid + /// + /// In en, this message translates to: + /// **'Write these words down in order and keep them somewhere only you can access. Do not screenshot or copy to a notes app.'** + String get recoveryPhraseBodyInstructions; + + /// Copy button on the recovery phrase screen + /// + /// In en, this message translates to: + /// **'Copy'** + String get recoveryPhraseBodyCopy; + + /// Toast when recovery phrase is copied + /// + /// In en, this message translates to: + /// **'Recovery phrase copied to clipboard'** + String get recoveryPhraseBodyCopiedMessage; + + /// Title when a new account is created + /// + /// In en, this message translates to: + /// **'Account Created'** + String get accountReadyAccountCreated; + + /// Title when a new wallet is created + /// + /// In en, this message translates to: + /// **'Wallet Created'** + String get accountReadyWalletCreated; + + /// Title when a wallet is imported + /// + /// In en, this message translates to: + /// **'Wallet Imported'** + String get accountReadyWalletImported; + + /// Done button on the account ready screen + /// + /// In en, this message translates to: + /// **'Done'** + String get accountReadyDone; + + /// App bar title on the import wallet screen + /// + /// In en, this message translates to: + /// **'Import Wallet'** + String get importWalletAppBarTitle; + + /// Description on the import wallet screen + /// + /// In en, this message translates to: + /// **'Restore an existing wallet with your 12 or 24 words recovery phrase'** + String get importWalletDescription; + + /// Hint for the recovery phrase text field + /// + /// In en, this message translates to: + /// **'Type in or paste your recovery phrase. Separate words with spaces.'** + String get importWalletHint; + + /// Import button on the import wallet screen + /// + /// In en, this message translates to: + /// **'Import'** + String get importWalletButton; + + /// Validation error when recovery phrase word count is invalid + /// + /// In en, this message translates to: + /// **'Recovery phrase must be 12 or 24 words'** + String get importWalletValidationError; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index d4b4da95..de2e3b03 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -28,4 +28,86 @@ class AppLocalizationsEn extends AppLocalizations { @override String get authAuthorizationRequired => 'Authorization \n Required'; + + @override + String get welcomeTagline => 'Quantum Secure Encrypted Money'; + + @override + String get welcomeCreateNewWallet => 'Create New Wallet'; + + @override + String get welcomeImportWallet => 'Import Wallet'; + + @override + String get createWalletAppBarTitle => 'Create Wallet'; + + @override + String get createWalletCautionHeadline => 'Keep your Recovery Phrase Secret'; + + @override + String get createWalletCautionBullet1 => 'If you lose this device, your recovery phrase is the only way back'; + + @override + String get createWalletCautionBullet2 => + 'Anyone who gets hold of it has complete control over your funds, permanently'; + + @override + String get createWalletCautionBullet3 => 'Write it down and keep it somewhere safe. Do not save it digitally'; + + @override + String get createWalletCautionCheckboxLabel => + 'I understand that anyone with my recovery phrase can access my wallet. I will store it safely.'; + + @override + String get createWalletCautionContinue => 'Continue'; + + @override + String get createWalletRecoveryPhraseNext => 'Next'; + + @override + String createWalletRecoveryPhraseFailedGenerate(String error) { + return 'Failed to generate: $error'; + } + + @override + String createWalletRecoveryPhraseSaveError(String error) { + return 'Error saving wallet: $error'; + } + + @override + String get recoveryPhraseBodyInstructions => + 'Write these words down in order and keep them somewhere only you can access. Do not screenshot or copy to a notes app.'; + + @override + String get recoveryPhraseBodyCopy => 'Copy'; + + @override + String get recoveryPhraseBodyCopiedMessage => 'Recovery phrase copied to clipboard'; + + @override + String get accountReadyAccountCreated => 'Account Created'; + + @override + String get accountReadyWalletCreated => 'Wallet Created'; + + @override + String get accountReadyWalletImported => 'Wallet Imported'; + + @override + String get accountReadyDone => 'Done'; + + @override + String get importWalletAppBarTitle => 'Import Wallet'; + + @override + String get importWalletDescription => 'Restore an existing wallet with your 12 or 24 words recovery phrase'; + + @override + String get importWalletHint => 'Type in or paste your recovery phrase. Separate words with spaces.'; + + @override + String get importWalletButton => 'Import'; + + @override + String get importWalletValidationError => 'Recovery phrase must be 12 or 24 words'; } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index 18cd7941..0f3d8650 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -28,4 +28,87 @@ class AppLocalizationsId extends AppLocalizations { @override String get authAuthorizationRequired => 'Otorisasi \n Diperlukan'; + + @override + String get welcomeTagline => 'Uang Terenkripsi Aman Kuantum'; + + @override + String get welcomeCreateNewWallet => 'Buat Wallet Baru'; + + @override + String get welcomeImportWallet => 'Impor Wallet'; + + @override + String get createWalletAppBarTitle => 'Buat Wallet'; + + @override + String get createWalletCautionHeadline => 'Jaga Kerahasiaan Recovery Phrase Anda'; + + @override + String get createWalletCautionBullet1 => + 'Jika Anda kehilangan perangkat ini, recovery phrase adalah satu-satunya cara kembali'; + + @override + String get createWalletCautionBullet2 => + 'Siapa pun yang mendapatkannya memiliki kendali penuh atas dana Anda, secara permanen'; + + @override + String get createWalletCautionBullet3 => 'Tuliskan dan simpan di tempat yang aman. Jangan simpan secara digital'; + + @override + String get createWalletCautionCheckboxLabel => + 'Saya memahami bahwa siapa pun yang memiliki recovery phrase saya dapat mengakses wallet saya. Saya akan menyimpannya dengan aman.'; + + @override + String get createWalletCautionContinue => 'Lanjutkan'; + + @override + String get createWalletRecoveryPhraseNext => 'Berikutnya'; + + @override + String createWalletRecoveryPhraseFailedGenerate(String error) { + return 'Gagal membuat: $error'; + } + + @override + String createWalletRecoveryPhraseSaveError(String error) { + return 'Gagal menyimpan wallet: $error'; + } + + @override + String get recoveryPhraseBodyInstructions => + 'Tuliskan kata-kata ini secara berurutan dan simpan di tempat yang hanya Anda yang bisa akses. Jangan screenshot atau salin ke aplikasi catatan.'; + + @override + String get recoveryPhraseBodyCopy => 'Salin'; + + @override + String get recoveryPhraseBodyCopiedMessage => 'Recovery phrase disalin ke clipboard'; + + @override + String get accountReadyAccountCreated => 'Akun Dibuat'; + + @override + String get accountReadyWalletCreated => 'Wallet Dibuat'; + + @override + String get accountReadyWalletImported => 'Wallet Diimpor'; + + @override + String get accountReadyDone => 'Selesai'; + + @override + String get importWalletAppBarTitle => 'Impor Wallet'; + + @override + String get importWalletDescription => 'Pulihkan wallet yang ada dengan recovery phrase 12 atau 24 kata Anda'; + + @override + String get importWalletHint => 'Ketik atau tempel recovery phrase Anda. Pisahkan kata dengan spasi.'; + + @override + String get importWalletButton => 'Impor'; + + @override + String get importWalletValidationError => 'Recovery phrase harus 12 atau 24 kata'; } diff --git a/mobile-app/lib/v2/components/recovery_phrase_body.dart b/mobile-app/lib/v2/components/recovery_phrase_body.dart index 3a6bc4b2..987ac675 100644 --- a/mobile-app/lib/v2/components/recovery_phrase_body.dart +++ b/mobile-app/lib/v2/components/recovery_phrase_body.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/features/components/mnemonic_grid.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -9,7 +11,7 @@ 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'; -class RecoveryPhraseBody extends StatelessWidget { +class RecoveryPhraseBody extends ConsumerWidget { final String appBarTitle; final List words; final String primaryButtonLabel; @@ -29,12 +31,17 @@ class RecoveryPhraseBody extends StatelessWidget { this.isPrimaryButtonLoading = false, }); - void _copyToClipboard(BuildContext context) { - context.copyTextWithToaster(words.join(' '), message: 'Recovery phrase copied to clipboard'); + void _copyToClipboard(BuildContext context, WidgetRef ref) { + final l10n = ref.read(l10nProvider); + context.copyTextWithToaster( + words.join(' '), + message: l10n.recoveryPhraseBodyCopiedMessage, + ); } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; @@ -44,7 +51,7 @@ class RecoveryPhraseBody extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - 'Write these words down in order and keep them somewhere only you can access. Do not screenshot or copy to a notes app.', + l10n.recoveryPhraseBodyInstructions, style: text.smallParagraph?.copyWith(color: colors.textTertiary), ), const SizedBox(height: 24), @@ -55,20 +62,22 @@ class RecoveryPhraseBody extends StatelessWidget { ), ], ), - bottomContent: _bottomBar(context, colors), + bottomContent: _bottomBar(context, ref, colors), ); } - Widget _bottomBar(BuildContext context, AppColorsV2 colors) { + Widget _bottomBar(BuildContext context, WidgetRef ref, AppColorsV2 colors) { + final l10n = ref.watch(l10nProvider); + return ScaffoldBaseBottomContent( child: Row( children: [ Expanded( child: QuantusButton.simple( - label: 'Copy', + label: l10n.recoveryPhraseBodyCopy, icon: Icon(Icons.copy, color: colors.textPrimary, size: 14), iconPlacement: IconPlacement.leading, - onTap: () => _copyToClipboard(context), + onTap: () => _copyToClipboard(context, ref), variant: ButtonVariant.secondary, ), ), diff --git a/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart b/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart index fc4b1959..49b974cf 100644 --- a/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/account_ready_screen.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/back_button.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; @@ -11,7 +14,7 @@ import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; enum AccountReadyOverviewOrigin { accountCreated, walletCreated, walletImported } -class AccountReadyScreen extends StatelessWidget { +class AccountReadyScreen extends ConsumerWidget { const AccountReadyScreen({ super.key, required this.origin, @@ -33,17 +36,18 @@ class AccountReadyScreen extends StatelessWidget { Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const HomeScreen()), (_) => false); } - String get _appBarTitle => switch (origin) { - AccountReadyOverviewOrigin.accountCreated => 'Account Created', - AccountReadyOverviewOrigin.walletCreated => 'Wallet Created', - AccountReadyOverviewOrigin.walletImported => 'Wallet Imported', + String _appBarTitle(AppLocalizations l10n) => switch (origin) { + AccountReadyOverviewOrigin.accountCreated => l10n.accountReadyAccountCreated, + AccountReadyOverviewOrigin.walletCreated => l10n.accountReadyWalletCreated, + AccountReadyOverviewOrigin.walletImported => l10n.accountReadyWalletImported, }; bool get isWalletRelated => origin == AccountReadyOverviewOrigin.walletCreated || origin == AccountReadyOverviewOrigin.walletImported; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final formattedAddress = AddressFormattingService.formatAddress( @@ -52,7 +56,8 @@ class AccountReadyScreen extends StatelessWidget { ellipses: '.......', postFix: 10, ); - final headline = isWalletRelated ? _appBarTitle : accountName; + final appBarTitle = _appBarTitle(l10n); + final headline = isWalletRelated ? appBarTitle : accountName; return PopScope( canPop: false, @@ -63,7 +68,7 @@ class AccountReadyScreen extends StatelessWidget { }, child: ScaffoldBase( appBar: V2AppBar( - title: _appBarTitle, + title: appBarTitle, leading: AppBackButton(onTap: () => _goHome(context)), ), mainContent: Column( @@ -130,7 +135,11 @@ class AccountReadyScreen extends StatelessWidget { ], ), bottomContent: ScaffoldBaseBottomContent( - child: QuantusButton.simple(label: 'Done', onTap: () => _goHome(context), variant: ButtonVariant.primary), + child: QuantusButton.simple( + label: l10n.accountReadyDone, + onTap: () => _goHome(context), + variant: ButtonVariant.primary, + ), ), ), ); diff --git a/mobile-app/lib/v2/screens/create/new_wallet_recovery_phrase_screen.dart b/mobile-app/lib/v2/screens/create/new_wallet_recovery_phrase_screen.dart index c05cb6e4..f811d769 100644 --- a/mobile-app/lib/v2/screens/create/new_wallet_recovery_phrase_screen.dart +++ b/mobile-app/lib/v2/screens/create/new_wallet_recovery_phrase_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/remote_config_provider.dart'; import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; import 'package:resonance_network_wallet/services/wallet_creation_service.dart'; @@ -51,7 +52,8 @@ class _NewWalletRecoveryPhraseScreenState extends ConsumerState false, ); } catch (e) { - if (mounted) context.showErrorToaster(message: 'Error saving wallet: $e'); + if (mounted) { + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.createWalletRecoveryPhraseSaveError(e.toString())); + } } finally { if (mounted) setState(() => _isSubmitting = false); } @@ -105,10 +110,12 @@ class _NewWalletRecoveryPhraseScreenState extends ConsumerState { @override Widget build(BuildContext context) { - final data = const SettingsCautionScaffoldData.recoveryPhrase(); + final l10n = ref.watch(l10nProvider); + final data = SettingsCautionScaffoldData( + headline: l10n.createWalletCautionHeadline, + bulletItems: [ + l10n.createWalletCautionBullet1, + l10n.createWalletCautionBullet2, + l10n.createWalletCautionBullet3, + ], + checkboxLabel: l10n.createWalletCautionCheckboxLabel, + ); return SettingsCautionScaffold( - appBarTitle: 'Create Wallet', + appBarTitle: l10n.createWalletAppBarTitle, data: data, + continueLabel: l10n.createWalletCautionContinue, checkboxChecked: _acknowledged, onCheckboxChanged: () => setState(() => _acknowledged = !_acknowledged), onContinue: _continue, diff --git a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart index b8499ca5..d43cd9e5 100644 --- a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/remote_config_provider.dart'; import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -64,7 +65,7 @@ class _ImportWalletScreenV2State extends ConsumerState { if (!mnemonic.startsWith('//')) { final words = mnemonic.split(' ').where((w) => w.isNotEmpty).toList(); if (words.length != 12 && words.length != 24) { - throw Exception('Recovery phrase must be 12 or 24 words'); + throw Exception(ref.read(l10nProvider).importWalletValidationError); } } @@ -118,12 +119,13 @@ class _ImportWalletScreenV2State extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final fieldTextStyle = text.smallTitle?.copyWith(color: colors.checksum, fontWeight: FontWeight.w400); return ScaffoldBase( - appBar: const V2AppBar(title: 'Import Wallet'), + appBar: V2AppBar(title: l10n.importWalletAppBarTitle), mainContent: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), behavior: HitTestBehavior.opaque, @@ -131,7 +133,7 @@ class _ImportWalletScreenV2State extends ConsumerState { child: Column( children: [ Text( - 'Restore an existing wallet with your 12 or 24 words recovery phrase', + l10n.importWalletDescription, style: text.smallParagraph?.copyWith(color: colors.textSecondary), ), const SizedBox(height: 16), @@ -149,7 +151,7 @@ class _ImportWalletScreenV2State extends ConsumerState { onChanged: (_) => setState(() {}), style: fieldTextStyle, decoration: InputDecoration.collapsed( - hintText: 'Type in or paste your recovery phrase. Separate words with spaces.', + hintText: l10n.importWalletHint, hintStyle: fieldTextStyle?.copyWith(color: colors.textSecondary), ), maxLines: null, @@ -172,7 +174,7 @@ class _ImportWalletScreenV2State extends ConsumerState { bottomContent: ScaffoldBaseBottomContent( child: QuantusButton.simple( key: _buttonKey, - label: 'Import', + label: l10n.importWalletButton, onTap: _import, isLoading: _isLoading, isDisabled: !_hasInput, diff --git a/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart b/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart index 840fd05e..8ea50417 100644 --- a/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart +++ b/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart @@ -41,6 +41,7 @@ class SettingsCautionScaffoldData { class SettingsCautionScaffold extends StatelessWidget { final String appBarTitle; + final String continueLabel; final bool checkboxChecked; final VoidCallback onCheckboxChanged; final VoidCallback onContinue; @@ -55,6 +56,7 @@ class SettingsCautionScaffold extends StatelessWidget { required this.onCheckboxChanged, required this.onContinue, required this.data, + this.continueLabel = 'Continue', this.betweenBulletsStyle = SettingsDividerStyle.list, this.continueButtonLoading = false, }); @@ -85,6 +87,7 @@ class SettingsCautionScaffold extends StatelessWidget { ), bottomContent: _SettingsCautionBottom( checkboxLabel: data.checkboxLabel, + continueLabel: continueLabel, checked: checkboxChecked, onCheckboxChanged: onCheckboxChanged, onContinue: onContinue, @@ -97,6 +100,7 @@ class SettingsCautionScaffold extends StatelessWidget { class _SettingsCautionBottom extends StatelessWidget { const _SettingsCautionBottom({ required this.checkboxLabel, + required this.continueLabel, required this.checked, required this.onCheckboxChanged, required this.onContinue, @@ -104,6 +108,7 @@ class _SettingsCautionBottom extends StatelessWidget { }); final String checkboxLabel; + final String continueLabel; final bool checked; final VoidCallback onCheckboxChanged; final VoidCallback onContinue; @@ -118,7 +123,7 @@ class _SettingsCautionBottom extends StatelessWidget { SettingsCheckbox(checked: checked, label: checkboxLabel, onTap: onCheckboxChanged), const SizedBox(height: 32), QuantusButton.simple( - label: 'Continue', + label: continueLabel, onTap: onContinue, variant: ButtonVariant.primary, isDisabled: !checked, diff --git a/mobile-app/lib/v2/screens/welcome/welcome_screen.dart b/mobile-app/lib/v2/screens/welcome/welcome_screen.dart index a98f7146..d1afd067 100644 --- a/mobile-app/lib/v2/screens/welcome/welcome_screen.dart +++ b/mobile-app/lib/v2/screens/welcome/welcome_screen.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/screens/create/wallet_ready_screen.dart'; @@ -6,11 +8,13 @@ import 'package:resonance_network_wallet/v2/screens/import/import_wallet_screen. import 'package:resonance_network_wallet/v2/screens/welcome/onboarding_background.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class WelcomeScreenV2 extends StatelessWidget { +class WelcomeScreenV2 extends ConsumerWidget { const WelcomeScreenV2({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); + return ScaffoldBase( backgroundWidget: const OnboardingBackground(), mainContent: Column( @@ -21,14 +25,14 @@ class WelcomeScreenV2 extends StatelessWidget { SizedBox( width: 210, child: Text( - 'Quantum Secure Encrypted Money', + l10n.welcomeTagline, textAlign: TextAlign.center, style: context.themeText.mediumTitle, ), ), const SizedBox(height: 56), QuantusButton.simple( - label: 'Create New Wallet', + label: l10n.welcomeCreateNewWallet, onTap: () => Navigator.push( context, MaterialPageRoute( @@ -39,7 +43,7 @@ class WelcomeScreenV2 extends StatelessWidget { ), const SizedBox(height: 24), QuantusButton.simple( - label: 'Import Wallet', + label: l10n.welcomeImportWallet, onTap: () => Navigator.push( context, MaterialPageRoute( From 4b3027cd1ee6af81829d1e5836e7668ea14c9c9b Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 16:31:01 +0800 Subject: [PATCH 04/25] feat: localize home and activity --- mobile-app/lib/l10n/app_en.arb | 63 ++++++++++++++ mobile-app/lib/l10n/app_id.arb | 18 +++- mobile-app/lib/l10n/app_localizations.dart | 84 +++++++++++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 44 ++++++++++ mobile-app/lib/l10n/app_localizations_id.dart | 46 +++++++++- .../lib/v2/screens/home/activity_section.dart | 25 +++--- .../lib/v2/screens/home/home_screen.dart | 37 ++++---- 7 files changed, 288 insertions(+), 29 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index b8092618..3037d9d8 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -142,5 +142,68 @@ "importWalletValidationError": "Recovery phrase must be 12 or 24 words", "@importWalletValidationError": { "description": "Validation error when recovery phrase word count is invalid" + }, + + "homeError": "Error: {error}", + "@homeError": { + "description": "Error message on the home screen", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "homeNoActiveAccount": "No active account", + "@homeNoActiveAccount": { + "description": "Shown when no account is active on the home screen" + }, + "homeCharge": "Charge", + "@homeCharge": { + "description": "POS charge button on the home screen bottom bar" + }, + "homeGetTestnetTokens": "Get Testnet Tokens ↗", + "@homeGetTestnetTokens": { + "description": "Faucet button when balance is zero on the home screen" + }, + "homeErrorLoadingBalance": "Error loading balance", + "@homeErrorLoadingBalance": { + "description": "Error when balance fails to load on the home screen" + }, + "homeReceive": "Receive", + "@homeReceive": { + "description": "Receive action button on the home screen" + }, + "homeSend": "Send", + "@homeSend": { + "description": "Send action button on the home screen" + }, + "homeSwap": "Swap", + "@homeSwap": { + "description": "Swap action button on the home screen" + }, + + "homeActivityTitle": "Activity", + "@homeActivityTitle": { + "description": "Section title for recent activity on the home screen" + }, + "homeActivityViewAll": "View All", + "@homeActivityViewAll": { + "description": "Link to full activity screen from home" + }, + "homeActivityErrorLoading": "Error loading transactions", + "@homeActivityErrorLoading": { + "description": "Error when transactions fail to load in home activity section" + }, + "homeActivityRetry": "Retry", + "@homeActivityRetry": { + "description": "Retry link in home activity section error state" + }, + "homeActivityEmptyTitle": "No Transactions Yet", + "@homeActivityEmptyTitle": { + "description": "Empty state title in home activity section" + }, + "homeActivityEmptyMessage": "Your activity will appear here once you send or receive QUAN.", + "@homeActivityEmptyMessage": { + "description": "Empty state message in home activity section" } } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index 310614e0..a5b884cc 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -36,5 +36,21 @@ "importWalletDescription": "Pulihkan wallet yang ada dengan recovery phrase 12 atau 24 kata Anda", "importWalletHint": "Ketik atau tempel recovery phrase Anda. Pisahkan kata dengan spasi.", "importWalletButton": "Impor", - "importWalletValidationError": "Recovery phrase harus 12 atau 24 kata" + "importWalletValidationError": "Recovery phrase harus 12 atau 24 kata", + + "homeError": "Eror: {error}", + "homeNoActiveAccount": "Tidak ada akun aktif", + "homeCharge": "Tagih", + "homeGetTestnetTokens": "Dapatkan Token Testnet ↗", + "homeErrorLoadingBalance": "Gagal memuat saldo", + "homeReceive": "Terima", + "homeSend": "Kirim", + "homeSwap": "Tukar", + + "homeActivityTitle": "Aktivitas", + "homeActivityViewAll": "Lihat Semua", + "homeActivityErrorLoading": "Gagal memuat transaksi", + "homeActivityRetry": "Coba Lagi", + "homeActivityEmptyTitle": "Belum Ada Transaksi", + "homeActivityEmptyMessage": "Aktivitas Anda akan muncul di sini setelah Anda mengirim atau menerima QUAN." } diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index dabc4a8a..c8445028 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -283,6 +283,90 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Recovery phrase must be 12 or 24 words'** String get importWalletValidationError; + + /// Error message on the home screen + /// + /// In en, this message translates to: + /// **'Error: {error}'** + String homeError(String error); + + /// Shown when no account is active on the home screen + /// + /// In en, this message translates to: + /// **'No active account'** + String get homeNoActiveAccount; + + /// POS charge button on the home screen bottom bar + /// + /// In en, this message translates to: + /// **'Charge'** + String get homeCharge; + + /// Faucet button when balance is zero on the home screen + /// + /// In en, this message translates to: + /// **'Get Testnet Tokens ↗'** + String get homeGetTestnetTokens; + + /// Error when balance fails to load on the home screen + /// + /// In en, this message translates to: + /// **'Error loading balance'** + String get homeErrorLoadingBalance; + + /// Receive action button on the home screen + /// + /// In en, this message translates to: + /// **'Receive'** + String get homeReceive; + + /// Send action button on the home screen + /// + /// In en, this message translates to: + /// **'Send'** + String get homeSend; + + /// Swap action button on the home screen + /// + /// In en, this message translates to: + /// **'Swap'** + String get homeSwap; + + /// Section title for recent activity on the home screen + /// + /// In en, this message translates to: + /// **'Activity'** + String get homeActivityTitle; + + /// Link to full activity screen from home + /// + /// In en, this message translates to: + /// **'View All'** + String get homeActivityViewAll; + + /// Error when transactions fail to load in home activity section + /// + /// In en, this message translates to: + /// **'Error loading transactions'** + String get homeActivityErrorLoading; + + /// Retry link in home activity section error state + /// + /// In en, this message translates to: + /// **'Retry'** + String get homeActivityRetry; + + /// Empty state title in home activity section + /// + /// In en, this message translates to: + /// **'No Transactions Yet'** + String get homeActivityEmptyTitle; + + /// Empty state message in home activity section + /// + /// In en, this message translates to: + /// **'Your activity will appear here once you send or receive QUAN.'** + String get homeActivityEmptyMessage; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index de2e3b03..6be82648 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -110,4 +110,48 @@ class AppLocalizationsEn extends AppLocalizations { @override String get importWalletValidationError => 'Recovery phrase must be 12 or 24 words'; + + @override + String homeError(String error) { + return 'Error: $error'; + } + + @override + String get homeNoActiveAccount => 'No active account'; + + @override + String get homeCharge => 'Charge'; + + @override + String get homeGetTestnetTokens => 'Get Testnet Tokens ↗'; + + @override + String get homeErrorLoadingBalance => 'Error loading balance'; + + @override + String get homeReceive => 'Receive'; + + @override + String get homeSend => 'Send'; + + @override + String get homeSwap => 'Swap'; + + @override + String get homeActivityTitle => 'Activity'; + + @override + String get homeActivityViewAll => 'View All'; + + @override + String get homeActivityErrorLoading => 'Error loading transactions'; + + @override + String get homeActivityRetry => 'Retry'; + + @override + String get homeActivityEmptyTitle => 'No Transactions Yet'; + + @override + String get homeActivityEmptyMessage => 'Your activity will appear here once you send or receive QUAN.'; } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index 0f3d8650..dd44ffac 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -50,7 +50,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get createWalletCautionBullet2 => - 'Siapa pun yang mendapatkannya memiliki kendali penuh atas dana Anda, secara permanen'; + 'Siapa pun yang mendapatkannya akan memiliki kendali penuh atas dana Anda, secara permanen'; @override String get createWalletCautionBullet3 => 'Tuliskan dan simpan di tempat yang aman. Jangan simpan secara digital'; @@ -111,4 +111,48 @@ class AppLocalizationsId extends AppLocalizations { @override String get importWalletValidationError => 'Recovery phrase harus 12 atau 24 kata'; + + @override + String homeError(String error) { + return 'Error: $error'; + } + + @override + String get homeNoActiveAccount => 'Tidak ada akun aktif'; + + @override + String get homeCharge => 'Tagih'; + + @override + String get homeGetTestnetTokens => 'Dapatkan Token Testnet ↗'; + + @override + String get homeErrorLoadingBalance => 'Gagal memuat saldo'; + + @override + String get homeReceive => 'Terima'; + + @override + String get homeSend => 'Kirim'; + + @override + String get homeSwap => 'Tukar'; + + @override + String get homeActivityTitle => 'Aktivitas'; + + @override + String get homeActivityViewAll => 'Lihat Semua'; + + @override + String get homeActivityErrorLoading => 'Gagal memuat transaksi'; + + @override + String get homeActivityRetry => 'Coba Lagi'; + + @override + String get homeActivityEmptyTitle => 'Belum Ada Transaksi'; + + @override + String get homeActivityEmptyMessage => 'Aktivitas Anda akan muncul di sini setelah Anda mengirim atau menerima QUAN.'; } diff --git a/mobile-app/lib/v2/screens/home/activity_section.dart b/mobile-app/lib/v2/screens/home/activity_section.dart index 6cf1d986..aa5d3fe2 100644 --- a/mobile-app/lib/v2/screens/home/activity_section.dart +++ b/mobile-app/lib/v2/screens/home/activity_section.dart @@ -4,7 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; import 'package:resonance_network_wallet/models/combined_transactions_list.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/active_account_transactions_provider.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; import 'package:resonance_network_wallet/services/transaction_service.dart'; import 'package:resonance_network_wallet/v2/screens/activity/activity_screen.dart'; @@ -27,6 +29,7 @@ class ActivitySection extends ConsumerStatefulWidget { class _ActivitySectionState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final formatTxAmount = ref.watch(txAmountDisplayProvider); final colors = context.colors; final text = context.themeText; @@ -44,14 +47,14 @@ class _ActivitySectionState extends ConsumerState { if (all.isEmpty) { return Column( - children: [const SizedBox(height: 40), _header(colors, text, context), _emptyState(text, colors)], + children: [const SizedBox(height: 40), _header(colors, text, context, l10n), _emptyState(text, colors, l10n)], ); } return Column( children: [ const SizedBox(height: 40), - _header(colors, text, context), + _header(colors, text, context, l10n), const SizedBox(height: 28), ...recentTransactions.mapIndexed((index, tx) { @@ -78,7 +81,7 @@ class _ActivitySectionState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _header(colors, text, context), + _header(colors, text, context, l10n), const SizedBox(height: 24), for (var i = 0; i < 3; i++) ...[ const TxItemSkeleton(), @@ -91,7 +94,7 @@ class _ActivitySectionState extends ConsumerState { padding: const EdgeInsets.only(top: 40), child: Column( children: [ - Text('Error loading transactions', style: text.detail?.copyWith(color: colors.textError)), + Text(l10n.homeActivityErrorLoading, style: text.detail?.copyWith(color: colors.textError)), const SizedBox(height: 12), GestureDetector( behavior: HitTestBehavior.opaque, @@ -102,7 +105,7 @@ class _ActivitySectionState extends ConsumerState { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: Text( - 'Retry', + l10n.homeActivityRetry, style: text.smallParagraph?.copyWith(color: colors.textPrimary, decoration: TextDecoration.underline), ), ), @@ -113,21 +116,21 @@ class _ActivitySectionState extends ConsumerState { ); } - Widget _emptyState(AppTextTheme text, AppColorsV2 colors) { + Widget _emptyState(AppTextTheme text, AppColorsV2 colors, AppLocalizations l10n) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 24), child: Column( children: [ Text( - 'No Transactions Yet', + l10n.homeActivityEmptyTitle, style: text.mediumTitle?.copyWith(color: colors.textMuted, fontWeight: FontWeight.w400), ), const SizedBox(height: 8), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 240), child: Text( - 'Your activity will appear here once you send or receive QUAN.', + l10n.homeActivityEmptyMessage, textAlign: TextAlign.center, style: text.smallParagraph?.copyWith(color: colors.txItemIconDefault), ), @@ -137,15 +140,15 @@ class _ActivitySectionState extends ConsumerState { ); } - Widget _header(AppColorsV2 colors, AppTextTheme text, BuildContext context) { + Widget _header(AppColorsV2 colors, AppTextTheme text, BuildContext context, AppLocalizations l10n) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Activity', style: text.smallTitle), + Text(l10n.homeActivityTitle, style: text.smallTitle), GestureDetector( onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ActivityScreen())), child: Text( - 'View All', + l10n.homeActivityViewAll, style: text.smallTitle?.copyWith( color: colors.textMuted, decoration: TextDecoration.underline, diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index 28a649fb..b5901008 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -22,7 +22,9 @@ 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/models/filtered_transactions_params.dart'; import 'package:resonance_network_wallet/providers/account_id_list_cache.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/active_account_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/filtered_all_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/route_intent_providers.dart'; @@ -118,6 +120,7 @@ class _HomeScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final accountAsync = ref.watch(activeAccountProvider); final txAsync = ref.watch(activeAccountTransactionsProvider(TransactionFilter.all)); final colors = context.colors; @@ -127,36 +130,38 @@ class _HomeScreenState extends ConsumerState { loading: () => const ScaffoldBase(mainContent: Center(child: Loader())), error: (e, _) => ScaffoldBase( mainContent: Center( - child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError)), + child: Text(l10n.homeError(e.toString()), style: text.detail?.copyWith(color: colors.textError)), ), ), data: (active) { if (active == null) { - return const ScaffoldBase(mainContent: Center(child: Text('No active account'))); + return ScaffoldBase( + mainContent: Center(child: Text(l10n.homeNoActiveAccount)), + ); } return ScaffoldBase.refreshable( onRefresh: _refresh, slivers: [ - _buildContent(active, colors, text), + _buildContent(active, colors, text, l10n), ActivitySection(txAsync: txAsync, activeAccount: active.account, onRetry: _refresh), const SizedBox(height: 58), ], - bottomContent: _buildBottomContent(), + bottomContent: _buildBottomContent(l10n), ); }, ); } - Widget _buildContent(DisplayAccount active, AppColorsV2 colors, AppTextTheme text) { + Widget _buildContent(DisplayAccount active, AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), _buildTopBar(), const SizedBox(height: 40), - _buildBalance(colors, text), + _buildBalance(colors, text, l10n), const SizedBox(height: 40), - if (active is RegularAccount) ...[_buildActionButtons(), const SizedBox(height: 40)], + if (active is RegularAccount) ...[_buildActionButtons(l10n), const SizedBox(height: 40)], DottedBorder( dashLength: 3, gapLength: 5, @@ -167,14 +172,14 @@ class _HomeScreenState extends ConsumerState { ); } - Widget? _buildBottomContent() { + Widget? _buildBottomContent(AppLocalizations l10n) { final enablePos = ref.watch(posModeProvider); final balanceAsync = ref.watch(balanceProvider); if (enablePos) { return ScaffoldBaseBottomContent( child: QuantusButton.simple( - label: 'Charge', + label: l10n.homeCharge, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PosAmountScreen())), ), ); @@ -185,7 +190,7 @@ class _HomeScreenState extends ConsumerState { (balance) => balance == BigInt.zero ? ScaffoldBaseBottomContent( child: QuantusButton.simple( - label: 'Get Testnet Tokens ↗', + label: l10n.homeGetTestnetTokens, onTap: () => launchXPost(AppConstants.faucetUrl), ), ) @@ -223,7 +228,7 @@ class _HomeScreenState extends ConsumerState { ); } - Widget _buildBalance(AppColorsV2 colors, AppTextTheme text) { + Widget _buildBalance(AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n) { final currencyAsync = ref.watch(balanceDisplayProvider); return Column( @@ -247,30 +252,30 @@ class _HomeScreenState extends ConsumerState { Row(mainAxisAlignment: MainAxisAlignment.center, children: [Skeleton(width: 100, height: 18)]), ], ), - error: (_, _) => Text('Error loading balance', style: text.detail?.copyWith(color: colors.textError)), + error: (_, _) => Text(l10n.homeErrorLoadingBalance, style: text.detail?.copyWith(color: colors.textError)), ), ], ); } - Widget _buildActionButtons() { + Widget _buildActionButtons(AppLocalizations l10n) { final enableSwap = ref.watch(remoteConfigProvider).enableSwap; final receiveCard = _actionCard( iconAsset: 'assets/v2/action_receive.svg', - label: 'Receive', + label: l10n.homeReceive, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ReceiveScreen())), ); final sendCard = _actionCard( iconAsset: 'assets/v2/action_send.svg', - label: 'Send', + label: l10n.homeSend, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SelectRecipientScreen())), ); final swapCard = _actionCard( iconAsset: 'assets/v2/action_swap.svg', - label: 'Swap', + label: l10n.homeSwap, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SwapScreen())), ); From 57cb374d7a9c611341b827b2b1e3ecc3c27a9e4a Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 16:38:00 +0800 Subject: [PATCH 05/25] feat: localize manage account flow --- mobile-app/lib/l10n/app_en.arb | 176 +++++++++++++ mobile-app/lib/l10n/app_id.arb | 48 +++- mobile-app/lib/l10n/app_localizations.dart | 234 ++++++++++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 121 +++++++++ mobile-app/lib/l10n/app_localizations_id.dart | 123 ++++++++- .../accounts/account_details_screen.dart | 12 +- .../screens/accounts/account_menu_screen.dart | 12 +- .../v2/screens/accounts/accounts_sheet.dart | 32 ++- .../accounts/add_account_menu_screen.dart | 12 +- .../accounts/add_hardware_account_screen.dart | 24 +- .../accounts/create_account_screen.dart | 15 +- .../screens/accounts/edit_account_screen.dart | 13 +- 12 files changed, 775 insertions(+), 47 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 3037d9d8..8dc9fbe9 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -205,5 +205,181 @@ "homeActivityEmptyMessage": "Your activity will appear here once you send or receive QUAN.", "@homeActivityEmptyMessage": { "description": "Empty state message in home activity section" + }, + + "accountsSheetTitle": "Accounts", + "@accountsSheetTitle": { + "description": "Title of the accounts bottom sheet" + }, + "accountsSheetFailedLoadAccounts": "Failed to load accounts.", + "@accountsSheetFailedLoadAccounts": { + "description": "Error when accounts list fails to load" + }, + "accountsSheetFailedLoadActiveAccount": "Failed to load active account.", + "@accountsSheetFailedLoadActiveAccount": { + "description": "Error when active account fails to load" + }, + "accountsSheetNoAccountsFound": "No accounts found.", + "@accountsSheetNoAccountsFound": { + "description": "Empty state in accounts sheet" + }, + "accountsSheetAddAccount": "Add Account", + "@accountsSheetAddAccount": { + "description": "Button to add a new account" + }, + "accountsSheetLoading": "Loading...", + "@accountsSheetLoading": { + "description": "Loading balance in accounts sheet" + }, + "accountsSheetBalanceUnavailable": "Balance unavailable", + "@accountsSheetBalanceUnavailable": { + "description": "When account balance fails to load" + }, + "accountsSheetBalance": "{balance} {symbol}", + "@accountsSheetBalance": { + "description": "Formatted balance with token symbol", + "placeholders": { + "balance": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } + }, + + "addAccountMenuTitle": "Add Account", + "@addAccountMenuTitle": { + "description": "App bar title on add account menu" + }, + "addAccountMenuCreateTitle": "Create New Account", + "@addAccountMenuCreateTitle": { + "description": "Create new account menu row title" + }, + "addAccountMenuCreateSubtitle": "Generate a fresh wallet address", + "@addAccountMenuCreateSubtitle": { + "description": "Create new account menu row subtitle" + }, + "addAccountMenuImportTitle": "Import Wallet", + "@addAccountMenuImportTitle": { + "description": "Import wallet menu row title" + }, + "addAccountMenuImportSubtitle": "Use a recovery phrase to import", + "@addAccountMenuImportSubtitle": { + "description": "Import wallet menu row subtitle" + }, + + "createAccountAppBarTitle": "Account Name", + "@createAccountAppBarTitle": { + "description": "App bar title when creating an account" + }, + "createAccountSubtitle": "Give this account a name you'll recognize. You can change it anytime.", + "@createAccountSubtitle": { + "description": "Subtitle on create account name field" + }, + "createAccountButton": "Create", + "@createAccountButton": { + "description": "Create account button" + }, + "createAccountErrorCouldNotAdd": "Could not add account.", + "@createAccountErrorCouldNotAdd": { + "description": "Error when account creation fails" + }, + "createAccountDefaultName": "Account {number}", + "@createAccountDefaultName": { + "description": "Default name for a new account", + "placeholders": { + "number": { + "type": "int" + } + } + }, + + "editAccountAppBarTitle": "Account Name", + "@editAccountAppBarTitle": { + "description": "App bar title when editing account name" + }, + "editAccountDone": "Done", + "@editAccountDone": { + "description": "Done button on edit account screen" + }, + "editAccountNameEmpty": "Account name can't be empty", + "@editAccountNameEmpty": { + "description": "Validation error when account name is empty" + }, + "editAccountRenameFailed": "Failed to rename account.", + "@editAccountRenameFailed": { + "description": "Error when renaming account fails" + }, + + "accountMenuTitle": "Accounts", + "@accountMenuTitle": { + "description": "App bar title on account menu screen" + }, + "accountMenuAccountName": "Account Name", + "@accountMenuAccountName": { + "description": "Account name menu row label" + }, + "accountMenuAddressDetails": "Address Details", + "@accountMenuAddressDetails": { + "description": "Address details menu row label" + }, + "accountMenuShowRecoveryPhrase": "Show Recovery Phrase", + "@accountMenuShowRecoveryPhrase": { + "description": "Show recovery phrase menu row label" + }, + "accountMenuNotFound": "Account not found", + "@accountMenuNotFound": { + "description": "When account is not found on menu screen" + }, + + "accountDetailsTitle": "Address Details", + "@accountDetailsTitle": { + "description": "App bar title on account details screen" + }, + + "addHardwareAccountAddWallet": "Add Hardware Wallet", + "@addHardwareAccountAddWallet": { + "description": "Title when adding a new hardware wallet" + }, + "addHardwareAccountAddAccount": "Add Hardware Account", + "@addHardwareAccountAddAccount": { + "description": "Title when adding a hardware account to existing wallet" + }, + "addHardwareAccountNameLabel": "NAME", + "@addHardwareAccountNameLabel": { + "description": "Name field label" + }, + "addHardwareAccountNameHintWallet": "Hardware Wallet", + "@addHardwareAccountNameHintWallet": { + "description": "Name field hint for new hardware wallet" + }, + "addHardwareAccountNameHintAccount": "Account", + "@addHardwareAccountNameHintAccount": { + "description": "Name field hint for hardware account" + }, + "addHardwareAccountAddressLabel": "ADDRESS", + "@addHardwareAccountAddressLabel": { + "description": "Address field label" + }, + "addHardwareAccountAddressHint": "SS58 address", + "@addHardwareAccountAddressHint": { + "description": "Address field hint" + }, + "addHardwareAccountScanQr": "Scan QR Code", + "@addHardwareAccountScanQr": { + "description": "Scan QR code button" + }, + "addHardwareAccountDebugFill": "Debug Fill", + "@addHardwareAccountDebugFill": { + "description": "Debug fill button" + }, + "addHardwareAccountNameRequired": "Name is required", + "@addHardwareAccountNameRequired": { + "description": "Validation when name is empty" + }, + "addHardwareAccountInvalidAddress": "Invalid address", + "@addHardwareAccountInvalidAddress": { + "description": "Validation when address is invalid" } } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index a5b884cc..b86a39b3 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -52,5 +52,51 @@ "homeActivityErrorLoading": "Gagal memuat transaksi", "homeActivityRetry": "Coba Lagi", "homeActivityEmptyTitle": "Belum Ada Transaksi", - "homeActivityEmptyMessage": "Aktivitas Anda akan muncul di sini setelah Anda mengirim atau menerima QUAN." + "homeActivityEmptyMessage": "Aktivitas Anda akan muncul di sini setelah Anda mengirim atau menerima QUAN.", + + "accountsSheetTitle": "Akun", + "accountsSheetFailedLoadAccounts": "Gagal memuat akun.", + "accountsSheetFailedLoadActiveAccount": "Gagal memuat akun aktif.", + "accountsSheetNoAccountsFound": "Tidak ada akun ditemukan.", + "accountsSheetAddAccount": "Tambah Akun", + "accountsSheetLoading": "Memuat...", + "accountsSheetBalanceUnavailable": "Saldo tidak tersedia", + "accountsSheetBalance": "{balance} {symbol}", + + "addAccountMenuTitle": "Tambah Akun", + "addAccountMenuCreateTitle": "Buat Akun Baru", + "addAccountMenuCreateSubtitle": "Buat alamat wallet baru", + "addAccountMenuImportTitle": "Impor Wallet", + "addAccountMenuImportSubtitle": "Gunakan recovery phrase untuk mengimpor", + + "createAccountAppBarTitle": "Nama Akun", + "createAccountSubtitle": "Berikan nama yang mudah Anda kenali. Anda bisa mengubahnya kapan saja.", + "createAccountButton": "Buat", + "createAccountErrorCouldNotAdd": "Gagal menambahkan akun.", + "createAccountDefaultName": "Akun {number}", + + "editAccountAppBarTitle": "Nama Akun", + "editAccountDone": "Selesai", + "editAccountNameEmpty": "Nama akun tidak boleh kosong", + "editAccountRenameFailed": "Gagal mengganti nama akun.", + + "accountMenuTitle": "Akun", + "accountMenuAccountName": "Nama Akun", + "accountMenuAddressDetails": "Detail Alamat", + "accountMenuShowRecoveryPhrase": "Tampilkan Recovery Phrase", + "accountMenuNotFound": "Akun tidak ditemukan", + + "accountDetailsTitle": "Detail Alamat", + + "addHardwareAccountAddWallet": "Tambah Hardware Wallet", + "addHardwareAccountAddAccount": "Tambah Akun Hardware", + "addHardwareAccountNameLabel": "NAMA", + "addHardwareAccountNameHintWallet": "Hardware Wallet", + "addHardwareAccountNameHintAccount": "Akun", + "addHardwareAccountAddressLabel": "ALAMAT", + "addHardwareAccountAddressHint": "Alamat SS58", + "addHardwareAccountScanQr": "Pindai Kode QR", + "addHardwareAccountDebugFill": "Isi Debug", + "addHardwareAccountNameRequired": "Nama wajib diisi", + "addHardwareAccountInvalidAddress": "Alamat tidak valid" } diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index c8445028..df884286 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -367,6 +367,240 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Your activity will appear here once you send or receive QUAN.'** String get homeActivityEmptyMessage; + + /// Title of the accounts bottom sheet + /// + /// In en, this message translates to: + /// **'Accounts'** + String get accountsSheetTitle; + + /// Error when accounts list fails to load + /// + /// In en, this message translates to: + /// **'Failed to load accounts.'** + String get accountsSheetFailedLoadAccounts; + + /// Error when active account fails to load + /// + /// In en, this message translates to: + /// **'Failed to load active account.'** + String get accountsSheetFailedLoadActiveAccount; + + /// Empty state in accounts sheet + /// + /// In en, this message translates to: + /// **'No accounts found.'** + String get accountsSheetNoAccountsFound; + + /// Button to add a new account + /// + /// In en, this message translates to: + /// **'Add Account'** + String get accountsSheetAddAccount; + + /// Loading balance in accounts sheet + /// + /// In en, this message translates to: + /// **'Loading...'** + String get accountsSheetLoading; + + /// When account balance fails to load + /// + /// In en, this message translates to: + /// **'Balance unavailable'** + String get accountsSheetBalanceUnavailable; + + /// Formatted balance with token symbol + /// + /// In en, this message translates to: + /// **'{balance} {symbol}'** + String accountsSheetBalance(String balance, String symbol); + + /// App bar title on add account menu + /// + /// In en, this message translates to: + /// **'Add Account'** + String get addAccountMenuTitle; + + /// Create new account menu row title + /// + /// In en, this message translates to: + /// **'Create New Account'** + String get addAccountMenuCreateTitle; + + /// Create new account menu row subtitle + /// + /// In en, this message translates to: + /// **'Generate a fresh wallet address'** + String get addAccountMenuCreateSubtitle; + + /// Import wallet menu row title + /// + /// In en, this message translates to: + /// **'Import Wallet'** + String get addAccountMenuImportTitle; + + /// Import wallet menu row subtitle + /// + /// In en, this message translates to: + /// **'Use a recovery phrase to import'** + String get addAccountMenuImportSubtitle; + + /// App bar title when creating an account + /// + /// In en, this message translates to: + /// **'Account Name'** + String get createAccountAppBarTitle; + + /// Subtitle on create account name field + /// + /// In en, this message translates to: + /// **'Give this account a name you\'ll recognize. You can change it anytime.'** + String get createAccountSubtitle; + + /// Create account button + /// + /// In en, this message translates to: + /// **'Create'** + String get createAccountButton; + + /// Error when account creation fails + /// + /// In en, this message translates to: + /// **'Could not add account.'** + String get createAccountErrorCouldNotAdd; + + /// Default name for a new account + /// + /// In en, this message translates to: + /// **'Account {number}'** + String createAccountDefaultName(int number); + + /// App bar title when editing account name + /// + /// In en, this message translates to: + /// **'Account Name'** + String get editAccountAppBarTitle; + + /// Done button on edit account screen + /// + /// In en, this message translates to: + /// **'Done'** + String get editAccountDone; + + /// Validation error when account name is empty + /// + /// In en, this message translates to: + /// **'Account name can\'t be empty'** + String get editAccountNameEmpty; + + /// Error when renaming account fails + /// + /// In en, this message translates to: + /// **'Failed to rename account.'** + String get editAccountRenameFailed; + + /// App bar title on account menu screen + /// + /// In en, this message translates to: + /// **'Accounts'** + String get accountMenuTitle; + + /// Account name menu row label + /// + /// In en, this message translates to: + /// **'Account Name'** + String get accountMenuAccountName; + + /// Address details menu row label + /// + /// In en, this message translates to: + /// **'Address Details'** + String get accountMenuAddressDetails; + + /// Show recovery phrase menu row label + /// + /// In en, this message translates to: + /// **'Show Recovery Phrase'** + String get accountMenuShowRecoveryPhrase; + + /// When account is not found on menu screen + /// + /// In en, this message translates to: + /// **'Account not found'** + String get accountMenuNotFound; + + /// App bar title on account details screen + /// + /// In en, this message translates to: + /// **'Address Details'** + String get accountDetailsTitle; + + /// Title when adding a new hardware wallet + /// + /// In en, this message translates to: + /// **'Add Hardware Wallet'** + String get addHardwareAccountAddWallet; + + /// Title when adding a hardware account to existing wallet + /// + /// In en, this message translates to: + /// **'Add Hardware Account'** + String get addHardwareAccountAddAccount; + + /// Name field label + /// + /// In en, this message translates to: + /// **'NAME'** + String get addHardwareAccountNameLabel; + + /// Name field hint for new hardware wallet + /// + /// In en, this message translates to: + /// **'Hardware Wallet'** + String get addHardwareAccountNameHintWallet; + + /// Name field hint for hardware account + /// + /// In en, this message translates to: + /// **'Account'** + String get addHardwareAccountNameHintAccount; + + /// Address field label + /// + /// In en, this message translates to: + /// **'ADDRESS'** + String get addHardwareAccountAddressLabel; + + /// Address field hint + /// + /// In en, this message translates to: + /// **'SS58 address'** + String get addHardwareAccountAddressHint; + + /// Scan QR code button + /// + /// In en, this message translates to: + /// **'Scan QR Code'** + String get addHardwareAccountScanQr; + + /// Debug fill button + /// + /// In en, this message translates to: + /// **'Debug Fill'** + String get addHardwareAccountDebugFill; + + /// Validation when name is empty + /// + /// In en, this message translates to: + /// **'Name is required'** + String get addHardwareAccountNameRequired; + + /// Validation when address is invalid + /// + /// In en, this message translates to: + /// **'Invalid address'** + String get addHardwareAccountInvalidAddress; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 6be82648..68a3589f 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -154,4 +154,125 @@ class AppLocalizationsEn extends AppLocalizations { @override String get homeActivityEmptyMessage => 'Your activity will appear here once you send or receive QUAN.'; + + @override + String get accountsSheetTitle => 'Accounts'; + + @override + String get accountsSheetFailedLoadAccounts => 'Failed to load accounts.'; + + @override + String get accountsSheetFailedLoadActiveAccount => 'Failed to load active account.'; + + @override + String get accountsSheetNoAccountsFound => 'No accounts found.'; + + @override + String get accountsSheetAddAccount => 'Add Account'; + + @override + String get accountsSheetLoading => 'Loading...'; + + @override + String get accountsSheetBalanceUnavailable => 'Balance unavailable'; + + @override + String accountsSheetBalance(String balance, String symbol) { + return '$balance $symbol'; + } + + @override + String get addAccountMenuTitle => 'Add Account'; + + @override + String get addAccountMenuCreateTitle => 'Create New Account'; + + @override + String get addAccountMenuCreateSubtitle => 'Generate a fresh wallet address'; + + @override + String get addAccountMenuImportTitle => 'Import Wallet'; + + @override + String get addAccountMenuImportSubtitle => 'Use a recovery phrase to import'; + + @override + String get createAccountAppBarTitle => 'Account Name'; + + @override + String get createAccountSubtitle => 'Give this account a name you\'ll recognize. You can change it anytime.'; + + @override + String get createAccountButton => 'Create'; + + @override + String get createAccountErrorCouldNotAdd => 'Could not add account.'; + + @override + String createAccountDefaultName(int number) { + return 'Account $number'; + } + + @override + String get editAccountAppBarTitle => 'Account Name'; + + @override + String get editAccountDone => 'Done'; + + @override + String get editAccountNameEmpty => 'Account name can\'t be empty'; + + @override + String get editAccountRenameFailed => 'Failed to rename account.'; + + @override + String get accountMenuTitle => 'Accounts'; + + @override + String get accountMenuAccountName => 'Account Name'; + + @override + String get accountMenuAddressDetails => 'Address Details'; + + @override + String get accountMenuShowRecoveryPhrase => 'Show Recovery Phrase'; + + @override + String get accountMenuNotFound => 'Account not found'; + + @override + String get accountDetailsTitle => 'Address Details'; + + @override + String get addHardwareAccountAddWallet => 'Add Hardware Wallet'; + + @override + String get addHardwareAccountAddAccount => 'Add Hardware Account'; + + @override + String get addHardwareAccountNameLabel => 'NAME'; + + @override + String get addHardwareAccountNameHintWallet => 'Hardware Wallet'; + + @override + String get addHardwareAccountNameHintAccount => 'Account'; + + @override + String get addHardwareAccountAddressLabel => 'ADDRESS'; + + @override + String get addHardwareAccountAddressHint => 'SS58 address'; + + @override + String get addHardwareAccountScanQr => 'Scan QR Code'; + + @override + String get addHardwareAccountDebugFill => 'Debug Fill'; + + @override + String get addHardwareAccountNameRequired => 'Name is required'; + + @override + String get addHardwareAccountInvalidAddress => 'Invalid address'; } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index dd44ffac..0a7cc15c 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -114,7 +114,7 @@ class AppLocalizationsId extends AppLocalizations { @override String homeError(String error) { - return 'Error: $error'; + return 'Eror: $error'; } @override @@ -155,4 +155,125 @@ class AppLocalizationsId extends AppLocalizations { @override String get homeActivityEmptyMessage => 'Aktivitas Anda akan muncul di sini setelah Anda mengirim atau menerima QUAN.'; + + @override + String get accountsSheetTitle => 'Akun'; + + @override + String get accountsSheetFailedLoadAccounts => 'Gagal memuat akun.'; + + @override + String get accountsSheetFailedLoadActiveAccount => 'Gagal memuat akun aktif.'; + + @override + String get accountsSheetNoAccountsFound => 'Tidak ada akun ditemukan.'; + + @override + String get accountsSheetAddAccount => 'Tambah Akun'; + + @override + String get accountsSheetLoading => 'Memuat...'; + + @override + String get accountsSheetBalanceUnavailable => 'Saldo tidak tersedia'; + + @override + String accountsSheetBalance(String balance, String symbol) { + return '$balance $symbol'; + } + + @override + String get addAccountMenuTitle => 'Tambah Akun'; + + @override + String get addAccountMenuCreateTitle => 'Buat Akun Baru'; + + @override + String get addAccountMenuCreateSubtitle => 'Buat alamat wallet baru'; + + @override + String get addAccountMenuImportTitle => 'Impor Wallet'; + + @override + String get addAccountMenuImportSubtitle => 'Gunakan recovery phrase untuk mengimpor'; + + @override + String get createAccountAppBarTitle => 'Nama Akun'; + + @override + String get createAccountSubtitle => 'Berikan nama yang mudah Anda kenali. Anda bisa mengubahnya kapan saja.'; + + @override + String get createAccountButton => 'Buat'; + + @override + String get createAccountErrorCouldNotAdd => 'Gagal menambahkan akun.'; + + @override + String createAccountDefaultName(int number) { + return 'Akun $number'; + } + + @override + String get editAccountAppBarTitle => 'Nama Akun'; + + @override + String get editAccountDone => 'Selesai'; + + @override + String get editAccountNameEmpty => 'Nama akun tidak boleh kosong'; + + @override + String get editAccountRenameFailed => 'Gagal mengganti nama akun.'; + + @override + String get accountMenuTitle => 'Akun'; + + @override + String get accountMenuAccountName => 'Nama Akun'; + + @override + String get accountMenuAddressDetails => 'Detail Alamat'; + + @override + String get accountMenuShowRecoveryPhrase => 'Tampilkan Recovery Phrase'; + + @override + String get accountMenuNotFound => 'Akun tidak ditemukan'; + + @override + String get accountDetailsTitle => 'Detail Alamat'; + + @override + String get addHardwareAccountAddWallet => 'Tambah Hardware Wallet'; + + @override + String get addHardwareAccountAddAccount => 'Tambah Akun Hardware'; + + @override + String get addHardwareAccountNameLabel => 'NAMA'; + + @override + String get addHardwareAccountNameHintWallet => 'Hardware Wallet'; + + @override + String get addHardwareAccountNameHintAccount => 'Akun'; + + @override + String get addHardwareAccountAddressLabel => 'ALAMAT'; + + @override + String get addHardwareAccountAddressHint => 'Alamat SS58'; + + @override + String get addHardwareAccountScanQr => 'Pindai Kode QR'; + + @override + String get addHardwareAccountDebugFill => 'Isi Debug'; + + @override + String get addHardwareAccountNameRequired => 'Nama wajib diisi'; + + @override + String get addHardwareAccountInvalidAddress => 'Alamat tidak valid'; } diff --git a/mobile-app/lib/v2/screens/accounts/account_details_screen.dart b/mobile-app/lib/v2/screens/accounts/account_details_screen.dart index cd9b75e7..59653f69 100644 --- a/mobile-app/lib/v2/screens/accounts/account_details_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/account_details_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; import 'package:resonance_network_wallet/v2/components/address_details_card.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; @@ -7,16 +9,16 @@ import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_cont import 'package:resonance_network_wallet/v2/components/share_account_button.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; -class AccountDetailsScreen extends StatefulWidget { +class AccountDetailsScreen extends ConsumerStatefulWidget { final Account account; const AccountDetailsScreen({super.key, required this.account}); @override - State createState() => _AccountDetailsScreenState(); + ConsumerState createState() => _AccountDetailsScreenState(); } -class _AccountDetailsScreenState extends State { +class _AccountDetailsScreenState extends ConsumerState { final _checksumService = HumanReadableChecksumService(); String? _checksum; bool _isLoading = true; @@ -45,9 +47,11 @@ class _AccountDetailsScreenState extends State { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final account = widget.account; + return ScaffoldBase( - appBar: const V2AppBar(title: 'Address Details'), + appBar: V2AppBar(title: l10n.accountDetailsTitle), mainContent: AddressDetailsCard(accountId: account.accountId, checksum: _checksum), bottomContent: ScaffoldBaseBottomContent( child: ShareAccountButton(onTap: _share, isDisabled: _isLoading), diff --git a/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart b/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart index bb155345..44729c0c 100644 --- a/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/account_badge.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -18,6 +19,7 @@ class AccountMenuScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; @@ -25,7 +27,7 @@ class AccountMenuScreen extends ConsumerWidget { final account = accounts.value?.firstWhere((a) => a.accountId == initialAccount.accountId); return ScaffoldBase( - appBar: const V2AppBar(title: 'Accounts'), + appBar: V2AppBar(title: l10n.accountMenuTitle), mainContent: account != null ? Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -34,17 +36,17 @@ class AccountMenuScreen extends ConsumerWidget { _ProfileHeader(account: account, colors: colors, text: text), const SizedBox(height: 80), _MenuRow( - label: 'Account Name', + label: l10n.accountMenuAccountName, value: account.name, onTap: () => _openNameEditor(context, ref, account), ), Divider(color: colors.toasterBackground, height: 1), - _MenuRow(label: 'Address Details', onTap: () => _openAddressDetails(context, account)), + _MenuRow(label: l10n.accountMenuAddressDetails, onTap: () => _openAddressDetails(context, account)), Divider(color: colors.toasterBackground, height: 1), - _MenuRow(label: 'Show Recovery Phrase', onTap: () => _openRecoveryPhrase(context, account)), + _MenuRow(label: l10n.accountMenuShowRecoveryPhrase, onTap: () => _openRecoveryPhrase(context, account)), ], ) - : const Center(child: Text('Account not found')), + : Center(child: Text(l10n.accountMenuNotFound)), ); } diff --git a/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart b/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart index a838dca8..60c44638 100644 --- a/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart +++ b/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart @@ -7,7 +7,9 @@ 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/theme/app_colors.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/screens/accounts/account_menu_screen.dart'; @@ -56,6 +58,7 @@ class _AccountsScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final accountsAsync = ref.watch(accountsProvider); final activeDisplayAccountAsync = ref.watch(activeAccountProvider); @@ -69,9 +72,10 @@ class _AccountsScreenState extends ConsumerState { final sheetHeight = math.min(610.0, maxHeight); return BottomSheetContainer( - title: 'Accounts', + title: l10n.accountsSheetTitle, height: sheetHeight, child: _buildContent( + l10n: l10n, accountsAsync: accountsAsync, activeDisplayAccountAsync: activeDisplayAccountAsync, displayAccounts: displayAccounts, @@ -81,6 +85,7 @@ class _AccountsScreenState extends ConsumerState { } Widget _buildContent({ + required AppLocalizations l10n, required AsyncValue> accountsAsync, required AsyncValue activeDisplayAccountAsync, required List displayAccounts, @@ -93,7 +98,7 @@ class _AccountsScreenState extends ConsumerState { if (accountsAsync.hasError) { return Center( child: Text( - 'Failed to load accounts.', + l10n.accountsSheetFailedLoadAccounts, style: context.themeText.smallParagraph?.copyWith(color: context.colors.textSecondary), ), ); @@ -102,16 +107,16 @@ class _AccountsScreenState extends ConsumerState { if (activeDisplayAccountAsync.hasError) { return Center( child: Text( - 'Failed to load active account.', + l10n.accountsSheetFailedLoadActiveAccount, style: context.themeText.smallParagraph?.copyWith(color: context.colors.textSecondary), ), ); } - return _buildAccountsListView(displayAccounts, activeAccountId); + return _buildAccountsListView(l10n, displayAccounts, activeAccountId); } - Widget _buildAccountsListView(List displayAccounts, String? activeAccountId) { + Widget _buildAccountsListView(AppLocalizations l10n, List displayAccounts, String? activeAccountId) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -119,7 +124,7 @@ class _AccountsScreenState extends ConsumerState { child: displayAccounts.isEmpty ? Center( child: Text( - 'No accounts found.', + l10n.accountsSheetNoAccountsFound, style: context.themeText.smallParagraph?.copyWith(color: context.colors.textSecondary), ), ) @@ -130,23 +135,26 @@ class _AccountsScreenState extends ConsumerState { final account = displayAccounts[index]; final isActive = account.accountId == activeAccountId; - return _buildAccountRow(account, isActive); + return _buildAccountRow(l10n, account, isActive); }, ), ), const SizedBox(height: 24), - QuantusButton.simple(label: 'Add Account', onTap: _openAddAccountMenu, variant: ButtonVariant.primary), + QuantusButton.simple(label: l10n.accountsSheetAddAccount, onTap: _openAddAccountMenu, variant: ButtonVariant.primary), ], ); } - Widget _buildAccountRow(Account account, bool isActive) { + Widget _buildAccountRow(AppLocalizations l10n, Account account, bool isActive) { final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); final formattingService = ref.watch(numberFormattingServiceProvider); final balanceText = balanceAsync.when( - loading: () => 'Loading...', - error: (_, _) => 'Balance unavailable', - data: (balance) => '${formattingService.formatBalance(balance)} ${AppConstants.tokenSymbol}', + loading: () => l10n.accountsSheetLoading, + error: (_, _) => l10n.accountsSheetBalanceUnavailable, + data: (balance) => l10n.accountsSheetBalance( + formattingService.formatBalance(balance), + AppConstants.tokenSymbol, + ), ); final colors = context.colors; diff --git a/mobile-app/lib/v2/screens/accounts/add_account_menu_screen.dart b/mobile-app/lib/v2/screens/accounts/add_account_menu_screen.dart index cc5ee035..5accf86e 100644 --- a/mobile-app/lib/v2/screens/accounts/add_account_menu_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/add_account_menu_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.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'; @@ -31,17 +32,18 @@ class _AddAccountMenuScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; return ScaffoldBase( - appBar: const V2AppBar(title: 'Add Account'), + appBar: V2AppBar(title: l10n.addAccountMenuTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _AddMenuRow( icon: Icons.add, - title: 'Create New Account', - subtitle: 'Generate a fresh wallet address', + title: l10n.addAccountMenuCreateTitle, + subtitle: l10n.addAccountMenuCreateSubtitle, onTap: _onCreateNewAccount, colors: colors, text: context.themeText, @@ -51,8 +53,8 @@ class _AddAccountMenuScreenState extends ConsumerState { const SizedBox(height: 24), _AddMenuRow( icon: Icons.save_alt, - title: 'Import Wallet', - subtitle: 'Use a recovery phrase to import', + title: l10n.addAccountMenuImportTitle, + subtitle: l10n.addAccountMenuImportSubtitle, onTap: _onImportWallet, colors: colors, text: context.themeText, diff --git a/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart b/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart index 42539d4a..83fe33c1 100644 --- a/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart @@ -9,6 +9,7 @@ import 'package:resonance_network_wallet/v2/components/qr_scanner_page.dart'; import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; class AddHardwareAccountScreen extends ConsumerStatefulWidget { @@ -52,12 +53,13 @@ class _AddHardwareAccountScreenState extends ConsumerState _error = 'Name is required'); + setState(() => _error = l10n.addHardwareAccountNameRequired); return; } if (!_substrateService.isValidSS58Address(address)) { - setState(() => _error = 'Invalid address'); + setState(() => _error = l10n.addHardwareAccountInvalidAddress); return; } @@ -95,7 +97,9 @@ class _AddHardwareAccountScreenState extends ConsumerState _error = null); }, @@ -115,8 +119,8 @@ class _AddHardwareAccountScreenState extends ConsumerState _error = null); }, @@ -126,7 +130,7 @@ class _AddHardwareAccountScreenState extends ConsumerState { } } catch (_) { if (mounted) { - context.showErrorToaster(message: 'Could not add account.'); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.createAccountErrorCouldNotAdd); } } finally { if (mounted) { @@ -107,7 +109,8 @@ class _CreateAccountScreenState extends ConsumerState { _accounts = ref.read(accountsProvider).value ?? []; _walletIndex = _walletIndexForActiveAccount(_accounts, activeAccount); - _accountName.text = 'Account ${_accounts.length + 1}'; + final l10n = ref.read(l10nProvider); + _accountName.text = l10n.createAccountDefaultName(_accounts.length + 1); _accountName.addListener(() => setState(() {})); } @@ -119,16 +122,18 @@ class _CreateAccountScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + return ScaffoldBase( - appBar: const V2AppBar(title: 'Account Name'), + appBar: V2AppBar(title: l10n.createAccountAppBarTitle), mainContent: NameField( controller: _accountName, - subtitle: "Give this account a name you'll recognize. You can change it anytime.", + subtitle: l10n.createAccountSubtitle, error: _error, ), bottomContent: ScaffoldBaseBottomContent( child: QuantusButton.simple( - label: 'Create', + label: l10n.createAccountButton, onTap: _createAccount, isLoading: _isLoading, isDisabled: _isDisabled, diff --git a/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart b/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart index b59fc54f..6c8e45f9 100644 --- a/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/v2/components/name_field.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -39,7 +40,8 @@ class EditAccountScreenState extends ConsumerState { Future _save() async { final name = _controller.text.trim(); if (name.isEmpty) { - context.showErrorToaster(message: "Account name can't be empty"); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.editAccountNameEmpty); return; } if (name == widget.initialAccount.name) { @@ -56,7 +58,8 @@ class EditAccountScreenState extends ConsumerState { } } catch (_) { if (mounted) { - context.showErrorToaster(message: 'Failed to rename account.'); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.editAccountRenameFailed); } } finally { if (mounted) setState(() => _saving = false); @@ -65,14 +68,16 @@ class EditAccountScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + return ScaffoldBase( - appBar: const V2AppBar(title: 'Account Name'), + appBar: V2AppBar(title: l10n.editAccountAppBarTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [NameField(controller: _controller)], ), bottomContent: ScaffoldBaseBottomContent( - child: QuantusButton.simple(variant: ButtonVariant.primary, label: 'Done', onTap: _save, isLoading: _saving), + child: QuantusButton.simple(variant: ButtonVariant.primary, label: l10n.editAccountDone, onTap: _save, isLoading: _saving), ), ); } From c46df933f9511f5db2891c0f8b092c3ec8193517 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 16:47:12 +0800 Subject: [PATCH 06/25] feat: localized send flow --- mobile-app/lib/l10n/app_en.arb | 184 +++++++++++++++ mobile-app/lib/l10n/app_id.arb | 44 +++- mobile-app/lib/l10n/app_localizations.dart | 216 ++++++++++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 118 ++++++++++ mobile-app/lib/l10n/app_localizations_id.dart | 118 ++++++++++ .../v2/screens/send/input_amount_screen.dart | 34 ++- .../v2/screens/send/review_send_screen.dart | 51 +++-- .../screens/send/select_recipient_screen.dart | 29 +-- .../v2/screens/send/send_screen_logic.dart | 16 +- .../v2/screens/send/tx_submitted_screen.dart | 20 +- .../test/unit/send_screen_logic_test.dart | 22 +- 11 files changed, 785 insertions(+), 67 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 8dc9fbe9..7cf40d12 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -381,5 +381,189 @@ "addHardwareAccountInvalidAddress": "Invalid address", "@addHardwareAccountInvalidAddress": { "description": "Validation when address is invalid" + }, + + "sendTitle": "Send", + "@sendTitle": { + "description": "Send flow app bar title" + }, + "sendPayTitle": "Pay", + "@sendPayTitle": { + "description": "Pay flow app bar title" + }, + "sendEnterAddress": "Enter Address", + "@sendEnterAddress": { + "description": "Button label when recipient address is missing" + }, + + "sendSelectRecipientSendTo": "Send To", + "@sendSelectRecipientSendTo": { + "description": "Section label on select recipient screen" + }, + "sendSelectRecipientSearchHint": "Search {symbol} Address", + "@sendSelectRecipientSearchHint": { + "description": "Hint for recipient search field", + "placeholders": { + "symbol": { + "type": "String" + } + } + }, + "sendSelectRecipientScanTitle": "Scan QR code", + "@sendSelectRecipientScanTitle": { + "description": "Scan QR row title" + }, + "sendSelectRecipientScanSubtitle": "Tap to scan a {symbol} Address", + "@sendSelectRecipientScanSubtitle": { + "description": "Scan QR row subtitle", + "placeholders": { + "symbol": { + "type": "String" + } + } + }, + "sendSelectRecipientRecents": "Recents", + "@sendSelectRecipientRecents": { + "description": "Recents section title" + }, + "sendSelectRecipientContinue": "Continue", + "@sendSelectRecipientContinue": { + "description": "Continue button on select recipient screen" + }, + + "sendInputAmountSendTo": "SEND TO", + "@sendInputAmountSendTo": { + "description": "Recipient card label on input amount screen" + }, + "sendInputAmountAvailableBalance": "Available Balance:", + "@sendInputAmountAvailableBalance": { + "description": "Available balance label" + }, + "sendInputAmountNetworkFee": "Network Fee:", + "@sendInputAmountNetworkFee": { + "description": "Network fee label" + }, + "sendInputAmountMax": "Max", + "@sendInputAmountMax": { + "description": "Max amount button" + }, + "sendInputAmountInvalidAmount": "Please enter a valid amount", + "@sendInputAmountInvalidAmount": { + "description": "Error when amount input is invalid" + }, + "sendInputAmountChecksumRequired": "Recipient checksum is required", + "@sendInputAmountChecksumRequired": { + "description": "Error when recipient checksum is missing" + }, + "sendInputAmountBalance": "{balance} {symbol}", + "@sendInputAmountBalance": { + "description": "Formatted balance with token symbol", + "placeholders": { + "balance": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } + }, + + "sendReviewSending": "SENDING", + "@sendReviewSending": { + "description": "Sending section label on review screen" + }, + "sendReviewTo": "TO", + "@sendReviewTo": { + "description": "To section label on review screen" + }, + "sendReviewAmount": "AMOUNT", + "@sendReviewAmount": { + "description": "Amount row label on review screen" + }, + "sendReviewNetworkFee": "NETWORK FEE", + "@sendReviewNetworkFee": { + "description": "Network fee row label on review screen" + }, + "sendReviewYouPay": "YOU PAY", + "@sendReviewYouPay": { + "description": "Total you pay row label on review screen" + }, + "sendReviewConfirm": "Confirm", + "@sendReviewConfirm": { + "description": "Confirm button on review screen" + }, + "sendReviewAuthReason": "Authenticate to confirm transaction", + "@sendReviewAuthReason": { + "description": "Biometric auth prompt on review screen" + }, + "sendReviewAuthRequired": "Authentication required to send", + "@sendReviewAuthRequired": { + "description": "Error when auth fails on review screen" + }, + "sendReviewSubmitFailed": "Failed submitting transaction", + "@sendReviewSubmitFailed": { + "description": "Error when transaction submission fails" + }, + + "sendTxSubmittedHeadlinePaid": "{amount} {symbol} paid", + "@sendTxSubmittedHeadlinePaid": { + "description": "Success headline when payment completed", + "placeholders": { + "amount": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } + }, + "sendTxSubmittedHeadlineSent": "{amount} {symbol} sent", + "@sendTxSubmittedHeadlineSent": { + "description": "Success headline when send completed", + "placeholders": { + "amount": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } + }, + "sendTxSubmittedOnItsWay": "On its way", + "@sendTxSubmittedOnItsWay": { + "description": "Subtitle on transaction submitted screen" + }, + "sendTxSubmittedToLabel": "To", + "@sendTxSubmittedToLabel": { + "description": "Recipient label on transaction submitted screen" + }, + "sendTxSubmittedDone": "Done", + "@sendTxSubmittedDone": { + "description": "Done button on transaction submitted screen" + }, + + "sendLogicCantSelfTransfer": "Can't Self Transfer", + "@sendLogicCantSelfTransfer": { + "description": "Button label when sending to own address" + }, + "sendLogicEnterAmount": "Enter Amount", + "@sendLogicEnterAmount": { + "description": "Button label when amount is zero" + }, + "sendLogicInvalidAmount": "Invalid Amount", + "@sendLogicInvalidAmount": { + "description": "Button label when amount is negative" + }, + "sendLogicBelowExistentialDeposit": "Below Existential Deposit", + "@sendLogicBelowExistentialDeposit": { + "description": "Button label when amount is below existential deposit" + }, + "sendLogicInsufficientBalance": "Insufficient Balance", + "@sendLogicInsufficientBalance": { + "description": "Button label when balance is insufficient" + }, + "sendLogicReviewSend": "Review Send", + "@sendLogicReviewSend": { + "description": "Button label to proceed to review" } } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index b86a39b3..b1aa8c72 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -98,5 +98,47 @@ "addHardwareAccountScanQr": "Pindai Kode QR", "addHardwareAccountDebugFill": "Isi Debug", "addHardwareAccountNameRequired": "Nama wajib diisi", - "addHardwareAccountInvalidAddress": "Alamat tidak valid" + "addHardwareAccountInvalidAddress": "Alamat tidak valid", + + "sendTitle": "Kirim", + "sendPayTitle": "Bayar", + "sendEnterAddress": "Masukkan Alamat", + + "sendSelectRecipientSendTo": "Kirim Ke", + "sendSelectRecipientSearchHint": "Cari Alamat {symbol}", + "sendSelectRecipientScanTitle": "Pindai kode QR", + "sendSelectRecipientScanSubtitle": "Ketuk untuk memindai Alamat {symbol}", + "sendSelectRecipientRecents": "Terbaru", + "sendSelectRecipientContinue": "Lanjutkan", + + "sendInputAmountSendTo": "KIRIM KE", + "sendInputAmountAvailableBalance": "Saldo Tersedia:", + "sendInputAmountNetworkFee": "Biaya Jaringan:", + "sendInputAmountMax": "Maks", + "sendInputAmountInvalidAmount": "Masukkan jumlah yang valid", + "sendInputAmountChecksumRequired": "Checksum penerima diperlukan", + "sendInputAmountBalance": "{balance} {symbol}", + + "sendReviewSending": "MENGIRIM", + "sendReviewTo": "KE", + "sendReviewAmount": "JUMLAH", + "sendReviewNetworkFee": "BIAYA JARINGAN", + "sendReviewYouPay": "ANDA BAYAR", + "sendReviewConfirm": "Konfirmasi", + "sendReviewAuthReason": "Autentikasi untuk mengonfirmasi transaksi", + "sendReviewAuthRequired": "Autentikasi diperlukan untuk mengirim", + "sendReviewSubmitFailed": "Gagal mengirim transaksi", + + "sendTxSubmittedHeadlinePaid": "{amount} {symbol} dibayar", + "sendTxSubmittedHeadlineSent": "{amount} {symbol} terkirim", + "sendTxSubmittedOnItsWay": "Sedang dalam perjalanan", + "sendTxSubmittedToLabel": "Ke", + "sendTxSubmittedDone": "Selesai", + + "sendLogicCantSelfTransfer": "Tidak Bisa Transfer ke Diri Sendiri", + "sendLogicEnterAmount": "Masukkan Jumlah", + "sendLogicInvalidAmount": "Jumlah Tidak Valid", + "sendLogicBelowExistentialDeposit": "Di Bawah Deposit Eksistensial", + "sendLogicInsufficientBalance": "Saldo Tidak Cukup", + "sendLogicReviewSend": "Tinjau Pengiriman" } diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index df884286..03908f70 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -601,6 +601,222 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Invalid address'** String get addHardwareAccountInvalidAddress; + + /// Send flow app bar title + /// + /// In en, this message translates to: + /// **'Send'** + String get sendTitle; + + /// Pay flow app bar title + /// + /// In en, this message translates to: + /// **'Pay'** + String get sendPayTitle; + + /// Button label when recipient address is missing + /// + /// In en, this message translates to: + /// **'Enter Address'** + String get sendEnterAddress; + + /// Section label on select recipient screen + /// + /// In en, this message translates to: + /// **'Send To'** + String get sendSelectRecipientSendTo; + + /// Hint for recipient search field + /// + /// In en, this message translates to: + /// **'Search {symbol} Address'** + String sendSelectRecipientSearchHint(String symbol); + + /// Scan QR row title + /// + /// In en, this message translates to: + /// **'Scan QR code'** + String get sendSelectRecipientScanTitle; + + /// Scan QR row subtitle + /// + /// In en, this message translates to: + /// **'Tap to scan a {symbol} Address'** + String sendSelectRecipientScanSubtitle(String symbol); + + /// Recents section title + /// + /// In en, this message translates to: + /// **'Recents'** + String get sendSelectRecipientRecents; + + /// Continue button on select recipient screen + /// + /// In en, this message translates to: + /// **'Continue'** + String get sendSelectRecipientContinue; + + /// Recipient card label on input amount screen + /// + /// In en, this message translates to: + /// **'SEND TO'** + String get sendInputAmountSendTo; + + /// Available balance label + /// + /// In en, this message translates to: + /// **'Available Balance:'** + String get sendInputAmountAvailableBalance; + + /// Network fee label + /// + /// In en, this message translates to: + /// **'Network Fee:'** + String get sendInputAmountNetworkFee; + + /// Max amount button + /// + /// In en, this message translates to: + /// **'Max'** + String get sendInputAmountMax; + + /// Error when amount input is invalid + /// + /// In en, this message translates to: + /// **'Please enter a valid amount'** + String get sendInputAmountInvalidAmount; + + /// Error when recipient checksum is missing + /// + /// In en, this message translates to: + /// **'Recipient checksum is required'** + String get sendInputAmountChecksumRequired; + + /// Formatted balance with token symbol + /// + /// In en, this message translates to: + /// **'{balance} {symbol}'** + String sendInputAmountBalance(String balance, String symbol); + + /// Sending section label on review screen + /// + /// In en, this message translates to: + /// **'SENDING'** + String get sendReviewSending; + + /// To section label on review screen + /// + /// In en, this message translates to: + /// **'TO'** + String get sendReviewTo; + + /// Amount row label on review screen + /// + /// In en, this message translates to: + /// **'AMOUNT'** + String get sendReviewAmount; + + /// Network fee row label on review screen + /// + /// In en, this message translates to: + /// **'NETWORK FEE'** + String get sendReviewNetworkFee; + + /// Total you pay row label on review screen + /// + /// In en, this message translates to: + /// **'YOU PAY'** + String get sendReviewYouPay; + + /// Confirm button on review screen + /// + /// In en, this message translates to: + /// **'Confirm'** + String get sendReviewConfirm; + + /// Biometric auth prompt on review screen + /// + /// In en, this message translates to: + /// **'Authenticate to confirm transaction'** + String get sendReviewAuthReason; + + /// Error when auth fails on review screen + /// + /// In en, this message translates to: + /// **'Authentication required to send'** + String get sendReviewAuthRequired; + + /// Error when transaction submission fails + /// + /// In en, this message translates to: + /// **'Failed submitting transaction'** + String get sendReviewSubmitFailed; + + /// Success headline when payment completed + /// + /// In en, this message translates to: + /// **'{amount} {symbol} paid'** + String sendTxSubmittedHeadlinePaid(String amount, String symbol); + + /// Success headline when send completed + /// + /// In en, this message translates to: + /// **'{amount} {symbol} sent'** + String sendTxSubmittedHeadlineSent(String amount, String symbol); + + /// Subtitle on transaction submitted screen + /// + /// In en, this message translates to: + /// **'On its way'** + String get sendTxSubmittedOnItsWay; + + /// Recipient label on transaction submitted screen + /// + /// In en, this message translates to: + /// **'To'** + String get sendTxSubmittedToLabel; + + /// Done button on transaction submitted screen + /// + /// In en, this message translates to: + /// **'Done'** + String get sendTxSubmittedDone; + + /// Button label when sending to own address + /// + /// In en, this message translates to: + /// **'Can\'t Self Transfer'** + String get sendLogicCantSelfTransfer; + + /// Button label when amount is zero + /// + /// In en, this message translates to: + /// **'Enter Amount'** + String get sendLogicEnterAmount; + + /// Button label when amount is negative + /// + /// In en, this message translates to: + /// **'Invalid Amount'** + String get sendLogicInvalidAmount; + + /// Button label when amount is below existential deposit + /// + /// In en, this message translates to: + /// **'Below Existential Deposit'** + String get sendLogicBelowExistentialDeposit; + + /// Button label when balance is insufficient + /// + /// In en, this message translates to: + /// **'Insufficient Balance'** + String get sendLogicInsufficientBalance; + + /// Button label to proceed to review + /// + /// In en, this message translates to: + /// **'Review Send'** + String get sendLogicReviewSend; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 68a3589f..54fecb34 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -275,4 +275,122 @@ class AppLocalizationsEn extends AppLocalizations { @override String get addHardwareAccountInvalidAddress => 'Invalid address'; + + @override + String get sendTitle => 'Send'; + + @override + String get sendPayTitle => 'Pay'; + + @override + String get sendEnterAddress => 'Enter Address'; + + @override + String get sendSelectRecipientSendTo => 'Send To'; + + @override + String sendSelectRecipientSearchHint(String symbol) { + return 'Search $symbol Address'; + } + + @override + String get sendSelectRecipientScanTitle => 'Scan QR code'; + + @override + String sendSelectRecipientScanSubtitle(String symbol) { + return 'Tap to scan a $symbol Address'; + } + + @override + String get sendSelectRecipientRecents => 'Recents'; + + @override + String get sendSelectRecipientContinue => 'Continue'; + + @override + String get sendInputAmountSendTo => 'SEND TO'; + + @override + String get sendInputAmountAvailableBalance => 'Available Balance:'; + + @override + String get sendInputAmountNetworkFee => 'Network Fee:'; + + @override + String get sendInputAmountMax => 'Max'; + + @override + String get sendInputAmountInvalidAmount => 'Please enter a valid amount'; + + @override + String get sendInputAmountChecksumRequired => 'Recipient checksum is required'; + + @override + String sendInputAmountBalance(String balance, String symbol) { + return '$balance $symbol'; + } + + @override + String get sendReviewSending => 'SENDING'; + + @override + String get sendReviewTo => 'TO'; + + @override + String get sendReviewAmount => 'AMOUNT'; + + @override + String get sendReviewNetworkFee => 'NETWORK FEE'; + + @override + String get sendReviewYouPay => 'YOU PAY'; + + @override + String get sendReviewConfirm => 'Confirm'; + + @override + String get sendReviewAuthReason => 'Authenticate to confirm transaction'; + + @override + String get sendReviewAuthRequired => 'Authentication required to send'; + + @override + String get sendReviewSubmitFailed => 'Failed submitting transaction'; + + @override + String sendTxSubmittedHeadlinePaid(String amount, String symbol) { + return '$amount $symbol paid'; + } + + @override + String sendTxSubmittedHeadlineSent(String amount, String symbol) { + return '$amount $symbol sent'; + } + + @override + String get sendTxSubmittedOnItsWay => 'On its way'; + + @override + String get sendTxSubmittedToLabel => 'To'; + + @override + String get sendTxSubmittedDone => 'Done'; + + @override + String get sendLogicCantSelfTransfer => 'Can\'t Self Transfer'; + + @override + String get sendLogicEnterAmount => 'Enter Amount'; + + @override + String get sendLogicInvalidAmount => 'Invalid Amount'; + + @override + String get sendLogicBelowExistentialDeposit => 'Below Existential Deposit'; + + @override + String get sendLogicInsufficientBalance => 'Insufficient Balance'; + + @override + String get sendLogicReviewSend => 'Review Send'; } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index 0a7cc15c..207dc4f9 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -276,4 +276,122 @@ class AppLocalizationsId extends AppLocalizations { @override String get addHardwareAccountInvalidAddress => 'Alamat tidak valid'; + + @override + String get sendTitle => 'Kirim'; + + @override + String get sendPayTitle => 'Bayar'; + + @override + String get sendEnterAddress => 'Masukkan Alamat'; + + @override + String get sendSelectRecipientSendTo => 'Kirim Ke'; + + @override + String sendSelectRecipientSearchHint(String symbol) { + return 'Cari Alamat $symbol'; + } + + @override + String get sendSelectRecipientScanTitle => 'Pindai kode QR'; + + @override + String sendSelectRecipientScanSubtitle(String symbol) { + return 'Ketuk untuk memindai Alamat $symbol'; + } + + @override + String get sendSelectRecipientRecents => 'Terbaru'; + + @override + String get sendSelectRecipientContinue => 'Lanjutkan'; + + @override + String get sendInputAmountSendTo => 'KIRIM KE'; + + @override + String get sendInputAmountAvailableBalance => 'Saldo Tersedia:'; + + @override + String get sendInputAmountNetworkFee => 'Biaya Jaringan:'; + + @override + String get sendInputAmountMax => 'Maks'; + + @override + String get sendInputAmountInvalidAmount => 'Masukkan jumlah yang valid'; + + @override + String get sendInputAmountChecksumRequired => 'Checksum penerima diperlukan'; + + @override + String sendInputAmountBalance(String balance, String symbol) { + return '$balance $symbol'; + } + + @override + String get sendReviewSending => 'MENGIRIM'; + + @override + String get sendReviewTo => 'KE'; + + @override + String get sendReviewAmount => 'JUMLAH'; + + @override + String get sendReviewNetworkFee => 'BIAYA JARINGAN'; + + @override + String get sendReviewYouPay => 'ANDA BAYAR'; + + @override + String get sendReviewConfirm => 'Konfirmasi'; + + @override + String get sendReviewAuthReason => 'Autentikasi untuk mengonfirmasi transaksi'; + + @override + String get sendReviewAuthRequired => 'Autentikasi diperlukan untuk mengirim'; + + @override + String get sendReviewSubmitFailed => 'Gagal mengirim transaksi'; + + @override + String sendTxSubmittedHeadlinePaid(String amount, String symbol) { + return '$amount $symbol dibayar'; + } + + @override + String sendTxSubmittedHeadlineSent(String amount, String symbol) { + return '$amount $symbol terkirim'; + } + + @override + String get sendTxSubmittedOnItsWay => 'Sedang dalam perjalanan'; + + @override + String get sendTxSubmittedToLabel => 'Ke'; + + @override + String get sendTxSubmittedDone => 'Selesai'; + + @override + String get sendLogicCantSelfTransfer => 'Tidak Bisa Transfer ke Diri Sendiri'; + + @override + String get sendLogicEnterAmount => 'Masukkan Jumlah'; + + @override + String get sendLogicInvalidAmount => 'Jumlah Tidak Valid'; + + @override + String get sendLogicBelowExistentialDeposit => 'Di Bawah Deposit Eksistensial'; + + @override + String get sendLogicInsufficientBalance => 'Saldo Tidak Cukup'; + + @override + String get sendLogicReviewSend => 'Tinjau Pengiriman'; } 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 05ca1c8c..2fb793a6 100644 --- a/mobile-app/lib/v2/screens/send/input_amount_screen.dart +++ b/mobile-app/lib/v2/screens/send/input_amount_screen.dart @@ -3,6 +3,8 @@ 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/account_providers.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.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/v2/components/quantus_button.dart'; @@ -126,7 +128,8 @@ class _InputAmountScreenState extends ConsumerState { 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'); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.sendInputAmountInvalidAmount); return; } if (_amount > BigInt.zero) _feeDebouncer.run(_fetchFee); @@ -207,7 +210,8 @@ class _InputAmountScreenState extends ConsumerState { Future _openReview() async { if (_recipientChecksum == null) { - context.showErrorToaster(message: 'Recipient checksum is required'); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.sendInputAmountChecksumRequired); return; } @@ -229,6 +233,7 @@ class _InputAmountScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); ref.watch(activeAccountProvider); final colors = context.colors; final text = context.themeText; @@ -248,6 +253,7 @@ class _InputAmountScreenState extends ConsumerState { activeAccountId: activeId, ); final btnText = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: amountStatus, recipientText: recipient, @@ -257,7 +263,7 @@ class _InputAmountScreenState extends ConsumerState { ); return ScaffoldBase( - appBar: V2AppBar(title: widget.isPayMode ? 'Pay' : 'Send'), + appBar: V2AppBar(title: widget.isPayMode ? l10n.sendPayTitle : l10n.sendTitle), mainContent: LayoutBuilder( builder: (context, constraints) => SingleChildScrollView( controller: _scrollController, @@ -267,7 +273,7 @@ class _InputAmountScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _recipientCard(colors, text), + _recipientCard(colors, text, l10n), const SizedBox(height: 32), _amountCenter(colors, text), const SizedBox(height: 32), @@ -277,11 +283,11 @@ class _InputAmountScreenState extends ConsumerState { ), ), ), - bottomContent: _bottomSection(colors, text, btnText, balance, btnDisabled), + bottomContent: _bottomSection(colors, text, l10n, btnText, balance, btnDisabled), ); } - Widget _recipientCard(AppColorsV2 colors, AppTextTheme text) { + Widget _recipientCard(AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n) { final addr = widget.recipientAddress.trim(); final shortAddr = AddressFormattingService.formatAddress(addr); @@ -295,7 +301,7 @@ class _InputAmountScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('SEND TO', style: context.themeText.receiveLabel?.copyWith(color: colors.textLabel)), + Text(l10n.sendInputAmountSendTo, style: context.themeText.receiveLabel?.copyWith(color: colors.textLabel)), const SizedBox(height: 16), if (_recipientChecksum != null) ...[ Text( @@ -426,6 +432,7 @@ class _InputAmountScreenState extends ConsumerState { Widget _bottomSection( AppColorsV2 colors, AppTextTheme text, + AppLocalizations l10n, String btnText, AsyncValue balance, bool btnDisabled, @@ -448,11 +455,11 @@ class _InputAmountScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Available Balance:', style: text.smallParagraph?.copyWith(color: colors.textTertiary)), + Text(l10n.sendInputAmountAvailableBalance, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), const SizedBox(height: 4), balance.when( data: (b) => Text( - '${formattingService.formatBalance(b)} ${AppConstants.tokenSymbol}', + l10n.sendInputAmountBalance(formattingService.formatBalance(b), AppConstants.tokenSymbol), style: text.smallParagraph?.copyWith(color: colors.textTertiary), ), loading: () => Text('...', style: text.smallParagraph?.copyWith(color: colors.textTertiary)), @@ -465,11 +472,14 @@ class _InputAmountScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text('Network Fee:', style: text.smallParagraph?.copyWith(color: colors.textTertiary)), + Text(l10n.sendInputAmountNetworkFee, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), const SizedBox(height: 4), if (!_isFetchingFee) Text( - '${formattingService.formatBalance(_networkFee, maxDecimals: 5)} ${AppConstants.tokenSymbol}', + l10n.sendInputAmountBalance( + formattingService.formatBalance(_networkFee, maxDecimals: 5), + AppConstants.tokenSymbol, + ), style: text.smallParagraph?.copyWith(color: colors.textTertiary), ) else @@ -482,7 +492,7 @@ class _InputAmountScreenState extends ConsumerState { const SizedBox(height: 4), IntrinsicWidth( child: QuantusButton.simple( - label: 'Max', + label: l10n.sendInputAmountMax, onTap: _setMax, padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), variant: ButtonVariant.transparent, diff --git a/mobile-app/lib/v2/screens/send/review_send_screen.dart b/mobile-app/lib/v2/screens/send/review_send_screen.dart index 4b40fc5f..af259a8e 100644 --- a/mobile-app/lib/v2/screens/send/review_send_screen.dart +++ b/mobile-app/lib/v2/screens/send/review_send_screen.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.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/services/local_auth_service.dart'; @@ -55,11 +57,12 @@ class _ReviewSendScreenState extends ConsumerState { _errorMessage = null; }); - final authed = await LocalAuthService().authenticate(localizedReason: 'Authenticate to confirm transaction'); + final l10n = ref.read(l10nProvider); + final authed = await LocalAuthService().authenticate(localizedReason: l10n.sendReviewAuthReason); if (!authed || !mounted) { setState(() { _submitting = false; - _errorMessage = 'Authentication required to send'; + _errorMessage = l10n.sendReviewAuthRequired; }); return; } @@ -104,7 +107,7 @@ class _ReviewSendScreenState extends ConsumerState { if (mounted) { setState(() { _submitting = false; - _errorMessage = 'Failed submitting transaction'; + _errorMessage = ref.read(l10nProvider).sendReviewSubmitFailed; }); } } @@ -112,6 +115,7 @@ class _ReviewSendScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); ref.watch(activeAccountProvider); final colors = context.colors; final text = context.themeText; @@ -126,16 +130,16 @@ class _ReviewSendScreenState extends ConsumerState { final totalRaw = widget.amount + widget.networkFee; return ScaffoldBase( - appBar: V2AppBar(title: widget.isPayMode ? 'Pay' : 'Send'), + appBar: V2AppBar(title: widget.isPayMode ? l10n.sendPayTitle : l10n.sendTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _heroCard(colors, text, approxDisplay), + _heroCard(colors, text, l10n, approxDisplay), const SizedBox(height: 28), - _summarySection(addr, totalRaw), + _summarySection(l10n, addr, totalRaw), if (_errorMessage != null) ...[ const SizedBox(height: 16), Text(_errorMessage!, style: text.detail?.copyWith(color: colors.textError)), @@ -146,7 +150,7 @@ class _ReviewSendScreenState extends ConsumerState { ), bottomContent: ScaffoldBaseBottomContent( child: QuantusButton.simple( - label: 'Confirm', + label: l10n.sendReviewConfirm, variant: ButtonVariant.primary, isLoading: _submitting, isDisabled: _submitting, @@ -156,14 +160,14 @@ class _ReviewSendScreenState extends ConsumerState { ); } - Widget _heroCard(AppColorsV2 colors, AppTextTheme text, CurrencyDisplayState approxDisplay) { + Widget _heroCard(AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n, CurrencyDisplayState approxDisplay) { final sectionLabelStyle = text.receiveLabel?.copyWith(color: colors.textLabel); return SplitCard( topChild: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('SENDING', style: sectionLabelStyle), + Text(l10n.sendReviewSending, style: sectionLabelStyle), const SizedBox(height: 16), AmountDisplayWithConversion( amountDisplay: approxDisplay, @@ -175,7 +179,7 @@ class _ReviewSendScreenState extends ConsumerState { bottomChild: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('TO', style: sectionLabelStyle), + Text(l10n.sendReviewTo, style: sectionLabelStyle), const SizedBox(height: 16), AddressCheckphraseWithInitial( recipientChecksum: widget.recipientChecksum, @@ -186,7 +190,7 @@ class _ReviewSendScreenState extends ConsumerState { ); } - Widget _summarySection(String addr, BigInt totalRaw) { + Widget _summarySection(AppLocalizations l10n, String addr, BigInt totalRaw) { final shownDecimals = AppConstants.decimals; final shortAddr = AddressFormattingService.formatAddress(addr); final formattingService = ref.watch(numberFormattingServiceProvider); @@ -195,23 +199,30 @@ class _ReviewSendScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SizedBox(height: 7), - _summaryRow(label: 'TO', value: shortAddr), + _summaryRow(label: l10n.sendReviewTo, value: shortAddr), const SizedBox(height: 7), _summaryRow( - label: 'AMOUNT', - value: - '${formattingService.formatBalance(widget.amount, maxDecimals: shownDecimals)} ${AppConstants.tokenSymbol}', + label: l10n.sendReviewAmount, + value: l10n.sendInputAmountBalance( + formattingService.formatBalance(widget.amount, maxDecimals: shownDecimals), + AppConstants.tokenSymbol, + ), ), const SizedBox(height: 7), _summaryRow( - label: 'NETWORK FEE', - value: - '${formattingService.formatBalance(widget.networkFee, maxDecimals: shownDecimals)} ${AppConstants.tokenSymbol}', + label: l10n.sendReviewNetworkFee, + value: l10n.sendInputAmountBalance( + formattingService.formatBalance(widget.networkFee, maxDecimals: shownDecimals), + AppConstants.tokenSymbol, + ), ), const SizedBox(height: 7), _summaryRow( - label: 'YOU PAY', - value: '${formattingService.formatBalance(totalRaw, maxDecimals: shownDecimals)} ${AppConstants.tokenSymbol}', + label: l10n.sendReviewYouPay, + value: l10n.sendInputAmountBalance( + formattingService.formatBalance(totalRaw, maxDecimals: shownDecimals), + AppConstants.tokenSymbol, + ), ), const SizedBox(height: 7), ], diff --git a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart index 47765482..4d62f752 100644 --- a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart +++ b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart @@ -4,6 +4,8 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/dotted_border.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/route_intent_providers.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/v2/components/address_checkphrase_with_initial.dart'; @@ -178,23 +180,24 @@ class _SelectRecipientScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); ref.watch(activeAccountProvider); final colors = context.colors; final text = context.themeText; return ScaffoldBase( - appBar: const V2AppBar(title: 'Send'), + appBar: V2AppBar(title: l10n.sendTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('Send To', style: text.sendSectionLabel?.copyWith(color: colors.textPrimary)), + Text(l10n.sendSelectRecipientSendTo, style: text.sendSectionLabel?.copyWith(color: colors.textPrimary)), const SizedBox(height: 12), - _buildRecipientField(colors, text), + _buildRecipientField(colors, text, l10n), const SizedBox(height: 28), - _buildScanRow(colors, text), + _buildScanRow(colors, text, l10n), const SizedBox(height: 28), DottedBorder( dashLength: 3, @@ -213,7 +216,7 @@ class _SelectRecipientScreenState extends ConsumerState { const SliverFillRemaining(hasScrollBody: false, child: Center(child: Loader())) else if (_recents.isNotEmpty) ...[ SliverToBoxAdapter( - child: Text('Recents', style: text.smallTitle?.copyWith(color: colors.textPrimary)), + child: Text(l10n.sendSelectRecipientRecents, style: text.smallTitle?.copyWith(color: colors.textPrimary)), ), const SliverToBoxAdapter(child: SizedBox(height: 32)), SliverList( @@ -241,11 +244,11 @@ class _SelectRecipientScreenState extends ConsumerState { ), ], ), - bottomContent: _buildBottomButton(), + bottomContent: _buildBottomButton(l10n), ); } - Widget _buildRecipientField(AppColorsV2 colors, AppTextTheme text) { + Widget _buildRecipientField(AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n) { final hasValid = _recipientController.text.trim().isNotEmpty && !_hasAddressError; return SizedBox( @@ -275,7 +278,7 @@ class _SelectRecipientScreenState extends ConsumerState { textCapitalization: TextCapitalization.none, scrollPadding: const EdgeInsets.only(bottom: 120), style: text.smallParagraph?.copyWith(color: colors.textPrimary), - decoration: const InputDecoration(hintText: 'Search ${AppConstants.tokenSymbol} Address'), + decoration: InputDecoration(hintText: l10n.sendSelectRecipientSearchHint(AppConstants.tokenSymbol)), ), ), ], @@ -316,7 +319,7 @@ class _SelectRecipientScreenState extends ConsumerState { ); } - Widget _buildScanRow(AppColorsV2 colors, AppTextTheme text) { + Widget _buildScanRow(AppColorsV2 colors, AppTextTheme text, AppLocalizations l10n) { final iconContainerSize = 44.0; final iconSize = 24.0; @@ -342,10 +345,10 @@ class _SelectRecipientScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Scan QR code', style: text.paragraph?.copyWith(color: colors.textPrimary)), + Text(l10n.sendSelectRecipientScanTitle, style: text.paragraph?.copyWith(color: colors.textPrimary)), const SizedBox(height: 4), Text( - 'Tap to scan a ${AppConstants.tokenSymbol} Address', + l10n.sendSelectRecipientScanSubtitle(AppConstants.tokenSymbol), style: text.detail?.copyWith(color: colors.textTertiary), ), ], @@ -373,8 +376,8 @@ class _SelectRecipientScreenState extends ConsumerState { ); } - Widget _buildBottomButton() { - final btnText = _canContinue ? 'Continue' : 'Enter Address'; + Widget _buildBottomButton(AppLocalizations l10n) { + final btnText = _canContinue ? l10n.sendSelectRecipientContinue : l10n.sendEnterAddress; return ScaffoldBaseBottomContent( child: QuantusButton.simple( diff --git a/mobile-app/lib/v2/screens/send/send_screen_logic.dart b/mobile-app/lib/v2/screens/send/send_screen_logic.dart index 31390e8f..da9a2c59 100644 --- a/mobile-app/lib/v2/screens/send/send_screen_logic.dart +++ b/mobile-app/lib/v2/screens/send/send_screen_logic.dart @@ -1,5 +1,6 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:quantus_sdk/generated/planck/pallets/balances.dart' as balances; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; enum AmountStatus { valid, negative, zero, belowExistential, insufficientBalance } @@ -36,6 +37,7 @@ class SendScreenLogic { } static String getButtonText({ + required AppLocalizations l10n, required bool hasAddressError, required AmountStatus amountStatus, required String recipientText, @@ -43,20 +45,20 @@ class SendScreenLogic { required String activeAccountId, required NumberFormattingService formattingService, }) { - if (hasAddressError || recipientText.isEmpty) return 'Enter Address'; - if (_isSelfTransfer(recipientText, activeAccountId)) return "Can't Self Transfer"; + if (hasAddressError || recipientText.isEmpty) return l10n.sendEnterAddress; + if (_isSelfTransfer(recipientText, activeAccountId)) return l10n.sendLogicCantSelfTransfer; switch (amountStatus) { case AmountStatus.zero: - return 'Enter Amount'; + return l10n.sendLogicEnterAmount; case AmountStatus.negative: - return 'Invalid Amount'; + return l10n.sendLogicInvalidAmount; case AmountStatus.belowExistential: - return 'Below Existential Deposit'; + return l10n.sendLogicBelowExistentialDeposit; case AmountStatus.insufficientBalance: - return 'Insufficient Balance'; + return l10n.sendLogicInsufficientBalance; case AmountStatus.valid: - return 'Review Send'; + return l10n.sendLogicReviewSend; } } diff --git a/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart b/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart index 298b32f1..e3e926f7 100644 --- a/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart +++ b/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/v2/components/back_button.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -29,15 +31,17 @@ class TxSubmittedScreen extends ConsumerWidget { Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const HomeScreen()), (route) => false); } - String _headline(WidgetRef ref) { + String _headline(WidgetRef ref, AppLocalizations l10n) { final formattingService = ref.watch(numberFormattingServiceProvider); final n = formattingService.formatBalance(amount, maxDecimals: 4); - final action = isPayMode ? 'paid' : 'sent'; - return '$n ${AppConstants.tokenSymbol} $action'; + return isPayMode + ? l10n.sendTxSubmittedHeadlinePaid(n, AppConstants.tokenSymbol) + : l10n.sendTxSubmittedHeadlineSent(n, AppConstants.tokenSymbol); } @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final addr = recipientAddress.trim(); @@ -51,7 +55,7 @@ class TxSubmittedScreen extends ConsumerWidget { }, child: ScaffoldBase( appBar: V2AppBar( - title: isPayMode ? 'Pay' : 'Send', + title: isPayMode ? l10n.sendPayTitle : l10n.sendTitle, leading: AppBackButton(onTap: () => _popToHome(context)), ), mainContent: Column( @@ -64,13 +68,13 @@ class TxSubmittedScreen extends ConsumerWidget { _successMark(colors), const SizedBox(height: 32), Text( - _headline(ref), + _headline(ref, l10n), textAlign: TextAlign.center, style: text.largeTitle?.copyWith(fontWeight: FontWeight.w400), ), const SizedBox(height: 4), Text( - 'On its way', + l10n.sendTxSubmittedOnItsWay, textAlign: TextAlign.center, style: text.smallParagraph?.copyWith(color: colors.textTertiary, letterSpacing: 0.74), ), @@ -81,7 +85,7 @@ class TxSubmittedScreen extends ConsumerWidget { style: text.paragraph?.copyWith(color: colors.textPrimary), children: [ TextSpan( - text: 'To', + text: l10n.sendTxSubmittedToLabel, style: text.paragraph?.copyWith(fontWeight: FontWeight.w500), ), TextSpan( @@ -116,7 +120,7 @@ class TxSubmittedScreen extends ConsumerWidget { ], ), bottomContent: ScaffoldBaseBottomContent( - child: QuantusButton.simple(label: 'Done', variant: ButtonVariant.primary, onTap: () => _popToHome(context)), + child: QuantusButton.simple(label: l10n.sendTxSubmittedDone, variant: ButtonVariant.primary, onTap: () => _popToHome(context)), ), ), ); diff --git a/mobile-app/test/unit/send_screen_logic_test.dart b/mobile-app/test/unit/send_screen_logic_test.dart index b4eb3506..7b319cfc 100644 --- a/mobile-app/test/unit/send_screen_logic_test.dart +++ b/mobile-app/test/unit/send_screen_logic_test.dart @@ -1,14 +1,18 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/v2/screens/send/send_screen_logic.dart'; import 'package:quantus_sdk/generated/planck/pallets/balances.dart' as balances; void main() { group('SendScreenLogic', () { late NumberFormattingService formattingService; + late AppLocalizations l10n; setUp(() { formattingService = NumberFormattingService(localeConfig: LocaleNumberConfig.dotDecimal); + l10n = lookupAppLocalizations(const Locale('en')); }); group('getAmountStatus', () { @@ -116,6 +120,7 @@ void main() { group('getButtonText', () { test('returns "Enter Address" when address is empty', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.valid, // Status doesn't matter if address is empty recipientText: '', @@ -123,11 +128,12 @@ void main() { activeAccountId: 'sender_address', formattingService: formattingService, ); - expect(result, equals('Enter Address')); + expect(result, equals(l10n.sendEnterAddress)); }); test('returns "Enter Amount" when status is zeroOrNegative', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.zero, recipientText: 'valid_address', @@ -135,11 +141,12 @@ void main() { activeAccountId: 'sender_address', formattingService: formattingService, ); - expect(result, equals('Enter Amount')); + expect(result, equals(l10n.sendLogicEnterAmount)); }); test('returns "Insufficient Balance" when status is insufficient', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.insufficientBalance, recipientText: 'valid_address', @@ -147,11 +154,12 @@ void main() { activeAccountId: 'sender_address', formattingService: formattingService, ); - expect(result, equals('Insufficient Balance')); + expect(result, equals(l10n.sendLogicInsufficientBalance)); }); test('returns "Can\'t Self Transfer" for same address', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.valid, recipientText: 'same_address', @@ -159,11 +167,12 @@ void main() { activeAccountId: 'same_address', formattingService: formattingService, ); - expect(result, equals("Can't Self Transfer")); + expect(result, equals(l10n.sendLogicCantSelfTransfer)); }); test('returns "Below Existential Deposit" when status matches', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.belowExistential, recipientText: 'valid_address', @@ -171,11 +180,12 @@ void main() { activeAccountId: 'sender_address', formattingService: formattingService, ); - expect(result, equals('Below Existential Deposit')); + expect(result, equals(l10n.sendLogicBelowExistentialDeposit)); }); test('returns Review Send for valid status', () { final result = SendScreenLogic.getButtonText( + l10n: l10n, hasAddressError: false, amountStatus: AmountStatus.valid, recipientText: 'valid_address', @@ -183,7 +193,7 @@ void main() { activeAccountId: 'sender_address', formattingService: formattingService, ); - expect(result, equals('Review Send')); + expect(result, equals(l10n.sendLogicReviewSend)); }); }); From 8a78bf6e760977bd0e3aa347b672a58f0a682561 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 17:59:48 +0800 Subject: [PATCH 07/25] feat: localize activity related screen and widgets --- mobile-app/lib/l10n/app_en.arb | 178 +++++++++++++++ mobile-app/lib/l10n/app_id.arb | 41 +++- mobile-app/lib/l10n/app_localizations.dart | 216 ++++++++++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 118 ++++++++++ mobile-app/lib/l10n/app_localizations_id.dart | 118 ++++++++++ .../v2/screens/activity/activity_screen.dart | 42 ++-- .../activity/transaction_detail_sheet.dart | 53 +++-- .../lib/v2/screens/activity/tx_item.dart | 64 +++--- .../lib/v2/screens/home/activity_section.dart | 3 +- 9 files changed, 768 insertions(+), 65 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 7cf40d12..e9df2843 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -565,5 +565,183 @@ "sendLogicReviewSend": "Review Send", "@sendLogicReviewSend": { "description": "Button label to proceed to review" + }, + + "activityTitle": "Activity", + "@activityTitle": { + "description": "App bar title on activity screen" + }, + "activityError": "Error: {error}", + "@activityError": { + "description": "Error message on activity screen", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "activityNoAccount": "No account", + "@activityNoAccount": { + "description": "Shown when no account is active on activity screen" + }, + "activityEmpty": "No transactions yet", + "@activityEmpty": { + "description": "Empty state on activity screen" + }, + "activityFilterAll": "All", + "@activityFilterAll": { + "description": "Filter button for all transactions" + }, + "activityFilterSend": "Send", + "@activityFilterSend": { + "description": "Filter button for sent transactions" + }, + "activityFilterReceive": "Receive", + "@activityFilterReceive": { + "description": "Filter button for received transactions" + }, + "activityDateToday": "Today", + "@activityDateToday": { + "description": "Date group label for today" + }, + "activityDateYesterday": "Yesterday", + "@activityDateYesterday": { + "description": "Date group label for yesterday" + }, + + "activityTxSending": "Sending", + "@activityTxSending": { + "description": "Transaction row label for pending send" + }, + "activityTxReceiving": "Receiving", + "@activityTxReceiving": { + "description": "Transaction row label for pending or scheduled receive" + }, + "activityTxPending": "Pending", + "@activityTxPending": { + "description": "Transaction row label for scheduled send" + }, + "activityTxSent": "Sent", + "@activityTxSent": { + "description": "Transaction row label for completed send" + }, + "activityTxReceived": "Received", + "@activityTxReceived": { + "description": "Transaction row label for completed receive" + }, + "activityTxTo": "To", + "@activityTxTo": { + "description": "Counterparty direction label for send" + }, + "activityTxFrom": "From", + "@activityTxFrom": { + "description": "Counterparty direction label for receive" + }, + "activityTxTimeNow": "now", + "@activityTxTimeNow": { + "description": "Time label for just now" + }, + "activityTxTimeMinutesAgo": "{minutes}m ago", + "@activityTxTimeMinutesAgo": { + "description": "Time label for minutes ago", + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "activityTxTimeHoursAgo": "{hours}h ago", + "@activityTxTimeHoursAgo": { + "description": "Time label for hours ago", + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "activityTxTimeDaysAgo": "{days}d ago", + "@activityTxTimeDaysAgo": { + "description": "Time label for days ago", + "placeholders": { + "days": { + "type": "int" + } + } + }, + "activityTxTimeRemaining": "{days}d:{hours}h:{minutes}m", + "@activityTxTimeRemaining": { + "description": "Time remaining for scheduled transaction", + "placeholders": { + "days": { + "type": "String" + }, + "hours": { + "type": "String" + }, + "minutes": { + "type": "String" + } + } + }, + + "activityDetailTitleSending": "Sending", + "@activityDetailTitleSending": { + "description": "Detail sheet title for pending send" + }, + "activityDetailTitleScheduled": "Scheduled", + "@activityDetailTitleScheduled": { + "description": "Detail sheet title for scheduled send" + }, + "activityDetailTitleReceiving": "Receiving", + "@activityDetailTitleReceiving": { + "description": "Detail sheet title for receiving" + }, + "activityDetailTitleSent": "Sent", + "@activityDetailTitleSent": { + "description": "Detail sheet title for completed send" + }, + "activityDetailTitleReceived": "Received", + "@activityDetailTitleReceived": { + "description": "Detail sheet title for completed receive" + }, + "activityDetailStatusInProcess": "In Process", + "@activityDetailStatusInProcess": { + "description": "Status label for in-process transaction" + }, + "activityDetailStatusScheduled": "Scheduled", + "@activityDetailStatusScheduled": { + "description": "Status label for scheduled transaction" + }, + "activityDetailStatusCompleted": "Completed", + "@activityDetailStatusCompleted": { + "description": "Status label for completed transaction" + }, + "activityDetailStatus": "STATUS", + "@activityDetailStatus": { + "description": "Status row label on detail sheet" + }, + "activityDetailTo": "TO", + "@activityDetailTo": { + "description": "To row label on detail sheet" + }, + "activityDetailFrom": "FROM", + "@activityDetailFrom": { + "description": "From row label on detail sheet" + }, + "activityDetailDate": "DATE", + "@activityDetailDate": { + "description": "Date row label on detail sheet" + }, + "activityDetailNetworkFee": "NETWORK FEE", + "@activityDetailNetworkFee": { + "description": "Network fee row label on detail sheet" + }, + "activityDetailTxHash": "TX HASH", + "@activityDetailTxHash": { + "description": "Transaction hash row label on detail sheet" + }, + "activityDetailViewExplorer": "View in Explorer ↗", + "@activityDetailViewExplorer": { + "description": "Link to view transaction in explorer" } } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index b1aa8c72..658f04ac 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -140,5 +140,44 @@ "sendLogicInvalidAmount": "Jumlah Tidak Valid", "sendLogicBelowExistentialDeposit": "Di Bawah Deposit Eksistensial", "sendLogicInsufficientBalance": "Saldo Tidak Cukup", - "sendLogicReviewSend": "Tinjau Pengiriman" + "sendLogicReviewSend": "Tinjau Pengiriman", + + "activityTitle": "Aktivitas", + "activityError": "Error: {error}", + "activityNoAccount": "Tidak ada akun", + "activityEmpty": "Belum ada transaksi", + "activityFilterAll": "Semua", + "activityFilterSend": "Kirim", + "activityFilterReceive": "Terima", + "activityDateToday": "Hari Ini", + "activityDateYesterday": "Kemarin", + + "activityTxSending": "Mengirim", + "activityTxReceiving": "Menerima", + "activityTxPending": "Tertunda", + "activityTxSent": "Terkirim", + "activityTxReceived": "Diterima", + "activityTxTo": "Ke", + "activityTxFrom": "Dari", + "activityTxTimeNow": "sekarang", + "activityTxTimeMinutesAgo": "{minutes}m lalu", + "activityTxTimeHoursAgo": "{hours}j lalu", + "activityTxTimeDaysAgo": "{days}h lalu", + "activityTxTimeRemaining": "{days}h:{hours}j:{minutes}m", + + "activityDetailTitleSending": "Mengirim", + "activityDetailTitleScheduled": "Terjadwal", + "activityDetailTitleReceiving": "Menerima", + "activityDetailTitleSent": "Terkirim", + "activityDetailTitleReceived": "Diterima", + "activityDetailStatusInProcess": "Diproses", + "activityDetailStatusScheduled": "Terjadwal", + "activityDetailStatusCompleted": "Selesai", + "activityDetailStatus": "STATUS", + "activityDetailTo": "KE", + "activityDetailFrom": "DARI", + "activityDetailDate": "TANGGAL", + "activityDetailNetworkFee": "BIAYA JARINGAN", + "activityDetailTxHash": "HASH TX", + "activityDetailViewExplorer": "Lihat di Explorer ↗" } diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 03908f70..dd8d8acc 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -817,6 +817,222 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Review Send'** String get sendLogicReviewSend; + + /// App bar title on activity screen + /// + /// In en, this message translates to: + /// **'Activity'** + String get activityTitle; + + /// Error message on activity screen + /// + /// In en, this message translates to: + /// **'Error: {error}'** + String activityError(String error); + + /// Shown when no account is active on activity screen + /// + /// In en, this message translates to: + /// **'No account'** + String get activityNoAccount; + + /// Empty state on activity screen + /// + /// In en, this message translates to: + /// **'No transactions yet'** + String get activityEmpty; + + /// Filter button for all transactions + /// + /// In en, this message translates to: + /// **'All'** + String get activityFilterAll; + + /// Filter button for sent transactions + /// + /// In en, this message translates to: + /// **'Send'** + String get activityFilterSend; + + /// Filter button for received transactions + /// + /// In en, this message translates to: + /// **'Receive'** + String get activityFilterReceive; + + /// Date group label for today + /// + /// In en, this message translates to: + /// **'Today'** + String get activityDateToday; + + /// Date group label for yesterday + /// + /// In en, this message translates to: + /// **'Yesterday'** + String get activityDateYesterday; + + /// Transaction row label for pending send + /// + /// In en, this message translates to: + /// **'Sending'** + String get activityTxSending; + + /// Transaction row label for pending or scheduled receive + /// + /// In en, this message translates to: + /// **'Receiving'** + String get activityTxReceiving; + + /// Transaction row label for scheduled send + /// + /// In en, this message translates to: + /// **'Pending'** + String get activityTxPending; + + /// Transaction row label for completed send + /// + /// In en, this message translates to: + /// **'Sent'** + String get activityTxSent; + + /// Transaction row label for completed receive + /// + /// In en, this message translates to: + /// **'Received'** + String get activityTxReceived; + + /// Counterparty direction label for send + /// + /// In en, this message translates to: + /// **'To'** + String get activityTxTo; + + /// Counterparty direction label for receive + /// + /// In en, this message translates to: + /// **'From'** + String get activityTxFrom; + + /// Time label for just now + /// + /// In en, this message translates to: + /// **'now'** + String get activityTxTimeNow; + + /// Time label for minutes ago + /// + /// In en, this message translates to: + /// **'{minutes}m ago'** + String activityTxTimeMinutesAgo(int minutes); + + /// Time label for hours ago + /// + /// In en, this message translates to: + /// **'{hours}h ago'** + String activityTxTimeHoursAgo(int hours); + + /// Time label for days ago + /// + /// In en, this message translates to: + /// **'{days}d ago'** + String activityTxTimeDaysAgo(int days); + + /// Time remaining for scheduled transaction + /// + /// In en, this message translates to: + /// **'{days}d:{hours}h:{minutes}m'** + String activityTxTimeRemaining(String days, String hours, String minutes); + + /// Detail sheet title for pending send + /// + /// In en, this message translates to: + /// **'Sending'** + String get activityDetailTitleSending; + + /// Detail sheet title for scheduled send + /// + /// In en, this message translates to: + /// **'Scheduled'** + String get activityDetailTitleScheduled; + + /// Detail sheet title for receiving + /// + /// In en, this message translates to: + /// **'Receiving'** + String get activityDetailTitleReceiving; + + /// Detail sheet title for completed send + /// + /// In en, this message translates to: + /// **'Sent'** + String get activityDetailTitleSent; + + /// Detail sheet title for completed receive + /// + /// In en, this message translates to: + /// **'Received'** + String get activityDetailTitleReceived; + + /// Status label for in-process transaction + /// + /// In en, this message translates to: + /// **'In Process'** + String get activityDetailStatusInProcess; + + /// Status label for scheduled transaction + /// + /// In en, this message translates to: + /// **'Scheduled'** + String get activityDetailStatusScheduled; + + /// Status label for completed transaction + /// + /// In en, this message translates to: + /// **'Completed'** + String get activityDetailStatusCompleted; + + /// Status row label on detail sheet + /// + /// In en, this message translates to: + /// **'STATUS'** + String get activityDetailStatus; + + /// To row label on detail sheet + /// + /// In en, this message translates to: + /// **'TO'** + String get activityDetailTo; + + /// From row label on detail sheet + /// + /// In en, this message translates to: + /// **'FROM'** + String get activityDetailFrom; + + /// Date row label on detail sheet + /// + /// In en, this message translates to: + /// **'DATE'** + String get activityDetailDate; + + /// Network fee row label on detail sheet + /// + /// In en, this message translates to: + /// **'NETWORK FEE'** + String get activityDetailNetworkFee; + + /// Transaction hash row label on detail sheet + /// + /// In en, this message translates to: + /// **'TX HASH'** + String get activityDetailTxHash; + + /// Link to view transaction in explorer + /// + /// In en, this message translates to: + /// **'View in Explorer ↗'** + String get activityDetailViewExplorer; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 54fecb34..88c1e416 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -393,4 +393,122 @@ class AppLocalizationsEn extends AppLocalizations { @override String get sendLogicReviewSend => 'Review Send'; + + @override + String get activityTitle => 'Activity'; + + @override + String activityError(String error) { + return 'Error: $error'; + } + + @override + String get activityNoAccount => 'No account'; + + @override + String get activityEmpty => 'No transactions yet'; + + @override + String get activityFilterAll => 'All'; + + @override + String get activityFilterSend => 'Send'; + + @override + String get activityFilterReceive => 'Receive'; + + @override + String get activityDateToday => 'Today'; + + @override + String get activityDateYesterday => 'Yesterday'; + + @override + String get activityTxSending => 'Sending'; + + @override + String get activityTxReceiving => 'Receiving'; + + @override + String get activityTxPending => 'Pending'; + + @override + String get activityTxSent => 'Sent'; + + @override + String get activityTxReceived => 'Received'; + + @override + String get activityTxTo => 'To'; + + @override + String get activityTxFrom => 'From'; + + @override + String get activityTxTimeNow => 'now'; + + @override + String activityTxTimeMinutesAgo(int minutes) { + return '${minutes}m ago'; + } + + @override + String activityTxTimeHoursAgo(int hours) { + return '${hours}h ago'; + } + + @override + String activityTxTimeDaysAgo(int days) { + return '${days}d ago'; + } + + @override + String activityTxTimeRemaining(String days, String hours, String minutes) { + return '${days}d:${hours}h:${minutes}m'; + } + + @override + String get activityDetailTitleSending => 'Sending'; + + @override + String get activityDetailTitleScheduled => 'Scheduled'; + + @override + String get activityDetailTitleReceiving => 'Receiving'; + + @override + String get activityDetailTitleSent => 'Sent'; + + @override + String get activityDetailTitleReceived => 'Received'; + + @override + String get activityDetailStatusInProcess => 'In Process'; + + @override + String get activityDetailStatusScheduled => 'Scheduled'; + + @override + String get activityDetailStatusCompleted => 'Completed'; + + @override + String get activityDetailStatus => 'STATUS'; + + @override + String get activityDetailTo => 'TO'; + + @override + String get activityDetailFrom => 'FROM'; + + @override + String get activityDetailDate => 'DATE'; + + @override + String get activityDetailNetworkFee => 'NETWORK FEE'; + + @override + String get activityDetailTxHash => 'TX HASH'; + + @override + String get activityDetailViewExplorer => 'View in Explorer ↗'; } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index 207dc4f9..7e331428 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -394,4 +394,122 @@ class AppLocalizationsId extends AppLocalizations { @override String get sendLogicReviewSend => 'Tinjau Pengiriman'; + + @override + String get activityTitle => 'Aktivitas'; + + @override + String activityError(String error) { + return 'Error: $error'; + } + + @override + String get activityNoAccount => 'Tidak ada akun'; + + @override + String get activityEmpty => 'Belum ada transaksi'; + + @override + String get activityFilterAll => 'Semua'; + + @override + String get activityFilterSend => 'Kirim'; + + @override + String get activityFilterReceive => 'Terima'; + + @override + String get activityDateToday => 'Hari Ini'; + + @override + String get activityDateYesterday => 'Kemarin'; + + @override + String get activityTxSending => 'Mengirim'; + + @override + String get activityTxReceiving => 'Menerima'; + + @override + String get activityTxPending => 'Tertunda'; + + @override + String get activityTxSent => 'Terkirim'; + + @override + String get activityTxReceived => 'Diterima'; + + @override + String get activityTxTo => 'Ke'; + + @override + String get activityTxFrom => 'Dari'; + + @override + String get activityTxTimeNow => 'sekarang'; + + @override + String activityTxTimeMinutesAgo(int minutes) { + return '${minutes}m lalu'; + } + + @override + String activityTxTimeHoursAgo(int hours) { + return '${hours}j lalu'; + } + + @override + String activityTxTimeDaysAgo(int days) { + return '${days}h lalu'; + } + + @override + String activityTxTimeRemaining(String days, String hours, String minutes) { + return '${days}h:${hours}j:${minutes}m'; + } + + @override + String get activityDetailTitleSending => 'Mengirim'; + + @override + String get activityDetailTitleScheduled => 'Terjadwal'; + + @override + String get activityDetailTitleReceiving => 'Menerima'; + + @override + String get activityDetailTitleSent => 'Terkirim'; + + @override + String get activityDetailTitleReceived => 'Diterima'; + + @override + String get activityDetailStatusInProcess => 'Diproses'; + + @override + String get activityDetailStatusScheduled => 'Terjadwal'; + + @override + String get activityDetailStatusCompleted => 'Selesai'; + + @override + String get activityDetailStatus => 'STATUS'; + + @override + String get activityDetailTo => 'KE'; + + @override + String get activityDetailFrom => 'DARI'; + + @override + String get activityDetailDate => 'TANGGAL'; + + @override + String get activityDetailNetworkFee => 'BIAYA JARINGAN'; + + @override + String get activityDetailTxHash => 'HASH TX'; + + @override + String get activityDetailViewExplorer => 'Lihat di Explorer ↗'; } diff --git a/mobile-app/lib/v2/screens/activity/activity_screen.dart b/mobile-app/lib/v2/screens/activity/activity_screen.dart index a83526f7..0970febb 100644 --- a/mobile-app/lib/v2/screens/activity/activity_screen.dart +++ b/mobile-app/lib/v2/screens/activity/activity_screen.dart @@ -3,9 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/providers/active_account_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/services/transaction_service.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; @@ -33,8 +35,16 @@ class _ActivityScreenState extends ConsumerState { }); } + String _filterLabel(TransactionFilter filter, AppLocalizations l10n) => switch (filter) { + TransactionFilter.all => l10n.activityFilterAll, + TransactionFilter.send => l10n.activityFilterSend, + TransactionFilter.receive => l10n.activityFilterReceive, + }; + @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + final locale = ref.watch(localeProvider); final colors = context.colors; final text = context.themeText; final accountAsync = ref.watch(activeAccountProvider); @@ -43,13 +53,16 @@ class _ActivityScreenState extends ConsumerState { final filterButtons = TransactionFilter.values .map( - (e) => - _buildFilterButton(e.displayName, onTap: () => _onFilterOptionChanged(e), isSelected: _filterOption == e), + (e) => _buildFilterButton( + _filterLabel(e, l10n), + onTap: () => _onFilterOptionChanged(e), + isSelected: _filterOption == e, + ), ) .toList(); return ScaffoldBase( - appBar: const V2AppBar(title: 'Activity'), + appBar: V2AppBar(title: l10n.activityTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, @@ -58,17 +71,17 @@ class _ActivityScreenState extends ConsumerState { scrollDirection: Axis.horizontal, child: Row(spacing: 12, children: filterButtons), ), - const SizedBox(height: 40), - Expanded( child: accountAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError)), + child: Text(l10n.activityError(e.toString()), style: text.detail?.copyWith(color: colors.textError)), ), data: (active) { - if (active == null) return const Center(child: Text('No account')); + if (active == null) { + return Center(child: Text(l10n.activityNoAccount)); + } return txAsync.when( loading: () => ListView.builder( itemCount: 3, @@ -76,10 +89,8 @@ class _ActivityScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (i > 0) const SizedBox(height: 32), - const Skeleton(width: 100, height: 24), const SizedBox(height: 12), - for (var j = 0; j < 3; j++) ...[ const TxItemSkeleton(), if (j < 2) Divider(color: colors.txItemSeparator, height: 24), @@ -88,7 +99,7 @@ class _ActivityScreenState extends ConsumerState { ), ), error: (e, _) => Center( - child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError)), + child: Text(l10n.activityError(e.toString()), style: text.detail?.copyWith(color: colors.textError)), ), data: (data) { final txService = ref.read(transactionServiceProvider); @@ -101,12 +112,12 @@ class _ActivityScreenState extends ConsumerState { if (all.isEmpty) { return Center( child: Text( - 'No transactions yet', + l10n.activityEmpty, style: text.paragraph?.copyWith(color: colors.textSecondary), ), ); } - final grouped = _groupByDate(all); + final grouped = _groupByDate(all, l10n, locale.toString()); return ListView.builder( padding: EdgeInsets.zero, @@ -119,13 +130,14 @@ class _ActivityScreenState extends ConsumerState { if (i > 0) const SizedBox(height: 32), Text(group.label, style: text.receiveLabel?.copyWith(color: colors.textTertiary)), ...group.transactions.mapIndexed((index, tx) { - final itemData = TxItemData.from(tx, active.account.accountId, colors); + final itemData = TxItemData.from(tx, active.account.accountId, colors, l10n); final isLastItem = index == group.transactions.length - 1; return buildTxItem( tx, itemData, colors, text, + l10n, formattedAmount: formatTxAmount(itemData.amount, isSend: itemData.isSend).primaryAmount, isLastItem: isLastItem, onTap: () { @@ -160,7 +172,7 @@ class _ActivityScreenState extends ConsumerState { ); } - List<_DateGroup> _groupByDate(List transactions) { + List<_DateGroup> _groupByDate(List transactions, AppLocalizations l10n, String localeName) { final Map> groups = {}; final Map labelMap = {}; @@ -169,7 +181,7 @@ class _ActivityScreenState extends ConsumerState { final key = '${day.year}-${day.month}-${day.day}'; groups.putIfAbsent(key, () => []); groups[key]!.add(tx); - labelMap.putIfAbsent(key, () => dateGroupLabel(tx.timestamp)); + labelMap.putIfAbsent(key, () => dateGroupLabel(tx.timestamp, l10n, localeName)); } return groups.entries.map((e) => _DateGroup(label: labelMap[e.key]!.toUpperCase(), transactions: e.value)).toList(); diff --git a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart index ef5ef693..63aab04b 100644 --- a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart +++ b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/dotted_border.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/shared/extensions/transaction_event_extension.dart'; import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; @@ -18,7 +20,7 @@ void showTransactionDetailSheet(BuildContext context, TransactionEvent tx, Strin ); } -class _TransactionDetailSheet extends StatelessWidget { +class _TransactionDetailSheet extends ConsumerWidget { final TransactionEvent tx; final String activeAccountId; @@ -27,16 +29,16 @@ class _TransactionDetailSheet extends StatelessWidget { bool get _isSend => tx.from == activeAccountId; bool get _isPending => tx is PendingTransactionEvent; - String get _title { - if (_isPending) return 'Sending'; - if (tx.isReversibleScheduled) return _isSend ? 'Scheduled' : 'Receiving'; - return _isSend ? 'Sent' : 'Received'; + String _title(AppLocalizations l10n) { + if (_isPending) return l10n.activityDetailTitleSending; + if (tx.isReversibleScheduled) return _isSend ? l10n.activityDetailTitleScheduled : l10n.activityDetailTitleReceiving; + return _isSend ? l10n.activityDetailTitleSent : l10n.activityDetailTitleReceived; } - String get _statusLabel { - if (_isPending) return 'In Process'; - if (tx.isReversibleScheduled) return 'Scheduled'; - return 'Completed'; + String _statusLabel(AppLocalizations l10n) { + if (_isPending) return l10n.activityDetailStatusInProcess; + if (tx.isReversibleScheduled) return l10n.activityDetailStatusScheduled; + return l10n.activityDetailStatusCompleted; } Color _statusColor(AppColorsV2 colors) { @@ -45,12 +47,13 @@ class _TransactionDetailSheet extends StatelessWidget { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; return BottomSheetContainer( - title: _title, + title: _title(l10n), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -58,7 +61,12 @@ class _TransactionDetailSheet extends StatelessWidget { const SizedBox(height: 16), _AmountSection(tx: tx, isSend: _isSend, colors: colors), const SizedBox(height: 20), - _DetailRow(label: 'STATUS', value: _statusLabel, valueColor: _statusColor(colors), colors: colors), + _DetailRow( + label: l10n.activityDetailStatus, + value: _statusLabel(l10n), + valueColor: _statusColor(colors), + colors: colors, + ), const SizedBox(height: 8), DottedBorder( dashLength: 3, @@ -108,6 +116,7 @@ class _DetailsSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final formattingService = ref.watch(numberFormattingServiceProvider); final counterparty = isSend ? tx.to : tx.from; @@ -118,7 +127,10 @@ class _DetailsSection extends ConsumerWidget { if (tx is TransferEvent) fee = (tx as TransferEvent).fee; if (tx is PendingTransactionEvent) fee = (tx as PendingTransactionEvent).fee; final feeStr = (fee != null && fee != BigInt.zero) - ? '${formattingService.formatBalance(fee, maxDecimals: AppConstants.decimals)} ${AppConstants.tokenSymbol}' + ? l10n.sendInputAmountBalance( + formattingService.formatBalance(fee, maxDecimals: AppConstants.decimals), + AppConstants.tokenSymbol, + ) : null; final txHash = tx.extrinsicHash != null @@ -127,10 +139,10 @@ class _DetailsSection extends ConsumerWidget { return Column( children: [ - _DetailRow(label: isSend ? 'TO' : 'FROM', value: address, colors: colors), - _DetailRow(label: 'DATE', value: dateTime, colors: colors), - if (feeStr != null) _DetailRow(label: 'NETWORK FEE', value: feeStr, colors: colors), - if (txHash != null) _DetailRow(label: 'TX HASH', value: txHash, colors: colors), + _DetailRow(label: isSend ? l10n.activityDetailTo : l10n.activityDetailFrom, value: address, colors: colors), + _DetailRow(label: l10n.activityDetailDate, value: dateTime, colors: colors), + if (feeStr != null) _DetailRow(label: l10n.activityDetailNetworkFee, value: feeStr, colors: colors), + if (txHash != null) _DetailRow(label: l10n.activityDetailTxHash, value: txHash, colors: colors), ], ); } @@ -164,7 +176,7 @@ class _DetailRow extends StatelessWidget { } } -class _ExplorerLink extends StatelessWidget { +class _ExplorerLink extends ConsumerWidget { final TransactionEvent tx; final AppColorsV2 colors; final AppTextTheme text; @@ -172,7 +184,8 @@ class _ExplorerLink extends StatelessWidget { const _ExplorerLink({required this.tx, required this.colors, required this.text}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final isPending = tx is PendingTransactionEvent; final color = isPending ? colors.accentOrange.withValues(alpha: 0.3) : colors.accentOrange; @@ -184,7 +197,7 @@ class _ExplorerLink extends StatelessWidget { border: Border(bottom: BorderSide(color: color, width: 1)), ), child: Text( - 'View in Explorer ↗', + l10n.activityDetailViewExplorer, style: text.smallParagraph?.copyWith(color: color, fontWeight: FontWeight.w400), ), ), diff --git a/mobile-app/lib/v2/screens/activity/tx_item.dart b/mobile-app/lib/v2/screens/activity/tx_item.dart index b88735f7..24c4de65 100644 --- a/mobile-app/lib/v2/screens/activity/tx_item.dart +++ b/mobile-app/lib/v2/screens/activity/tx_item.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/shared/extensions/transaction_event_extension.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; @@ -29,7 +31,12 @@ class TxItemData { required this.counterpartyAddr, }); - factory TxItemData.from(TransactionEvent tx, String accountId, AppColorsV2 colors) { + factory TxItemData.from( + TransactionEvent tx, + String accountId, + AppColorsV2 colors, + AppLocalizations l10n, + ) { final isSend = tx.from == accountId; final isPending = tx is PendingTransactionEvent; final isScheduled = tx.isReversibleScheduled; @@ -37,32 +44,32 @@ class TxItemData { String getLabel() { if (isPending && isSend) { - return 'Sending'; + return l10n.activityTxSending; } if (isPending && !isSend) { - return 'Receiving'; + return l10n.activityTxReceiving; } if (isScheduled && isSend) { - return 'Pending'; + return l10n.activityTxPending; } if (isScheduled && !isSend) { - return 'Receiving'; + return l10n.activityTxReceiving; } if (isSend && !isScheduled) { - return 'Sent'; + return l10n.activityTxSent; } - return 'Received'; + return l10n.activityTxReceived; } String getTimeLabel() { if (isPending) { - return 'now'; + return l10n.activityTxTimeNow; } if (isScheduled) { - return _formatDuration(tx.timeRemaining); + return _formatDuration(tx.timeRemaining, l10n); } - return _timeAgo(tx.timestamp); + return _timeAgo(tx.timestamp, l10n); } Color getIconBg() { @@ -137,11 +144,14 @@ Widget buildTxItem( TransactionEvent tx, TxItemData data, AppColorsV2 colors, - AppTextTheme text, { + AppTextTheme text, + AppLocalizations l10n, { required String formattedAmount, required bool isLastItem, VoidCallback? onTap, }) { + final directionLabel = data.isSend ? l10n.activityTxTo : l10n.activityTxFrom; + return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, @@ -175,7 +185,6 @@ Widget buildTxItem( ], ), ), - Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ @@ -188,7 +197,7 @@ Widget buildTxItem( ), const SizedBox(height: 2), Text( - '${data.isSend ? "To" : "From"}: ${data.counterpartyAddr}', + '$directionLabel: ${data.counterpartyAddr}', style: text.detail?.copyWith(color: colors.textTertiary), ), ], @@ -202,28 +211,27 @@ Widget buildTxItem( ); } -String _formatDuration(Duration d) { - final days = d.inDays; - final hours = d.inHours % 24; - final mins = d.inMinutes % 60; - return '${days.toString().padLeft(2, '0')}d:${hours.toString().padLeft(2, '0')}h:${mins.toString().padLeft(2, '0')}m'; +String _formatDuration(Duration d, AppLocalizations l10n) { + final days = d.inDays.toString().padLeft(2, '0'); + final hours = (d.inHours % 24).toString().padLeft(2, '0'); + final mins = (d.inMinutes % 60).toString().padLeft(2, '0'); + return l10n.activityTxTimeRemaining(days, hours, mins); } -String _timeAgo(DateTime timestamp) { +String _timeAgo(DateTime timestamp, AppLocalizations l10n) { final diff = DateTime.now().difference(timestamp); - if (diff.inMinutes < 1) return 'now'; - if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; - if (diff.inHours < 24) return '${diff.inHours}h ago'; - return '${diff.inDays}d ago'; + if (diff.inMinutes < 1) return l10n.activityTxTimeNow; + if (diff.inMinutes < 60) return l10n.activityTxTimeMinutesAgo(diff.inMinutes); + if (diff.inHours < 24) return l10n.activityTxTimeHoursAgo(diff.inHours); + return l10n.activityTxTimeDaysAgo(diff.inDays); } -String dateGroupLabel(DateTime date) { +String dateGroupLabel(DateTime date, AppLocalizations l10n, String localeName) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final txDay = DateTime(date.year, date.month, date.day); final diff = today.difference(txDay).inDays; - if (diff == 0) return 'Today'; - if (diff == 1) return 'Yesterday'; - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - return '${months[date.month - 1]} ${date.day}, ${date.year}'; + if (diff == 0) return l10n.activityDateToday; + if (diff == 1) return l10n.activityDateYesterday; + return DateFormat.yMMMd(localeName).format(date); } diff --git a/mobile-app/lib/v2/screens/home/activity_section.dart b/mobile-app/lib/v2/screens/home/activity_section.dart index aa5d3fe2..39c29431 100644 --- a/mobile-app/lib/v2/screens/home/activity_section.dart +++ b/mobile-app/lib/v2/screens/home/activity_section.dart @@ -58,7 +58,7 @@ class _ActivitySectionState extends ConsumerState { const SizedBox(height: 28), ...recentTransactions.mapIndexed((index, tx) { - final data = TxItemData.from(tx, widget.activeAccount.accountId, colors); + final data = TxItemData.from(tx, widget.activeAccount.accountId, colors, l10n); final isLastItem = index == recentTransactions.length - 1; return buildTxItem( @@ -66,6 +66,7 @@ class _ActivitySectionState extends ConsumerState { data, colors, text, + l10n, formattedAmount: formatTxAmount(data.amount, isSend: data.isSend).primaryAmount, isLastItem: isLastItem, onTap: () { From d1c1ef9ed6e395d8101b9dcc559396c8fd9a3343 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 18:21:06 +0800 Subject: [PATCH 08/25] feat: localize receive and pos screens --- mobile-app/lib/l10n/app_en.arb | 136 +++++++++++++ mobile-app/lib/l10n/app_id.arb | 30 ++- mobile-app/lib/l10n/app_localizations.dart | 150 ++++++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 87 ++++++++ mobile-app/lib/l10n/app_localizations_id.dart | 87 ++++++++ .../lib/v2/screens/pos/pos_amount_screen.dart | 15 +- .../lib/v2/screens/pos/pos_qr_screen.dart | 186 ++++++++++++------ .../v2/screens/receive/receive_screen.dart | 27 ++- 8 files changed, 640 insertions(+), 78 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index e9df2843..150487a4 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -743,5 +743,141 @@ "activityDetailViewExplorer": "View in Explorer ↗", "@activityDetailViewExplorer": { "description": "Link to view transaction in explorer" + }, + + "receiveTitle": "Receive", + "@receiveTitle": { + "description": "App bar title on receive screen" + }, + "receiveTabQrCode": "QR Code", + "@receiveTabQrCode": { + "description": "QR Code tab on receive screen" + }, + "receiveTabAddress": "Address", + "@receiveTabAddress": { + "description": "Address tab on receive screen" + }, + "receiveCopy": "Copy", + "@receiveCopy": { + "description": "Copy button on receive screen" + }, + "receiveErrorLoadingAccount": "Error loading account data: {error}", + "@receiveErrorLoadingAccount": { + "description": "Error when account data fails to load on receive screen", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "receiveClipboardContent": "Account Id:\n{accountId}\n\nCheckphrase:\n{checksum}", + "@receiveClipboardContent": { + "description": "Clipboard content when copying account details", + "placeholders": { + "accountId": { + "type": "String" + }, + "checksum": { + "type": "String" + } + } + }, + "receiveCopiedMessage": "Account details copied to clipboard", + "@receiveCopiedMessage": { + "description": "Toast when account details are copied" + }, + + "posAmountTitle": "New Charge", + "@posAmountTitle": { + "description": "App bar title on POS amount screen" + }, + "posAmountCharge": "Charge {amount}", + "@posAmountCharge": { + "description": "Charge button with formatted amount", + "placeholders": { + "amount": { + "type": "String" + } + } + }, + "posAmountEnterAmount": "Enter Amount", + "@posAmountEnterAmount": { + "description": "Charge button when amount is empty" + }, + + "posQrTitleScanToPay": "Scan to Pay", + "@posQrTitleScanToPay": { + "description": "App bar title while waiting for payment" + }, + "posQrTitlePaymentReceived": "Payment Received", + "@posQrTitlePaymentReceived": { + "description": "App bar title when payment is received" + }, + "posQrError": "Error: {error}", + "@posQrError": { + "description": "Error message on POS QR screen", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "posQrNoActiveAccount": "No active account", + "@posQrNoActiveAccount": { + "description": "Shown when no active account on POS QR screen" + }, + "posQrInvalidAmount": "Invalid amount. Tap to retry.", + "@posQrInvalidAmount": { + "description": "Error when amount cannot be parsed" + }, + "posQrConnectionLost": "Connection lost. Tap to retry.", + "@posQrConnectionLost": { + "description": "Error when payment watch connection is lost" + }, + "posQrTimedOut": "Timed out. Tap to retry.", + "@posQrTimedOut": { + "description": "Error when payment watch times out" + }, + "posQrNewCharge": "New Charge", + "@posQrNewCharge": { + "description": "New charge button on POS QR screen" + }, + "posQrDone": "Done", + "@posQrDone": { + "description": "Done button after payment received" + }, + "posQrAmountReceived": "{amount} received", + "@posQrAmountReceived": { + "description": "Headline when payment is received", + "placeholders": { + "amount": { + "type": "String" + } + } + }, + "posQrFrom": "From:", + "@posQrFrom": { + "description": "Sender label on payment received screen" + }, + "posQrWaitingForPayment": "Waiting for payment", + "@posQrWaitingForPayment": { + "description": "Status while waiting for payment" + }, + "posQrNetworkError": "Network Error", + "@posQrNetworkError": { + "description": "Network error title on POS QR screen" + }, + "posQrTryAgain": "Try Again", + "@posQrTryAgain": { + "description": "Retry button on POS QR screen" + }, + "posQrPaidAt": "At {time}", + "@posQrPaidAt": { + "description": "Paid at timestamp on payment received screen", + "placeholders": { + "time": { + "type": "String" + } + } } } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index 658f04ac..3866dcdf 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -179,5 +179,33 @@ "activityDetailDate": "TANGGAL", "activityDetailNetworkFee": "BIAYA JARINGAN", "activityDetailTxHash": "HASH TX", - "activityDetailViewExplorer": "Lihat di Explorer ↗" + "activityDetailViewExplorer": "Lihat di Explorer ↗", + + "receiveTitle": "Terima", + "receiveTabQrCode": "Kode QR", + "receiveTabAddress": "Alamat", + "receiveCopy": "Salin", + "receiveErrorLoadingAccount": "Gagal memuat data akun: {error}", + "receiveClipboardContent": "ID Akun:\n{accountId}\n\nCheckphrase:\n{checksum}", + "receiveCopiedMessage": "Detail akun disalin ke clipboard", + + "posAmountTitle": "Tagihan Baru", + "posAmountCharge": "Tagih {amount}", + "posAmountEnterAmount": "Masukkan Jumlah", + + "posQrTitleScanToPay": "Pindai untuk Bayar", + "posQrTitlePaymentReceived": "Pembayaran Diterima", + "posQrError": "Error: {error}", + "posQrNoActiveAccount": "Tidak ada akun aktif", + "posQrInvalidAmount": "Jumlah tidak valid. Ketuk untuk coba lagi.", + "posQrConnectionLost": "Koneksi terputus. Ketuk untuk coba lagi.", + "posQrTimedOut": "Waktu habis. Ketuk untuk coba lagi.", + "posQrNewCharge": "Tagihan Baru", + "posQrDone": "Selesai", + "posQrAmountReceived": "{amount} diterima", + "posQrFrom": "Dari:", + "posQrWaitingForPayment": "Menunggu pembayaran", + "posQrNetworkError": "Error Jaringan", + "posQrTryAgain": "Coba Lagi", + "posQrPaidAt": "Pada {time}" } diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index dd8d8acc..5c306a51 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -1033,6 +1033,156 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'View in Explorer ↗'** String get activityDetailViewExplorer; + + /// App bar title on receive screen + /// + /// In en, this message translates to: + /// **'Receive'** + String get receiveTitle; + + /// QR Code tab on receive screen + /// + /// In en, this message translates to: + /// **'QR Code'** + String get receiveTabQrCode; + + /// Address tab on receive screen + /// + /// In en, this message translates to: + /// **'Address'** + String get receiveTabAddress; + + /// Copy button on receive screen + /// + /// In en, this message translates to: + /// **'Copy'** + String get receiveCopy; + + /// Error when account data fails to load on receive screen + /// + /// In en, this message translates to: + /// **'Error loading account data: {error}'** + String receiveErrorLoadingAccount(String error); + + /// Clipboard content when copying account details + /// + /// In en, this message translates to: + /// **'Account Id:\n{accountId}\n\nCheckphrase:\n{checksum}'** + String receiveClipboardContent(String accountId, String checksum); + + /// Toast when account details are copied + /// + /// In en, this message translates to: + /// **'Account details copied to clipboard'** + String get receiveCopiedMessage; + + /// App bar title on POS amount screen + /// + /// In en, this message translates to: + /// **'New Charge'** + String get posAmountTitle; + + /// Charge button with formatted amount + /// + /// In en, this message translates to: + /// **'Charge {amount}'** + String posAmountCharge(String amount); + + /// Charge button when amount is empty + /// + /// In en, this message translates to: + /// **'Enter Amount'** + String get posAmountEnterAmount; + + /// App bar title while waiting for payment + /// + /// In en, this message translates to: + /// **'Scan to Pay'** + String get posQrTitleScanToPay; + + /// App bar title when payment is received + /// + /// In en, this message translates to: + /// **'Payment Received'** + String get posQrTitlePaymentReceived; + + /// Error message on POS QR screen + /// + /// In en, this message translates to: + /// **'Error: {error}'** + String posQrError(String error); + + /// Shown when no active account on POS QR screen + /// + /// In en, this message translates to: + /// **'No active account'** + String get posQrNoActiveAccount; + + /// Error when amount cannot be parsed + /// + /// In en, this message translates to: + /// **'Invalid amount. Tap to retry.'** + String get posQrInvalidAmount; + + /// Error when payment watch connection is lost + /// + /// In en, this message translates to: + /// **'Connection lost. Tap to retry.'** + String get posQrConnectionLost; + + /// Error when payment watch times out + /// + /// In en, this message translates to: + /// **'Timed out. Tap to retry.'** + String get posQrTimedOut; + + /// New charge button on POS QR screen + /// + /// In en, this message translates to: + /// **'New Charge'** + String get posQrNewCharge; + + /// Done button after payment received + /// + /// In en, this message translates to: + /// **'Done'** + String get posQrDone; + + /// Headline when payment is received + /// + /// In en, this message translates to: + /// **'{amount} received'** + String posQrAmountReceived(String amount); + + /// Sender label on payment received screen + /// + /// In en, this message translates to: + /// **'From:'** + String get posQrFrom; + + /// Status while waiting for payment + /// + /// In en, this message translates to: + /// **'Waiting for payment'** + String get posQrWaitingForPayment; + + /// Network error title on POS QR screen + /// + /// In en, this message translates to: + /// **'Network Error'** + String get posQrNetworkError; + + /// Retry button on POS QR screen + /// + /// In en, this message translates to: + /// **'Try Again'** + String get posQrTryAgain; + + /// Paid at timestamp on payment received screen + /// + /// In en, this message translates to: + /// **'At {time}'** + String posQrPaidAt(String time); } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 88c1e416..e9c0c722 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -511,4 +511,91 @@ class AppLocalizationsEn extends AppLocalizations { @override String get activityDetailViewExplorer => 'View in Explorer ↗'; + + @override + String get receiveTitle => 'Receive'; + + @override + String get receiveTabQrCode => 'QR Code'; + + @override + String get receiveTabAddress => 'Address'; + + @override + String get receiveCopy => 'Copy'; + + @override + String receiveErrorLoadingAccount(String error) { + return 'Error loading account data: $error'; + } + + @override + String receiveClipboardContent(String accountId, String checksum) { + return 'Account Id:\n$accountId\n\nCheckphrase:\n$checksum'; + } + + @override + String get receiveCopiedMessage => 'Account details copied to clipboard'; + + @override + String get posAmountTitle => 'New Charge'; + + @override + String posAmountCharge(String amount) { + return 'Charge $amount'; + } + + @override + String get posAmountEnterAmount => 'Enter Amount'; + + @override + String get posQrTitleScanToPay => 'Scan to Pay'; + + @override + String get posQrTitlePaymentReceived => 'Payment Received'; + + @override + String posQrError(String error) { + return 'Error: $error'; + } + + @override + String get posQrNoActiveAccount => 'No active account'; + + @override + String get posQrInvalidAmount => 'Invalid amount. Tap to retry.'; + + @override + String get posQrConnectionLost => 'Connection lost. Tap to retry.'; + + @override + String get posQrTimedOut => 'Timed out. Tap to retry.'; + + @override + String get posQrNewCharge => 'New Charge'; + + @override + String get posQrDone => 'Done'; + + @override + String posQrAmountReceived(String amount) { + return '$amount received'; + } + + @override + String get posQrFrom => 'From:'; + + @override + String get posQrWaitingForPayment => 'Waiting for payment'; + + @override + String get posQrNetworkError => 'Network Error'; + + @override + String get posQrTryAgain => 'Try Again'; + + @override + String posQrPaidAt(String time) { + return 'At $time'; + } } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index 7e331428..c48ba517 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -512,4 +512,91 @@ class AppLocalizationsId extends AppLocalizations { @override String get activityDetailViewExplorer => 'Lihat di Explorer ↗'; + + @override + String get receiveTitle => 'Terima'; + + @override + String get receiveTabQrCode => 'Kode QR'; + + @override + String get receiveTabAddress => 'Alamat'; + + @override + String get receiveCopy => 'Salin'; + + @override + String receiveErrorLoadingAccount(String error) { + return 'Gagal memuat data akun: $error'; + } + + @override + String receiveClipboardContent(String accountId, String checksum) { + return 'ID Akun:\n$accountId\n\nCheckphrase:\n$checksum'; + } + + @override + String get receiveCopiedMessage => 'Detail akun disalin ke clipboard'; + + @override + String get posAmountTitle => 'Tagihan Baru'; + + @override + String posAmountCharge(String amount) { + return 'Tagih $amount'; + } + + @override + String get posAmountEnterAmount => 'Masukkan Jumlah'; + + @override + String get posQrTitleScanToPay => 'Pindai untuk Bayar'; + + @override + String get posQrTitlePaymentReceived => 'Pembayaran Diterima'; + + @override + String posQrError(String error) { + return 'Error: $error'; + } + + @override + String get posQrNoActiveAccount => 'Tidak ada akun aktif'; + + @override + String get posQrInvalidAmount => 'Jumlah tidak valid. Ketuk untuk coba lagi.'; + + @override + String get posQrConnectionLost => 'Koneksi terputus. Ketuk untuk coba lagi.'; + + @override + String get posQrTimedOut => 'Waktu habis. Ketuk untuk coba lagi.'; + + @override + String get posQrNewCharge => 'Tagihan Baru'; + + @override + String get posQrDone => 'Selesai'; + + @override + String posQrAmountReceived(String amount) { + return '$amount diterima'; + } + + @override + String get posQrFrom => 'Dari:'; + + @override + String get posQrWaitingForPayment => 'Menunggu pembayaran'; + + @override + String get posQrNetworkError => 'Error Jaringan'; + + @override + String get posQrTryAgain => 'Coba Lagi'; + + @override + String posQrPaidAt(String time) { + return 'Pada $time'; + } } 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 6e519630..bef14940 100644 --- a/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart @@ -2,6 +2,8 @@ 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/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.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'; @@ -47,7 +49,7 @@ class _PosAmountScreenState extends ConsumerState { 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'); + context.showErrorToaster(message: ref.read(l10nProvider).sendInputAmountInvalidAmount); return; } } @@ -74,6 +76,7 @@ class _PosAmountScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final primaryAmount = ref @@ -81,9 +84,9 @@ class _PosAmountScreenState extends ConsumerState { .primaryAmount; return ScaffoldBase( - appBar: const V2AppBar(title: 'New Charge'), + appBar: V2AppBar(title: l10n.posAmountTitle), mainContent: _amountCenter(colors, text), - bottomContent: _bottomContent(colors, text, primaryAmount), + bottomContent: _bottomContent(l10n, primaryAmount), ); } @@ -167,8 +170,10 @@ class _PosAmountScreenState extends ConsumerState { ); } - Widget _bottomContent(AppColorsV2 colors, AppTextTheme text, String amountDisplay) { - final label = _amount > BigInt.zero ? 'Charge $amountDisplay' : 'Enter Amount'; + Widget _bottomContent(AppLocalizations l10n, String amountDisplay) { + final label = _amount > BigInt.zero + ? l10n.posAmountCharge(amountDisplay) + : l10n.posAmountEnterAmount; 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 ef3aa7d8..197aece0 100644 --- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -2,9 +2,12 @@ import 'dart:async'; 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/l10n/app_localizations.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/l10n_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'; @@ -51,14 +54,15 @@ class _PosQrScreenState extends ConsumerState { } void _startWatching() { + final l10n = ref.read(l10nProvider); final formattingService = ref.watch(numberFormattingServiceProvider); final active = ref.read(activeAccountProvider).value; if (active == null) return; final expectedPlanck = formattingService.parseAmount(widget.amount); if (expectedPlanck == null) { - print('[PosQr] ERROR: failed to parse amount "${widget.amount}"'); - if (mounted) setState(() => _watchError = 'Invalid amount. Tap to retry.'); + debugPrint('[PosQr] ERROR: failed to parse amount "${widget.amount}"'); + if (mounted) setState(() => _watchError = l10n.posQrInvalidAmount); return; } @@ -67,15 +71,21 @@ class _PosQrScreenState extends ConsumerState { _watchError = null; }); - print('[PosQr] watching address=${active.account.accountId} expected=$expectedPlanck planck'); + debugPrint( + '[PosQr] watching address=${active.account.accountId} expected=$expectedPlanck planck', + ); _txWatch.watch( address: active.account.accountId, onTransfer: (tx) { - print('[PosQr] onTransfer from=${tx.from} amount=${tx.amount} hash=${tx.txHash}'); + debugPrint( + '[PosQr] onTransfer from=${tx.from} amount=${tx.amount} hash=${tx.txHash}', + ); if (_isPaid) return; final received = BigInt.tryParse(tx.amount); if (received != expectedPlanck) { - print('[PosQr] amount mismatch (received=$received expected=$expectedPlanck), ignoring'); + debugPrint( + '[PosQr] amount mismatch (received=$received expected=$expectedPlanck), ignoring', + ); return; } @@ -102,12 +112,13 @@ class _PosQrScreenState extends ConsumerState { } }, onError: (e) { + debugPrint('[PosQr] watch error: $e'); _txWatch.dispose(); _timeoutTimer?.cancel(); if (mounted) { setState(() { _watching = false; - _watchError = 'Connection lost. Tap to retry.'; + _watchError = ref.read(l10nProvider).posQrConnectionLost; }); } }, @@ -118,7 +129,7 @@ class _PosQrScreenState extends ConsumerState { if (mounted) { setState(() { _watching = false; - _watchError = 'Timed out. Tap to retry.'; + _watchError = ref.read(l10nProvider).posQrTimedOut; }); } }); @@ -159,6 +170,8 @@ class _PosQrScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + final locale = ref.watch(localeProvider); final colors = context.colors; final text = context.themeText; final accountAsync = ref.watch(activeAccountProvider); @@ -167,40 +180,69 @@ class _PosQrScreenState extends ConsumerState { final display = ref.watch(txAmountDisplayProvider)(planck, withSignPrefix: false, isSend: false, quanDecimals: 4); return ScaffoldBase( - appBar: V2AppBar(title: _isPaid ? 'Payment Received' : 'Scan to Pay'), + appBar: V2AppBar( + title: _isPaid ? l10n.posQrTitlePaymentReceived : l10n.posQrTitleScanToPay, + ), mainContent: accountAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text('Error: $e', style: text.detail?.copyWith(color: colors.textError)), + child: Text( + l10n.posQrError('$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); - if (_isPaid) return _buildPaidContent(colors, text, display.primaryAmount); - return _buildQrContent(_request!, colors, text, display); + if (active == null) { + return Center(child: Text(l10n.posQrNoActiveAccount)); + } + _request ??= _posService.createPaymentRequest( + accountId: active.account.accountId, + amount: widget.amount, + ); + if (_isPaid) { + return _buildPaidContent( + l10n, + locale.toString(), + colors, + text, + display.primaryAmount, + ); + } + return _buildQrContent(l10n, _request!, colors, text, display); }, ), - bottomContent: ScaffoldBaseBottomContent(child: _isPaid ? _buildPaidButtons() : _buildQrButton()), + bottomContent: ScaffoldBaseBottomContent( + child: _isPaid ? _buildPaidButtons(l10n) : _buildQrButton(l10n), + ), ); } - Widget _buildQrButton() { - return QuantusButton.simple(label: 'New Charge', onTap: _newCharge, variant: ButtonVariant.primary); + Widget _buildQrButton(AppLocalizations l10n) { + return QuantusButton.simple( + label: l10n.posQrNewCharge, + onTap: _newCharge, + variant: ButtonVariant.primary, + ); } - Widget _buildPaidButtons() { + Widget _buildPaidButtons(AppLocalizations l10n) { 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), + child: QuantusButton.simple( + padding: padding, + label: l10n.posQrDone, + onTap: _done, + variant: ButtonVariant.secondary, + ), ), Expanded( child: QuantusButton.simple( padding: padding, - label: 'New Charge', + label: l10n.posQrNewCharge, onTap: _newCharge, variant: ButtonVariant.primary, ), @@ -209,7 +251,13 @@ class _PosQrScreenState extends ConsumerState { ); } - Widget _buildPaidContent(AppColorsV2 colors, AppTextTheme text, String amountDisplay) { + Widget _buildPaidContent( + AppLocalizations l10n, + String localeName, + AppColorsV2 colors, + AppTextTheme text, + String amountDisplay, + ) { final transfer = _paidTransfer!; final formattedAddress = AddressFormattingService.formatAddress(transfer.from.trim()); @@ -220,21 +268,28 @@ class _PosQrScreenState extends ConsumerState { _buildSuccessCircle(colors), const SizedBox(height: 32), Text( - '$amountDisplay received', - style: text.smallTitle?.copyWith(color: colors.textLightGray, fontSize: 32, fontWeight: FontWeight.w400), + l10n.posQrAmountReceived(amountDisplay), + 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), + _formatPaidAt(_paidAt!, localeName, l10n), + style: text.smallParagraph?.copyWith( + color: colors.textTertiary, + letterSpacing: 0.7, + ), textAlign: TextAlign.center, ), const SizedBox(height: 32), - _buildFromSection(colors, text, formattedAddress), + _buildFromSection(l10n, colors, text, formattedAddress), const Spacer(), - _buildExplorerLink(colors, text), + _buildExplorerLink(l10n, colors, text), const SizedBox(height: 16), ], ); @@ -252,13 +307,21 @@ class _PosQrScreenState extends ConsumerState { ); } - Widget _buildFromSection(AppColorsV2 colors, AppTextTheme text, String formattedAddress) { + Widget _buildFromSection( + AppLocalizations l10n, + AppColorsV2 colors, + AppTextTheme text, + String formattedAddress, + ) { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - 'From:', - style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500), + l10n.posQrFrom, + style: text.paragraph?.copyWith( + color: colors.textPrimary, + fontWeight: FontWeight.w500, + ), textAlign: TextAlign.center, ), const SizedBox(height: 16), @@ -289,7 +352,7 @@ class _PosQrScreenState extends ConsumerState { ); } - Widget _buildExplorerLink(AppColorsV2 colors, AppTextTheme text) { + Widget _buildExplorerLink(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return GestureDetector( onTap: _openExplorer, child: Container( @@ -297,12 +360,16 @@ class _PosQrScreenState extends ConsumerState { 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)), + child: Text( + l10n.activityDetailViewExplorer, + style: text.smallParagraph?.copyWith(color: colors.textTertiary), + ), ), ); } Widget _buildQrContent( + AppLocalizations l10n, PosPaymentRequest request, AppColorsV2 colors, AppTextTheme text, @@ -314,8 +381,8 @@ class _PosQrScreenState extends ConsumerState { const SizedBox(height: 16), QuantusQr(accountId: request.paymentUrl), const Spacer(), - if (!_watching && _watchError != null) _buildErrorSection(colors, text), - if (_watching) _buildWaitingPill(colors, text), + if (!_watching && _watchError != null) _buildErrorSection(l10n, colors, text), + if (_watching) _buildWaitingPill(l10n, colors, text), const SizedBox(height: 16), ], ); @@ -326,7 +393,10 @@ class _PosQrScreenState extends ConsumerState { children: [ Text( display.primaryAmount, - style: text.totalMinedBlocks?.copyWith(color: colors.textPrimary, letterSpacing: -2.77), + style: text.totalMinedBlocks?.copyWith( + color: colors.textPrimary, + letterSpacing: -2.77, + ), ), const SizedBox(height: 8), Row( @@ -334,7 +404,10 @@ class _PosQrScreenState extends ConsumerState { children: [ Text( '≈ ${display.secondaryAmount}', - style: text.paragraph?.copyWith(color: colors.textTertiary, fontFamily: AppTextTheme.fontFamilySecondary), + style: text.paragraph?.copyWith( + color: colors.textTertiary, + fontFamily: AppTextTheme.fontFamilySecondary, + ), ), const SizedBox(width: 8), QuantusIconButton.circular( @@ -349,7 +422,7 @@ class _PosQrScreenState extends ConsumerState { ); } - Widget _buildWaitingPill(AppColorsV2 colors, AppTextTheme text) { + Widget _buildWaitingPill(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Container( padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 9), decoration: BoxDecoration( @@ -362,19 +435,25 @@ class _PosQrScreenState extends ConsumerState { children: [ Loader(size: 14, color: colors.textMuted), const SizedBox(width: 9), - Text('Waiting for payment', style: text.detail?.copyWith(color: colors.textMuted)), + Text( + l10n.posQrWaitingForPayment, + style: text.detail?.copyWith(color: colors.textMuted), + ), ], ), ); } - Widget _buildErrorSection(AppColorsV2 colors, AppTextTheme text) { + Widget _buildErrorSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( children: [ - Text('Network Error', style: text.detail?.copyWith(color: colors.textError)), + Text( + l10n.posQrNetworkError, + style: text.detail?.copyWith(color: colors.textError), + ), const SizedBox(height: 8), QuantusButton.simple( - label: 'Try Again', + label: l10n.posQrTryAgain, padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 8), onTap: _startWatching, variant: ButtonVariant.secondary, @@ -383,28 +462,9 @@ class _PosQrScreenState extends ConsumerState { ); } - 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'; - } + String _formatPaidAt(DateTime dt, String localeName, AppLocalizations l10n) { + final date = DateFormat.yMMMd(localeName).format(dt); + final time = DateFormat.jm(localeName).format(dt); + return l10n.posQrPaidAt('$date, $time'); } } diff --git a/mobile-app/lib/v2/screens/receive/receive_screen.dart b/mobile-app/lib/v2/screens/receive/receive_screen.dart index 6403a8d6..9573f4d8 100644 --- a/mobile-app/lib/v2/screens/receive/receive_screen.dart +++ b/mobile-app/lib/v2/screens/receive/receive_screen.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.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'; @@ -53,7 +55,8 @@ class _ReceiveScreenState extends ConsumerState { debugPrint('Error loading account data: $e'); if (mounted) { - context.showErrorToaster(message: 'Error loading account data: $e'); + final l10n = ref.read(l10nProvider); + context.showErrorToaster(message: l10n.receiveErrorLoadingAccount('$e')); } } } @@ -65,23 +68,25 @@ class _ReceiveScreenState extends ConsumerState { } void _copyAccountDetails(BuildContext context) { + final l10n = ref.read(l10nProvider); context.copyTextWithToaster( - 'Account Id:\n$_accountId\n\nCheckphrase:\n$_checksum', - message: 'Account details copied to clipboard', + l10n.receiveClipboardContent(_accountId!, _checksum!), + message: l10n.receiveCopiedMessage, ); } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final tabs = [ - const SegmentedControlItem(label: 'QR Code', value: ReceiveTab.qrCode), - const SegmentedControlItem(label: 'Address', value: ReceiveTab.address), + SegmentedControlItem(label: l10n.receiveTabQrCode, value: ReceiveTab.qrCode), + SegmentedControlItem(label: l10n.receiveTabAddress, value: ReceiveTab.address), ]; final isLoading = _accountId == null || _checksum == null; return ScaffoldBase( - appBar: const V2AppBar(title: 'Receive'), + appBar: V2AppBar(title: l10n.receiveTitle), mainContent: Column( children: [ SegmentedControls( @@ -102,11 +107,15 @@ class _ReceiveScreenState extends ConsumerState { AddressTab(accountId: _accountId!, checksum: _checksum!), ], ), - bottomContent: _buildBottomContent(isLoading, _selectedTab), + bottomContent: _buildBottomContent(l10n, isLoading, _selectedTab), ); } - Widget? _buildBottomContent(bool isLoading, ReceiveTab selectedTab) { + Widget? _buildBottomContent( + AppLocalizations l10n, + bool isLoading, + ReceiveTab selectedTab, + ) { Widget content; if (isLoading) { @@ -120,7 +129,7 @@ class _ReceiveScreenState extends ConsumerState { children: [ Expanded( child: QuantusButton.simple( - label: 'Copy', + label: l10n.receiveCopy, onTap: () => _copyAccountDetails(context), isDisabled: isLoading, icon: Icon(Icons.copy, size: 20, color: context.colors.textPrimary), From e920e722b93448938c1ea1eefadc5bb927f917fe Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 18:54:40 +0800 Subject: [PATCH 09/25] feat: localize setting screens --- mobile-app/lib/l10n/app_en.arb | 434 +++++++++++++ mobile-app/lib/l10n/app_id.arb | 110 +++- mobile-app/lib/l10n/app_localizations.dart | 570 ++++++++++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 302 ++++++++++ mobile-app/lib/l10n/app_localizations_id.dart | 302 ++++++++++ .../settings/about_quantus_screen.dart | 45 +- .../account_type_settings_screen.dart | 54 +- .../settings/currency_picker_screen.dart | 25 +- .../settings/help_and_support_screen.dart | 13 +- .../settings/mining_rewards_screen.dart | 121 ++-- .../settings/preferences_settings_screen.dart | 16 +- .../recovery_phrase_confirmation_screen.dart | 30 +- .../settings/recovery_phrase_screen.dart | 14 +- .../settings/reset_confirmation_screen.dart | 18 +- .../settings/select_wallet_screen.dart | 27 +- .../settings/settings_caution_scaffold.dart | 51 +- .../v2/screens/settings/settings_screen.dart | 81 ++- .../settings/testnet_rewards_screen.dart | 48 +- .../settings/wallet_settings_screen.dart | 20 +- 19 files changed, 2088 insertions(+), 193 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 150487a4..8745c8ce 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -879,5 +879,439 @@ "type": "String" } } + }, + + "settingsTitle": "Settings", + "@settingsTitle": { + "description": "App bar title on settings hub" + }, + "settingsWalletTitle": "Wallet", + "@settingsWalletTitle": { + "description": "Wallet row title on settings hub" + }, + "settingsWalletSubtitle": "Recovery Phrase, Reset Wallet", + "@settingsWalletSubtitle": { + "description": "Wallet row subtitle on settings hub" + }, + "settingsPreferencesTitle": "Preferences", + "@settingsPreferencesTitle": { + "description": "Preferences row title on settings hub" + }, + "settingsPreferencesSubtitle": "Currency, POS mode, notifications", + "@settingsPreferencesSubtitle": { + "description": "Preferences row subtitle on settings hub" + }, + "settingsMiningRewards": "Mining Rewards", + "@settingsMiningRewards": { + "description": "Mining rewards row title on settings hub" + }, + "settingsMiningRewardsSubtitle": "{count} blocks mined", + "@settingsMiningRewardsSubtitle": { + "description": "Mining rewards row subtitle when data loaded", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "settingsMiningRewardsError": "Error getting mining rewards", + "@settingsMiningRewardsError": { + "description": "Mining rewards row subtitle on error" + }, + "settingsAccountTypeTitle": "Account Type", + "@settingsAccountTypeTitle": { + "description": "Account type row title on settings hub" + }, + "settingsAccountTypeSubtitle": "Advanced Account Features", + "@settingsAccountTypeSubtitle": { + "description": "Account type row subtitle on settings hub" + }, + "settingsHelpTitle": "Help & Support", + "@settingsHelpTitle": { + "description": "Help row title on settings hub" + }, + "settingsHelpSubtitle": "FAQs, Contact the team", + "@settingsHelpSubtitle": { + "description": "Help row subtitle on settings hub" + }, + "settingsAboutTitle": "About Quantus", + "@settingsAboutTitle": { + "description": "About row title on settings hub" + }, + "settingsAboutHubSubtitle": "Version {version} ({build})", + "@settingsAboutHubSubtitle": { + "description": "About row subtitle on settings hub", + "placeholders": { + "version": { + "type": "String" + }, + "build": { + "type": "String" + } + } + }, + + "settingsWalletRecoveryPhrase": "Recovery Phrase", + "@settingsWalletRecoveryPhrase": { + "description": "Recovery phrase row on wallet settings" + }, + "settingsWalletRecoveryPhraseSubtitle": "View your 24-word Backup Password", + "@settingsWalletRecoveryPhraseSubtitle": { + "description": "Recovery phrase row subtitle" + }, + "settingsWalletReset": "Reset Wallet", + "@settingsWalletReset": { + "description": "Reset wallet row title" + }, + "settingsWalletResetSubtitle": "Removes all data from this device", + "@settingsWalletResetSubtitle": { + "description": "Reset wallet row subtitle" + }, + "settingsWalletNoWalletsFound": "No wallets found", + "@settingsWalletNoWalletsFound": { + "description": "Error when no wallets exist" + }, + "settingsWalletFailedToLoad": "Failed to load wallets", + "@settingsWalletFailedToLoad": { + "description": "Error when wallet list fails to load" + }, + + "settingsSelectWalletTitle": "Select Wallet", + "@settingsSelectWalletTitle": { + "description": "App bar on select wallet screen" + }, + "settingsSelectWalletNoWallets": "No wallets found", + "@settingsSelectWalletNoWallets": { + "description": "Empty state on select wallet screen" + }, + "settingsSelectWalletItem": "Wallet {number}", + "@settingsSelectWalletItem": { + "description": "Wallet list item label on select wallet screen", + "placeholders": { + "number": { + "type": "int" + } + } + }, + + "settingsRecoveryConfirmAuthReason": "Authenticate to see recovery phrase", + "@settingsRecoveryConfirmAuthReason": { + "description": "Biometric prompt when viewing recovery phrase" + }, + "settingsRecoveryConfirmAuthRequired": "Authentication required to see recovery phrase", + "@settingsRecoveryConfirmAuthRequired": { + "description": "Toaster when auth fails for recovery phrase" + }, + + "settingsRecoveryPhraseTitle": "Recovery Phrase", + "@settingsRecoveryPhraseTitle": { + "description": "App bar on recovery phrase screen" + }, + "settingsRecoveryPhraseDone": "Done", + "@settingsRecoveryPhraseDone": { + "description": "Done button on recovery phrase screen" + }, + + "settingsResetTitle": "Reset Wallet", + "@settingsResetTitle": { + "description": "App bar on reset wallet caution screen" + }, + "settingsResetAuthReason": "Authenticate to reset wallet", + "@settingsResetAuthReason": { + "description": "Biometric prompt when resetting wallet" + }, + "settingsResetFailed": "Failed to reset wallet: {error}", + "@settingsResetFailed": { + "description": "Toaster when wallet reset fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, + "settingsResetAuthRequired": "Authentication required to reset wallet", + "@settingsResetAuthRequired": { + "description": "Toaster when auth fails for wallet reset" + }, + "settingsResetCautionHeadline": "This will erase\nyour wallet", + "@settingsResetCautionHeadline": { + "description": "Headline on wallet reset caution screen" + }, + "settingsResetCautionBullet1": "All wallet data will be permanently removed from this device", + "@settingsResetCautionBullet1": { + "description": "First bullet on wallet reset caution" + }, + "settingsResetCautionBullet2": "Your funds stay on the blockchain but only your recovery phrase can restore access", + "@settingsResetCautionBullet2": { + "description": "Second bullet on wallet reset caution" + }, + "settingsResetCautionBullet3": "Without it, your funds are gone forever", + "@settingsResetCautionBullet3": { + "description": "Third bullet on wallet reset caution" + }, + "settingsResetCautionCheckbox": "I've backed up my recovery phrase", + "@settingsResetCautionCheckbox": { + "description": "Checkbox label on wallet reset caution" + }, + + "settingsPreferencesCurrency": "Currency", + "@settingsPreferencesCurrency": { + "description": "Currency row on preferences screen" + }, + "settingsPreferencesCurrencySubtitle": "Fiat display preference", + "@settingsPreferencesCurrencySubtitle": { + "description": "Currency row subtitle" + }, + "settingsPreferencesPosMode": "POS Mode", + "@settingsPreferencesPosMode": { + "description": "POS mode row on preferences" + }, + "settingsPreferencesPosModeSubtitle": "Point of sale features", + "@settingsPreferencesPosModeSubtitle": { + "description": "POS mode row subtitle" + }, + "settingsPreferencesNotifications": "Notifications", + "@settingsPreferencesNotifications": { + "description": "Notifications row on preferences" + }, + "settingsPreferencesNotificationsSubtitle": "Transaction and wallet alerts", + "@settingsPreferencesNotificationsSubtitle": { + "description": "Notifications row subtitle" + }, + + "settingsCurrencyTitle": "Currency", + "@settingsCurrencyTitle": { + "description": "App bar on currency picker" + }, + "settingsCurrencySearchHint": "Search", + "@settingsCurrencySearchHint": { + "description": "Search field hint on currency picker" + }, + "settingsCurrencyNoMatch": "No currencies match your search", + "@settingsCurrencyNoMatch": { + "description": "Empty state when search has no results" + }, + + "settingsMiningTitle": "Mining Rewards", + "@settingsMiningTitle": { + "description": "App bar on mining rewards screen" + }, + "settingsMiningRedeem": "Redeem", + "@settingsMiningRedeem": { + "description": "Redeem button on mining rewards" + }, + "settingsMiningStatusMining": "Mining", + "@settingsMiningStatusMining": { + "description": "Active mining status label" + }, + "settingsMiningStatusPending": "Pending", + "@settingsMiningStatusPending": { + "description": "Pending mining status label" + }, + "settingsMiningBlocksMined": "BLOCKS MINED", + "@settingsMiningBlocksMined": { + "description": "Blocks mined stat label" + }, + "settingsMiningBlocksAcrossTestnets": "blocks across all testnets", + "@settingsMiningBlocksAcrossTestnets": { + "description": "Subtitle under blocks mined count" + }, + "settingsMiningStatTestnetBlocks": "TESTNET BLOCKS", + "@settingsMiningStatTestnetBlocks": { + "description": "Testnet blocks stat label" + }, + "settingsMiningStatTestnetRewards": "TESTNET REWARDS", + "@settingsMiningStatTestnetRewards": { + "description": "Testnet rewards stat label" + }, + "settingsMiningStatRedeemed": "REDEEMED", + "@settingsMiningStatRedeemed": { + "description": "Redeemed rewards stat label" + }, + "settingsMiningStatRedeemable": "REDEEMABLE", + "@settingsMiningStatRedeemable": { + "description": "Redeemable rewards stat label" + }, + "settingsMiningQuanEarned": "QUAN EARNED", + "@settingsMiningQuanEarned": { + "description": "QUAN earned stat label" + }, + "settingsMiningViewTelemetry": "View Telemetry ↗", + "@settingsMiningViewTelemetry": { + "description": "Link to mining telemetry" + }, + "settingsMiningNoDataTitle": "No mining data yet", + "@settingsMiningNoDataTitle": { + "description": "Empty state title on mining rewards" + }, + "settingsMiningNoDataBody": "Set up a Quantus mining node to start earning rewards.", + "@settingsMiningNoDataBody": { + "description": "Empty state body on mining rewards" + }, + "settingsMiningSetupGuide": "Mining Setup Guide ↗", + "@settingsMiningSetupGuide": { + "description": "Link to mining setup guide" + }, + "settingsMiningLoadError": "Failed to load mining rewards", + "@settingsMiningLoadError": { + "description": "Error title on mining rewards screen" + }, + "settingsMiningCheckConnection": "Please check your connection", + "@settingsMiningCheckConnection": { + "description": "Error subtitle when connection fails" + }, + "settingsMiningTestnetBlocks": "blocks", + "@settingsMiningTestnetBlocks": { + "description": "Blocks label on testnet row" + }, + "settingsMiningDiracSince": "Nov 2025", + "@settingsMiningDiracSince": { + "description": "Dirac testnet active since date" + }, + "settingsMiningSchrodingerSince": "Oct 2025", + "@settingsMiningSchrodingerSince": { + "description": "Schrödinger testnet active since date" + }, + "settingsMiningResonanceSince": "Jul 2025", + "@settingsMiningResonanceSince": { + "description": "Resonance testnet active since date" + }, + + "settingsTestnetTitle": "Testnet Rewards", + "@settingsTestnetTitle": { + "description": "App bar on testnet rewards screen" + }, + "settingsTestnetLoadError": "Failed to load testnet rewards", + "@settingsTestnetLoadError": { + "description": "Error title on testnet rewards" + }, + "settingsTestnetTotalBlocks": "{count} blocks", + "@settingsTestnetTotalBlocks": { + "description": "Total blocks headline on testnet rewards", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "settingsTestnetTotalDescription": "Total blocks mined across all testnets", + "@settingsTestnetTotalDescription": { + "description": "Description under total blocks" + }, + "settingsTestnetBreakdown": "Breakdown", + "@settingsTestnetBreakdown": { + "description": "Breakdown section header" + }, + "settingsTestnetRowBlocks": "{count} blocks", + "@settingsTestnetRowBlocks": { + "description": "Blocks count in testnet breakdown row", + "placeholders": { + "count": { + "type": "int" + } + } + }, + + "settingsHelpScreenTitle": "Help & Support", + "@settingsHelpScreenTitle": { + "description": "App bar on help and support screen" + }, + "settingsHelpEmail": "Email Support", + "@settingsHelpEmail": { + "description": "Email support row title" + }, + "settingsHelpTelegram": "Telegram", + "@settingsHelpTelegram": { + "description": "Telegram row title" + }, + + "settingsAboutScreenTitle": "About", + "@settingsAboutScreenTitle": { + "description": "App bar on about screen" + }, + "settingsAboutIntro": "Quantus is a Layer 1 blockchain secured by ML-DSA Dilithium-5, the gold standard in quantum-resistant encryption. Built for a future where classical cryptography is no longer enough. Post-quantum cryptography for everyone.", + "@settingsAboutIntro": { + "description": "Intro paragraph on about screen" + }, + "settingsAboutTerms": "Terms of Service", + "@settingsAboutTerms": { + "description": "Terms of service link title" + }, + "settingsAboutTermsSubtitle": "quantus.com/terms/", + "@settingsAboutTermsSubtitle": { + "description": "Terms of service link subtitle" + }, + "settingsAboutPrivacy": "Privacy policy", + "@settingsAboutPrivacy": { + "description": "Privacy policy link title" + }, + "settingsAboutPrivacySubtitle": "quantus.com/privacy-policy/", + "@settingsAboutPrivacySubtitle": { + "description": "Privacy policy link subtitle" + }, + "settingsAboutWebsite": "Visit Website", + "@settingsAboutWebsite": { + "description": "Website link title" + }, + "settingsAboutWebsiteSubtitle": "quantus.com", + "@settingsAboutWebsiteSubtitle": { + "description": "Website link subtitle" + }, + "settingsAboutVersion": "Version {version} ({build})", + "@settingsAboutVersion": { + "description": "Version label on about screen", + "placeholders": { + "version": { + "type": "String" + }, + "build": { + "type": "String" + } + } + }, + + "settingsAccountTypeScreenTitle": "Account Type", + "@settingsAccountTypeScreenTitle": { + "description": "App bar on account type settings" + }, + "settingsAccountTypeIntro": "Advanced account features are coming soon. These will give you greater control over how transactions are authorised and secured.", + "@settingsAccountTypeIntro": { + "description": "Intro on account type settings" + }, + "settingsAccountTypeReversibleTitle": "Reversible Transactions", + "@settingsAccountTypeReversibleTitle": { + "description": "Reversible transactions feature title" + }, + "settingsAccountTypeReversibleSubtitle": "Reverse your sends within a time window", + "@settingsAccountTypeReversibleSubtitle": { + "description": "Reversible transactions feature subtitle" + }, + "settingsAccountTypeHighSecurityTitle": "High Security Account", + "@settingsAccountTypeHighSecurityTitle": { + "description": "High security account feature title" + }, + "settingsAccountTypeHighSecuritySubtitle": "Guardian approval required", + "@settingsAccountTypeHighSecuritySubtitle": { + "description": "High security account feature subtitle" + }, + "settingsAccountTypeMultiSigTitle": "Multi-Signature", + "@settingsAccountTypeMultiSigTitle": { + "description": "Multi-signature feature title" + }, + "settingsAccountTypeMultiSigSubtitle": "Multiple approvals required", + "@settingsAccountTypeMultiSigSubtitle": { + "description": "Multi-signature feature subtitle" + }, + "settingsAccountTypeHardwareTitle": "Hardware Wallet", + "@settingsAccountTypeHardwareTitle": { + "description": "Hardware wallet feature title" + }, + "settingsAccountTypeHardwareSubtitle": "Pair a hardware device", + "@settingsAccountTypeHardwareSubtitle": { + "description": "Hardware wallet feature subtitle" + }, + "settingsAccountTypeComingSoon": "Coming Soon", + "@settingsAccountTypeComingSoon": { + "description": "Coming soon badge on account type features" } } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index 3866dcdf..2a80ba8c 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -207,5 +207,113 @@ "posQrWaitingForPayment": "Menunggu pembayaran", "posQrNetworkError": "Error Jaringan", "posQrTryAgain": "Coba Lagi", - "posQrPaidAt": "Pada {time}" + "posQrPaidAt": "Pada {time}", + + "settingsTitle": "Pengaturan", + "settingsWalletTitle": "Dompet", + "settingsWalletSubtitle": "Frasa Pemulihan, Reset Dompet", + "settingsPreferencesTitle": "Preferensi", + "settingsPreferencesSubtitle": "Mata uang, mode POS, notifikasi", + "settingsMiningRewards": "Hadiah Mining", + "settingsMiningRewardsSubtitle": "{count} blok ditambang", + "settingsMiningRewardsError": "Gagal memuat hadiah mining", + "settingsAccountTypeTitle": "Jenis Akun", + "settingsAccountTypeSubtitle": "Fitur Akun Lanjutan", + "settingsHelpTitle": "Bantuan & Dukungan", + "settingsHelpSubtitle": "FAQ, Hubungi tim", + "settingsAboutTitle": "Tentang Quantus", + "settingsAboutHubSubtitle": "Versi {version} ({build})", + + "settingsWalletRecoveryPhrase": "Frasa Pemulihan", + "settingsWalletRecoveryPhraseSubtitle": "Lihat Kata Sandi Cadangan 24 kata Anda", + "settingsWalletReset": "Reset Dompet", + "settingsWalletResetSubtitle": "Menghapus semua data dari perangkat ini", + "settingsWalletNoWalletsFound": "Tidak ada dompet ditemukan", + "settingsWalletFailedToLoad": "Gagal memuat dompet", + + "settingsSelectWalletTitle": "Pilih Dompet", + "settingsSelectWalletNoWallets": "Tidak ada dompet ditemukan", + "settingsSelectWalletItem": "Dompet {number}", + + "settingsRecoveryConfirmAuthReason": "Autentikasi untuk melihat frasa pemulihan", + "settingsRecoveryConfirmAuthRequired": "Autentikasi diperlukan untuk melihat frasa pemulihan", + + "settingsRecoveryPhraseTitle": "Frasa Pemulihan", + "settingsRecoveryPhraseDone": "Selesai", + + "settingsResetTitle": "Reset Dompet", + "settingsResetAuthReason": "Autentikasi untuk mereset dompet", + "settingsResetFailed": "Gagal mereset dompet: {error}", + "settingsResetAuthRequired": "Autentikasi diperlukan untuk mereset dompet", + "settingsResetCautionHeadline": "Ini akan menghapus\ndompet Anda", + "settingsResetCautionBullet1": "Semua data dompet akan dihapus permanen dari perangkat ini", + "settingsResetCautionBullet2": "Dana Anda tetap di blockchain tetapi hanya frasa pemulihan yang dapat memulihkan akses", + "settingsResetCautionBullet3": "Tanpa frasa pemulihan, dana Anda hilang selamanya", + "settingsResetCautionCheckbox": "Saya sudah mencadangkan frasa pemulihan saya", + + "settingsPreferencesCurrency": "Mata Uang", + "settingsPreferencesCurrencySubtitle": "Preferensi tampilan fiat", + "settingsPreferencesPosMode": "Mode POS", + "settingsPreferencesPosModeSubtitle": "Fitur point of sale", + "settingsPreferencesNotifications": "Notifikasi", + "settingsPreferencesNotificationsSubtitle": "Peringatan transaksi dan dompet", + + "settingsCurrencyTitle": "Mata Uang", + "settingsCurrencySearchHint": "Cari", + "settingsCurrencyNoMatch": "Tidak ada mata uang yang cocok dengan pencarian Anda", + + "settingsMiningTitle": "Hadiah Mining", + "settingsMiningRedeem": "Tukar", + "settingsMiningStatusMining": "Mining", + "settingsMiningStatusPending": "Menunggu", + "settingsMiningBlocksMined": "BLOK DITAMBANG", + "settingsMiningBlocksAcrossTestnets": "blok di semua testnet", + "settingsMiningStatTestnetBlocks": "BLOK TESTNET", + "settingsMiningStatTestnetRewards": "HADIAH TESTNET", + "settingsMiningStatRedeemed": "DITUKAR", + "settingsMiningStatRedeemable": "DAPAT DITUKAR", + "settingsMiningQuanEarned": "QUAN DIHASILKAN", + "settingsMiningViewTelemetry": "Lihat Telemetri ↗", + "settingsMiningNoDataTitle": "Belum ada data mining", + "settingsMiningNoDataBody": "Siapkan node mining Quantus untuk mulai mendapatkan hadiah.", + "settingsMiningSetupGuide": "Panduan Setup Mining ↗", + "settingsMiningLoadError": "Gagal memuat hadiah mining", + "settingsMiningCheckConnection": "Periksa koneksi Anda", + "settingsMiningTestnetBlocks": "blok", + "settingsMiningDiracSince": "Nov 2025", + "settingsMiningSchrodingerSince": "Okt 2025", + "settingsMiningResonanceSince": "Jul 2025", + + "settingsTestnetTitle": "Hadiah Testnet", + "settingsTestnetLoadError": "Gagal memuat hadiah testnet", + "settingsTestnetTotalBlocks": "{count} blok", + "settingsTestnetTotalDescription": "Total blok ditambang di semua testnet", + "settingsTestnetBreakdown": "Rincian", + "settingsTestnetRowBlocks": "{count} blok", + + "settingsHelpScreenTitle": "Bantuan & Dukungan", + "settingsHelpEmail": "Dukungan Email", + "settingsHelpTelegram": "Telegram", + + "settingsAboutScreenTitle": "Tentang", + "settingsAboutIntro": "Quantus adalah blockchain Layer 1 yang diamankan oleh ML-DSA Dilithium-5, standar emas enkripsi tahan kuantum. Dibangun untuk masa depan di mana kriptografi klasik tidak lagi cukup. Kriptografi pasca-kuantum untuk semua orang.", + "settingsAboutTerms": "Ketentuan Layanan", + "settingsAboutTermsSubtitle": "quantus.com/terms/", + "settingsAboutPrivacy": "Kebijakan privasi", + "settingsAboutPrivacySubtitle": "quantus.com/privacy-policy/", + "settingsAboutWebsite": "Kunjungi Situs Web", + "settingsAboutWebsiteSubtitle": "quantus.com", + "settingsAboutVersion": "Versi {version} ({build})", + + "settingsAccountTypeScreenTitle": "Jenis Akun", + "settingsAccountTypeIntro": "Fitur akun lanjutan akan segera hadir. Fitur ini memberi Anda kontrol lebih besar atas cara transaksi diotorisasi dan diamankan.", + "settingsAccountTypeReversibleTitle": "Transaksi Reversible", + "settingsAccountTypeReversibleSubtitle": "Batalkan pengiriman dalam jangka waktu tertentu", + "settingsAccountTypeHighSecurityTitle": "Akun Keamanan Tinggi", + "settingsAccountTypeHighSecuritySubtitle": "Persetujuan guardian diperlukan", + "settingsAccountTypeMultiSigTitle": "Multi-Tanda Tangan", + "settingsAccountTypeMultiSigSubtitle": "Beberapa persetujuan diperlukan", + "settingsAccountTypeHardwareTitle": "Dompet Hardware", + "settingsAccountTypeHardwareSubtitle": "Pasangkan perangkat hardware", + "settingsAccountTypeComingSoon": "Segera Hadir" } diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 5c306a51..20d7fa56 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -1183,6 +1183,576 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'At {time}'** String posQrPaidAt(String time); + + /// App bar title on settings hub + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsTitle; + + /// Wallet row title on settings hub + /// + /// In en, this message translates to: + /// **'Wallet'** + String get settingsWalletTitle; + + /// Wallet row subtitle on settings hub + /// + /// In en, this message translates to: + /// **'Recovery Phrase, Reset Wallet'** + String get settingsWalletSubtitle; + + /// Preferences row title on settings hub + /// + /// In en, this message translates to: + /// **'Preferences'** + String get settingsPreferencesTitle; + + /// Preferences row subtitle on settings hub + /// + /// In en, this message translates to: + /// **'Currency, POS mode, notifications'** + String get settingsPreferencesSubtitle; + + /// Mining rewards row title on settings hub + /// + /// In en, this message translates to: + /// **'Mining Rewards'** + String get settingsMiningRewards; + + /// Mining rewards row subtitle when data loaded + /// + /// In en, this message translates to: + /// **'{count} blocks mined'** + String settingsMiningRewardsSubtitle(int count); + + /// Mining rewards row subtitle on error + /// + /// In en, this message translates to: + /// **'Error getting mining rewards'** + String get settingsMiningRewardsError; + + /// Account type row title on settings hub + /// + /// In en, this message translates to: + /// **'Account Type'** + String get settingsAccountTypeTitle; + + /// Account type row subtitle on settings hub + /// + /// In en, this message translates to: + /// **'Advanced Account Features'** + String get settingsAccountTypeSubtitle; + + /// Help row title on settings hub + /// + /// In en, this message translates to: + /// **'Help & Support'** + String get settingsHelpTitle; + + /// Help row subtitle on settings hub + /// + /// In en, this message translates to: + /// **'FAQs, Contact the team'** + String get settingsHelpSubtitle; + + /// About row title on settings hub + /// + /// In en, this message translates to: + /// **'About Quantus'** + String get settingsAboutTitle; + + /// About row subtitle on settings hub + /// + /// In en, this message translates to: + /// **'Version {version} ({build})'** + String settingsAboutHubSubtitle(String version, String build); + + /// Recovery phrase row on wallet settings + /// + /// In en, this message translates to: + /// **'Recovery Phrase'** + String get settingsWalletRecoveryPhrase; + + /// Recovery phrase row subtitle + /// + /// In en, this message translates to: + /// **'View your 24-word Backup Password'** + String get settingsWalletRecoveryPhraseSubtitle; + + /// Reset wallet row title + /// + /// In en, this message translates to: + /// **'Reset Wallet'** + String get settingsWalletReset; + + /// Reset wallet row subtitle + /// + /// In en, this message translates to: + /// **'Removes all data from this device'** + String get settingsWalletResetSubtitle; + + /// Error when no wallets exist + /// + /// In en, this message translates to: + /// **'No wallets found'** + String get settingsWalletNoWalletsFound; + + /// Error when wallet list fails to load + /// + /// In en, this message translates to: + /// **'Failed to load wallets'** + String get settingsWalletFailedToLoad; + + /// App bar on select wallet screen + /// + /// In en, this message translates to: + /// **'Select Wallet'** + String get settingsSelectWalletTitle; + + /// Empty state on select wallet screen + /// + /// In en, this message translates to: + /// **'No wallets found'** + String get settingsSelectWalletNoWallets; + + /// Wallet list item label on select wallet screen + /// + /// In en, this message translates to: + /// **'Wallet {number}'** + String settingsSelectWalletItem(int number); + + /// Biometric prompt when viewing recovery phrase + /// + /// In en, this message translates to: + /// **'Authenticate to see recovery phrase'** + String get settingsRecoveryConfirmAuthReason; + + /// Toaster when auth fails for recovery phrase + /// + /// In en, this message translates to: + /// **'Authentication required to see recovery phrase'** + String get settingsRecoveryConfirmAuthRequired; + + /// App bar on recovery phrase screen + /// + /// In en, this message translates to: + /// **'Recovery Phrase'** + String get settingsRecoveryPhraseTitle; + + /// Done button on recovery phrase screen + /// + /// In en, this message translates to: + /// **'Done'** + String get settingsRecoveryPhraseDone; + + /// App bar on reset wallet caution screen + /// + /// In en, this message translates to: + /// **'Reset Wallet'** + String get settingsResetTitle; + + /// Biometric prompt when resetting wallet + /// + /// In en, this message translates to: + /// **'Authenticate to reset wallet'** + String get settingsResetAuthReason; + + /// Toaster when wallet reset fails + /// + /// In en, this message translates to: + /// **'Failed to reset wallet: {error}'** + String settingsResetFailed(String error); + + /// Toaster when auth fails for wallet reset + /// + /// In en, this message translates to: + /// **'Authentication required to reset wallet'** + String get settingsResetAuthRequired; + + /// Headline on wallet reset caution screen + /// + /// In en, this message translates to: + /// **'This will erase\nyour wallet'** + String get settingsResetCautionHeadline; + + /// First bullet on wallet reset caution + /// + /// In en, this message translates to: + /// **'All wallet data will be permanently removed from this device'** + String get settingsResetCautionBullet1; + + /// Second bullet on wallet reset caution + /// + /// In en, this message translates to: + /// **'Your funds stay on the blockchain but only your recovery phrase can restore access'** + String get settingsResetCautionBullet2; + + /// Third bullet on wallet reset caution + /// + /// In en, this message translates to: + /// **'Without it, your funds are gone forever'** + String get settingsResetCautionBullet3; + + /// Checkbox label on wallet reset caution + /// + /// In en, this message translates to: + /// **'I\'ve backed up my recovery phrase'** + String get settingsResetCautionCheckbox; + + /// Currency row on preferences screen + /// + /// In en, this message translates to: + /// **'Currency'** + String get settingsPreferencesCurrency; + + /// Currency row subtitle + /// + /// In en, this message translates to: + /// **'Fiat display preference'** + String get settingsPreferencesCurrencySubtitle; + + /// POS mode row on preferences + /// + /// In en, this message translates to: + /// **'POS Mode'** + String get settingsPreferencesPosMode; + + /// POS mode row subtitle + /// + /// In en, this message translates to: + /// **'Point of sale features'** + String get settingsPreferencesPosModeSubtitle; + + /// Notifications row on preferences + /// + /// In en, this message translates to: + /// **'Notifications'** + String get settingsPreferencesNotifications; + + /// Notifications row subtitle + /// + /// In en, this message translates to: + /// **'Transaction and wallet alerts'** + String get settingsPreferencesNotificationsSubtitle; + + /// App bar on currency picker + /// + /// In en, this message translates to: + /// **'Currency'** + String get settingsCurrencyTitle; + + /// Search field hint on currency picker + /// + /// In en, this message translates to: + /// **'Search'** + String get settingsCurrencySearchHint; + + /// Empty state when search has no results + /// + /// In en, this message translates to: + /// **'No currencies match your search'** + String get settingsCurrencyNoMatch; + + /// App bar on mining rewards screen + /// + /// In en, this message translates to: + /// **'Mining Rewards'** + String get settingsMiningTitle; + + /// Redeem button on mining rewards + /// + /// In en, this message translates to: + /// **'Redeem'** + String get settingsMiningRedeem; + + /// Active mining status label + /// + /// In en, this message translates to: + /// **'Mining'** + String get settingsMiningStatusMining; + + /// Pending mining status label + /// + /// In en, this message translates to: + /// **'Pending'** + String get settingsMiningStatusPending; + + /// Blocks mined stat label + /// + /// In en, this message translates to: + /// **'BLOCKS MINED'** + String get settingsMiningBlocksMined; + + /// Subtitle under blocks mined count + /// + /// In en, this message translates to: + /// **'blocks across all testnets'** + String get settingsMiningBlocksAcrossTestnets; + + /// Testnet blocks stat label + /// + /// In en, this message translates to: + /// **'TESTNET BLOCKS'** + String get settingsMiningStatTestnetBlocks; + + /// Testnet rewards stat label + /// + /// In en, this message translates to: + /// **'TESTNET REWARDS'** + String get settingsMiningStatTestnetRewards; + + /// Redeemed rewards stat label + /// + /// In en, this message translates to: + /// **'REDEEMED'** + String get settingsMiningStatRedeemed; + + /// Redeemable rewards stat label + /// + /// In en, this message translates to: + /// **'REDEEMABLE'** + String get settingsMiningStatRedeemable; + + /// QUAN earned stat label + /// + /// In en, this message translates to: + /// **'QUAN EARNED'** + String get settingsMiningQuanEarned; + + /// Link to mining telemetry + /// + /// In en, this message translates to: + /// **'View Telemetry ↗'** + String get settingsMiningViewTelemetry; + + /// Empty state title on mining rewards + /// + /// In en, this message translates to: + /// **'No mining data yet'** + String get settingsMiningNoDataTitle; + + /// Empty state body on mining rewards + /// + /// In en, this message translates to: + /// **'Set up a Quantus mining node to start earning rewards.'** + String get settingsMiningNoDataBody; + + /// Link to mining setup guide + /// + /// In en, this message translates to: + /// **'Mining Setup Guide ↗'** + String get settingsMiningSetupGuide; + + /// Error title on mining rewards screen + /// + /// In en, this message translates to: + /// **'Failed to load mining rewards'** + String get settingsMiningLoadError; + + /// Error subtitle when connection fails + /// + /// In en, this message translates to: + /// **'Please check your connection'** + String get settingsMiningCheckConnection; + + /// Blocks label on testnet row + /// + /// In en, this message translates to: + /// **'blocks'** + String get settingsMiningTestnetBlocks; + + /// Dirac testnet active since date + /// + /// In en, this message translates to: + /// **'Nov 2025'** + String get settingsMiningDiracSince; + + /// Schrödinger testnet active since date + /// + /// In en, this message translates to: + /// **'Oct 2025'** + String get settingsMiningSchrodingerSince; + + /// Resonance testnet active since date + /// + /// In en, this message translates to: + /// **'Jul 2025'** + String get settingsMiningResonanceSince; + + /// App bar on testnet rewards screen + /// + /// In en, this message translates to: + /// **'Testnet Rewards'** + String get settingsTestnetTitle; + + /// Error title on testnet rewards + /// + /// In en, this message translates to: + /// **'Failed to load testnet rewards'** + String get settingsTestnetLoadError; + + /// Total blocks headline on testnet rewards + /// + /// In en, this message translates to: + /// **'{count} blocks'** + String settingsTestnetTotalBlocks(int count); + + /// Description under total blocks + /// + /// In en, this message translates to: + /// **'Total blocks mined across all testnets'** + String get settingsTestnetTotalDescription; + + /// Breakdown section header + /// + /// In en, this message translates to: + /// **'Breakdown'** + String get settingsTestnetBreakdown; + + /// Blocks count in testnet breakdown row + /// + /// In en, this message translates to: + /// **'{count} blocks'** + String settingsTestnetRowBlocks(int count); + + /// App bar on help and support screen + /// + /// In en, this message translates to: + /// **'Help & Support'** + String get settingsHelpScreenTitle; + + /// Email support row title + /// + /// In en, this message translates to: + /// **'Email Support'** + String get settingsHelpEmail; + + /// Telegram row title + /// + /// In en, this message translates to: + /// **'Telegram'** + String get settingsHelpTelegram; + + /// App bar on about screen + /// + /// In en, this message translates to: + /// **'About'** + String get settingsAboutScreenTitle; + + /// Intro paragraph on about screen + /// + /// In en, this message translates to: + /// **'Quantus is a Layer 1 blockchain secured by ML-DSA Dilithium-5, the gold standard in quantum-resistant encryption. Built for a future where classical cryptography is no longer enough. Post-quantum cryptography for everyone.'** + String get settingsAboutIntro; + + /// Terms of service link title + /// + /// In en, this message translates to: + /// **'Terms of Service'** + String get settingsAboutTerms; + + /// Terms of service link subtitle + /// + /// In en, this message translates to: + /// **'quantus.com/terms/'** + String get settingsAboutTermsSubtitle; + + /// Privacy policy link title + /// + /// In en, this message translates to: + /// **'Privacy policy'** + String get settingsAboutPrivacy; + + /// Privacy policy link subtitle + /// + /// In en, this message translates to: + /// **'quantus.com/privacy-policy/'** + String get settingsAboutPrivacySubtitle; + + /// Website link title + /// + /// In en, this message translates to: + /// **'Visit Website'** + String get settingsAboutWebsite; + + /// Website link subtitle + /// + /// In en, this message translates to: + /// **'quantus.com'** + String get settingsAboutWebsiteSubtitle; + + /// Version label on about screen + /// + /// In en, this message translates to: + /// **'Version {version} ({build})'** + String settingsAboutVersion(String version, String build); + + /// App bar on account type settings + /// + /// In en, this message translates to: + /// **'Account Type'** + String get settingsAccountTypeScreenTitle; + + /// Intro on account type settings + /// + /// In en, this message translates to: + /// **'Advanced account features are coming soon. These will give you greater control over how transactions are authorised and secured.'** + String get settingsAccountTypeIntro; + + /// Reversible transactions feature title + /// + /// In en, this message translates to: + /// **'Reversible Transactions'** + String get settingsAccountTypeReversibleTitle; + + /// Reversible transactions feature subtitle + /// + /// In en, this message translates to: + /// **'Reverse your sends within a time window'** + String get settingsAccountTypeReversibleSubtitle; + + /// High security account feature title + /// + /// In en, this message translates to: + /// **'High Security Account'** + String get settingsAccountTypeHighSecurityTitle; + + /// High security account feature subtitle + /// + /// In en, this message translates to: + /// **'Guardian approval required'** + String get settingsAccountTypeHighSecuritySubtitle; + + /// Multi-signature feature title + /// + /// In en, this message translates to: + /// **'Multi-Signature'** + String get settingsAccountTypeMultiSigTitle; + + /// Multi-signature feature subtitle + /// + /// In en, this message translates to: + /// **'Multiple approvals required'** + String get settingsAccountTypeMultiSigSubtitle; + + /// Hardware wallet feature title + /// + /// In en, this message translates to: + /// **'Hardware Wallet'** + String get settingsAccountTypeHardwareTitle; + + /// Hardware wallet feature subtitle + /// + /// In en, this message translates to: + /// **'Pair a hardware device'** + String get settingsAccountTypeHardwareSubtitle; + + /// Coming soon badge on account type features + /// + /// In en, this message translates to: + /// **'Coming Soon'** + String get settingsAccountTypeComingSoon; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index e9c0c722..4857012b 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -598,4 +598,306 @@ class AppLocalizationsEn extends AppLocalizations { String posQrPaidAt(String time) { return 'At $time'; } + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsWalletTitle => 'Wallet'; + + @override + String get settingsWalletSubtitle => 'Recovery Phrase, Reset Wallet'; + + @override + String get settingsPreferencesTitle => 'Preferences'; + + @override + String get settingsPreferencesSubtitle => 'Currency, POS mode, notifications'; + + @override + String get settingsMiningRewards => 'Mining Rewards'; + + @override + String settingsMiningRewardsSubtitle(int count) { + return '$count blocks mined'; + } + + @override + String get settingsMiningRewardsError => 'Error getting mining rewards'; + + @override + String get settingsAccountTypeTitle => 'Account Type'; + + @override + String get settingsAccountTypeSubtitle => 'Advanced Account Features'; + + @override + String get settingsHelpTitle => 'Help & Support'; + + @override + String get settingsHelpSubtitle => 'FAQs, Contact the team'; + + @override + String get settingsAboutTitle => 'About Quantus'; + + @override + String settingsAboutHubSubtitle(String version, String build) { + return 'Version $version ($build)'; + } + + @override + String get settingsWalletRecoveryPhrase => 'Recovery Phrase'; + + @override + String get settingsWalletRecoveryPhraseSubtitle => 'View your 24-word Backup Password'; + + @override + String get settingsWalletReset => 'Reset Wallet'; + + @override + String get settingsWalletResetSubtitle => 'Removes all data from this device'; + + @override + String get settingsWalletNoWalletsFound => 'No wallets found'; + + @override + String get settingsWalletFailedToLoad => 'Failed to load wallets'; + + @override + String get settingsSelectWalletTitle => 'Select Wallet'; + + @override + String get settingsSelectWalletNoWallets => 'No wallets found'; + + @override + String settingsSelectWalletItem(int number) { + return 'Wallet $number'; + } + + @override + String get settingsRecoveryConfirmAuthReason => 'Authenticate to see recovery phrase'; + + @override + String get settingsRecoveryConfirmAuthRequired => 'Authentication required to see recovery phrase'; + + @override + String get settingsRecoveryPhraseTitle => 'Recovery Phrase'; + + @override + String get settingsRecoveryPhraseDone => 'Done'; + + @override + String get settingsResetTitle => 'Reset Wallet'; + + @override + String get settingsResetAuthReason => 'Authenticate to reset wallet'; + + @override + String settingsResetFailed(String error) { + return 'Failed to reset wallet: $error'; + } + + @override + String get settingsResetAuthRequired => 'Authentication required to reset wallet'; + + @override + String get settingsResetCautionHeadline => 'This will erase\nyour wallet'; + + @override + String get settingsResetCautionBullet1 => 'All wallet data will be permanently removed from this device'; + + @override + String get settingsResetCautionBullet2 => + 'Your funds stay on the blockchain but only your recovery phrase can restore access'; + + @override + String get settingsResetCautionBullet3 => 'Without it, your funds are gone forever'; + + @override + String get settingsResetCautionCheckbox => 'I\'ve backed up my recovery phrase'; + + @override + String get settingsPreferencesCurrency => 'Currency'; + + @override + String get settingsPreferencesCurrencySubtitle => 'Fiat display preference'; + + @override + String get settingsPreferencesPosMode => 'POS Mode'; + + @override + String get settingsPreferencesPosModeSubtitle => 'Point of sale features'; + + @override + String get settingsPreferencesNotifications => 'Notifications'; + + @override + String get settingsPreferencesNotificationsSubtitle => 'Transaction and wallet alerts'; + + @override + String get settingsCurrencyTitle => 'Currency'; + + @override + String get settingsCurrencySearchHint => 'Search'; + + @override + String get settingsCurrencyNoMatch => 'No currencies match your search'; + + @override + String get settingsMiningTitle => 'Mining Rewards'; + + @override + String get settingsMiningRedeem => 'Redeem'; + + @override + String get settingsMiningStatusMining => 'Mining'; + + @override + String get settingsMiningStatusPending => 'Pending'; + + @override + String get settingsMiningBlocksMined => 'BLOCKS MINED'; + + @override + String get settingsMiningBlocksAcrossTestnets => 'blocks across all testnets'; + + @override + String get settingsMiningStatTestnetBlocks => 'TESTNET BLOCKS'; + + @override + String get settingsMiningStatTestnetRewards => 'TESTNET REWARDS'; + + @override + String get settingsMiningStatRedeemed => 'REDEEMED'; + + @override + String get settingsMiningStatRedeemable => 'REDEEMABLE'; + + @override + String get settingsMiningQuanEarned => 'QUAN EARNED'; + + @override + String get settingsMiningViewTelemetry => 'View Telemetry ↗'; + + @override + String get settingsMiningNoDataTitle => 'No mining data yet'; + + @override + String get settingsMiningNoDataBody => 'Set up a Quantus mining node to start earning rewards.'; + + @override + String get settingsMiningSetupGuide => 'Mining Setup Guide ↗'; + + @override + String get settingsMiningLoadError => 'Failed to load mining rewards'; + + @override + String get settingsMiningCheckConnection => 'Please check your connection'; + + @override + String get settingsMiningTestnetBlocks => 'blocks'; + + @override + String get settingsMiningDiracSince => 'Nov 2025'; + + @override + String get settingsMiningSchrodingerSince => 'Oct 2025'; + + @override + String get settingsMiningResonanceSince => 'Jul 2025'; + + @override + String get settingsTestnetTitle => 'Testnet Rewards'; + + @override + String get settingsTestnetLoadError => 'Failed to load testnet rewards'; + + @override + String settingsTestnetTotalBlocks(int count) { + return '$count blocks'; + } + + @override + String get settingsTestnetTotalDescription => 'Total blocks mined across all testnets'; + + @override + String get settingsTestnetBreakdown => 'Breakdown'; + + @override + String settingsTestnetRowBlocks(int count) { + return '$count blocks'; + } + + @override + String get settingsHelpScreenTitle => 'Help & Support'; + + @override + String get settingsHelpEmail => 'Email Support'; + + @override + String get settingsHelpTelegram => 'Telegram'; + + @override + String get settingsAboutScreenTitle => 'About'; + + @override + String get settingsAboutIntro => + 'Quantus is a Layer 1 blockchain secured by ML-DSA Dilithium-5, the gold standard in quantum-resistant encryption. Built for a future where classical cryptography is no longer enough. Post-quantum cryptography for everyone.'; + + @override + String get settingsAboutTerms => 'Terms of Service'; + + @override + String get settingsAboutTermsSubtitle => 'quantus.com/terms/'; + + @override + String get settingsAboutPrivacy => 'Privacy policy'; + + @override + String get settingsAboutPrivacySubtitle => 'quantus.com/privacy-policy/'; + + @override + String get settingsAboutWebsite => 'Visit Website'; + + @override + String get settingsAboutWebsiteSubtitle => 'quantus.com'; + + @override + String settingsAboutVersion(String version, String build) { + return 'Version $version ($build)'; + } + + @override + String get settingsAccountTypeScreenTitle => 'Account Type'; + + @override + String get settingsAccountTypeIntro => + 'Advanced account features are coming soon. These will give you greater control over how transactions are authorised and secured.'; + + @override + String get settingsAccountTypeReversibleTitle => 'Reversible Transactions'; + + @override + String get settingsAccountTypeReversibleSubtitle => 'Reverse your sends within a time window'; + + @override + String get settingsAccountTypeHighSecurityTitle => 'High Security Account'; + + @override + String get settingsAccountTypeHighSecuritySubtitle => 'Guardian approval required'; + + @override + String get settingsAccountTypeMultiSigTitle => 'Multi-Signature'; + + @override + String get settingsAccountTypeMultiSigSubtitle => 'Multiple approvals required'; + + @override + String get settingsAccountTypeHardwareTitle => 'Hardware Wallet'; + + @override + String get settingsAccountTypeHardwareSubtitle => 'Pair a hardware device'; + + @override + String get settingsAccountTypeComingSoon => 'Coming Soon'; } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index c48ba517..ea1fb690 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -599,4 +599,306 @@ class AppLocalizationsId extends AppLocalizations { String posQrPaidAt(String time) { return 'Pada $time'; } + + @override + String get settingsTitle => 'Pengaturan'; + + @override + String get settingsWalletTitle => 'Dompet'; + + @override + String get settingsWalletSubtitle => 'Frasa Pemulihan, Reset Dompet'; + + @override + String get settingsPreferencesTitle => 'Preferensi'; + + @override + String get settingsPreferencesSubtitle => 'Mata uang, mode POS, notifikasi'; + + @override + String get settingsMiningRewards => 'Hadiah Mining'; + + @override + String settingsMiningRewardsSubtitle(int count) { + return '$count blok ditambang'; + } + + @override + String get settingsMiningRewardsError => 'Gagal memuat hadiah mining'; + + @override + String get settingsAccountTypeTitle => 'Jenis Akun'; + + @override + String get settingsAccountTypeSubtitle => 'Fitur Akun Lanjutan'; + + @override + String get settingsHelpTitle => 'Bantuan & Dukungan'; + + @override + String get settingsHelpSubtitle => 'FAQ, Hubungi tim'; + + @override + String get settingsAboutTitle => 'Tentang Quantus'; + + @override + String settingsAboutHubSubtitle(String version, String build) { + return 'Versi $version ($build)'; + } + + @override + String get settingsWalletRecoveryPhrase => 'Frasa Pemulihan'; + + @override + String get settingsWalletRecoveryPhraseSubtitle => 'Lihat Kata Sandi Cadangan 24 kata Anda'; + + @override + String get settingsWalletReset => 'Reset Dompet'; + + @override + String get settingsWalletResetSubtitle => 'Menghapus semua data dari perangkat ini'; + + @override + String get settingsWalletNoWalletsFound => 'Tidak ada dompet ditemukan'; + + @override + String get settingsWalletFailedToLoad => 'Gagal memuat dompet'; + + @override + String get settingsSelectWalletTitle => 'Pilih Dompet'; + + @override + String get settingsSelectWalletNoWallets => 'Tidak ada dompet ditemukan'; + + @override + String settingsSelectWalletItem(int number) { + return 'Dompet $number'; + } + + @override + String get settingsRecoveryConfirmAuthReason => 'Autentikasi untuk melihat frasa pemulihan'; + + @override + String get settingsRecoveryConfirmAuthRequired => 'Autentikasi diperlukan untuk melihat frasa pemulihan'; + + @override + String get settingsRecoveryPhraseTitle => 'Frasa Pemulihan'; + + @override + String get settingsRecoveryPhraseDone => 'Selesai'; + + @override + String get settingsResetTitle => 'Reset Dompet'; + + @override + String get settingsResetAuthReason => 'Autentikasi untuk mereset dompet'; + + @override + String settingsResetFailed(String error) { + return 'Gagal mereset dompet: $error'; + } + + @override + String get settingsResetAuthRequired => 'Autentikasi diperlukan untuk mereset dompet'; + + @override + String get settingsResetCautionHeadline => 'Ini akan menghapus\ndompet Anda'; + + @override + String get settingsResetCautionBullet1 => 'Semua data dompet akan dihapus permanen dari perangkat ini'; + + @override + String get settingsResetCautionBullet2 => + 'Dana Anda tetap di blockchain tetapi hanya frasa pemulihan yang dapat memulihkan akses'; + + @override + String get settingsResetCautionBullet3 => 'Tanpa frasa pemulihan, dana Anda hilang selamanya'; + + @override + String get settingsResetCautionCheckbox => 'Saya sudah mencadangkan frasa pemulihan saya'; + + @override + String get settingsPreferencesCurrency => 'Mata Uang'; + + @override + String get settingsPreferencesCurrencySubtitle => 'Preferensi tampilan fiat'; + + @override + String get settingsPreferencesPosMode => 'Mode POS'; + + @override + String get settingsPreferencesPosModeSubtitle => 'Fitur point of sale'; + + @override + String get settingsPreferencesNotifications => 'Notifikasi'; + + @override + String get settingsPreferencesNotificationsSubtitle => 'Peringatan transaksi dan dompet'; + + @override + String get settingsCurrencyTitle => 'Mata Uang'; + + @override + String get settingsCurrencySearchHint => 'Cari'; + + @override + String get settingsCurrencyNoMatch => 'Tidak ada mata uang yang cocok dengan pencarian Anda'; + + @override + String get settingsMiningTitle => 'Hadiah Mining'; + + @override + String get settingsMiningRedeem => 'Tukar'; + + @override + String get settingsMiningStatusMining => 'Mining'; + + @override + String get settingsMiningStatusPending => 'Menunggu'; + + @override + String get settingsMiningBlocksMined => 'BLOK DITAMBANG'; + + @override + String get settingsMiningBlocksAcrossTestnets => 'blok di semua testnet'; + + @override + String get settingsMiningStatTestnetBlocks => 'BLOK TESTNET'; + + @override + String get settingsMiningStatTestnetRewards => 'HADIAH TESTNET'; + + @override + String get settingsMiningStatRedeemed => 'DITUKAR'; + + @override + String get settingsMiningStatRedeemable => 'DAPAT DITUKAR'; + + @override + String get settingsMiningQuanEarned => 'QUAN DIHASILKAN'; + + @override + String get settingsMiningViewTelemetry => 'Lihat Telemetri ↗'; + + @override + String get settingsMiningNoDataTitle => 'Belum ada data mining'; + + @override + String get settingsMiningNoDataBody => 'Siapkan node mining Quantus untuk mulai mendapatkan hadiah.'; + + @override + String get settingsMiningSetupGuide => 'Panduan Setup Mining ↗'; + + @override + String get settingsMiningLoadError => 'Gagal memuat hadiah mining'; + + @override + String get settingsMiningCheckConnection => 'Periksa koneksi Anda'; + + @override + String get settingsMiningTestnetBlocks => 'blok'; + + @override + String get settingsMiningDiracSince => 'Nov 2025'; + + @override + String get settingsMiningSchrodingerSince => 'Okt 2025'; + + @override + String get settingsMiningResonanceSince => 'Jul 2025'; + + @override + String get settingsTestnetTitle => 'Hadiah Testnet'; + + @override + String get settingsTestnetLoadError => 'Gagal memuat hadiah testnet'; + + @override + String settingsTestnetTotalBlocks(int count) { + return '$count blok'; + } + + @override + String get settingsTestnetTotalDescription => 'Total blok ditambang di semua testnet'; + + @override + String get settingsTestnetBreakdown => 'Rincian'; + + @override + String settingsTestnetRowBlocks(int count) { + return '$count blok'; + } + + @override + String get settingsHelpScreenTitle => 'Bantuan & Dukungan'; + + @override + String get settingsHelpEmail => 'Dukungan Email'; + + @override + String get settingsHelpTelegram => 'Telegram'; + + @override + String get settingsAboutScreenTitle => 'Tentang'; + + @override + String get settingsAboutIntro => + 'Quantus adalah blockchain Layer 1 yang diamankan oleh ML-DSA Dilithium-5, standar emas enkripsi tahan kuantum. Dibangun untuk masa depan di mana kriptografi klasik tidak lagi cukup. Kriptografi pasca-kuantum untuk semua orang.'; + + @override + String get settingsAboutTerms => 'Ketentuan Layanan'; + + @override + String get settingsAboutTermsSubtitle => 'quantus.com/terms/'; + + @override + String get settingsAboutPrivacy => 'Kebijakan privasi'; + + @override + String get settingsAboutPrivacySubtitle => 'quantus.com/privacy-policy/'; + + @override + String get settingsAboutWebsite => 'Kunjungi Situs Web'; + + @override + String get settingsAboutWebsiteSubtitle => 'quantus.com'; + + @override + String settingsAboutVersion(String version, String build) { + return 'Versi $version ($build)'; + } + + @override + String get settingsAccountTypeScreenTitle => 'Jenis Akun'; + + @override + String get settingsAccountTypeIntro => + 'Fitur akun lanjutan akan segera hadir. Fitur ini memberi Anda kontrol lebih besar atas cara transaksi diotorisasi dan diamankan.'; + + @override + String get settingsAccountTypeReversibleTitle => 'Transaksi Reversible'; + + @override + String get settingsAccountTypeReversibleSubtitle => 'Batalkan pengiriman dalam jangka waktu tertentu'; + + @override + String get settingsAccountTypeHighSecurityTitle => 'Akun Keamanan Tinggi'; + + @override + String get settingsAccountTypeHighSecuritySubtitle => 'Persetujuan guardian diperlukan'; + + @override + String get settingsAccountTypeMultiSigTitle => 'Multi-Tanda Tangan'; + + @override + String get settingsAccountTypeMultiSigSubtitle => 'Beberapa persetujuan diperlukan'; + + @override + String get settingsAccountTypeHardwareTitle => 'Dompet Hardware'; + + @override + String get settingsAccountTypeHardwareSubtitle => 'Pasangkan perangkat hardware'; + + @override + String get settingsAccountTypeComingSoon => 'Segera Hadir'; } diff --git a/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart b/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart index e9c369b0..9b033218 100644 --- a/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart +++ b/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/generated/version.g.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -9,20 +12,9 @@ import 'package:resonance_network_wallet/v2/screens/settings/settings_tappable_r import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class AboutQuantusScreenV2 extends StatelessWidget { +class AboutQuantusScreenV2 extends ConsumerWidget { const AboutQuantusScreenV2({super.key}); - static const _kIntro = - 'Quantus is a Layer 1 blockchain secured by ML-DSA Dilithium-5, the gold standard in quantum-resistant encryption. ' - 'Built for a future where classical cryptography is no longer enough. Post-quantum cryptography for everyone.'; - - /// [path] is a path under [AppConstants.websiteBaseUrl] (e.g. `/terms`), or empty for the site root. - static const _externalLinks = <({String title, String subtitle, String path})>[ - (title: 'Terms of Service', subtitle: 'quantus.com/terms/', path: '/terms'), - (title: 'Privacy policy', subtitle: 'quantus.com/privacy-policy/', path: '/privacy-policy'), - (title: 'Visit Website', subtitle: 'quantus.com', path: ''), - ]; - static Uri _uriForAboutLink(({String title, String subtitle, String path}) link) { if (link.path.isEmpty) { return Uri.parse(AppConstants.websiteBaseUrl); @@ -30,29 +22,46 @@ class AboutQuantusScreenV2 extends StatelessWidget { return Uri.parse('${AppConstants.websiteBaseUrl}${link.path}'); } + static List<({String title, String subtitle, String path})> _externalLinks(AppLocalizations l10n) { + return [ + (title: l10n.settingsAboutTerms, subtitle: l10n.settingsAboutTermsSubtitle, path: '/terms'), + ( + title: l10n.settingsAboutPrivacy, + subtitle: l10n.settingsAboutPrivacySubtitle, + path: '/privacy-policy', + ), + (title: l10n.settingsAboutWebsite, subtitle: l10n.settingsAboutWebsiteSubtitle, path: ''), + ]; + } + @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; + final externalLinks = _externalLinks(l10n); return ScaffoldBase( - appBar: const V2AppBar(title: 'About'), + appBar: V2AppBar(title: l10n.settingsAboutScreenTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: ListView( children: [ - Text(_kIntro, style: text.smallParagraph?.copyWith(color: colors.textMuted, height: 1.35)), + Text( + l10n.settingsAboutIntro, + style: text.smallParagraph?.copyWith(color: colors.textMuted, height: 1.35), + ), const SizedBox(height: 40), - for (final entry in _externalLinks.asMap().entries) ...[ + for (final entry in externalLinks.asMap().entries) ...[ SettingsTappableRow( title: entry.value.title, subtitle: entry.value.subtitle, onTap: () => openUrl(_uriForAboutLink(entry.value).toString()), trailing: SettingsTappableRowUtils.externalLink(colors), ), - if (entry.key < _externalLinks.length - 1) const SettingsDivider(), + if (entry.key < externalLinks.length - 1) const SettingsDivider(), ], ], ), @@ -63,7 +72,7 @@ class AboutQuantusScreenV2 extends StatelessWidget { Image.asset('assets/v2/quantus_orange_logo.png', height: 40), const SizedBox(height: 14), Text( - 'Version $appVersion ($appBuildNumber)', + l10n.settingsAboutVersion(appVersion, appBuildNumber), textAlign: TextAlign.center, style: text.paragraph?.copyWith(color: colors.textMuted, fontSize: 16, height: 1.0), ), diff --git a/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart b/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart index fadbdcf0..9f9673c2 100644 --- a/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart @@ -1,41 +1,52 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.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/settings/settings_divider.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class AccountTypeSettingsScreenV2 extends StatelessWidget { +class AccountTypeSettingsScreenV2 extends ConsumerWidget { const AccountTypeSettingsScreenV2({super.key}); - static const _intro = - 'Advanced account features are coming soon. These will give you greater control over how transactions are authorised and secured.'; - - static const _upcomingFeatures = <({String title, String subtitle})>[ - (title: 'Reversible Transactions', subtitle: 'Reverse your sends within a time window'), - (title: 'High Security Account', subtitle: 'Guardian approval required'), - (title: 'Multi-Signature', subtitle: 'Multiple approvals required'), - (title: 'Hardware Wallet', subtitle: 'Pair a hardware device'), - ]; + static List<({String title, String subtitle})> _upcomingFeatures(AppLocalizations l10n) { + return [ + (title: l10n.settingsAccountTypeReversibleTitle, subtitle: l10n.settingsAccountTypeReversibleSubtitle), + ( + title: l10n.settingsAccountTypeHighSecurityTitle, + subtitle: l10n.settingsAccountTypeHighSecuritySubtitle, + ), + (title: l10n.settingsAccountTypeMultiSigTitle, subtitle: l10n.settingsAccountTypeMultiSigSubtitle), + (title: l10n.settingsAccountTypeHardwareTitle, subtitle: l10n.settingsAccountTypeHardwareSubtitle), + ]; + } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; + final upcomingFeatures = _upcomingFeatures(l10n); return ScaffoldBase( - appBar: const V2AppBar(title: 'Account Type'), + appBar: V2AppBar(title: l10n.settingsAccountTypeScreenTitle), mainContent: ListView( children: [ - Text(_intro, style: text.smallParagraph?.copyWith(color: colors.textMuted)), + Text( + l10n.settingsAccountTypeIntro, + style: text.smallParagraph?.copyWith(color: colors.textMuted), + ), const SizedBox(height: 40), - for (var i = 0; i < _upcomingFeatures.length; i++) + for (var i = 0; i < upcomingFeatures.length; i++) _AccountFeatureBlock( colors: colors, text: text, - title: _upcomingFeatures[i].title, - subtitle: _upcomingFeatures[i].subtitle, - showDividerBelow: i < _upcomingFeatures.length - 1, + comingSoonLabel: l10n.settingsAccountTypeComingSoon, + title: upcomingFeatures[i].title, + subtitle: upcomingFeatures[i].subtitle, + showDividerBelow: i < upcomingFeatures.length - 1, ), ], ), @@ -47,6 +58,7 @@ class _AccountFeatureBlock extends StatelessWidget { const _AccountFeatureBlock({ required this.colors, required this.text, + required this.comingSoonLabel, required this.title, required this.subtitle, required this.showDividerBelow, @@ -54,6 +66,7 @@ class _AccountFeatureBlock extends StatelessWidget { final AppColorsV2 colors; final AppTextTheme text; + final String comingSoonLabel; final String title; final String subtitle; final bool showDividerBelow; @@ -80,7 +93,7 @@ class _AccountFeatureBlock extends StatelessWidget { ), ), const SizedBox(width: 24), - _ComingSoonBadge(colors: colors, text: text), + _ComingSoonBadge(colors: colors, text: text, label: comingSoonLabel), ], ), ), @@ -91,10 +104,11 @@ class _AccountFeatureBlock extends StatelessWidget { } class _ComingSoonBadge extends StatelessWidget { - const _ComingSoonBadge({required this.colors, required this.text}); + const _ComingSoonBadge({required this.colors, required this.text, required this.label}); final AppColorsV2 colors; final AppTextTheme text; + final String label; @override Widget build(BuildContext context) { @@ -106,7 +120,7 @@ class _ComingSoonBadge extends StatelessWidget { border: Border.all(color: colors.borderButton), ), alignment: Alignment.center, - child: Text('Coming Soon', style: text.detail?.copyWith(color: colors.textMuted)), + child: Text(label, style: text.detail?.copyWith(color: colors.textMuted)), ); } } diff --git a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart index 18fdf261..ab7e17fb 100644 --- a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/models/fiat_currency.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -41,17 +42,24 @@ class _CurrencyPickerScreenV2State extends ConsumerState @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final selected = ref.watch(selectedFiatCurrencyProvider); final filtered = _filtered(_searchController.text); return ScaffoldBase( - appBar: const V2AppBar(title: 'Currency'), + appBar: V2AppBar(title: l10n.settingsCurrencyTitle), mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _SearchField(controller: _searchController, colors: colors, text: text, onChanged: (_) => setState(() {})), + _SearchField( + controller: _searchController, + colors: colors, + text: text, + hintText: l10n.settingsCurrencySearchHint, + onChanged: (_) => setState(() {}), + ), const SizedBox(height: 24), Expanded( child: Container( @@ -64,7 +72,7 @@ class _CurrencyPickerScreenV2State extends ConsumerState child: filtered.isEmpty ? Center( child: Text( - 'No currencies match your search', + l10n.settingsCurrencyNoMatch, style: text.smallParagraph?.copyWith(color: colors.textMuted), textAlign: TextAlign.center, ), @@ -97,11 +105,18 @@ class _CurrencyPickerScreenV2State extends ConsumerState } class _SearchField extends StatelessWidget { - const _SearchField({required this.controller, required this.colors, required this.text, required this.onChanged}); + const _SearchField({ + required this.controller, + required this.colors, + required this.text, + required this.hintText, + required this.onChanged, + }); final TextEditingController controller; final AppColorsV2 colors; final AppTextTheme text; + final String hintText; final ValueChanged onChanged; @override @@ -123,7 +138,7 @@ class _SearchField extends StatelessWidget { decoration: InputDecoration( isDense: true, border: InputBorder.none, - hintText: 'Search', + hintText: hintText, hintStyle: text.smallParagraph?.copyWith(color: colors.textLabel), ), ), diff --git a/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart b/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart index 2dc2c32b..466d2f17 100644 --- a/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart +++ b/mobile-app/lib/v2/screens/settings/help_and_support_screen.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -7,25 +9,26 @@ import 'package:resonance_network_wallet/v2/screens/settings/settings_divider.da import 'package:resonance_network_wallet/v2/screens/settings/settings_tappable_row.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -class HelpAndSupportScreenV2 extends StatelessWidget { +class HelpAndSupportScreenV2 extends ConsumerWidget { const HelpAndSupportScreenV2({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; return ScaffoldBase( - appBar: const V2AppBar(title: 'Help & Support'), + appBar: V2AppBar(title: l10n.settingsHelpScreenTitle), mainContent: ListView( children: [ _contactBlock( - title: 'Email Support', + title: l10n.settingsHelpEmail, subtitle: AppConstants.emailSupport, colors: colors, onTap: () => openUrl('mailto:${AppConstants.emailSupport}'), ), _contactBlock( - title: 'Telegram', + title: l10n.settingsHelpTelegram, subtitle: AppConstants.telegramHandle, colors: colors, onTap: () => openUrl(AppConstants.communityUrl), diff --git a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart index 9b2eaa11..be2309ce 100644 --- a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart +++ b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; import 'package:resonance_network_wallet/services/mining_rewards_service.dart'; @@ -19,24 +21,31 @@ class MiningRewardsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final miningAsync = ref.watch(miningRewardsProvider); final colors = context.colors; final text = context.themeText; return ScaffoldBase.refreshable( - appBar: const V2AppBar(title: 'Mining Rewards'), + appBar: V2AppBar(title: l10n.settingsMiningTitle), onRefresh: () async => ref.invalidate(miningRewardsProvider), slivers: [ miningAsync.when( - data: (data) => data.totalBlocks > 0 ? _WithRewards(data: data) : const _NoRewards(), - loading: () => const _NoRewards(isLoading: true), - error: (err, _) => - _ErrorState(colors: colors, text: text, onRetry: () => ref.invalidate(miningRewardsProvider)), + data: (data) => data.totalBlocks > 0 ? _WithRewards(data: data) : _NoRewards(l10n: l10n), + loading: () => _NoRewards(l10n: l10n, isLoading: true), + error: (err, _) => _ErrorState( + colors: colors, + text: text, + l10n: l10n, + onRetry: () => ref.invalidate(miningRewardsProvider), + ), ), ], bottomContent: miningAsync.when( data: (data) => data.totalBlocks > 0 - ? const ScaffoldBaseBottomContent(child: QuantusButton.simple(label: 'Redeem', onTap: null)) + ? ScaffoldBaseBottomContent( + child: QuantusButton.simple(label: l10n.settingsMiningRedeem, onTap: null), + ) : null, loading: () => null, error: (err, _) => null, @@ -50,12 +59,9 @@ class _WithRewards extends ConsumerWidget { const _WithRewards({required this.data}); - static const _resonanceSince = 'Jul 2025'; - static const _schrodingerSince = 'Oct 2025'; - static const _diracSince = 'Nov 2025'; - @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final numberFmt = ref.watch(numberFormattingServiceProvider); final quanEarned = numberFmt.formatBalance(data.planckRewards, maxDecimals: 2, addSymbol: true); final redeemedRewards = numberFmt.formatBalance(data.redeemedRewards, maxDecimals: 2, addSymbol: true); @@ -65,19 +71,35 @@ class _WithRewards extends ConsumerWidget { final text = context.themeText; final testnets = [ - _TestnetEntry('Dirac', _diracSince, data.diracBlocks), - _TestnetEntry('Schrödinger', _schrodingerSince, data.schrodingerBlocks), - _TestnetEntry('Resonance', _resonanceSince, data.resonanceBlocks), + _TestnetEntry('Dirac', l10n.settingsMiningDiracSince, data.diracBlocks), + _TestnetEntry('Schrödinger', l10n.settingsMiningSchrodingerSince, data.schrodingerBlocks), + _TestnetEntry('Resonance', l10n.settingsMiningResonanceSince, data.resonanceBlocks), ]; final miningSummaryPairRows = [ _StatPairRow( - left: _MiningStatCell(label: 'TESTNET BLOCKS', value: '${data.totalBlocks}', valueColor: colors.textLightGray), - right: _MiningStatCell(label: 'TESTNET REWARDS', value: quanEarned, valueColor: colors.accentOrange), + left: _MiningStatCell( + label: l10n.settingsMiningStatTestnetBlocks, + value: '${data.totalBlocks}', + valueColor: colors.textLightGray, + ), + right: _MiningStatCell( + label: l10n.settingsMiningStatTestnetRewards, + value: quanEarned, + valueColor: colors.accentOrange, + ), ), _StatPairRow( - left: _MiningStatCell(label: 'REDEEMED', value: redeemedRewards, valueColor: colors.textLightGray), - right: _MiningStatCell(label: 'REDEEMABLE', value: redeemableRewards, valueColor: colors.success), + left: _MiningStatCell( + label: l10n.settingsMiningStatRedeemed, + value: redeemedRewards, + valueColor: colors.textLightGray, + ), + right: _MiningStatCell( + label: l10n.settingsMiningStatRedeemable, + value: redeemableRewards, + valueColor: colors.success, + ), ), ]; @@ -86,9 +108,10 @@ class _WithRewards extends ConsumerWidget { children: [ SplitCard( topChild: _CardTopSection( + l10n: l10n, totalBlocks: data.totalBlocks, totalBlocksColor: colors.textLightGray, - statusLabel: 'Mining', + statusLabel: l10n.settingsMiningStatusMining, statusColor: colors.success, ), bottomChild: Column( @@ -103,13 +126,13 @@ class _WithRewards extends ConsumerWidget { ), const SizedBox(height: 32), for (var i = 0; i < testnets.length; i++) ...[ - _TestnetRow(entry: testnets[i]), + _TestnetRow(entry: testnets[i], blocksLabel: l10n.settingsMiningTestnetBlocks), if (i < testnets.length - 1) Divider(color: colors.toasterBackground, height: 1, thickness: 1), ], const SizedBox(height: 48), Center( child: _OrangeLinkButton( - label: 'View Telemetry ↗', + label: l10n.settingsMiningViewTelemetry, text: text, onTap: () => openUrl(AppConstants.telemetryUrl), ), @@ -121,9 +144,10 @@ class _WithRewards extends ConsumerWidget { } class _NoRewards extends StatelessWidget { + final AppLocalizations l10n; final bool isLoading; - const _NoRewards({this.isLoading = false}); + const _NoRewards({required this.l10n, this.isLoading = false}); @override Widget build(BuildContext context) { @@ -135,14 +159,15 @@ class _NoRewards extends StatelessWidget { children: [ SplitCard( topChild: _CardTopSection( + l10n: l10n, totalBlocks: 0, totalBlocksColor: colors.textTertiary, - statusLabel: 'Pending', + statusLabel: l10n.settingsMiningStatusPending, statusColor: colors.textTertiary, isLoading: isLoading, ), bottomChild: _StatColumn( - label: 'QUAN EARNED', + label: l10n.settingsMiningQuanEarned, value: '0.00', valueColor: colors.textTertiary, isLoading: isLoading, @@ -175,18 +200,18 @@ class _NoRewards extends StatelessWidget { ] else ...[ const SizedBox(height: 64), Text( - 'No mining data yet', + l10n.settingsMiningNoDataTitle, style: text.mediumTitle?.copyWith(fontWeight: FontWeight.w400, color: colors.textMuted), ), const SizedBox(height: 8), Text( - 'Set up a Quantus mining node to start earning rewards.', + l10n.settingsMiningNoDataBody, textAlign: TextAlign.center, style: text.smallParagraph?.copyWith(color: colors.txItemIconDefault, height: 1.35), ), const SizedBox(height: 64), _OrangeLinkButton( - label: 'Mining Setup Guide ↗', + label: l10n.settingsMiningSetupGuide, text: text, onTap: () => openUrl(AppConstants.miningSetupGuideUrl), ), @@ -198,6 +223,7 @@ class _NoRewards extends StatelessWidget { } class _CardTopSection extends StatelessWidget { + final AppLocalizations l10n; final int totalBlocks; final Color totalBlocksColor; final String statusLabel; @@ -205,6 +231,7 @@ class _CardTopSection extends StatelessWidget { final bool isLoading; const _CardTopSection({ + required this.l10n, required this.totalBlocks, required this.totalBlocksColor, required this.statusLabel, @@ -223,7 +250,10 @@ class _CardTopSection extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('BLOCKS MINED', style: text.receiveLabel?.copyWith(color: colors.textLabel)), + Text( + l10n.settingsMiningBlocksMined, + style: text.receiveLabel?.copyWith(color: colors.textLabel), + ), Row( children: [ Container( @@ -246,7 +276,10 @@ class _CardTopSection extends StatelessWidget { else Text('$totalBlocks', style: text.totalMinedBlocks?.copyWith(color: totalBlocksColor)), const SizedBox(height: 4), - Text('blocks across all testnets', style: text.detail?.copyWith(color: colors.textMuted)), + Text( + l10n.settingsMiningBlocksAcrossTestnets, + style: text.detail?.copyWith(color: colors.textMuted), + ), ], ); } @@ -306,7 +339,12 @@ class _StatColumn extends StatelessWidget { FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, - child: Text(value, maxLines: 1, softWrap: false, style: text.sendSectionLabel?.copyWith(color: valueColor)), + child: Text( + value, + maxLines: 1, + softWrap: false, + style: text.sendSectionLabel?.copyWith(color: valueColor), + ), ), ], ); @@ -315,8 +353,9 @@ class _StatColumn extends StatelessWidget { class _TestnetRow extends StatelessWidget { final _TestnetEntry entry; + final String blocksLabel; - const _TestnetRow({required this.entry}); + const _TestnetRow({required this.entry, required this.blocksLabel}); @override Widget build(BuildContext context) { @@ -349,7 +388,7 @@ class _TestnetRow extends StatelessWidget { ), ), const SizedBox(height: 4), - Text('blocks', style: text.detail?.copyWith(color: colors.textMuted)), + Text(blocksLabel, style: text.detail?.copyWith(color: colors.textMuted)), ], ), ], @@ -385,9 +424,15 @@ class _OrangeLinkButton extends StatelessWidget { class _ErrorState extends StatelessWidget { final AppColorsV2 colors; final AppTextTheme text; + final AppLocalizations l10n; final VoidCallback onRetry; - const _ErrorState({required this.colors, required this.text, required this.onRetry}); + const _ErrorState({ + required this.colors, + required this.text, + required this.l10n, + required this.onRetry, + }); @override Widget build(BuildContext context) { @@ -397,14 +442,20 @@ class _ErrorState extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Failed to load mining rewards', style: text.paragraph?.copyWith(color: colors.textPrimary)), + Text( + l10n.settingsMiningLoadError, + style: text.paragraph?.copyWith(color: colors.textPrimary), + ), const SizedBox(height: 8), - Text('Please check your connection', style: text.detail?.copyWith(color: colors.textTertiary)), + Text( + l10n.settingsMiningCheckConnection, + style: text.detail?.copyWith(color: colors.textTertiary), + ), const SizedBox(height: 20), GestureDetector( onTap: onRetry, child: Text( - 'Try Again', + l10n.posQrTryAgain, style: text.smallParagraph?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w600), ), ), diff --git a/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart b/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart index 7d4eb1cb..f890f7f1 100644 --- a/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; import 'package:resonance_network_wallet/providers/notification_config_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.dart'; @@ -30,6 +31,7 @@ class _PreferencesSettingsScreenV2State extends ConsumerState ref.read(posModeProvider.notifier).setPosMode(v), ), const SettingsDivider(), SettingsSwitchRow( - title: 'Notifications', - subtitle: 'Transaction and wallet alerts', + title: l10n.settingsPreferencesNotifications, + subtitle: l10n.settingsPreferencesNotificationsSubtitle, value: notifConfig.enabled, onChanged: _toggleNotifications, ), diff --git a/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart b/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart index 4d86afff..851f15c5 100644 --- a/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart +++ b/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart @@ -1,44 +1,48 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/services/local_auth_service.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/v2/screens/settings/recovery_phrase_screen.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_caution_scaffold.dart'; -class RecoveryPhraseConfirmationScreen extends StatefulWidget { +class RecoveryPhraseConfirmationScreen extends ConsumerStatefulWidget { const RecoveryPhraseConfirmationScreen({super.key, required this.walletIndex}); final int walletIndex; @override - State createState() => _RecoveryPhraseConfirmationScreenState(); + ConsumerState createState() => _RecoveryPhraseConfirmationScreenState(); } -class _RecoveryPhraseConfirmationScreenState extends State { +class _RecoveryPhraseConfirmationScreenState extends ConsumerState { bool _acknowledged = false; Future _onContinue() async { - final authed = await LocalAuthService().authenticate(localizedReason: 'Authenticate to see recovery phrase'); + final l10n = ref.read(l10nProvider); + final authed = await LocalAuthService().authenticate( + localizedReason: l10n.settingsRecoveryConfirmAuthReason, + ); if (authed && mounted) { - Navigator.of( - context, - ).pushReplacement(MaterialPageRoute(builder: (_) => RecoveryPhraseScreen(walletIndex: widget.walletIndex))); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => RecoveryPhraseScreen(walletIndex: widget.walletIndex)), + ); } else { if (mounted) { - context.showErrorToaster(message: 'Authentication required to see recovery phrase'); + context.showErrorToaster(message: l10n.settingsRecoveryConfirmAuthRequired); } - - return; } } @override Widget build(BuildContext context) { - final data = const SettingsCautionScaffoldData.recoveryPhrase(); + final l10n = ref.watch(l10nProvider); return SettingsCautionScaffold( - appBarTitle: 'Recovery Phrase', - data: data, + appBarTitle: l10n.settingsRecoveryPhraseTitle, + data: SettingsCautionScaffoldData.recoveryPhrase(l10n), + continueLabel: l10n.createWalletCautionContinue, checkboxChecked: _acknowledged, onCheckboxChanged: () => setState(() => _acknowledged = !_acknowledged), onContinue: _onContinue, diff --git a/mobile-app/lib/v2/screens/settings/recovery_phrase_screen.dart b/mobile-app/lib/v2/screens/settings/recovery_phrase_screen.dart index c7c37546..57b86aff 100644 --- a/mobile-app/lib/v2/screens/settings/recovery_phrase_screen.dart +++ b/mobile-app/lib/v2/screens/settings/recovery_phrase_screen.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/recovery_phrase_body.dart'; -class RecoveryPhraseScreen extends StatefulWidget { +class RecoveryPhraseScreen extends ConsumerStatefulWidget { const RecoveryPhraseScreen({super.key, this.walletIndex = 0}); final int walletIndex; @override - State createState() => _RecoveryPhraseScreenState(); + ConsumerState createState() => _RecoveryPhraseScreenState(); } -class _RecoveryPhraseScreenState extends State { +class _RecoveryPhraseScreenState extends ConsumerState { final _settingsService = SettingsService(); List _words = []; @@ -32,10 +34,12 @@ class _RecoveryPhraseScreenState extends State { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + return RecoveryPhraseBody( - appBarTitle: 'Recovery Phrase', + appBarTitle: l10n.settingsRecoveryPhraseTitle, words: _words, - primaryButtonLabel: 'Done', + primaryButtonLabel: l10n.settingsRecoveryPhraseDone, onPrimary: () => Navigator.of(context).pop(), ); } diff --git a/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart b/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart index aa6832b8..19e950da 100644 --- a/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart +++ b/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/services/local_auth_service.dart'; import 'package:resonance_network_wallet/services/logout_service.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; @@ -18,31 +19,36 @@ class _ResetConfirmationScreenState extends ConsumerState _resetAndClearData() async { + final l10n = ref.read(l10nProvider); setState(() => _isResetting = true); - final authed = await LocalAuthService().authenticate(localizedReason: 'Authenticate to reset wallet'); + final authed = await LocalAuthService().authenticate( + localizedReason: l10n.settingsResetAuthReason, + ); if (authed && mounted) { try { await ref.read(logoutServiceProvider).logout(context); } catch (e) { if (mounted) { - context.showErrorToaster(message: 'Failed to reset wallet: $e'); + context.showErrorToaster(message: l10n.settingsResetFailed('$e')); } setState(() => _isResetting = false); } } else if (mounted) { - context.showErrorToaster(message: 'Authentication required to reset wallet'); - + context.showErrorToaster(message: l10n.settingsResetAuthRequired); setState(() => _isResetting = false); } } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + return SettingsCautionScaffold( - appBarTitle: 'Reset Wallet', - data: const SettingsCautionScaffoldData.walletReset(), + appBarTitle: l10n.settingsResetTitle, + data: SettingsCautionScaffoldData.walletReset(l10n), + continueLabel: l10n.createWalletCautionContinue, betweenBulletsStyle: SettingsDividerStyle.sectionEmphasis, checkboxChecked: _backedUpChecked, onCheckboxChanged: () => setState(() => _backedUpChecked = !_backedUpChecked), diff --git a/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart b/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart index f4a3eafb..97b957ec 100644 --- a/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/utils/account_utils.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; @@ -8,41 +9,55 @@ import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/screens/settings/recovery_phrase_confirmation_screen.dart'; 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/l10n/app_localizations.dart'; class SelectWalletScreen extends ConsumerWidget { const SelectWalletScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final accountsAsync = ref.watch(accountsProvider); return ScaffoldBase( - appBar: const V2AppBar(title: 'Select Wallet'), + appBar: V2AppBar(title: l10n.settingsSelectWalletTitle), mainContent: accountsAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text('Failed to load wallets', style: text.paragraph?.copyWith(color: colors.textSecondary)), + child: Text( + l10n.settingsWalletFailedToLoad, + style: text.paragraph?.copyWith(color: colors.textSecondary), + ), ), data: (accounts) { final indices = getNonHardwareWalletIndices(accounts); if (indices.isEmpty) { return Center( - child: Text('No wallets found', style: text.paragraph?.copyWith(color: colors.textSecondary)), + child: Text( + l10n.settingsSelectWalletNoWallets, + style: text.paragraph?.copyWith(color: colors.textSecondary), + ), ); } return ListView.separated( itemCount: indices.length, separatorBuilder: (_, _) => const SizedBox(height: 12), - itemBuilder: (_, i) => _walletItem(context, indices[i], colors, text), + itemBuilder: (_, i) => _walletItem(context, l10n, indices[i], colors, text), ); }, ), ); } - Widget _walletItem(BuildContext context, int walletIndex, AppColorsV2 colors, AppTextTheme text) { + Widget _walletItem( + BuildContext context, + AppLocalizations l10n, + int walletIndex, + AppColorsV2 colors, + AppTextTheme text, + ) { return GestureDetector( onTap: () => Navigator.push( context, @@ -55,7 +70,7 @@ class SelectWalletScreen extends ConsumerWidget { children: [ Expanded( child: Text( - 'Wallet ${walletIndex + 1}', + l10n.settingsSelectWalletItem(walletIndex + 1), style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500), ), ), diff --git a/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart b/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart index 8ea50417..0f940820 100644 --- a/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart +++ b/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart'; @@ -14,29 +15,35 @@ class SettingsCautionScaffoldData { final List bulletItems; final String checkboxLabel; - const SettingsCautionScaffoldData({required this.headline, required this.bulletItems, required this.checkboxLabel}); + const SettingsCautionScaffoldData({ + required this.headline, + required this.bulletItems, + required this.checkboxLabel, + }); - const SettingsCautionScaffoldData.recoveryPhrase() - : this( - headline: 'Keep your Recovery Phrase Secret', - bulletItems: const [ - 'If you lose this device, your recovery phrase is the only way back', - 'Anyone who gets hold of it has complete control over your funds, permanently', - 'Write it down and keep it somewhere safe. Do not save it digitally', - ], - checkboxLabel: 'I understand that anyone with my recovery phrase can access my wallet. I will store it safely.', - ); + factory SettingsCautionScaffoldData.recoveryPhrase(AppLocalizations l10n) { + return SettingsCautionScaffoldData( + headline: l10n.createWalletCautionHeadline, + bulletItems: [ + l10n.createWalletCautionBullet1, + l10n.createWalletCautionBullet2, + l10n.createWalletCautionBullet3, + ], + checkboxLabel: l10n.createWalletCautionCheckboxLabel, + ); + } - const SettingsCautionScaffoldData.walletReset() - : this( - headline: 'This will erase\nyour wallet', - bulletItems: const [ - 'All wallet data will be permanently removed from this device', - 'Your funds stay on the blockchain but only your recovery phrase can restore access', - 'Without it, your funds are gone forever', - ], - checkboxLabel: "I've backed up my recovery phrase", - ); + factory SettingsCautionScaffoldData.walletReset(AppLocalizations l10n) { + return SettingsCautionScaffoldData( + headline: l10n.settingsResetCautionHeadline, + bulletItems: [ + l10n.settingsResetCautionBullet1, + l10n.settingsResetCautionBullet2, + l10n.settingsResetCautionBullet3, + ], + checkboxLabel: l10n.settingsResetCautionCheckbox, + ); + } } class SettingsCautionScaffold extends StatelessWidget { @@ -56,7 +63,7 @@ class SettingsCautionScaffold extends StatelessWidget { required this.onCheckboxChanged, required this.onContinue, required this.data, - this.continueLabel = 'Continue', + this.continueLabel = '', this.betweenBulletsStyle = SettingsDividerStyle.list, this.continueButtonLoading = false, }); diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index 718bf4ab..40bd0f8b 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:resonance_network_wallet/generated/version.g.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; @@ -15,8 +17,6 @@ import 'package:resonance_network_wallet/v2/screens/settings/mining_rewards_scre import 'package:resonance_network_wallet/v2/screens/settings/wallet_settings_screen.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -const _miningRewardsTitle = 'Mining Rewards'; - class SettingsScreenV2 extends ConsumerStatefulWidget { const SettingsScreenV2({super.key}); @@ -27,27 +27,39 @@ class SettingsScreenV2 extends ConsumerStatefulWidget { class _SettingsScreenV2State extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final miningAsync = ref.watch(miningRewardsProvider); final colors = context.colors; final trailing = SettingsTappableRowUtils.chevron(colors); - final entries = _settingsHubItems(colors); + final entries = _settingsHubItems(colors, l10n); return ScaffoldBase( - appBar: const V2AppBar(title: 'Settings'), + appBar: V2AppBar(title: l10n.settingsTitle), mainContent: ListView( children: [ for (final e in entries.asMap().entries) ...[ - if (e.value.title == _miningRewardsTitle) + if (e.value.isMiningRewards) miningAsync.when( - data: (data) => - _buildTappableRow(e.value, subtitle: '${data.totalBlocks} blocks mined', trailing: trailing), - loading: () => _buildTappableRow(e.value, subtitle: 'Loading...', trailing: trailing), + data: (data) => _buildTappableRow( + e.value, + subtitle: l10n.settingsMiningRewardsSubtitle(data.totalBlocks), + trailing: trailing, + ), + loading: () => _buildTappableRow( + e.value, + subtitle: l10n.accountsSheetLoading, + trailing: trailing, + ), error: (err, st) { debugPrint('Error getting mining rewards: ${err.toString()}'); debugPrint('Stack trace: ${st.toString()}'); - return _buildTappableRow(e.value, subtitle: 'Error getting mining rewards', trailing: trailing); + return _buildTappableRow( + e.value, + subtitle: l10n.settingsMiningRewardsError, + trailing: trailing, + ); }, ) else @@ -59,60 +71,69 @@ class _SettingsScreenV2State extends ConsumerState { ); } - Widget _buildTappableRow(_SettingsHubItem item, {required Widget trailing, String? subtitle}) => SettingsTappableRow( - leading: item.leading, - title: item.title, - subtitle: subtitle ?? item.subtitle, - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => item.page)), - trailing: trailing, - ); + Widget _buildTappableRow(_SettingsHubItem item, {required Widget trailing, String? subtitle}) => + SettingsTappableRow( + leading: item.leading, + title: item.title, + subtitle: subtitle ?? item.subtitle, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => item.page)), + trailing: trailing, + ); } class _SettingsHubItem { - const _SettingsHubItem({required this.leading, required this.title, required this.subtitle, required this.page}); + const _SettingsHubItem({ + required this.leading, + required this.title, + required this.subtitle, + required this.page, + this.isMiningRewards = false, + }); final Widget leading; final String title; final String subtitle; final Widget page; + final bool isMiningRewards; } -List<_SettingsHubItem> _settingsHubItems(AppColorsV2 colors) { +List<_SettingsHubItem> _settingsHubItems(AppColorsV2 colors, AppLocalizations l10n) { return [ _SettingsHubItem( leading: _settingsHubIcon(colors, icon: Icons.account_balance_wallet_outlined), - title: 'Wallet', - subtitle: 'Recovery Phrase, Reset Wallet', + title: l10n.settingsWalletTitle, + subtitle: l10n.settingsWalletSubtitle, page: const WalletSettingsScreenV2(), ), _SettingsHubItem( leading: _settingsHubIcon(colors, icon: Icons.tune), - title: 'Preferences', - subtitle: 'Currency, POS mode, notifications', + title: l10n.settingsPreferencesTitle, + subtitle: l10n.settingsPreferencesSubtitle, page: const PreferencesSettingsScreenV2(), ), _SettingsHubItem( leading: _settingsHubIcon(colors, svg: SvgPicture.asset('assets/v2/axe.svg', width: 18, height: 18)), - title: _miningRewardsTitle, - subtitle: 'Loading...', + title: l10n.settingsMiningRewards, + subtitle: l10n.accountsSheetLoading, page: const MiningRewardsScreen(), + isMiningRewards: true, ), _SettingsHubItem( leading: _settingsHubIcon(colors, icon: Icons.shield_outlined), - title: 'Account Type', - subtitle: 'Advanced Account Features', + title: l10n.settingsAccountTypeTitle, + subtitle: l10n.settingsAccountTypeSubtitle, page: const AccountTypeSettingsScreenV2(), ), _SettingsHubItem( leading: _settingsHubIcon(colors, icon: Icons.help_outline), - title: 'Help & Support', - subtitle: 'FAQs, Contact the team', + title: l10n.settingsHelpTitle, + subtitle: l10n.settingsHelpSubtitle, page: const HelpAndSupportScreenV2(), ), _SettingsHubItem( leading: _settingsHubIcon(colors, svg: SvgPicture.asset('assets/v2/uppercase_q.svg', width: 18, height: 18)), - title: 'About Quantus', - subtitle: 'Version $appVersion ($appBuildNumber)', + title: l10n.settingsAboutTitle, + subtitle: l10n.settingsAboutHubSubtitle(appVersion, appBuildNumber), page: const AboutQuantusScreenV2(), ), ]; diff --git a/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart index 585603cd..5f665b1e 100644 --- a/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart +++ b/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; import 'package:resonance_network_wallet/services/mining_rewards_service.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; @@ -8,37 +9,45 @@ 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'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; class TestnetRewardsScreen extends ConsumerWidget { const TestnetRewardsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final miningAsync = ref.watch(miningRewardsProvider); return ScaffoldBase( - appBar: const V2AppBar(title: 'Testnet Rewards'), + appBar: V2AppBar(title: l10n.settingsTestnetTitle), mainContent: miningAsync.when( skipLoadingOnRefresh: false, data: (data) => RefreshIndicator( onRefresh: () async => ref.invalidate(miningRewardsProvider), - child: _buildContent(data, colors, text), + child: _buildContent(l10n, data, colors, text), ), loading: () => const Center(child: Loader()), error: (_, _) => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Failed to load testnet rewards', style: text.paragraph?.copyWith(color: colors.textPrimary)), + Text( + l10n.settingsTestnetLoadError, + style: text.paragraph?.copyWith(color: colors.textPrimary), + ), const SizedBox(height: 8), - Text('Please check your connection', style: text.detail?.copyWith(color: colors.textTertiary)), + Text( + l10n.settingsMiningCheckConnection, + style: text.detail?.copyWith(color: colors.textTertiary), + ), const SizedBox(height: 20), GestureDetector( onTap: () => ref.invalidate(miningRewardsProvider), child: Text( - 'Try Again', + l10n.posQrTryAgain, style: text.smallParagraph?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w600), ), ), @@ -49,7 +58,12 @@ class TestnetRewardsScreen extends ConsumerWidget { ); } - Widget _buildContent(MiningRewardsData data, AppColorsV2 colors, AppTextTheme text) { + Widget _buildContent( + AppLocalizations l10n, + MiningRewardsData data, + AppColorsV2 colors, + AppTextTheme text, + ) { final testnets = [ ('Planck', data.planckBlocks), ('Dirac', data.diracBlocks), @@ -69,11 +83,14 @@ class TestnetRewardsScreen extends ConsumerWidget { const Text('💰', style: TextStyle(fontSize: 40)), const SizedBox(height: 12), Text( - '${data.totalBlocks} blocks', + l10n.settingsTestnetTotalBlocks(data.totalBlocks), style: text.largeTitle?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w700), ), const SizedBox(height: 8), - Text('Total blocks mined across all testnets', style: text.detail?.copyWith(color: colors.textTertiary)), + Text( + l10n.settingsTestnetTotalDescription, + style: text.detail?.copyWith(color: colors.textTertiary), + ), ], ), ), @@ -81,7 +98,7 @@ class TestnetRewardsScreen extends ConsumerWidget { Padding( padding: const EdgeInsets.only(bottom: 16), child: Text( - 'Breakdown', + l10n.settingsTestnetBreakdown, style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w600), ), ), @@ -92,16 +109,21 @@ class TestnetRewardsScreen extends ConsumerWidget { children: [ for (var i = 0; i < testnets.length; i++) ...[ if (i > 0) const SettingsDivider(style: SettingsDividerStyle.cardInterior), - Row( children: [ Expanded( - child: Text(testnets[i].$1, style: text.paragraph?.copyWith(color: colors.textPrimary)), + child: Text( + testnets[i].$1, + style: text.paragraph?.copyWith(color: colors.textPrimary), + ), ), const Text('💰 ', style: TextStyle(fontSize: 14)), Text( - '${testnets[i].$2} blocks', - style: text.smallParagraph?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w600), + l10n.settingsTestnetRowBlocks(testnets[i].$2), + style: text.smallParagraph?.copyWith( + color: colors.accentGreen, + fontWeight: FontWeight.w600, + ), ), ], ), diff --git a/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart b/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart index d956e94b..b5508bdf 100644 --- a/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/shared/utils/account_utils.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; @@ -24,9 +25,10 @@ class WalletSettingsScreenV2 extends ConsumerStatefulWidget { class _WalletSettingsScreenV2State extends ConsumerState { void _navigateToRecoveryPhrase(List accounts) { + final l10n = ref.read(l10nProvider); final walletIndices = getNonHardwareWalletIndices(accounts); if (walletIndices.isEmpty) { - context.showErrorToaster(message: 'No wallets found'); + context.showErrorToaster(message: l10n.settingsWalletNoWalletsFound); return; } @@ -46,6 +48,7 @@ class _WalletSettingsScreenV2State extends ConsumerState @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final titleColor = colors.textError; @@ -54,25 +57,28 @@ class _WalletSettingsScreenV2State extends ConsumerState final accountsAsync = ref.watch(accountsProvider); return ScaffoldBase( - appBar: const V2AppBar(title: 'Wallet'), + appBar: V2AppBar(title: l10n.settingsWalletTitle), mainContent: accountsAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text('Failed to load wallets', style: text.paragraph?.copyWith(color: colors.textSecondary)), + child: Text( + l10n.settingsWalletFailedToLoad, + style: text.paragraph?.copyWith(color: colors.textSecondary), + ), ), data: (accounts) => ListView( children: [ SettingsTappableRow( - title: 'Recovery Phrase', - subtitle: 'View your 24-word Backup Password', + title: l10n.settingsWalletRecoveryPhrase, + subtitle: l10n.settingsWalletRecoveryPhraseSubtitle, onTap: () => _navigateToRecoveryPhrase(accounts), trailing: SettingsTappableRowUtils.chevron(colors), ), const SettingsDivider(style: SettingsDividerStyle.walletSection), SettingsTappableRow( - title: 'Reset Wallet', + title: l10n.settingsWalletReset, titleColor: titleColor, - subtitle: 'Removes all data from this device', + subtitle: l10n.settingsWalletResetSubtitle, subtitleColor: subtitleColor, onTap: _showResetConfirmation, trailing: SettingsTappableRowUtils.chevron(colors, color: titleColor), From c8a86e981f4908f74ff4a22c32c4e647a41568c0 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 19:15:16 +0800 Subject: [PATCH 10/25] feat: localized swap screens --- mobile-app/lib/l10n/app_en.arb | 183 +++++++++++++++++ mobile-app/lib/l10n/app_id.arb | 39 +++- mobile-app/lib/l10n/app_localizations.dart | 192 ++++++++++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 110 ++++++++++ mobile-app/lib/l10n/app_localizations_id.dart | 110 ++++++++++ .../lib/v2/screens/swap/deposit_screen.dart | 124 +++++------ .../swap/refund_address_picker_sheet.dart | 16 +- .../v2/screens/swap/review_quote_sheet.dart | 26 ++- .../lib/v2/screens/swap/swap_screen.dart | 56 ++--- .../v2/screens/swap/token_picker_sheet.dart | 25 ++- 10 files changed, 773 insertions(+), 108 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 8745c8ce..b2b60886 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -1313,5 +1313,188 @@ "settingsAccountTypeComingSoon": "Coming Soon", "@settingsAccountTypeComingSoon": { "description": "Coming soon badge on account type features" + }, + + "swapTitle": "Swap", + "@swapTitle": { + "description": "App bar title on swap screens" + }, + "swapFrom": "From", + "@swapFrom": { + "description": "From token section label on swap screen" + }, + "swapTo": "To", + "@swapTo": { + "description": "To token section label on swap screen" + }, + "swapRefundAddress": "Refund Address", + "@swapRefundAddress": { + "description": "Refund address field label on swap screen" + }, + "swapRefundAddressHint": "{network} Address", + "@swapRefundAddressHint": { + "description": "Refund address field hint", + "placeholders": { + "network": { + "type": "String" + } + } + }, + "swapSlippageTolerance": "Slippage Tolerance", + "@swapSlippageTolerance": { + "description": "Slippage tolerance label on swap screen" + }, + "swapRate": "Rate", + "@swapRate": { + "description": "Exchange rate label on swap screen" + }, + "swapGetQuote": "Get a Quote", + "@swapGetQuote": { + "description": "Get quote button on swap screen" + }, + "swapRateLabel": "1 QUAN = {amount} {symbol}", + "@swapRateLabel": { + "description": "Exchange rate display", + "placeholders": { + "amount": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } + }, + "swapRateZero": "1 QUAN = 0 {symbol}", + "@swapRateZero": { + "description": "Exchange rate when amount is zero", + "placeholders": { + "symbol": { + "type": "String" + } + } + }, + + "swapTokenPickerTitle": "Select Token", + "@swapTokenPickerTitle": { + "description": "Title on token picker sheet" + }, + "swapTokenPickerLoadError": "Failed to load tokens", + "@swapTokenPickerLoadError": { + "description": "Error when token list fails to load" + }, + + "swapReviewTitle": "Review Quote", + "@swapReviewTitle": { + "description": "Title on review quote sheet" + }, + "swapReviewTotalFees": "Total fees", + "@swapReviewTotalFees": { + "description": "Total fees row on review quote sheet" + }, + "swapReviewTotalAmount": "Total Amount", + "@swapReviewTotalAmount": { + "description": "Total amount row on review quote sheet" + }, + "swapReviewSlippageWarning": "You could receive up to ${amount} less based on the {percent}% slippage you set", + "@swapReviewSlippageWarning": { + "description": "Slippage warning on review quote sheet", + "placeholders": { + "amount": { + "type": "String" + }, + "percent": { + "type": "String" + } + } + }, + "swapReviewConfirm": "Confirm", + "@swapReviewConfirm": { + "description": "Confirm button on review quote sheet" + }, + + "swapDepositAmount": "Deposit Amount", + "@swapDepositAmount": { + "description": "Deposit amount label on deposit screen" + }, + "swapDepositAmountCopied": "Deposit amount copied to clipboard", + "@swapDepositAmountCopied": { + "description": "Toast when deposit amount is copied" + }, + "swapDepositDemoWarning": "For demo purposes only - do not send funds!", + "@swapDepositDemoWarning": { + "description": "Demo warning on deposit screen" + }, + "swapDepositShareQr": "Share QR", + "@swapDepositShareQr": { + "description": "Share QR button on deposit screen" + }, + "swapDepositShareContent": "Network: {network}\nToken: {token}\nAddress: {address}", + "@swapDepositShareContent": { + "description": "Share text for deposit details", + "placeholders": { + "network": { + "type": "String" + }, + "token": { + "type": "String" + }, + "address": { + "type": "String" + } + } + }, + "swapDepositNotice": "Use your {symbol} or {network} wallet to deposit funds. Depositing other assets may result in loss of funds.", + "@swapDepositNotice": { + "description": "Deposit wallet notice on deposit screen", + "placeholders": { + "symbol": { + "type": "String" + }, + "network": { + "type": "String" + } + } + }, + "swapDepositProcessingTitle": "Processing Swap", + "@swapDepositProcessingTitle": { + "description": "Title while swap is processing" + }, + "swapDepositProcessingBody": "This may take a few minutes...", + "@swapDepositProcessingBody": { + "description": "Body while swap is processing" + }, + "swapDepositCompleteTitle": "Swap Complete", + "@swapDepositCompleteTitle": { + "description": "Title when swap is complete" + }, + "swapDepositCompleteBody": "Your swap for {amount} QUAN is complete.", + "@swapDepositCompleteBody": { + "description": "Body when swap is complete", + "placeholders": { + "amount": { + "type": "String" + } + } + }, + "swapDepositTestnetBanner": "DEMO ONLY - WE ARE STILL ON TESTNET", + "@swapDepositTestnetBanner": { + "description": "Testnet demo banner on deposit screen" + }, + "swapDepositSentFunds": "I've sent the funds", + "@swapDepositSentFunds": { + "description": "Button to confirm funds sent" + }, + "swapDepositDone": "Done", + "@swapDepositDone": { + "description": "Done button after swap completes" + }, + + "swapRefundPickerTitle": "Refund Addresses", + "@swapRefundPickerTitle": { + "description": "Title on refund address picker sheet" + }, + "swapRefundPickerEmpty": "No recent refund addresses", + "@swapRefundPickerEmpty": { + "description": "Empty state on refund address picker" } } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index 2a80ba8c..85726a6a 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -315,5 +315,42 @@ "settingsAccountTypeMultiSigSubtitle": "Beberapa persetujuan diperlukan", "settingsAccountTypeHardwareTitle": "Dompet Hardware", "settingsAccountTypeHardwareSubtitle": "Pasangkan perangkat hardware", - "settingsAccountTypeComingSoon": "Segera Hadir" + "settingsAccountTypeComingSoon": "Segera Hadir", + + "swapTitle": "Tukar", + "swapFrom": "Dari", + "swapTo": "Ke", + "swapRefundAddress": "Alamat Refund", + "swapRefundAddressHint": "Alamat {network}", + "swapSlippageTolerance": "Toleransi Slippage", + "swapRate": "Kurs", + "swapGetQuote": "Dapatkan Penawaran", + "swapRateLabel": "1 QUAN = {amount} {symbol}", + "swapRateZero": "1 QUAN = 0 {symbol}", + + "swapTokenPickerTitle": "Pilih Token", + "swapTokenPickerLoadError": "Gagal memuat token", + + "swapReviewTitle": "Tinjau Penawaran", + "swapReviewTotalFees": "Total biaya", + "swapReviewTotalAmount": "Jumlah Total", + "swapReviewSlippageWarning": "Anda bisa menerima hingga ${amount} lebih sedikit berdasarkan slippage {percent}% yang Anda atur", + "swapReviewConfirm": "Konfirmasi", + + "swapDepositAmount": "Jumlah Deposit", + "swapDepositAmountCopied": "Jumlah deposit disalin ke clipboard", + "swapDepositDemoWarning": "Hanya untuk demo - jangan kirim dana!", + "swapDepositShareQr": "Bagikan QR", + "swapDepositShareContent": "Jaringan: {network}\nToken: {token}\nAlamat: {address}", + "swapDepositNotice": "Gunakan dompet {symbol} atau {network} Anda untuk deposit. Menyetor aset lain dapat mengakibatkan kehilangan dana.", + "swapDepositProcessingTitle": "Memproses Swap", + "swapDepositProcessingBody": "Ini mungkin memakan waktu beberapa menit...", + "swapDepositCompleteTitle": "Swap Selesai", + "swapDepositCompleteBody": "Swap Anda untuk {amount} QUAN telah selesai.", + "swapDepositTestnetBanner": "HANYA DEMO - KAMI MASIH DI TESTNET", + "swapDepositSentFunds": "Saya sudah mengirim dana", + "swapDepositDone": "Selesai", + + "swapRefundPickerTitle": "Alamat Refund", + "swapRefundPickerEmpty": "Tidak ada alamat refund terbaru" } diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 20d7fa56..44d64370 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -1753,6 +1753,198 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Coming Soon'** String get settingsAccountTypeComingSoon; + + /// App bar title on swap screens + /// + /// In en, this message translates to: + /// **'Swap'** + String get swapTitle; + + /// From token section label on swap screen + /// + /// In en, this message translates to: + /// **'From'** + String get swapFrom; + + /// To token section label on swap screen + /// + /// In en, this message translates to: + /// **'To'** + String get swapTo; + + /// Refund address field label on swap screen + /// + /// In en, this message translates to: + /// **'Refund Address'** + String get swapRefundAddress; + + /// Refund address field hint + /// + /// In en, this message translates to: + /// **'{network} Address'** + String swapRefundAddressHint(String network); + + /// Slippage tolerance label on swap screen + /// + /// In en, this message translates to: + /// **'Slippage Tolerance'** + String get swapSlippageTolerance; + + /// Exchange rate label on swap screen + /// + /// In en, this message translates to: + /// **'Rate'** + String get swapRate; + + /// Get quote button on swap screen + /// + /// In en, this message translates to: + /// **'Get a Quote'** + String get swapGetQuote; + + /// Exchange rate display + /// + /// In en, this message translates to: + /// **'1 QUAN = {amount} {symbol}'** + String swapRateLabel(String amount, String symbol); + + /// Exchange rate when amount is zero + /// + /// In en, this message translates to: + /// **'1 QUAN = 0 {symbol}'** + String swapRateZero(String symbol); + + /// Title on token picker sheet + /// + /// In en, this message translates to: + /// **'Select Token'** + String get swapTokenPickerTitle; + + /// Error when token list fails to load + /// + /// In en, this message translates to: + /// **'Failed to load tokens'** + String get swapTokenPickerLoadError; + + /// Title on review quote sheet + /// + /// In en, this message translates to: + /// **'Review Quote'** + String get swapReviewTitle; + + /// Total fees row on review quote sheet + /// + /// In en, this message translates to: + /// **'Total fees'** + String get swapReviewTotalFees; + + /// Total amount row on review quote sheet + /// + /// In en, this message translates to: + /// **'Total Amount'** + String get swapReviewTotalAmount; + + /// Slippage warning on review quote sheet + /// + /// In en, this message translates to: + /// **'You could receive up to \${amount} less based on the {percent}% slippage you set'** + String swapReviewSlippageWarning(String amount, String percent); + + /// Confirm button on review quote sheet + /// + /// In en, this message translates to: + /// **'Confirm'** + String get swapReviewConfirm; + + /// Deposit amount label on deposit screen + /// + /// In en, this message translates to: + /// **'Deposit Amount'** + String get swapDepositAmount; + + /// Toast when deposit amount is copied + /// + /// In en, this message translates to: + /// **'Deposit amount copied to clipboard'** + String get swapDepositAmountCopied; + + /// Demo warning on deposit screen + /// + /// In en, this message translates to: + /// **'For demo purposes only - do not send funds!'** + String get swapDepositDemoWarning; + + /// Share QR button on deposit screen + /// + /// In en, this message translates to: + /// **'Share QR'** + String get swapDepositShareQr; + + /// Share text for deposit details + /// + /// In en, this message translates to: + /// **'Network: {network}\nToken: {token}\nAddress: {address}'** + String swapDepositShareContent(String network, String token, String address); + + /// Deposit wallet notice on deposit screen + /// + /// In en, this message translates to: + /// **'Use your {symbol} or {network} wallet to deposit funds. Depositing other assets may result in loss of funds.'** + String swapDepositNotice(String symbol, String network); + + /// Title while swap is processing + /// + /// In en, this message translates to: + /// **'Processing Swap'** + String get swapDepositProcessingTitle; + + /// Body while swap is processing + /// + /// In en, this message translates to: + /// **'This may take a few minutes...'** + String get swapDepositProcessingBody; + + /// Title when swap is complete + /// + /// In en, this message translates to: + /// **'Swap Complete'** + String get swapDepositCompleteTitle; + + /// Body when swap is complete + /// + /// In en, this message translates to: + /// **'Your swap for {amount} QUAN is complete.'** + String swapDepositCompleteBody(String amount); + + /// Testnet demo banner on deposit screen + /// + /// In en, this message translates to: + /// **'DEMO ONLY - WE ARE STILL ON TESTNET'** + String get swapDepositTestnetBanner; + + /// Button to confirm funds sent + /// + /// In en, this message translates to: + /// **'I\'ve sent the funds'** + String get swapDepositSentFunds; + + /// Done button after swap completes + /// + /// In en, this message translates to: + /// **'Done'** + String get swapDepositDone; + + /// Title on refund address picker sheet + /// + /// In en, this message translates to: + /// **'Refund Addresses'** + String get swapRefundPickerTitle; + + /// Empty state on refund address picker + /// + /// In en, this message translates to: + /// **'No recent refund addresses'** + String get swapRefundPickerEmpty; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 4857012b..3615d558 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -900,4 +900,114 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsAccountTypeComingSoon => 'Coming Soon'; + + @override + String get swapTitle => 'Swap'; + + @override + String get swapFrom => 'From'; + + @override + String get swapTo => 'To'; + + @override + String get swapRefundAddress => 'Refund Address'; + + @override + String swapRefundAddressHint(String network) { + return '$network Address'; + } + + @override + String get swapSlippageTolerance => 'Slippage Tolerance'; + + @override + String get swapRate => 'Rate'; + + @override + String get swapGetQuote => 'Get a Quote'; + + @override + String swapRateLabel(String amount, String symbol) { + return '1 QUAN = $amount $symbol'; + } + + @override + String swapRateZero(String symbol) { + return '1 QUAN = 0 $symbol'; + } + + @override + String get swapTokenPickerTitle => 'Select Token'; + + @override + String get swapTokenPickerLoadError => 'Failed to load tokens'; + + @override + String get swapReviewTitle => 'Review Quote'; + + @override + String get swapReviewTotalFees => 'Total fees'; + + @override + String get swapReviewTotalAmount => 'Total Amount'; + + @override + String swapReviewSlippageWarning(String amount, String percent) { + return 'You could receive up to \$$amount less based on the $percent% slippage you set'; + } + + @override + String get swapReviewConfirm => 'Confirm'; + + @override + String get swapDepositAmount => 'Deposit Amount'; + + @override + String get swapDepositAmountCopied => 'Deposit amount copied to clipboard'; + + @override + String get swapDepositDemoWarning => 'For demo purposes only - do not send funds!'; + + @override + String get swapDepositShareQr => 'Share QR'; + + @override + String swapDepositShareContent(String network, String token, String address) { + return 'Network: $network\nToken: $token\nAddress: $address'; + } + + @override + String swapDepositNotice(String symbol, String network) { + return 'Use your $symbol or $network wallet to deposit funds. Depositing other assets may result in loss of funds.'; + } + + @override + String get swapDepositProcessingTitle => 'Processing Swap'; + + @override + String get swapDepositProcessingBody => 'This may take a few minutes...'; + + @override + String get swapDepositCompleteTitle => 'Swap Complete'; + + @override + String swapDepositCompleteBody(String amount) { + return 'Your swap for $amount QUAN is complete.'; + } + + @override + String get swapDepositTestnetBanner => 'DEMO ONLY - WE ARE STILL ON TESTNET'; + + @override + String get swapDepositSentFunds => 'I\'ve sent the funds'; + + @override + String get swapDepositDone => 'Done'; + + @override + String get swapRefundPickerTitle => 'Refund Addresses'; + + @override + String get swapRefundPickerEmpty => 'No recent refund addresses'; } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index ea1fb690..c28a6abc 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -901,4 +901,114 @@ class AppLocalizationsId extends AppLocalizations { @override String get settingsAccountTypeComingSoon => 'Segera Hadir'; + + @override + String get swapTitle => 'Tukar'; + + @override + String get swapFrom => 'Dari'; + + @override + String get swapTo => 'Ke'; + + @override + String get swapRefundAddress => 'Alamat Refund'; + + @override + String swapRefundAddressHint(String network) { + return 'Alamat $network'; + } + + @override + String get swapSlippageTolerance => 'Toleransi Slippage'; + + @override + String get swapRate => 'Kurs'; + + @override + String get swapGetQuote => 'Dapatkan Penawaran'; + + @override + String swapRateLabel(String amount, String symbol) { + return '1 QUAN = $amount $symbol'; + } + + @override + String swapRateZero(String symbol) { + return '1 QUAN = 0 $symbol'; + } + + @override + String get swapTokenPickerTitle => 'Pilih Token'; + + @override + String get swapTokenPickerLoadError => 'Gagal memuat token'; + + @override + String get swapReviewTitle => 'Tinjau Penawaran'; + + @override + String get swapReviewTotalFees => 'Total biaya'; + + @override + String get swapReviewTotalAmount => 'Jumlah Total'; + + @override + String swapReviewSlippageWarning(String amount, String percent) { + return 'Anda bisa menerima hingga \$$amount lebih sedikit berdasarkan slippage $percent% yang Anda atur'; + } + + @override + String get swapReviewConfirm => 'Konfirmasi'; + + @override + String get swapDepositAmount => 'Jumlah Deposit'; + + @override + String get swapDepositAmountCopied => 'Jumlah deposit disalin ke clipboard'; + + @override + String get swapDepositDemoWarning => 'Hanya untuk demo - jangan kirim dana!'; + + @override + String get swapDepositShareQr => 'Bagikan QR'; + + @override + String swapDepositShareContent(String network, String token, String address) { + return 'Jaringan: $network\nToken: $token\nAlamat: $address'; + } + + @override + String swapDepositNotice(String symbol, String network) { + return 'Gunakan dompet $symbol atau $network Anda untuk deposit. Menyetor aset lain dapat mengakibatkan kehilangan dana.'; + } + + @override + String get swapDepositProcessingTitle => 'Memproses Swap'; + + @override + String get swapDepositProcessingBody => 'Ini mungkin memakan waktu beberapa menit...'; + + @override + String get swapDepositCompleteTitle => 'Swap Selesai'; + + @override + String swapDepositCompleteBody(String amount) { + return 'Swap Anda untuk $amount QUAN telah selesai.'; + } + + @override + String get swapDepositTestnetBanner => 'HANYA DEMO - KAMI MASIH DI TESTNET'; + + @override + String get swapDepositSentFunds => 'Saya sudah mengirim dana'; + + @override + String get swapDepositDone => 'Selesai'; + + @override + String get swapRefundPickerTitle => 'Alamat Refund'; + + @override + String get swapRefundPickerEmpty => 'Tidak ada alamat refund terbaru'; } diff --git a/mobile-app/lib/v2/screens/swap/deposit_screen.dart b/mobile-app/lib/v2/screens/swap/deposit_screen.dart index 3d56652f..f6773af0 100644 --- a/mobile-app/lib/v2/screens/swap/deposit_screen.dart +++ b/mobile-app/lib/v2/screens/swap/deposit_screen.dart @@ -1,6 +1,8 @@ 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/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; @@ -11,16 +13,17 @@ import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/components/success_check.dart'; 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/l10n/app_localizations.dart'; -class DepositScreen extends StatefulWidget { +class DepositScreen extends ConsumerStatefulWidget { final SwapOrder order; const DepositScreen({super.key, required this.order}); @override - State createState() => _DepositScreenState(); + ConsumerState createState() => _DepositScreenState(); } -class _DepositScreenState extends State { +class _DepositScreenState extends ConsumerState { final _swapService = SwapService(); late SwapOrder _order; bool _confirming = false; @@ -42,6 +45,7 @@ class _DepositScreenState extends State { }); _pollStatus(); } catch (e) { + debugPrint('Confirm funds sent failed: $e'); setState(() => _confirming = false); } } @@ -54,21 +58,23 @@ class _DepositScreenState extends State { final updated = await _swapService.getSwapStatus(_order.orderId); if (!mounted) return; setState(() => _order = updated); - } catch (_) {} + } catch (e) { + debugPrint('Swap status poll failed: $e'); + } } } - String _getDepositAddress() { - // return _order.depositAddress - return 'For demo purposes only - do not send funds!'; + String _getDepositAddress(AppLocalizations l10n) { + return l10n.swapDepositDemoWarning; } - void _copyAddress() { - context.copyTextWithToaster(_getDepositAddress()); + void _copyAddress(AppLocalizations l10n) { + context.copyTextWithToaster(_getDepositAddress(l10n)); } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final quote = _order.quote; @@ -76,38 +82,46 @@ class _DepositScreenState extends State { return ScaffoldBase( appBar: V2AppBar( - title: 'Swap', + title: l10n.swapTitle, trailing: Icon(Icons.info_outline, color: colors.textPrimary, size: 24), ), mainContent: Column( children: [ if (_order.status == SwapStatus.complete) - _completedBody(colors, text) + _completedBody(l10n, colors, text) else if (_order.status == SwapStatus.processing) - _processingBody(colors, text) + _processingBody(l10n, colors, text) else - _depositBody(colors, text, quote, usd), + _depositBody(l10n, colors, text, quote, usd), const Spacer(), - if (_order.status == SwapStatus.depositing) _sentButton(colors, text), - if (_order.status == SwapStatus.complete) _doneButton(colors, text), + if (_order.status == SwapStatus.depositing) _sentButton(l10n, colors, text), + if (_order.status == SwapStatus.complete) _doneButton(l10n, colors, text), const SizedBox(height: 24), ], ), ); } - Widget _depositBody(AppColorsV2 colors, AppTextTheme text, SwapQuote quote, double usd) { + Widget _depositBody( + AppLocalizations l10n, + AppColorsV2 colors, + AppTextTheme text, + SwapQuote quote, + double usd, + ) { + final demoWarning = l10n.swapDepositDemoWarning; + return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Deposit Amount', style: text.smallParagraph?.copyWith(color: colors.textPrimary, height: 1.35)), + Text(l10n.swapDepositAmount, style: text.smallParagraph?.copyWith(color: colors.textPrimary, height: 1.35)), const SizedBox(width: 6), GestureDetector( onTap: () => context.copyTextWithToaster( SwapService.formatTokenAmount(quote.totalAmount, quote.fromToken), - message: 'Deposit amount copied to clipboard', + message: l10n.swapDepositAmountCopied, ), child: Container( width: 20, @@ -141,9 +155,6 @@ class _DepositScreenState extends State { child: Container( color: Colors.white, padding: const EdgeInsets.all(8), - - /// for now this QR Code is invalid so people don't transfer by accident - // child: QrImageView(data: _getDepositAddress(), version: QrVersions.auto, size: 184), child: QrImageView(data: 'Quantum secure bitcoin - quantus!', version: QrVersions.auto, size: 184), ), ), @@ -152,10 +163,8 @@ class _DepositScreenState extends State { width: 264, child: Stack( children: [ - // for now put invalid address so people don't transfer by accident Text( - // _getDepositAddress().toLowerCase(), - 'For demo purposes only - do not send funds!', + demoWarning, style: text.smallParagraph?.copyWith( color: colors.textPrimary, fontWeight: FontWeight.w500, @@ -167,7 +176,7 @@ class _DepositScreenState extends State { right: 0, top: 19, child: GestureDetector( - onTap: _copyAddress, + onTap: () => _copyAddress(l10n), child: Container( width: 20, height: 20, @@ -184,22 +193,26 @@ class _DepositScreenState extends State { children: [ Expanded( child: QuantusButton.simple( - label: 'Copy', + label: l10n.receiveCopy, variant: ButtonVariant.transparent, - onTap: _copyAddress, + onTap: () => _copyAddress(l10n), icon: Icon(Icons.copy, color: colors.textPrimary, size: 20), ), ), const SizedBox(width: 16), Expanded( child: QuantusButton.simple( - label: 'Share QR', + label: l10n.swapDepositShareQr, icon: Icon(Icons.qr_code, color: colors.textPrimary, size: 20), variant: ButtonVariant.transparent, onTap: () { shareText( context, - 'Network: ${_order.quote.fromToken.network}\nToken: ${_order.quote.fromToken.symbol}\nAddress: ${_getDepositAddress()}', + l10n.swapDepositShareContent( + _order.quote.fromToken.network, + _order.quote.fromToken.symbol, + _getDepositAddress(l10n), + ), ); }, ), @@ -207,59 +220,56 @@ class _DepositScreenState extends State { ], ), const SizedBox(height: 40), - Text.rich( - TextSpan( - style: text.detail?.copyWith(color: colors.textSecondary, height: 1.35), - children: [ - const TextSpan(text: 'Use your '), - TextSpan( - text: quote.fromToken.symbol, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - const TextSpan(text: ' or '), - TextSpan( - text: quote.fromToken.network, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - const TextSpan(text: ' wallet to deposit funds. Depositing other assets may result in loss of funds.'), - ], - ), + Text( + l10n.swapDepositNotice(quote.fromToken.symbol, quote.fromToken.network), + style: text.detail?.copyWith(color: colors.textSecondary, height: 1.35), textAlign: TextAlign.center, ), ], ); } - Widget _processingBody(AppColorsV2 colors, AppTextTheme text) { + Widget _processingBody(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( children: [ const SizedBox(height: 80), Loader(color: colors.accentGreen), const SizedBox(height: 32), - Text('Processing Swap', style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20)), + Text( + l10n.swapDepositProcessingTitle, + style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20), + ), const SizedBox(height: 12), - Text('This may take a few minutes...', style: text.paragraph?.copyWith(color: colors.textSecondary)), + Text( + l10n.swapDepositProcessingBody, + style: text.paragraph?.copyWith(color: colors.textSecondary), + ), ], ); } - Widget _completedBody(AppColorsV2 colors, AppTextTheme text) { + Widget _completedBody(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { + final amount = SwapService.formatTokenAmount(_order.quote.toAmount, _order.quote.toToken); + return Column( children: [ const SizedBox(height: 80), const SuccessCheck(), const SizedBox(height: 32), - Text('Swap Complete', style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20)), + Text( + l10n.swapDepositCompleteTitle, + style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20), + ), const SizedBox(height: 12), Text( - 'Your swap for ${SwapService.formatTokenAmount(_order.quote.toAmount, _order.quote.toToken)} QUAN is complete.', + l10n.swapDepositCompleteBody(amount), style: text.paragraph?.copyWith(color: colors.textSecondary), textAlign: TextAlign.center, ), const SizedBox(height: 40), if (AppConstants.stillOnTestnet) Text( - 'DEMO ONLY - WE ARE STILL ON TESTNET', + l10n.swapDepositTestnetBanner, style: text.paragraph?.copyWith(color: Colors.yellow), textAlign: TextAlign.center, ), @@ -267,18 +277,18 @@ class _DepositScreenState extends State { ); } - Widget _sentButton(AppColorsV2 colors, AppTextTheme text) { + Widget _sentButton(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return QuantusButton.simple( - label: "I've sent the funds", + label: l10n.swapDepositSentFunds, onTap: _confirmSent, variant: ButtonVariant.secondary, isLoading: _confirming, ); } - Widget _doneButton(AppColorsV2 colors, AppTextTheme text) { + Widget _doneButton(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return QuantusButton.simple( - label: 'Done', + label: l10n.swapDepositDone, onTap: () => Navigator.popUntil(context, (r) => r.isFirst), variant: ButtonVariant.secondary, ); diff --git a/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart b/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart index fa58ecca..1d1db629 100644 --- a/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; @@ -8,15 +10,15 @@ Future showRefundAddressPickerSheet(BuildContext context, String networ return BottomSheetContainer.show(context, builder: (_) => _RefundAddressPickerContent(network: network)); } -class _RefundAddressPickerContent extends StatefulWidget { +class _RefundAddressPickerContent extends ConsumerStatefulWidget { final String network; const _RefundAddressPickerContent({required this.network}); @override - State<_RefundAddressPickerContent> createState() => _RefundAddressPickerContentState(); + ConsumerState<_RefundAddressPickerContent> createState() => _RefundAddressPickerContentState(); } -class _RefundAddressPickerContentState extends State<_RefundAddressPickerContent> { +class _RefundAddressPickerContentState extends ConsumerState<_RefundAddressPickerContent> { List _addresses = []; @override @@ -32,11 +34,12 @@ class _RefundAddressPickerContentState extends State<_RefundAddressPickerContent @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; return BottomSheetContainer( - title: 'Refund Addresses', + title: l10n.swapRefundPickerTitle, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -49,7 +52,10 @@ class _RefundAddressPickerContentState extends State<_RefundAddressPickerContent if (_addresses.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 32), - child: Text('No recent refund addresses', style: text.detail?.copyWith(color: colors.textTertiary)), + child: Text( + l10n.swapRefundPickerEmpty, + style: text.detail?.copyWith(color: colors.textTertiary), + ), ) else ConstrainedBox( diff --git a/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart b/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart index 9f80add9..422eaf7f 100644 --- a/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/review_quote_sheet.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/token_icon.dart'; import 'package:resonance_network_wallet/v2/screens/swap/deposit_screen.dart'; 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/l10n/app_localizations.dart'; void showReviewQuoteSheet(BuildContext context, SwapQuote quote, String refundAddress) { BottomSheetContainer.show( @@ -14,13 +17,14 @@ void showReviewQuoteSheet(BuildContext context, SwapQuote quote, String refundAd ); } -class _ReviewQuoteContent extends StatelessWidget { +class _ReviewQuoteContent extends ConsumerWidget { final SwapQuote quote; final String refundAddress; const _ReviewQuoteContent({required this.quote, required this.refundAddress}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final swapService = SwapService(); @@ -28,7 +32,7 @@ class _ReviewQuoteContent extends StatelessWidget { final toUsd = quote.toAmount * swapService.getUsdPrice(quote.toToken); return BottomSheetContainer( - title: 'Review Quote', + title: l10n.swapReviewTitle, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -36,14 +40,14 @@ class _ReviewQuoteContent extends StatelessWidget { _swapVisual(context, colors, text, fromUsd, toUsd), const SizedBox(height: 48), _feeRow( - 'Total fees', + l10n.swapReviewTotalFees, '${SwapService.formatTokenAmount(quote.networkFee, quote.fromToken)} ${quote.fromToken.symbol}', colors, text, ), Divider(color: colors.separator, height: 32), _feeRow( - 'Total Amount', + l10n.swapReviewTotalAmount, '${SwapService.formatTokenAmount(quote.totalAmount, quote.fromToken)} ${quote.fromToken.symbol}', colors, text, @@ -51,11 +55,14 @@ class _ReviewQuoteContent extends StatelessWidget { ), const SizedBox(height: 24), Text( - 'You could receive up to \$${(quote.fromAmount * quote.slippageTolerance).toStringAsFixed(2)} less based on the ${(quote.slippageTolerance * 100).toStringAsFixed(0)}% slippage you set', + l10n.swapReviewSlippageWarning( + (quote.fromAmount * quote.slippageTolerance).toStringAsFixed(2), + (quote.slippageTolerance * 100).toStringAsFixed(0), + ), style: text.tiny?.copyWith(color: colors.textSecondary, height: 1.35), ), const SizedBox(height: 24), - _confirmButton(context, colors, text), + _confirmButton(context, l10n, colors, text), ], ), ); @@ -130,10 +137,9 @@ class _ReviewQuoteContent extends StatelessWidget { ); } - Widget _confirmButton(BuildContext context, AppColorsV2 colors, AppTextTheme text) { + Widget _confirmButton(BuildContext context, AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return QuantusButton.simple( - label: 'Confirm', - + label: l10n.swapReviewConfirm, onTap: () async { final swapService = SwapService(); final order = await swapService.createSwap(quote); diff --git a/mobile-app/lib/v2/screens/swap/swap_screen.dart b/mobile-app/lib/v2/screens/swap/swap_screen.dart index 1fc1a4bc..6e07a6c6 100644 --- a/mobile-app/lib/v2/screens/swap/swap_screen.dart +++ b/mobile-app/lib/v2/screens/swap/swap_screen.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/qr_scanner_page.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; @@ -12,14 +15,14 @@ import 'package:resonance_network_wallet/v2/screens/swap/token_picker_sheet.dart import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class SwapScreen extends StatefulWidget { +class SwapScreen extends ConsumerStatefulWidget { const SwapScreen({super.key}); @override - State createState() => _SwapScreenState(); + ConsumerState createState() => _SwapScreenState(); } -class _SwapScreenState extends State { +class _SwapScreenState extends ConsumerState { static const _qrIconAsset = 'assets/v2/swap_qr_code.svg'; static const _historyIconAsset = 'assets/v2/swap_clock_counter_clockwise.svg'; static const _swapDirectionIconAsset = 'assets/v2/swap_arrows_down_up.svg'; @@ -34,9 +37,10 @@ class _SwapScreenState extends State { bool _loading = false; double get _rate => _swapService.getRate(_fromToken); - String get _rateLabel { + + String _rateLabel(AppLocalizations l10n) { final val = 1 / _rate; - if (val == 0) return '1 QUAN = 0 ${_fromToken.symbol}'; + if (val == 0) return l10n.swapRateZero(_fromToken.symbol); final decimals = val >= 100 ? 2 : val >= 1 @@ -48,7 +52,7 @@ class _SwapScreenState extends State { : 10; var formatted = val.toStringAsFixed(decimals).replaceAll(RegExp(r'0+$'), ''); if (formatted.endsWith('.')) formatted = formatted.substring(0, formatted.length - 1); - return '1 QUAN = $formatted ${_fromToken.symbol}'; + return l10n.swapRateLabel(formatted, _fromToken.symbol); } @override @@ -90,6 +94,7 @@ class _SwapScreenState extends State { _swapService.addRefundAddress(_fromToken.network, _addressController.text.trim()); showReviewQuoteSheet(context, quote, _addressController.text); } catch (e) { + debugPrint('Swap quote failed: $e'); setState(() => _loading = false); } } @@ -117,12 +122,13 @@ class _SwapScreenState extends State { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; return ScaffoldBase( appBar: V2AppBar( - title: 'Swap', + title: l10n.swapTitle, trailing: Icon(Icons.info_outline, color: colors.textPrimary, size: 24), ), mainContent: Column( @@ -133,32 +139,32 @@ class _SwapScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _fromSection(colors, text), + _fromSection(l10n, colors, text), const SizedBox(height: 32), - _refundAddressSection(colors, text), + _refundAddressSection(l10n, colors, text), const SizedBox(height: 32), _swapDivider(colors), const SizedBox(height: 32), - _toSection(colors, text), + _toSection(l10n, colors, text), const SizedBox(height: 32), - _infoSection(colors, text), + _infoSection(l10n, colors, text), ], ), ), ), const SizedBox(height: 16), - _quoteButton(colors, text), + _quoteButton(l10n, colors, text), const SizedBox(height: 24), ], ), ); } - Widget _fromSection(AppColorsV2 colors, AppTextTheme text) { + Widget _fromSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('From', style: text.smallParagraph?.copyWith(color: colors.textPrimary)), + Text(l10n.swapFrom, style: text.smallParagraph?.copyWith(color: colors.textPrimary)), const SizedBox(height: 12), Row( children: [ @@ -234,13 +240,13 @@ class _SwapScreenState extends State { ); } - Widget _refundAddressSection(AppColorsV2 colors, AppTextTheme text) { + Widget _refundAddressSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Text('Refund Address', style: text.smallParagraph?.copyWith(color: colors.textPrimary)), + Text(l10n.swapRefundAddress, style: text.smallParagraph?.copyWith(color: colors.textPrimary)), const SizedBox(width: 4), Icon(Icons.info_outline, color: colors.textSecondary, size: 14), ], @@ -256,7 +262,7 @@ class _SwapScreenState extends State { controller: _addressController, style: text.smallParagraph?.copyWith(color: colors.textPrimary), decoration: InputDecoration( - hintText: '${_fromToken.network} Address', + hintText: l10n.swapRefundAddressHint(_fromToken.network), hintStyle: text.smallParagraph?.copyWith(color: colors.textTertiary), border: InputBorder.none, isDense: true, @@ -322,11 +328,11 @@ class _SwapScreenState extends State { ); } - Widget _toSection(AppColorsV2 colors, AppTextTheme text) { + Widget _toSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('To', style: text.smallParagraph?.copyWith(color: colors.textPrimary)), + Text(l10n.swapTo, style: text.smallParagraph?.copyWith(color: colors.textPrimary)), const SizedBox(height: 12), Row( children: [ @@ -382,13 +388,13 @@ class _SwapScreenState extends State { ); } - Widget _infoSection(AppColorsV2 colors, AppTextTheme text) { + Widget _infoSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Slippage Tolerance', style: text.detail?.copyWith(color: colors.textSecondary)), + Text(l10n.swapSlippageTolerance, style: text.detail?.copyWith(color: colors.textSecondary)), Row( children: [ Text('1%', style: text.detail?.copyWith(color: colors.textSecondary)), @@ -402,9 +408,9 @@ class _SwapScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Rate', style: text.detail?.copyWith(color: colors.textSecondary)), + Text(l10n.swapRate, style: text.detail?.copyWith(color: colors.textSecondary)), Text( - _rateLabel, + _rateLabel(l10n), style: text.detail?.copyWith(color: colors.textSecondary, fontWeight: FontWeight.w500), ), ], @@ -413,10 +419,10 @@ class _SwapScreenState extends State { ); } - Widget _quoteButton(AppColorsV2 colors, AppTextTheme text) { + Widget _quoteButton(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { final enabled = _canGetQuote && !_loading; return QuantusButton.simple( - label: 'Get a Quote', + label: l10n.swapGetQuote, onTap: _getQuote, isDisabled: !enabled, variant: ButtonVariant.secondary, diff --git a/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart b/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart index 7ab5ca73..b7af5c3a 100644 --- a/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/token_picker_sheet.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; import 'package:resonance_network_wallet/v2/components/loader.dart'; import 'package:resonance_network_wallet/v2/components/token_icon.dart'; 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/l10n/app_localizations.dart'; typedef SwapTokenLoader = Future> Function({bool forceRefresh}); @@ -19,16 +22,16 @@ Future showTokenPickerSheet( ); } -class _TokenPickerContent extends StatefulWidget { +class _TokenPickerContent extends ConsumerStatefulWidget { final SwapTokenLoader loadTokens; final SwapToken current; const _TokenPickerContent({required this.loadTokens, required this.current}); @override - State<_TokenPickerContent> createState() => _TokenPickerContentState(); + ConsumerState<_TokenPickerContent> createState() => _TokenPickerContentState(); } -class _TokenPickerContentState extends State<_TokenPickerContent> { +class _TokenPickerContentState extends ConsumerState<_TokenPickerContent> { final _scrollController = ScrollController(); List _tokens = const []; bool _loading = true; @@ -58,17 +61,19 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { _tokens = tokens; _loading = false; }); - } catch (_) { + } catch (e) { + debugPrint('Token picker load failed: $e'); if (!mounted) return; setState(() { _loading = false; - _error = 'Failed to load tokens'; + _error = ref.read(l10nProvider).swapTokenPickerLoadError; }); } } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final text = context.themeText; final size = MediaQuery.of(context).size; @@ -76,16 +81,16 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { final cardHeight = (height - 120).clamp(360.0, 506.0); return BottomSheetContainer( - title: 'Select Token', + title: l10n.swapTokenPickerTitle, height: cardHeight, child: DefaultTextStyle( style: const TextStyle(decoration: TextDecoration.none), - child: _content(colors, text), + child: _content(l10n, colors, text), ), ); } - Widget _content(AppColorsV2 colors, AppTextTheme text) { + Widget _content(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { if (_loading && _tokens.isEmpty) { return const Center(child: Loader()); } @@ -102,7 +107,7 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { GestureDetector( onTap: () => _loadTokens(forceRefresh: true), child: Text( - 'Retry', + l10n.posQrTryAgain, style: text.paragraph?.copyWith( color: colors.textPrimary, fontWeight: FontWeight.w600, @@ -128,7 +133,7 @@ class _TokenPickerContentState extends State<_TokenPickerContent> { GestureDetector( onTap: () => _loadTokens(forceRefresh: true), child: Text( - 'Retry', + l10n.posQrTryAgain, style: text.detail?.copyWith( color: colors.textPrimary, fontWeight: FontWeight.w600, From d3520043ca7996f945a51b31fafd653e75fab001 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 19:22:32 +0800 Subject: [PATCH 11/25] feat: localized shared components --- mobile-app/lib/l10n/app_en.arb | 25 +++++++++++++ mobile-app/lib/l10n/app_id.arb | 9 ++++- mobile-app/lib/l10n/app_localizations.dart | 36 +++++++++++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 18 ++++++++++ mobile-app/lib/l10n/app_localizations_id.dart | 18 ++++++++++ .../v2/components/address_details_card.dart | 30 ++++++++++------ mobile-app/lib/v2/components/name_field.dart | 9 +++-- .../lib/v2/components/qr_scanner_page.dart | 21 ++++++++--- .../v2/components/share_account_button.dart | 10 ++++-- 9 files changed, 153 insertions(+), 23 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index b2b60886..d4a677cc 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -1496,5 +1496,30 @@ "swapRefundPickerEmpty": "No recent refund addresses", "@swapRefundPickerEmpty": { "description": "Empty state on refund address picker" + }, + + "componentQrScannerNoCode": "No QR code found in image", + "@componentQrScannerNoCode": { + "description": "Snackbar when gallery image has no QR code" + }, + "componentShare": "Share", + "@componentShare": { + "description": "Share button label on account screens" + }, + "componentAddressLabel": "ADDRESS", + "@componentAddressLabel": { + "description": "Address field label on address details card" + }, + "componentCheckphraseLabel": "CHECKPHRASE", + "@componentCheckphraseLabel": { + "description": "Checkphrase field label on address details card" + }, + "componentCheckphraseCopied": "Checkphrase copied", + "@componentCheckphraseCopied": { + "description": "Toast when checkphrase is copied" + }, + "componentNameFieldHint": "Enter a name for your account", + "@componentNameFieldHint": { + "description": "Hint text on account name field" } } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index 85726a6a..50932959 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -352,5 +352,12 @@ "swapDepositDone": "Selesai", "swapRefundPickerTitle": "Alamat Refund", - "swapRefundPickerEmpty": "Tidak ada alamat refund terbaru" + "swapRefundPickerEmpty": "Tidak ada alamat refund terbaru", + + "componentQrScannerNoCode": "Tidak ada kode QR pada gambar", + "componentShare": "Bagikan", + "componentAddressLabel": "ALAMAT", + "componentCheckphraseLabel": "CHECKPHRASE", + "componentCheckphraseCopied": "Checkphrase disalin", + "componentNameFieldHint": "Masukkan nama untuk akun Anda" } diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 44d64370..70ee826c 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -1945,6 +1945,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'No recent refund addresses'** String get swapRefundPickerEmpty; + + /// Snackbar when gallery image has no QR code + /// + /// In en, this message translates to: + /// **'No QR code found in image'** + String get componentQrScannerNoCode; + + /// Share button label on account screens + /// + /// In en, this message translates to: + /// **'Share'** + String get componentShare; + + /// Address field label on address details card + /// + /// In en, this message translates to: + /// **'ADDRESS'** + String get componentAddressLabel; + + /// Checkphrase field label on address details card + /// + /// In en, this message translates to: + /// **'CHECKPHRASE'** + String get componentCheckphraseLabel; + + /// Toast when checkphrase is copied + /// + /// In en, this message translates to: + /// **'Checkphrase copied'** + String get componentCheckphraseCopied; + + /// Hint text on account name field + /// + /// In en, this message translates to: + /// **'Enter a name for your account'** + String get componentNameFieldHint; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 3615d558..68c52277 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -1010,4 +1010,22 @@ class AppLocalizationsEn extends AppLocalizations { @override String get swapRefundPickerEmpty => 'No recent refund addresses'; + + @override + String get componentQrScannerNoCode => 'No QR code found in image'; + + @override + String get componentShare => 'Share'; + + @override + String get componentAddressLabel => 'ADDRESS'; + + @override + String get componentCheckphraseLabel => 'CHECKPHRASE'; + + @override + String get componentCheckphraseCopied => 'Checkphrase copied'; + + @override + String get componentNameFieldHint => 'Enter a name for your account'; } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index c28a6abc..1558adda 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -1011,4 +1011,22 @@ class AppLocalizationsId extends AppLocalizations { @override String get swapRefundPickerEmpty => 'Tidak ada alamat refund terbaru'; + + @override + String get componentQrScannerNoCode => 'Tidak ada kode QR pada gambar'; + + @override + String get componentShare => 'Bagikan'; + + @override + String get componentAddressLabel => 'ALAMAT'; + + @override + String get componentCheckphraseLabel => 'CHECKPHRASE'; + + @override + String get componentCheckphraseCopied => 'Checkphrase disalin'; + + @override + String get componentNameFieldHint => 'Masukkan nama untuk akun Anda'; } diff --git a/mobile-app/lib/v2/components/address_details_card.dart b/mobile-app/lib/v2/components/address_details_card.dart index 06769d9b..ee303a6f 100644 --- a/mobile-app/lib/v2/components/address_details_card.dart +++ b/mobile-app/lib/v2/components/address_details_card.dart @@ -1,22 +1,25 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/v2/components/split_card.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class AddressDetailsCard extends StatefulWidget { +class AddressDetailsCard extends ConsumerStatefulWidget { final String accountId; final String? checksum; const AddressDetailsCard({super.key, required this.accountId, this.checksum}); @override - State createState() => _AddressDetailsCardState(); + ConsumerState createState() => _AddressDetailsCardState(); } -class _AddressDetailsCardState extends State { +class _AddressDetailsCardState extends ConsumerState { bool _addressCopied = false; bool _checksumCopied = false; Timer? _resetTimer; @@ -26,10 +29,10 @@ class _AddressDetailsCardState extends State { _triggerCopied(isAddress: true); } - void _copyChecksum(BuildContext context) { + void _copyChecksum(BuildContext context, AppLocalizations l10n) { if (widget.checksum == null) return; - context.copyTextWithToaster(widget.checksum!, message: 'Checkphrase copied'); + context.copyTextWithToaster(widget.checksum!, message: l10n.componentCheckphraseCopied); _triggerCopied(isAddress: false); } @@ -67,17 +70,24 @@ class _AddressDetailsCardState extends State { @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + return SplitCard( topChild: InkWell( onTap: () => _copyAddress(context), - child: _buildItem(context, 'ADDRESS', widget.accountId, isCopied: _addressCopied), + child: _buildItem( + context, + l10n.componentAddressLabel, + widget.accountId, + isCopied: _addressCopied, + ), ), bottomChild: InkWell( - onTap: () => _copyChecksum(context), + onTap: () => _copyChecksum(context, l10n), child: _buildItem( context, - 'CHECKPHRASE', - widget.checksum ?? 'Loading...', + l10n.componentCheckphraseLabel, + widget.checksum ?? l10n.accountsSheetLoading, isCheckphrase: true, isCopied: _checksumCopied, ), @@ -109,9 +119,7 @@ class _AddressDetailsCardState extends State { ], ), ), - const SizedBox(width: 32), - _copyButton(isCopied: isCopied), ], ); diff --git a/mobile-app/lib/v2/components/name_field.dart b/mobile-app/lib/v2/components/name_field.dart index a63ec2a9..bcdeaae2 100644 --- a/mobile-app/lib/v2/components/name_field.dart +++ b/mobile-app/lib/v2/components/name_field.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -class NameField extends StatelessWidget { +class NameField extends ConsumerWidget { final TextEditingController controller; final String? subtitle; final String? error; @@ -10,7 +12,8 @@ class NameField extends StatelessWidget { const NameField({super.key, required this.controller, this.subtitle, this.error}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); final textStyle = context.themeText.smallTitle!.copyWith(fontWeight: FontWeight.w400); return Column( @@ -25,7 +28,7 @@ class NameField extends StatelessWidget { controller: controller, style: textStyle, decoration: InputDecoration.collapsed( - hintText: 'Enter a name for your account', + hintText: l10n.componentNameFieldHint, hintStyle: textStyle.copyWith(color: context.colors.textSecondary), ), ), diff --git a/mobile-app/lib/v2/components/qr_scanner_page.dart b/mobile-app/lib/v2/components/qr_scanner_page.dart index 4c8f228c..c4682550 100644 --- a/mobile-app/lib/v2/components/qr_scanner_page.dart +++ b/mobile-app/lib/v2/components/qr_scanner_page.dart @@ -1,19 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/quantus_icon_button.dart'; import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -class QrScannerPage extends StatefulWidget { +class QrScannerPage extends ConsumerStatefulWidget { final bool Function(String)? validator; const QrScannerPage({super.key, this.validator}); @override - State createState() => _QrScannerPageState(); + ConsumerState createState() => _QrScannerPageState(); } -class _QrScannerPageState extends State { +class _QrScannerPageState extends ConsumerState { final _controller = MobileScannerController(); bool _scanned = false; @@ -40,12 +42,16 @@ class _QrScannerPageState extends State { if (capture != null) { _onDetect(capture); } else { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('No QR code found in image'))); + final l10n = ref.read(l10nProvider); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.componentQrScannerNoCode)), + ); } } @override Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); final colors = context.colors; final screen = MediaQuery.of(context).size; final frameSize = (screen.width - 112).clamp(220.0, 280.0); @@ -83,7 +89,12 @@ class _QrScannerPageState extends State { ], ), ), - const Positioned(top: 20, left: 24, right: 24, child: V2AppBar(title: 'Scan QR Code')), + Positioned( + top: 20, + left: 24, + right: 24, + child: V2AppBar(title: l10n.addHardwareAccountScanQr), + ), ], ), ); diff --git a/mobile-app/lib/v2/components/share_account_button.dart b/mobile-app/lib/v2/components/share_account_button.dart index e8c7a3be..12dea41a 100644 --- a/mobile-app/lib/v2/components/share_account_button.dart +++ b/mobile-app/lib/v2/components/share_account_button.dart @@ -1,17 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -class ShareAccountButton extends StatelessWidget { +class ShareAccountButton extends ConsumerWidget { final VoidCallback onTap; final bool isDisabled; const ShareAccountButton({super.key, required this.onTap, this.isDisabled = false}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = ref.watch(l10nProvider); + return QuantusButton.simple( - label: 'Share', + label: l10n.componentShare, onTap: onTap, icon: Icon(Icons.shortcut_rounded, size: 20, color: context.colors.background), iconPlacement: IconPlacement.leading, From 005fc556439a43d03ab52078ac12607cc9edbd91 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 19 May 2026 19:42:36 +0800 Subject: [PATCH 12/25] feat: finish integrating language localization --- mobile-app/lib/l10n/app_en.arb | 23 +++- mobile-app/lib/l10n/app_id.arb | 8 +- mobile-app/lib/l10n/app_localizations.dart | 32 ++++- mobile-app/lib/l10n/app_localizations_en.dart | 17 ++- mobile-app/lib/l10n/app_localizations_id.dart | 17 ++- mobile-app/lib/models/app_locale.dart | 34 ++++++ mobile-app/lib/providers/l10n_provider.dart | 40 ++++++- .../lib/providers/wallet_providers.dart | 8 +- .../settings/currency_picker_screen.dart | 113 +++--------------- .../settings/language_picker_screen.dart | 112 +++++++++++++++++ .../settings/preferences_settings_screen.dart | 58 +++++++-- .../settings/settings_picker_widgets.dart | 103 ++++++++++++++++ .../lib/src/services/settings_service.dart | 12 ++ 13 files changed, 458 insertions(+), 119 deletions(-) create mode 100644 mobile-app/lib/models/app_locale.dart create mode 100644 mobile-app/lib/v2/screens/settings/language_picker_screen.dart create mode 100644 mobile-app/lib/v2/screens/settings/settings_picker_widgets.dart diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index d4a677cc..a0d347c5 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -897,7 +897,7 @@ "@settingsPreferencesTitle": { "description": "Preferences row title on settings hub" }, - "settingsPreferencesSubtitle": "Currency, POS mode, notifications", + "settingsPreferencesSubtitle": "Language, currency, POS mode, notifications", "@settingsPreferencesSubtitle": { "description": "Preferences row subtitle on settings hub" }, @@ -1062,6 +1062,14 @@ "@settingsPreferencesCurrencySubtitle": { "description": "Currency row subtitle" }, + "settingsPreferencesLanguage": "Language", + "@settingsPreferencesLanguage": { + "description": "Language row on preferences screen" + }, + "settingsPreferencesLanguageSubtitle": "App display language", + "@settingsPreferencesLanguageSubtitle": { + "description": "Language row subtitle" + }, "settingsPreferencesPosMode": "POS Mode", "@settingsPreferencesPosMode": { "description": "POS mode row on preferences" @@ -1092,6 +1100,19 @@ "description": "Empty state when search has no results" }, + "settingsLanguageTitle": "Language", + "@settingsLanguageTitle": { + "description": "App bar on language picker" + }, + "settingsLanguageSearchHint": "Search", + "@settingsLanguageSearchHint": { + "description": "Search field hint on language picker" + }, + "settingsLanguageNoMatch": "No languages match your search", + "@settingsLanguageNoMatch": { + "description": "Empty state when language search has no results" + }, + "settingsMiningTitle": "Mining Rewards", "@settingsMiningTitle": { "description": "App bar on mining rewards screen" diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index 50932959..6168aede 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -213,7 +213,7 @@ "settingsWalletTitle": "Dompet", "settingsWalletSubtitle": "Frasa Pemulihan, Reset Dompet", "settingsPreferencesTitle": "Preferensi", - "settingsPreferencesSubtitle": "Mata uang, mode POS, notifikasi", + "settingsPreferencesSubtitle": "Bahasa, mata uang, mode POS, notifikasi", "settingsMiningRewards": "Hadiah Mining", "settingsMiningRewardsSubtitle": "{count} blok ditambang", "settingsMiningRewardsError": "Gagal memuat hadiah mining", @@ -251,6 +251,8 @@ "settingsResetCautionBullet3": "Tanpa frasa pemulihan, dana Anda hilang selamanya", "settingsResetCautionCheckbox": "Saya sudah mencadangkan frasa pemulihan saya", + "settingsPreferencesLanguage": "Bahasa", + "settingsPreferencesLanguageSubtitle": "Bahasa tampilan aplikasi", "settingsPreferencesCurrency": "Mata Uang", "settingsPreferencesCurrencySubtitle": "Preferensi tampilan fiat", "settingsPreferencesPosMode": "Mode POS", @@ -262,6 +264,10 @@ "settingsCurrencySearchHint": "Cari", "settingsCurrencyNoMatch": "Tidak ada mata uang yang cocok dengan pencarian Anda", + "settingsLanguageTitle": "Bahasa", + "settingsLanguageSearchHint": "Cari", + "settingsLanguageNoMatch": "Tidak ada bahasa yang cocok dengan pencarian Anda", + "settingsMiningTitle": "Hadiah Mining", "settingsMiningRedeem": "Tukar", "settingsMiningStatusMining": "Mining", diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 70ee826c..32809024 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -1211,7 +1211,7 @@ abstract class AppLocalizations { /// Preferences row subtitle on settings hub /// /// In en, this message translates to: - /// **'Currency, POS mode, notifications'** + /// **'Language, currency, POS mode, notifications'** String get settingsPreferencesSubtitle; /// Mining rewards row title on settings hub @@ -1412,6 +1412,18 @@ abstract class AppLocalizations { /// **'Fiat display preference'** String get settingsPreferencesCurrencySubtitle; + /// Language row on preferences screen + /// + /// In en, this message translates to: + /// **'Language'** + String get settingsPreferencesLanguage; + + /// Language row subtitle + /// + /// In en, this message translates to: + /// **'App display language'** + String get settingsPreferencesLanguageSubtitle; + /// POS mode row on preferences /// /// In en, this message translates to: @@ -1454,6 +1466,24 @@ abstract class AppLocalizations { /// **'No currencies match your search'** String get settingsCurrencyNoMatch; + /// App bar on language picker + /// + /// In en, this message translates to: + /// **'Language'** + String get settingsLanguageTitle; + + /// Search field hint on language picker + /// + /// In en, this message translates to: + /// **'Search'** + String get settingsLanguageSearchHint; + + /// Empty state when language search has no results + /// + /// In en, this message translates to: + /// **'No languages match your search'** + String get settingsLanguageNoMatch; + /// App bar on mining rewards screen /// /// In en, this message translates to: diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 68c52277..fd284afb 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -612,7 +612,7 @@ class AppLocalizationsEn extends AppLocalizations { String get settingsPreferencesTitle => 'Preferences'; @override - String get settingsPreferencesSubtitle => 'Currency, POS mode, notifications'; + String get settingsPreferencesSubtitle => 'Language, currency, POS mode, notifications'; @override String get settingsMiningRewards => 'Mining Rewards'; @@ -722,6 +722,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsPreferencesCurrencySubtitle => 'Fiat display preference'; + @override + String get settingsPreferencesLanguage => 'Language'; + + @override + String get settingsPreferencesLanguageSubtitle => 'App display language'; + @override String get settingsPreferencesPosMode => 'POS Mode'; @@ -743,6 +749,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsCurrencyNoMatch => 'No currencies match your search'; + @override + String get settingsLanguageTitle => 'Language'; + + @override + String get settingsLanguageSearchHint => 'Search'; + + @override + String get settingsLanguageNoMatch => 'No languages match your search'; + @override String get settingsMiningTitle => 'Mining Rewards'; diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index 1558adda..dbc082b5 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -613,7 +613,7 @@ class AppLocalizationsId extends AppLocalizations { String get settingsPreferencesTitle => 'Preferensi'; @override - String get settingsPreferencesSubtitle => 'Mata uang, mode POS, notifikasi'; + String get settingsPreferencesSubtitle => 'Bahasa, mata uang, mode POS, notifikasi'; @override String get settingsMiningRewards => 'Hadiah Mining'; @@ -723,6 +723,12 @@ class AppLocalizationsId extends AppLocalizations { @override String get settingsPreferencesCurrencySubtitle => 'Preferensi tampilan fiat'; + @override + String get settingsPreferencesLanguage => 'Bahasa'; + + @override + String get settingsPreferencesLanguageSubtitle => 'Bahasa tampilan aplikasi'; + @override String get settingsPreferencesPosMode => 'Mode POS'; @@ -744,6 +750,15 @@ class AppLocalizationsId extends AppLocalizations { @override String get settingsCurrencyNoMatch => 'Tidak ada mata uang yang cocok dengan pencarian Anda'; + @override + String get settingsLanguageTitle => 'Bahasa'; + + @override + String get settingsLanguageSearchHint => 'Cari'; + + @override + String get settingsLanguageNoMatch => 'Tidak ada bahasa yang cocok dengan pencarian Anda'; + @override String get settingsMiningTitle => 'Hadiah Mining'; diff --git a/mobile-app/lib/models/app_locale.dart b/mobile-app/lib/models/app_locale.dart new file mode 100644 index 00000000..4d2d2796 --- /dev/null +++ b/mobile-app/lib/models/app_locale.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +/// App UI locales the user can select in settings. +enum AppLocale { + en( + languageCode: 'en', + displayName: 'English', + numberFormatLocale: 'en_US', + ), + id( + languageCode: 'id', + displayName: 'Bahasa Indonesia', + numberFormatLocale: 'id_ID', + ); + + const AppLocale({ + required this.languageCode, + required this.displayName, + required this.numberFormatLocale, + }); + + final String languageCode; + final String displayName; + final String numberFormatLocale; + + Locale get flutterLocale => Locale(languageCode); + + static AppLocale fromCode(String code, {AppLocale fallback = AppLocale.en}) { + return AppLocale.values.firstWhere( + (l) => l.languageCode == code, + orElse: () => fallback, + ); + } +} diff --git a/mobile-app/lib/providers/l10n_provider.dart b/mobile-app/lib/providers/l10n_provider.dart index dae74db4..e8a855c6 100644 --- a/mobile-app/lib/providers/l10n_provider.dart +++ b/mobile-app/lib/providers/l10n_provider.dart @@ -1,11 +1,39 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:resonance_network_wallet/l10n/app_localizations.dart'; // Import your generated file +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:resonance_network_wallet/models/app_locale.dart'; +/// Persists and exposes the user's chosen app locale. +/// Defaults to [AppLocale.en] when no preference has been saved. +/// +/// To change the active locale (e.g. from a settings screen): +/// ref.read(selectedAppLocaleProvider.notifier).select(AppLocale.id); +final selectedAppLocaleProvider = + StateNotifierProvider((ref) { + return SelectedAppLocaleNotifier(SettingsService()); +}); -final localeProvider = StateProvider((ref) => const Locale('en')); +class SelectedAppLocaleNotifier extends StateNotifier { + final SettingsService _settings; + + SelectedAppLocaleNotifier(this._settings) : super(_load(_settings)); + + Future select(AppLocale locale) async { + await _settings.setSelectedAppLocale(locale.languageCode); + state = locale; + } + + static AppLocale _load(SettingsService settings) { + final code = settings.getSelectedAppLocale(); + if (code == null) return AppLocale.en; + return AppLocale.fromCode(code); + } +} + +final localeProvider = Provider((ref) { + return ref.watch(selectedAppLocaleProvider).flutterLocale; +}); final l10nProvider = Provider((ref) { - final locale = ref.watch(localeProvider); - - return lookupAppLocalizations(locale); -}); \ No newline at end of file + return lookupAppLocalizations(ref.watch(localeProvider)); +}); diff --git a/mobile-app/lib/providers/wallet_providers.dart b/mobile-app/lib/providers/wallet_providers.dart index f04e57b9..2dd0f160 100644 --- a/mobile-app/lib/providers/wallet_providers.dart +++ b/mobile-app/lib/providers/wallet_providers.dart @@ -1,8 +1,7 @@ -import 'dart:io' show Platform; - import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/pending_transactions_provider.dart'; final settingsServiceProvider = Provider((ref) { @@ -25,10 +24,9 @@ final recentAddressesServiceProvider = Provider((ref) { return RecentAddressesService(); }); -/// Caveat: snapshots [Platform.localeName] at provider creation time. -/// A mid-session locale change (rare) won't be picked up until app restart. final localeNumberConfigProvider = Provider((ref) { - return LocaleNumberConfig.fromLocale(Platform.localeName); + final appLocale = ref.watch(selectedAppLocaleProvider); + return LocaleNumberConfig.fromLocale(appLocale.numberFormatLocale); }); final numberFormattingServiceProvider = Provider((ref) { diff --git a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart index ab7e17fb..9b0d4ab1 100644 --- a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart @@ -6,6 +6,7 @@ import 'package:resonance_network_wallet/providers/currency_display_provider.dar 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/settings/settings_divider.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_widgets.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; @@ -13,7 +14,8 @@ class CurrencyPickerScreenV2 extends ConsumerStatefulWidget { const CurrencyPickerScreenV2({super.key}); @override - ConsumerState createState() => _CurrencyPickerScreenV2State(); + ConsumerState createState() => + _CurrencyPickerScreenV2State(); } class _CurrencyPickerScreenV2State extends ConsumerState { @@ -53,7 +55,7 @@ class _CurrencyPickerScreenV2State extends ConsumerState mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _SearchField( + SettingsPickerSearchField( controller: _searchController, colors: colors, text: text, @@ -63,7 +65,10 @@ class _CurrencyPickerScreenV2State extends ConsumerState const SizedBox(height: 24), Expanded( child: Container( - decoration: BoxDecoration(color: colors.surfaceDeep, borderRadius: BorderRadius.circular(14)), + decoration: BoxDecoration( + color: colors.surfaceDeep, + borderRadius: BorderRadius.circular(14), + ), clipBehavior: Clip.antiAlias, child: Scrollbar( thumbVisibility: true, @@ -73,21 +78,24 @@ class _CurrencyPickerScreenV2State extends ConsumerState ? Center( child: Text( l10n.settingsCurrencyNoMatch, - style: text.smallParagraph?.copyWith(color: colors.textMuted), + style: text.smallParagraph?.copyWith( + color: colors.textMuted, + ), textAlign: TextAlign.center, ), ) : ListView.separated( itemCount: filtered.length, separatorBuilder: (context, index) => - const SettingsDivider(style: SettingsDividerStyle.currencyList, padding: EdgeInsets.zero), + const SettingsDivider( + style: SettingsDividerStyle.currencyList, + padding: EdgeInsets.zero, + ), itemBuilder: (context, index) { final c = filtered[index]; - final isSelected = c == selected; - - return _CurrencyListTile( + return SettingsPickerListTile( label: c.line, - selected: isSelected, + selected: c == selected, colors: colors, text: text, onTap: () => _onSelect(c), @@ -103,90 +111,3 @@ class _CurrencyPickerScreenV2State extends ConsumerState ); } } - -class _SearchField extends StatelessWidget { - const _SearchField({ - required this.controller, - required this.colors, - required this.text, - required this.hintText, - required this.onChanged, - }); - - final TextEditingController controller; - final AppColorsV2 colors; - final AppTextTheme text; - final String hintText; - final ValueChanged onChanged; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 48, - child: Container( - padding: const EdgeInsets.only(left: 12, right: 8), - decoration: BoxDecoration(color: colors.surfaceDeep, borderRadius: BorderRadius.circular(14)), - child: Row( - children: [ - Icon(Icons.search, size: 18, color: colors.textLabel), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: controller, - onChanged: onChanged, - style: text.smallParagraph, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - hintText: hintText, - hintStyle: text.smallParagraph?.copyWith(color: colors.textLabel), - ), - ), - ), - ], - ), - ), - ); - } -} - -class _CurrencyListTile extends StatelessWidget { - const _CurrencyListTile({ - required this.label, - required this.selected, - required this.colors, - required this.text, - required this.onTap, - }); - - final String label; - final bool selected; - final AppColorsV2 colors; - final AppTextTheme text; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final accent = colors.accentOrange; - final fg = selected ? accent : colors.textPrimary; - - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(24), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Text(label, style: text.paragraph?.copyWith(color: fg, height: 1.2)), - ), - if (selected) ...[const SizedBox(width: 12), Icon(Icons.check, size: 18, color: accent)], - ], - ), - ), - ), - ); - } -} diff --git a/mobile-app/lib/v2/screens/settings/language_picker_screen.dart b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart new file mode 100644 index 00000000..1ce7712e --- /dev/null +++ b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:resonance_network_wallet/models/app_locale.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.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/settings/settings_divider.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_widgets.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; + +class LanguagePickerScreenV2 extends ConsumerStatefulWidget { + const LanguagePickerScreenV2({super.key}); + + @override + ConsumerState createState() => + _LanguagePickerScreenV2State(); +} + +class _LanguagePickerScreenV2State extends ConsumerState { + final _searchController = TextEditingController(); + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + List _filtered(String query) { + final q = query.trim().toLowerCase(); + final list = List.from(AppLocale.values); + if (q.isEmpty) return list; + return list.where((l) { + return l.displayName.toLowerCase().contains(q) || + l.languageCode.toLowerCase().contains(q); + }).toList(); + } + + Future _onSelect(AppLocale locale) async { + await ref.read(selectedAppLocaleProvider.notifier).select(locale); + if (mounted) Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + final colors = context.colors; + final text = context.themeText; + final selected = ref.watch(selectedAppLocaleProvider); + final filtered = _filtered(_searchController.text); + + return ScaffoldBase( + appBar: V2AppBar(title: l10n.settingsLanguageTitle), + mainContent: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SettingsPickerSearchField( + controller: _searchController, + colors: colors, + text: text, + hintText: l10n.settingsLanguageSearchHint, + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + Expanded( + child: Container( + decoration: BoxDecoration( + color: colors.surfaceDeep, + borderRadius: BorderRadius.circular(14), + ), + clipBehavior: Clip.antiAlias, + child: Scrollbar( + thumbVisibility: true, + thickness: 4, + radius: const Radius.circular(25), + child: filtered.isEmpty + ? Center( + child: Text( + l10n.settingsLanguageNoMatch, + style: text.smallParagraph?.copyWith( + color: colors.textMuted, + ), + textAlign: TextAlign.center, + ), + ) + : ListView.separated( + itemCount: filtered.length, + separatorBuilder: (context, index) => + const SettingsDivider( + style: SettingsDividerStyle.currencyList, + padding: EdgeInsets.zero, + ), + itemBuilder: (context, index) { + final locale = filtered[index]; + return SettingsPickerListTile( + label: locale.displayName, + selected: locale == selected, + colors: colors, + text: text, + onTap: () => _onSelect(locale), + ); + }, + ), + ), + ), + ), + const SizedBox(height: 40), + ], + ), + ); + } +} diff --git a/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart b/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart index f890f7f1..8bc9bfce 100644 --- a/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.dart'; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/notification_config_provider.dart'; import 'package:resonance_network_wallet/providers/wallet_providers.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/settings/currency_picker_screen.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/language_picker_screen.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_divider.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_tappable_row.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; @@ -16,17 +17,31 @@ class PreferencesSettingsScreenV2 extends ConsumerStatefulWidget { const PreferencesSettingsScreenV2({super.key}); @override - ConsumerState createState() => _PreferencesSettingsScreenV2State(); + ConsumerState createState() => + _PreferencesSettingsScreenV2State(); } -class _PreferencesSettingsScreenV2State extends ConsumerState { +class _PreferencesSettingsScreenV2State + extends ConsumerState { void _toggleNotifications(bool enable) { final current = ref.read(notificationConfigProvider); - ref.read(notificationConfigProvider.notifier).updateConfig(current.copyWith(enabled: enable)); + ref + .read(notificationConfigProvider.notifier) + .updateConfig(current.copyWith(enabled: enable)); + } + + void _openLanguagePicker() { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const LanguagePickerScreenV2()), + ); } void _openCurrencyPicker() { - Navigator.push(context, MaterialPageRoute(builder: (_) => const CurrencyPickerScreenV2())); + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const CurrencyPickerScreenV2()), + ); } @override @@ -36,12 +51,34 @@ class _PreferencesSettingsScreenV2State extends ConsumerState onChanged; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 48, + child: Container( + padding: const EdgeInsets.only(left: 12, right: 8), + decoration: BoxDecoration( + color: colors.surfaceDeep, + borderRadius: BorderRadius.circular(14), + ), + child: Row( + children: [ + Icon(Icons.search, size: 18, color: colors.textLabel), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: controller, + onChanged: onChanged, + style: text.smallParagraph, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: hintText, + hintStyle: text.smallParagraph?.copyWith( + color: colors.textLabel, + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class SettingsPickerListTile extends StatelessWidget { + const SettingsPickerListTile({ + super.key, + required this.label, + required this.selected, + required this.colors, + required this.text, + required this.onTap, + }); + + final String label; + final bool selected; + final AppColorsV2 colors; + final AppTextTheme text; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final accent = colors.accentOrange; + final fg = selected ? accent : colors.textPrimary; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + label, + style: text.paragraph?.copyWith(color: fg, height: 1.2), + ), + ), + if (selected) ...[ + const SizedBox(width: 12), + Icon(Icons.check, size: 18, color: accent), + ], + ], + ), + ), + ), + ); + } +} diff --git a/quantus_sdk/lib/src/services/settings_service.dart b/quantus_sdk/lib/src/services/settings_service.dart index 684b5c61..d29e4ee3 100644 --- a/quantus_sdk/lib/src/services/settings_service.dart +++ b/quantus_sdk/lib/src/services/settings_service.dart @@ -28,6 +28,7 @@ class SettingsService { static const String _balanceHiddenKey = 'balance_hidden'; static const String _currencyFlippedKey = 'currency_flipped'; static const String _selectedFiatCurrencyKey = 'selected_fiat_currency'; + static const String _selectedAppLocaleKey = 'selected_app_locale'; static const String _lastPausedTimeKey = 'last_paused_time'; @@ -314,6 +315,17 @@ class SettingsService { return _prefs.getString(_selectedFiatCurrencyKey); } + // Selected App Locale Settings + Future setSelectedAppLocale(String languageCode) async { + await _prefs.setString(_selectedAppLocaleKey, languageCode); + } + + /// Returns the persisted language code (e.g. "en", "id"), or null when no + /// preference has been saved yet (caller should fall back to English). + String? getSelectedAppLocale() { + return _prefs.getString(_selectedAppLocaleKey); + } + // POS Mode Settings static const String _posModeEnabledKey = 'pos_mode_enabled'; From cdea84515397ac77b45f5fb81494b62d9b8cb6f8 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 20 May 2026 12:13:56 +0800 Subject: [PATCH 13/25] feat: address review issues - refactor DRY widget - delete redundant provider - fix bad number formatting locale --- mobile-app/lib/app.dart | 5 +- mobile-app/lib/providers/l10n_provider.dart | 51 +++++-- .../v2/screens/activity/activity_screen.dart | 4 +- .../screens/import/import_wallet_screen.dart | 11 +- .../lib/v2/screens/pos/pos_qr_screen.dart | 4 +- .../settings/currency_picker_screen.dart | 128 ++++-------------- .../settings/language_picker_screen.dart | 126 ++++------------- .../settings/settings_picker_screen.dart | 121 +++++++++++++++++ mobile-app/lib/wallet_initializer.dart | 2 +- 9 files changed, 235 insertions(+), 217 deletions(-) create mode 100644 mobile-app/lib/v2/screens/settings/settings_picker_screen.dart diff --git a/mobile-app/lib/app.dart b/mobile-app/lib/app.dart index 26422182..3c4b72dc 100644 --- a/mobile-app/lib/app.dart +++ b/mobile-app/lib/app.dart @@ -43,11 +43,12 @@ class _ResonanceWalletAppState extends ConsumerState { @override Widget build(BuildContext context) { - final locale = ref.watch(localeProvider); + final appLocale = ref.watch(selectedAppLocaleProvider); return MaterialApp( title: 'Quantus Wallet', - locale: locale, + locale: appLocale.flutterLocale, + // Framework widgets only; app strings use l10nProvider (see l10n_provider.dart). localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, navigatorObservers: [TelemetryNavigatorObserver()], diff --git a/mobile-app/lib/providers/l10n_provider.dart b/mobile-app/lib/providers/l10n_provider.dart index e8a855c6..3b7d0bfc 100644 --- a/mobile-app/lib/providers/l10n_provider.dart +++ b/mobile-app/lib/providers/l10n_provider.dart @@ -1,16 +1,53 @@ +/// App locale persistence and localized strings via Riverpod. +/// +/// ## App strings (canonical) +/// +/// Use [l10nProvider] for all user-facing copy. Do **not** use +/// `AppLocalizations.of(context)` in widgets. +/// +/// - **`ref.watch(l10nProvider)`** — in `build` (or anywhere the UI must +/// rebuild when the user changes language). +/// - **`ref.read(l10nProvider)`** — one-off access in callbacks, timers, or +/// `try/catch` handlers where a subscription is wrong or wasteful. Capture +/// localized strings early if a callback can outlive a locale change. +/// +/// ```dart +/// // build — rebuilds on locale change +/// final l10n = ref.watch(l10nProvider); +/// +/// // callback — no subscription +/// onTap: () => context.showErrorToaster( +/// message: ref.read(l10nProvider).someError, +/// ); +/// ``` +/// +/// ## Framework localization +/// +/// [MaterialApp] in `app.dart` sets `locale`, `localizationsDelegates`, and +/// `supportedLocales` so Material/Cupertino built-ins (date pickers, etc.) +/// respect the active locale. App copy still comes from [l10nProvider]. +/// +/// ## Pure logic and tests +/// +/// Pass [AppLocalizations] as a parameter, or call +/// `lookupAppLocalizations(const Locale('en'))` in unit tests. +library; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/models/app_locale.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; + /// Persists and exposes the user's chosen app locale. /// Defaults to [AppLocale.en] when no preference has been saved. /// /// To change the active locale (e.g. from a settings screen): /// ref.read(selectedAppLocaleProvider.notifier).select(AppLocale.id); -final selectedAppLocaleProvider = - StateNotifierProvider((ref) { - return SelectedAppLocaleNotifier(SettingsService()); +final selectedAppLocaleProvider = StateNotifierProvider((ref) { + final settings = ref.watch(settingsServiceProvider); + return SelectedAppLocaleNotifier(settings); }); class SelectedAppLocaleNotifier extends StateNotifier { @@ -30,10 +67,8 @@ class SelectedAppLocaleNotifier extends StateNotifier { } } -final localeProvider = Provider((ref) { - return ref.watch(selectedAppLocaleProvider).flutterLocale; -}); - +/// Localized strings for the active app locale. final l10nProvider = Provider((ref) { - return lookupAppLocalizations(ref.watch(localeProvider)); + final locale = ref.watch(selectedAppLocaleProvider).flutterLocale; + return lookupAppLocalizations(locale); }); diff --git a/mobile-app/lib/v2/screens/activity/activity_screen.dart b/mobile-app/lib/v2/screens/activity/activity_screen.dart index 0970febb..06a941b4 100644 --- a/mobile-app/lib/v2/screens/activity/activity_screen.dart +++ b/mobile-app/lib/v2/screens/activity/activity_screen.dart @@ -44,7 +44,7 @@ class _ActivityScreenState extends ConsumerState { @override Widget build(BuildContext context) { final l10n = ref.watch(l10nProvider); - final locale = ref.watch(localeProvider); + final appLocale = ref.watch(selectedAppLocaleProvider); final colors = context.colors; final text = context.themeText; final accountAsync = ref.watch(activeAccountProvider); @@ -117,7 +117,7 @@ class _ActivityScreenState extends ConsumerState { ), ); } - final grouped = _groupByDate(all, l10n, locale.toString()); + final grouped = _groupByDate(all, l10n, appLocale.numberFormatLocale); return ListView.builder( padding: EdgeInsets.zero, diff --git a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart index d43cd9e5..55ae66f6 100644 --- a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart @@ -5,6 +5,7 @@ import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/remote_config_provider.dart'; import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; +import 'package:resonance_network_wallet/services/telemetry_service.dart'; import 'package:resonance_network_wallet/v2/components/quantus_button.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart'; @@ -114,7 +115,10 @@ class _ImportWalletScreenV2State extends ConsumerState { } ref.invalidate(accountsProvider); ref.invalidate(activeAccountProvider); - } catch (_) {} + } catch (e, st) { + print('error discovering accounts: $e'); + TelemetryService().sendError('Error discovering accounts', error: e, stackTrace: st); + } } @override @@ -132,10 +136,7 @@ class _ImportWalletScreenV2State extends ConsumerState { child: SingleChildScrollView( child: Column( children: [ - Text( - l10n.importWalletDescription, - style: text.smallParagraph?.copyWith(color: colors.textSecondary), - ), + Text(l10n.importWalletDescription, style: text.smallParagraph?.copyWith(color: colors.textSecondary)), const SizedBox(height: 16), Container( height: 202, 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 197aece0..904e71d2 100644 --- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -171,7 +171,7 @@ class _PosQrScreenState extends ConsumerState { @override Widget build(BuildContext context) { final l10n = ref.watch(l10nProvider); - final locale = ref.watch(localeProvider); + final appLocale = ref.watch(selectedAppLocaleProvider); final colors = context.colors; final text = context.themeText; final accountAsync = ref.watch(activeAccountProvider); @@ -202,7 +202,7 @@ class _PosQrScreenState extends ConsumerState { if (_isPaid) { return _buildPaidContent( l10n, - locale.toString(), + appLocale.numberFormatLocale, colors, text, display.primaryAmount, diff --git a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart index 9b0d4ab1..87aa4987 100644 --- a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart @@ -1,113 +1,43 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/models/fiat_currency.dart'; -import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/currency_display_provider.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/settings/settings_divider.dart'; -import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_widgets.dart'; -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/providers/l10n_provider.dart'; +import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_screen.dart'; -class CurrencyPickerScreenV2 extends ConsumerStatefulWidget { +class CurrencyPickerScreenV2 extends ConsumerWidget { const CurrencyPickerScreenV2({super.key}); @override - ConsumerState createState() => - _CurrencyPickerScreenV2State(); -} - -class _CurrencyPickerScreenV2State extends ConsumerState { - final _searchController = TextEditingController(); - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - List _filtered(String query) { - final q = query.trim().toLowerCase(); - final list = List.from(FiatCurrency.values); - if (q.isEmpty) return list; - return list.where((c) { - final line = c.line.toLowerCase(); - return line.contains(q) || c.code.toLowerCase().contains(q); - }).toList(); - } - - Future _onSelect(FiatCurrency c) async { - await ref.read(selectedFiatCurrencyProvider.notifier).select(c); - if (mounted) Navigator.pop(context); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final l10n = ref.watch(l10nProvider); - final colors = context.colors; - final text = context.themeText; final selected = ref.watch(selectedFiatCurrencyProvider); - final filtered = _filtered(_searchController.text); - return ScaffoldBase( - appBar: V2AppBar(title: l10n.settingsCurrencyTitle), - mainContent: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SettingsPickerSearchField( - controller: _searchController, - colors: colors, - text: text, - hintText: l10n.settingsCurrencySearchHint, - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 24), - Expanded( - child: Container( - decoration: BoxDecoration( - color: colors.surfaceDeep, - borderRadius: BorderRadius.circular(14), - ), - clipBehavior: Clip.antiAlias, - child: Scrollbar( - thumbVisibility: true, - thickness: 4, - radius: const Radius.circular(25), - child: filtered.isEmpty - ? Center( - child: Text( - l10n.settingsCurrencyNoMatch, - style: text.smallParagraph?.copyWith( - color: colors.textMuted, - ), - textAlign: TextAlign.center, - ), - ) - : ListView.separated( - itemCount: filtered.length, - separatorBuilder: (context, index) => - const SettingsDivider( - style: SettingsDividerStyle.currencyList, - padding: EdgeInsets.zero, - ), - itemBuilder: (context, index) { - final c = filtered[index]; - return SettingsPickerListTile( - label: c.line, - selected: c == selected, - colors: colors, - text: text, - onTap: () => _onSelect(c), - ); - }, - ), - ), - ), - ), - const SizedBox(height: 40), - ], - ), + return SettingsPickerScreen( + title: l10n.settingsCurrencyTitle, + searchHint: l10n.settingsCurrencySearchHint, + emptyMessage: l10n.settingsCurrencyNoMatch, + items: FiatCurrency.values, + selected: selected, + labelBuilder: (currency) => currency.line, + filter: (currency, query) { + final line = currency.line.toLowerCase(); + return line.contains(query) || currency.code.toLowerCase().contains(query); + }, + onSelect: (currency) async { + try { + await ref.read(selectedFiatCurrencyProvider.notifier).select(currency); + if (context.mounted) { + Navigator.pop(context); + } + } catch (e) { + debugPrint('error selecting locale: $e'); + if (context.mounted) { + context.showErrorToaster(message: 'Error selecting locale: $e'); + } + } + }, ); } } diff --git a/mobile-app/lib/v2/screens/settings/language_picker_screen.dart b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart index 1ce7712e..1d6a8f1f 100644 --- a/mobile-app/lib/v2/screens/settings/language_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart @@ -2,111 +2,41 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/models/app_locale.dart'; import 'package:resonance_network_wallet/providers/l10n_provider.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/settings/settings_divider.dart'; -import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_widgets.dart'; -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/v2/screens/settings/settings_picker_screen.dart'; -class LanguagePickerScreenV2 extends ConsumerStatefulWidget { +class LanguagePickerScreenV2 extends ConsumerWidget { const LanguagePickerScreenV2({super.key}); @override - ConsumerState createState() => - _LanguagePickerScreenV2State(); -} - -class _LanguagePickerScreenV2State extends ConsumerState { - final _searchController = TextEditingController(); - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - List _filtered(String query) { - final q = query.trim().toLowerCase(); - final list = List.from(AppLocale.values); - if (q.isEmpty) return list; - return list.where((l) { - return l.displayName.toLowerCase().contains(q) || - l.languageCode.toLowerCase().contains(q); - }).toList(); - } - - Future _onSelect(AppLocale locale) async { - await ref.read(selectedAppLocaleProvider.notifier).select(locale); - if (mounted) Navigator.pop(context); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final l10n = ref.watch(l10nProvider); - final colors = context.colors; - final text = context.themeText; final selected = ref.watch(selectedAppLocaleProvider); - final filtered = _filtered(_searchController.text); - return ScaffoldBase( - appBar: V2AppBar(title: l10n.settingsLanguageTitle), - mainContent: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SettingsPickerSearchField( - controller: _searchController, - colors: colors, - text: text, - hintText: l10n.settingsLanguageSearchHint, - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 24), - Expanded( - child: Container( - decoration: BoxDecoration( - color: colors.surfaceDeep, - borderRadius: BorderRadius.circular(14), - ), - clipBehavior: Clip.antiAlias, - child: Scrollbar( - thumbVisibility: true, - thickness: 4, - radius: const Radius.circular(25), - child: filtered.isEmpty - ? Center( - child: Text( - l10n.settingsLanguageNoMatch, - style: text.smallParagraph?.copyWith( - color: colors.textMuted, - ), - textAlign: TextAlign.center, - ), - ) - : ListView.separated( - itemCount: filtered.length, - separatorBuilder: (context, index) => - const SettingsDivider( - style: SettingsDividerStyle.currencyList, - padding: EdgeInsets.zero, - ), - itemBuilder: (context, index) { - final locale = filtered[index]; - return SettingsPickerListTile( - label: locale.displayName, - selected: locale == selected, - colors: colors, - text: text, - onTap: () => _onSelect(locale), - ); - }, - ), - ), - ), - ), - const SizedBox(height: 40), - ], - ), + return SettingsPickerScreen( + title: l10n.settingsLanguageTitle, + searchHint: l10n.settingsLanguageSearchHint, + emptyMessage: l10n.settingsLanguageNoMatch, + items: AppLocale.values, + selected: selected, + labelBuilder: (locale) => locale.displayName, + filter: (locale, query) { + return locale.displayName.toLowerCase().contains(query) || + locale.languageCode.toLowerCase().contains(query); + }, + onSelect: (locale) async { + try { + await ref.read(selectedAppLocaleProvider.notifier).select(locale); + if (context.mounted) { + Navigator.pop(context); + } + } catch (e) { + debugPrint('error selecting locale: $e'); + if (context.mounted) { + context.showErrorToaster(message: 'Error selecting locale: $e'); + } + } + }, ); } } diff --git a/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart b/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart new file mode 100644 index 00000000..2cfef5a0 --- /dev/null +++ b/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.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/settings/settings_divider.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_widgets.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; + +/// Searchable list for choosing a single settings value (language, currency, etc.). +class SettingsPickerScreen extends StatefulWidget { + const SettingsPickerScreen({ + super.key, + required this.title, + required this.searchHint, + required this.emptyMessage, + required this.items, + required this.selected, + required this.labelBuilder, + required this.onSelect, + this.filter, + }); + + final String title; + final String searchHint; + final String emptyMessage; + final List items; + final T selected; + final String Function(T) labelBuilder; + final bool Function(T item, String query)? filter; + final Future Function(T) onSelect; + + @override + State> createState() => _SettingsPickerScreenState(); +} + +class _SettingsPickerScreenState extends State> { + final _searchController = TextEditingController(); + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + List _filtered(String query) { + final q = query.trim().toLowerCase(); + if (q.isEmpty) return List.from(widget.items); + return widget.items.where((item) { + if (widget.filter != null) { + return widget.filter!(item, q); + } + return widget.labelBuilder(item).toLowerCase().contains(q); + }).toList(); + } + + @override + Widget build(BuildContext context) { + final colors = context.colors; + final text = context.themeText; + final filtered = _filtered(_searchController.text); + + return ScaffoldBase( + appBar: V2AppBar(title: widget.title), + mainContent: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SettingsPickerSearchField( + controller: _searchController, + colors: colors, + text: text, + hintText: widget.searchHint, + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + Expanded( + child: Container( + decoration: BoxDecoration( + color: colors.surfaceDeep, + borderRadius: BorderRadius.circular(14), + ), + clipBehavior: Clip.antiAlias, + child: Scrollbar( + thumbVisibility: true, + thickness: 4, + radius: const Radius.circular(25), + child: filtered.isEmpty + ? Center( + child: Text( + widget.emptyMessage, + style: text.smallParagraph?.copyWith( + color: colors.textMuted, + ), + textAlign: TextAlign.center, + ), + ) + : ListView.separated( + itemCount: filtered.length, + separatorBuilder: (context, index) => const SettingsDivider( + style: SettingsDividerStyle.currencyList, + padding: EdgeInsets.zero, + ), + itemBuilder: (context, index) { + final item = filtered[index]; + return SettingsPickerListTile( + label: widget.labelBuilder(item), + selected: item == widget.selected, + colors: colors, + text: text, + onTap: () => widget.onSelect(item), + ); + }, + ), + ), + ), + ), + const SizedBox(height: 40), + ], + ), + ); + } +} diff --git a/mobile-app/lib/wallet_initializer.dart b/mobile-app/lib/wallet_initializer.dart index b11f4137..349fdda4 100644 --- a/mobile-app/lib/wallet_initializer.dart +++ b/mobile-app/lib/wallet_initializer.dart @@ -90,7 +90,7 @@ class WalletInitializerState extends ConsumerState { } Future _showMnemonicLostDialog() async { - final l10n = ref.watch(l10nProvider); + final l10n = ref.read(l10nProvider); await BottomSheetContainer.show( context, From 9a4711297d1b599bbe24b5513c16950afcea5624 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 20 May 2026 12:31:07 +0800 Subject: [PATCH 14/25] feat: another review fixes --- mobile-app/lib/l10n/app_en.arb | 18 ++++++++++++++ mobile-app/lib/l10n/app_id.arb | 2 ++ mobile-app/lib/l10n/app_localizations.dart | 12 ++++++++++ mobile-app/lib/l10n/app_localizations_en.dart | 10 ++++++++ mobile-app/lib/l10n/app_localizations_id.dart | 10 ++++++++ .../lib/v2/screens/pos/pos_qr_screen.dart | 2 +- .../settings/currency_picker_screen.dart | 2 +- .../settings/language_picker_screen.dart | 2 +- .../settings/settings_picker_screen.dart | 24 +++++++++---------- 9 files changed, 67 insertions(+), 15 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index a0d347c5..a7d67def 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -1099,6 +1099,15 @@ "@settingsCurrencyNoMatch": { "description": "Empty state when search has no results" }, + "settingsCurrencyError": "Error selecting currency: {error}", + "@settingsCurrencyError": { + "description": "Error when currency selection fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, "settingsLanguageTitle": "Language", "@settingsLanguageTitle": { @@ -1112,6 +1121,15 @@ "@settingsLanguageNoMatch": { "description": "Empty state when language search has no results" }, + "settingsLanguageError": "Error selecting language: {error}", + "@settingsLanguageError": { + "description": "Error when language selection fails", + "placeholders": { + "error": { + "type": "String" + } + } + }, "settingsMiningTitle": "Mining Rewards", "@settingsMiningTitle": { diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index 6168aede..32e7bc10 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -263,10 +263,12 @@ "settingsCurrencyTitle": "Mata Uang", "settingsCurrencySearchHint": "Cari", "settingsCurrencyNoMatch": "Tidak ada mata uang yang cocok dengan pencarian Anda", + "settingsCurrencyError": "Gagal memilih mata uang: {error}", "settingsLanguageTitle": "Bahasa", "settingsLanguageSearchHint": "Cari", "settingsLanguageNoMatch": "Tidak ada bahasa yang cocok dengan pencarian Anda", + "settingsLanguageError": "Gagal memilih bahasa: {error}", "settingsMiningTitle": "Hadiah Mining", "settingsMiningRedeem": "Tukar", diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 32809024..fa150993 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -1466,6 +1466,12 @@ abstract class AppLocalizations { /// **'No currencies match your search'** String get settingsCurrencyNoMatch; + /// Error when currency selection fails + /// + /// In en, this message translates to: + /// **'Error selecting currency: {error}'** + String settingsCurrencyError(String error); + /// App bar on language picker /// /// In en, this message translates to: @@ -1484,6 +1490,12 @@ abstract class AppLocalizations { /// **'No languages match your search'** String get settingsLanguageNoMatch; + /// Error when language selection fails + /// + /// In en, this message translates to: + /// **'Error selecting language: {error}'** + String settingsLanguageError(String error); + /// App bar on mining rewards screen /// /// In en, this message translates to: diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index fd284afb..1497815d 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -749,6 +749,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsCurrencyNoMatch => 'No currencies match your search'; + @override + String settingsCurrencyError(String error) { + return 'Error selecting currency: $error'; + } + @override String get settingsLanguageTitle => 'Language'; @@ -758,6 +763,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsLanguageNoMatch => 'No languages match your search'; + @override + String settingsLanguageError(String error) { + return 'Error selecting language: $error'; + } + @override String get settingsMiningTitle => 'Mining Rewards'; diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index dbc082b5..eca6a295 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -750,6 +750,11 @@ class AppLocalizationsId extends AppLocalizations { @override String get settingsCurrencyNoMatch => 'Tidak ada mata uang yang cocok dengan pencarian Anda'; + @override + String settingsCurrencyError(String error) { + return 'Gagal memilih mata uang: $error'; + } + @override String get settingsLanguageTitle => 'Bahasa'; @@ -759,6 +764,11 @@ class AppLocalizationsId extends AppLocalizations { @override String get settingsLanguageNoMatch => 'Tidak ada bahasa yang cocok dengan pencarian Anda'; + @override + String settingsLanguageError(String error) { + return 'Gagal memilih bahasa: $error'; + } + @override String get settingsMiningTitle => 'Hadiah Mining'; 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 904e71d2..d615a482 100644 --- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -55,7 +55,7 @@ class _PosQrScreenState extends ConsumerState { void _startWatching() { final l10n = ref.read(l10nProvider); - final formattingService = ref.watch(numberFormattingServiceProvider); + final formattingService = ref.read(numberFormattingServiceProvider); final active = ref.read(activeAccountProvider).value; if (active == null) return; diff --git a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart index 87aa4987..3f0cb175 100644 --- a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart @@ -34,7 +34,7 @@ class CurrencyPickerScreenV2 extends ConsumerWidget { } catch (e) { debugPrint('error selecting locale: $e'); if (context.mounted) { - context.showErrorToaster(message: 'Error selecting locale: $e'); + context.showErrorToaster(message: l10n.settingsCurrencyError(e.toString())); } } }, diff --git a/mobile-app/lib/v2/screens/settings/language_picker_screen.dart b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart index 1d6a8f1f..6355d4c4 100644 --- a/mobile-app/lib/v2/screens/settings/language_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart @@ -33,7 +33,7 @@ class LanguagePickerScreenV2 extends ConsumerWidget { } catch (e) { debugPrint('error selecting locale: $e'); if (context.mounted) { - context.showErrorToaster(message: 'Error selecting locale: $e'); + context.showErrorToaster(message: l10n.settingsLanguageError(e.toString())); } } }, diff --git a/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart b/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart index 2cfef5a0..3a7d950a 100644 --- a/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart @@ -35,6 +35,7 @@ class SettingsPickerScreen extends StatefulWidget { class _SettingsPickerScreenState extends State> { final _searchController = TextEditingController(); + bool _isLoading = false; @override void dispose() { @@ -74,10 +75,7 @@ class _SettingsPickerScreenState extends State> { const SizedBox(height: 24), Expanded( child: Container( - decoration: BoxDecoration( - color: colors.surfaceDeep, - borderRadius: BorderRadius.circular(14), - ), + decoration: BoxDecoration(color: colors.surfaceDeep, borderRadius: BorderRadius.circular(14)), clipBehavior: Clip.antiAlias, child: Scrollbar( thumbVisibility: true, @@ -87,18 +85,14 @@ class _SettingsPickerScreenState extends State> { ? Center( child: Text( widget.emptyMessage, - style: text.smallParagraph?.copyWith( - color: colors.textMuted, - ), + style: text.smallParagraph?.copyWith(color: colors.textMuted), textAlign: TextAlign.center, ), ) : ListView.separated( itemCount: filtered.length, - separatorBuilder: (context, index) => const SettingsDivider( - style: SettingsDividerStyle.currencyList, - padding: EdgeInsets.zero, - ), + separatorBuilder: (context, index) => + const SettingsDivider(style: SettingsDividerStyle.currencyList, padding: EdgeInsets.zero), itemBuilder: (context, index) { final item = filtered[index]; return SettingsPickerListTile( @@ -106,7 +100,13 @@ class _SettingsPickerScreenState extends State> { selected: item == widget.selected, colors: colors, text: text, - onTap: () => widget.onSelect(item), + onTap: () { + if (_isLoading) return; + + setState(() => _isLoading = true); + widget.onSelect(item); + setState(() => _isLoading = false); + }, ); }, ), From 68b8ae388bb9f77cb2c9a076e90c44ef6be2c916 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 20 May 2026 12:40:26 +0800 Subject: [PATCH 15/25] chore: formatting --- mobile-app/lib/models/app_locale.dart | 23 +---- .../v2/components/address_details_card.dart | 7 +- .../lib/v2/components/qr_scanner_page.dart | 11 +-- .../v2/components/recovery_phrase_body.dart | 10 +- .../v2/screens/accounts/accounts_sheet.dart | 11 ++- .../accounts/add_hardware_account_screen.dart | 10 +- .../accounts/create_account_screen.dart | 6 +- .../screens/accounts/edit_account_screen.dart | 7 +- .../v2/screens/activity/activity_screen.dart | 10 +- .../activity/transaction_detail_sheet.dart | 3 +- .../lib/v2/screens/activity/tx_item.dart | 7 +- .../screens/create/wallet_ready_screen.dart | 6 +- .../lib/v2/screens/home/activity_section.dart | 6 +- .../lib/v2/screens/home/home_screen.dart | 4 +- .../lib/v2/screens/pos/pos_amount_screen.dart | 4 +- .../lib/v2/screens/pos/pos_qr_screen.dart | 92 ++++--------------- .../v2/screens/receive/receive_screen.dart | 6 +- .../v2/screens/send/input_amount_screen.dart | 15 ++- .../screens/send/select_recipient_screen.dart | 9 +- .../v2/screens/send/tx_submitted_screen.dart | 6 +- .../settings/about_quantus_screen.dart | 6 +- .../account_type_settings_screen.dart | 10 +- .../settings/language_picker_screen.dart | 3 +- .../settings/mining_rewards_screen.dart | 46 ++-------- .../settings/preferences_settings_screen.dart | 42 ++------- .../recovery_phrase_confirmation_screen.dart | 10 +- .../settings/reset_confirmation_screen.dart | 4 +- .../settings/select_wallet_screen.dart | 5 +- .../settings/settings_caution_scaffold.dart | 12 +-- .../settings/settings_picker_widgets.dart | 19 +--- .../v2/screens/settings/settings_screen.dart | 27 ++---- .../settings/testnet_rewards_screen.dart | 32 ++----- .../settings/wallet_settings_screen.dart | 5 +- .../lib/v2/screens/swap/deposit_screen.dart | 18 +--- .../swap/refund_address_picker_sheet.dart | 5 +- .../v2/screens/welcome/welcome_screen.dart | 6 +- 36 files changed, 140 insertions(+), 363 deletions(-) diff --git a/mobile-app/lib/models/app_locale.dart b/mobile-app/lib/models/app_locale.dart index 4d2d2796..03ab8aff 100644 --- a/mobile-app/lib/models/app_locale.dart +++ b/mobile-app/lib/models/app_locale.dart @@ -2,22 +2,10 @@ import 'package:flutter/material.dart'; /// App UI locales the user can select in settings. enum AppLocale { - en( - languageCode: 'en', - displayName: 'English', - numberFormatLocale: 'en_US', - ), - id( - languageCode: 'id', - displayName: 'Bahasa Indonesia', - numberFormatLocale: 'id_ID', - ); + en(languageCode: 'en', displayName: 'English', numberFormatLocale: 'en_US'), + id(languageCode: 'id', displayName: 'Bahasa Indonesia', numberFormatLocale: 'id_ID'); - const AppLocale({ - required this.languageCode, - required this.displayName, - required this.numberFormatLocale, - }); + const AppLocale({required this.languageCode, required this.displayName, required this.numberFormatLocale}); final String languageCode; final String displayName; @@ -26,9 +14,6 @@ enum AppLocale { Locale get flutterLocale => Locale(languageCode); static AppLocale fromCode(String code, {AppLocale fallback = AppLocale.en}) { - return AppLocale.values.firstWhere( - (l) => l.languageCode == code, - orElse: () => fallback, - ); + return AppLocale.values.firstWhere((l) => l.languageCode == code, orElse: () => fallback); } } diff --git a/mobile-app/lib/v2/components/address_details_card.dart b/mobile-app/lib/v2/components/address_details_card.dart index ee303a6f..fc7e1072 100644 --- a/mobile-app/lib/v2/components/address_details_card.dart +++ b/mobile-app/lib/v2/components/address_details_card.dart @@ -75,12 +75,7 @@ class _AddressDetailsCardState extends ConsumerState { return SplitCard( topChild: InkWell( onTap: () => _copyAddress(context), - child: _buildItem( - context, - l10n.componentAddressLabel, - widget.accountId, - isCopied: _addressCopied, - ), + child: _buildItem(context, l10n.componentAddressLabel, widget.accountId, isCopied: _addressCopied), ), bottomChild: InkWell( onTap: () => _copyChecksum(context, l10n), diff --git a/mobile-app/lib/v2/components/qr_scanner_page.dart b/mobile-app/lib/v2/components/qr_scanner_page.dart index c4682550..dffe5e43 100644 --- a/mobile-app/lib/v2/components/qr_scanner_page.dart +++ b/mobile-app/lib/v2/components/qr_scanner_page.dart @@ -43,9 +43,7 @@ class _QrScannerPageState extends ConsumerState { _onDetect(capture); } else { final l10n = ref.read(l10nProvider); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.componentQrScannerNoCode)), - ); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(l10n.componentQrScannerNoCode))); } } @@ -89,12 +87,7 @@ class _QrScannerPageState extends ConsumerState { ], ), ), - Positioned( - top: 20, - left: 24, - right: 24, - child: V2AppBar(title: l10n.addHardwareAccountScanQr), - ), + Positioned(top: 20, left: 24, right: 24, child: V2AppBar(title: l10n.addHardwareAccountScanQr)), ], ), ); diff --git a/mobile-app/lib/v2/components/recovery_phrase_body.dart b/mobile-app/lib/v2/components/recovery_phrase_body.dart index 987ac675..60a3301d 100644 --- a/mobile-app/lib/v2/components/recovery_phrase_body.dart +++ b/mobile-app/lib/v2/components/recovery_phrase_body.dart @@ -33,10 +33,7 @@ class RecoveryPhraseBody extends ConsumerWidget { void _copyToClipboard(BuildContext context, WidgetRef ref) { final l10n = ref.read(l10nProvider); - context.copyTextWithToaster( - words.join(' '), - message: l10n.recoveryPhraseBodyCopiedMessage, - ); + context.copyTextWithToaster(words.join(' '), message: l10n.recoveryPhraseBodyCopiedMessage); } @override @@ -50,10 +47,7 @@ class RecoveryPhraseBody extends ConsumerWidget { mainContent: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - l10n.recoveryPhraseBodyInstructions, - style: text.smallParagraph?.copyWith(color: colors.textTertiary), - ), + Text(l10n.recoveryPhraseBodyInstructions, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), const SizedBox(height: 24), Expanded( child: isGridLoading diff --git a/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart b/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart index 60c44638..04fcb329 100644 --- a/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart +++ b/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart @@ -140,7 +140,11 @@ class _AccountsScreenState extends ConsumerState { ), ), const SizedBox(height: 24), - QuantusButton.simple(label: l10n.accountsSheetAddAccount, onTap: _openAddAccountMenu, variant: ButtonVariant.primary), + QuantusButton.simple( + label: l10n.accountsSheetAddAccount, + onTap: _openAddAccountMenu, + variant: ButtonVariant.primary, + ), ], ); } @@ -151,10 +155,7 @@ class _AccountsScreenState extends ConsumerState { final balanceText = balanceAsync.when( loading: () => l10n.accountsSheetLoading, error: (_, _) => l10n.accountsSheetBalanceUnavailable, - data: (balance) => l10n.accountsSheetBalance( - formattingService.formatBalance(balance), - AppConstants.tokenSymbol, - ), + data: (balance) => l10n.accountsSheetBalance(formattingService.formatBalance(balance), AppConstants.tokenSymbol), ); final colors = context.colors; diff --git a/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart b/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart index 83fe33c1..264e7243 100644 --- a/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart @@ -111,7 +111,9 @@ class _AddHardwareAccountScreenState extends ConsumerState _error = null); }, @@ -152,11 +154,7 @@ class _AddHardwareAccountScreenState extends ConsumerState { return ScaffoldBase( appBar: V2AppBar(title: l10n.createAccountAppBarTitle), - mainContent: NameField( - controller: _accountName, - subtitle: l10n.createAccountSubtitle, - error: _error, - ), + mainContent: NameField(controller: _accountName, subtitle: l10n.createAccountSubtitle, error: _error), bottomContent: ScaffoldBaseBottomContent( child: QuantusButton.simple( label: l10n.createAccountButton, diff --git a/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart b/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart index 6c8e45f9..6bb7ea76 100644 --- a/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/edit_account_screen.dart @@ -77,7 +77,12 @@ class EditAccountScreenState extends ConsumerState { children: [NameField(controller: _controller)], ), bottomContent: ScaffoldBaseBottomContent( - child: QuantusButton.simple(variant: ButtonVariant.primary, label: l10n.editAccountDone, onTap: _save, isLoading: _saving), + child: QuantusButton.simple( + variant: ButtonVariant.primary, + label: l10n.editAccountDone, + onTap: _save, + isLoading: _saving, + ), ), ); } diff --git a/mobile-app/lib/v2/screens/activity/activity_screen.dart b/mobile-app/lib/v2/screens/activity/activity_screen.dart index 06a941b4..264c0ef2 100644 --- a/mobile-app/lib/v2/screens/activity/activity_screen.dart +++ b/mobile-app/lib/v2/screens/activity/activity_screen.dart @@ -99,7 +99,10 @@ class _ActivityScreenState extends ConsumerState { ), ), error: (e, _) => Center( - child: Text(l10n.activityError(e.toString()), style: text.detail?.copyWith(color: colors.textError)), + child: Text( + l10n.activityError(e.toString()), + style: text.detail?.copyWith(color: colors.textError), + ), ), data: (data) { final txService = ref.read(transactionServiceProvider); @@ -111,10 +114,7 @@ class _ActivityScreenState extends ConsumerState { ); if (all.isEmpty) { return Center( - child: Text( - l10n.activityEmpty, - style: text.paragraph?.copyWith(color: colors.textSecondary), - ), + child: Text(l10n.activityEmpty, style: text.paragraph?.copyWith(color: colors.textSecondary)), ); } final grouped = _groupByDate(all, l10n, appLocale.numberFormatLocale); diff --git a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart index 63aab04b..bad32a9b 100644 --- a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart +++ b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart @@ -31,7 +31,8 @@ class _TransactionDetailSheet extends ConsumerWidget { String _title(AppLocalizations l10n) { if (_isPending) return l10n.activityDetailTitleSending; - if (tx.isReversibleScheduled) return _isSend ? l10n.activityDetailTitleScheduled : l10n.activityDetailTitleReceiving; + if (tx.isReversibleScheduled) + return _isSend ? l10n.activityDetailTitleScheduled : l10n.activityDetailTitleReceiving; return _isSend ? l10n.activityDetailTitleSent : l10n.activityDetailTitleReceived; } diff --git a/mobile-app/lib/v2/screens/activity/tx_item.dart b/mobile-app/lib/v2/screens/activity/tx_item.dart index 24c4de65..96857ed0 100644 --- a/mobile-app/lib/v2/screens/activity/tx_item.dart +++ b/mobile-app/lib/v2/screens/activity/tx_item.dart @@ -31,12 +31,7 @@ class TxItemData { required this.counterpartyAddr, }); - factory TxItemData.from( - TransactionEvent tx, - String accountId, - AppColorsV2 colors, - AppLocalizations l10n, - ) { + factory TxItemData.from(TransactionEvent tx, String accountId, AppColorsV2 colors, AppLocalizations l10n) { final isSend = tx.from == accountId; final isPending = tx is PendingTransactionEvent; final isScheduled = tx.isReversibleScheduled; diff --git a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart index 83ddf85b..c4a5fda8 100644 --- a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart +++ b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart @@ -23,11 +23,7 @@ class _WalletReadyScreenV2State extends ConsumerState { final l10n = ref.watch(l10nProvider); final data = SettingsCautionScaffoldData( headline: l10n.createWalletCautionHeadline, - bulletItems: [ - l10n.createWalletCautionBullet1, - l10n.createWalletCautionBullet2, - l10n.createWalletCautionBullet3, - ], + bulletItems: [l10n.createWalletCautionBullet1, l10n.createWalletCautionBullet2, l10n.createWalletCautionBullet3], checkboxLabel: l10n.createWalletCautionCheckboxLabel, ); diff --git a/mobile-app/lib/v2/screens/home/activity_section.dart b/mobile-app/lib/v2/screens/home/activity_section.dart index 39c29431..43a9dc47 100644 --- a/mobile-app/lib/v2/screens/home/activity_section.dart +++ b/mobile-app/lib/v2/screens/home/activity_section.dart @@ -47,7 +47,11 @@ class _ActivitySectionState extends ConsumerState { if (all.isEmpty) { return Column( - children: [const SizedBox(height: 40), _header(colors, text, context, l10n), _emptyState(text, colors, l10n)], + children: [ + const SizedBox(height: 40), + _header(colors, text, context, l10n), + _emptyState(text, colors, l10n), + ], ); } diff --git a/mobile-app/lib/v2/screens/home/home_screen.dart b/mobile-app/lib/v2/screens/home/home_screen.dart index b5901008..b8e6a7fc 100644 --- a/mobile-app/lib/v2/screens/home/home_screen.dart +++ b/mobile-app/lib/v2/screens/home/home_screen.dart @@ -135,9 +135,7 @@ class _HomeScreenState extends ConsumerState { ), data: (active) { if (active == null) { - return ScaffoldBase( - mainContent: Center(child: Text(l10n.homeNoActiveAccount)), - ); + return ScaffoldBase(mainContent: Center(child: Text(l10n.homeNoActiveAccount))); } return ScaffoldBase.refreshable( onRefresh: _refresh, 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 bef14940..170f003e 100644 --- a/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_amount_screen.dart @@ -171,9 +171,7 @@ class _PosAmountScreenState extends ConsumerState { } Widget _bottomContent(AppLocalizations l10n, String amountDisplay) { - final label = _amount > BigInt.zero - ? l10n.posAmountCharge(amountDisplay) - : l10n.posAmountEnterAmount; + final label = _amount > BigInt.zero ? l10n.posAmountCharge(amountDisplay) : l10n.posAmountEnterAmount; 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 d615a482..2602b988 100644 --- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -71,21 +71,15 @@ class _PosQrScreenState extends ConsumerState { _watchError = null; }); - debugPrint( - '[PosQr] watching address=${active.account.accountId} expected=$expectedPlanck planck', - ); + debugPrint('[PosQr] watching address=${active.account.accountId} expected=$expectedPlanck planck'); _txWatch.watch( address: active.account.accountId, onTransfer: (tx) { - debugPrint( - '[PosQr] onTransfer from=${tx.from} amount=${tx.amount} hash=${tx.txHash}', - ); + debugPrint('[PosQr] onTransfer from=${tx.from} amount=${tx.amount} hash=${tx.txHash}'); if (_isPaid) return; final received = BigInt.tryParse(tx.amount); if (received != expectedPlanck) { - debugPrint( - '[PosQr] amount mismatch (received=$received expected=$expectedPlanck), ignoring', - ); + debugPrint('[PosQr] amount mismatch (received=$received expected=$expectedPlanck), ignoring'); return; } @@ -180,49 +174,29 @@ class _PosQrScreenState extends ConsumerState { final display = ref.watch(txAmountDisplayProvider)(planck, withSignPrefix: false, isSend: false, quanDecimals: 4); return ScaffoldBase( - appBar: V2AppBar( - title: _isPaid ? l10n.posQrTitlePaymentReceived : l10n.posQrTitleScanToPay, - ), + appBar: V2AppBar(title: _isPaid ? l10n.posQrTitlePaymentReceived : l10n.posQrTitleScanToPay), mainContent: accountAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text( - l10n.posQrError('$e'), - style: text.detail?.copyWith(color: colors.textError), - ), + child: Text(l10n.posQrError('$e'), style: text.detail?.copyWith(color: colors.textError)), ), data: (active) { if (active == null) { return Center(child: Text(l10n.posQrNoActiveAccount)); } - _request ??= _posService.createPaymentRequest( - accountId: active.account.accountId, - amount: widget.amount, - ); + _request ??= _posService.createPaymentRequest(accountId: active.account.accountId, amount: widget.amount); if (_isPaid) { - return _buildPaidContent( - l10n, - appLocale.numberFormatLocale, - colors, - text, - display.primaryAmount, - ); + return _buildPaidContent(l10n, appLocale.numberFormatLocale, colors, text, display.primaryAmount); } return _buildQrContent(l10n, _request!, colors, text, display); }, ), - bottomContent: ScaffoldBaseBottomContent( - child: _isPaid ? _buildPaidButtons(l10n) : _buildQrButton(l10n), - ), + bottomContent: ScaffoldBaseBottomContent(child: _isPaid ? _buildPaidButtons(l10n) : _buildQrButton(l10n)), ); } Widget _buildQrButton(AppLocalizations l10n) { - return QuantusButton.simple( - label: l10n.posQrNewCharge, - onTap: _newCharge, - variant: ButtonVariant.primary, - ); + return QuantusButton.simple(label: l10n.posQrNewCharge, onTap: _newCharge, variant: ButtonVariant.primary); } Widget _buildPaidButtons(AppLocalizations l10n) { @@ -269,21 +243,14 @@ class _PosQrScreenState extends ConsumerState { const SizedBox(height: 32), Text( l10n.posQrAmountReceived(amountDisplay), - style: text.smallTitle?.copyWith( - color: colors.textLightGray, - fontSize: 32, - fontWeight: FontWeight.w400, - ), + 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!, localeName, l10n), - style: text.smallParagraph?.copyWith( - color: colors.textTertiary, - letterSpacing: 0.7, - ), + style: text.smallParagraph?.copyWith(color: colors.textTertiary, letterSpacing: 0.7), textAlign: TextAlign.center, ), const SizedBox(height: 32), @@ -307,21 +274,13 @@ class _PosQrScreenState extends ConsumerState { ); } - Widget _buildFromSection( - AppLocalizations l10n, - AppColorsV2 colors, - AppTextTheme text, - String formattedAddress, - ) { + Widget _buildFromSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text, String formattedAddress) { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( l10n.posQrFrom, - style: text.paragraph?.copyWith( - color: colors.textPrimary, - fontWeight: FontWeight.w500, - ), + style: text.paragraph?.copyWith(color: colors.textPrimary, fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), const SizedBox(height: 16), @@ -360,10 +319,7 @@ class _PosQrScreenState extends ConsumerState { border: Border(bottom: BorderSide(color: colors.textTertiary, width: 1)), ), padding: const EdgeInsets.only(bottom: 3), - child: Text( - l10n.activityDetailViewExplorer, - style: text.smallParagraph?.copyWith(color: colors.textTertiary), - ), + child: Text(l10n.activityDetailViewExplorer, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), ), ); } @@ -393,10 +349,7 @@ class _PosQrScreenState extends ConsumerState { children: [ Text( display.primaryAmount, - style: text.totalMinedBlocks?.copyWith( - color: colors.textPrimary, - letterSpacing: -2.77, - ), + style: text.totalMinedBlocks?.copyWith(color: colors.textPrimary, letterSpacing: -2.77), ), const SizedBox(height: 8), Row( @@ -404,10 +357,7 @@ class _PosQrScreenState extends ConsumerState { children: [ Text( '≈ ${display.secondaryAmount}', - style: text.paragraph?.copyWith( - color: colors.textTertiary, - fontFamily: AppTextTheme.fontFamilySecondary, - ), + style: text.paragraph?.copyWith(color: colors.textTertiary, fontFamily: AppTextTheme.fontFamilySecondary), ), const SizedBox(width: 8), QuantusIconButton.circular( @@ -435,10 +385,7 @@ class _PosQrScreenState extends ConsumerState { children: [ Loader(size: 14, color: colors.textMuted), const SizedBox(width: 9), - Text( - l10n.posQrWaitingForPayment, - style: text.detail?.copyWith(color: colors.textMuted), - ), + Text(l10n.posQrWaitingForPayment, style: text.detail?.copyWith(color: colors.textMuted)), ], ), ); @@ -447,10 +394,7 @@ class _PosQrScreenState extends ConsumerState { Widget _buildErrorSection(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text) { return Column( children: [ - Text( - l10n.posQrNetworkError, - style: text.detail?.copyWith(color: colors.textError), - ), + Text(l10n.posQrNetworkError, style: text.detail?.copyWith(color: colors.textError)), const SizedBox(height: 8), QuantusButton.simple( label: l10n.posQrTryAgain, diff --git a/mobile-app/lib/v2/screens/receive/receive_screen.dart b/mobile-app/lib/v2/screens/receive/receive_screen.dart index 9573f4d8..4bf175a3 100644 --- a/mobile-app/lib/v2/screens/receive/receive_screen.dart +++ b/mobile-app/lib/v2/screens/receive/receive_screen.dart @@ -111,11 +111,7 @@ class _ReceiveScreenState extends ConsumerState { ); } - Widget? _buildBottomContent( - AppLocalizations l10n, - bool isLoading, - ReceiveTab selectedTab, - ) { + Widget? _buildBottomContent(AppLocalizations l10n, bool isLoading, ReceiveTab selectedTab) { Widget content; if (isLoading) { 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 2fb793a6..d7e72d9f 100644 --- a/mobile-app/lib/v2/screens/send/input_amount_screen.dart +++ b/mobile-app/lib/v2/screens/send/input_amount_screen.dart @@ -301,7 +301,10 @@ class _InputAmountScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.sendInputAmountSendTo, style: context.themeText.receiveLabel?.copyWith(color: colors.textLabel)), + Text( + l10n.sendInputAmountSendTo, + style: context.themeText.receiveLabel?.copyWith(color: colors.textLabel), + ), const SizedBox(height: 16), if (_recipientChecksum != null) ...[ Text( @@ -455,7 +458,10 @@ class _InputAmountScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.sendInputAmountAvailableBalance, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), + Text( + l10n.sendInputAmountAvailableBalance, + style: text.smallParagraph?.copyWith(color: colors.textTertiary), + ), const SizedBox(height: 4), balance.when( data: (b) => Text( @@ -472,7 +478,10 @@ class _InputAmountScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text(l10n.sendInputAmountNetworkFee, style: text.smallParagraph?.copyWith(color: colors.textTertiary)), + Text( + l10n.sendInputAmountNetworkFee, + style: text.smallParagraph?.copyWith(color: colors.textTertiary), + ), const SizedBox(height: 4), if (!_isFetchingFee) Text( diff --git a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart index 4d62f752..5acf6e35 100644 --- a/mobile-app/lib/v2/screens/send/select_recipient_screen.dart +++ b/mobile-app/lib/v2/screens/send/select_recipient_screen.dart @@ -216,7 +216,10 @@ class _SelectRecipientScreenState extends ConsumerState { const SliverFillRemaining(hasScrollBody: false, child: Center(child: Loader())) else if (_recents.isNotEmpty) ...[ SliverToBoxAdapter( - child: Text(l10n.sendSelectRecipientRecents, style: text.smallTitle?.copyWith(color: colors.textPrimary)), + child: Text( + l10n.sendSelectRecipientRecents, + style: text.smallTitle?.copyWith(color: colors.textPrimary), + ), ), const SliverToBoxAdapter(child: SizedBox(height: 32)), SliverList( @@ -278,7 +281,9 @@ class _SelectRecipientScreenState extends ConsumerState { textCapitalization: TextCapitalization.none, scrollPadding: const EdgeInsets.only(bottom: 120), style: text.smallParagraph?.copyWith(color: colors.textPrimary), - decoration: InputDecoration(hintText: l10n.sendSelectRecipientSearchHint(AppConstants.tokenSymbol)), + decoration: InputDecoration( + hintText: l10n.sendSelectRecipientSearchHint(AppConstants.tokenSymbol), + ), ), ), ], diff --git a/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart b/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart index e3e926f7..911c04bd 100644 --- a/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart +++ b/mobile-app/lib/v2/screens/send/tx_submitted_screen.dart @@ -120,7 +120,11 @@ class TxSubmittedScreen extends ConsumerWidget { ], ), bottomContent: ScaffoldBaseBottomContent( - child: QuantusButton.simple(label: l10n.sendTxSubmittedDone, variant: ButtonVariant.primary, onTap: () => _popToHome(context)), + child: QuantusButton.simple( + label: l10n.sendTxSubmittedDone, + variant: ButtonVariant.primary, + onTap: () => _popToHome(context), + ), ), ), ); diff --git a/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart b/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart index 9b033218..938f76f8 100644 --- a/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart +++ b/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart @@ -25,11 +25,7 @@ class AboutQuantusScreenV2 extends ConsumerWidget { static List<({String title, String subtitle, String path})> _externalLinks(AppLocalizations l10n) { return [ (title: l10n.settingsAboutTerms, subtitle: l10n.settingsAboutTermsSubtitle, path: '/terms'), - ( - title: l10n.settingsAboutPrivacy, - subtitle: l10n.settingsAboutPrivacySubtitle, - path: '/privacy-policy', - ), + (title: l10n.settingsAboutPrivacy, subtitle: l10n.settingsAboutPrivacySubtitle, path: '/privacy-policy'), (title: l10n.settingsAboutWebsite, subtitle: l10n.settingsAboutWebsiteSubtitle, path: ''), ]; } diff --git a/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart b/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart index 9f9673c2..637b1961 100644 --- a/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/account_type_settings_screen.dart @@ -14,10 +14,7 @@ class AccountTypeSettingsScreenV2 extends ConsumerWidget { static List<({String title, String subtitle})> _upcomingFeatures(AppLocalizations l10n) { return [ (title: l10n.settingsAccountTypeReversibleTitle, subtitle: l10n.settingsAccountTypeReversibleSubtitle), - ( - title: l10n.settingsAccountTypeHighSecurityTitle, - subtitle: l10n.settingsAccountTypeHighSecuritySubtitle, - ), + (title: l10n.settingsAccountTypeHighSecurityTitle, subtitle: l10n.settingsAccountTypeHighSecuritySubtitle), (title: l10n.settingsAccountTypeMultiSigTitle, subtitle: l10n.settingsAccountTypeMultiSigSubtitle), (title: l10n.settingsAccountTypeHardwareTitle, subtitle: l10n.settingsAccountTypeHardwareSubtitle), ]; @@ -34,10 +31,7 @@ class AccountTypeSettingsScreenV2 extends ConsumerWidget { appBar: V2AppBar(title: l10n.settingsAccountTypeScreenTitle), mainContent: ListView( children: [ - Text( - l10n.settingsAccountTypeIntro, - style: text.smallParagraph?.copyWith(color: colors.textMuted), - ), + Text(l10n.settingsAccountTypeIntro, style: text.smallParagraph?.copyWith(color: colors.textMuted)), const SizedBox(height: 40), for (var i = 0; i < upcomingFeatures.length; i++) _AccountFeatureBlock( diff --git a/mobile-app/lib/v2/screens/settings/language_picker_screen.dart b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart index 6355d4c4..e65cd27f 100644 --- a/mobile-app/lib/v2/screens/settings/language_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart @@ -21,8 +21,7 @@ class LanguagePickerScreenV2 extends ConsumerWidget { selected: selected, labelBuilder: (locale) => locale.displayName, filter: (locale, query) { - return locale.displayName.toLowerCase().contains(query) || - locale.languageCode.toLowerCase().contains(query); + return locale.displayName.toLowerCase().contains(query) || locale.languageCode.toLowerCase().contains(query); }, onSelect: (locale) async { try { diff --git a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart index be2309ce..2e103720 100644 --- a/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart +++ b/mobile-app/lib/v2/screens/settings/mining_rewards_screen.dart @@ -33,19 +33,13 @@ class MiningRewardsScreen extends ConsumerWidget { miningAsync.when( data: (data) => data.totalBlocks > 0 ? _WithRewards(data: data) : _NoRewards(l10n: l10n), loading: () => _NoRewards(l10n: l10n, isLoading: true), - error: (err, _) => _ErrorState( - colors: colors, - text: text, - l10n: l10n, - onRetry: () => ref.invalidate(miningRewardsProvider), - ), + error: (err, _) => + _ErrorState(colors: colors, text: text, l10n: l10n, onRetry: () => ref.invalidate(miningRewardsProvider)), ), ], bottomContent: miningAsync.when( data: (data) => data.totalBlocks > 0 - ? ScaffoldBaseBottomContent( - child: QuantusButton.simple(label: l10n.settingsMiningRedeem, onTap: null), - ) + ? ScaffoldBaseBottomContent(child: QuantusButton.simple(label: l10n.settingsMiningRedeem, onTap: null)) : null, loading: () => null, error: (err, _) => null, @@ -250,10 +244,7 @@ class _CardTopSection extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - l10n.settingsMiningBlocksMined, - style: text.receiveLabel?.copyWith(color: colors.textLabel), - ), + Text(l10n.settingsMiningBlocksMined, style: text.receiveLabel?.copyWith(color: colors.textLabel)), Row( children: [ Container( @@ -276,10 +267,7 @@ class _CardTopSection extends StatelessWidget { else Text('$totalBlocks', style: text.totalMinedBlocks?.copyWith(color: totalBlocksColor)), const SizedBox(height: 4), - Text( - l10n.settingsMiningBlocksAcrossTestnets, - style: text.detail?.copyWith(color: colors.textMuted), - ), + Text(l10n.settingsMiningBlocksAcrossTestnets, style: text.detail?.copyWith(color: colors.textMuted)), ], ); } @@ -339,12 +327,7 @@ class _StatColumn extends StatelessWidget { FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, - child: Text( - value, - maxLines: 1, - softWrap: false, - style: text.sendSectionLabel?.copyWith(color: valueColor), - ), + child: Text(value, maxLines: 1, softWrap: false, style: text.sendSectionLabel?.copyWith(color: valueColor)), ), ], ); @@ -427,12 +410,7 @@ class _ErrorState extends StatelessWidget { final AppLocalizations l10n; final VoidCallback onRetry; - const _ErrorState({ - required this.colors, - required this.text, - required this.l10n, - required this.onRetry, - }); + const _ErrorState({required this.colors, required this.text, required this.l10n, required this.onRetry}); @override Widget build(BuildContext context) { @@ -442,15 +420,9 @@ class _ErrorState extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - l10n.settingsMiningLoadError, - style: text.paragraph?.copyWith(color: colors.textPrimary), - ), + Text(l10n.settingsMiningLoadError, style: text.paragraph?.copyWith(color: colors.textPrimary)), const SizedBox(height: 8), - Text( - l10n.settingsMiningCheckConnection, - style: text.detail?.copyWith(color: colors.textTertiary), - ), + Text(l10n.settingsMiningCheckConnection, style: text.detail?.copyWith(color: colors.textTertiary)), const SizedBox(height: 20), GestureDetector( onTap: onRetry, diff --git a/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart b/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart index 8bc9bfce..24b3fc21 100644 --- a/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/preferences_settings_screen.dart @@ -17,31 +17,21 @@ class PreferencesSettingsScreenV2 extends ConsumerStatefulWidget { const PreferencesSettingsScreenV2({super.key}); @override - ConsumerState createState() => - _PreferencesSettingsScreenV2State(); + ConsumerState createState() => _PreferencesSettingsScreenV2State(); } -class _PreferencesSettingsScreenV2State - extends ConsumerState { +class _PreferencesSettingsScreenV2State extends ConsumerState { void _toggleNotifications(bool enable) { final current = ref.read(notificationConfigProvider); - ref - .read(notificationConfigProvider.notifier) - .updateConfig(current.copyWith(enabled: enable)); + ref.read(notificationConfigProvider.notifier).updateConfig(current.copyWith(enabled: enable)); } void _openLanguagePicker() { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const LanguagePickerScreenV2()), - ); + Navigator.push(context, MaterialPageRoute(builder: (_) => const LanguagePickerScreenV2())); } void _openCurrencyPicker() { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const CurrencyPickerScreenV2()), - ); + Navigator.push(context, MaterialPageRoute(builder: (_) => const CurrencyPickerScreenV2())); } @override @@ -65,16 +55,9 @@ class _PreferencesSettingsScreenV2State trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - appLocale.displayName, - style: text.smallParagraph?.copyWith(color: colors.textMuted), - ), + Text(appLocale.displayName, style: text.smallParagraph?.copyWith(color: colors.textMuted)), const SizedBox(width: 4), - SettingsTappableRowUtils.chevron( - colors, - color: colors.textMuted, - size: 18, - ), + SettingsTappableRowUtils.chevron(colors, color: colors.textMuted, size: 18), ], ), ), @@ -86,16 +69,9 @@ class _PreferencesSettingsScreenV2State trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - fiat.code, - style: text.smallParagraph?.copyWith(color: colors.textMuted), - ), + Text(fiat.code, style: text.smallParagraph?.copyWith(color: colors.textMuted)), const SizedBox(width: 4), - SettingsTappableRowUtils.chevron( - colors, - color: colors.textMuted, - size: 18, - ), + SettingsTappableRowUtils.chevron(colors, color: colors.textMuted, size: 18), ], ), ), diff --git a/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart b/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart index 851f15c5..9a8569fe 100644 --- a/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart +++ b/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart @@ -20,14 +20,12 @@ class _RecoveryPhraseConfirmationScreenState extends ConsumerState _onContinue() async { final l10n = ref.read(l10nProvider); - final authed = await LocalAuthService().authenticate( - localizedReason: l10n.settingsRecoveryConfirmAuthReason, - ); + final authed = await LocalAuthService().authenticate(localizedReason: l10n.settingsRecoveryConfirmAuthReason); if (authed && mounted) { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => RecoveryPhraseScreen(walletIndex: widget.walletIndex)), - ); + Navigator.of( + context, + ).pushReplacement(MaterialPageRoute(builder: (_) => RecoveryPhraseScreen(walletIndex: widget.walletIndex))); } else { if (mounted) { context.showErrorToaster(message: l10n.settingsRecoveryConfirmAuthRequired); diff --git a/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart b/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart index 19e950da..a7198971 100644 --- a/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart +++ b/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart @@ -22,9 +22,7 @@ class _ResetConfirmationScreenState extends ConsumerState _isResetting = true); - final authed = await LocalAuthService().authenticate( - localizedReason: l10n.settingsResetAuthReason, - ); + final authed = await LocalAuthService().authenticate(localizedReason: l10n.settingsResetAuthReason); if (authed && mounted) { try { diff --git a/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart b/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart index 97b957ec..2c001b8f 100644 --- a/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/settings/select_wallet_screen.dart @@ -26,10 +26,7 @@ class SelectWalletScreen extends ConsumerWidget { mainContent: accountsAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text( - l10n.settingsWalletFailedToLoad, - style: text.paragraph?.copyWith(color: colors.textSecondary), - ), + child: Text(l10n.settingsWalletFailedToLoad, style: text.paragraph?.copyWith(color: colors.textSecondary)), ), data: (accounts) { final indices = getNonHardwareWalletIndices(accounts); diff --git a/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart b/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart index 0f940820..969127ed 100644 --- a/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart +++ b/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart @@ -15,20 +15,12 @@ class SettingsCautionScaffoldData { final List bulletItems; final String checkboxLabel; - const SettingsCautionScaffoldData({ - required this.headline, - required this.bulletItems, - required this.checkboxLabel, - }); + const SettingsCautionScaffoldData({required this.headline, required this.bulletItems, required this.checkboxLabel}); factory SettingsCautionScaffoldData.recoveryPhrase(AppLocalizations l10n) { return SettingsCautionScaffoldData( headline: l10n.createWalletCautionHeadline, - bulletItems: [ - l10n.createWalletCautionBullet1, - l10n.createWalletCautionBullet2, - l10n.createWalletCautionBullet3, - ], + bulletItems: [l10n.createWalletCautionBullet1, l10n.createWalletCautionBullet2, l10n.createWalletCautionBullet3], checkboxLabel: l10n.createWalletCautionCheckboxLabel, ); } diff --git a/mobile-app/lib/v2/screens/settings/settings_picker_widgets.dart b/mobile-app/lib/v2/screens/settings/settings_picker_widgets.dart index 55ea1977..7bfc3a40 100644 --- a/mobile-app/lib/v2/screens/settings/settings_picker_widgets.dart +++ b/mobile-app/lib/v2/screens/settings/settings_picker_widgets.dart @@ -24,10 +24,7 @@ class SettingsPickerSearchField extends StatelessWidget { height: 48, child: Container( padding: const EdgeInsets.only(left: 12, right: 8), - decoration: BoxDecoration( - color: colors.surfaceDeep, - borderRadius: BorderRadius.circular(14), - ), + decoration: BoxDecoration(color: colors.surfaceDeep, borderRadius: BorderRadius.circular(14)), child: Row( children: [ Icon(Icons.search, size: 18, color: colors.textLabel), @@ -41,9 +38,7 @@ class SettingsPickerSearchField extends StatelessWidget { isDense: true, border: InputBorder.none, hintText: hintText, - hintStyle: text.smallParagraph?.copyWith( - color: colors.textLabel, - ), + hintStyle: text.smallParagraph?.copyWith(color: colors.textLabel), ), ), ), @@ -85,15 +80,9 @@ class SettingsPickerListTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( - child: Text( - label, - style: text.paragraph?.copyWith(color: fg, height: 1.2), - ), + child: Text(label, style: text.paragraph?.copyWith(color: fg, height: 1.2)), ), - if (selected) ...[ - const SizedBox(width: 12), - Icon(Icons.check, size: 18, color: accent), - ], + if (selected) ...[const SizedBox(width: 12), Icon(Icons.check, size: 18, color: accent)], ], ), ), diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index 40bd0f8b..c26ae389 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -46,20 +46,12 @@ class _SettingsScreenV2State extends ConsumerState { subtitle: l10n.settingsMiningRewardsSubtitle(data.totalBlocks), trailing: trailing, ), - loading: () => _buildTappableRow( - e.value, - subtitle: l10n.accountsSheetLoading, - trailing: trailing, - ), + loading: () => _buildTappableRow(e.value, subtitle: l10n.accountsSheetLoading, trailing: trailing), error: (err, st) { debugPrint('Error getting mining rewards: ${err.toString()}'); debugPrint('Stack trace: ${st.toString()}'); - return _buildTappableRow( - e.value, - subtitle: l10n.settingsMiningRewardsError, - trailing: trailing, - ); + return _buildTappableRow(e.value, subtitle: l10n.settingsMiningRewardsError, trailing: trailing); }, ) else @@ -71,14 +63,13 @@ class _SettingsScreenV2State extends ConsumerState { ); } - Widget _buildTappableRow(_SettingsHubItem item, {required Widget trailing, String? subtitle}) => - SettingsTappableRow( - leading: item.leading, - title: item.title, - subtitle: subtitle ?? item.subtitle, - onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => item.page)), - trailing: trailing, - ); + Widget _buildTappableRow(_SettingsHubItem item, {required Widget trailing, String? subtitle}) => SettingsTappableRow( + leading: item.leading, + title: item.title, + subtitle: subtitle ?? item.subtitle, + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => item.page)), + trailing: trailing, + ); } class _SettingsHubItem { diff --git a/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart b/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart index 5f665b1e..aa26fe24 100644 --- a/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart +++ b/mobile-app/lib/v2/screens/settings/testnet_rewards_screen.dart @@ -34,15 +34,9 @@ class TestnetRewardsScreen extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - l10n.settingsTestnetLoadError, - style: text.paragraph?.copyWith(color: colors.textPrimary), - ), + Text(l10n.settingsTestnetLoadError, style: text.paragraph?.copyWith(color: colors.textPrimary)), const SizedBox(height: 8), - Text( - l10n.settingsMiningCheckConnection, - style: text.detail?.copyWith(color: colors.textTertiary), - ), + Text(l10n.settingsMiningCheckConnection, style: text.detail?.copyWith(color: colors.textTertiary)), const SizedBox(height: 20), GestureDetector( onTap: () => ref.invalidate(miningRewardsProvider), @@ -58,12 +52,7 @@ class TestnetRewardsScreen extends ConsumerWidget { ); } - Widget _buildContent( - AppLocalizations l10n, - MiningRewardsData data, - AppColorsV2 colors, - AppTextTheme text, - ) { + Widget _buildContent(AppLocalizations l10n, MiningRewardsData data, AppColorsV2 colors, AppTextTheme text) { final testnets = [ ('Planck', data.planckBlocks), ('Dirac', data.diracBlocks), @@ -87,10 +76,7 @@ class TestnetRewardsScreen extends ConsumerWidget { style: text.largeTitle?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w700), ), const SizedBox(height: 8), - Text( - l10n.settingsTestnetTotalDescription, - style: text.detail?.copyWith(color: colors.textTertiary), - ), + Text(l10n.settingsTestnetTotalDescription, style: text.detail?.copyWith(color: colors.textTertiary)), ], ), ), @@ -112,18 +98,12 @@ class TestnetRewardsScreen extends ConsumerWidget { Row( children: [ Expanded( - child: Text( - testnets[i].$1, - style: text.paragraph?.copyWith(color: colors.textPrimary), - ), + child: Text(testnets[i].$1, style: text.paragraph?.copyWith(color: colors.textPrimary)), ), const Text('💰 ', style: TextStyle(fontSize: 14)), Text( l10n.settingsTestnetRowBlocks(testnets[i].$2), - style: text.smallParagraph?.copyWith( - color: colors.accentGreen, - fontWeight: FontWeight.w600, - ), + style: text.smallParagraph?.copyWith(color: colors.accentGreen, fontWeight: FontWeight.w600), ), ], ), diff --git a/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart b/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart index b5508bdf..2b65a698 100644 --- a/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/wallet_settings_screen.dart @@ -61,10 +61,7 @@ class _WalletSettingsScreenV2State extends ConsumerState mainContent: accountsAsync.when( loading: () => const Center(child: Loader()), error: (e, _) => Center( - child: Text( - l10n.settingsWalletFailedToLoad, - style: text.paragraph?.copyWith(color: colors.textSecondary), - ), + child: Text(l10n.settingsWalletFailedToLoad, style: text.paragraph?.copyWith(color: colors.textSecondary)), ), data: (accounts) => ListView( children: [ diff --git a/mobile-app/lib/v2/screens/swap/deposit_screen.dart b/mobile-app/lib/v2/screens/swap/deposit_screen.dart index f6773af0..2f1bf42d 100644 --- a/mobile-app/lib/v2/screens/swap/deposit_screen.dart +++ b/mobile-app/lib/v2/screens/swap/deposit_screen.dart @@ -102,13 +102,7 @@ class _DepositScreenState extends ConsumerState { ); } - Widget _depositBody( - AppLocalizations l10n, - AppColorsV2 colors, - AppTextTheme text, - SwapQuote quote, - double usd, - ) { + Widget _depositBody(AppLocalizations l10n, AppColorsV2 colors, AppTextTheme text, SwapQuote quote, double usd) { final demoWarning = l10n.swapDepositDemoWarning; return Column( @@ -240,10 +234,7 @@ class _DepositScreenState extends ConsumerState { style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20), ), const SizedBox(height: 12), - Text( - l10n.swapDepositProcessingBody, - style: text.paragraph?.copyWith(color: colors.textSecondary), - ), + Text(l10n.swapDepositProcessingBody, style: text.paragraph?.copyWith(color: colors.textSecondary)), ], ); } @@ -256,10 +247,7 @@ class _DepositScreenState extends ConsumerState { const SizedBox(height: 80), const SuccessCheck(), const SizedBox(height: 32), - Text( - l10n.swapDepositCompleteTitle, - style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20), - ), + Text(l10n.swapDepositCompleteTitle, style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20)), const SizedBox(height: 12), Text( l10n.swapDepositCompleteBody(amount), diff --git a/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart b/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart index 1d1db629..82dae312 100644 --- a/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart +++ b/mobile-app/lib/v2/screens/swap/refund_address_picker_sheet.dart @@ -52,10 +52,7 @@ class _RefundAddressPickerContentState extends ConsumerState<_RefundAddressPicke if (_addresses.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 32), - child: Text( - l10n.swapRefundPickerEmpty, - style: text.detail?.copyWith(color: colors.textTertiary), - ), + child: Text(l10n.swapRefundPickerEmpty, style: text.detail?.copyWith(color: colors.textTertiary)), ) else ConstrainedBox( diff --git a/mobile-app/lib/v2/screens/welcome/welcome_screen.dart b/mobile-app/lib/v2/screens/welcome/welcome_screen.dart index d1afd067..d568e341 100644 --- a/mobile-app/lib/v2/screens/welcome/welcome_screen.dart +++ b/mobile-app/lib/v2/screens/welcome/welcome_screen.dart @@ -24,11 +24,7 @@ class WelcomeScreenV2 extends ConsumerWidget { const SizedBox(height: 16), SizedBox( width: 210, - child: Text( - l10n.welcomeTagline, - textAlign: TextAlign.center, - style: context.themeText.mediumTitle, - ), + child: Text(l10n.welcomeTagline, textAlign: TextAlign.center, style: context.themeText.mediumTitle), ), const SizedBox(height: 56), QuantusButton.simple( From ff5950ecb861bcea06d0d344e8364b8451aac4d4 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 20 May 2026 13:00:39 +0800 Subject: [PATCH 16/25] fix: lint error - use intl from sdk - import legacy provider --- mobile-app/lib/providers/l10n_provider.dart | 1 + .../v2/screens/activity/transaction_detail_sheet.dart | 3 ++- mobile-app/lib/v2/screens/activity/tx_item.dart | 3 +-- mobile-app/lib/v2/screens/pos/pos_qr_screen.dart | 6 ++---- .../lib/src/services/datetime_formatting_service.dart | 11 +++++++++++ 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/mobile-app/lib/providers/l10n_provider.dart b/mobile-app/lib/providers/l10n_provider.dart index 3b7d0bfc..fe7e4cab 100644 --- a/mobile-app/lib/providers/l10n_provider.dart +++ b/mobile-app/lib/providers/l10n_provider.dart @@ -34,6 +34,7 @@ library; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/legacy.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/l10n/app_localizations.dart'; diff --git a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart index bad32a9b..dfc650e2 100644 --- a/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart +++ b/mobile-app/lib/v2/screens/activity/transaction_detail_sheet.dart @@ -31,8 +31,9 @@ class _TransactionDetailSheet extends ConsumerWidget { String _title(AppLocalizations l10n) { if (_isPending) return l10n.activityDetailTitleSending; - if (tx.isReversibleScheduled) + if (tx.isReversibleScheduled) { return _isSend ? l10n.activityDetailTitleScheduled : l10n.activityDetailTitleReceiving; + } return _isSend ? l10n.activityDetailTitleSent : l10n.activityDetailTitleReceived; } diff --git a/mobile-app/lib/v2/screens/activity/tx_item.dart b/mobile-app/lib/v2/screens/activity/tx_item.dart index 96857ed0..8c581c74 100644 --- a/mobile-app/lib/v2/screens/activity/tx_item.dart +++ b/mobile-app/lib/v2/screens/activity/tx_item.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/shared/extensions/transaction_event_extension.dart'; @@ -228,5 +227,5 @@ String dateGroupLabel(DateTime date, AppLocalizations l10n, String localeName) { final diff = today.difference(txDay).inDays; if (diff == 0) return l10n.activityDateToday; if (diff == 1) return l10n.activityDateYesterday; - return DateFormat.yMMMd(localeName).format(date); + return DatetimeFormattingService.formatDateGroupLabel(date, localeName); } 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 2602b988..75e0ab13 100644 --- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -2,7 +2,6 @@ import 'dart:async'; 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/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; @@ -407,8 +406,7 @@ class _PosQrScreenState extends ConsumerState { } String _formatPaidAt(DateTime dt, String localeName, AppLocalizations l10n) { - final date = DateFormat.yMMMd(localeName).format(dt); - final time = DateFormat.jm(localeName).format(dt); - return l10n.posQrPaidAt('$date, $time'); + final dateTime = DatetimeFormattingService.formatPaidAt(dt, localeName); + return l10n.posQrPaidAt(dateTime); } } diff --git a/quantus_sdk/lib/src/services/datetime_formatting_service.dart b/quantus_sdk/lib/src/services/datetime_formatting_service.dart index 962e2b3a..2581acfc 100644 --- a/quantus_sdk/lib/src/services/datetime_formatting_service.dart +++ b/quantus_sdk/lib/src/services/datetime_formatting_service.dart @@ -142,4 +142,15 @@ class DatetimeFormattingService { return '$hours hr${hours != 1 ? 's' : ''}'; } } + + static String formatPaidAt(DateTime dt, String localeName) { + final date = DateFormat.yMMMd(localeName).format(dt); + final time = DateFormat.jm(localeName).format(dt); + + return '$date, $time'; + } + + static String formatDateGroupLabel(DateTime dt, String localeName) { + return DateFormat.yMMMd(localeName).format(dt); + } } From 9bdcbfb5d934e4d752b4b623b217ed49abd8f1ff Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 20 May 2026 13:35:51 +0800 Subject: [PATCH 17/25] feat: address reset bug --- .../ios/Runner.xcodeproj/project.pbxproj | 42 +++--- .../xcshareddata/swiftpm/Package.resolved | 122 ++++++++++++++++++ .../xcshareddata/xcschemes/Runner.xcscheme | 18 +++ .../xcshareddata/swiftpm/Package.resolved | 122 ++++++++++++++++++ .../providers/currency_display_provider.dart | 4 + mobile-app/lib/providers/l10n_provider.dart | 11 +- mobile-app/lib/services/logout_service.dart | 4 + .../v2/components/recovery_phrase_body.dart | 3 + mobile-app/pubspec.lock | 16 +-- 9 files changed, 314 insertions(+), 28 deletions(-) create mode 100644 mobile-app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 mobile-app/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/mobile-app/ios/Runner.xcodeproj/project.pbxproj b/mobile-app/ios/Runner.xcodeproj/project.pbxproj index c7658d5c..2ef55597 100644 --- a/mobile-app/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile-app/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3EB5255C5F1F1513728E6747 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9580F7AEEA7699162A02D8FF /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -56,6 +57,7 @@ 68138D4BA211BBE4928DCB99 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7B158D4527F6BD4C2533FF77 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9580F7AEEA7699162A02D8FF /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -83,6 +85,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 3EB5255C5F1F1513728E6747 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -114,6 +117,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -203,13 +207,15 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 52CEAAE956158F7EC6981CFF /* [CP] Embed Pods Frameworks */, - D8ADC208C23E118C9A22C81C /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -243,6 +249,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -346,23 +355,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$PROJECT_DIR/../tool/generate_version.sh\"\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - D8ADC208C23E118C9A22C81C /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; E20B4FB1A7C79433CBBF421D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -782,6 +774,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/mobile-app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile-app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..d46b0f76 --- /dev/null +++ b/mobile-app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,122 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "d10045cace0b4c335c4efa8f7df7e9a9fc5a7c60", + "version" : "12.13.0" + } + }, + { + "identity" : "google-ads-on-device-conversion-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state" : { + "revision" : "19dffda9a9caf8d86570ff846535902d8509d7bf", + "version" : "3.5.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "c2c76bebcfbb90d90ea10599f934f9af160e1604", + "version" : "12.13.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a", + "version" : "5.3.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + } + ], + "version" : 2 +} diff --git a/mobile-app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile-app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d42..c3fedb29 100644 --- a/mobile-app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/mobile-app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + { state = currency; } + void reset() { + state = _load(_settings); + } + static FiatCurrency _load(SettingsService settings) { final code = settings.getSelectedFiatCurrency(); if (code == null) return FiatCurrency.usd; diff --git a/mobile-app/lib/providers/l10n_provider.dart b/mobile-app/lib/providers/l10n_provider.dart index fe7e4cab..61d5a233 100644 --- a/mobile-app/lib/providers/l10n_provider.dart +++ b/mobile-app/lib/providers/l10n_provider.dart @@ -33,6 +33,8 @@ /// `lookupAppLocalizations(const Locale('en'))` in unit tests. library; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/legacy.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -61,9 +63,14 @@ class SelectedAppLocaleNotifier extends StateNotifier { state = locale; } + void reset() { + state = _load(_settings); + } + static AppLocale _load(SettingsService settings) { - final code = settings.getSelectedAppLocale(); - if (code == null) return AppLocale.en; + String? code = settings.getSelectedAppLocale(); + code ??= Platform.localeName.split('_').first.toLowerCase(); + return AppLocale.fromCode(code); } } diff --git a/mobile-app/lib/services/logout_service.dart b/mobile-app/lib/services/logout_service.dart index f6075005..14aa55b6 100644 --- a/mobile-app/lib/services/logout_service.dart +++ b/mobile-app/lib/services/logout_service.dart @@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_associations_providers.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/l10n_provider.dart'; import 'package:resonance_network_wallet/providers/mining_rewards_provider.dart'; import 'package:resonance_network_wallet/providers/pending_transactions_provider.dart'; import 'package:resonance_network_wallet/providers/remote_config_provider.dart'; @@ -28,6 +30,8 @@ class LogoutService { _ref.read(accountsProvider.notifier).reset(); _ref.read(activeAccountProvider.notifier).reset(); _ref.read(accountAssociationsProvider.notifier).reset(); + _ref.read(selectedAppLocaleProvider.notifier).reset(); + _ref.read(selectedFiatCurrencyProvider.notifier).reset(); Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const WelcomeScreenV2()), (r) => false); } diff --git a/mobile-app/lib/v2/components/recovery_phrase_body.dart b/mobile-app/lib/v2/components/recovery_phrase_body.dart index 60a3301d..50907991 100644 --- a/mobile-app/lib/v2/components/recovery_phrase_body.dart +++ b/mobile-app/lib/v2/components/recovery_phrase_body.dart @@ -62,6 +62,7 @@ class RecoveryPhraseBody extends ConsumerWidget { Widget _bottomBar(BuildContext context, WidgetRef ref, AppColorsV2 colors) { final l10n = ref.watch(l10nProvider); + final padding = const EdgeInsets.symmetric(horizontal: 24, vertical: 12); return ScaffoldBaseBottomContent( child: Row( @@ -73,6 +74,7 @@ class RecoveryPhraseBody extends ConsumerWidget { iconPlacement: IconPlacement.leading, onTap: () => _copyToClipboard(context, ref), variant: ButtonVariant.secondary, + padding: padding, ), ), const SizedBox(width: 24), @@ -83,6 +85,7 @@ class RecoveryPhraseBody extends ConsumerWidget { isLoading: isPrimaryButtonLoading, onTap: onPrimary, variant: ButtonVariant.primary, + padding: padding, ), ), ], diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index 96024dfc..29ea6276 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -1026,10 +1026,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -1661,26 +1661,26 @@ packages: dependency: transitive description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.17" timezone: dependency: "direct main" description: From 72f5aae1c8ea21b3a44f107b3cf1b67e4b5f4c88 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 20 May 2026 13:49:13 +0800 Subject: [PATCH 18/25] feat: remove redundant setting clear Setting already cleared in substarte service logout --- mobile-app/lib/services/logout_service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile-app/lib/services/logout_service.dart b/mobile-app/lib/services/logout_service.dart index 14aa55b6..60a4acf4 100644 --- a/mobile-app/lib/services/logout_service.dart +++ b/mobile-app/lib/services/logout_service.dart @@ -22,7 +22,6 @@ class LogoutService { _ref.read(firebaseMessagingServiceProvider).unregisterDevice(); } - SettingsService().clearAll(); SubstrateService().logout(); _ref.read(pendingTransactionsProvider.notifier).clear(); _ref.read(miningRewardsServiceProvider).clearCachedRewardsData(); From 04306f726338b57dd74cbb9c9732809b96bfddb4 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 20 May 2026 13:49:25 +0800 Subject: [PATCH 19/25] fix: reset depend on setting clear --- mobile-app/lib/providers/currency_display_provider.dart | 6 ++++-- mobile-app/lib/providers/l10n_provider.dart | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mobile-app/lib/providers/currency_display_provider.dart b/mobile-app/lib/providers/currency_display_provider.dart index 4c1e3263..89ced883 100644 --- a/mobile-app/lib/providers/currency_display_provider.dart +++ b/mobile-app/lib/providers/currency_display_provider.dart @@ -145,6 +145,8 @@ final selectedFiatCurrencyProvider = StateNotifierProvider { final SettingsService _settings; + static final FiatCurrency _defaultCurrency = FiatCurrency.usd; + SelectedFiatCurrencyNotifier(this._settings) : super(_load(_settings)); /// Persists and applies [currency] as the active fiat currency. @@ -154,12 +156,12 @@ class SelectedFiatCurrencyNotifier extends StateNotifier { } void reset() { - state = _load(_settings); + state = _defaultCurrency; } static FiatCurrency _load(SettingsService settings) { final code = settings.getSelectedFiatCurrency(); - if (code == null) return FiatCurrency.usd; + if (code == null) return _defaultCurrency; return FiatCurrency.fromCode(code); } } diff --git a/mobile-app/lib/providers/l10n_provider.dart b/mobile-app/lib/providers/l10n_provider.dart index 61d5a233..9ca29dd2 100644 --- a/mobile-app/lib/providers/l10n_provider.dart +++ b/mobile-app/lib/providers/l10n_provider.dart @@ -56,6 +56,8 @@ final selectedAppLocaleProvider = StateNotifierProvider { final SettingsService _settings; + static final String _defaultLocaleCode = Platform.localeName.split('_').first.toLowerCase(); + SelectedAppLocaleNotifier(this._settings) : super(_load(_settings)); Future select(AppLocale locale) async { @@ -64,12 +66,12 @@ class SelectedAppLocaleNotifier extends StateNotifier { } void reset() { - state = _load(_settings); + state = AppLocale.fromCode(_defaultLocaleCode); } static AppLocale _load(SettingsService settings) { String? code = settings.getSelectedAppLocale(); - code ??= Platform.localeName.split('_').first.toLowerCase(); + code ??= _defaultLocaleCode; return AppLocale.fromCode(code); } From e47947a00d0fd63722077bde5ac143e6bce10464 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 20 May 2026 13:55:48 +0800 Subject: [PATCH 20/25] chore: sync lock file --- miner-app/pubspec.lock | 8 ++++---- quantus_sdk/pubspec.lock | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/miner-app/pubspec.lock b/miner-app/pubspec.lock index c624f81f..71908c5e 100644 --- a/miner-app/pubspec.lock +++ b/miner-app/pubspec.lock @@ -564,10 +564,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" native_toolchain_c: dependency: transitive description: @@ -967,10 +967,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" typed_data: dependency: transitive description: diff --git a/quantus_sdk/pubspec.lock b/quantus_sdk/pubspec.lock index 7a95d46a..f708ea54 100644 --- a/quantus_sdk/pubspec.lock +++ b/quantus_sdk/pubspec.lock @@ -516,10 +516,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" native_toolchain_c: dependency: transitive description: @@ -904,10 +904,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" typed_data: dependency: transitive description: From 8a0ba03fae6c0c631924d8f035f0643539e4b845 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 20 May 2026 15:33:41 +0800 Subject: [PATCH 21/25] fix: resolve review issues --- mobile-app/lib/l10n/app_en.arb | 41 ++++++++++--------- mobile-app/lib/l10n/app_id.arb | 21 +++++----- mobile-app/lib/l10n/app_localizations.dart | 36 ++++++++-------- mobile-app/lib/l10n/app_localizations_en.dart | 22 +++++----- mobile-app/lib/l10n/app_localizations_id.dart | 34 +++++++-------- .../v2/components/address_details_card.dart | 2 +- .../lib/v2/components/qr_scanner_page.dart | 2 +- .../v2/screens/accounts/accounts_sheet.dart | 2 +- .../accounts/add_hardware_account_screen.dart | 2 +- .../activity/transaction_detail_sheet.dart | 2 +- .../v2/screens/send/input_amount_screen.dart | 4 +- .../v2/screens/send/review_send_screen.dart | 6 +-- .../settings/currency_picker_screen.dart | 16 +------- .../settings/language_picker_screen.dart | 16 +------- .../settings/settings_picker_screen.dart | 21 ++++++++-- .../v2/screens/settings/settings_screen.dart | 4 +- .../lib/v2/screens/swap/deposit_screen.dart | 2 + 17 files changed, 114 insertions(+), 119 deletions(-) diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index a7d67def..9e7970e2 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -227,10 +227,6 @@ "@accountsSheetAddAccount": { "description": "Button to add a new account" }, - "accountsSheetLoading": "Loading...", - "@accountsSheetLoading": { - "description": "Loading balance in accounts sheet" - }, "accountsSheetBalanceUnavailable": "Balance unavailable", "@accountsSheetBalanceUnavailable": { "description": "When account balance fails to load" @@ -366,10 +362,6 @@ "@addHardwareAccountAddressHint": { "description": "Address field hint" }, - "addHardwareAccountScanQr": "Scan QR Code", - "@addHardwareAccountScanQr": { - "description": "Scan QR code button" - }, "addHardwareAccountDebugFill": "Debug Fill", "@addHardwareAccountDebugFill": { "description": "Debug fill button" @@ -455,18 +447,6 @@ "@sendInputAmountChecksumRequired": { "description": "Error when recipient checksum is missing" }, - "sendInputAmountBalance": "{balance} {symbol}", - "@sendInputAmountBalance": { - "description": "Formatted balance with token symbol", - "placeholders": { - "balance": { - "type": "String" - }, - "symbol": { - "type": "String" - } - } - }, "sendReviewSending": "SENDING", "@sendReviewSending": { @@ -1537,6 +1517,10 @@ "description": "Empty state on refund address picker" }, + "componentQrScannerTitle": "Scan QR Code", + "@componentQrScannerTitle": { + "description": "Text for app bar or button label on QR scanner component" + }, "componentQrScannerNoCode": "No QR code found in image", "@componentQrScannerNoCode": { "description": "Snackbar when gallery image has no QR code" @@ -1560,5 +1544,22 @@ "componentNameFieldHint": "Enter a name for your account", "@componentNameFieldHint": { "description": "Hint text on account name field" + }, + + "commonLoading": "Loading...", + "@commonLoading": { + "description": "Text for generic loading state" + }, + "commonAmountBalance": "{balance} {symbol}", + "@commonAmountBalance": { + "description": "Formatted balance with token symbol", + "placeholders": { + "balance": { + "type": "String" + }, + "symbol": { + "type": "String" + } + } } } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index 32e7bc10..4aa200b5 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -1,9 +1,9 @@ { - "walletInitErrorTitle": "Wallet Eror", + "walletInitErrorTitle": "Wallet Bermasalah", "walletInitErrorMessage": "Gagal mencari secret phrase. Coba pulihkan wallet anda.", "walletInitErrorButtonLabel": "OK", - "authUseDeviceBiometricsToUnlock": "Gunakan biometrik untuk membuka perangkat", + "authUseDeviceBiometricsToUnlock": "Gunakan biometrik untuk mengakses wallet", "authAuthenticating": "Mengotentikasi...", "authUnlockWallet": "Buka Wallet", "authAuthorizationRequired": "Otorisasi \n Diperlukan", @@ -38,7 +38,7 @@ "importWalletButton": "Impor", "importWalletValidationError": "Recovery phrase harus 12 atau 24 kata", - "homeError": "Eror: {error}", + "homeError": "Gagal: {error}", "homeNoActiveAccount": "Tidak ada akun aktif", "homeCharge": "Tagih", "homeGetTestnetTokens": "Dapatkan Token Testnet ↗", @@ -59,7 +59,6 @@ "accountsSheetFailedLoadActiveAccount": "Gagal memuat akun aktif.", "accountsSheetNoAccountsFound": "Tidak ada akun ditemukan.", "accountsSheetAddAccount": "Tambah Akun", - "accountsSheetLoading": "Memuat...", "accountsSheetBalanceUnavailable": "Saldo tidak tersedia", "accountsSheetBalance": "{balance} {symbol}", @@ -95,7 +94,6 @@ "addHardwareAccountNameHintAccount": "Akun", "addHardwareAccountAddressLabel": "ALAMAT", "addHardwareAccountAddressHint": "Alamat SS58", - "addHardwareAccountScanQr": "Pindai Kode QR", "addHardwareAccountDebugFill": "Isi Debug", "addHardwareAccountNameRequired": "Nama wajib diisi", "addHardwareAccountInvalidAddress": "Alamat tidak valid", @@ -117,7 +115,6 @@ "sendInputAmountMax": "Maks", "sendInputAmountInvalidAmount": "Masukkan jumlah yang valid", "sendInputAmountChecksumRequired": "Checksum penerima diperlukan", - "sendInputAmountBalance": "{balance} {symbol}", "sendReviewSending": "MENGIRIM", "sendReviewTo": "KE", @@ -143,7 +140,7 @@ "sendLogicReviewSend": "Tinjau Pengiriman", "activityTitle": "Aktivitas", - "activityError": "Error: {error}", + "activityError": "Gagal: {error}", "activityNoAccount": "Tidak ada akun", "activityEmpty": "Belum ada transaksi", "activityFilterAll": "Semua", @@ -195,7 +192,7 @@ "posQrTitleScanToPay": "Pindai untuk Bayar", "posQrTitlePaymentReceived": "Pembayaran Diterima", - "posQrError": "Error: {error}", + "posQrError": "Gagal: {error}", "posQrNoActiveAccount": "Tidak ada akun aktif", "posQrInvalidAmount": "Jumlah tidak valid. Ketuk untuk coba lagi.", "posQrConnectionLost": "Koneksi terputus. Ketuk untuk coba lagi.", @@ -205,7 +202,7 @@ "posQrAmountReceived": "{amount} diterima", "posQrFrom": "Dari:", "posQrWaitingForPayment": "Menunggu pembayaran", - "posQrNetworkError": "Error Jaringan", + "posQrNetworkError": "Jaringan Bermasalah", "posQrTryAgain": "Coba Lagi", "posQrPaidAt": "Pada {time}", @@ -362,10 +359,14 @@ "swapRefundPickerTitle": "Alamat Refund", "swapRefundPickerEmpty": "Tidak ada alamat refund terbaru", + "componentQrScannerTitle": "Pindai Kode QR", "componentQrScannerNoCode": "Tidak ada kode QR pada gambar", "componentShare": "Bagikan", "componentAddressLabel": "ALAMAT", "componentCheckphraseLabel": "CHECKPHRASE", "componentCheckphraseCopied": "Checkphrase disalin", - "componentNameFieldHint": "Masukkan nama untuk akun Anda" + "componentNameFieldHint": "Masukkan nama untuk akun Anda", + + "commonLoading": "Memuat...", + "commonAmountBalance": "{balance} {symbol}" } diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index fa150993..079e2f6a 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -398,12 +398,6 @@ abstract class AppLocalizations { /// **'Add Account'** String get accountsSheetAddAccount; - /// Loading balance in accounts sheet - /// - /// In en, this message translates to: - /// **'Loading...'** - String get accountsSheetLoading; - /// When account balance fails to load /// /// In en, this message translates to: @@ -578,12 +572,6 @@ abstract class AppLocalizations { /// **'SS58 address'** String get addHardwareAccountAddressHint; - /// Scan QR code button - /// - /// In en, this message translates to: - /// **'Scan QR Code'** - String get addHardwareAccountScanQr; - /// Debug fill button /// /// In en, this message translates to: @@ -692,12 +680,6 @@ abstract class AppLocalizations { /// **'Recipient checksum is required'** String get sendInputAmountChecksumRequired; - /// Formatted balance with token symbol - /// - /// In en, this message translates to: - /// **'{balance} {symbol}'** - String sendInputAmountBalance(String balance, String symbol); - /// Sending section label on review screen /// /// In en, this message translates to: @@ -1988,6 +1970,12 @@ abstract class AppLocalizations { /// **'No recent refund addresses'** String get swapRefundPickerEmpty; + /// Text for app bar or button label on QR scanner component + /// + /// In en, this message translates to: + /// **'Scan QR Code'** + String get componentQrScannerTitle; + /// Snackbar when gallery image has no QR code /// /// In en, this message translates to: @@ -2023,6 +2011,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Enter a name for your account'** String get componentNameFieldHint; + + /// Text for generic loading state + /// + /// In en, this message translates to: + /// **'Loading...'** + String get commonLoading; + + /// Formatted balance with token symbol + /// + /// In en, this message translates to: + /// **'{balance} {symbol}'** + String commonAmountBalance(String balance, String symbol); } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 1497815d..7bd54392 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -170,9 +170,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get accountsSheetAddAccount => 'Add Account'; - @override - String get accountsSheetLoading => 'Loading...'; - @override String get accountsSheetBalanceUnavailable => 'Balance unavailable'; @@ -264,9 +261,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get addHardwareAccountAddressHint => 'SS58 address'; - @override - String get addHardwareAccountScanQr => 'Scan QR Code'; - @override String get addHardwareAccountDebugFill => 'Debug Fill'; @@ -325,11 +319,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get sendInputAmountChecksumRequired => 'Recipient checksum is required'; - @override - String sendInputAmountBalance(String balance, String symbol) { - return '$balance $symbol'; - } - @override String get sendReviewSending => 'SENDING'; @@ -1036,6 +1025,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get swapRefundPickerEmpty => 'No recent refund addresses'; + @override + String get componentQrScannerTitle => 'Scan QR Code'; + @override String get componentQrScannerNoCode => 'No QR code found in image'; @@ -1053,4 +1045,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get componentNameFieldHint => 'Enter a name for your account'; + + @override + String get commonLoading => 'Loading...'; + + @override + String commonAmountBalance(String balance, String symbol) { + return '$balance $symbol'; + } } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index eca6a295..57b5e447 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -9,7 +9,7 @@ class AppLocalizationsId extends AppLocalizations { AppLocalizationsId([String locale = 'id']) : super(locale); @override - String get walletInitErrorTitle => 'Wallet Eror'; + String get walletInitErrorTitle => 'Wallet Bermasalah'; @override String get walletInitErrorMessage => 'Gagal mencari secret phrase. Coba pulihkan wallet anda.'; @@ -18,7 +18,7 @@ class AppLocalizationsId extends AppLocalizations { String get walletInitErrorButtonLabel => 'OK'; @override - String get authUseDeviceBiometricsToUnlock => 'Gunakan biometrik untuk membuka perangkat'; + String get authUseDeviceBiometricsToUnlock => 'Gunakan biometrik untuk mengakses wallet'; @override String get authAuthenticating => 'Mengotentikasi...'; @@ -114,7 +114,7 @@ class AppLocalizationsId extends AppLocalizations { @override String homeError(String error) { - return 'Eror: $error'; + return 'Gagal: $error'; } @override @@ -171,9 +171,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get accountsSheetAddAccount => 'Tambah Akun'; - @override - String get accountsSheetLoading => 'Memuat...'; - @override String get accountsSheetBalanceUnavailable => 'Saldo tidak tersedia'; @@ -265,9 +262,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get addHardwareAccountAddressHint => 'Alamat SS58'; - @override - String get addHardwareAccountScanQr => 'Pindai Kode QR'; - @override String get addHardwareAccountDebugFill => 'Isi Debug'; @@ -326,11 +320,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get sendInputAmountChecksumRequired => 'Checksum penerima diperlukan'; - @override - String sendInputAmountBalance(String balance, String symbol) { - return '$balance $symbol'; - } - @override String get sendReviewSending => 'MENGIRIM'; @@ -400,7 +389,7 @@ class AppLocalizationsId extends AppLocalizations { @override String activityError(String error) { - return 'Error: $error'; + return 'Gagal: $error'; } @override @@ -557,7 +546,7 @@ class AppLocalizationsId extends AppLocalizations { @override String posQrError(String error) { - return 'Error: $error'; + return 'Gagal: $error'; } @override @@ -590,7 +579,7 @@ class AppLocalizationsId extends AppLocalizations { String get posQrWaitingForPayment => 'Menunggu pembayaran'; @override - String get posQrNetworkError => 'Error Jaringan'; + String get posQrNetworkError => 'Jaringan Bermasalah'; @override String get posQrTryAgain => 'Coba Lagi'; @@ -1037,6 +1026,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get swapRefundPickerEmpty => 'Tidak ada alamat refund terbaru'; + @override + String get componentQrScannerTitle => 'Pindai Kode QR'; + @override String get componentQrScannerNoCode => 'Tidak ada kode QR pada gambar'; @@ -1054,4 +1046,12 @@ class AppLocalizationsId extends AppLocalizations { @override String get componentNameFieldHint => 'Masukkan nama untuk akun Anda'; + + @override + String get commonLoading => 'Memuat...'; + + @override + String commonAmountBalance(String balance, String symbol) { + return '$balance $symbol'; + } } diff --git a/mobile-app/lib/v2/components/address_details_card.dart b/mobile-app/lib/v2/components/address_details_card.dart index fc7e1072..c8eb5b67 100644 --- a/mobile-app/lib/v2/components/address_details_card.dart +++ b/mobile-app/lib/v2/components/address_details_card.dart @@ -82,7 +82,7 @@ class _AddressDetailsCardState extends ConsumerState { child: _buildItem( context, l10n.componentCheckphraseLabel, - widget.checksum ?? l10n.accountsSheetLoading, + widget.checksum ?? l10n.commonLoading, isCheckphrase: true, isCopied: _checksumCopied, ), diff --git a/mobile-app/lib/v2/components/qr_scanner_page.dart b/mobile-app/lib/v2/components/qr_scanner_page.dart index dffe5e43..84fc849d 100644 --- a/mobile-app/lib/v2/components/qr_scanner_page.dart +++ b/mobile-app/lib/v2/components/qr_scanner_page.dart @@ -87,7 +87,7 @@ class _QrScannerPageState extends ConsumerState { ], ), ), - Positioned(top: 20, left: 24, right: 24, child: V2AppBar(title: l10n.addHardwareAccountScanQr)), + Positioned(top: 20, left: 24, right: 24, child: V2AppBar(title: l10n.componentQrScannerTitle)), ], ), ); diff --git a/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart b/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart index 04fcb329..94471aca 100644 --- a/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart +++ b/mobile-app/lib/v2/screens/accounts/accounts_sheet.dart @@ -153,7 +153,7 @@ class _AccountsScreenState extends ConsumerState { final balanceAsync = ref.watch(balanceProviderFamily(account.accountId)); final formattingService = ref.watch(numberFormattingServiceProvider); final balanceText = balanceAsync.when( - loading: () => l10n.accountsSheetLoading, + loading: () => l10n.commonLoading, error: (_, _) => l10n.accountsSheetBalanceUnavailable, data: (balance) => l10n.accountsSheetBalance(formattingService.formatBalance(balance), AppConstants.tokenSymbol), ); diff --git a/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart b/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart index 264e7243..e61d2beb 100644 --- a/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart @@ -132,7 +132,7 @@ class _AddHardwareAccountScreenState extends ConsumerState { const SizedBox(height: 4), balance.when( data: (b) => Text( - l10n.sendInputAmountBalance(formattingService.formatBalance(b), AppConstants.tokenSymbol), + l10n.commonAmountBalance(formattingService.formatBalance(b), AppConstants.tokenSymbol), style: text.smallParagraph?.copyWith(color: colors.textTertiary), ), loading: () => Text('...', style: text.smallParagraph?.copyWith(color: colors.textTertiary)), @@ -485,7 +485,7 @@ class _InputAmountScreenState extends ConsumerState { const SizedBox(height: 4), if (!_isFetchingFee) Text( - l10n.sendInputAmountBalance( + l10n.commonAmountBalance( formattingService.formatBalance(_networkFee, maxDecimals: 5), AppConstants.tokenSymbol, ), diff --git a/mobile-app/lib/v2/screens/send/review_send_screen.dart b/mobile-app/lib/v2/screens/send/review_send_screen.dart index af259a8e..5fe82048 100644 --- a/mobile-app/lib/v2/screens/send/review_send_screen.dart +++ b/mobile-app/lib/v2/screens/send/review_send_screen.dart @@ -203,7 +203,7 @@ class _ReviewSendScreenState extends ConsumerState { const SizedBox(height: 7), _summaryRow( label: l10n.sendReviewAmount, - value: l10n.sendInputAmountBalance( + value: l10n.commonAmountBalance( formattingService.formatBalance(widget.amount, maxDecimals: shownDecimals), AppConstants.tokenSymbol, ), @@ -211,7 +211,7 @@ class _ReviewSendScreenState extends ConsumerState { const SizedBox(height: 7), _summaryRow( label: l10n.sendReviewNetworkFee, - value: l10n.sendInputAmountBalance( + value: l10n.commonAmountBalance( formattingService.formatBalance(widget.networkFee, maxDecimals: shownDecimals), AppConstants.tokenSymbol, ), @@ -219,7 +219,7 @@ class _ReviewSendScreenState extends ConsumerState { const SizedBox(height: 7), _summaryRow( label: l10n.sendReviewYouPay, - value: l10n.sendInputAmountBalance( + value: l10n.commonAmountBalance( formattingService.formatBalance(totalRaw, maxDecimals: shownDecimals), AppConstants.tokenSymbol, ), diff --git a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart index 3f0cb175..1ac7994e 100644 --- a/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/currency_picker_screen.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.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/l10n_provider.dart'; -import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_screen.dart'; class CurrencyPickerScreenV2 extends ConsumerWidget { @@ -25,19 +24,8 @@ class CurrencyPickerScreenV2 extends ConsumerWidget { final line = currency.line.toLowerCase(); return line.contains(query) || currency.code.toLowerCase().contains(query); }, - onSelect: (currency) async { - try { - await ref.read(selectedFiatCurrencyProvider.notifier).select(currency); - if (context.mounted) { - Navigator.pop(context); - } - } catch (e) { - debugPrint('error selecting locale: $e'); - if (context.mounted) { - context.showErrorToaster(message: l10n.settingsCurrencyError(e.toString())); - } - } - }, + onSelect: ref.read(selectedFiatCurrencyProvider.notifier).select, + errorMessageBuilder: l10n.settingsCurrencyError, ); } } diff --git a/mobile-app/lib/v2/screens/settings/language_picker_screen.dart b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart index e65cd27f..bf90b3a4 100644 --- a/mobile-app/lib/v2/screens/settings/language_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/language_picker_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:resonance_network_wallet/models/app_locale.dart'; import 'package:resonance_network_wallet/providers/l10n_provider.dart'; -import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/v2/screens/settings/settings_picker_screen.dart'; class LanguagePickerScreenV2 extends ConsumerWidget { @@ -23,19 +22,8 @@ class LanguagePickerScreenV2 extends ConsumerWidget { filter: (locale, query) { return locale.displayName.toLowerCase().contains(query) || locale.languageCode.toLowerCase().contains(query); }, - onSelect: (locale) async { - try { - await ref.read(selectedAppLocaleProvider.notifier).select(locale); - if (context.mounted) { - Navigator.pop(context); - } - } catch (e) { - debugPrint('error selecting locale: $e'); - if (context.mounted) { - context.showErrorToaster(message: l10n.settingsLanguageError(e.toString())); - } - } - }, + onSelect: ref.read(selectedAppLocaleProvider.notifier).select, + errorMessageBuilder: l10n.settingsLanguageError, ); } } diff --git a/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart b/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart index 3a7d950a..e516d068 100644 --- a/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.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/settings/settings_divider.dart'; @@ -17,6 +18,7 @@ class SettingsPickerScreen extends StatefulWidget { required this.selected, required this.labelBuilder, required this.onSelect, + required this.errorMessageBuilder, this.filter, }); @@ -28,6 +30,7 @@ class SettingsPickerScreen extends StatefulWidget { final String Function(T) labelBuilder; final bool Function(T item, String query)? filter; final Future Function(T) onSelect; + final String Function(String error) errorMessageBuilder; @override State> createState() => _SettingsPickerScreenState(); @@ -100,12 +103,24 @@ class _SettingsPickerScreenState extends State> { selected: item == widget.selected, colors: colors, text: text, - onTap: () { + onTap: () async { if (_isLoading) return; setState(() => _isLoading = true); - widget.onSelect(item); - setState(() => _isLoading = false); + + try { + await widget.onSelect(item); + if (context.mounted) { + Navigator.pop(context); + } + } catch (e) { + debugPrint('error selecting locale: $e'); + if (context.mounted) { + context.showErrorToaster(message: widget.errorMessageBuilder(e.toString())); + } + } finally { + if (mounted) setState(() => _isLoading = false); + } }, ); }, diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index c26ae389..f93172c8 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -46,7 +46,7 @@ class _SettingsScreenV2State extends ConsumerState { subtitle: l10n.settingsMiningRewardsSubtitle(data.totalBlocks), trailing: trailing, ), - loading: () => _buildTappableRow(e.value, subtitle: l10n.accountsSheetLoading, trailing: trailing), + loading: () => _buildTappableRow(e.value, subtitle: l10n.commonLoading, trailing: trailing), error: (err, st) { debugPrint('Error getting mining rewards: ${err.toString()}'); debugPrint('Stack trace: ${st.toString()}'); @@ -105,7 +105,7 @@ List<_SettingsHubItem> _settingsHubItems(AppColorsV2 colors, AppLocalizations l1 _SettingsHubItem( leading: _settingsHubIcon(colors, svg: SvgPicture.asset('assets/v2/axe.svg', width: 18, height: 18)), title: l10n.settingsMiningRewards, - subtitle: l10n.accountsSheetLoading, + subtitle: l10n.commonLoading, page: const MiningRewardsScreen(), isMiningRewards: true, ), diff --git a/mobile-app/lib/v2/screens/swap/deposit_screen.dart b/mobile-app/lib/v2/screens/swap/deposit_screen.dart index 2f1bf42d..67e2a050 100644 --- a/mobile-app/lib/v2/screens/swap/deposit_screen.dart +++ b/mobile-app/lib/v2/screens/swap/deposit_screen.dart @@ -64,6 +64,8 @@ class _DepositScreenState extends ConsumerState { } } + // Currently this is only for demo purposes + // We just return the demo warning for now String _getDepositAddress(AppLocalizations l10n) { return l10n.swapDepositDemoWarning; } From e92bb036ad35ef8f29fae8c1774cd576a317a535 Mon Sep 17 00:00:00 2001 From: Beast Date: Thu, 21 May 2026 21:31:40 +0800 Subject: [PATCH 22/25] feat: properly debug sensitive information --- mobile-app/lib/v2/screens/pos/pos_qr_screen.dart | 11 ++++++----- quantus_sdk/lib/src/extensions/context_extension.dart | 9 +++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) 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 75e0ab13..d86e374c 100644 --- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -60,7 +60,8 @@ class _PosQrScreenState extends ConsumerState { final expectedPlanck = formattingService.parseAmount(widget.amount); if (expectedPlanck == null) { - debugPrint('[PosQr] ERROR: failed to parse amount "${widget.amount}"'); + context.debugPrint('[PosQr] ERROR: failed to parse amount "${widget.amount}"'); + if (mounted) setState(() => _watchError = l10n.posQrInvalidAmount); return; } @@ -70,15 +71,15 @@ class _PosQrScreenState extends ConsumerState { _watchError = null; }); - debugPrint('[PosQr] watching address=${active.account.accountId} expected=$expectedPlanck planck'); + context.debugPrint('[PosQr] watching address=${active.account.accountId} expected=$expectedPlanck planck'); _txWatch.watch( address: active.account.accountId, onTransfer: (tx) { - debugPrint('[PosQr] onTransfer from=${tx.from} amount=${tx.amount} hash=${tx.txHash}'); + context.debugPrint('[PosQr] onTransfer from=${tx.from} amount=${tx.amount} hash=${tx.txHash}'); if (_isPaid) return; final received = BigInt.tryParse(tx.amount); if (received != expectedPlanck) { - debugPrint('[PosQr] amount mismatch (received=$received expected=$expectedPlanck), ignoring'); + context.debugPrint('[PosQr] amount mismatch (received=$received expected=$expectedPlanck), ignoring'); return; } @@ -105,7 +106,7 @@ class _PosQrScreenState extends ConsumerState { } }, onError: (e) { - debugPrint('[PosQr] watch error: $e'); + context.debugPrint('[PosQr] watch error: $e'); _txWatch.dispose(); _timeoutTimer?.cancel(); if (mounted) { diff --git a/quantus_sdk/lib/src/extensions/context_extension.dart b/quantus_sdk/lib/src/extensions/context_extension.dart index f6e0478c..f7add296 100644 --- a/quantus_sdk/lib/src/extensions/context_extension.dart +++ b/quantus_sdk/lib/src/extensions/context_extension.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; extension SharePositionOriginExtension on BuildContext { @@ -6,3 +7,11 @@ extension SharePositionOriginExtension on BuildContext { return box == null ? null : box.localToGlobal(Offset.zero) & box.size; } } + +extension DebugPrintExtension on BuildContext { + void debugPrint(String message) { + if (kDebugMode) { + debugPrint(message); + } + } +} \ No newline at end of file From aeaad0d6dafea2a573ea50dd2188ccf3a24584eb Mon Sep 17 00:00:00 2001 From: Beast Date: Thu, 21 May 2026 21:40:22 +0800 Subject: [PATCH 23/25] fix: not waiting settings clean up --- mobile-app/lib/services/logout_service.dart | 7 +++++-- .../lib/v2/screens/settings/reset_confirmation_screen.dart | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mobile-app/lib/services/logout_service.dart b/mobile-app/lib/services/logout_service.dart index 60a4acf4..cc4b9e9c 100644 --- a/mobile-app/lib/services/logout_service.dart +++ b/mobile-app/lib/services/logout_service.dart @@ -17,12 +17,12 @@ class LogoutService { final Ref _ref; LogoutService(this._ref); - Future logout(BuildContext context) async { + Future _clearSettingsAndMemory() async { if (_ref.read(remoteConfigProvider).enableRemoteNotifications) { _ref.read(firebaseMessagingServiceProvider).unregisterDevice(); } - SubstrateService().logout(); + await SubstrateService().logout(); _ref.read(pendingTransactionsProvider.notifier).clear(); _ref.read(miningRewardsServiceProvider).clearCachedRewardsData(); _ref.invalidate(miningRewardsProvider); @@ -31,7 +31,10 @@ class LogoutService { _ref.read(accountAssociationsProvider.notifier).reset(); _ref.read(selectedAppLocaleProvider.notifier).reset(); _ref.read(selectedFiatCurrencyProvider.notifier).reset(); + } + void logout(BuildContext context) { + _clearSettingsAndMemory(); Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const WelcomeScreenV2()), (r) => false); } } diff --git a/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart b/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart index a7198971..d0d18b0d 100644 --- a/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart +++ b/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart @@ -26,7 +26,7 @@ class _ResetConfirmationScreenState extends ConsumerState Date: Thu, 21 May 2026 22:40:24 +0800 Subject: [PATCH 24/25] feat: resolve PR issues --- mobile-app/lib/l10n/app_en.arb | 8 ++++---- mobile-app/lib/l10n/app_id.arb | 4 ++-- mobile-app/lib/l10n/app_localizations.dart | 12 ++++++------ mobile-app/lib/l10n/app_localizations_en.dart | 6 +++--- mobile-app/lib/l10n/app_localizations_id.dart | 6 +++--- mobile-app/lib/shared/utils/print.dart | 7 +++++++ .../lib/v2/screens/create/wallet_ready_screen.dart | 2 +- mobile-app/lib/v2/screens/pos/pos_qr_screen.dart | 11 ++++++----- .../recovery_phrase_confirmation_screen.dart | 2 +- .../screens/settings/reset_confirmation_screen.dart | 2 +- .../screens/settings/settings_caution_scaffold.dart | 2 +- .../v2/screens/settings/settings_picker_screen.dart | 3 ++- .../lib/src/extensions/context_extension.dart | 9 --------- 13 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 mobile-app/lib/shared/utils/print.dart diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 9e7970e2..737f9982 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -66,10 +66,6 @@ "@createWalletCautionCheckboxLabel": { "description": "Checkbox label on the recovery phrase caution screen" }, - "createWalletCautionContinue": "Continue", - "@createWalletCautionContinue": { - "description": "Continue button on the recovery phrase caution screen" - }, "createWalletRecoveryPhraseNext": "Next", "@createWalletRecoveryPhraseNext": { "description": "Primary button on the new wallet recovery phrase screen" @@ -1561,5 +1557,9 @@ "type": "String" } } + }, + "commonContinue": "Continue", + "@commonContinue": { + "description": "Continue button on various screens" } } diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index 4aa200b5..73450e04 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -18,7 +18,6 @@ "createWalletCautionBullet2": "Siapa pun yang mendapatkannya akan memiliki kendali penuh atas dana Anda, secara permanen", "createWalletCautionBullet3": "Tuliskan dan simpan di tempat yang aman. Jangan simpan secara digital", "createWalletCautionCheckboxLabel": "Saya memahami bahwa siapa pun yang memiliki recovery phrase saya dapat mengakses wallet saya. Saya akan menyimpannya dengan aman.", - "createWalletCautionContinue": "Lanjutkan", "createWalletRecoveryPhraseNext": "Berikutnya", "createWalletRecoveryPhraseFailedGenerate": "Gagal membuat: {error}", "createWalletRecoveryPhraseSaveError": "Gagal menyimpan wallet: {error}", @@ -368,5 +367,6 @@ "componentNameFieldHint": "Masukkan nama untuk akun Anda", "commonLoading": "Memuat...", - "commonAmountBalance": "{balance} {symbol}" + "commonAmountBalance": "{balance} {symbol}", + "commonContinue": "Lanjutkan" } diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 079e2f6a..dcd5b871 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -188,12 +188,6 @@ abstract class AppLocalizations { /// **'I understand that anyone with my recovery phrase can access my wallet. I will store it safely.'** String get createWalletCautionCheckboxLabel; - /// Continue button on the recovery phrase caution screen - /// - /// In en, this message translates to: - /// **'Continue'** - String get createWalletCautionContinue; - /// Primary button on the new wallet recovery phrase screen /// /// In en, this message translates to: @@ -2023,6 +2017,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'{balance} {symbol}'** String commonAmountBalance(String balance, String symbol); + + /// Continue button on various screens + /// + /// In en, this message translates to: + /// **'Continue'** + String get commonContinue; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index 7bd54392..0d497542 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -58,9 +58,6 @@ class AppLocalizationsEn extends AppLocalizations { String get createWalletCautionCheckboxLabel => 'I understand that anyone with my recovery phrase can access my wallet. I will store it safely.'; - @override - String get createWalletCautionContinue => 'Continue'; - @override String get createWalletRecoveryPhraseNext => 'Next'; @@ -1053,4 +1050,7 @@ class AppLocalizationsEn extends AppLocalizations { String commonAmountBalance(String balance, String symbol) { return '$balance $symbol'; } + + @override + String get commonContinue => 'Continue'; } diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index 57b5e447..f35b2555 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -59,9 +59,6 @@ class AppLocalizationsId extends AppLocalizations { String get createWalletCautionCheckboxLabel => 'Saya memahami bahwa siapa pun yang memiliki recovery phrase saya dapat mengakses wallet saya. Saya akan menyimpannya dengan aman.'; - @override - String get createWalletCautionContinue => 'Lanjutkan'; - @override String get createWalletRecoveryPhraseNext => 'Berikutnya'; @@ -1054,4 +1051,7 @@ class AppLocalizationsId extends AppLocalizations { String commonAmountBalance(String balance, String symbol) { return '$balance $symbol'; } + + @override + String get commonContinue => 'Lanjutkan'; } diff --git a/mobile-app/lib/shared/utils/print.dart b/mobile-app/lib/shared/utils/print.dart new file mode 100644 index 00000000..9738a366 --- /dev/null +++ b/mobile-app/lib/shared/utils/print.dart @@ -0,0 +1,7 @@ +import 'package:flutter/foundation.dart'; + +void quantusDebugPrint(String message) { + if (kDebugMode) { + debugPrint(message); + } +} diff --git a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart index c4a5fda8..c33ddeab 100644 --- a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart +++ b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart @@ -30,7 +30,7 @@ class _WalletReadyScreenV2State extends ConsumerState { return SettingsCautionScaffold( appBarTitle: l10n.createWalletAppBarTitle, data: data, - continueLabel: l10n.createWalletCautionContinue, + continueLabel: l10n.commonContinue, checkboxChecked: _acknowledged, onCheckboxChanged: () => setState(() => _acknowledged = !_acknowledged), onContinue: _continue, 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 d86e374c..636a1fc7 100644 --- a/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart +++ b/mobile-app/lib/v2/screens/pos/pos_qr_screen.dart @@ -12,6 +12,7 @@ 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/shared/utils/print.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'; @@ -60,7 +61,7 @@ class _PosQrScreenState extends ConsumerState { final expectedPlanck = formattingService.parseAmount(widget.amount); if (expectedPlanck == null) { - context.debugPrint('[PosQr] ERROR: failed to parse amount "${widget.amount}"'); + quantusDebugPrint('[PosQr] ERROR: failed to parse amount "${widget.amount}"'); if (mounted) setState(() => _watchError = l10n.posQrInvalidAmount); return; @@ -71,15 +72,15 @@ class _PosQrScreenState extends ConsumerState { _watchError = null; }); - context.debugPrint('[PosQr] watching address=${active.account.accountId} expected=$expectedPlanck planck'); + quantusDebugPrint('[PosQr] watching address=${active.account.accountId} expected=$expectedPlanck planck'); _txWatch.watch( address: active.account.accountId, onTransfer: (tx) { - context.debugPrint('[PosQr] onTransfer from=${tx.from} amount=${tx.amount} hash=${tx.txHash}'); + quantusDebugPrint('[PosQr] onTransfer from=${tx.from} amount=${tx.amount} hash=${tx.txHash}'); if (_isPaid) return; final received = BigInt.tryParse(tx.amount); if (received != expectedPlanck) { - context.debugPrint('[PosQr] amount mismatch (received=$received expected=$expectedPlanck), ignoring'); + quantusDebugPrint('[PosQr] amount mismatch (received=$received expected=$expectedPlanck), ignoring'); return; } @@ -106,7 +107,7 @@ class _PosQrScreenState extends ConsumerState { } }, onError: (e) { - context.debugPrint('[PosQr] watch error: $e'); + quantusDebugPrint('[PosQr] watch error: $e'); _txWatch.dispose(); _timeoutTimer?.cancel(); if (mounted) { diff --git a/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart b/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart index 9a8569fe..ab2b2bf3 100644 --- a/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart +++ b/mobile-app/lib/v2/screens/settings/recovery_phrase_confirmation_screen.dart @@ -40,7 +40,7 @@ class _RecoveryPhraseConfirmationScreenState extends ConsumerState setState(() => _acknowledged = !_acknowledged), onContinue: _onContinue, diff --git a/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart b/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart index d0d18b0d..9ce29eb4 100644 --- a/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart +++ b/mobile-app/lib/v2/screens/settings/reset_confirmation_screen.dart @@ -46,7 +46,7 @@ class _ResetConfirmationScreenState extends ConsumerState setState(() => _backedUpChecked = !_backedUpChecked), diff --git a/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart b/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart index 969127ed..85b614ba 100644 --- a/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart +++ b/mobile-app/lib/v2/screens/settings/settings_caution_scaffold.dart @@ -55,7 +55,7 @@ class SettingsCautionScaffold extends StatelessWidget { required this.onCheckboxChanged, required this.onContinue, required this.data, - this.continueLabel = '', + required this.continueLabel, this.betweenBulletsStyle = SettingsDividerStyle.list, this.continueButtonLoading = false, }); diff --git a/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart b/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart index e516d068..a7419aa8 100644 --- a/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_picker_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; +import 'package:resonance_network_wallet/shared/utils/print.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/settings/settings_divider.dart'; @@ -114,7 +115,7 @@ class _SettingsPickerScreenState extends State> { Navigator.pop(context); } } catch (e) { - debugPrint('error selecting locale: $e'); + quantusDebugPrint('[SettingsPickerScreen] error selecting item: $e'); if (context.mounted) { context.showErrorToaster(message: widget.errorMessageBuilder(e.toString())); } diff --git a/quantus_sdk/lib/src/extensions/context_extension.dart b/quantus_sdk/lib/src/extensions/context_extension.dart index f7add296..f6e0478c 100644 --- a/quantus_sdk/lib/src/extensions/context_extension.dart +++ b/quantus_sdk/lib/src/extensions/context_extension.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; extension SharePositionOriginExtension on BuildContext { @@ -7,11 +6,3 @@ extension SharePositionOriginExtension on BuildContext { return box == null ? null : box.localToGlobal(Offset.zero) & box.size; } } - -extension DebugPrintExtension on BuildContext { - void debugPrint(String message) { - if (kDebugMode) { - debugPrint(message); - } - } -} \ No newline at end of file From 2b7bd9a6df0e58f46f87e070f0e9d7f7179247f8 Mon Sep 17 00:00:00 2001 From: Beast Date: Fri, 22 May 2026 16:16:20 +0800 Subject: [PATCH 25/25] fix: race condition when logout --- .../lib/providers/currency_display_provider.dart | 3 ++- mobile-app/lib/providers/l10n_provider.dart | 3 ++- mobile-app/lib/services/logout_service.dart | 13 ++++++------- quantus_sdk/lib/src/services/settings_service.dart | 8 ++++++++ quantus_sdk/lib/src/services/substrate_service.dart | 2 +- .../lib/src/services/taskmaster_service.dart | 2 +- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/mobile-app/lib/providers/currency_display_provider.dart b/mobile-app/lib/providers/currency_display_provider.dart index 89ced883..7d1c57f1 100644 --- a/mobile-app/lib/providers/currency_display_provider.dart +++ b/mobile-app/lib/providers/currency_display_provider.dart @@ -155,7 +155,8 @@ class SelectedFiatCurrencyNotifier extends StateNotifier { state = currency; } - void reset() { + Future reset() async { + await _settings.clearSelectedFiatCurrency(); state = _defaultCurrency; } diff --git a/mobile-app/lib/providers/l10n_provider.dart b/mobile-app/lib/providers/l10n_provider.dart index 9ca29dd2..feb9e9ab 100644 --- a/mobile-app/lib/providers/l10n_provider.dart +++ b/mobile-app/lib/providers/l10n_provider.dart @@ -65,7 +65,8 @@ class SelectedAppLocaleNotifier extends StateNotifier { state = locale; } - void reset() { + Future reset() async { + await _settings.clearSelectedAppLocale(); state = AppLocale.fromCode(_defaultLocaleCode); } diff --git a/mobile-app/lib/services/logout_service.dart b/mobile-app/lib/services/logout_service.dart index cc4b9e9c..405c17dd 100644 --- a/mobile-app/lib/services/logout_service.dart +++ b/mobile-app/lib/services/logout_service.dart @@ -17,7 +17,7 @@ class LogoutService { final Ref _ref; LogoutService(this._ref); - Future _clearSettingsAndMemory() async { + Future logout(BuildContext context) async { if (_ref.read(remoteConfigProvider).enableRemoteNotifications) { _ref.read(firebaseMessagingServiceProvider).unregisterDevice(); } @@ -29,12 +29,11 @@ class LogoutService { _ref.read(accountsProvider.notifier).reset(); _ref.read(activeAccountProvider.notifier).reset(); _ref.read(accountAssociationsProvider.notifier).reset(); - _ref.read(selectedAppLocaleProvider.notifier).reset(); - _ref.read(selectedFiatCurrencyProvider.notifier).reset(); - } + await _ref.read(selectedAppLocaleProvider.notifier).reset(); + await _ref.read(selectedFiatCurrencyProvider.notifier).reset(); - void logout(BuildContext context) { - _clearSettingsAndMemory(); - Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const WelcomeScreenV2()), (r) => false); + if (context.mounted) { + Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const WelcomeScreenV2()), (r) => false); + } } } diff --git a/quantus_sdk/lib/src/services/settings_service.dart b/quantus_sdk/lib/src/services/settings_service.dart index d29e4ee3..54243bae 100644 --- a/quantus_sdk/lib/src/services/settings_service.dart +++ b/quantus_sdk/lib/src/services/settings_service.dart @@ -309,6 +309,10 @@ class SettingsService { await _prefs.setString(_selectedFiatCurrencyKey, currencyCode); } + Future clearSelectedFiatCurrency() async { + await _prefs.remove(_selectedFiatCurrencyKey); + } + /// Returns the persisted fiat currency code (e.g. "USD"), or null when no /// preference has been saved yet (caller should fall back to the default). String? getSelectedFiatCurrency() { @@ -320,6 +324,10 @@ class SettingsService { await _prefs.setString(_selectedAppLocaleKey, languageCode); } + Future clearSelectedAppLocale() async { + await _prefs.remove(_selectedAppLocaleKey); + } + /// Returns the persisted language code (e.g. "en", "id"), or null when no /// preference has been saved yet (caller should fall back to English). String? getSelectedAppLocale() { diff --git a/quantus_sdk/lib/src/services/substrate_service.dart b/quantus_sdk/lib/src/services/substrate_service.dart index 941e1273..cec7c175 100644 --- a/quantus_sdk/lib/src/services/substrate_service.dart +++ b/quantus_sdk/lib/src/services/substrate_service.dart @@ -374,7 +374,7 @@ class SubstrateService { Future logout() async { print('Log out!'); await _settingsService.clearAll(); - await TaskmasterService().logout(); + TaskmasterService().logout(); } Future generateMnemonic() async { diff --git a/quantus_sdk/lib/src/services/taskmaster_service.dart b/quantus_sdk/lib/src/services/taskmaster_service.dart index 16169bbe..f9ef454e 100644 --- a/quantus_sdk/lib/src/services/taskmaster_service.dart +++ b/quantus_sdk/lib/src/services/taskmaster_service.dart @@ -613,7 +613,7 @@ class TaskmasterService { return _authenticatedGet(uri, OptedInPosition.fromJson); } - Future logout() async { + void logout() { _clearToken(); }