diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 3f78bda33..5c8c800be 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -333,53 +333,57 @@ class _AddWalletViewState extends ConsumerState { borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), - child: TextField( - autofocus: isDesktop, - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: _searchFieldController, - focusNode: _searchFocusNode, - onChanged: (value) => setState(() => _searchTerm = value), - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, + child: Semantics( + label: "Search Text Field. Inputs Text To Search In Wallets.", + excludeSemantics: true, + child: TextField( + autofocus: isDesktop, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: (value) => setState(() => _searchTerm = value), + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: _searchFieldController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchFieldController.text = ""; - _searchTerm = ""; - }); - }, - ), - ], + suffixIcon: _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchFieldController.text = ""; + _searchTerm = ""; + }); + }, ), - ), - ) - : null, + ], + ), + ), + ) + : null, + ), ), - ), + ) ), const SizedBox( height: 10, diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index 6a3f28318..805355071 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -238,14 +238,22 @@ class _NameYourWalletViewState extends ConsumerState { TextFieldIconButton( key: const Key("genRandomWalletNameButtonKey"), child: _showDiceIcon - ? DiceIcon( - width: isDesktop ? 20 : 17, - height: isDesktop ? 20 : 17, - ) - : XIcon( + ? Semantics( + label: "Generate Random Wallet Name Button. Generates A Random Name For Wallet.", + excludeSemantics: true, + child: DiceIcon( + width: isDesktop ? 20 : 17, + height: isDesktop ? 20 : 17, + ), + ) + : Semantics( + label: "Generate Random Wallet Name Button. Generates A Random Name For Wallet.", + excludeSemantics: true, + child: XIcon( width: isDesktop ? 21 : 18, height: isDesktop ? 21 : 18, ), + ), onTap: () async { if (_showDiceIcon) { textEditingController.text = diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart index 0b358753d..5050324ba 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart @@ -140,6 +140,7 @@ class _NewWalletRecoveryPhraseViewState child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( + semanticsLabel: "Copy Button. Copies The Recovery Phrase To Clipboard.", color: Theme.of(context) .extension()! .background, diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 262d882d4..50c8bc1eb 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -87,6 +87,7 @@ class _NewWalletRecoveryPhraseWarningViewState right: 10, ), child: AppBarIconButton( + semanticsLabel: "Question Button. Opens A Dialog For Recovery Phrase Explanation.", icon: SvgPicture.asset( Assets.svg.circleQuestion, width: 20, diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index ec0d5bc2c..7c7ae4523 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -636,6 +636,7 @@ class _RestoreWalletViewState extends ConsumerState { child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( + semanticsLabel: "View QR Code Button. Opens Camera To Scan QR Code For Restoring Wallet.", key: const Key("restoreWalletViewQrCodeButton"), size: 36, shadows: const [], @@ -662,6 +663,7 @@ class _RestoreWalletViewState extends ConsumerState { child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( + semanticsLabel: "Paste Button. Pastes From Clipboard For Restoring Wallet.", key: const Key("restoreWalletPasteButton"), size: 36, shadows: const [], diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 11b4f4e51..389bedd05 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -781,31 +781,35 @@ class _ExchangeFormState extends ConsumerState { cursor: SystemMouseCursors.click, child: child, ), - child: RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(6) - : const EdgeInsets.all(2), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - radiusMultiplier: 0.75, - child: GestureDetector( - onTap: () async { - await _swap(); - }, - child: Padding( - padding: const EdgeInsets.all(4), - child: SvgPicture.asset( - Assets.svg.swap, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + child: Semantics( + label: "Swap Button. Reverse The Exchange Currencies.", + excludeSemantics: true, + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(6) + : const EdgeInsets.all(2), + color: Theme.of(context) + .extension()! + .buttonBackSecondary, + radiusMultiplier: 0.75, + child: GestureDetector( + onTap: () async { + await _swap(); + }, + child: Padding( + padding: const EdgeInsets.all(4), + child: SvgPicture.asset( + Assets.svg.swap, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), - ), + ) ), ], ), diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index ea9b29558..9e8f7fde1 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -191,6 +191,7 @@ class _HomeViewState extends ConsumerState { child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( + semanticsLabel: "Notifications Button. Takes To Notifications Page.", key: const Key("walletsViewAlertsButton"), size: 36, shadows: const [], @@ -254,6 +255,7 @@ class _HomeViewState extends ConsumerState { child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( + semanticsLabel: "Settings Button. Takes To Settings Page.", key: const Key("walletsViewSettingsButton"), size: 36, shadows: const [], diff --git a/lib/pages/pinpad_views/create_pin_view.dart b/lib/pages/pinpad_views/create_pin_view.dart index 3180689c2..d2b5a5ce1 100644 --- a/lib/pages/pinpad_views/create_pin_view.dart +++ b/lib/pages/pinpad_views/create_pin_view.dart @@ -9,7 +9,6 @@ import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/biometrics.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -139,6 +138,8 @@ class _CreatePinViewState extends ConsumerState { .background, counterText: "", ), + isRandom: + ref.read(prefsChangeNotifierProvider).randomizePIN, submittedFieldDecoration: _pinPutDecoration.copyWith( color: Theme.of(context) .extension()! @@ -221,6 +222,8 @@ class _CreatePinViewState extends ConsumerState { ), selectedFieldDecoration: _pinPutDecoration, followingFieldDecoration: _pinPutDecoration, + isRandom: + ref.read(prefsChangeNotifierProvider).randomizePIN, onSubmit: (String pin) async { // _onSubmitCount++; // if (_onSubmitCount - _onSubmitFailCount > 1) return; diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index 7e9f9b77f..316ef9b5d 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -21,8 +20,8 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_pin_put/custom_pin_put.dart'; -import 'package:stackwallet/widgets/custom_pin_put/pin_keyboard.dart'; import 'package:stackwallet/widgets/shake/shake.dart'; import 'package:tuple/tuple.dart'; @@ -39,6 +38,7 @@ class LockscreenView extends ConsumerStatefulWidget { this.routeOnSuccessArguments, this.biometrics = const Biometrics(), this.onSuccess, + this.customKeyLabel = "Button", }) : super(key: key); static const String routeName = "/lockscreen"; @@ -53,6 +53,8 @@ class LockscreenView extends ConsumerStatefulWidget { final String biometricsCancelButtonString; final Biometrics biometrics; final VoidCallback? onSuccess; + final String customKeyLabel; + @override ConsumerState createState() => _LockscreenViewState(); @@ -90,7 +92,7 @@ class _LockscreenViewState extends ConsumerState { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { + if (manager.coin == Coin.monero) { await showLoading( opaqueBG: true, whileFuture: manager.initializeExisting(), @@ -226,6 +228,26 @@ class _LockscreenViewState extends ConsumerState { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + // check prefs and hide if user has biometrics toggle off? + Padding( + padding: const EdgeInsets.only(right: 40.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (ref.read(prefsChangeNotifierProvider).useBiometrics == + true) + CustomTextButton( + text: "Use biometrics", + onTap: () async { + await _checkUseBiometrics(); + }, + ), + ], + ), + ), + const SizedBox( + height: 55, + ), Shake( animationDuration: const Duration(milliseconds: 700), animationRange: 12, @@ -244,12 +266,12 @@ class _LockscreenViewState extends ConsumerState { height: 52, ), CustomPinPut( - customKey: CustomKey( - onPressed: _checkUseBiometrics, - iconAssetName: Platform.isIOS - ? Assets.svg.faceId - : Assets.svg.fingerprint, - ), + // customKey: CustomKey( + // onPressed: _checkUseBiometrics, + // iconAssetName: Platform.isIOS + // ? Assets.svg.faceId + // : Assets.svg.fingerprint, + // ), fieldsCount: Constants.pinLength, eachFieldHeight: 12, eachFieldWidth: 12, @@ -285,6 +307,9 @@ class _LockscreenViewState extends ConsumerState { ), selectedFieldDecoration: _pinPutDecoration, followingFieldDecoration: _pinPutDecoration, + isRandom: ref + .read(prefsChangeNotifierProvider) + .randomizePIN, onSubmit: (String pin) async { _attempts++; diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 6890539ff..9800c9c38 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -143,6 +143,7 @@ class _ReceiveViewState extends ConsumerState { child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( + semanticsLabel: "Address List Pop-up Button. Opens A Pop-up For Adress List Button.", key: const Key("walletNetworkSettingsAddNewNodeViewButton"), size: 36, shadows: const [], diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index b3d2a6b8a..6f09a647f 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -981,6 +981,7 @@ class _SendViewState extends ConsumerState { children: [ _addressToggleFlag ? TextFieldIconButton( + semanticsLabel: "Clear Button. Clears The Address Field Input.", key: const Key( "sendViewClearAddressFieldButtonKey"), onTap: () { @@ -997,6 +998,7 @@ class _SendViewState extends ConsumerState { child: const XIcon(), ) : TextFieldIconButton( + semanticsLabel: "Paste Button. Pastes From Clipboard To Address Field Input.", key: const Key( "sendViewPasteAddressFieldButtonKey"), onTap: () async { @@ -1046,6 +1048,7 @@ class _SendViewState extends ConsumerState { ), if (sendToController.text.isEmpty) TextFieldIconButton( + semanticsLabel: "Address Book Button. Opens Address Book For Address Field.", key: const Key( "sendViewAddressBookButtonKey"), onTap: () { @@ -1058,6 +1061,7 @@ class _SendViewState extends ConsumerState { ), if (sendToController.text.isEmpty) TextFieldIconButton( + semanticsLabel: "Scan QR Button. Opens Camera For Scanning QR Code.", key: const Key( "sendViewScanQrButtonKey"), onTap: () async { diff --git a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart index fb5722594..cf848739f 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/security_views/security_view.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -123,6 +123,8 @@ class _ChangePinViewState extends ConsumerState { .background, counterText: "", ), + isRandom: + ref.read(prefsChangeNotifierProvider).randomizePIN, submittedFieldDecoration: _pinPutDecoration.copyWith( color: Theme.of(context) .extension()! @@ -188,6 +190,8 @@ class _ChangePinViewState extends ConsumerState { .background, counterText: "", ), + isRandom: + ref.read(prefsChangeNotifierProvider).randomizePIN, submittedFieldDecoration: _pinPutDecoration.copyWith( color: Theme.of(context) .extension()! diff --git a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart index c2a64bb50..74121b39c 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart @@ -146,6 +146,53 @@ class SecurityView extends StatelessWidget { }, ), ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Randomize PIN Pad", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.randomizePIN), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .randomizePIN = newValue; + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), ], ), ), diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 50b6e9768..42931c354 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -14,6 +14,8 @@ import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/dialogs/basic_dialog.dart'; import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -36,6 +38,36 @@ class _MyTokenSelectItemState extends ConsumerState { late final CachedEthTokenBalance cachedBalance; + Future _loadTokenWallet( + BuildContext context, + WidgetRef ref, + ) async { + try { + await ref.read(tokenServiceProvider)!.initialize(); + return true; + } catch (_) { + await showDialog( + barrierDismissible: false, + context: context, + builder: (context) => BasicDialog( + title: "Failed to load token data", + desktopHeight: double.infinity, + desktopWidth: 450, + rightButton: PrimaryButton( + label: "OK", + onPressed: () { + Navigator.of(context).pop(); + if (!isDesktop) { + Navigator.of(context).pop(); + } + }, + ), + ), + ); + return false; + } + } + void _onPressed() async { ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( token: widget.token, @@ -49,13 +81,17 @@ class _MyTokenSelectItemState extends ConsumerState { ), ); - await showLoading( - whileFuture: ref.read(tokenServiceProvider)!.initialize(), + final success = await showLoading( + whileFuture: _loadTokenWallet(context, ref), context: context, isDesktop: isDesktop, message: "Loading ${widget.token.name}", ); + if (!success) { + return; + } + if (mounted) { await Navigator.of(context).pushNamed( isDesktop ? DesktopTokenView.routeName : TokenView.routeName, diff --git a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart index a4b6a34de..06985cd7c 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart @@ -99,56 +99,60 @@ class _RefreshButtonState extends ConsumerState { return SizedBox( height: isDesktop ? 22 : 36, width: isDesktop ? 22 : 36, - child: MaterialButton( - color: isDesktop - ? Theme.of(context).extension()!.buttonBackSecondary - : null, - splashColor: Theme.of(context).extension()!.highlight, - onPressed: () { - if (widget.tokenContractAddress == null) { - final managerProvider = ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(widget.walletId); - final isRefreshing = ref.read(managerProvider).isRefreshing; - if (!isRefreshing) { - _spinController.repeat?.call(); - ref - .read(managerProvider) - .refresh() - .then((_) => _spinController.stop?.call()); + child: Semantics( + label: "Refresh Button. Refreshes The Values In Summary.", + excludeSemantics: true, + child: MaterialButton( + color: isDesktop + ? Theme.of(context).extension()!.buttonBackSecondary + : null, + splashColor: Theme.of(context).extension()!.highlight, + onPressed: () { + if (widget.tokenContractAddress == null) { + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(widget.walletId); + final isRefreshing = ref.read(managerProvider).isRefreshing; + if (!isRefreshing) { + _spinController.repeat?.call(); + ref + .read(managerProvider) + .refresh() + .then((_) => _spinController.stop?.call()); + } + } else { + if (!ref.read(tokenServiceProvider)!.isRefreshing) { + ref.read(tokenServiceProvider)!.refresh(); + } } - } else { - if (!ref.read(tokenServiceProvider)!.isRefreshing) { - ref.read(tokenServiceProvider)!.refresh(); - } - } - }, - elevation: 0, - highlightElevation: 0, - hoverElevation: 0, - padding: EdgeInsets.zero, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + }, + elevation: 0, + highlightElevation: 0, + hoverElevation: 0, + padding: EdgeInsets.zero, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: RotatingArrows( + spinByDefault: widget.initialSyncStatus == WalletSyncStatus.syncing, + width: isDesktop ? 12 : 24, + height: isDesktop ? 12 : 24, + controller: _spinController, + color: widget.overrideIconColor != null + ? widget.overrideIconColor! + : isDesktop + ? Theme.of(context) + .extension()! + .textFieldDefaultSearchIconRight + : Theme.of(context) + .extension()! + .textFavoriteCard, ), ), - child: RotatingArrows( - spinByDefault: widget.initialSyncStatus == WalletSyncStatus.syncing, - width: isDesktop ? 12 : 24, - height: isDesktop ? 12 : 24, - controller: _spinController, - color: widget.overrideIconColor != null - ? widget.overrideIconColor! - : isDesktop - ? Theme.of(context) - .extension()! - .textFieldDefaultSearchIconRight - : Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), + ) ); } } diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index b19d291fa..b70240c3c 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -53,6 +53,7 @@ import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart'; @@ -420,6 +421,33 @@ class _WalletViewState extends ConsumerState { eventBus: null, textColor: Theme.of(context).extension()!.textDark, + actionButton: SecondaryButton( + label: "Cancel", + onPressed: () async { + await showDialog( + context: context, + builder: (context) => StackDialog( + title: "Warning!", + message: "Skipping this process can completely" + " break your wallet. It is only meant to be done in" + " emergency situations where the migration fails" + " and will not let you continue. Still skip?", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: + Navigator.of(context, rootNavigator: true).pop, + ), + rightButton: SecondaryButton( + label: "Ok", + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + setState(() => _rescanningOnOpen = false); + }, + ), + ), + ); + }, + ), ), ) ], @@ -473,6 +501,7 @@ class _WalletViewState extends ConsumerState { child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( + semanticsLabel: "Network Button. Takes To Network Status Page.", key: const Key("walletViewRadioButton"), size: 36, shadows: const [], @@ -502,6 +531,7 @@ class _WalletViewState extends ConsumerState { child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( + semanticsLabel: "Notifications Button. Takes To Notifications Page.", key: const Key("walletViewAlertsButton"), size: 36, shadows: const [], @@ -569,6 +599,7 @@ class _WalletViewState extends ConsumerState { child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( + semanticsLabel: "Settings Button. Takes To Wallet Settings Page.", key: const Key("walletViewSettingsButton"), size: 36, shadows: const [], @@ -790,7 +821,6 @@ class _WalletViewState extends ConsumerState { label: "Receive", icon: const ReceiveNavIcon(), onTap: () { - final coin = ref.read(managerProvider).coin; if (mounted) { unawaited( Navigator.of(context).pushNamed( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 512774df8..0d0c408c1 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -31,7 +31,11 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/hover_text_field.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -150,6 +154,83 @@ class _DesktopWalletViewState extends ConsumerState { subMessage: "This only needs to run once per wallet", eventBus: null, textColor: Theme.of(context).extension()!.textDark, + actionButton: SecondaryButton( + label: "Skip", + buttonHeight: ButtonHeight.l, + onPressed: () async { + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 500, + maxHeight: double.infinity, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Warning!", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32), + child: Text( + "Skipping this process can completely" + " break your wallet. It is only meant to be done in" + " emergency situations where the migration fails" + " and will not let you continue. Still skip?", + style: STextStyles.desktopTextSmall(context), + ), + ), + const SizedBox( + height: 32, + ), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context, + rootNavigator: true) + .pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context, + rootNavigator: true) + .pop(); + setState( + () => _rescanningOnOpen = false); + }, + ), + ), + ], + ), + ) + ], + ), + ), + ); + }, + ), ), ) ], diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 7903927ae..e108352b4 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -1949,8 +1949,6 @@ class BitcoinCashWallet extends CoinServiceAPI List> allTransactions = []; - final currentHeight = await chainHeight; - for (final txHash in allTxHashes) { final storedTx = await db .getTransactions(walletId) @@ -1958,7 +1956,9 @@ class BitcoinCashWallet extends CoinServiceAPI .txidEqualTo(txHash["tx_hash"] as String) .findFirst(); - if (storedTx == null || storedTx.address.value == null + if (storedTx == null || + storedTx.address.value == null || + storedTx.height == null // zero conf messes this up // !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS) ) { diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 624b6b59a..c7007c905 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -253,13 +253,17 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { ); _credentials = web3dart.EthPrivateKey.fromHex(privateKey); - _deployedContract = web3dart.DeployedContract( - ContractAbiExtensions.fromJsonList( - jsonList: tokenContract.abi!, - name: tokenContract.name, - ), - contractAddress, - ); + try { + _deployedContract = web3dart.DeployedContract( + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), + contractAddress, + ); + } catch (_) { + rethrow; + } try { _sendFunction = _deployedContract.function('transfer'); @@ -328,13 +332,17 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { //==================================================================== } - _deployedContract = web3dart.DeployedContract( - ContractAbiExtensions.fromJsonList( - jsonList: tokenContract.abi!, - name: tokenContract.name, - ), - contractAddress, - ); + try { + _deployedContract = web3dart.DeployedContract( + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), + contractAddress, + ); + } catch (_) { + rethrow; + } _sendFunction = _deployedContract.function('transfer'); diff --git a/lib/utilities/extensions/impl/contract_abi.dart b/lib/utilities/extensions/impl/contract_abi.dart index c5a2877d8..61827b32d 100644 --- a/lib/utilities/extensions/impl/contract_abi.dart +++ b/lib/utilities/extensions/impl/contract_abi.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:stackwallet/utilities/logger.dart'; import 'package:web3dart/web3dart.dart'; extension ContractAbiExtensions on ContractAbi { @@ -7,50 +8,61 @@ extension ContractAbiExtensions on ContractAbi { required String name, required String jsonList, }) { - final List functions = []; - final List events = []; + try { + final List functions = []; + final List events = []; - final list = List>.from(jsonDecode(jsonList) as List); + final list = + List>.from(jsonDecode(jsonList) as List); - for (final json in list) { - final type = json["type"] as String; - final name = json["name"] as String? ?? ""; + for (final json in list) { + final type = json["type"] as String; + final name = json["name"] as String? ?? ""; - if (type == "event") { - final anonymous = json["anonymous"] as bool? ?? false; - final List> components = []; + if (type == "event") { + final anonymous = json["anonymous"] as bool? ?? false; + final List> components = []; - for (final input in json["inputs"] as List) { - components.add( - EventComponent( - _parseParam(input as Map), - input['indexed'] as bool? ?? false, - ), - ); - } + if (json["inputs"] is List) { + for (final input in json["inputs"] as List) { + components.add( + EventComponent( + _parseParam(input as Map), + input['indexed'] as bool? ?? false, + ), + ); + } + } - events.add(ContractEvent(anonymous, name, components)); - } else { - final mutability = _mutabilityNames[json['stateMutability']]; - final parsedType = _functionTypeNames[json['type']]; - if (parsedType != null) { - final inputs = _parseParams(json['inputs'] as List?); - final outputs = _parseParams(json['outputs'] as List?); + events.add(ContractEvent(anonymous, name, components)); + } else { + final mutability = _mutabilityNames[json['stateMutability']]; + final parsedType = _functionTypeNames[json['type']]; + if (parsedType != null) { + final inputs = _parseParams(json['inputs'] as List?); + final outputs = _parseParams(json['outputs'] as List?); - functions.add( - ContractFunction( - name, - inputs, - outputs: outputs, - type: parsedType, - mutability: mutability ?? StateMutability.nonPayable, - ), - ); + functions.add( + ContractFunction( + name, + inputs, + outputs: outputs, + type: parsedType, + mutability: mutability ?? StateMutability.nonPayable, + ), + ); + } } } - } - return ContractAbi(name, functions, events); + return ContractAbi(name, functions, events); + } catch (e, s) { + Logging.instance.log( + "Failed to parse ABI for $name: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } } static const Map _functionTypeNames = { diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 7dd17e2ad..973541366 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -19,6 +19,7 @@ class Prefs extends ChangeNotifier { if (!_initialized) { _currency = await _getPreferredCurrency(); // _exchangeRateType = await _getExchangeRateType(); + _randomizePIN = await _getRandomizePIN(); _useBiometrics = await _getUseBiometrics(); _hasPin = await _getHasPin(); _language = await _getPreferredLanguage(); @@ -295,6 +296,27 @@ class Prefs extends ChangeNotifier { // } // } + // randomize PIN + + bool _randomizePIN = false; + + bool get randomizePIN => _randomizePIN; + + set randomizePIN(bool randomizePIN) { + if (_randomizePIN != randomizePIN) { + DB.instance.put( + boxName: DB.boxNamePrefs, key: "randomizePIN", value: randomizePIN); + _randomizePIN = randomizePIN; + notifyListeners(); + } + } + + Future _getRandomizePIN() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, key: "randomizePIN") as bool? ?? + false; + } + // use biometrics bool _useBiometrics = false; diff --git a/lib/widgets/custom_buttons/app_bar_icon_button.dart b/lib/widgets/custom_buttons/app_bar_icon_button.dart index 9edc1ca5f..71d8910d0 100644 --- a/lib/widgets/custom_buttons/app_bar_icon_button.dart +++ b/lib/widgets/custom_buttons/app_bar_icon_button.dart @@ -13,6 +13,7 @@ class AppBarIconButton extends StatelessWidget { // this.circularBorderRadius = 10.0, this.size = 36.0, this.shadows = const [], + this.semanticsLabel = "Button", }) : super(key: key); final Widget icon; @@ -21,6 +22,7 @@ class AppBarIconButton extends StatelessWidget { // final double circularBorderRadius; final double size; final List shadows; + final String semanticsLabel; @override Widget build(BuildContext context) { @@ -32,16 +34,20 @@ class AppBarIconButton extends StatelessWidget { color: color ?? Theme.of(context).extension()!.background, boxShadow: shadows, ), - child: MaterialButton( - splashColor: Theme.of(context).extension()!.highlight, - padding: EdgeInsets.zero, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(1000), + child: Semantics( + excludeSemantics: true, + label: semanticsLabel, + child: MaterialButton( + splashColor: Theme.of(context).extension()!.highlight, + padding: EdgeInsets.zero, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + onPressed: onPressed, + child: icon, ), - onPressed: onPressed, - child: icon, - ), + ) ); } } @@ -53,12 +59,14 @@ class AppBarBackButton extends StatelessWidget { this.isCompact = false, this.size, this.iconSize, + this.semanticsLabel = "Back Button. Takes Back To Previous Page.", }) : super(key: key); final VoidCallback? onPressed; final bool isCompact; final double? size; final double? iconSize; + final String semanticsLabel; @override Widget build(BuildContext context) { @@ -71,24 +79,25 @@ class AppBarBackButton extends StatelessWidget { ) : const EdgeInsets.all(10), child: AppBarIconButton( - size: size ?? - (isDesktop - ? isCompact - ? 42 - : 56 - : 32), - color: isDesktop - ? Theme.of(context).extension()!.textFieldDefaultBG - : Theme.of(context).extension()!.background, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.arrowLeft, - width: iconSize ?? (isCompact ? 18 : 24), - height: iconSize ?? (isCompact ? 18 : 24), - color: Theme.of(context).extension()!.topNavIconPrimary, - ), - onPressed: onPressed ?? Navigator.of(context).pop, - ), + semanticsLabel: semanticsLabel, + size: size ?? + (isDesktop + ? isCompact + ? 42 + : 56 + : 32), + color: isDesktop + ? Theme.of(context).extension()!.textFieldDefaultBG + : Theme.of(context).extension()!.background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: iconSize ?? (isCompact ? 18 : 24), + height: iconSize ?? (isCompact ? 18 : 24), + color: Theme.of(context).extension()!.topNavIconPrimary, + ), + onPressed: onPressed ?? Navigator.of(context).pop, + ) ); } } diff --git a/lib/widgets/custom_loading_overlay.dart b/lib/widgets/custom_loading_overlay.dart index e05158bc4..e2669d054 100644 --- a/lib/widgets/custom_loading_overlay.dart +++ b/lib/widgets/custom_loading_overlay.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; class CustomLoadingOverlay extends ConsumerStatefulWidget { @@ -14,12 +16,14 @@ class CustomLoadingOverlay extends ConsumerStatefulWidget { this.subMessage, required this.eventBus, this.textColor, + this.actionButton, }) : super(key: key); final String message; final String? subMessage; final EventBus? eventBus; final Color? textColor; + final Widget? actionButton; @override ConsumerState createState() => @@ -30,6 +34,7 @@ class _CustomLoadingOverlayState extends ConsumerState { double _percent = 0; late final StreamSubscription? subscription; + final bool isDesktop = Util.isDesktop; @override void initState() { @@ -49,68 +54,97 @@ class _CustomLoadingOverlayState extends ConsumerState { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Column( - children: [ - Text( - widget.message, - textAlign: TextAlign.center, - style: STextStyles.pageTitleH2(context).copyWith( - color: widget.textColor ?? - Theme.of(context) - .extension()! - .loadingOverlayTextColor, + return Material( + color: Colors.transparent, + child: ConditionalParent( + condition: widget.actionButton != null, + builder: (child) => Stack( + children: [ + child, + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: SizedBox( + width: 100, + child: widget.actionButton!, + ), + ), + ], + ), + if (!isDesktop) + Positioned( + bottom: 1, + left: 0, + right: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded(child: widget.actionButton!), + ], ), ), - if (widget.eventBus != null) - const SizedBox( - height: 10, - ), - if (widget.eventBus != null) - Text( - "${(_percent * 100).toStringAsFixed(2)}%", - style: STextStyles.pageTitleH2(context).copyWith( - color: widget.textColor ?? - Theme.of(context) - .extension()! - .loadingOverlayTextColor, - ), - ), - if (widget.subMessage != null) - const SizedBox( - height: 10, - ), - if (widget.subMessage != null) - Text( - widget.subMessage!, - textAlign: TextAlign.center, - style: STextStyles.pageTitleH2(context).copyWith( - fontSize: 14, - color: widget.textColor ?? - Theme.of(context) - .extension()! - .loadingOverlayTextColor, - ), - ) - ], + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.message, + textAlign: TextAlign.center, + style: STextStyles.pageTitleH2(context).copyWith( + color: widget.textColor ?? + Theme.of(context) + .extension()! + .loadingOverlayTextColor, + ), ), - ), + if (widget.eventBus != null) + const SizedBox( + height: 10, + ), + if (widget.eventBus != null) + Text( + "${(_percent * 100).toStringAsFixed(2)}%", + style: STextStyles.pageTitleH2(context).copyWith( + color: widget.textColor ?? + Theme.of(context) + .extension()! + .loadingOverlayTextColor, + ), + ), + if (widget.subMessage != null) + const SizedBox( + height: 10, + ), + if (widget.subMessage != null) + Text( + widget.subMessage!, + textAlign: TextAlign.center, + style: STextStyles.pageTitleH2(context).copyWith( + fontSize: 14, + color: widget.textColor ?? + Theme.of(context) + .extension()! + .loadingOverlayTextColor, + ), + ), + const SizedBox( + height: 64, + ), + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, - ), - ), - ], + ), ); } } diff --git a/lib/widgets/custom_pin_put/custom_pin_put.dart b/lib/widgets/custom_pin_put/custom_pin_put.dart index ca7b28581..4a4e4a4e4 100644 --- a/lib/widgets/custom_pin_put/custom_pin_put.dart +++ b/lib/widgets/custom_pin_put/custom_pin_put.dart @@ -7,6 +7,7 @@ class CustomPinPut extends StatefulWidget { const CustomPinPut({ Key? key, required this.fieldsCount, + required this.isRandom, this.height, this.width, this.onSubmit, @@ -60,6 +61,8 @@ class CustomPinPut extends StatefulWidget { final CustomKey? customKey; + final bool isRandom; + /// Displayed fields count. PIN code length. final int fieldsCount; diff --git a/lib/widgets/custom_pin_put/custom_pin_put_state.dart b/lib/widgets/custom_pin_put/custom_pin_put_state.dart index 0413aef92..355656638 100644 --- a/lib/widgets/custom_pin_put/custom_pin_put_state.dart +++ b/lib/widgets/custom_pin_put/custom_pin_put_state.dart @@ -32,9 +32,9 @@ class CustomPinPutState extends State } catch (e) { _textControllerValue = ValueNotifier(_controller.value.text); } - if (pin.length == widget.fieldsCount) { - widget.onSubmit?.call(pin); - } + // if (pin.length == widget.fieldsCount) { + // widget.onSubmit?.call(pin); + // } } } @@ -50,6 +50,9 @@ class CustomPinPutState extends State @override Widget build(BuildContext context) { + // final bool randomize = ref + // .read(prefsChangeNotifierProvider) + // .randomizePIN; return SizedBox( width: widget.width, height: widget.height, @@ -69,6 +72,7 @@ class CustomPinPutState extends State ), Center( child: PinKeyboard( + isRandom: widget.isRandom, customKey: widget.customKey, onNumberKeyPressed: (number) { if (_controller.text.length < widget.fieldsCount) { diff --git a/lib/widgets/custom_pin_put/pin_keyboard.dart b/lib/widgets/custom_pin_put/pin_keyboard.dart index 1ec611a80..6b100a2f9 100644 --- a/lib/widgets/custom_pin_put/pin_keyboard.dart +++ b/lib/widgets/custom_pin_put/pin_keyboard.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -140,15 +141,19 @@ class _BackspaceKeyState extends State { } }); }, - child: Center( - child: SvgPicture.asset( - Assets.svg.delete, - width: 20, - height: 20, - color: - Theme.of(context).extension()!.numpadTextDefault, + child: Semantics( + label: "Backspace Button. Deletes The Last Digit.", + excludeSemantics: true, + child: Center( + child: SvgPicture.asset( + Assets.svg.delete, + width: 20, + height: 20, + color: + Theme.of(context).extension()!.numpadTextDefault, + ), ), - ), + ) ), ); } @@ -198,10 +203,12 @@ class CustomKey extends StatelessWidget { Key? key, required this.onPressed, this.iconAssetName, + this.semanticsLabel = "Button", }) : super(key: key); final VoidCallback onPressed; final String? iconAssetName; + final String semanticsLabel; @override Widget build(BuildContext context) { @@ -213,32 +220,197 @@ class CustomKey extends StatelessWidget { color: Theme.of(context).extension()!.numpadBackDefault, shadows: const [], ), - child: MaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: const StadiumBorder(), - onPressed: () { - onPressed.call(); - }, - child: Center( - child: iconAssetName == null - ? null - : SvgPicture.asset( - iconAssetName!, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .numpadTextDefault, - ), + child: Semantics( + label: semanticsLabel, + excludeSemantics: true, + child: MaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: const StadiumBorder(), + onPressed: () { + onPressed.call(); + }, + child: Center( + child: iconAssetName == null + ? null + : SvgPicture.asset( + iconAssetName!, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .numpadTextDefault, + ), + ), ), + ) + ); + } +} + +class PinKeyboard extends ConsumerWidget { + const PinKeyboard({ + Key? key, + required this.onNumberKeyPressed, + required this.onBackPressed, + required this.onSubmitPressed, + required this.isRandom, + this.backgroundColor, + this.width = 264, + this.height = 360, + this.customKey, + }) : super(key: key); + + final ValueSetter onNumberKeyPressed; + final VoidCallback onBackPressed; + final VoidCallback onSubmitPressed; + final Color? backgroundColor; + final double? width; + final double? height; + final CustomKey? customKey; + final bool isRandom; + + void _backHandler() { + onBackPressed.call(); + } + + void _submitHandler() { + onSubmitPressed.call(); + } + + void _numberHandler(String number) { + onNumberKeyPressed.call(number); + HapticFeedback.lightImpact(); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final list = [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + ]; + + // final isRandom = ref.read(prefsChangeNotifierProvider).randomizePIN; + + if (isRandom) list.shuffle(); + + return Container( + width: width, + height: height, + color: backgroundColor ?? Colors.transparent, + child: Column( + children: [ + Row( + children: [ + NumberKey( + number: list[0], + onPressed: _numberHandler, + ), + const SizedBox( + width: 24, + ), + NumberKey( + number: list[1], + onPressed: _numberHandler, + ), + const SizedBox( + width: 24, + ), + NumberKey( + number: list[2], + onPressed: _numberHandler, + ), + ], + ), + const SizedBox( + height: 24, + ), + Row( + children: [ + NumberKey( + number: list[3], + onPressed: _numberHandler, + ), + const SizedBox( + width: 24, + ), + NumberKey( + number: list[4], + onPressed: _numberHandler, + ), + const SizedBox( + width: 24, + ), + NumberKey( + number: list[5], + onPressed: _numberHandler, + ), + ], + ), + const SizedBox( + height: 24, + ), + Row( + children: [ + NumberKey( + number: list[6], + onPressed: _numberHandler, + ), + const SizedBox( + width: 24, + ), + NumberKey( + number: list[7], + onPressed: _numberHandler, + ), + const SizedBox( + width: 24, + ), + NumberKey( + number: list[8], + onPressed: _numberHandler, + ), + ], + ), + const SizedBox( + height: 24, + ), + Row( + children: [ + BackspaceKey( + onPressed: _backHandler, + ), + const SizedBox( + width: 24, + ), + NumberKey( + number: list[9], + onPressed: _numberHandler, + ), + const SizedBox( + width: 24, + ), + SubmitKey( + onPressed: _submitHandler, + ), + ], + ) + ], ), ); } } -class PinKeyboard extends StatelessWidget { - const PinKeyboard({ +class RandomKeyboard extends StatelessWidget { + const RandomKeyboard({ Key? key, required this.onNumberKeyPressed, required this.onBackPressed, @@ -268,10 +440,24 @@ class PinKeyboard extends StatelessWidget { void _numberHandler(String number) { onNumberKeyPressed.call(number); HapticFeedback.lightImpact(); + debugPrint("NUMBER: $number"); } @override Widget build(BuildContext context) { + final list = [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + ]; + list.shuffle(); return Container( width: width, height: height, @@ -281,21 +467,21 @@ class PinKeyboard extends StatelessWidget { Row( children: [ NumberKey( - number: "1", + number: list[0], onPressed: _numberHandler, ), const SizedBox( width: 24, ), NumberKey( - number: "2", + number: list[1], onPressed: _numberHandler, ), const SizedBox( width: 24, ), NumberKey( - number: "3", + number: list[2], onPressed: _numberHandler, ), ], @@ -306,21 +492,21 @@ class PinKeyboard extends StatelessWidget { Row( children: [ NumberKey( - number: "4", + number: list[3], onPressed: _numberHandler, ), const SizedBox( width: 24, ), NumberKey( - number: "5", + number: list[4], onPressed: _numberHandler, ), const SizedBox( width: 24, ), NumberKey( - number: "6", + number: list[5], onPressed: _numberHandler, ), ], @@ -331,21 +517,21 @@ class PinKeyboard extends StatelessWidget { Row( children: [ NumberKey( - number: "7", + number: list[6], onPressed: _numberHandler, ), const SizedBox( width: 24, ), NumberKey( - number: "8", + number: list[7], onPressed: _numberHandler, ), const SizedBox( width: 24, ), NumberKey( - number: "9", + number: list[8], onPressed: _numberHandler, ), ], @@ -355,28 +541,22 @@ class PinKeyboard extends StatelessWidget { ), Row( children: [ - customKey == null - ? const SizedBox( - height: 72, - width: 72, - ) - : customKey!, - const SizedBox( - width: 24, - ), - NumberKey( - number: "0", - onPressed: _numberHandler, - ), - const SizedBox( - width: 24, - ), BackspaceKey( onPressed: _backHandler, ), - // SubmitKey( - // onPressed: _submitHandler, - // ), + const SizedBox( + width: 24, + ), + NumberKey( + number: list[9], + onPressed: _numberHandler, + ), + const SizedBox( + width: 24, + ), + SubmitKey( + onPressed: _submitHandler, + ), ], ) ], diff --git a/lib/widgets/dialogs/basic_dialog.dart b/lib/widgets/dialogs/basic_dialog.dart new file mode 100644 index 000000000..f6dab30f0 --- /dev/null +++ b/lib/widgets/dialogs/basic_dialog.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class BasicDialog extends StatelessWidget { + const BasicDialog({ + Key? key, + this.leftButton, + this.rightButton, + this.icon, + required this.title, + this.message, + this.desktopHeight = 474, + this.desktopWidth = 641, + this.canPopWithBackButton = false, + }) : super(key: key); + + final Widget? leftButton; + final Widget? rightButton; + + final Widget? icon; + + final String title; + final String? message; + + final double? desktopHeight; + final double desktopWidth; + + final bool canPopWithBackButton; + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + if (isDesktop) { + return DesktopDialog( + maxHeight: desktopHeight, + maxWidth: desktopWidth, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + ), + if (message != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + message!, + style: STextStyles.desktopTextSmall(context), + ), + ), + if (leftButton != null || rightButton != null) + const SizedBox( + height: 32, + ), + if (leftButton != null || rightButton != null) + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + leftButton != null + ? Expanded(child: leftButton!) + : const Spacer(), + const SizedBox( + width: 16, + ), + rightButton != null + ? Expanded(child: rightButton!) + : const Spacer(), + ], + ), + ) + ], + ), + ); + } else { + return WillPopScope( + onWillPop: () async { + return canPopWithBackButton; + }, + child: StackDialog( + title: title, + leftButton: leftButton, + rightButton: rightButton, + icon: icon, + message: message, + ), + ); + } + } +} diff --git a/lib/widgets/textfield_icon_button.dart b/lib/widgets/textfield_icon_button.dart index 9375c6f31..df231a781 100644 --- a/lib/widgets/textfield_icon_button.dart +++ b/lib/widgets/textfield_icon_button.dart @@ -8,6 +8,7 @@ class TextFieldIconButton extends StatefulWidget { this.onTap, required this.child, this.color = Colors.transparent, + this.semanticsLabel = "Button", }) : super(key: key); final double width; @@ -15,6 +16,7 @@ class TextFieldIconButton extends StatefulWidget { final VoidCallback? onTap; final Widget child; final Color color; + final String semanticsLabel; @override State createState() => _TextFieldIconButtonState(); @@ -36,21 +38,25 @@ class _TextFieldIconButtonState extends State { width: widget.width, child: ClipRRect( borderRadius: BorderRadius.circular(100), - child: RawMaterialButton( - constraints: BoxConstraints( - minWidth: widget.width, - minHeight: widget.height, - ), - onPressed: onTap, - child: Container( - width: widget.width, - height: widget.height, - color: widget.color, - child: Center( - child: widget.child, + child: Semantics( + label: widget.semanticsLabel, + excludeSemantics: true, + child: RawMaterialButton( + constraints: BoxConstraints( + minWidth: widget.width, + minHeight: widget.height, + ), + onPressed: onTap, + child: Container( + width: widget.width, + height: widget.height, + color: widget.color, + child: Center( + child: widget.child, + ), ), ), - ), + ) ), ); } diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 399ae6211..a5130dd1e 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -19,6 +19,8 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/dialogs/basic_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; import 'package:tuple/tuple.dart'; @@ -37,7 +39,7 @@ class SimpleWalletCard extends ConsumerWidget { final bool popPrevious; final NavigatorState? desktopNavigatorState; - Future _loadTokenWallet( + Future _loadTokenWallet( BuildContext context, WidgetRef ref, Manager manager, @@ -52,7 +54,31 @@ class SimpleWalletCard extends ConsumerWidget { ), ); - await ref.read(tokenServiceProvider)!.initialize(); + try { + await ref.read(tokenServiceProvider)!.initialize(); + return true; + } catch (_) { + await showDialog( + barrierDismissible: false, + context: context, + builder: (context) => BasicDialog( + title: "Failed to load token data", + desktopHeight: double.infinity, + desktopWidth: 450, + rightButton: PrimaryButton( + label: "OK", + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + if (desktopNavigatorState == null) { + Navigator.of(context).pop(); + } + }, + ), + ), + ); + return false; + } } void _openWallet(BuildContext context, WidgetRef ref) async { @@ -91,13 +117,21 @@ class SimpleWalletCard extends ConsumerWidget { final contract = ref.read(mainDBProvider).getEthContractSync(contractAddress!)!; - await showLoading( - whileFuture: _loadTokenWallet(context, ref, manager, contract), - context: context, + final success = await showLoading( + whileFuture: _loadTokenWallet( + desktopNavigatorState?.context ?? context, + ref, + manager, + contract), + context: desktopNavigatorState?.context ?? context, opaqueBG: true, message: "Loading ${contract.name}", ); + if (!success) { + return; + } + if (desktopNavigatorState == null) { // pop loading nav.pop(); diff --git a/pubspec.yaml b/pubspec.yaml index d84d0d15d..96d1ae623 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.7.4+164 +version: 1.7.4+165 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/test/widget_tests/custom_pin_put_test.dart b/test/widget_tests/custom_pin_put_test.dart index d3a449865..ad472707e 100644 --- a/test/widget_tests/custom_pin_put_test.dart +++ b/test/widget_tests/custom_pin_put_test.dart @@ -7,9 +7,13 @@ import 'package:stackwallet/widgets/custom_pin_put/custom_pin_put.dart'; import 'package:stackwallet/widgets/custom_pin_put/pin_keyboard.dart'; void main() { - group("CustomPinPut tests", () { - testWidgets("CustomPinPut with 4 fields builds correctly", (tester) async { - const pinPut = CustomPinPut(fieldsCount: 4); + group("CustomPinPut tests, non-random PIN", () { + testWidgets("CustomPinPut with 4 fields builds correctly, non-random PIN", + (tester) async { + const pinPut = CustomPinPut( + fieldsCount: 4, + isRandom: false, + ); await tester.pumpWidget( MaterialApp( @@ -31,12 +35,17 @@ void main() { expect(find.byType(NumberKey), findsNWidgets(10)); }); - testWidgets("CustomPinPut entering a pin successfully", (tester) async { + testWidgets("CustomPinPut entering a pin successfully, non-random PIN", + (tester) async { bool submittedPinMatches = false; final pinPut = CustomPinPut( fieldsCount: 4, - onSubmit: (pin) => submittedPinMatches = pin == "1234", + onSubmit: (pin) { + submittedPinMatches = pin == "1234"; + print("pin entered: $pin"); + }, useNativeKeyboard: false, + isRandom: false, ); await tester.pumpWidget( @@ -69,16 +78,20 @@ void main() { await tester.tap(find.byWidgetPredicate( (widget) => widget is NumberKey && widget.number == "4")); await tester.pumpAndSettle(); + await tester.tap(find.byType(SubmitKey)); + await tester.pumpAndSettle(); expect(submittedPinMatches, true); }); - testWidgets("CustomPinPut pin enter fade animation", (tester) async { + testWidgets("CustomPinPut pin enter fade animation, non-random PIN", + (tester) async { final controller = TextEditingController(); final pinPut = CustomPinPut( fieldsCount: 4, pinAnimationType: PinAnimationType.fade, controller: controller, + isRandom: false, ); await tester.pumpWidget( @@ -104,12 +117,14 @@ void main() { expect(controller.text, ""); }); - testWidgets("CustomPinPut pin enter scale animation", (tester) async { + testWidgets("CustomPinPut pin enter scale animation, non-random PIN", + (tester) async { final controller = TextEditingController(); final pinPut = CustomPinPut( fieldsCount: 4, pinAnimationType: PinAnimationType.scale, controller: controller, + isRandom: false, ); await tester.pumpWidget( @@ -135,12 +150,14 @@ void main() { expect(controller.text, ""); }); - testWidgets("CustomPinPut pin enter rotate animation", (tester) async { + testWidgets("CustomPinPut pin enter rotate animation, non-random PIN", + (tester) async { final controller = TextEditingController(); final pinPut = CustomPinPut( fieldsCount: 4, pinAnimationType: PinAnimationType.rotation, controller: controller, + isRandom: false, ); await tester.pumpWidget( @@ -167,11 +184,12 @@ void main() { }); }); - testWidgets("PinKeyboard builds correctly", (tester) async { + testWidgets("PinKeyboard builds correctly, non-random PIN", (tester) async { final keyboard = PinKeyboard( onNumberKeyPressed: (_) {}, onBackPressed: () {}, onSubmitPressed: () {}, + isRandom: false, ); await tester.pumpWidget( @@ -188,6 +206,7 @@ void main() { ); expect(find.byType(BackspaceKey), findsOneWidget); + expect(find.byType(SubmitKey), findsOneWidget); expect(find.byType(NumberKey), findsNWidgets(10)); expect(find.text("0"), findsOneWidget); expect(find.text("1"), findsOneWidget); @@ -199,6 +218,220 @@ void main() { expect(find.text("7"), findsOneWidget); expect(find.text("8"), findsOneWidget); expect(find.text("9"), findsOneWidget); - expect(find.byType(SvgPicture), findsOneWidget); + expect(find.byType(SvgPicture), findsNWidgets(2)); + }); + + group("CustomPinPut tests, with random PIN", () { + testWidgets("CustomPinPut with 4 fields builds correctly, with random PIN", + (tester) async { + const pinPut = CustomPinPut( + fieldsCount: 4, + isRandom: true, + ); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme(LightColors()), + ], + ), + home: const Material( + child: pinPut, + ), + ), + ); + + // expects 5 here. Four + the actual text field text + expect(find.text(""), findsNWidgets(5)); + expect(find.byType(PinKeyboard), findsOneWidget); + expect(find.byType(BackspaceKey), findsOneWidget); + expect(find.byType(NumberKey), findsNWidgets(10)); + }); + + testWidgets("CustomPinPut entering a pin successfully, with random PIN", + (tester) async { + bool submittedPinMatches = false; + final pinPut = CustomPinPut( + fieldsCount: 4, + onSubmit: (pin) { + submittedPinMatches = pin == "1234"; + print("pin entered: $pin"); + }, + useNativeKeyboard: false, + isRandom: true, + ); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme(LightColors()), + ], + ), + home: Material( + child: pinPut, + ), + ), + ); + + await tester.tap(find.byWidgetPredicate( + (widget) => widget is NumberKey && widget.number == "1")); + await tester.pumpAndSettle(); + await tester.tap(find.byWidgetPredicate( + (widget) => widget is NumberKey && widget.number == "2")); + await tester.pumpAndSettle(); + await tester.tap(find.byWidgetPredicate( + (widget) => widget is NumberKey && widget.number == "6")); + await tester.pumpAndSettle(); + await tester.tap(find.byType(BackspaceKey)); + await tester.pumpAndSettle(); + await tester.tap(find.byWidgetPredicate( + (widget) => widget is NumberKey && widget.number == "3")); + await tester.pumpAndSettle(); + await tester.tap(find.byWidgetPredicate( + (widget) => widget is NumberKey && widget.number == "4")); + await tester.pumpAndSettle(); + await tester.tap(find.byType(SubmitKey)); + await tester.pumpAndSettle(); + + expect(submittedPinMatches, true); + }); + + testWidgets("CustomPinPut pin enter fade animation, with random PIN", + (tester) async { + final controller = TextEditingController(); + final pinPut = CustomPinPut( + fieldsCount: 4, + pinAnimationType: PinAnimationType.fade, + controller: controller, + isRandom: true, + ); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme(LightColors()), + ], + ), + home: Material( + child: pinPut, + ), + ), + ); + + await tester.tap(find.byWidgetPredicate( + (widget) => widget is NumberKey && widget.number == "1")); + await tester.pumpAndSettle(); + expect(controller.text, "1"); + + await tester.tap(find.byType(BackspaceKey)); + await tester.pumpAndSettle(); + expect(controller.text, ""); + }); + + testWidgets("CustomPinPut pin enter scale animation, with random PIN", + (tester) async { + final controller = TextEditingController(); + final pinPut = CustomPinPut( + fieldsCount: 4, + pinAnimationType: PinAnimationType.scale, + controller: controller, + isRandom: true, + ); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme(LightColors()), + ], + ), + home: Material( + child: pinPut, + ), + ), + ); + + await tester.tap(find.byWidgetPredicate( + (widget) => widget is NumberKey && widget.number == "1")); + await tester.pumpAndSettle(); + expect(controller.text, "1"); + + await tester.tap(find.byType(BackspaceKey)); + await tester.pumpAndSettle(); + expect(controller.text, ""); + }); + + testWidgets("CustomPinPut pin enter rotate animation, with random PIN", + (tester) async { + final controller = TextEditingController(); + final pinPut = CustomPinPut( + fieldsCount: 4, + pinAnimationType: PinAnimationType.rotation, + controller: controller, + isRandom: true, + ); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme(LightColors()), + ], + ), + home: Material( + child: pinPut, + ), + ), + ); + + await tester.tap(find.byWidgetPredicate( + (widget) => widget is NumberKey && widget.number == "1")); + await tester.pumpAndSettle(); + expect(controller.text, "1"); + + await tester.tap(find.byType(BackspaceKey)); + await tester.pumpAndSettle(); + expect(controller.text, ""); + }); + }); + + testWidgets("PinKeyboard builds correctly, with random PIN", (tester) async { + final keyboard = PinKeyboard( + onNumberKeyPressed: (_) {}, + onBackPressed: () {}, + onSubmitPressed: () {}, + isRandom: true, + ); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme(LightColors()), + ], + ), + home: Material( + child: keyboard, + ), + ), + ); + + expect(find.byType(BackspaceKey), findsOneWidget); + expect(find.byType(SubmitKey), findsOneWidget); + expect(find.byType(NumberKey), findsNWidgets(10)); + expect(find.text("0"), findsOneWidget); + expect(find.text("1"), findsOneWidget); + expect(find.text("2"), findsOneWidget); + expect(find.text("3"), findsOneWidget); + expect(find.text("4"), findsOneWidget); + expect(find.text("5"), findsOneWidget); + expect(find.text("6"), findsOneWidget); + expect(find.text("7"), findsOneWidget); + expect(find.text("8"), findsOneWidget); + expect(find.text("9"), findsOneWidget); + expect(find.byType(SvgPicture), findsNWidgets(2)); }); }