diff --git a/analysis_options.yaml b/analysis_options.yaml index b991a218..11e7dd0f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -17,6 +17,7 @@ linter: prefer_const_constructors: true prefer_single_quotes: true unawaited_futures: true + discarded_futures: true avoid_dynamic_calls: true cast_nullable_to_non_nullable: true null_check_on_nullable_type_parameter: true diff --git a/assets/languages/strings_de.arb b/assets/languages/strings_de.arb index e3f21595..54b0cd9e 100644 --- a/assets/languages/strings_de.arb +++ b/assets/languages/strings_de.arb @@ -33,8 +33,8 @@ "buyPaymentConfirmFailedAktionariat": "Es gibt ein technisches Problem. Bitte überprüfen Sie Ihr E-Mail-Postfach, möglicherweise fehlt noch eine Bestätigung Ihrer Blockchain-Adresse. Andernfalls versuchen Sie es später erneut. Falls der Fehler weiterhin besteht, kontaktieren Sie unseren Support.", "buyPaymentInformation": "Zahlungsinformationen", "buyPaymentInformationDescription": "Bitte überweisen Sie den Kaufbetrag mit diesen Angaben über Ihre Bankanwendung. Der Verwendungszweck ist wichtig!", - "buyRealUnit": "RealUnit kaufen", "buyRealu": "RealUnit Token kaufen", + "buyRealUnit": "RealUnit kaufen", "cancel": "Abbrechen", "changeAddress": "Adresse ändern", "changeInReview": "Änderung in Prüfung", @@ -53,11 +53,11 @@ "connectBitboxContent": "Bitte verbinden Sie Ihre BitBox mit Ihrem Smartphone.", "connectBitboxContentIos": "Bitte verbinden Sie Ihre BitBox mit Ihrem Smartphone und aktivieren Sie zusätzlich Bluetooth.", "connectBitboxFailed": "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.", - "connectBitboxSignInHint": "Nach der Code-Überprüfung wird die BitBox um eine zusätzliche Bestätigung zur Anmeldung gebeten.", "connectBitboxSignatureCapturing": "Bitte bestätigen Sie die Anmeldeanfrage auf Ihrem BitBox-Gerät. Diese Signatur wird einmalig erfasst, damit künftige Käufe Ihre BitBox nicht erneut benötigen.", "connectBitboxSignatureCapturingTitle": "Anmeldung bestätigen", "connectBitboxSignatureFailed": "Ihre Anmeldesignatur konnte nicht erfasst werden. Sie können es erneut versuchen oder trotzdem fortfahren – Ihre BitBox wird dann möglicherweise für Ihren ersten Kauf erneut benötigt.", "connectBitboxSignatureFailedTitle": "Anmeldung nicht abgeschlossen", + "connectBitboxSignInHint": "Nach der Code-Überprüfung wird die BitBox um eine zusätzliche Bestätigung zur Anmeldung gebeten.", "connectBitboxTitle": "BitBox verbinden", "connected": "Verbunden", "connectedBitboxContent": "Bitte bestätigen Sie und folgen nun den letzten Anweisungen auf Ihrer BitBox.", @@ -197,8 +197,8 @@ "proofDocument": "Nachweis-Dokument", "purposeOfPayment": "Verwendungszweck", "qrCode": "QR-Code", - "realunitStockToken": "RealUnit Aktientoken", "realunitStockprice": "RealUnit Aktienkurs", + "realunitStockToken": "RealUnit Aktientoken", "realunitWallet": "RealUnit Wallet", "realunitWalletLogout": "Aus REALU Wallet abmelden", "realunitWalletLogoutCheck": "Ich habe meine Wiederherstellungsphrase gesichert.", @@ -246,18 +246,18 @@ "sellBitboxCheckingEth": "Wallet-Guthaben wird geprüft", "sellBitboxDepositDescription": "Bestätigen Sie auf der BitBox, um ZCHF an die DFX-Einzahlungsadresse zu überweisen.", "sellBitboxDepositFrom": "Sie senden", + "sellBitboxDepositing": "ZCHF wird gesendet. Bestätigen Sie auf der Bitbox", "sellBitboxDepositRetryDescription": "Der Tausch wurde abgeschlossen, aber die ZCHF-Einzahlung konnte nicht gesendet werden. Ihre Mittel sind sicher. Tippen Sie auf Wiederholen.", "sellBitboxDepositRetryTitle": "Einzahlung fehlgeschlagen", "sellBitboxDepositTitle": "ZCHF an DFX senden", "sellBitboxDepositTo": "DFX-Einzahlung", - "sellBitboxDepositing": "ZCHF wird gesendet. Bestätigen Sie auf der Bitbox", "sellBitboxEthReady": "Wallet bereit", "sellBitboxEthReadyDescription": "Ihr Wallet hat genug ETH, um mit dem Verkauf fortzufahren.", "sellBitboxSwapDescription": "Bestätigen Sie auf Ihrem BitBox, um REALU über den BrokerBot in ZCHF zu tauschen.", "sellBitboxSwapFrom": "Sie senden", + "sellBitboxSwapping": "Tausch on-chain. Bestätigen Sie auf der Bitbox.", "sellBitboxSwapTitle": "REALU → ZCHF tauschen", "sellBitboxSwapTo": "Sie erhalten", - "sellBitboxSwapping": "Tausch on-chain. Bestätigen Sie auf der Bitbox.", "sellBitboxWaitingForEth": "Gasgebühren werden angefordert", "sellBitboxWaitingForEthDescription": "Ein kleiner ETH-Betrag wird an Ihr Wallet gesendet, um die Transaktionsgebühren zu decken. Dies kann einige Minuten dauern.", "sellMinAmount": "Mindestbetrag: ${amount} ${currency}", @@ -282,10 +282,10 @@ "settingsWalletBackupSubtitle1": "Bitte notieren Sie Ihre 12 Wiederherstellungs-Wörter in der exakten Reihenfolge auf einem Blatt Papier und bewahren Sie sie absolut sicher auf.", "settingsWalletBackupSubtitle2": "Dies ist die einzige Möglichkeit, Ihre Wallet wiederherzustellen.", "showSeed": "Seed anzeigen", - "signMessage": "Signierte Nachricht", - "signMessageGet": "Signierte Nachricht abrufen", "signature": "Signatur", "signingCancelled": "Signatur abgebrochen — bitte BitBox erneut bestätigen", + "signMessage": "Signierte Nachricht", + "signMessageGet": "Signierte Nachricht abrufen", "skip": "Überspringen", "softwareTermsText": "Mit der Nutzung dieser App akzeptieren Sie die Nutzungsbedingungen dieser Software.", "softwareTermsTextHighlighted": "Nutzungsbedingungen", @@ -329,9 +329,9 @@ "transactionBuy": "Kauf", "transactionHistory": "Transaktionshistorie", "transactionPending": "In Bearbeitung", + "transactions": "Transaktionen", "transactionSell": "Verkauf", "transactionWaitingForPayment": "Warte auf Zahlung", - "transactions": "Transaktionen", "twoFa": "2-Faktor Authentifizierung", "twoFaCodeRequired": "Code ist erforderlich", "twoFaCodeTooShort": "Der Code sollte 6 Ziffern lang sein", @@ -356,4 +356,4 @@ "youPay": "Sie bezahlen", "youReceive": "Sie erhalten", "youSell": "Sie verkaufen" -} +} \ No newline at end of file diff --git a/assets/languages/strings_en.arb b/assets/languages/strings_en.arb index 56e4db7f..bd63560a 100644 --- a/assets/languages/strings_en.arb +++ b/assets/languages/strings_en.arb @@ -33,8 +33,8 @@ "buyPaymentConfirmFailedAktionariat": "There is a technical problem. Please check your email inbox — you may still need to confirm your blockchain address. Otherwise, please try again later. If the error persists, contact our support team.", "buyPaymentInformation": "Payment information", "buyPaymentInformationDescription": "Please transfer the purchase amount using your banking app with these details. The purpose of payment is important!", - "buyRealUnit": "Buy RealUnit", "buyRealu": "Buy RealUnit Token", + "buyRealUnit": "Buy RealUnit", "cancel": "Cancel", "changeAddress": "Change address", "changeInReview": "Change in review", @@ -53,11 +53,11 @@ "connectBitboxContent": "Please connect your BitBox with your Smartphone.", "connectBitboxContentIos": "Please connect your BitBox with your Smartphone and activate Bluetooth.", "connectBitboxFailed": "Something went wrong. Please try to connect again.", - "connectBitboxSignInHint": "After verifying the code, the BitBox will ask for one additional confirmation to sign you in.", "connectBitboxSignatureCapturing": "Please confirm the sign-in request on your BitBox device. This signature is captured once so future purchases won't need your BitBox again.", "connectBitboxSignatureCapturingTitle": "Confirm sign-in", "connectBitboxSignatureFailed": "We couldn't capture your sign-in signature. You can retry, or continue anyway – your BitBox may then be needed again for your first purchase.", "connectBitboxSignatureFailedTitle": "Sign-in not completed", + "connectBitboxSignInHint": "After verifying the code, the BitBox will ask for one additional confirmation to sign you in.", "connectBitboxTitle": "Connect BitBox", "connected": "Connected", "connectedBitboxContent": "Please confirm and follow the last steps on your BitBox.", @@ -197,8 +197,8 @@ "proofDocument": "Proof document", "purposeOfPayment": "Purpose of payment", "qrCode": "QR code", - "realunitStockToken": "RealUnit Stock Token", "realunitStockprice": "RealUnit Stockprice", + "realunitStockToken": "RealUnit Stock Token", "realunitWallet": "RealUnit Wallet", "realunitWalletLogout": "Log out of REALU Wallet", "realunitWalletLogoutCheck": "I have backed up my recovery phrase.", @@ -246,18 +246,18 @@ "sellBitboxCheckingEth": "Checking your wallet balance", "sellBitboxDepositDescription": "Confirm on your BitBox to transfer ZCHF to the DFX deposit address.", "sellBitboxDepositFrom": "You send", + "sellBitboxDepositing": "Sending ZCHF. Please confirm on the Bitbox.", "sellBitboxDepositRetryDescription": "The swap was completed but the ZCHF deposit could not be sent. Your funds are safe. Tap retry to try again.", "sellBitboxDepositRetryTitle": "Deposit failed", "sellBitboxDepositTitle": "Send ZCHF to DFX", "sellBitboxDepositTo": "DFX deposit", - "sellBitboxDepositing": "Sending ZCHF. Please confirm on the Bitbox.", "sellBitboxEthReady": "Wallet ready", "sellBitboxEthReadyDescription": "Your wallet has enough ETH to proceed with the sale.", "sellBitboxSwapDescription": "Confirm on your BitBox to swap REALU for ZCHF via the BrokerBot.", "sellBitboxSwapFrom": "You send", + "sellBitboxSwapping": "Swapping on-chain. Please confirm on the Bitbox.", "sellBitboxSwapTitle": "Swap REALU → ZCHF", "sellBitboxSwapTo": "You receive", - "sellBitboxSwapping": "Swapping on-chain. Please confirm on the Bitbox.", "sellBitboxWaitingForEth": "Requesting gas funds", "sellBitboxWaitingForEthDescription": "A small amount of ETH is being sent to your wallet to cover transaction fees. This may take a few minutes.", "sellMinAmount": "Minimum amount: ${amount} ${currency}", @@ -282,10 +282,10 @@ "settingsWalletBackupSubtitle1": "Please write down your 12 recovery words in the exact order on a piece of paper and keep them in a completely safe place.", "settingsWalletBackupSubtitle2": "This is the only way to recover your wallet.", "showSeed": "Show Seed", - "signMessage": "Sign Message", - "signMessageGet": "Get Sign Message", "signature": "Signature", "signingCancelled": "Signature cancelled — please confirm on the BitBox again", + "signMessage": "Sign Message", + "signMessageGet": "Get Sign Message", "skip": "Skip", "softwareTermsText": "By using this app, you accept the terms of use of this software.", "softwareTermsTextHighlighted": "terms of use", @@ -329,9 +329,9 @@ "transactionBuy": "Buy", "transactionHistory": "Transaction history", "transactionPending": "Processing", + "transactions": "Transactions", "transactionSell": "Sell", "transactionWaitingForPayment": "Waiting for payment", - "transactions": "Transactions", "twoFa": "Two-factor authentication", "twoFaCodeRequired": "Code is required", "twoFaCodeTooShort": "Code should be 6 digits", @@ -356,4 +356,4 @@ "youPay": "You pay", "youReceive": "You receive", "youSell": "You sell" -} +} \ No newline at end of file diff --git a/lib/packages/config/legal_documents_config.dart b/lib/packages/config/legal_documents_config.dart index 06176595..e18a7dfe 100644 --- a/lib/packages/config/legal_documents_config.dart +++ b/lib/packages/config/legal_documents_config.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -144,13 +146,15 @@ class WebDocumentConfig implements DocumentConfig { @override void onTap(BuildContext context) { if (openExternally) { - launchUrl(Uri.parse(url(context)), mode: LaunchMode.externalApplication); + unawaited(launchUrl(Uri.parse(url(context)), mode: LaunchMode.externalApplication)); } else { - context.pushNamed( - AppRoutes.webView, - extra: WebViewRouteParams( - title: title(context), - url: Uri.parse(url(context)), + unawaited( + context.pushNamed( + AppRoutes.webView, + extra: WebViewRouteParams( + title: title(context), + url: Uri.parse(url(context)), + ), ), ); } diff --git a/lib/packages/service/transaction_history_service.dart b/lib/packages/service/transaction_history_service.dart index 18fdafbc..b28dcba3 100644 --- a/lib/packages/service/transaction_history_service.dart +++ b/lib/packages/service/transaction_history_service.dart @@ -129,6 +129,7 @@ extension ToEpiAddress on String { String get asHexEip55 => EthereumAddress.fromHex(this).hexEip55; String get asShortTxId { + // realunit-lint:ignore fixed_index_address_substring — a tx id is always a 66-char hash; truncation for display. return '${substring(0, 10)}...${substring(length - 10)}'; } } diff --git a/lib/screens/buy/buy_page.dart b/lib/screens/buy/buy_page.dart index a6d6b9e9..36c357f8 100644 --- a/lib/screens/buy/buy_page.dart +++ b/lib/screens/buy/buy_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/generated/i18n.dart'; @@ -18,9 +20,11 @@ class BuyPage extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => BuyConverterCubit( - getIt(), - )..onFiatChanged('300'), + create: (_) { + final cubit = BuyConverterCubit(getIt()); + unawaited(cubit.onFiatChanged('300')); + return cubit; + }, ), BlocProvider( create: (_) => BuyPaymentInfoCubit( @@ -57,9 +61,11 @@ class _BuyViewState extends State { listener: (context, state) { _syncController(_amountController, state.fiatText); _syncController(_resultController, state.sharesText); - context.read().getPaymentInfo( - amount: _amountController.text, - currency: state.currency, + unawaited( + context.read().getPaymentInfo( + amount: _amountController.text, + currency: state.currency, + ), ); }, builder: (context, state) { diff --git a/lib/screens/buy/widgets/payment_converter.dart b/lib/screens/buy/widgets/payment_converter.dart index 4f9d0f9c..5323a5fa 100644 --- a/lib/screens/buy/widgets/payment_converter.dart +++ b/lib/screens/buy/widgets/payment_converter.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:flutter/material.dart'; @@ -39,32 +40,34 @@ class _PaymentConverterState extends State { @override void initState() { super.initState(); - getIt().getBuyable().then( - (currencies) { - if (mounted) setState(() => _buyable = currencies); - }, - onError: (Object error, StackTrace stack) { - developer.log( - 'PaymentConverter: failed to load buyable currencies — picker will ' - 'be disabled and the user is notified', - name: 'realunit_wallet.buy', - error: error, - stackTrace: stack, - level: 1000, // SEVERE - ); - if (!mounted) return; - setState(() => _loadFailed = true); - // Defer to post-frame so we have a Scaffold ancestor in scope. - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(S.of(context).settingsCurrencyLoadFailed), - backgroundColor: RealUnitColors.status.red600, - ), + unawaited( + getIt().getBuyable().then( + (currencies) { + if (mounted) setState(() => _buyable = currencies); + }, + onError: (Object error, StackTrace stack) { + developer.log( + 'PaymentConverter: failed to load buyable currencies — picker will ' + 'be disabled and the user is notified', + name: 'realunit_wallet.buy', + error: error, + stackTrace: stack, + level: 1000, // SEVERE ); - }); - }, + if (!mounted) return; + setState(() => _loadFailed = true); + // Defer to post-frame so we have a Scaffold ancestor in scope. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).settingsCurrencyLoadFailed), + backgroundColor: RealUnitColors.status.red600, + ), + ); + }); + }, + ), ); } @@ -131,7 +134,9 @@ class _PaymentConverterState extends State { initialValue: state.currency, onSelected: (currency) { if (currency == state.currency) return; - context.read().onCurrencyChanged(currency); + unawaited( + context.read().onCurrencyChanged(currency), + ); }, itemBuilder: (context) => _buyable.map((currency) { return PopupMenuItem( diff --git a/lib/screens/create_wallet/create_wallet_view.dart b/lib/screens/create_wallet/create_wallet_view.dart index 4d9f174e..90160de8 100644 --- a/lib/screens/create_wallet/create_wallet_view.dart +++ b/lib/screens/create_wallet/create_wallet_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -21,7 +23,7 @@ class CreateWalletView extends StatefulWidget { class _CreateWalletViewState extends State { @override void initState() { - NoScreenshot.instance.screenshotOff(); + unawaited(NoScreenshot.instance.screenshotOff()); super.initState(); } @@ -102,7 +104,7 @@ class _CreateWalletViewState extends State { @override void dispose() { - NoScreenshot.instance.screenshotOn(); + unawaited(NoScreenshot.instance.screenshotOn()); super.dispose(); } } diff --git a/lib/screens/dashboard/bloc/balance_cubit.dart b/lib/screens/dashboard/bloc/balance_cubit.dart index c89d39de..f5a099b3 100644 --- a/lib/screens/dashboard/bloc/balance_cubit.dart +++ b/lib/screens/dashboard/bloc/balance_cubit.dart @@ -28,7 +28,7 @@ class BalanceCubit extends Cubit { @override Future close() { - _subscription?.cancel(); + unawaited(_subscription?.cancel()); return super.close(); } } diff --git a/lib/screens/dashboard/bloc/dashboard_transaction_history_cubit.dart b/lib/screens/dashboard/bloc/dashboard_transaction_history_cubit.dart index b6b13bc6..ce229076 100644 --- a/lib/screens/dashboard/bloc/dashboard_transaction_history_cubit.dart +++ b/lib/screens/dashboard/bloc/dashboard_transaction_history_cubit.dart @@ -21,7 +21,7 @@ class DashboardTransactionHistoryCubit extends Cubit> { @override Future close() { - _subscription.cancel(); + unawaited(_subscription.cancel()); return super.close(); } } diff --git a/lib/screens/dashboard/bloc/pending_transactions_cubit.dart b/lib/screens/dashboard/bloc/pending_transactions_cubit.dart index 7db47c80..d3c8d45d 100644 --- a/lib/screens/dashboard/bloc/pending_transactions_cubit.dart +++ b/lib/screens/dashboard/bloc/pending_transactions_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer show log; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -6,7 +7,7 @@ import 'package:realunit_wallet/packages/service/transaction_history_service.dar class PendingTransactionsCubit extends Cubit> { PendingTransactionsCubit(this._transactionHistoryService) : super([]) { - _loadPendingTransactions(); + unawaited(_loadPendingTransactions()); } final TransactionHistoryService _transactionHistoryService; diff --git a/lib/screens/debug_auth/debug_auth_view.dart b/lib/screens/debug_auth/debug_auth_view.dart index 918cd668..4dd444b5 100644 --- a/lib/screens/debug_auth/debug_auth_view.dart +++ b/lib/screens/debug_auth/debug_auth_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -73,8 +75,10 @@ class _DebugAuthViewState extends State { ), GestureDetector( onTap: () { - Clipboard.setData( - ClipboardData(text: state.signMessage!), + unawaited( + Clipboard.setData( + ClipboardData(text: state.signMessage!), + ), ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart index 26cdc5a7..9b6ce9a4 100644 --- a/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart +++ b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart @@ -35,7 +35,7 @@ class ConnectBitboxCubit extends Cubit { _createWalletTimeout = createWalletTimeout, _pairingPinTimeout = pairingPinTimeout, super(BitboxNotConnected()) { - _startScanning(); + unawaited(_startScanning()); } final Duration _confirmPairingTimeout; diff --git a/lib/screens/home/bloc/home_bloc.dart b/lib/screens/home/bloc/home_bloc.dart index 0e97f15f..6257c7e3 100644 --- a/lib/screens/home/bloc/home_bloc.dart +++ b/lib/screens/home/bloc/home_bloc.dart @@ -129,8 +129,8 @@ class HomeBloc extends Bloc { void _updateWallet(AWallet wallet) { _appStore.wallet = wallet; - _balanceService.updateBalance(_appStore.primaryAddress); + unawaited(_balanceService.updateBalance(_appStore.primaryAddress)); _balanceService.startSync(_appStore.primaryAddress); - _transactionHistoryService.apiBasedSync(); + unawaited(_transactionHistoryService.apiBasedSync()); } } diff --git a/lib/screens/kyc/kyc_page_manager.dart b/lib/screens/kyc/kyc_page_manager.dart index 93694a3f..604634c1 100644 --- a/lib/screens/kyc/kyc_page_manager.dart +++ b/lib/screens/kyc/kyc_page_manager.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/generated/i18n.dart'; @@ -31,11 +33,15 @@ class KycPageManager extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => KycCubit( - getIt(), - getIt(), - getIt(), - )..checkKyc(context: kycContext), + create: (_) { + final cubit = KycCubit( + getIt(), + getIt(), + getIt(), + ); + unawaited(cubit.checkKyc(context: kycContext)); + return cubit; + }, child: const KycViewManager(), ); } @@ -67,7 +73,7 @@ class KycViewManager extends StatelessWidget { KycStep.legalDisclaimer => LegalDisclaimerPage( onCompleted: () { context.read().markLegalDisclaimerAccepted(); - context.read().checkKyc(); + unawaited(context.read().checkKyc()); }, ), KycStep.registration => KycRegistrationPage(initialUserData: realUnitUserData), @@ -79,8 +85,7 @@ class KycViewManager extends StatelessWidget { // Exhaustive over KycStep so a new value is a compile error here // (forced handling) rather than a silent blank Scaffold. dfxApproval // was the missing case that fell through to the old blank fallback. - KycStep.dfxApproval => - const KycPendingPage(pendingStep: KycStep.dfxApproval), + KycStep.dfxApproval => const KycPendingPage(pendingStep: KycStep.dfxApproval), }, KycState() => const Scaffold(), }, diff --git a/lib/screens/kyc/steps/2fa/kyc_2fa_page.dart b/lib/screens/kyc/steps/2fa/kyc_2fa_page.dart index cf317672..70666ff1 100644 --- a/lib/screens/kyc/steps/2fa/kyc_2fa_page.dart +++ b/lib/screens/kyc/steps/2fa/kyc_2fa_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/generated/i18n.dart'; @@ -48,7 +50,7 @@ class _Kyc2FaViewState extends State { @override void initState() { - context.read().requestCode(); + unawaited(context.read().requestCode()); super.initState(); } @@ -61,7 +63,7 @@ class _Kyc2FaViewState extends State { BlocListener( listener: (context, state) { if (state is Kyc2FaVerifySuccess) { - context.read().checkKyc(); + unawaited(context.read().checkKyc()); } if (state is Kyc2FaVerifyFailure) { ScaffoldMessenger.of(context).showSnackBar( @@ -136,7 +138,9 @@ class _Kyc2FaViewState extends State { state: state is Kyc2FaVerifyLoading ? .loading : .idle, onPressed: () { if (_formKey.currentState?.validate() ?? false) { - context.read().verifyCode(codeCtrl.text); + unawaited( + context.read().verifyCode(codeCtrl.text), + ); } }, label: S.of(context).next, diff --git a/lib/screens/kyc/steps/email/kyc_email_page.dart b/lib/screens/kyc/steps/email/kyc_email_page.dart index 11f399fb..7ea9d3de 100644 --- a/lib/screens/kyc/steps/email/kyc_email_page.dart +++ b/lib/screens/kyc/steps/email/kyc_email_page.dart @@ -126,8 +126,10 @@ class _KycEmailFormState extends State { onPressed: () { FocusManager.instance.primaryFocus?.unfocus(); if (_formKey.currentState?.validate() ?? false) { - context.read().registerEmail( - _emailCtrl.text.trim(), + unawaited( + context.read().registerEmail( + _emailCtrl.text.trim(), + ), ); } }, diff --git a/lib/screens/kyc/steps/financial_data/kyc_financial_data_page.dart b/lib/screens/kyc/steps/financial_data/kyc_financial_data_page.dart index 647b89da..40c0c5c9 100644 --- a/lib/screens/kyc/steps/financial_data/kyc_financial_data_page.dart +++ b/lib/screens/kyc/steps/financial_data/kyc_financial_data_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart'; @@ -18,13 +20,16 @@ class KycFinancialDataPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => - KycFinancialDataCubit( - getIt(), - )..loadQuestions( + create: (_) { + final cubit = KycFinancialDataCubit(getIt()); + unawaited( + cubit.loadQuestions( url, language: context.read().state.language, ), + ); + return cubit; + }, child: const KycFinancialDataView(), ); } @@ -38,7 +43,7 @@ class KycFinancialDataView extends StatelessWidget { return BlocListener( listener: (context, state) { if (state is KycFinancialDataSubmitSuccess) { - context.read().checkKyc(); + unawaited(context.read().checkKyc()); } if (state is KycFinancialDataFailure) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/screens/kyc/steps/ident/kyc_ident_page.dart b/lib/screens/kyc/steps/ident/kyc_ident_page.dart index 0339f9bb..47983032 100644 --- a/lib/screens/kyc/steps/ident/kyc_ident_page.dart +++ b/lib/screens/kyc/steps/ident/kyc_ident_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/generated/i18n.dart'; @@ -41,7 +43,7 @@ class KycIdentView extends StatelessWidget { child: BlocListener( listener: (context, state) { if (state is KycIdentSuccess) { - context.read().checkKyc(); + unawaited(context.read().checkKyc()); } if (state is KycIdentFailure) { if (state.status == FailureStatus.finallyRejected) { @@ -121,9 +123,11 @@ class KycIdentView extends StatelessWidget { return AppFilledButton( state: state is KycIdentLoading ? .loading : .idle, onPressed: () { - context.read().startIdent( - accessToken, - localeCode: context.read().state.language.code, + unawaited( + context.read().startIdent( + accessToken, + localeCode: context.read().state.language.code, + ), ); }, label: S.of(context).next, diff --git a/lib/screens/kyc/steps/link_wallet/kyc_link_wallet_page.dart b/lib/screens/kyc/steps/link_wallet/kyc_link_wallet_page.dart index 79f82e72..89e14e98 100644 --- a/lib/screens/kyc/steps/link_wallet/kyc_link_wallet_page.dart +++ b/lib/screens/kyc/steps/link_wallet/kyc_link_wallet_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -52,7 +54,7 @@ class KycLinkWalletView extends StatelessWidget { // Aktionariat share register, so `getRegistrationInfo` will return // `AlreadyRegistered` and `_runCheckKyc` will dispatch the next // KYC step. - context.read().checkKyc(); + unawaited(context.read().checkKyc()); } if (state is KycLinkWalletFailure) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/screens/kyc/steps/nationality/kyc_nationality_page.dart b/lib/screens/kyc/steps/nationality/kyc_nationality_page.dart index bdb7c632..54509957 100644 --- a/lib/screens/kyc/steps/nationality/kyc_nationality_page.dart +++ b/lib/screens/kyc/steps/nationality/kyc_nationality_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/generated/i18n.dart'; @@ -46,7 +48,7 @@ class _KycNationalityViewState extends State { body: BlocListener( listener: (context, state) { if (state is KycNationalitySuccess) { - context.read().checkKyc(); + unawaited(context.read().checkKyc()); } if (state is KycNationalityFailure) { ScaffoldMessenger.of(context).showSnackBar( @@ -81,9 +83,11 @@ class _KycNationalityViewState extends State { state: state is KycNationalityLoading ? .loading : .idle, onPressed: () { if (_formKey.currentState?.validate() ?? false) { - context.read().registerNationality( - url: widget.url, - nationality: nationalityCtrl.value!, + unawaited( + context.read().registerNationality( + url: widget.url, + nationality: nationalityCtrl.value!, + ), ); } }, diff --git a/lib/screens/kyc/steps/registration/kyc_registration_page.dart b/lib/screens/kyc/steps/registration/kyc_registration_page.dart index e4ee5bff..38357443 100644 --- a/lib/screens/kyc/steps/registration/kyc_registration_page.dart +++ b/lib/screens/kyc/steps/registration/kyc_registration_page.dart @@ -85,10 +85,12 @@ class _KycRegistrationViewState extends State { void initState() { super.initState(); _stepSubscription = context.read().stream.listen((state) { - _pageController.animateToPage( - state.index, - duration: const Duration(milliseconds: 350), - curve: Curves.easeOut, + unawaited( + _pageController.animateToPage( + state.index, + duration: const Duration(milliseconds: 350), + curve: Curves.easeOut, + ), ); }); @@ -277,12 +279,15 @@ class _KycRegistrationViewState extends State { addressPostalCode: postalCodeCtrl.text.trim(), addressCity: cityCtrl.text.trim(), addressCountry: countryCtrl.value!, + // realunit-lint:ignore hardcoded_swiss_tax_residence — this registration step is the + // Swiss-resident path; the literal is intentional here. Flagged for product review of + // whether a non-Swiss path must set this from user input. Baselined, not silently fixed. swissTaxResidence: true, ); @override void dispose() { - _stepSubscription?.cancel(); + unawaited(_stepSubscription?.cancel()); _pageController.dispose(); typeCtrl.dispose(); firstnameCtrl.dispose(); diff --git a/lib/screens/legal/subpages/legal_document_page.dart b/lib/screens/legal/subpages/legal_document_page.dart index 4994b730..a471ed28 100644 --- a/lib/screens/legal/subpages/legal_document_page.dart +++ b/lib/screens/legal/subpages/legal_document_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:flutter/material.dart'; @@ -52,7 +53,7 @@ class _LegalDocumentPageState extends State { if (widget.initialMarkdownContent != null) { _markdownContent = widget.initialMarkdownContent; } else { - _loadMarkdown(); + unawaited(_loadMarkdown()); } } @@ -81,7 +82,7 @@ class _LegalDocumentPageState extends State { _loadFailed = false; _markdownContent = null; }); - _loadMarkdown(); + unawaited(_loadMarkdown()); } String? get _pdfUrl { @@ -108,11 +109,13 @@ class _LegalDocumentPageState extends State { ), onTapLink: (text, href, title) { if (href == null || href.startsWith('mailto:') || href.contains('@')) return; - context.pushNamed( - AppRoutes.webView, - extra: WebViewRouteParams( - title: text, - url: Uri.parse(href), + unawaited( + context.pushNamed( + AppRoutes.webView, + extra: WebViewRouteParams( + title: text, + url: Uri.parse(href), + ), ), ); }, diff --git a/lib/screens/pin/bloc/setup_pin/setup_pin_cubit.dart b/lib/screens/pin/bloc/setup_pin/setup_pin_cubit.dart index 11e77ac5..3a822a72 100644 --- a/lib/screens/pin/bloc/setup_pin/setup_pin_cubit.dart +++ b/lib/screens/pin/bloc/setup_pin/setup_pin_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/packages/service/biometric_service.dart'; @@ -52,7 +54,7 @@ class SetupPinCubit extends Cubit { ); break; case SetupPinMode.confirm: - _confirmPin(pin); + unawaited(_confirmPin(pin)); break; } } diff --git a/lib/screens/pin/bloc/verify_pin/verify_pin_cubit.dart b/lib/screens/pin/bloc/verify_pin/verify_pin_cubit.dart index 8a43a36b..c02d0ee8 100644 --- a/lib/screens/pin/bloc/verify_pin/verify_pin_cubit.dart +++ b/lib/screens/pin/bloc/verify_pin/verify_pin_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/packages/service/biometric_service.dart'; @@ -23,7 +25,7 @@ class VerifyPinCubit extends Cubit { if (state is VerifyPinTemporarilyLocked || state is VerifyPinLocked) return; if (state.pin.length == pinLength) return; emit(state.copyWith(pin: '${state.pin}$digit')); - if (state.pin.length == pinLength) checkPin(); + if (state.pin.length == pinLength) unawaited(checkPin()); } void deleteDigit() { diff --git a/lib/screens/pin/verify_pin_page.dart b/lib/screens/pin/verify_pin_page.dart index f0af94a9..3584a876 100644 --- a/lib/screens/pin/verify_pin_page.dart +++ b/lib/screens/pin/verify_pin_page.dart @@ -83,7 +83,7 @@ class _VerifyPinViewState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().checkBiometricAvailability(); + unawaited(context.read().checkBiometricAvailability()); }); } diff --git a/lib/screens/receive/widgets/qr_address_widget.dart b/lib/screens/receive/widgets/qr_address_widget.dart index 677ba900..19700468 100644 --- a/lib/screens/receive/widgets/qr_address_widget.dart +++ b/lib/screens/receive/widgets/qr_address_widget.dart @@ -38,16 +38,24 @@ class QRAddressWidget extends StatelessWidget { Text.rich( TextSpan( children: [ + // The fixed 0/6/21/36 slices assume `subtitle` is a full + // 42-char EVM address — true at the only call site, but a + // guarded slice is the better shape and is tracked as a + // follow-up. Baselined below so the pattern guard blocks + // NEW occurrences (this is the audit's RangeError site). TextSpan( + // realunit-lint:ignore fixed_index_address_substring — baselined audit site, see note above. text: subtitle.substring(0, 6), style: const TextStyle(fontWeight: .bold), ), const TextSpan(text: ' '), TextSpan( + // realunit-lint:ignore fixed_index_address_substring — baselined audit site, see note above. text: subtitle.substring(6, 21), ), const TextSpan(text: '\n'), TextSpan( + // realunit-lint:ignore fixed_index_address_substring — baselined audit site, see note above. text: subtitle.substring(21, 36), ), const TextSpan(text: ' '), diff --git a/lib/screens/sell/cubits/sell_balance/sell_balance_cubit.dart b/lib/screens/sell/cubits/sell_balance/sell_balance_cubit.dart index dc80bd20..c4eee03a 100644 --- a/lib/screens/sell/cubits/sell_balance/sell_balance_cubit.dart +++ b/lib/screens/sell/cubits/sell_balance/sell_balance_cubit.dart @@ -27,7 +27,7 @@ class SellBalanceCubit extends Cubit { @override Future close() { - _subscription?.cancel(); + unawaited(_subscription?.cancel()); return super.close(); } } diff --git a/lib/screens/sell/cubits/sell_bank_accounts/sell_bank_accounts_cubit.dart b/lib/screens/sell/cubits/sell_bank_accounts/sell_bank_accounts_cubit.dart index 71ac82b1..5698a1c8 100644 --- a/lib/screens/sell/cubits/sell_bank_accounts/sell_bank_accounts_cubit.dart +++ b/lib/screens/sell/cubits/sell_bank_accounts/sell_bank_accounts_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:equatable/equatable.dart'; @@ -14,7 +15,7 @@ class SellBankAccountsCubit extends Cubit { DfxBankAccountService bankAccountService, ) : _dfxBankAccountService = bankAccountService, super(const SellBankAccountsInitial()) { - _loadBankAccounts(); + unawaited(_loadBankAccounts()); } Future add({required String iban, String? label}) async { diff --git a/lib/screens/sell/cubits/sell_converter/sell_converter_cubit.dart b/lib/screens/sell/cubits/sell_converter/sell_converter_cubit.dart index 93660b47..d92ad072 100644 --- a/lib/screens/sell/cubits/sell_converter/sell_converter_cubit.dart +++ b/lib/screens/sell/cubits/sell_converter/sell_converter_cubit.dart @@ -87,6 +87,10 @@ class SellConverterCubit extends Cubit { // the seq guard. emit(state.copyWith(loading: true, currency: currency)); try { + // realunit-lint:ignore cross_flow_brokerbot_endpoint — current behaviour is pinned by + // sell_converter_cubit_test.dart ('onCurrencyChanged calls getBuyPrice (not getSellPrice)'). + // Baselined and flagged for product review of whether the sell flow should price via + // getSellPrice on a currency change; not changed here to keep this PR behaviour-neutral. final result = await _brokerbotService.getBuyPrice(state.sharesText, currency); if (isClosed || mySeq != _seq) return; emit( diff --git a/lib/screens/sell/sell_page.dart b/lib/screens/sell/sell_page.dart index cf27a5de..bf4b0761 100644 --- a/lib/screens/sell/sell_page.dart +++ b/lib/screens/sell/sell_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/generated/i18n.dart'; @@ -28,9 +30,11 @@ class SellPage extends StatelessWidget { ), ), BlocProvider( - create: (context) => SellConverterCubit( - getIt(), - )..onSharesChanged('100'), + create: (context) { + final cubit = SellConverterCubit(getIt()); + unawaited(cubit.onSharesChanged('100')); + return cubit; + }, ), BlocProvider( create: (context) => SellPaymentInfoCubit( diff --git a/lib/screens/sell/widgets/sell_add_bank_account_sheet.dart b/lib/screens/sell/widgets/sell_add_bank_account_sheet.dart index 9d1afccb..7685b020 100644 --- a/lib/screens/sell/widgets/sell_add_bank_account_sheet.dart +++ b/lib/screens/sell/widgets/sell_add_bank_account_sheet.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -183,11 +185,13 @@ class _SellAddBankAccountSheetState extends State { fullWidth: false, onPressed: () { if (_formKey.currentState?.validate() ?? false) { - context.read().add( - iban: _ibanController.text, - label: _nameController.text.isNotEmpty - ? _nameController.text - : null, + unawaited( + context.read().add( + iban: _ibanController.text, + label: _nameController.text.isNotEmpty + ? _nameController.text + : null, + ), ); } }, diff --git a/lib/screens/sell/widgets/sell_converter.dart b/lib/screens/sell/widgets/sell_converter.dart index 8fed74c4..30109a63 100644 --- a/lib/screens/sell/widgets/sell_converter.dart +++ b/lib/screens/sell/widgets/sell_converter.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:flutter/material.dart'; @@ -41,31 +42,33 @@ class _SellConverterState extends State { @override void initState() { super.initState(); - getIt().getSellable().then( - (currencies) { - if (mounted) setState(() => _sellable = currencies); - }, - onError: (Object error, StackTrace stack) { - developer.log( - 'SellConverter: failed to load sellable currencies — picker will ' - 'be disabled and the user is notified', - name: 'realunit_wallet.sell', - error: error, - stackTrace: stack, - level: 1000, // SEVERE - ); - if (!mounted) return; - setState(() => _loadFailed = true); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(S.of(context).settingsCurrencyLoadFailed), - backgroundColor: RealUnitColors.status.red600, - ), + unawaited( + getIt().getSellable().then( + (currencies) { + if (mounted) setState(() => _sellable = currencies); + }, + onError: (Object error, StackTrace stack) { + developer.log( + 'SellConverter: failed to load sellable currencies — picker will ' + 'be disabled and the user is notified', + name: 'realunit_wallet.sell', + error: error, + stackTrace: stack, + level: 1000, // SEVERE ); - }); - }, + if (!mounted) return; + setState(() => _loadFailed = true); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).settingsCurrencyLoadFailed), + backgroundColor: RealUnitColors.status.red600, + ), + ); + }); + }, + ), ); } @@ -127,7 +130,9 @@ class _SellConverterState extends State { onTap: () { final maxStr = state.balance.toString(); _amountController.text = maxStr; - context.read().onSharesChanged(maxStr); + unawaited( + context.read().onSharesChanged(maxStr), + ); }, ); }, @@ -247,7 +252,9 @@ class _SellConverterState extends State { initialValue: state.currency, onSelected: (currency) { if (currency == state.currency) return; - context.read().onCurrencyChanged(currency); + unawaited( + context.read().onCurrencyChanged(currency), + ); }, itemBuilder: (context) => _sellable.map((currency) { return PopupMenuItem( diff --git a/lib/screens/sell_bitbox/widgets/sell_bitbox_deposit_step.dart b/lib/screens/sell_bitbox/widgets/sell_bitbox_deposit_step.dart index c13decdf..7822460e 100644 --- a/lib/screens/sell_bitbox/widgets/sell_bitbox_deposit_step.dart +++ b/lib/screens/sell_bitbox/widgets/sell_bitbox_deposit_step.dart @@ -133,6 +133,7 @@ class SellBitboxDepositStep extends StatelessWidget { String _truncateAddress(String address) { if (address.length <= 12) return address; + // realunit-lint:ignore fixed_index_address_substring — guarded by the length check above (>12). return '${address.substring(0, 6)}…${address.substring(address.length - 4)}'; } } diff --git a/lib/screens/settings_contact/settings_contact_page.dart b/lib/screens/settings_contact/settings_contact_page.dart index 04d20b40..c4751154 100644 --- a/lib/screens/settings_contact/settings_contact_page.dart +++ b/lib/screens/settings_contact/settings_contact_page.dart @@ -20,7 +20,11 @@ class SettingsContactPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => SettingsContactCubit(getIt())..init(), + create: (_) { + final cubit = SettingsContactCubit(getIt()); + unawaited(cubit.init()); + return cubit; + }, child: const SettingsContactView(), ); } diff --git a/lib/screens/settings_seed/bloc/settings_seed_cubit.dart b/lib/screens/settings_seed/bloc/settings_seed_cubit.dart index e7a80430..f4acc232 100644 --- a/lib/screens/settings_seed/bloc/settings_seed_cubit.dart +++ b/lib/screens/settings_seed/bloc/settings_seed_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/packages/service/app_store.dart'; @@ -16,8 +18,8 @@ class SettingsSeedCubit extends Cubit { // briefly be empty, which trips MnemonicReadOnlyField's `length == 12` // assert and crashes the screen on open. SettingsSeedCubit(this._appStore, this._walletService) - : super(SettingsSeedState(_initialSeed(_appStore))) { - _loadSeed(); + : super(SettingsSeedState(_initialSeed(_appStore))) { + unawaited(_loadSeed()); } static String _initialSeed(AppStore store) { diff --git a/lib/screens/settings_seed/settings_seed_view.dart b/lib/screens/settings_seed/settings_seed_view.dart index 25f5fef0..9a7fad45 100644 --- a/lib/screens/settings_seed/settings_seed_view.dart +++ b/lib/screens/settings_seed/settings_seed_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -19,7 +21,7 @@ class SettingsSeedView extends StatefulWidget { class _SettingsSeedViewState extends State { @override void initState() { - NoScreenshot.instance.screenshotOff(); + unawaited(NoScreenshot.instance.screenshotOff()); super.initState(); } @@ -114,7 +116,7 @@ class _SettingsSeedViewState extends State { @override void dispose() { - NoScreenshot.instance.screenshotOn(); + unawaited(NoScreenshot.instance.screenshotOn()); super.dispose(); } } diff --git a/lib/screens/settings_user_data/cubit/settings_user_data_cubit.dart b/lib/screens/settings_user_data/cubit/settings_user_data_cubit.dart index 0fc09b62..381b025d 100644 --- a/lib/screens/settings_user_data/cubit/settings_user_data_cubit.dart +++ b/lib/screens/settings_user_data/cubit/settings_user_data_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:equatable/equatable.dart'; @@ -34,7 +35,7 @@ class SettingsUserDataCubit extends Cubit { _countryService = countryService, _kycService = kycService, super(const SettingsUserDataInitial()) { - getUserData(); + unawaited(getUserData()); } Future getUserData() async { diff --git a/lib/screens/settings_user_data/subpages/edit_address/cubit/settings_edit_address_cubit.dart b/lib/screens/settings_user_data/subpages/edit_address/cubit/settings_edit_address_cubit.dart index fc6c1a5e..0785180f 100644 --- a/lib/screens/settings_user_data/subpages/edit_address/cubit/settings_edit_address_cubit.dart +++ b/lib/screens/settings_user_data/subpages/edit_address/cubit/settings_edit_address_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart'; @@ -11,7 +13,7 @@ class SettingsEditAddressCubit extends Cubit { SettingsEditAddressCubit({required DfxKycService kycService}) : _kycService = kycService, super(const SettingsEditAddressInitial()) { - _loadEdit(); + unawaited(_loadEdit()); } Future _loadEdit() async { diff --git a/lib/screens/settings_user_data/subpages/edit_name/cubit/settings_edit_name_cubit.dart b/lib/screens/settings_user_data/subpages/edit_name/cubit/settings_edit_name_cubit.dart index 12076192..2d549f13 100644 --- a/lib/screens/settings_user_data/subpages/edit_name/cubit/settings_edit_name_cubit.dart +++ b/lib/screens/settings_user_data/subpages/edit_name/cubit/settings_edit_name_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart'; @@ -11,7 +13,7 @@ class SettingsEditNameCubit extends Cubit { SettingsEditNameCubit({required DfxKycService kycService}) : _kycService = kycService, super(const SettingsEditNameInitial()) { - _loadEdit(); + unawaited(_loadEdit()); } Future _loadEdit() async { diff --git a/lib/screens/settings_user_data/subpages/edit_phone_number/settings_edit_phone_number_page.dart b/lib/screens/settings_user_data/subpages/edit_phone_number/settings_edit_phone_number_page.dart index aaa2c99c..121a0e33 100644 --- a/lib/screens/settings_user_data/subpages/edit_phone_number/settings_edit_phone_number_page.dart +++ b/lib/screens/settings_user_data/subpages/edit_phone_number/settings_edit_phone_number_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -108,7 +110,7 @@ class _SettingsEditPhoneNumberViewState extends State().editPhoneNumber(phone); + unawaited(context.read().editPhoneNumber(phone)); } } } diff --git a/lib/screens/support/cubits/support_chat/support_chat_cubit.dart b/lib/screens/support/cubits/support_chat/support_chat_cubit.dart index 28e1d434..137645a0 100644 --- a/lib/screens/support/cubits/support_chat/support_chat_cubit.dart +++ b/lib/screens/support/cubits/support_chat/support_chat_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -13,7 +14,7 @@ class SupportChatCubit extends Cubit { : _supportService = supportService, _ticketUid = ticketUid, super(const SupportChatInitial()) { - loadTicket(); + unawaited(loadTicket()); } Future loadTicket() async { diff --git a/lib/screens/support/cubits/support_tickets/support_tickets_cubit.dart b/lib/screens/support/cubits/support_tickets/support_tickets_cubit.dart index b7156824..60349799 100644 --- a/lib/screens/support/cubits/support_tickets/support_tickets_cubit.dart +++ b/lib/screens/support/cubits/support_tickets/support_tickets_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -13,7 +14,7 @@ class SupportTicketsCubit extends Cubit { super( const SupportTicketsInitial(), ) { - loadTickets(); + unawaited(loadTickets()); } Future loadTickets() async { diff --git a/lib/screens/support/subpages/support_chat_page.dart b/lib/screens/support/subpages/support_chat_page.dart index 2269cbf9..02b2b57f 100644 --- a/lib/screens/support/subpages/support_chat_page.dart +++ b/lib/screens/support/subpages/support_chat_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -77,7 +79,7 @@ class _SupportChatViewState extends State { onSend: () { final message = _messageController.text; if (message.trim().isNotEmpty) { - context.read().sendMessage(message); + unawaited(context.read().sendMessage(message)); _messageController.clear(); } }, @@ -101,10 +103,12 @@ class _SupportChatViewState extends State { void _scrollToBottom() { if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, + unawaited( + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ), ); } } diff --git a/lib/screens/support/subpages/support_email_capture_page.dart b/lib/screens/support/subpages/support_email_capture_page.dart index 21f91858..c6540fe4 100644 --- a/lib/screens/support/subpages/support_email_capture_page.dart +++ b/lib/screens/support/subpages/support_email_capture_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/generated/i18n.dart'; @@ -111,14 +113,14 @@ class _SupportEmailCaptureFormState extends State { BlocBuilder( builder: (context, state) { return AppFilledButton( - state: state is SupportEmailCaptureLoading - ? .loading - : .idle, + state: state is SupportEmailCaptureLoading ? .loading : .idle, onPressed: () { FocusManager.instance.primaryFocus?.unfocus(); if (_formKey.currentState?.validate() ?? false) { - context.read().submit( - _emailCtrl.text.trim(), + unawaited( + context.read().submit( + _emailCtrl.text.trim(), + ), ); } }, diff --git a/lib/screens/transaction_history/cubits/filter/transaction_history_filter_cubit.dart b/lib/screens/transaction_history/cubits/filter/transaction_history_filter_cubit.dart index fbe743f1..6541a9b1 100644 --- a/lib/screens/transaction_history/cubits/filter/transaction_history_filter_cubit.dart +++ b/lib/screens/transaction_history/cubits/filter/transaction_history_filter_cubit.dart @@ -15,9 +15,11 @@ class TransactionHistoryFilterCubit extends Cubit required String walletAddress, int? limit, }) : super(TransactionHistoryFilterState()) { - _subscription = _repository.watchTransactionsOfAssets([asset], walletAddress).listen( - _onTransactionsUpdated, - ); + _subscription = _repository + .watchTransactionsOfAssets([asset], walletAddress) + .listen( + _onTransactionsUpdated, + ); } final TransactionRepository _repository; @@ -65,7 +67,7 @@ class TransactionHistoryFilterCubit extends Cubit @override Future close() { - _subscription?.cancel(); + unawaited(_subscription?.cancel()); return super.close(); } } diff --git a/lib/screens/transaction_history/widgets/transaction_history_row.dart b/lib/screens/transaction_history/widgets/transaction_history_row.dart index 0051bbdf..5f973609 100644 --- a/lib/screens/transaction_history/widgets/transaction_history_row.dart +++ b/lib/screens/transaction_history/widgets/transaction_history_row.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; @@ -146,9 +148,11 @@ class TransactionHistoryRowView extends StatelessWidget { ) : GestureDetector( onTap: () { - context.read().generateReceipt( - transaction.txId, - currency: context.read().state.currency, + unawaited( + context.read().generateReceipt( + transaction.txId, + currency: context.read().state.currency, + ), ); }, child: const Icon( diff --git a/lib/setup/lifecycle_initializer.dart b/lib/setup/lifecycle_initializer.dart index 1c0f9a0a..a75c436b 100644 --- a/lib/setup/lifecycle_initializer.dart +++ b/lib/setup/lifecycle_initializer.dart @@ -46,7 +46,7 @@ class _LifecycleInitializerState extends State { void _onResumed() { getIt().onAppResumed(); - getIt().updateBalance(getIt().primaryAddress); + unawaited(getIt().updateBalance(getIt().primaryAddress)); } void _onInactive() {} diff --git a/pubspec.lock b/pubspec.lock index bf626bd7..999f3f25 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -18,7 +18,7 @@ packages: source: hosted version: "0.14.0" analyzer: - dependency: "direct overridden" + dependency: "direct dev" description: name: analyzer sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b diff --git a/pubspec.yaml b/pubspec.yaml index 9877d964..8bbbd3f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,6 +86,9 @@ dev_dependencies: sdk: flutter alchemist: ^0.14.0 + # Direct dev dependency so tool/lints/pattern_guard.dart may import it (the + # version is pinned by the analyzer override below). + analyzer: ^10.0.0 bloc_test: ^10.0.0 build_runner: ^2.13.0 drift_dev: ^2.32.1 diff --git a/test/analysis_options.yaml b/test/analysis_options.yaml new file mode 100644 index 00000000..177d5d09 --- /dev/null +++ b/test/analysis_options.yaml @@ -0,0 +1,17 @@ +# Inherits the repo-root analyzer config (strict casts/inference/raw-types + +# null/async lints + the hardened gate from PR #663) and relaxes ONLY the +# `discarded_futures` lint for test code. +# +# Rationale: `discarded_futures` flags Future-returning calls in non-`async` +# functions. In production (`lib/**`) every such site is resolved with an +# explicit `unawaited(...)`. In tests, fire-and-forget is idiomatic (pumping +# a widget, kicking a cubit method inside `setUp`/`blocTest` act blocks, +# `tester.runAsync` callbacks) and wrapping each one adds noise without +# guarding any production race. `unawaited_futures` (un-awaited futures inside +# `async` functions) stays active here, so genuinely awaitable calls in async +# test bodies are still caught. +include: ../analysis_options.yaml + +linter: + rules: + discarded_futures: false diff --git a/test/tool/pattern_guard_test.dart b/test/tool/pattern_guard_test.dart new file mode 100644 index 00000000..9cdfdfa2 --- /dev/null +++ b/test/tool/pattern_guard_test.dart @@ -0,0 +1,99 @@ +// Unit test for the high-pattern guard (tool/lints/pattern_guard.dart). +// Verifies each rule fires on a known-bad snippet, stays silent on a +// known-good one, and that `// realunit-lint:ignore ` suppresses a hit. +@TestOn('vm') +library; + +import 'package:flutter_test/flutter_test.dart'; + +import '../../tool/lints/pattern_guard.dart'; + +List _rules(String path, String src) => + scanDartSource(path, src).map((f) => f.rule).toList(); + +void main() { + group('hardcoded_swiss_tax_residence', () { + test('fires on a boolean literal', () { + final hits = _rules('lib/x.dart', ''' +void f() { + register(swissTaxResidence: true); +} +'''); + expect(hits, contains('hardcoded_swiss_tax_residence')); + }); + + test('silent when passed a value', () { + final hits = _rules('lib/x.dart', ''' +void f(bool resident) { + register(swissTaxResidence: resident); +} +'''); + expect(hits, isNot(contains('hardcoded_swiss_tax_residence'))); + }); + }); + + group('fixed_index_address_substring', () { + test('fires on two constant indices with end >= 6', () { + final hits = _rules('lib/x.dart', 'void f(String a) => a.substring(0, 6);'); + expect(hits, contains('fixed_index_address_substring')); + }); + + test('silent on a trivial peek (end < 6)', () { + final hits = _rules('lib/x.dart', 'void f(String a) => a.substring(0, 1);'); + expect(hits, isNot(contains('fixed_index_address_substring'))); + }); + + test('silent on a single dynamic index', () { + final hits = _rules('lib/x.dart', 'void f(String a) => a.substring(2);'); + expect(hits, isNot(contains('fixed_index_address_substring'))); + }); + }); + + group('cross_flow_brokerbot_endpoint', () { + test('fires when a sell file calls a buy endpoint', () { + final hits = _rules( + 'lib/screens/sell/cubits/sell_converter_cubit.dart', + "void f(s) => s.getBuyPrice('1', c);", + ); + expect(hits, contains('cross_flow_brokerbot_endpoint')); + }); + + test('fires when a buy file calls a sell endpoint', () { + final hits = _rules( + 'lib/screens/buy/cubits/buy_converter_cubit.dart', + "void f(s) => s.getSellPrice('1', c);", + ); + expect(hits, contains('cross_flow_brokerbot_endpoint')); + }); + + test('silent when a sell file calls a sell endpoint', () { + final hits = _rules( + 'lib/screens/sell/cubits/sell_converter_cubit.dart', + "void f(s) => s.getSellPrice('1', c);", + ); + expect(hits, isNot(contains('cross_flow_brokerbot_endpoint'))); + }); + }); + + group('suppression', () { + test('// realunit-lint:ignore on the line above silences the hit', () { + final hits = _rules('lib/x.dart', ''' +void f() { + // realunit-lint:ignore hardcoded_swiss_tax_residence — test + register(swissTaxResidence: true); +} +'''); + expect(hits, isEmpty); + }); + + test('ignore for a different rule does not silence', () { + final hits = _rules('lib/x.dart', ''' +void f() { + // realunit-lint:ignore fixed_index_address_substring — wrong rule + register(swissTaxResidence: true); +} +'''); + expect(hits, contains('hardcoded_swiss_tax_residence')); + }); + }); +} diff --git a/tool/lints/pattern_guard.dart b/tool/lints/pattern_guard.dart new file mode 100644 index 00000000..b72f3224 --- /dev/null +++ b/tool/lints/pattern_guard.dart @@ -0,0 +1,175 @@ +// High-pattern guard — a lightweight static check for the three recurring +// HIGH-severity defect shapes the Big Brother audit (#657) surfaced and that +// PR #663 named as `custom_lint` follow-ups. `custom_lint` itself cannot be +// added today: its current release pins `analyzer: ^8`, while this repo +// overrides `analyzer: ^10` for the strict-inference gate, so the two cannot +// co-resolve. This guard reaches the same goal with the `analyzer` package the +// repo already depends on — purely syntactic (`parseFile`, no element model), +// so it stays fast and version-tolerant. +// +// Run: dart run tool/lints/pattern_guard.dart +// CI: the `High-Pattern Guard` job in .github/workflows/pull-request.yaml +// fails the build on any non-allowlisted hit. +// +// Suppressing a site (use sparingly, always with a reason on the same line or +// the line directly above): +// // realunit-lint:ignore +// +// Rule ids: +// hardcoded_swiss_tax_residence a `swissTaxResidence:` argument passed a +// `true`/`false` literal instead of a value +// derived from the user's actual residence. +// fixed_index_address_substring `.substring(, )` with two +// constant indices — assumes a fixed string +// length and throws RangeError on a shorter +// one (the audit's qr_address_widget crash). +// cross_flow_brokerbot_endpoint a sell-flow file calling a buy-price/- +// shares brokerbot method (or vice versa). + +import 'dart:io'; + +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/source/line_info.dart'; + +class Finding { + Finding(this.rule, this.path, this.line, this.message); + final String rule; + final String path; + final int line; + final String message; +} + +const _buyMethods = {'getBuyPrice', 'getBuyShares'}; +const _sellMethods = {'getSellPrice', 'getSellShares'}; + +/// Scans a single Dart source for the three high-pattern violations, honouring +/// `// realunit-lint:ignore ` markers. `path` drives the buy/sell flow +/// heuristic (rule C). Exposed for the unit test in +/// `test/tool/pattern_guard_test.dart`. +List scanDartSource(String path, String content) { + final result = parseString(content: content, path: path, throwIfDiagnostics: false); + final visitor = _PatternVisitor(path, result.unit.lineInfo, content.split('\n')); + result.unit.accept(visitor); + return visitor.findings; +} + +void main() { + final root = Directory('lib'); + if (!root.existsSync()) { + stderr.writeln('pattern_guard: run from the repo root (no lib/ here)'); + exit(2); + } + + final findings = []; + final files = root + .listSync(recursive: true) + .whereType() + .where((f) => f.path.endsWith('.dart')) + // Generated code is tool output, not developer code. + .where((f) => !f.path.endsWith('.g.dart')) + .where((f) => !f.path.startsWith('lib/generated/')) + .toList() + ..sort((a, b) => a.path.compareTo(b.path)); + + for (final file in files) { + findings.addAll(scanDartSource(file.path, file.readAsStringSync())); + } + + if (findings.isEmpty) { + stdout.writeln('pattern_guard: OK — no high-pattern violations in lib/.'); + return; + } + + findings.sort((a, b) { + final p = a.path.compareTo(b.path); + return p != 0 ? p : a.line.compareTo(b.line); + }); + for (final f in findings) { + stdout.writeln('${f.path}:${f.line} • ${f.rule} • ${f.message}'); + } + stderr.writeln('\npattern_guard: ${findings.length} violation(s). ' + 'Fix them, or add `// realunit-lint:ignore `.'); + exit(1); +} + +class _PatternVisitor extends RecursiveAstVisitor { + _PatternVisitor(this.path, this.lineInfo, this.lines); + + final String path; + final LineInfo lineInfo; + final List lines; + final List findings = []; + + bool get _isSellFile => path.contains('/sell/') || path.contains('/sell_'); + bool get _isBuyFile => + (path.contains('/buy/') || path.contains('/buy_')) && !_isSellFile; + + int _lineOf(int offset) => lineInfo.getLocation(offset).lineNumber; + + // A hit is suppressed by a `// realunit-lint:ignore ` + // marker on the hit line itself or anywhere in the contiguous block of + // comment lines directly above it (multi-line reasons are fine). + bool _suppressed(int line, String rule) { + bool has(String s) => + s.contains('realunit-lint:ignore') && + (s.contains(rule) || s.contains('realunit-lint:ignore-all')); + if (line >= 1 && line <= lines.length && has(lines[line - 1])) return true; + var i = line - 1; // index of the line above the hit (0-based: line-2 + 1) + while (i >= 1 && lines[i - 1].trimLeft().startsWith('//')) { + if (has(lines[i - 1])) return true; + i--; + } + return false; + } + + void _report(String rule, int offset, String message) { + final line = _lineOf(offset); + if (_suppressed(line, rule)) return; + findings.add(Finding(rule, path, line, message)); + } + + @override + void visitNamedExpression(NamedExpression node) { + if (node.name.label.name == 'swissTaxResidence' && + node.expression is BooleanLiteral) { + _report('hardcoded_swiss_tax_residence', node.offset, + 'swissTaxResidence passed a boolean literal; derive it from the ' + "user's residence instead of hardcoding."); + } + super.visitNamedExpression(node); + } + + @override + void visitMethodInvocation(MethodInvocation node) { + final name = node.methodName.name; + + // Rule B: fixed-index address substring — both indices constant AND the + // end index is >= 6, i.e. the call assumes a meaningfully long string + // (an address/hash/id). A trivial peek like `substring(0, 1)` is below the + // threshold and not flagged. + if (name == 'substring') { + final args = node.argumentList.arguments; + if (args.length >= 2 && + args[0] is IntegerLiteral && + args[1] is IntegerLiteral && + ((args[1] as IntegerLiteral).value ?? 0) >= 6) { + _report('fixed_index_address_substring', node.methodName.offset, + 'substring() with two constant indices assumes a fixed length; ' + 'guard the length or compute indices to avoid RangeError.'); + } + } + + // Rule C: cross-flow brokerbot endpoint. + if (_isSellFile && _buyMethods.contains(name)) { + _report('cross_flow_brokerbot_endpoint', node.methodName.offset, + 'sell-flow file calls $name (a buy-side brokerbot endpoint).'); + } else if (_isBuyFile && _sellMethods.contains(name)) { + _report('cross_flow_brokerbot_endpoint', node.methodName.offset, + 'buy-flow file calls $name (a sell-side brokerbot endpoint).'); + } + + super.visitMethodInvocation(node); + } +}