From 0a17aad6e96d8be31a053fa2e1efb8fcb73bceb4 Mon Sep 17 00:00:00 2001 From: joshua Date: Wed, 3 Jun 2026 19:18:25 +0200 Subject: [PATCH 1/3] =?UTF-8?q?ci:=20harden=20static=20analysis=20?= =?UTF-8?q?=E2=80=94=20strict=20casts=20+=20null/async=20lints,=20--fatal-?= =?UTF-8?q?infos/-warnings,=20FD-limit=20fix=20for=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pull-request.yaml | 4 ++-- analysis_options.yaml | 12 ++++++++++++ lib/main.dart | 2 +- .../repository/supported_fiat_repository.dart | 3 ++- lib/packages/service/balance_service.dart | 2 +- lib/packages/service/debug_auth_service.dart | 2 +- .../service/dfx/dfx_auth_service.dart | 5 +++-- .../service/dfx/dfx_bank_account_service.dart | 9 +++++---- .../service/dfx/dfx_brokerbot_service.dart | 8 ++++---- .../service/dfx/dfx_country_service.dart | 4 ++-- .../service/dfx/dfx_fiat_service.dart | 5 +++-- lib/packages/service/dfx/dfx_kyc_service.dart | 6 +++--- .../service/dfx/dfx_language_service.dart | 4 ++-- .../service/dfx/dfx_price_service.dart | 19 ++++++++++--------- .../real_unit_buy_payment_info_service.dart | 2 +- .../service/dfx/real_unit_pdf_service.dart | 6 +++--- .../dfx/real_unit_registration_service.dart | 11 +++++++---- .../real_unit_sell_payment_info_service.dart | 2 +- .../service/transaction_history_service.dart | 6 +++--- lib/packages/utils/format_fixed.dart | 4 ++-- lib/packages/utils/parse_fixed.dart | 4 ++-- ...yment_additional_action_needed_button.dart | 13 +++++++------ .../widgets/payment_information_details.dart | 2 +- .../bloc/connect_bitbox_cubit.dart | 4 ++-- lib/screens/home/bloc/home_bloc.dart | 5 +++-- .../kyc/steps/email/kyc_email_page.dart | 5 +++-- .../registration/kyc_registration_page.dart | 6 +++--- lib/screens/pin/verify_pin_page.dart | 4 ++-- .../validate_seed/validate_seed_cubit.dart | 2 +- .../restore_wallet/restore_wallet_view.dart | 2 +- .../sell/widgets/sell_bank_account_field.dart | 7 ++++--- .../sell_bank_account_selection_page.dart | 2 +- lib/screens/sell/widgets/sell_button.dart | 11 ++++++----- lib/screens/sell_bitbox/sell_bitbox_page.dart | 3 ++- lib/screens/settings/settings_page.dart | 4 ++-- .../settings_contact_page.dart | 9 +++++---- .../settings_user_data_page.dart | 7 ++++--- .../settings_edit_address_page.dart | 5 +++-- .../edit_name/settings_edit_name_page.dart | 5 +++-- lib/screens/verify_seed/verify_seed_page.dart | 2 +- lib/screens/welcome/welcome_page.dart | 2 +- lib/setup/routing/router_config.dart | 10 +++++----- lib/widgets/buttons/app_filled_button.dart | 6 +++--- lib/widgets/buttons/app_text_button.dart | 2 +- test/packages/config/network_mode_test.dart | 4 ++-- .../dfx/dfx_blockchain_api_service_test.dart | 6 +++--- .../registration/registration_dtos_test.dart | 4 ++-- test/screens/buy/buy_page_test.dart | 4 ++-- .../cubit/legal_disclaimer_cubit_test.dart | 2 +- test/screens/pin/setup_pin_cubit_test.dart | 4 ++-- test/screens/pin/setup_pin_page_test.dart | 2 +- test/screens/pin/verify_pin_cubit_test.dart | 6 +++--- test/screens/pin/verify_pin_page_test.dart | 4 ++-- .../pin/widgets/pin_indicator_test.dart | 8 ++++---- .../welcome/widgets/welcome_card_test.dart | 2 +- .../text_substring_highlighting_test.dart | 2 +- 56 files changed, 158 insertions(+), 128 deletions(-) diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 43276344f..2626ec07e 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -70,7 +70,7 @@ jobs: - run: dart run tool/generate_localization.dart - run: dart run tool/generate_release_info.dart - run: flutter pub run build_runner build - - run: flutter analyze + - run: flutter analyze --fatal-infos --fatal-warnings # Excludes the `golden` tag: visual-regression tests live under # `test/goldens/` and are validated on the dfx01 self-hosted runner # in the parallel `golden-tests` job (Hardware-Determinismus, see @@ -78,7 +78,7 @@ jobs: # both duplicate work and erroneously red this job on macos-latest # where the Skia/font-rendering does not match the committed # baselines. - - run: flutter test --coverage --exclude-tags golden + - run: ulimit -n 16384 && flutter test --coverage --exclude-tags golden # Narrow the coverage report to the README-defined activated surface: # lib/packages/** — services, repositories, signers, utils diff --git a/analysis_options.yaml b/analysis_options.yaml index e9380ff99..b991a2181 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,9 +4,21 @@ formatter: page_width: 100 trailing_commas: preserve +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + linter: rules: annotate_overrides: true avoid_print: true prefer_const_constructors: true prefer_single_quotes: true + unawaited_futures: true + avoid_dynamic_calls: true + cast_nullable_to_non_nullable: true + null_check_on_nullable_type_parameter: true + unnecessary_null_checks: true + prefer_final_locals: true diff --git a/lib/main.dart b/lib/main.dart index 4efe297c8..e9051415c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -41,7 +41,7 @@ Future _initialize() async { Future _initializeWithSplashDuration() async { await Future.wait([ _initialize(), - Future.delayed(const Duration(seconds: 3)), + Future.delayed(const Duration(seconds: 3)), ]); } diff --git a/lib/packages/repository/supported_fiat_repository.dart b/lib/packages/repository/supported_fiat_repository.dart index 4ba5ea216..8045a852f 100644 --- a/lib/packages/repository/supported_fiat_repository.dart +++ b/lib/packages/repository/supported_fiat_repository.dart @@ -1,6 +1,7 @@ import 'dart:developer' as developer; import 'package:realunit_wallet/packages/service/dfx/dfx_fiat_service.dart'; +import 'package:realunit_wallet/packages/service/dfx/models/fiat/dto/dfx_fiat_dto.dart'; import 'package:realunit_wallet/styles/currency.dart'; /// Translates the backend's per-user fiat list into the local [Currency] @@ -23,7 +24,7 @@ class SupportedFiatRepository { Future> getAll() => _resolve((_) => true); - Future> _resolve(bool Function(dynamic) filter) async { + Future> _resolve(bool Function(DfxFiatDto) filter) async { final fiats = await _fiatService.getAllFiats(); final result = []; for (final fiat in fiats) { diff --git a/lib/packages/service/balance_service.dart b/lib/packages/service/balance_service.dart index 771fdab2b..6457bd458 100644 --- a/lib/packages/service/balance_service.dart +++ b/lib/packages/service/balance_service.dart @@ -36,7 +36,7 @@ class BalanceService { final response = await _appStore.httpClient.get(uri); if (response.statusCode == 200) { - final json = jsonDecode(response.body); + final json = jsonDecode(response.body) as Map; final balanceString = json['balance'] as String?; if (balanceString != null) { diff --git a/lib/packages/service/debug_auth_service.dart b/lib/packages/service/debug_auth_service.dart index ed8c17ed1..b2d643fe7 100644 --- a/lib/packages/service/debug_auth_service.dart +++ b/lib/packages/service/debug_auth_service.dart @@ -30,7 +30,7 @@ class DebugAuthService { ); if (response.statusCode == 200) { - final body = jsonDecode(response.body); + final body = jsonDecode(response.body) as Map; return body['message'] as String; } throw Exception('Failed to fetch sign message (${response.statusCode})'); diff --git a/lib/packages/service/dfx/dfx_auth_service.dart b/lib/packages/service/dfx/dfx_auth_service.dart index 0ac444786..97feb8fd1 100644 --- a/lib/packages/service/dfx/dfx_auth_service.dart +++ b/lib/packages/service/dfx/dfx_auth_service.dart @@ -146,8 +146,9 @@ abstract class DFXAuthService { final responseBody = jsonDecode(response.body); return responseBody as Map; } else if (response.statusCode == 403) { - final responseBody = jsonDecode(response.body); - final message = responseBody['message'] ?? 'Service unavailable in your country'; + final responseBody = jsonDecode(response.body) as Map; + final message = + responseBody['message'] ?? 'Service unavailable in your country'; throw Exception(message); } else { throw Exception('Failed to sign up. Status: ${response.statusCode} ${response.body}'); diff --git a/lib/packages/service/dfx/dfx_bank_account_service.dart b/lib/packages/service/dfx/dfx_bank_account_service.dart index 6dd21d937..95878781d 100644 --- a/lib/packages/service/dfx/dfx_bank_account_service.dart +++ b/lib/packages/service/dfx/dfx_bank_account_service.dart @@ -24,8 +24,9 @@ class DfxBankAccountService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - final List jsonList = jsonDecode(response.body); - final bankAccounts = jsonList.map((json) => BankAccountDto.fromJson(json)).toList(); + final jsonList = jsonDecode(response.body) as List; + final bankAccounts = + jsonList.map((json) => BankAccountDto.fromJson(json as Map)).toList(); return bankAccounts; } @@ -47,7 +48,7 @@ class DfxBankAccountService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - return BankAccountDto.fromJson(jsonDecode(response.body)); + return BankAccountDto.fromJson(jsonDecode(response.body) as Map); } Future updateBankAccount({ @@ -74,6 +75,6 @@ class DfxBankAccountService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - return BankAccountDto.fromJson(jsonDecode(response.body)); + return BankAccountDto.fromJson(jsonDecode(response.body) as Map); } } diff --git a/lib/packages/service/dfx/dfx_brokerbot_service.dart b/lib/packages/service/dfx/dfx_brokerbot_service.dart index 01e9f30cb..38760f8ac 100644 --- a/lib/packages/service/dfx/dfx_brokerbot_service.dart +++ b/lib/packages/service/dfx/dfx_brokerbot_service.dart @@ -34,7 +34,7 @@ class DfxBrokerbotService extends DFXAuthService { throw Exception('BuyPrice request failed: ${res.body}'); } - return BrokerbotBuyPriceDto.fromJson(jsonDecode(res.body)); + return BrokerbotBuyPriceDto.fromJson(jsonDecode(res.body) as Map); } /// Convert CHF → REALU shares @@ -54,7 +54,7 @@ class DfxBrokerbotService extends DFXAuthService { throw Exception('Shares request failed: ${res.body}'); } - return BrokerbotBuySharesDto.fromJson(jsonDecode(res.body)); + return BrokerbotBuySharesDto.fromJson(jsonDecode(res.body) as Map); } /// Convert REALU shares → CHF (with fees) @@ -75,7 +75,7 @@ class DfxBrokerbotService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: res.statusCode); } - return BrokerbotSellPriceDto.fromJson(jsonDecode(res.body)); + return BrokerbotSellPriceDto.fromJson(jsonDecode(res.body) as Map); } /// Convert CHF → REALU shares (with fees) @@ -96,6 +96,6 @@ class DfxBrokerbotService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: res.statusCode); } - return BrokerbotSellSharesDto.fromJson(jsonDecode(res.body)); + return BrokerbotSellSharesDto.fromJson(jsonDecode(res.body) as Map); } } diff --git a/lib/packages/service/dfx/dfx_country_service.dart b/lib/packages/service/dfx/dfx_country_service.dart index dc35a42a2..d4810fd78 100644 --- a/lib/packages/service/dfx/dfx_country_service.dart +++ b/lib/packages/service/dfx/dfx_country_service.dart @@ -23,9 +23,9 @@ class DfxCountryService { final response = await _appStore.httpClient.get(uri); if (response.statusCode == 200) { - final List jsonList = jsonDecode(response.body); + final jsonList = jsonDecode(response.body) as List; final countries = jsonList - .map((json) => DfxCountryDto.fromJson(json)) + .map((json) => DfxCountryDto.fromJson(json as Map)) .map((dto) => dto.toCountry()) .toList(); cachedCountries = countries; diff --git a/lib/packages/service/dfx/dfx_fiat_service.dart b/lib/packages/service/dfx/dfx_fiat_service.dart index f2a25c50d..7f7784a04 100644 --- a/lib/packages/service/dfx/dfx_fiat_service.dart +++ b/lib/packages/service/dfx/dfx_fiat_service.dart @@ -40,8 +40,9 @@ class DfxFiatService { throw Exception('Failed to fetch fiats: ${response.statusCode}'); } - final List jsonList = jsonDecode(response.body); - final fiats = jsonList.map((json) => DfxFiatDto.fromJson(json)).toList(); + final jsonList = jsonDecode(response.body) as List; + final fiats = + jsonList.map((json) => DfxFiatDto.fromJson(json as Map)).toList(); _cachedFiats = fiats; _cachedAt = clock.now(); return fiats; diff --git a/lib/packages/service/dfx/dfx_kyc_service.dart b/lib/packages/service/dfx/dfx_kyc_service.dart index d7acd58b5..b6cf1d723 100644 --- a/lib/packages/service/dfx/dfx_kyc_service.dart +++ b/lib/packages/service/dfx/dfx_kyc_service.dart @@ -53,7 +53,7 @@ class DfxKycService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - final json = jsonDecode(response.body); + final json = jsonDecode(response.body) as Map; return KycLevelDto.fromJson(json); } @@ -76,7 +76,7 @@ class DfxKycService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - final json = jsonDecode(response.body); + final json = jsonDecode(response.body) as Map; return KycSessionDto.fromJson(json); } @@ -98,7 +98,7 @@ class DfxKycService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - final json = jsonDecode(response.body); + final json = jsonDecode(response.body) as Map; return KycSessionDto.fromJson(json); } diff --git a/lib/packages/service/dfx/dfx_language_service.dart b/lib/packages/service/dfx/dfx_language_service.dart index 21302a6fd..e5eaa115b 100644 --- a/lib/packages/service/dfx/dfx_language_service.dart +++ b/lib/packages/service/dfx/dfx_language_service.dart @@ -39,9 +39,9 @@ class DfxLanguageService { throw Exception('Failed to fetch languages: ${response.statusCode}'); } - final List jsonList = jsonDecode(response.body); + final jsonList = jsonDecode(response.body) as List; final languages = jsonList - .map((json) => DfxLanguageDto.fromJson(json)) + .map((json) => DfxLanguageDto.fromJson(json as Map)) .toList(); _cachedLanguages = languages; _cachedAt = clock.now(); diff --git a/lib/packages/service/dfx/dfx_price_service.dart b/lib/packages/service/dfx/dfx_price_service.dart index dba6fe7b7..59c5e7872 100644 --- a/lib/packages/service/dfx/dfx_price_service.dart +++ b/lib/packages/service/dfx/dfx_price_service.dart @@ -24,25 +24,26 @@ class DFXPriceService extends APriceService { if (response.statusCode != 200) throw Exception(response.body); - final body = jsonDecode(response.body) as List; + final body = jsonDecode(response.body) as List; final result = []; - for (final entry in body) { + for (final raw in body) { + final entry = raw as Map; BigInt price; switch (currency) { case Currency.eur: - price = BigInt.from(entry['eur'] * 100); + price = BigInt.from((entry['eur'] as num) * 100); break; case Currency.chf: - price = BigInt.from(entry['chf'] * 100); + price = BigInt.from((entry['chf'] as num) * 100); break; } result.add( PricePoint( asset: asset, price: price, - time: DateTime.parse(entry['timestamp']), + time: DateTime.parse(entry['timestamp'] as String), ), ); } @@ -57,13 +58,13 @@ class DFXPriceService extends APriceService { if (response.statusCode != 200) throw Exception(response.body); - final body = jsonDecode(response.body); + final body = jsonDecode(response.body) as Map; switch (currency) { case Currency.eur: - return BigInt.from(body['eur'] * 100); + return BigInt.from((body['eur'] as num) * 100); case Currency.chf: - return BigInt.from(body['chf'] * 100); + return BigInt.from((body['chf'] as num) * 100); } } @@ -74,7 +75,7 @@ class DFXPriceService extends APriceService { if (response.statusCode != 200) throw Exception(response.body); - final body = jsonDecode(response.body); + final body = jsonDecode(response.body) as Map; final chf = (body['chf'] as num).toDouble(); final eur = (body['eur'] as num).toDouble(); diff --git a/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart b/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart index 86e50d82b..bbad63d98 100644 --- a/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart +++ b/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart @@ -28,7 +28,7 @@ class RealUnitBuyPaymentInfoService extends DFXAuthService { ); if (response.statusCode == 200) { - final json = jsonDecode(response.body); + final json = jsonDecode(response.body) as Map; final responseDto = RealUnitBuyPaymentInfoDto.fromJson(json); return BuyPaymentInfo( diff --git a/lib/packages/service/dfx/real_unit_pdf_service.dart b/lib/packages/service/dfx/real_unit_pdf_service.dart index 50e4b6a1b..0ff63d24e 100644 --- a/lib/packages/service/dfx/real_unit_pdf_service.dart +++ b/lib/packages/service/dfx/real_unit_pdf_service.dart @@ -43,7 +43,7 @@ class RealUnitPdfService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - return PdfDto.fromJson(jsonDecode(response.body)); + return PdfDto.fromJson(jsonDecode(response.body) as Map); } Future getTransactionsReceipt( @@ -64,7 +64,7 @@ class RealUnitPdfService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - return PdfDto.fromJson(jsonDecode(response.body)); + return PdfDto.fromJson(jsonDecode(response.body) as Map); } Future getTransactionReceipt(String id, {Currency currency = Currency.chf}) async { @@ -82,6 +82,6 @@ class RealUnitPdfService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - return PdfDto.fromJson(jsonDecode(response.body)); + return PdfDto.fromJson(jsonDecode(response.body) as Map); } } diff --git a/lib/packages/service/dfx/real_unit_registration_service.dart b/lib/packages/service/dfx/real_unit_registration_service.dart index ab4f1711e..76bb80ef7 100644 --- a/lib/packages/service/dfx/real_unit_registration_service.dart +++ b/lib/packages/service/dfx/real_unit_registration_service.dart @@ -45,7 +45,7 @@ class RealUnitRegistrationService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - return RealUnitRegistrationInfoDto.fromJson(jsonDecode(response.body)); + return RealUnitRegistrationInfoDto.fromJson(jsonDecode(response.body) as Map); } /// registers an email on the wallet. Should always be called first when registering @@ -67,7 +67,8 @@ class RealUnitRegistrationService extends DFXAuthService { final errorJson = jsonDecode(response.body) as Map; throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - final responseDto = RealUnitRegistrationEmailResponseDto.fromJson(jsonDecode(response.body)); + final responseDto = + RealUnitRegistrationEmailResponseDto.fromJson(jsonDecode(response.body) as Map); return responseDto.status; } @@ -166,7 +167,8 @@ class RealUnitRegistrationService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - final responseDto = RealUnitRegistrationResponseDto.fromJson(jsonDecode(response.body)); + final responseDto = + RealUnitRegistrationResponseDto.fromJson(jsonDecode(response.body) as Map); return responseDto.status; } @@ -223,7 +225,8 @@ class RealUnitRegistrationService extends DFXAuthService { throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } - final responseDto = RealUnitRegistrationResponseDto.fromJson(jsonDecode(response.body)); + final responseDto = + RealUnitRegistrationResponseDto.fromJson(jsonDecode(response.body) as Map); return responseDto.status; } } diff --git a/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart b/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart index 871580954..5c1b20212 100644 --- a/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart +++ b/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart @@ -49,7 +49,7 @@ class RealUnitSellPaymentInfoService extends DFXAuthService { ); if (response.statusCode == 200) { - final json = jsonDecode(response.body); + final json = jsonDecode(response.body) as Map; final responseDto = RealUnitSellPaymentInfoDto.fromJson(json); return SellPaymentInfo( diff --git a/lib/packages/service/transaction_history_service.dart b/lib/packages/service/transaction_history_service.dart index 2212fa5d9..18fdafbc2 100644 --- a/lib/packages/service/transaction_history_service.dart +++ b/lib/packages/service/transaction_history_service.dart @@ -25,7 +25,7 @@ class TransactionHistoryService extends DFXAuthService { ]); final accountHistory = results.elementAt(0) as AccountHistoryDto?; - final transactions = results.elementAt(1) as List; + final transactions = results.elementAt(1)! as List; if (accountHistory == null) return; @@ -105,7 +105,7 @@ class TransactionHistoryService extends DFXAuthService { final response = await appStore.httpClient.get(uri); if (response.statusCode != 200) return []; - final List json = jsonDecode(response.body); + final json = jsonDecode(response.body) as List; return json.map((e) => TransactionDto.fromJson(e as Map)).toList(); } @@ -115,7 +115,7 @@ class TransactionHistoryService extends DFXAuthService { if (response.statusCode != 200) return []; - final List json = jsonDecode(response.body); + final json = jsonDecode(response.body) as List; final transactions = json .map((e) => TransactionDto.fromJson(e as Map)) .toList(); diff --git a/lib/packages/utils/format_fixed.dart b/lib/packages/utils/format_fixed.dart index 79583ae9f..e53a7648c 100644 --- a/lib/packages/utils/format_fixed.dart +++ b/lib/packages/utils/format_fixed.dart @@ -4,9 +4,9 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri decimals ??= 0; fractionalDigits ??= decimals; - var multiplier = getMultiplier(decimals); + final multiplier = getMultiplier(decimals); // Make sure wei is a big number (convert as necessary) - var negative = value.isNegative; + final negative = value.isNegative; if (negative) value = value * BigInt.from(-1); var fraction = diff --git a/lib/packages/utils/parse_fixed.dart b/lib/packages/utils/parse_fixed.dart index 94fbc8175..e4bc407b9 100644 --- a/lib/packages/utils/parse_fixed.dart +++ b/lib/packages/utils/parse_fixed.dart @@ -14,8 +14,8 @@ BigInt parseFixed(String value, int? decimals) { throw Exception('too many decimal points, value, $value'); } - var whole = comps.isNotEmpty ? comps[0] : '0'; - var fraction = (comps.length == 2 ? comps[1] : '0').padRight(decimals, '0'); + final whole = comps.isNotEmpty ? comps[0] : '0'; + final fraction = (comps.length == 2 ? comps[1] : '0').padRight(decimals, '0'); // Check the fraction doesn't exceed our decimals size if (fraction.length > multiplier.length - 1) { diff --git a/lib/screens/buy/widgets/payment_additional_action_needed_button.dart b/lib/screens/buy/widgets/payment_additional_action_needed_button.dart index 13d718327..2d7082ea9 100644 --- a/lib/screens/buy/widgets/payment_additional_action_needed_button.dart +++ b/lib/screens/buy/widgets/payment_additional_action_needed_button.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -57,10 +58,10 @@ class PaymentAdditionalActionNeededButton extends StatelessWidget { onPressed: () async { await context.pushNamed(AppRoutes.kyc, extra: paymentState.context); if (context.mounted) { - context.read().getPaymentInfo( + unawaited(context.read().getPaymentInfo( amount: amountController.text, currency: context.read().state.currency, - ); + )); } }, label: S.of(context).next, @@ -74,10 +75,10 @@ class PaymentAdditionalActionNeededButton extends StatelessWidget { onPressed: () async { await context.pushNamed(AppRoutes.kyc, extra: paymentState.context); if (context.mounted) { - context.read().getPaymentInfo( + unawaited(context.read().getPaymentInfo( amount: amountController.text, currency: context.read().state.currency, - ); + )); } }, label: S.of(context).next, @@ -92,10 +93,10 @@ class PaymentAdditionalActionNeededButton extends StatelessWidget { final paymentInfoCubit = context.read(); final converterCubit = context.read(); await showBitboxReconnectSheet(context); - paymentInfoCubit.getPaymentInfo( + unawaited(paymentInfoCubit.getPaymentInfo( amount: amountController.text, currency: converterCubit.state.currency, - ); + )); }, label: S.of(context).bitboxReconnect, ), diff --git a/lib/screens/buy/widgets/payment_information_details.dart b/lib/screens/buy/widgets/payment_information_details.dart index d1e794b19..5642527c6 100644 --- a/lib/screens/buy/widgets/payment_information_details.dart +++ b/lib/screens/buy/widgets/payment_information_details.dart @@ -62,7 +62,7 @@ class PaymentInformationDetailsView extends StatelessWidget { return BlocListener( listener: (context, state) async { if (state is BuyConfirmSuccess) { - await showModalBottomSheet( + await showModalBottomSheet( context: context, builder: (_) => PaymentExecutedSheet(reference: state.reference), ); 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 60170bb63..26cdc5a74 100644 --- a/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart +++ b/lib/screens/hardware_connect_bitbox/bloc/connect_bitbox_cubit.dart @@ -59,7 +59,7 @@ class ConnectBitboxCubit extends Cubit { if (devices.isNotEmpty) { emit(BitboxFound(devices.first)); _checkForTimer?.cancel(); - connectToBitbox(devices.first); + unawaited(connectToBitbox(devices.first)); } } @@ -98,7 +98,7 @@ class ConnectBitboxCubit extends Cubit { // First sleep is longer so the SDK can finish setting up its Go-side // device pointer before we call into it; subsequent iterations stay // tight so the post-PIN handover feels snappy. - await Future.delayed( + await Future.delayed( firstIteration ? const Duration(milliseconds: 500) : const Duration(milliseconds: 100), ); firstIteration = false; diff --git a/lib/screens/home/bloc/home_bloc.dart b/lib/screens/home/bloc/home_bloc.dart index 2ddc90f6c..0e97f15f7 100644 --- a/lib/screens/home/bloc/home_bloc.dart +++ b/lib/screens/home/bloc/home_bloc.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:equatable/equatable.dart'; @@ -73,9 +74,9 @@ class HomeBloc extends Bloc { return; } - _balanceService.updateBalance(_appStore.primaryAddress); + unawaited(_balanceService.updateBalance(_appStore.primaryAddress)); _balanceService.startSync(_appStore.primaryAddress); - _transactionHistoryService.apiBasedSync(); + unawaited(_transactionHistoryService.apiBasedSync()); } Future _onDeleteCurrentWallet( diff --git a/lib/screens/kyc/steps/email/kyc_email_page.dart b/lib/screens/kyc/steps/email/kyc_email_page.dart index 9710e06ca..11f399fb2 100644 --- a/lib/screens/kyc/steps/email/kyc_email_page.dart +++ b/lib/screens/kyc/steps/email/kyc_email_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:realunit_wallet/generated/i18n.dart'; @@ -70,7 +71,7 @@ class _KycEmailFormState extends State { } if (state is KycEmailStepSuccess) { if (state.status == .emailRegistered) { - context.read().checkKyc(); + unawaited(context.read().checkKyc()); } if (state.status == .mergeRequested) { final isConfirmed = await Navigator.push( @@ -86,7 +87,7 @@ class _KycEmailFormState extends State { // `checkKyc()` re-fetches `getRegistrationInfo`, sees // `AlreadyRegistered`, and routes forward — no local sign-gate // flag needed. - context.read().checkKyc(); + unawaited(context.read().checkKyc()); } } } diff --git a/lib/screens/kyc/steps/registration/kyc_registration_page.dart b/lib/screens/kyc/steps/registration/kyc_registration_page.dart index f73be532e..e4ee5bff0 100644 --- a/lib/screens/kyc/steps/registration/kyc_registration_page.dart +++ b/lib/screens/kyc/steps/registration/kyc_registration_page.dart @@ -167,7 +167,7 @@ class _KycRegistrationViewState extends State { // alreadyRegistered). The backend now reflects the new wallet, // so re-fetching `getRegistrationInfo` in `_runCheckKyc` will return // `AlreadyRegistered` and dispatch the next KYC step. - context.read().checkKyc(); + unawaited(context.read().checkKyc()); if (state.status == RegistrationStatus.forwardingFailed) { ScaffoldMessenger.of(context).showSnackBar( @@ -191,7 +191,7 @@ class _KycRegistrationViewState extends State { } if (state is KycRegistrationSubmitBitboxRequired) { final registration = state.registration; - final result = await showModalBottomSheet( + final result = await showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => ConnectBitboxPage( @@ -202,7 +202,7 @@ class _KycRegistrationViewState extends State { ), ); if (context.mounted && result == true) { - context.read().retrySubmit(registration); + unawaited(context.read().retrySubmit(registration)); } } }, diff --git a/lib/screens/pin/verify_pin_page.dart b/lib/screens/pin/verify_pin_page.dart index 9b2d9384f..f0af94a9d 100644 --- a/lib/screens/pin/verify_pin_page.dart +++ b/lib/screens/pin/verify_pin_page.dart @@ -152,7 +152,7 @@ class _VerifyPinViewState extends State { height: 40.0, child: Text( switch (state) { - VerifyPinTemporarilyLocked s => + final VerifyPinTemporarilyLocked s => S .of(context) .pinVerifyLockedTemporarily( @@ -243,7 +243,7 @@ class _ForgotPinButton extends StatelessWidget { builder: (_) => const ForgotPinBottomSheet(), ); if (isReset == true) { - await Future.delayed(const Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: 300)); if (context.mounted) { await context.read().reset(); if (context.mounted) { diff --git a/lib/screens/restore_wallet/cubit/validate_seed/validate_seed_cubit.dart b/lib/screens/restore_wallet/cubit/validate_seed/validate_seed_cubit.dart index a1910b249..c6a8e1ada 100644 --- a/lib/screens/restore_wallet/cubit/validate_seed/validate_seed_cubit.dart +++ b/lib/screens/restore_wallet/cubit/validate_seed/validate_seed_cubit.dart @@ -27,7 +27,7 @@ class ValidateSeedCubit extends Cubit { } } - bool _containsAll(Iterable a, Iterable b) { + bool _containsAll(Iterable a, Iterable b) { for (final element in b) { if (!a.contains(element)) return false; } diff --git a/lib/screens/restore_wallet/restore_wallet_view.dart b/lib/screens/restore_wallet/restore_wallet_view.dart index b72484a75..6fecc904d 100644 --- a/lib/screens/restore_wallet/restore_wallet_view.dart +++ b/lib/screens/restore_wallet/restore_wallet_view.dart @@ -30,7 +30,7 @@ class _RestoreWalletViewState extends State { listenWhen: (previous, current) => previous.wallet != current.wallet, listener: (context, state) async { if (state.wallet != null) { - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 2)); if (context.mounted) context.read().add(LoadWalletEvent(state.wallet!)); } }, diff --git a/lib/screens/sell/widgets/sell_bank_account_field.dart b/lib/screens/sell/widgets/sell_bank_account_field.dart index 21149ae4f..b6e50d477 100644 --- a/lib/screens/sell/widgets/sell_bank_account_field.dart +++ b/lib/screens/sell/widgets/sell_bank_account_field.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer' as developer; import 'package:collection/collection.dart'; @@ -140,7 +141,7 @@ class BankAccountFieldView extends StatelessWidget { final sellSelectedBankAccountCubit = context.read(); if (sellBankAccountsCubit.state.accounts.isEmpty) { - await showModalBottomSheet( + await showModalBottomSheet( isScrollControlled: true, context: context, builder: (_) => BlocProvider.value( @@ -149,7 +150,7 @@ class BankAccountFieldView extends StatelessWidget { ), ); } else { - Navigator.push( + unawaited(Navigator.push( context, MaterialPageRoute( builder: (_) => MultiBlocProvider( @@ -164,7 +165,7 @@ class BankAccountFieldView extends StatelessWidget { child: const SellBankAccountSelectionPage(), ), ), - ); + )); } } } diff --git a/lib/screens/sell/widgets/sell_bank_account_selection_page.dart b/lib/screens/sell/widgets/sell_bank_account_selection_page.dart index 1d7c32a36..6a83f6983 100644 --- a/lib/screens/sell/widgets/sell_bank_account_selection_page.dart +++ b/lib/screens/sell/widgets/sell_bank_account_selection_page.dart @@ -108,7 +108,7 @@ class SellBankAccountSelectionPage extends StatelessWidget { Future _onAddBankAccountPressed(BuildContext context) async { final sellBankAccountsCubit = context.read(); - await showModalBottomSheet( + await showModalBottomSheet( isScrollControlled: true, context: context, builder: (_) => BlocProvider.value( diff --git a/lib/screens/sell/widgets/sell_button.dart b/lib/screens/sell/widgets/sell_button.dart index e3ea05813..2946a3e2b 100644 --- a/lib/screens/sell/widgets/sell_button.dart +++ b/lib/screens/sell/widgets/sell_button.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -36,11 +37,11 @@ class SellButton extends StatelessWidget { final converterCurrency = context.read().state.currency; await showBitboxReconnectSheet(context); if (bankAccount != null && amount.isNotEmpty) { - paymentInfoCubit.getPaymentInfo( + unawaited(paymentInfoCubit.getPaymentInfo( amount: amount, iban: bankAccount!.iban, currency: converterCurrency, - ); + )); } return; } @@ -55,10 +56,10 @@ class SellButton extends StatelessWidget { } if (state is SellPaymentInfoSuccess) { if (state.isBitbox && context.mounted) { - context.pushNamed(AppRoutes.sellBitbox, extra: state.sellPaymentInfo); + unawaited(context.pushNamed(AppRoutes.sellBitbox, extra: state.sellPaymentInfo)); return; } else { - final bool? confirmedSuccess = await showModalBottomSheet( + final bool? confirmedSuccess = await showModalBottomSheet( isScrollControlled: true, context: context, builder: (_) => SellConfirmSheet( @@ -67,7 +68,7 @@ class SellButton extends StatelessWidget { ); if (confirmedSuccess ?? false) { if (context.mounted) { - await showModalBottomSheet( + await showModalBottomSheet( context: context, builder: (_) => const SellExecutedSheet(), ); diff --git a/lib/screens/sell_bitbox/sell_bitbox_page.dart b/lib/screens/sell_bitbox/sell_bitbox_page.dart index 8a3effdc9..041e309cf 100644 --- a/lib/screens/sell_bitbox/sell_bitbox_page.dart +++ b/lib/screens/sell_bitbox/sell_bitbox_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -48,7 +49,7 @@ class SellBitboxView extends StatelessWidget { if (state is SellBitboxBitboxRequired) { final reconnected = await showBitboxReconnectSheet(context); if (reconnected && context.mounted) { - context.read().retryAfterConnection(); + unawaited(context.read().retryAfterConnection()); } return; } diff --git a/lib/screens/settings/settings_page.dart b/lib/screens/settings/settings_page.dart index d66313b7e..2e98adb9f 100644 --- a/lib/screens/settings/settings_page.dart +++ b/lib/screens/settings/settings_page.dart @@ -125,13 +125,13 @@ class SettingsPage extends StatelessWidget { title: S.of(context).settingsDeleteWallet, leading: const XCircleIcon(size: 24), onTap: () async { - bool? isLogout = await showModalBottomSheet( + final bool? isLogout = await showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => const SettingsConfirmLogoutWalletSheet(), ); if (isLogout ?? false) { - await Future.delayed(const Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: 300)); if (context.mounted) { await context.read().reset(); if (context.mounted) { diff --git a/lib/screens/settings_contact/settings_contact_page.dart b/lib/screens/settings_contact/settings_contact_page.dart index 9aae96066..04d20b407 100644 --- a/lib/screens/settings_contact/settings_contact_page.dart +++ b/lib/screens/settings_contact/settings_contact_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -142,13 +143,13 @@ class SettingsContactView extends StatelessWidget { // / Failure). API is the authority — if the call is not allowed, // the Support page surfaces the API error. if (capability == null) { - context.pushNamed(SupportRoutes.support); + unawaited(context.pushNamed(SupportRoutes.support)); return; } // Branch 2: explicitly available → straight to Support. if (capability.available) { - context.pushNamed(SupportRoutes.support); + unawaited(context.pushNamed(SupportRoutes.support)); return; } @@ -164,7 +165,7 @@ class SettingsContactView extends StatelessWidget { // Defensive: API reported `available: false` without a // prerequisite this app version routes for. Push Support // directly and let the API render the error. - context.pushNamed(SupportRoutes.support); + unawaited(context.pushNamed(SupportRoutes.support)); } } @@ -185,7 +186,7 @@ class SettingsContactView extends StatelessWidget { if (state is! SettingsContactSuccess) return; final refreshed = state.capability; if (refreshed == null || refreshed.available) { - context.pushNamed(SupportRoutes.support); + unawaited(context.pushNamed(SupportRoutes.support)); } } } diff --git a/lib/screens/settings_user_data/settings_user_data_page.dart b/lib/screens/settings_user_data/settings_user_data_page.dart index 4c61451f1..a7057aef2 100644 --- a/lib/screens/settings_user_data/settings_user_data_page.dart +++ b/lib/screens/settings_user_data/settings_user_data_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -70,7 +71,7 @@ class SettingsUserDataView extends StatelessWidget { SettingsRoutes.editName, ); if (isEdited == true && context.mounted) { - context.read().getUserData(); + unawaited(context.read().getUserData()); } } : null, @@ -93,7 +94,7 @@ class SettingsUserDataView extends StatelessWidget { SettingsRoutes.editPhone, ); if (isEdited == true && context.mounted) { - context.read().getUserData(); + unawaited(context.read().getUserData()); } } : null, @@ -111,7 +112,7 @@ class SettingsUserDataView extends StatelessWidget { SettingsRoutes.editAddress, ); if (isEdited == true && context.mounted) { - context.read().getUserData(); + unawaited(context.read().getUserData()); } } : null, diff --git a/lib/screens/settings_user_data/subpages/edit_address/settings_edit_address_page.dart b/lib/screens/settings_user_data/subpages/edit_address/settings_edit_address_page.dart index a72cd9cf6..2fc62e252 100644 --- a/lib/screens/settings_user_data/subpages/edit_address/settings_edit_address_page.dart +++ b/lib/screens/settings_user_data/subpages/edit_address/settings_edit_address_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -219,7 +220,7 @@ class _SettingsEditAddressViewState extends State { if ((_formKey.currentState?.validate() ?? false) && _selectedFile != null && country != null) { final fileBase64 = await _selectedFile!.toBase64DataUri(); if (mounted) { - context.read().submitAddress( + unawaited(context.read().submitAddress( street: _streetCtrl.text, houseNumber: _numberCtrl.text, zip: _postalCodeCtrl.text, @@ -227,7 +228,7 @@ class _SettingsEditAddressViewState extends State { countryId: country.id, fileBase64: fileBase64, fileName: _selectedFile!.name, - ); + )); } } } diff --git a/lib/screens/settings_user_data/subpages/edit_name/settings_edit_name_page.dart b/lib/screens/settings_user_data/subpages/edit_name/settings_edit_name_page.dart index 1eb291181..3ff2b5370 100644 --- a/lib/screens/settings_user_data/subpages/edit_name/settings_edit_name_page.dart +++ b/lib/screens/settings_user_data/subpages/edit_name/settings_edit_name_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -160,12 +161,12 @@ class _SettingsEditNameViewState extends State { if ((_formKey.currentState?.validate() ?? false) && _selectedFile != null) { final fileBase64 = await _selectedFile!.toBase64DataUri(); if (mounted) { - context.read().submitName( + unawaited(context.read().submitName( firstName: _firstNameCtrl.text, lastName: _lastNameCtrl.text, fileBase64: fileBase64, fileName: _selectedFile!.name, - ); + )); } } } diff --git a/lib/screens/verify_seed/verify_seed_page.dart b/lib/screens/verify_seed/verify_seed_page.dart index 17804a1a6..b25a9ff59 100644 --- a/lib/screens/verify_seed/verify_seed_page.dart +++ b/lib/screens/verify_seed/verify_seed_page.dart @@ -39,7 +39,7 @@ class VerifySeedView extends StatelessWidget { child: BlocConsumer( listener: (context, state) async { if (state.isVerified) { - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 2)); if (context.mounted) { // Load the *committed* wallet (`state.committedWallet`), not // the page's draft (`id == 0`). `committedWallet` is only diff --git a/lib/screens/welcome/welcome_page.dart b/lib/screens/welcome/welcome_page.dart index 9c5ddefb8..229842a73 100644 --- a/lib/screens/welcome/welcome_page.dart +++ b/lib/screens/welcome/welcome_page.dart @@ -163,7 +163,7 @@ class _WelcomePageState extends State { ); Future onBitboxPressed(BuildContext context) async { - await showModalBottomSheet( + await showModalBottomSheet( isScrollControlled: true, context: context, builder: (_) => ConnectBitboxPage( diff --git a/lib/setup/routing/router_config.dart b/lib/setup/routing/router_config.dart index 3d0b9e263..4e1bdf001 100644 --- a/lib/setup/routing/router_config.dart +++ b/lib/setup/routing/router_config.dart @@ -79,7 +79,7 @@ final GoRouter routerConfig = GoRouter( GoRoute( name: OnboardingRoutes.verifySeed, path: '/verifySeed', - builder: (_, state) => VerifySeedPage(wallet: state.extra as SoftwareWallet), + builder: (_, state) => VerifySeedPage(wallet: state.extra! as SoftwareWallet), ), GoRoute( @@ -99,7 +99,7 @@ final GoRouter routerConfig = GoRouter( name: PinRoutes.gate, path: '/pinGate', builder: (_, state) { - final params = state.extra as VerifyPinParams; + final params = state.extra! as VerifyPinParams; return VerifyPinPage( description: params.description, onAuthenticated: params.onAuthenticated, @@ -147,7 +147,7 @@ final GoRouter routerConfig = GoRouter( GoRoute( name: AppRoutes.sellBitbox, path: '/sellBitbox', - builder: (_, state) => SellBitboxPage(paymentInfo: state.extra as SellPaymentInfo), + builder: (_, state) => SellBitboxPage(paymentInfo: state.extra! as SellPaymentInfo), ), GoRoute( @@ -162,7 +162,7 @@ final GoRouter routerConfig = GoRouter( builder: (_, state) { final extra = state.extra; return LegalDocumentPage( - params: extra as LegalDocumentParams, + params: extra! as LegalDocumentParams, ); }, ), @@ -303,7 +303,7 @@ final GoRouter routerConfig = GoRouter( GoRoute( name: AppRoutes.webView, path: '/webView', - builder: (_, state) => WebViewPage(state.extra as WebViewRouteParams), + builder: (_, state) => WebViewPage(state.extra! as WebViewRouteParams), ), ], ); diff --git a/lib/widgets/buttons/app_filled_button.dart b/lib/widgets/buttons/app_filled_button.dart index 2fe2790fe..77c883968 100644 --- a/lib/widgets/buttons/app_filled_button.dart +++ b/lib/widgets/buttons/app_filled_button.dart @@ -44,7 +44,7 @@ class AppFilledButton extends StatelessWidget { onPressed: onPressed, style: style, icon: Icon( - icon!, + icon, color: variant == FilledButtonVariant.secondary ? RealUnitColors.realUnitBlack : null, ), label: Text( @@ -99,7 +99,7 @@ class AppFilledButton extends StatelessWidget { foregroundColor: WidgetStateProperty.all(RealUnitColors.basic.white), iconColor: WidgetStateProperty.all(RealUnitColors.basic.white), ), - icon: Icon(icon!), + icon: Icon(icon), label: Text( label, textAlign: .center, @@ -129,7 +129,7 @@ class AppFilledButton extends StatelessWidget { foregroundColor: WidgetStateProperty.all(RealUnitColors.basic.white), iconColor: WidgetStateProperty.all(RealUnitColors.basic.white), ), - icon: Icon(icon!), + icon: Icon(icon), label: Text( label, textAlign: .center, diff --git a/lib/widgets/buttons/app_text_button.dart b/lib/widgets/buttons/app_text_button.dart index 24bf46ff6..a7aa46dbe 100644 --- a/lib/widgets/buttons/app_text_button.dart +++ b/lib/widgets/buttons/app_text_button.dart @@ -18,7 +18,7 @@ class AppTextButton extends StatelessWidget { @override Widget build(BuildContext context) { final button = icon != null - ? TextButton.icon(onPressed: onPressed, icon: Icon(icon!), label: Text(label)) + ? TextButton.icon(onPressed: onPressed, icon: Icon(icon), label: Text(label)) : TextButton(onPressed: onPressed, child: Text(label)); if (fullWidth) return SizedBox(width: .infinity, child: button); return button; diff --git a/test/packages/config/network_mode_test.dart b/test/packages/config/network_mode_test.dart index 70e896e5f..c074fa59a 100644 --- a/test/packages/config/network_mode_test.dart +++ b/test/packages/config/network_mode_test.dart @@ -48,7 +48,7 @@ void main() { ); expect(resolved, isNotNull); - expect(resolved!, isNotEmpty); + expect(resolved, isNotEmpty); }); testWidgets('testnet resolves to a non-empty localized string distinct from mainnet', ( @@ -67,7 +67,7 @@ void main() { ); expect(testnetText, isNotNull); - expect(testnetText!, isNotEmpty); + expect(testnetText, isNotEmpty); // Pin the per-arm wiring — if both arms accidentally resolved to // the same string key, the user would see "Mainnet" on testnet. expect(testnetText, isNot(equals(mainnetText))); diff --git a/test/packages/service/dfx/dfx_blockchain_api_service_test.dart b/test/packages/service/dfx/dfx_blockchain_api_service_test.dart index 443974908..7e5f4c9b2 100644 --- a/test/packages/service/dfx/dfx_blockchain_api_service_test.dart +++ b/test/packages/service/dfx/dfx_blockchain_api_service_test.dart @@ -66,7 +66,7 @@ void main() { expect(capturedBody!['address'], _testAddress); // chainId 1 → 'Ethereum'. expect(capturedBody!['blockchain'], 'Ethereum'); - expect(capturedBody!['assetIds'], isA()); + expect(capturedBody!['assetIds'], isA>()); expect(capturedHeaders!['Authorization'], 'Bearer jwt-abc'); expect(capturedHeaders!['Content-Type'], 'application/json'); }); @@ -78,7 +78,7 @@ void main() { Map? capturedBody; final client = MockClient((request) async { capturedBody = jsonDecode(request.body) as Map; - return http.Response(jsonEncode({'balances': []}), 200); + return http.Response(jsonEncode({'balances': []}), 200); }); await build(client).getEthBalance(_testAddress); @@ -89,7 +89,7 @@ void main() { test('returns 0.0 when the balances list is empty', () async { sessionCache.setAuthToken('jwt-abc'); final client = MockClient((_) async => http.Response( - jsonEncode({'balances': []}), + jsonEncode({'balances': []}), 200, )); diff --git a/test/packages/service/dfx/models/registration/registration_dtos_test.dart b/test/packages/service/dfx/models/registration/registration_dtos_test.dart index 7834fe353..d42f85de4 100644 --- a/test/packages/service/dfx/models/registration/registration_dtos_test.dart +++ b/test/packages/service/dfx/models/registration/registration_dtos_test.dart @@ -97,7 +97,7 @@ void main() { ); expect(label, isNotNull); - expect(label!, isNotEmpty); + expect(label, isNotEmpty); }); testWidgets('corporation resolves to a non-empty label distinct from human', (tester) async { @@ -114,7 +114,7 @@ void main() { ); expect(corporation, isNotNull); - expect(corporation!, isNotEmpty); + expect(corporation, isNotEmpty); expect(corporation, isNot(equals(human))); }); }); diff --git a/test/screens/buy/buy_page_test.dart b/test/screens/buy/buy_page_test.dart index 4678f2f11..5dcb09ffa 100644 --- a/test/screens/buy/buy_page_test.dart +++ b/test/screens/buy/buy_page_test.dart @@ -299,8 +299,8 @@ void main() { final amountField = find.byType(TextField).first; final resultField = find.byType(TextField).last; - TextField amount = tester.widget(amountField); - TextField result = tester.widget(resultField); + final TextField amount = tester.widget(amountField); + final TextField result = tester.widget(resultField); expect(amount.controller!.text, equals('5.00')); expect(result.controller!.text, equals('0.50')); diff --git a/test/screens/legal/cubit/legal_disclaimer_cubit_test.dart b/test/screens/legal/cubit/legal_disclaimer_cubit_test.dart index 2119642e4..71d899392 100644 --- a/test/screens/legal/cubit/legal_disclaimer_cubit_test.dart +++ b/test/screens/legal/cubit/legal_disclaimer_cubit_test.dart @@ -89,7 +89,7 @@ void main() { 'previousStep at step 0 is a no-op (cannot go below zero)', build: LegalDisclaimerCubit.new, act: (cubit) => cubit.previousStep(), - expect: () => [], + expect: () => [], ); }); } diff --git a/test/screens/pin/setup_pin_cubit_test.dart b/test/screens/pin/setup_pin_cubit_test.dart index cc0256c6c..03c9cc8af 100644 --- a/test/screens/pin/setup_pin_cubit_test.dart +++ b/test/screens/pin/setup_pin_cubit_test.dart @@ -58,7 +58,7 @@ void main() { build: build, seed: () => const SetupPinState(currentPin: '123456'), act: (cubit) => cubit.addDigit(7), - expect: () => [], + expect: () => [], ); blocTest( @@ -73,7 +73,7 @@ void main() { 'deleteDigit on an empty pin is a no-op', build: build, act: (cubit) => cubit.deleteDigit(), - expect: () => [], + expect: () => [], ); test('completing 6 digits in create mode switches to confirm with an empty pin', () async { diff --git a/test/screens/pin/setup_pin_page_test.dart b/test/screens/pin/setup_pin_page_test.dart index d781887ee..bad19bc8d 100644 --- a/test/screens/pin/setup_pin_page_test.dart +++ b/test/screens/pin/setup_pin_page_test.dart @@ -44,7 +44,7 @@ void main() { setUpAll(() { pinAuthCubit = MockPinAuthCubit(); when(() => pinAuthCubit.state).thenReturn(const PinAuthState()); - when(() => pinAuthCubit.onPinSetupComplete()).thenAnswer((_) => Future.value()); + when(() => pinAuthCubit.onPinSetupComplete()).thenAnswer((_) => Future.value()); setupDependencyInjection(); }); diff --git a/test/screens/pin/verify_pin_cubit_test.dart b/test/screens/pin/verify_pin_cubit_test.dart index 3bf861832..77ed51440 100644 --- a/test/screens/pin/verify_pin_cubit_test.dart +++ b/test/screens/pin/verify_pin_cubit_test.dart @@ -74,7 +74,7 @@ void main() { 'deleteDigit on empty pin is a no-op', build: build, act: (cubit) => cubit.deleteDigit(), - expect: () => [], + expect: () => [], ); blocTest( @@ -85,7 +85,7 @@ void main() { lockedUntil: DateTime(2030), ), act: (cubit) => cubit.addDigit(1), - expect: () => [], + expect: () => [], ); blocTest( @@ -93,7 +93,7 @@ void main() { build: build, seed: () => const VerifyPinLocked(failedAttempts: 9), act: (cubit) => cubit.addDigit(1), - expect: () => [], + expect: () => [], ); }); diff --git a/test/screens/pin/verify_pin_page_test.dart b/test/screens/pin/verify_pin_page_test.dart index 37fc6ad06..69f075ed6 100644 --- a/test/screens/pin/verify_pin_page_test.dart +++ b/test/screens/pin/verify_pin_page_test.dart @@ -34,7 +34,7 @@ void main() { verifyPinCubit = MockVerifyPinCubit(); when(() => verifyPinCubit.state).thenReturn(const VerifyPinState()); - when(() => verifyPinCubit.checkBiometricAvailability()).thenAnswer((_) => Future.value()); + when(() => verifyPinCubit.checkBiometricAvailability()).thenAnswer((_) => Future.value()); }); void setupDependencyInjection() { @@ -52,7 +52,7 @@ void main() { setUpAll(() { pinAuthCubit = MockPinAuthCubit(); when(() => pinAuthCubit.state).thenReturn(const PinAuthState()); - when(() => pinAuthCubit.onPinVerified()).thenAnswer((_) => Future.value()); + when(() => pinAuthCubit.onPinVerified()).thenAnswer((_) => Future.value()); setupDependencyInjection(); }); diff --git a/test/screens/pin/widgets/pin_indicator_test.dart b/test/screens/pin/widgets/pin_indicator_test.dart index 7f1750aaf..1cd4fde4d 100644 --- a/test/screens/pin/widgets/pin_indicator_test.dart +++ b/test/screens/pin/widgets/pin_indicator_test.dart @@ -25,7 +25,7 @@ void main() { )); final fills = dots(tester) - .map((c) => (c.decoration as BoxDecoration).color) + .map((c) => (c.decoration! as BoxDecoration).color) .toList(); // First three filled (black), last three transparent. expect(fills.take(3), everyElement(RealUnitColors.realUnitBlack)); @@ -38,7 +38,7 @@ void main() { )); final borderColors = dots(tester) - .map((c) => (c.decoration as BoxDecoration).border!.top.color) + .map((c) => (c.decoration! as BoxDecoration).border!.top.color) .toList(); expect(borderColors, everyElement(RealUnitColors.status.red600)); @@ -51,7 +51,7 @@ void main() { )); final borderColors = dots(tester) - .map((c) => (c.decoration as BoxDecoration).border!.top.color) + .map((c) => (c.decoration! as BoxDecoration).border!.top.color) .toList(); expect(borderColors, everyElement(RealUnitColors.realUnitBlack)); @@ -63,7 +63,7 @@ void main() { )); final fills = dots(tester) - .map((c) => (c.decoration as BoxDecoration).color) + .map((c) => (c.decoration! as BoxDecoration).color) .toList(); expect(fills, everyElement(RealUnitColors.realUnitBlack)); }); diff --git a/test/screens/welcome/widgets/welcome_card_test.dart b/test/screens/welcome/widgets/welcome_card_test.dart index a3f48455c..50d831420 100644 --- a/test/screens/welcome/widgets/welcome_card_test.dart +++ b/test/screens/welcome/widgets/welcome_card_test.dart @@ -14,7 +14,7 @@ void main() { String? description; VoidCallback? onPressed; Widget? trailing; - MockFunction functions = MockFunction(); + final MockFunction functions = MockFunction(); setUp(() { title = 'Welcome'; diff --git a/test/widgets/text_substring_highlighting_test.dart b/test/widgets/text_substring_highlighting_test.dart index 94e2aed7b..abd4ed9e8 100644 --- a/test/widgets/text_substring_highlighting_test.dart +++ b/test/widgets/text_substring_highlighting_test.dart @@ -117,7 +117,7 @@ void main() { // Fire the recognizer manually since hit-testing a TextSpan in a widget // test is fiddly — the gestureRecognizer itself is what would receive // the gesture in production. - (highlightedSpan.recognizer as TapGestureRecognizer).onTap!(); + (highlightedSpan.recognizer! as TapGestureRecognizer).onTap!(); expect(taps, 1); }); From 64382b4e957115ac33188aa9899cc0cd897055ec Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Thu, 4 Jun 2026 08:51:32 +0200 Subject: [PATCH 2/3] ci: drop --fatal-infos from analyze, keep --fatal-warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The targeted bug classes (unguarded casts, unawaited futures, dynamic calls) already fail under the new strict-* modes and warning-level lints. --fatal-infos would hard-red every PR on future SDK/lint bumps that add new info-level lints — the noisiest tier — for no extra bug coverage. --- .github/workflows/pull-request.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 2626ec07e..e65f82ed1 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -70,7 +70,7 @@ jobs: - run: dart run tool/generate_localization.dart - run: dart run tool/generate_release_info.dart - run: flutter pub run build_runner build - - run: flutter analyze --fatal-infos --fatal-warnings + - run: flutter analyze --fatal-warnings # Excludes the `golden` tag: visual-regression tests live under # `test/goldens/` and are validated on the dfx01 self-hosted runner # in the parallel `golden-tests` job (Hardware-Determinismus, see From be00a36144397779cdeb38f399881a5f57693094 Mon Sep 17 00:00:00 2001 From: joshua Date: Thu, 4 Jun 2026 09:15:07 +0200 Subject: [PATCH 3/3] =?UTF-8?q?ci:=20#663=20follow-ups=20=E2=80=94=20disca?= =?UTF-8?q?rded=5Ffutures=20hygiene=20+=20high-pattern=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- analysis_options.yaml | 1 + assets/languages/strings_de.arb | 18 +- assets/languages/strings_en.arb | 18 +- .../config/legal_documents_config.dart | 16 +- .../service/transaction_history_service.dart | 1 + lib/screens/buy/buy_page.dart | 18 +- .../buy/widgets/payment_converter.dart | 57 +++--- .../create_wallet/create_wallet_view.dart | 6 +- lib/screens/dashboard/bloc/balance_cubit.dart | 2 +- .../dashboard_transaction_history_cubit.dart | 2 +- .../bloc/pending_transactions_cubit.dart | 3 +- lib/screens/debug_auth/debug_auth_view.dart | 8 +- .../bloc/connect_bitbox_cubit.dart | 2 +- lib/screens/home/bloc/home_bloc.dart | 4 +- lib/screens/kyc/kyc_page_manager.dart | 21 ++- lib/screens/kyc/steps/2fa/kyc_2fa_page.dart | 10 +- .../kyc/steps/email/kyc_email_page.dart | 6 +- .../kyc_financial_data_page.dart | 15 +- .../kyc/steps/ident/kyc_ident_page.dart | 12 +- .../link_wallet/kyc_link_wallet_page.dart | 4 +- .../nationality/kyc_nationality_page.dart | 12 +- .../registration/kyc_registration_page.dart | 15 +- .../legal/subpages/legal_document_page.dart | 17 +- .../pin/bloc/setup_pin/setup_pin_cubit.dart | 4 +- .../pin/bloc/verify_pin/verify_pin_cubit.dart | 4 +- lib/screens/pin/verify_pin_page.dart | 2 +- .../receive/widgets/qr_address_widget.dart | 8 + .../sell_balance/sell_balance_cubit.dart | 2 +- .../sell_bank_accounts_cubit.dart | 3 +- .../sell_converter/sell_converter_cubit.dart | 4 + lib/screens/sell/sell_page.dart | 10 +- .../widgets/sell_add_bank_account_sheet.dart | 14 +- lib/screens/sell/widgets/sell_converter.dart | 59 +++--- .../widgets/sell_bitbox_deposit_step.dart | 1 + .../settings_contact_page.dart | 6 +- .../bloc/settings_seed_cubit.dart | 6 +- .../settings_seed/settings_seed_view.dart | 6 +- .../cubit/settings_user_data_cubit.dart | 3 +- .../cubit/settings_edit_address_cubit.dart | 4 +- .../cubit/settings_edit_name_cubit.dart | 4 +- .../settings_edit_phone_number_page.dart | 4 +- .../support_chat/support_chat_cubit.dart | 3 +- .../support_tickets_cubit.dart | 3 +- .../support/subpages/support_chat_page.dart | 14 +- .../subpages/support_email_capture_page.dart | 12 +- .../transaction_history_filter_cubit.dart | 10 +- .../widgets/transaction_history_row.dart | 10 +- lib/setup/lifecycle_initializer.dart | 2 +- pubspec.lock | 2 +- pubspec.yaml | 3 + test/analysis_options.yaml | 17 ++ test/tool/pattern_guard_test.dart | 99 ++++++++++ tool/lints/pattern_guard.dart | 175 ++++++++++++++++++ 53 files changed, 588 insertions(+), 174 deletions(-) create mode 100644 test/analysis_options.yaml create mode 100644 test/tool/pattern_guard_test.dart create mode 100644 tool/lints/pattern_guard.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index b991a2181..11e7dd0f1 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 e3f215950..54b0cd9ef 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 56e4db7f0..bd63560a2 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 061765957..e18a7dfe5 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 18fdafbc2..b28dcba33 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 a6d6b9e9d..36c357f89 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 4f9d0f9c3..5323a5fae 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 4d9f174e2..90160de8f 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 c89d39de1..f5a099b31 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 b6b13bc62..ce2290769 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 7db47c801..d3c8d45d1 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 918cd668e..4dd444b5a 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 26cdc5a74..9b6ce9a47 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 0e97f15f7..6257c7e34 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 93694a3f2..604634c1a 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 cf3176725..70666ff1a 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 11f399fb2..7ea9d3de3 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 647b89da6..40c0c5c92 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 0339f9bb7..47983032d 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 79f82e729..89e14e981 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 bdb7c6328..545099576 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 e4ee5bff0..383574438 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 4994b7306..a471ed28b 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 11e77ac52..3a822a72e 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 8a43a36b5..c02d0ee83 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 f0af94a9d..3584a8764 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 677ba9000..197004686 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 dc80bd20f..c4eee03a1 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 71ac82b16..5698a1c85 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 93660b47b..d92ad072b 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 cf27a5dea..bf4b07613 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 9d1afccbf..7685b0206 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 8fed74c4e..30109a639 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 c13decdfd..7822460ea 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 04d20b407..c47511547 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 e7a804306..f4acc2320 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 25f5fef07..9a7fad456 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 0fc09b626..381b025d1 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 fc6c1a5e5..0785180f1 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 120761924..2d549f131 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 aaa2c99ca..121a0e33b 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 28e1d4342..137645a09 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 b71568244..603497998 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 2269cbf9b..02b2b57f0 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 21f91858a..c6540fe49 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 fbe743f15..6541a9b16 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 0051bbdfa..5f9736090 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 1c0f9a0ac..a75c436b8 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 bf626bd73..999f3f25a 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 9877d9640..8bbbd3f63 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 000000000..177d5d093 --- /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 000000000..9cdfdfa2c --- /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 000000000..b72f3224a --- /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); + } +}