diff --git a/mobile-app/lib/features/components/migration_dialog.dart b/mobile-app/lib/features/components/migration_dialog.dart deleted file mode 100644 index 508d2819..00000000 --- a/mobile-app/lib/features/components/migration_dialog.dart +++ /dev/null @@ -1,124 +0,0 @@ -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/quantus_button.dart'; -import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; -import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; - -class MigrationDialog extends ConsumerStatefulWidget { - final List migrationResults; - final Future Function() onMigrate; - final Future Function()? onTryLater; - - const MigrationDialog({super.key, required this.migrationResults, required this.onMigrate, this.onTryLater}); - - static Future show({ - required BuildContext context, - required List migrationResults, - required Future Function() onMigrate, - Future Function()? onTryLater, - }) { - return showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - isScrollControlled: true, - isDismissible: false, - enableDrag: false, - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width), - builder: (ctx) => - MigrationDialog(migrationResults: migrationResults, onMigrate: onMigrate, onTryLater: onTryLater), - ); - } - - @override - ConsumerState createState() => _MigrationDialogState(); -} - -class _MigrationDialogState extends ConsumerState { - bool _isMigrating = false; - String? _errorMessage; - - @override - Widget build(BuildContext context) { - final successCount = widget.migrationResults.whereType().length; - final failureCount = widget.migrationResults.whereType().length; - final l10n = ref.watch(l10nProvider); - final colors = context.colors; - final text = context.themeText; - - return Container( - padding: const EdgeInsets.fromLTRB(24, 40, 24, 40), - decoration: BoxDecoration( - color: colors.sheetBackground, - border: Border.all(color: const Color(0xFF3D3D3D)), - borderRadius: BorderRadius.circular(24), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(l10n.migrationDialogTitle, style: text.smallTitle?.copyWith(color: colors.textPrimary, fontSize: 20)), - const SizedBox(height: 24), - Text(l10n.migrationDialogBody, style: text.smallParagraph?.copyWith(color: colors.textSecondary)), - const SizedBox(height: 24), - Text( - l10n.migrationDialogAccountsToMigrate(successCount), - style: text.paragraph?.copyWith(fontWeight: FontWeight.w600, color: colors.accentGreen), - ), - if (failureCount > 0) ...[ - const SizedBox(height: 8), - Text( - l10n.migrationDialogAccountsCannotMigrate(failureCount), - style: text.smallParagraph?.copyWith(color: colors.accentOrange), - ), - ], - const SizedBox(height: 40), - if (_errorMessage != null) - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration(color: colors.error.useOpacity(0.15), borderRadius: BorderRadius.circular(8)), - child: Text(_errorMessage!, style: text.smallParagraph?.copyWith(color: colors.textError)), - ), - QuantusButton.simple( - label: _errorMessage != null ? l10n.migrationDialogRetry : l10n.migrationDialogMigrate, - isLoading: _isMigrating, - onTap: successCount == 0 - ? null - : () async { - setState(() => _isMigrating = true); - try { - await widget.onMigrate(); - // ignore: use_build_context_synchronously - if (mounted) Navigator.of(context).pop(); - } catch (e) { - if (mounted) { - setState(() => _errorMessage = ref.read(l10nProvider).migrationDialogUploadError); - } - } finally { - if (mounted) setState(() => _isMigrating = false); - } - }, - ), - // Show "Try later" when there's an error OR when there are no migratable accounts - if (_errorMessage != null || successCount == 0) ...[ - const SizedBox(height: 12), - QuantusButton.simple( - label: successCount == 0 && _errorMessage == null - ? l10n.migrationDialogSkip - : l10n.migrationDialogTryLater, - variant: ButtonVariant.transparent, - onTap: () async { - if (widget.onTryLater != null) await widget.onTryLater!(); - // ignore: use_build_context_synchronously - if (mounted) Navigator.of(context).pop(); - }, - ), - ], - ], - ), - ); - } -} diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 3c2c2b4b..083d7f17 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -12,61 +12,6 @@ "description": "Label for the button on the error dialog when the wallet is not found" }, - "migrationDialogTitle": "Migrate your accounts", - "@migrationDialogTitle": { - "description": "Title of the account migration dialog" - }, - "migrationDialogBody": "We'll record your old\u2011chain testnet rewards and actions to determine rewards on the new Quantus Testnet.\n\nBalances do not migrate.\n\nUse the new testnet faucet for funds.", - "@migrationDialogBody": { - "description": "Body text of the account migration dialog" - }, - "migrationDialogAccountsToMigrate": "{count, plural, =1{1 Account to migrate.} other{{count} Accounts to migrate.}}", - "@migrationDialogAccountsToMigrate": { - "description": "Number of accounts that will be migrated", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "migrationDialogAccountsCannotMigrate": "{count, plural, =1{1 account cannot be migrated (missing wallet data).} other{{count} accounts cannot be migrated (missing wallet data).}}", - "@migrationDialogAccountsCannotMigrate": { - "description": "Number of accounts that cannot be migrated", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "migrationDialogUploadError": "We couldn't upload migration data. Please retry or try later.", - "@migrationDialogUploadError": { - "description": "Error shown in the migration dialog when the upload fails" - }, - "migrationDialogMigrate": "Migrate Accounts", - "@migrationDialogMigrate": { - "description": "Label for the migrate button in the migration dialog" - }, - "migrationDialogRetry": "Retry", - "@migrationDialogRetry": { - "description": "Label for the retry button in the migration dialog" - }, - "migrationDialogTryLater": "Try later", - "@migrationDialogTryLater": { - "description": "Label for the try-later button in the migration dialog" - }, - "migrationDialogSkip": "Skip", - "@migrationDialogSkip": { - "description": "Label for the skip button in the migration dialog when no accounts can be migrated" - }, - "migrationPartialFailureToast": "{count, plural, =1{1 account could not be migrated. Migration will retry on next app launch.} other{{count} accounts could not be migrated. Migration will retry on next app launch.}}", - "@migrationPartialFailureToast": { - "description": "Toast shown when some accounts failed to migrate", - "placeholders": { - "count": { - "type": "int" - } - } - }, "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 f6eb1c6a..1137652e 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -2,39 +2,23 @@ "walletInitErrorTitle": "Wallet Bermasalah", "walletInitErrorMessage": "Gagal mencari secret phrase. Coba pulihkan wallet anda.", "walletInitErrorButtonLabel": "OK", - - "migrationDialogTitle": "Migrasikan akun Anda", - "migrationDialogBody": "Kami akan mencatat hadiah dan aktivitas testnet chain lama Anda untuk menentukan hadiah di Quantus Testnet yang baru.\n\nSaldo tidak ikut dimigrasikan.\n\nGunakan faucet testnet baru untuk mendapatkan dana.", - "migrationDialogAccountsToMigrate": "{count, plural, other{{count} Akun akan dimigrasikan.}}", - "migrationDialogAccountsCannotMigrate": "{count, plural, other{{count} akun tidak dapat dimigrasikan (data wallet hilang).}}", - "migrationDialogUploadError": "Kami tidak dapat mengunggah data migrasi. Silakan coba lagi atau coba nanti.", - "migrationDialogMigrate": "Migrasikan Akun", - "migrationDialogRetry": "Coba Lagi", - "migrationDialogTryLater": "Coba nanti", - "migrationDialogSkip": "Lewati", - "migrationPartialFailureToast": "{count, plural, other{{count} akun tidak dapat dimigrasikan. Migrasi akan diulang saat aplikasi dibuka berikutnya.}}", - "authUseDeviceBiometricsToUnlock": "Gunakan biometrik untuk mengakses wallet", "authAuthenticating": "Mengotentikasi...", "authUnlockWallet": "Buka Wallet", "authAuthorizationRequired": "Otorisasi \n Diperlukan", - "welcomeTagline": "Uang Terenkripsi Aman Kuantum", "welcomeCreateNewWallet": "Buat Wallet Baru", "welcomeImportWallet": "Impor 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", "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", "recoveryPhraseBodyTapToReveal": "Ketuk untuk menampilkan", "recoveryPhraseBodyTapToHide": "Ketuk untuk menyembunyikan", "recoveryPhraseBodyCopiedMessage": "Recovery phrase disalin ke clipboard", - "accountReadyAccountCreated": "Akun Dibuat", "accountReadyWalletCreated": "Wallet Dibuat", "accountReadyWalletImported": "Wallet Diimpor", @@ -43,13 +27,11 @@ "accountReadyMainAccountDescription": "Akun utama Anda. Cepat, terlihat di chain.", "accountReadyEncryptedAccountDescription": "Untuk transaksi privat. Tershield, lebih lambat.", "accountReadyGoToWallet": "Buka Wallet", - "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", - "homeError": "Gagal: {error}", "homeNoActiveAccount": "Tidak ada akun aktif", "homeCharge": "Tagih", @@ -59,14 +41,12 @@ "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 {tokenSymbol}.", - "accountsSheetTitle": "Akun", "accountsSheetFailedLoadAccounts": "Gagal memuat akun.", "accountsSheetFailedLoadActiveAccount": "Gagal memuat akun aktif.", @@ -81,7 +61,6 @@ "walletNameTitle": "Nama Wallet", "walletNameSubtitle": "Memberi nama wallet memudahkan Anda membedakan akun-akunnya. Setiap wallet memiliki akun terenkripsi sendiri.", "walletNameHint": "Masukkan nama untuk wallet Anda", - "addAccountMenuTitle": "Tambah Akun", "addAccountMenuCreateTitle": "Tambah Akun Transparan", "addAccountMenuCreateSubtitle": "Tambahkan akun publik lainnya", @@ -94,7 +73,6 @@ "addAccountMenuMultisigSubtitle": "Siapkan alamat bersama dengan beberapa penandatangan", "addAccountMenuDiscoverMultisigTitle": "Tambah Akun Multisig", "addAccountMenuDiscoverMultisigSubtitle": "Cari multisig di mana akun Anda adalah penandatangan", - "multisigTag": "MULTISIG", "multisigProposeTitle": "Ajukan", "multisigAddTitle": "Buat Multisig", @@ -198,7 +176,6 @@ "multisigAlreadyApproved": "Sudah Ditandatangani", "multisigSignerPickerTitle": "Pilih akun", "multisigSignerPickerBody": "Beberapa akun di perangkat ini dapat menandatangani. Pilih akun mana yang akan digunakan untuk menyetujui.", - "multisigCancelProposalButton": "Batalkan Proposal", "multisigProposalExpiresLabel": "KEDALUWARSA", "multisigProposalAtLabel": "PADA", @@ -248,19 +225,16 @@ "multisigApproveDoneRecorded": "Persetujuan dicatat", "multisigApproveDoneExecutedSubline": "Ambang tercapai — transfer dikirim.", "multisigApproveDoneRecordedSubline": "Menunggu co-signer lainnya.", - "createAccountAppBarTitle": "Nama Akun", "createAccountSubtitle": "Berikan nama yang mudah Anda kenali. Anda bisa mengubahnya kapan saja.", "createAccountButton": "Buat", "createAccountErrorCouldNotAdd": "Gagal menambahkan akun.", "createAccountEncryptedDefaultName": "Akun Terenkripsi", "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", @@ -281,29 +255,23 @@ "accountMenuDeleteWalletTitle": "Apakah Anda yakin?", "accountMenuDeleteWalletMessage": "Frasa pemulihan Dompet {number} akan dihapus permanen dari perangkat ini. Pastikan sudah dicadangkan — ini tidak dapat dibatalkan.", "accountMenuDeleteWalletConfirm": "Hapus Dompet", - "accountDetailsTitle": "Detail Alamat", - "invalidAddress": "Alamat tidak valid", - "sendTitle": "Kirim", "sendPayTitle": "Bayar", "sendEnterAddress": "Masukkan Alamat", - "sendSelectRecipientSendTo": "Kirim Ke", "sendSelectRecipientSearchHint": "Masukkan 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", - "sendReviewSending": "MENGIRIM", "sendReviewTo": "KE", "sendReviewAmount": "JUMLAH", @@ -314,27 +282,23 @@ "sendReviewAuthRequired": "Autentikasi diperlukan untuk mengirim", "sendReviewSubmitFailed": "Gagal mengirim transaksi", "sendRegularAccountRequired": "Beralih ke akun reguler untuk mengirim", - "sendTxSubmittedHeadlinePaid": "{amount} {symbol} dibayar", "sendTxSubmittedHeadlineSent": "{amount} {symbol} terkirim", "sendTxSubmittedOnItsWay": "Sedang dalam perjalanan", "sendTxSubmittedToLabel": "Ke", "sendTxSubmittedDone": "Selesai", - "keystoneSignTitle": "Pindai dengan Keystone Anda", "keystoneSignError": "Gagal menyiapkan transaksi. Silakan coba lagi.", "keystoneScanScanning": "{count} bingkai dipindai", "keystoneScanSubmitting": "Mengirim transaksi...", "keystoneScanError": "Tidak dapat membaca tanda tangan. Silakan coba lagi.", "keystoneScanExpired": "Transaksi kedaluwarsa sebelum sempat dikirim. Kembali dan pindai kode QR baru dengan perangkat Anda.", - "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", - "activityTitle": "Aktivitas", "activityError": "Gagal: {error}", "activityNoAccount": "Tidak ada akun", @@ -346,7 +310,6 @@ "activityFilterReceive": "Terima", "activityDateToday": "Hari Ini", "activityDateYesterday": "Kemarin", - "activityTxSending": "Mengirim", "activityTxReceiving": "Menerima", "activityTxPending": "Tertunda", @@ -366,7 +329,6 @@ "activityTxTimeHoursAgo": "{hours}j lalu", "activityTxTimeDaysAgo": "{days}h lalu", "activityTxTimeRemaining": "{days}h:{hours}j:{minutes}m", - "activityDetailTitleSending": "Mengirim", "activityDetailTitleScheduled": "Terjadwal", "activityDetailTitleReceiving": "Menerima", @@ -404,18 +366,15 @@ "activityDetailMultisigCreationFee": "BIAYA PALLET", "activityDetailMultisigDeposit": "DEPOSIT TERSIMPAN", "activityDetailMultisigFeePaidByCreator": "Dibayar oleh pembuat", - "receiveTitle": "Terima", "receiveTabQrCode": "Kode QR", "receiveTabAddress": "Alamat", "receiveCopy": "Salin", "receiveErrorLoadingAccount": "Gagal memuat data akun: {error}", "receiveCopiedMessage": "Alamat disalin ke clipboard", - "posAmountTitle": "Tagihan Baru", "posAmountCharge": "Tagih {amount}", "posAmountEnterAmount": "Masukkan Jumlah", - "posQrTitleScanToPay": "Pindai untuk Bayar", "posQrTitlePaymentReceived": "Pembayaran Diterima", "posQrError": "Gagal: {error}", @@ -431,7 +390,6 @@ "posQrNetworkError": "Jaringan Bermasalah", "posQrTryAgain": "Coba Lagi", "posQrPaidAt": "Pada {time}", - "settingsTitle": "Pengaturan", "settingsWalletTitle": "Dompet", "settingsWalletSubtitle": "Frasa Pemulihan, Reset Dompet", @@ -446,25 +404,20 @@ "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", "settingsRecoveryAlreadyBackedUp": "Saya sudah mencadangkan dompet saya", - "settingsResetTitle": "Reset Dompet", "settingsResetAuthReason": "Autentikasi untuk mereset dompet", "settingsResetFailed": "Gagal mereset dompet: {error}", @@ -474,7 +427,6 @@ "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", - "settingsPreferencesLanguage": "Bahasa", "settingsPreferencesLanguageSubtitle": "Bahasa tampilan aplikasi", "settingsPreferencesCurrency": "Mata Uang", @@ -483,17 +435,14 @@ "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", "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", "settingsMiningStatusMining": "Mining", @@ -515,18 +464,15 @@ "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", @@ -536,7 +482,6 @@ "settingsAboutWebsite": "Kunjungi Situs Web", "settingsAboutWebsiteSubtitle": "quantus.com", "settingsAboutVersion": "Versi {version} ({build})", - "swapTitle": "Tukar", "swapFrom": "Dari", "swapTo": "Ke", @@ -547,16 +492,13 @@ "swapGetQuote": "Dapatkan Penawaran", "swapRateLabel": "1 {tokenSymbol} = {amount} {symbol}", "swapRateZero": "1 {tokenSymbol} = 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!", @@ -572,10 +514,8 @@ "swapDemoOnlyBody": "Tidak ada swap sungguhan yang dilakukan.", "swapDepositSentFunds": "Saya sudah mengirim dana", "swapDepositDone": "Selesai", - "swapRefundPickerTitle": "Alamat Refund", "swapRefundPickerEmpty": "Tidak ada alamat refund terbaru", - "componentQrScannerTitle": "Pindai Kode QR", "componentQrScannerNoCode": "Tidak ada kode QR pada gambar", "componentShare": "Bagikan", @@ -583,14 +523,12 @@ "componentCheckphraseLabel": "CHECKPHRASE", "componentCheckphraseCopied": "Checkphrase disalin", "componentNameFieldHint": "Masukkan nama untuk akun Anda", - "commonLoading": "Memuat...", "commonCancel": "Batal", "commonCanceling": "Membatalkan...", "commonAmountBalance": "{balance} {symbol}", "commonContinue": "Lanjutkan", "commonDone": "Selesai", - "redeemToLabel": "Tukar Ke", "redeemAddressHint": "Tempel Alamat {symbol}", "redeemAmountCta": "Tukar {amount}", diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index d640387f..73c8fdb8 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -110,66 +110,6 @@ abstract class AppLocalizations { /// **'OK'** String get walletInitErrorButtonLabel; - /// Title of the account migration dialog - /// - /// In en, this message translates to: - /// **'Migrate your accounts'** - String get migrationDialogTitle; - - /// Body text of the account migration dialog - /// - /// In en, this message translates to: - /// **'We\'ll record your old‑chain testnet rewards and actions to determine rewards on the new Quantus Testnet.\n\nBalances do not migrate.\n\nUse the new testnet faucet for funds.'** - String get migrationDialogBody; - - /// Number of accounts that will be migrated - /// - /// In en, this message translates to: - /// **'{count, plural, =1{1 Account to migrate.} other{{count} Accounts to migrate.}}'** - String migrationDialogAccountsToMigrate(int count); - - /// Number of accounts that cannot be migrated - /// - /// In en, this message translates to: - /// **'{count, plural, =1{1 account cannot be migrated (missing wallet data).} other{{count} accounts cannot be migrated (missing wallet data).}}'** - String migrationDialogAccountsCannotMigrate(int count); - - /// Error shown in the migration dialog when the upload fails - /// - /// In en, this message translates to: - /// **'We couldn\'t upload migration data. Please retry or try later.'** - String get migrationDialogUploadError; - - /// Label for the migrate button in the migration dialog - /// - /// In en, this message translates to: - /// **'Migrate Accounts'** - String get migrationDialogMigrate; - - /// Label for the retry button in the migration dialog - /// - /// In en, this message translates to: - /// **'Retry'** - String get migrationDialogRetry; - - /// Label for the try-later button in the migration dialog - /// - /// In en, this message translates to: - /// **'Try later'** - String get migrationDialogTryLater; - - /// Label for the skip button in the migration dialog when no accounts can be migrated - /// - /// In en, this message translates to: - /// **'Skip'** - String get migrationDialogSkip; - - /// Toast shown when some accounts failed to migrate - /// - /// In en, this message translates to: - /// **'{count, plural, =1{1 account could not be migrated. Migration will retry on next app launch.} other{{count} accounts could not be migrated. Migration will retry on next app launch.}}'** - String migrationPartialFailureToast(int count); - /// Text for the text on the lock screen when using device biometrics to unlock /// /// 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 03ed86d8..01256885 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -17,61 +17,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get walletInitErrorButtonLabel => 'OK'; - @override - String get migrationDialogTitle => 'Migrate your accounts'; - - @override - String get migrationDialogBody => - 'We\'ll record your old‑chain testnet rewards and actions to determine rewards on the new Quantus Testnet.\n\nBalances do not migrate.\n\nUse the new testnet faucet for funds.'; - - @override - String migrationDialogAccountsToMigrate(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count Accounts to migrate.', - one: '1 Account to migrate.', - ); - return '$_temp0'; - } - - @override - String migrationDialogAccountsCannotMigrate(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count accounts cannot be migrated (missing wallet data).', - one: '1 account cannot be migrated (missing wallet data).', - ); - return '$_temp0'; - } - - @override - String get migrationDialogUploadError => 'We couldn\'t upload migration data. Please retry or try later.'; - - @override - String get migrationDialogMigrate => 'Migrate Accounts'; - - @override - String get migrationDialogRetry => 'Retry'; - - @override - String get migrationDialogTryLater => 'Try later'; - - @override - String get migrationDialogSkip => 'Skip'; - - @override - String migrationPartialFailureToast(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count accounts could not be migrated. Migration will retry on next app launch.', - one: '1 account could not be migrated. Migration will retry on next app launch.', - ); - return '$_temp0'; - } - @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 e24dd31b..5da9e59e 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -17,55 +17,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get walletInitErrorButtonLabel => 'OK'; - @override - String get migrationDialogTitle => 'Migrasikan akun Anda'; - - @override - String get migrationDialogBody => - 'Kami akan mencatat hadiah dan aktivitas testnet chain lama Anda untuk menentukan hadiah di Quantus Testnet yang baru.\n\nSaldo tidak ikut dimigrasikan.\n\nGunakan faucet testnet baru untuk mendapatkan dana.'; - - @override - String migrationDialogAccountsToMigrate(int count) { - String _temp0 = intl.Intl.pluralLogic(count, locale: localeName, other: '$count Akun akan dimigrasikan.'); - return '$_temp0'; - } - - @override - String migrationDialogAccountsCannotMigrate(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count akun tidak dapat dimigrasikan (data wallet hilang).', - ); - return '$_temp0'; - } - - @override - String get migrationDialogUploadError => - 'Kami tidak dapat mengunggah data migrasi. Silakan coba lagi atau coba nanti.'; - - @override - String get migrationDialogMigrate => 'Migrasikan Akun'; - - @override - String get migrationDialogRetry => 'Coba Lagi'; - - @override - String get migrationDialogTryLater => 'Coba nanti'; - - @override - String get migrationDialogSkip => 'Lewati'; - - @override - String migrationPartialFailureToast(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count akun tidak dapat dimigrasikan. Migrasi akan diulang saat aplikasi dibuka berikutnya.', - ); - return '$_temp0'; - } - @override String get authUseDeviceBiometricsToUnlock => 'Gunakan biometrik untuk mengakses wallet'; diff --git a/mobile-app/lib/wallet_initializer.dart b/mobile-app/lib/wallet_initializer.dart index 5922b2a0..97b80e89 100644 --- a/mobile-app/lib/wallet_initializer.dart +++ b/mobile-app/lib/wallet_initializer.dart @@ -1,20 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; -import 'package:resonance_network_wallet/features/components/migration_dialog.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/print.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'; import 'package:resonance_network_wallet/v2/screens/home/home_screen.dart'; import 'package:resonance_network_wallet/v2/screens/welcome/welcome_screen.dart'; import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; -import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/services/logout_service.dart'; import 'package:resonance_network_wallet/services/telemetry_service.dart'; -import 'package:resonance_network_wallet/shared/utils/env_utils.dart'; class WalletInitializer extends ConsumerStatefulWidget { const WalletInitializer({super.key}); @@ -26,19 +21,15 @@ class WalletInitializer extends ConsumerStatefulWidget { class WalletInitializerState extends ConsumerState { bool _loading = true; bool _walletExists = false; - bool _needsMigration = false; - List? _migrationResults; final SettingsService _settingsService = SettingsService(); - late final MigrationService _migrationService; @override void initState() { super.initState(); - _migrationService = MigrationService(_settingsService, HdWalletService()); - _checkWalletAndMigration(); + _checkWallet(); } - Future _checkWalletAndMigration() async { + Future _checkWallet() async { final hasWallet = await _settingsService.getHasWallet(); if (hasWallet) { @@ -50,61 +41,10 @@ class WalletInitializerState extends ConsumerState { } } - final needsMigration = _migrationService.needsMigration(); - - if (needsMigration) { - try { - final migrationResults = await _migrationService.getMigrationData(); - - for (final result in migrationResults) { - switch (result) { - case MigrationSuccess(:final oldAccount, :final newAccountId): - quantusPrint( - 'MIGRATION SUCCESS: \n' - ' walletIndex: ${oldAccount.walletIndex} \n' - ' old index: ${oldAccount.index} \n' - ' old name: ${oldAccount.name} \n' - ' old accountId: ${oldAccount.accountId} \n' - ' new accountId: $newAccountId', - ); - case MigrationFailure(:final oldAccount, :final reason): - quantusPrint( - 'MIGRATION FAILURE: \n' - ' walletIndex: ${oldAccount.walletIndex} \n' - ' old index: ${oldAccount.index} \n' - ' old name: ${oldAccount.name} \n' - ' reason: $reason', - ); - } - } - setState(() { - _needsMigration = true; - _migrationResults = migrationResults; - _loading = false; - }); - - // Show migration dialog - WidgetsBinding.instance.addPostFrameCallback((_) { - MigrationDialog.show( - context: context, - migrationResults: _migrationResults!, - onMigrate: _performMigration, - onTryLater: _tryLater, - ); - }); - } catch (e) { - // If migration data can't be loaded, continue without migration - setState(() { - _walletExists = hasWallet; - _loading = false; - }); - } - } else { - setState(() { - _walletExists = hasWallet; - _loading = false; - }); - } + setState(() { + _walletExists = hasWallet; + _loading = false; + }); } Future _showMnemonicLostDialog() async { @@ -131,126 +71,12 @@ class WalletInitializerState extends ConsumerState { if (mounted) ref.read(logoutServiceProvider).logout(context); } - void _reloadAccounts() { - invalidateAccountProviders(ref); - } - - Future _performMigration() async { - if (_migrationResults == null) return; - - try { - // Upload successful migrations to Supabase first. Encrypted (wormhole) - // accounts are excluded: their addresses are meant to be unlinkable to - // the user's identity. Accounts already saved by a previous partial - // migration are excluded so a retry doesn't upload duplicate rows. - final migratedIds = (await _settingsService.getAccounts()).map((a) => a.accountId).toSet(); - final uploadable = _migrationResults! - .whereType() - .where((s) => s.oldAccount.accountType != AccountType.encrypted && !migratedIds.contains(s.newAccountId)) - .toList(); - if (uploadable.isNotEmpty) { - await _uploadMigrationDataToSupabase(uploadable); - } - - // Then perform the actual migration - final failures = await _migrationService.performMigration(_migrationResults!); - - if (failures.isNotEmpty) { - quantusPrint('Migration completed with ${failures.length} failures'); - for (final failure in failures) { - TelemetryService().sendEvent( - 'migration_account_failure', - parameters: { - 'wallet_index': failure.oldAccount.walletIndex.toString(), - 'account_index': failure.oldAccount.index.toString(), - 'reason': failure.code.name, - }, - ); - } - } - - _reloadAccounts(); - setState(() { - _needsMigration = false; - _walletExists = true; - _loading = false; - }); - - if (failures.isNotEmpty && mounted) { - context.showErrorToaster(message: ref.read(l10nProvider).migrationPartialFailureToast(failures.length)); - } - } catch (e) { - quantusPrint('migration error: $e'); - rethrow; - } - } - - Future _tryLater() async { - // Persist the old accounts so we can retry upload later from settings - final oldAccounts = _settingsService.getOldAccounts(); - await _settingsService.setAccountsToMigrate(oldAccounts); - - // Proceed with local migration immediately - if (_migrationResults != null) { - try { - await _migrationService.performMigration(_migrationResults!); - } catch (e, stackTrace) { - quantusPrint('error in tryLater: $e'); - quantusPrint('stack trace: $stackTrace'); - TelemetryService().sendError('Error-Migration-TryLater', error: e); - rethrow; - } - } - - _reloadAccounts(); - - if (!mounted) return; - setState(() { - _needsMigration = false; - _walletExists = true; - _loading = false; - }); - } - - Future _uploadMigrationDataToSupabase(List migrationSuccesses) async { - quantusPrint('_uploadMigrationDataToSupabase'); - final supabase = EnvUtils.supabaseClient; - - try { - // Prepare the data for insertion - final dataToInsert = migrationSuccesses - .map( - (data) => { - 'old_account_id': data.oldAccount.accountId, - 'new_account_id': data.newAccountId, - 'public_key_hex': data.publicKeyHex, - }, - ) - .toList(); - - quantusPrint('uploading data to supabase: $dataToInsert'); - - // Insert all records at once - await supabase.from('account_id_mappings').insert(dataToInsert); - - quantusPrint('Successfully uploaded ${migrationSuccesses.length} migration records to Supabase'); - } catch (e) { - quantusPrint('Failed to upload migration data to Supabase: $e'); - // Re-throw the error so it gets caught by the caller - rethrow; - } - } - @override Widget build(BuildContext context) { if (_loading) { return const ScaffoldBase(mainContent: Center(child: CircularProgressIndicator())); } - if (_needsMigration) { - return Scaffold(backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: const SizedBox.shrink()); - } - if (_walletExists) { return const HomeScreen(); } else { diff --git a/mobile-app/test/unit/wallet_creation_service_test.mocks.dart b/mobile-app/test/unit/wallet_creation_service_test.mocks.dart index a3840dc3..40485cd6 100644 --- a/mobile-app/test/unit/wallet_creation_service_test.mocks.dart +++ b/mobile-app/test/unit/wallet_creation_service_test.mocks.dart @@ -60,33 +60,6 @@ class MockSettingsService extends _i1.Mock implements _i4.SettingsService { ) as _i5.Future); - @override - _i5.Future setAccountsToMigrate(List<_i2.Account>? accounts) => - (super.noSuchMethod( - Invocation.method(#setAccountsToMigrate, [accounts]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); - - @override - List<_i2.Account> getAccountsToMigrate() => - (super.noSuchMethod( - Invocation.method(#getAccountsToMigrate, []), - returnValue: <_i2.Account>[], - returnValueForMissingStub: <_i2.Account>[], - ) - as List<_i2.Account>); - - @override - _i5.Future clearAccountsToMigrate() => - (super.noSuchMethod( - Invocation.method(#clearAccountsToMigrate, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); - @override _i5.Future addAccount(_i2.Account? account) => (super.noSuchMethod( @@ -370,38 +343,6 @@ class MockSettingsService extends _i1.Mock implements _i4.SettingsService { ) as _i5.Future); - @override - bool hasOldAccounts() => - (super.noSuchMethod(Invocation.method(#hasOldAccounts, []), returnValue: false, returnValueForMissingStub: false) - as bool); - - @override - List<_i2.Account> getOldAccounts() => - (super.noSuchMethod( - Invocation.method(#getOldAccounts, []), - returnValue: <_i2.Account>[], - returnValueForMissingStub: <_i2.Account>[], - ) - as List<_i2.Account>); - - @override - _i5.Future clearOldAccounts() => - (super.noSuchMethod( - Invocation.method(#clearOldAccounts, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); - - @override - _i5.Future setOldAccountsData(String? jsonData) => - (super.noSuchMethod( - Invocation.method(#setOldAccountsData, [jsonData]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); - @override void resetForTest() => super.noSuchMethod(Invocation.method(#resetForTest, []), returnValueForMissingStub: null); diff --git a/quantus_sdk/lib/quantus_sdk.dart b/quantus_sdk/lib/quantus_sdk.dart index c11614bb..1054bb4f 100644 --- a/quantus_sdk/lib/quantus_sdk.dart +++ b/quantus_sdk/lib/quantus_sdk.dart @@ -58,7 +58,6 @@ export 'src/services/datetime_formatting_service.dart'; export 'src/services/hd_wallet_service.dart'; export 'src/services/high_security_service.dart'; export 'src/services/human_readable_checksum_service.dart'; -export 'src/services/migration_service.dart'; export 'src/services/network/redundant_endpoint.dart'; export 'src/services/locale_number_config.dart'; export 'src/services/number_formatting_service.dart'; diff --git a/quantus_sdk/lib/src/services/migration_service.dart b/quantus_sdk/lib/src/services/migration_service.dart deleted file mode 100644 index bad5dc5e..00000000 --- a/quantus_sdk/lib/src/services/migration_service.dart +++ /dev/null @@ -1,221 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:quantus_sdk/src/constants/app_constants.dart'; -import 'package:quantus_sdk/src/models/account.dart'; -import 'package:quantus_sdk/src/models/display_account.dart'; -import 'package:quantus_sdk/src/rust/api/crypto.dart' as crypto; -import 'package:quantus_sdk/src/services/hd_wallet_service.dart'; -import 'package:quantus_sdk/src/services/settings_service.dart'; -import 'package:quantus_sdk/src/utils/print.dart'; - -/// Result of attempting to migrate an account. -sealed class MigrationResult { - final Account oldAccount; - const MigrationResult(this.oldAccount); -} - -/// Successfully migrated account with new derived address. -class MigrationSuccess extends MigrationResult { - final String publicKeyHex; - final String newAccountId; - - const MigrationSuccess({required Account oldAccount, required this.publicKeyHex, required this.newAccountId}) - : super(oldAccount); -} - -/// Whitelisted failure category, safe to transmit in telemetry. -enum MigrationFailureReason { noMnemonic, derivationError } - -/// Account that cannot be migrated due to missing mnemonic or other error. -/// -/// [reason] is free-form detail for local logging only and may embed raw -/// exception text; telemetry must send [code] instead. -class MigrationFailure extends MigrationResult { - final MigrationFailureReason code; - final String reason; - - const MigrationFailure({required Account oldAccount, required this.code, required this.reason}) : super(oldAccount); -} - -class MigrationService { - final SettingsService _settingsService; - final HdWalletService _hdWalletService; - - MigrationService(this._settingsService, this._hdWalletService); - - /// Check if migration is needed (old accounts exist) - bool needsMigration() { - return _settingsService.hasOldAccounts(); - } - - /// Get migration data including old accounts with their public keys. - /// - /// Each result is either a [MigrationSuccess] with the derived address or a - /// [MigrationFailure] (e.g. missing mnemonic). Uses the correct mnemonic and - /// derivation path for each account's [Account.walletIndex] and - /// [Account.accountType]. - Future> getMigrationData() async { - final oldAccounts = _settingsService.getOldAccounts(); - final migrationResults = []; - - final mnemonicCache = {}; - - for (final rawAccount in oldAccounts) { - // Normalize the account type so every downstream check (derivation, - // upload filters, saved account type) agrees on what is a wormhole - // account. - final isWormhole = - rawAccount.accountType == AccountType.encrypted || rawAccount.index == AppConstants.encryptedAccountIndex; - final account = isWormhole ? rawAccount.copyWith(accountType: AccountType.encrypted) : rawAccount; - try { - final walletIndex = account.walletIndex; - if (!mnemonicCache.containsKey(walletIndex)) { - mnemonicCache[walletIndex] = await _settingsService.getMnemonic(walletIndex); - } - final mnemonic = mnemonicCache[walletIndex]; - - if (mnemonic == null) { - migrationResults.add( - MigrationFailure( - oldAccount: account, - code: MigrationFailureReason.noMnemonic, - reason: 'No mnemonic found for wallet $walletIndex', - ), - ); - continue; - } - - final String publicKeyHex; - final String newAccountId; - - if (isWormhole) { - final wormholeKeyPair = _hdWalletService.deriveWormholeKeyPair(mnemonic: mnemonic, index: 0); - publicKeyHex = wormholeKeyPair.addressHex.replaceFirst('0x', ''); - newAccountId = wormholeKeyPair.address; - } else { - final keypair = _hdWalletService.keyPairAtIndex(mnemonic, account.index); - publicKeyHex = _uint8ListToHex(keypair.publicKey); - newAccountId = crypto.toAccountId(obj: keypair); - } - - migrationResults.add( - MigrationSuccess(oldAccount: account, publicKeyHex: publicKeyHex, newAccountId: newAccountId), - ); - } catch (e) { - migrationResults.add( - MigrationFailure( - oldAccount: account, - code: MigrationFailureReason.derivationError, - reason: 'Derivation error: $e', - ), - ); - } - } - - return migrationResults; - } - - /// Perform the migration by creating new accounts and clearing old data. - /// - /// Only [MigrationSuccess] results are migrated. Old accounts are only - /// cleared when every account migrated successfully, preventing data loss. - /// - /// Returns the list of accounts that failed to migrate (if any). - Future> performMigration(List migrationResults) async { - final newAccounts = []; - final failures = []; - - for (final result in migrationResults) { - switch (result) { - case MigrationSuccess(:final oldAccount, :final newAccountId): - quantusPrint( - 'performMigration: \n' - ' walletIndex: ${oldAccount.walletIndex} \n' - ' old index: ${oldAccount.index} \n' - ' old name: ${oldAccount.name} \n' - ' old accountId: ${oldAccount.accountId} \n' - ' accountType: ${oldAccount.accountType} \n' - ' new accountId: $newAccountId', - ); - - newAccounts.add( - Account( - walletIndex: oldAccount.walletIndex, - index: oldAccount.index, - name: oldAccount.name, - accountId: newAccountId, - accountType: oldAccount.accountType, - ), - ); - - case MigrationFailure(:final oldAccount, :final reason): - quantusPrint( - 'performMigration SKIPPED: \n' - ' walletIndex: ${oldAccount.walletIndex} \n' - ' index: ${oldAccount.index} \n' - ' name: ${oldAccount.name} \n' - ' reason: $reason', - ); - failures.add(result); - } - } - - if (newAccounts.isNotEmpty) { - // Merge by accountId so a retried migration never wipes accounts - // created since the last attempt. - final existing = await _settingsService.getAccounts(); - final existingIds = existing.map((a) => a.accountId).toSet(); - await _settingsService.saveAccounts([ - ...existing, - ...newAccounts.where((a) => !existingIds.contains(a.accountId)), - ]); - if (existing.isEmpty) { - await _settingsService.setActiveAccount(RegularAccount(newAccounts.first)); - } - } - - // Only clear old accounts if all migrations succeeded, to prevent data loss. - if (failures.isEmpty) { - await _settingsService.clearOldAccounts(); - } else { - quantusPrint( - 'WARNING: ${failures.length} account(s) failed to migrate. ' - 'Old accounts NOT cleared to prevent data loss.', - ); - } - - return failures; - } - - String _uint8ListToHex(Uint8List bytes) { - return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); - } - - /// Debug method to test migration - Future createDebugOldAccounts() async { - final debugAccounts = [ - const Account( - walletIndex: 0, - index: -1, - name: 'Primary Account', - accountId: 'qznd1YWbgQrviV76psu5n8d24mHSuHtAc9JmJLB42gTELksvQ', - ), - const Account(walletIndex: 0, index: 0, name: 'Account 0', accountId: 'debug_id_0'), - const Account(walletIndex: 0, index: 1, name: 'Account 1', accountId: 'debug_id_1'), - // Test multi-wallet migration - const Account(walletIndex: 1, index: 0, name: 'Wallet 1 Account', accountId: 'debug_wallet1_id'), - // Test encrypted account migration - const Account( - walletIndex: 0, - index: AppConstants.encryptedAccountIndex, - name: 'Encrypted Account', - accountId: 'debug_encrypted_id', - accountType: AccountType.encrypted, - ), - ]; - - final jsonData = jsonEncode(debugAccounts.map((a) => a.toJson()).toList()); - await _settingsService.setOldAccountsData(jsonData); - } -} diff --git a/quantus_sdk/lib/src/services/settings_service.dart b/quantus_sdk/lib/src/services/settings_service.dart index 6d014ca6..35fd8fcc 100644 --- a/quantus_sdk/lib/src/services/settings_service.dart +++ b/quantus_sdk/lib/src/services/settings_service.dart @@ -4,7 +4,6 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:quantus_sdk/src/models/account.dart'; import 'package:quantus_sdk/src/models/display_account.dart'; import 'package:quantus_sdk/src/models/multisig_account.dart'; -import 'package:quantus_sdk/src/utils/print.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SettingsService { @@ -18,13 +17,8 @@ class SettingsService { // New keys for multi-account support static const String _accountsKey = 'accounts_v5'; static const String _multisigAccountsKey = 'multisig_accounts_v1'; - static const String _accountsToMigrateKey = 'accounts_to_migrate'; static const String _addressBookKey = 'address_book'; - static const String _oldAccountsKeyV4 = 'accounts_v4'; - static const String _oldAccountsKeyV3 = 'accounts_v3'; - static const String _oldAccountsKeyV2 = 'accounts_v2'; - static const String _oldAccountsKeyV1 = 'accounts'; static const String _activeAccountIndexKey = 'active_account_index'; static const String _activeAccountIdKey = 'active_account_id'; static const String _activeDisplayAccountKey = 'active_display_account'; @@ -78,27 +72,6 @@ class SettingsService { await _prefs.setString(_accountsKey, jsonEncode(jsonData)); } - // --- Accounts To Migrate (for deferred upload) --- - Future setAccountsToMigrate(List accounts) async { - final List> jsonData = accounts.map((a) => a.toJson()).toList(); - await _prefs.setString(_accountsToMigrateKey, jsonEncode(jsonData)); - } - - List getAccountsToMigrate() { - final jsonStr = _prefs.getString(_accountsToMigrateKey); - if (jsonStr == null) return []; - try { - final decoded = jsonDecode(jsonStr) as List; - return decoded.map((e) => Account.fromJson(e)).toList(); - } catch (_) { - return []; - } - } - - Future clearAccountsToMigrate() async { - await _prefs.remove(_accountsToMigrateKey); - } - Future addAccount(Account account) async { final accounts = await getAccounts(); // Check for duplicates by index or accountId before adding @@ -459,48 +432,6 @@ class SettingsService { await _prefs.setString(key, value); } - // --- Migration Methods --- - - /// Check if old accounts exist in legacy storage - bool hasOldAccounts() { - final oldAccounts = getOldAccounts(); - return oldAccounts.isNotEmpty; - } - - /// Get old accounts from legacy storage or v2 storage - List getOldAccounts() { - final oldAccountsJson = - _prefs.getString(_oldAccountsKeyV1) ?? - _prefs.getString(_oldAccountsKeyV2) ?? - _prefs.getString(_oldAccountsKeyV3) ?? - _prefs.getString(_oldAccountsKeyV4); - if (oldAccountsJson != null) { - try { - final decoded = jsonDecode(oldAccountsJson) as List; - return decoded.map((e) => Account.fromJson(e)).toList(); - } catch (e) { - return []; - } - } - return []; - } - - /// Remove old accounts from legacy storage after successful migration - Future clearOldAccounts() async { - await _prefs.remove(_oldAccountsKeyV1); - await _prefs.remove(_oldAccountsKeyV2); - await _prefs.remove(_oldAccountsKeyV3); - await _prefs.remove(_oldAccountsKeyV4); - } - - /// Set old accounts data (for debugging/testing) - Future setOldAccountsData(String jsonData) async { - quantusPrint('removing accounts data'); - await _prefs.remove(_accountsKey); - quantusPrint('setting old accounts data - reload app after this'); - await _prefs.setString(_oldAccountsKeyV4, jsonData); - } - // Test-only helper to reset initialization between tests void resetForTest() { assert(() { diff --git a/quantus_sdk/test/services/migration_derivation_test.dart b/quantus_sdk/test/services/migration_derivation_test.dart deleted file mode 100644 index 3305e1ea..00000000 --- a/quantus_sdk/test/services/migration_derivation_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -@Tags(['native']) -library; - -import 'dart:convert'; - -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:quantus_sdk/quantus_sdk.dart'; -import 'package:quantus_sdk/src/rust/api/crypto.dart' as crypto; -import 'package:quantus_sdk/src/rust/frb_generated.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -const _mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - setUpAll(() async { - await RustLib.init(); - }); - - late SettingsService settings; - late MigrationService service; - final hdWallet = HdWalletService(); - - Future seedOldAccounts(List accounts) async { - await settings.setOldAccountsData(jsonEncode(accounts.map((a) => a.toJson()).toList())); - } - - setUp(() async { - SharedPreferences.setMockInitialValues({}); - FlutterSecureStorage.setMockInitialValues({'mnemonic': _mnemonic}); - settings = SettingsService(); - await settings.initialize(); - service = MigrationService(settings, hdWallet); - }); - - group('MigrationService.getMigrationData', () { - test('derives transparent accounts from their wallet mnemonic and index', () async { - const old = Account(walletIndex: 0, index: 0, name: 'A', accountId: 'old_a'); - await seedOldAccounts([old]); - - final results = await service.getMigrationData(); - - final success = results.single as MigrationSuccess; - expect(success.newAccountId, crypto.toAccountId(obj: hdWallet.keyPairAtIndex(_mnemonic, 0))); - }); - - test('index-flagged wormhole accounts derive via the wormhole path and are typed encrypted', () async { - // Legacy data may carry the reserved index without an accountType. - const old = Account( - walletIndex: 0, - index: AppConstants.encryptedAccountIndex, - name: 'Wormhole', - accountId: 'old_wormhole', - ); - await seedOldAccounts([old]); - - final results = await service.getMigrationData(); - - final success = results.single as MigrationSuccess; - expect(success.newAccountId, hdWallet.deriveWormholeKeyPair(mnemonic: _mnemonic).address); - // Normalized so the Supabase upload and Senoti filters exclude it. - expect(success.oldAccount.accountType, AccountType.encrypted); - }); - - test('accounts of a wallet with no mnemonic become failures', () async { - const old = Account(walletIndex: 1, index: 0, name: 'B', accountId: 'old_b'); - await seedOldAccounts([old]); - - final results = await service.getMigrationData(); - - final failure = results.single as MigrationFailure; - expect(failure.reason, contains('wallet 1')); - }); - }); -} diff --git a/quantus_sdk/test/services/migration_service_test.dart b/quantus_sdk/test/services/migration_service_test.dart deleted file mode 100644 index 09c4e1fe..00000000 --- a/quantus_sdk/test/services/migration_service_test.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:quantus_sdk/quantus_sdk.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - late SettingsService settings; - late MigrationService service; - - const oldA = Account(walletIndex: 0, index: 0, name: 'A', accountId: 'old_a'); - const oldB = Account(walletIndex: 1, index: 0, name: 'B', accountId: 'old_b'); - - const successA = MigrationSuccess(oldAccount: oldA, publicKeyHex: 'hex_a', newAccountId: 'new_a'); - const failureB = MigrationFailure( - oldAccount: oldB, - code: MigrationFailureReason.noMnemonic, - reason: 'No mnemonic found for wallet 1', - ); - - Future seedOldAccounts(List accounts) async { - await settings.setOldAccountsData(jsonEncode(accounts.map((a) => a.toJson()).toList())); - } - - setUp(() async { - SharedPreferences.setMockInitialValues({}); - settings = SettingsService(); - await settings.initialize(); - service = MigrationService(settings, HdWalletService()); - }); - - group('MigrationService.performMigration', () { - test('full success saves accounts, sets active account and clears old accounts', () async { - await seedOldAccounts([oldA]); - - final failures = await service.performMigration([successA]); - - expect(failures, isEmpty); - expect((await settings.getAccounts()).map((a) => a.accountId), ['new_a']); - expect((await settings.getActiveRegularAccount())?.accountId, 'new_a'); - expect(settings.hasOldAccounts(), isFalse); - }); - - test('partial failure reports failures and keeps old accounts for retry', () async { - await seedOldAccounts([oldA, oldB]); - - final failures = await service.performMigration([successA, failureB]); - - expect(failures.map((f) => f.oldAccount.accountId), ['old_b']); - expect((await settings.getAccounts()).map((a) => a.accountId), ['new_a']); - expect(settings.hasOldAccounts(), isTrue); - }); - - test('retry merges by accountId and never wipes accounts created in between', () async { - await seedOldAccounts([oldA, oldB]); - await service.performMigration([successA, failureB]); - - // User creates an account between the failed attempt and the retry. - const created = Account(walletIndex: 0, index: 1, name: 'Created', accountId: 'created_id'); - await settings.addAccount(created); - await settings.setActiveAccount(const RegularAccount(created)); - - final failures = await service.performMigration([successA, failureB]); - - expect(failures, hasLength(1)); - final ids = (await settings.getAccounts()).map((a) => a.accountId).toList(); - expect(ids, containsAll(['new_a', 'created_id'])); - expect(ids.where((id) => id == 'new_a'), hasLength(1)); - expect((await settings.getActiveRegularAccount())?.accountId, 'created_id'); - }); - - test('all-failure migration saves nothing and keeps old accounts', () async { - await seedOldAccounts([oldB]); - - final failures = await service.performMigration([failureB]); - - expect(failures, hasLength(1)); - expect(await settings.getAccounts(), isEmpty); - expect(settings.hasOldAccounts(), isTrue); - }); - }); -}