Merge branch 'refs/heads/dev' into cw-1440-new-buy-sell-flow

# Conflicts:
#	lib/new-ui/widgets/coins_page/cards/cards_view.dart
#	lib/new-ui/widgets/receive_page/receive_top_bar.dart
This commit is contained in:
Robert Malikowski
2026-08-07 01:35:54 +02:00
286 changed files with 13997 additions and 6504 deletions
+10 -18
View File
@@ -687,28 +687,20 @@ class CWBitcoin extends Bitcoin {
}
List<Output> updateOutputs(PendingTransaction pendingTransaction, List<Output> outputs) {
if (pendingTransaction is PendingLightningTransaction) return outputs;
final pendingTx = pendingTransaction as PendingBitcoinTransaction;
if (!pendingTx.hasSilentPayment) {
if (pendingTransaction is! PendingBitcoinTransaction || !pendingTransaction.hasSilentPayment) {
return outputs;
}
final updatedOutputs = outputs.map((output) {
try {
final pendingOut = pendingTx.outputs[outputs.indexOf(output)];
final updatedOutput = output;
final stealthAddresses = pendingTransaction.stealthAddresses;
if (stealthAddresses.length != outputs.length) {
printV("well, that shouldn't happen");
return outputs;
}
updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey)
.toAddress(BitcoinNetwork.mainnet);
return updatedOutput;
} catch (_) {}
return output;
}).toList();
return updatedOutputs;
for (var i = 0; i < outputs.length; i++) {
outputs[i].stealthAddress = stealthAddresses[i];
}
return outputs;
}
@override
+17 -8
View File
@@ -336,14 +336,23 @@ class DFXBuyProvider extends BuyProvider {
String? countryCode}) async {
if (wallet.isHardwareWallet) {
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: wallet.walletInfo.type,
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (context, hwwVM) {
hwwVM.initWallet(wallet);
Navigator.of(context).pop();
}));
await Navigator.of(context).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: wallet.walletInfo.type,
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (context, hwwVM) {
hwwVM.initWallet(wallet);
Navigator.of(context).pop();
},
isReconnect: false,
),
);
// Recheck to handle tap-backs
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
return;
}
} else {
hardwareWalletVM!.initWallet(wallet);
}
+15 -8
View File
@@ -174,14 +174,21 @@ class RobinhoodBuyProvider extends BuyProvider {
String? countryCode}) async {
if (wallet.isHardwareWallet) {
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: wallet.walletInfo.type,
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (context, hwwVM) {
hwwVM.initWallet(wallet);
Navigator.of(context).pop();
}));
await Navigator.of(context).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: wallet.walletInfo.type,
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (context, hwwVM) {
hwwVM.initWallet(wallet);
Navigator.of(context).pop();
},
isReconnect: false,
),
);
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
return;
}
} else {
hardwareWalletVM!.initWallet(wallet);
}
@@ -15,7 +15,7 @@ class RoundedOverlayCards extends StatelessWidget {
return ClipRRect(
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
BorderRadius.only(bottomLeft: Radius.circular(25), bottomRight: Radius.circular(25)),
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
@@ -30,8 +30,8 @@ class RoundedOverlayCards extends StatelessWidget {
Flexible(
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(25.0),
bottomRight: Radius.circular(25.0),
bottomLeft: Radius.circular(25),
bottomRight: Radius.circular(25),
),
child: Container(
decoration: BoxDecoration(
@@ -20,7 +20,7 @@ class YatAddressProvider extends AddressLookupProvider {
bool canHandle(String q) => q.hasOnlyEmojis; // Yat handle example: 🐶🐾
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown;
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsYatService;
@override
Future<List<ParsedAddress>> resolve({
@@ -15,7 +15,7 @@ class ZcashMeAddressProvider extends AddressLookupProvider {
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.zec];
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames;
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashAddress;
@override
bool canHandle(String query) {
+11 -4
View File
@@ -3,6 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cake_wallet/zcash/zcash_network_type.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
@@ -10,8 +11,11 @@ const BEFORE_REGEX = '(^|\\s)';
const AFTER_REGEX = '(\$|\\s)';
class AddressValidator extends TextValidator {
AddressValidator({required CryptoCurrency type, bool isTestnet = false})
: super(
AddressValidator({
required CryptoCurrency type,
bool isTestnet = false,
String? network,
}) : super(
errorMessage: S.current.error_text_address,
useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type)
? (String txt) {
@@ -32,7 +36,7 @@ class AddressValidator extends TextValidator {
: type == CryptoCurrency.zano
? zano?.validateAddress
: null,
pattern: getPattern(type, isTestnet: isTestnet),
pattern: getPattern(type, isTestnet: isTestnet, network: network),
length: getLength(type),
);
@@ -61,7 +65,7 @@ class AddressValidator extends TextValidator {
CryptoCurrency.btcln,
];
static String getPattern(CryptoCurrency type, {bool isTestnet = false}) {
static String? getPattern(CryptoCurrency type, {bool isTestnet = false, String? network}) {
var pattern = "";
if (type is Erc20Token) {
pattern = '0x[0-9a-zA-Z]+';
@@ -170,6 +174,9 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.hbar:
pattern = '[0-9a-zA-Z.]+';
case CryptoCurrency.zec:
if (ZcashNetworkType.isDevNetwork(network)) {
return null;
}
pattern = '(?:'
't1[0-9A-Za-z]{33}'
'|t3[0-9A-Za-z]{33}'
+5 -1
View File
@@ -210,7 +210,11 @@ class BackgroundSync {
}
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
tx.date.add(Duration(minutes: 1)).toIso8601String());
final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
final action = tx.additionalInfo['isAutoShield'] == true
? "Autoshield"
: tx.direction == TransactionDirection.incoming
? "Received"
: "Sent";
if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
await showNotification(
"$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}");
+21 -15
View File
@@ -1,6 +1,6 @@
import 'dart:convert';
import "dart:convert";
import 'package:cw_core/utils/proxy_wrapper.dart';
import "package:cw_core/utils/proxy_wrapper.dart";
Future<String?> getBolt11FromLightingAddress(String lightningAddress, {int amount = 0}) async {
try {
@@ -31,28 +31,34 @@ Uri getURlOfLightningAddress(String lightningAddress) {
}
class _LNURLPResponseDTO {
const _LNURLPResponseDTO(
this.callback,
this.maxSendable,
this.minSendable,
this.tag,
this.metadata,
);
factory _LNURLPResponseDTO.fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
map["callback"] as String,
map["maxSendable"] as int,
map["minSendable"] as int,
map["tag"] as String,
map["metadata"] as String,
);
final String callback;
final int maxSendable;
final int minSendable;
final String tag;
final String metadata;
const _LNURLPResponseDTO(
this.callback, this.maxSendable, this.minSendable, this.tag, this.metadata);
static _LNURLPResponseDTO fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
map["callback"] as String,
map["maxSendable"] as int,
map["minSendable"] as int,
map["tag"] as String,
map["metadata"] as String);
}
class _LNURLPCallbackResponseDTO {
final String pr;
const _LNURLPCallbackResponseDTO(this.pr);
static _LNURLPCallbackResponseDTO fromJson(Map<String, dynamic> map) =>
factory _LNURLPCallbackResponseDTO.fromJson(Map<String, dynamic> map) =>
_LNURLPCallbackResponseDTO(map["pr"] as String);
final String pr;
}
+3 -1
View File
@@ -436,7 +436,9 @@ Future<void> setup({
() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"),
);
getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt<TrezorConnect>()));
getIt.registerLazySingleton(
() => TrezorConnectViewModel(getIt<TrezorConnect>(), getIt<SecureStorage>()),
);
getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
+72 -4
View File
@@ -10,7 +10,6 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/fs_migration.dart';
import 'package:cake_wallet/entities/haven_seed_store.dart';
import 'package:cake_wallet/entities/node_list.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/monero/monero.dart';
@@ -46,7 +45,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002';
const nanoDefaultNodeUri = 'rpc.nano.to';
const nanoDefaultPowNodeUri = 'rpc.nano.to';
const solanaDefaultNodeUri = 'solana-mainnet.core.chainstack.com';
const tronDefaultNodeUri = 'api.trongrid.io';
const tronDefaultNodeUri = 'trx.nownodes.io';
const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002';
const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568';
const zanoDefaultNodeUri = '37.27.100.59:10500';
@@ -56,7 +55,7 @@ const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022';
const baseDefaultNodeUri = 'base-rpc.publicnode.com';
const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io';
const bscDefaultNodeUri = 'bsc-dataseed.bnbchain.org';
const zcashDefaultNodeUri = 'zec-node.cakewallet.com:443';
const zcashDefaultNodeUri = 'zec.rocks:443';
Future<void> defaultSettingsMigration(
{required int version,
@@ -607,6 +606,38 @@ Future<void> defaultSettingsMigration(
enabled: true,
);
break;
case 68:
await _changeDefaultNode(
sharedPreferences: sharedPreferences,
type: WalletType.tron,
newDefaultUri: tronDefaultNodeUri,
currentNodePreferenceKey: PreferencesKey.currentTronNodeIdKey,
useSSL: true,
oldUri: [
'tron-rpc.publicnode.com:443',
'api.trongrid.io',
],
);
await _changeDefaultNode(
sharedPreferences: sharedPreferences,
type: WalletType.monero,
newDefaultUri: newCakeWalletMoneroUri,
currentNodePreferenceKey: PreferencesKey.currentNodeIdKey,
useSSL: true,
trusted: true,
oldUri: ['nodes.hashvault.pro:18081'],
);
break;
case 69:
_changeExchangeProviderAvailability(
sharedPreferences,
providerName: "Exolix",
enabled: false,
);
break;
case 70:
await _addTbbTokenToExistingSolanaWallets();
break;
default:
break;
}
@@ -663,7 +694,7 @@ Future<void> _changeDefaultNode({
} else {
final currentNode = nodes.firstWhereOrNull((node) => node.id == currentNodeId);
shouldReplace =
currentNode == null || (oldUri?.any((e) => currentNode!.uriRaw.contains(e)) ?? true);
currentNode == null || (oldUri?.any((e) => currentNode.uriRaw.contains(e)) ?? true);
}
if (shouldReplace) {
@@ -1297,3 +1328,40 @@ Future<void> _addXaut0TokenToExistingSolanaWallets() async {
printV('Error in XAUT0 migration: $e');
}
}
Future<void> _addTbbTokenToExistingSolanaWallets() async {
try {
final tbbToken = SPLToken(
name: "The Bitcoin Bull",
symbol: "TBB",
mintAddress: "42cXQvAAr7hcPBPWAS4ocVtDyeJ4Fa6gRR2uG4gppump",
decimal: 6,
mint: "tbb",
enabled: false,
iconPath: "assets/images/tbb_icon.png",
);
final allWallets = await WalletInfo.getAll();
final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList();
for (final walletInfo in solanaWallets) {
final sanitizedName = walletInfo.name.replaceAll(" ", "_");
final boxName = "${sanitizedName}_${SPLToken.boxName}";
Box<SPLToken> tokenBox;
if (CakeHive.isBoxOpen(boxName)) {
tokenBox = CakeHive.box<SPLToken>(boxName);
} else {
tokenBox = await CakeHive.openBox<SPLToken>(boxName);
}
final tbbAddress = tbbToken.mintAddress;
if (!tokenBox.containsKey(tbbAddress)) {
await tokenBox.put(tbbAddress, tbbToken);
}
}
} catch (e) {
printV("Error in TBB migration: $e");
}
}
+1
View File
@@ -145,4 +145,5 @@ class PreferencesKey {
static const backgroundImage = 'background_image';
static const mwebAdDismissed = "mweb_ad_dismissed";
static const balanceHideCounter = "balance_hide_counter";
static const zcashMigrationModalViewed = "zcash_migration_modal_viewed";
}
+1 -1
View File
@@ -69,7 +69,7 @@ class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).error_dialog_content,
buttonText: 'ok',
buttonText: S.of(context).ok,
buttonAction: () {
Navigator.of(context).pop();
},
@@ -325,6 +325,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
return 'btc';
case CryptoCurrency.arb:
return 'arbitrum';
case CryptoCurrency.nano:
return 'nano';
default:
return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title.toLowerCase();
}
@@ -337,6 +339,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
switch (currency) {
case CryptoCurrency.zec:
return 'zec';
case CryptoCurrency.maticpoly:
return 'matic';
default:
return currency.title.toLowerCase();
}
@@ -344,7 +348,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
String _normalizeTag(String tag) {
switch (tag) {
case 'POLY':
case 'POL':
return 'matic';
case 'LN':
return 'lightning';
@@ -539,7 +539,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
}
Future<Uri> _getUri(String path, Map<String, String> queryParams) async {
final uri = Uri.http(onionApiAuthority, path, queryParams);
final uri = Uri.https(onionApiAuthority, path, queryParams);
if (useTorOnly) return uri;
+5 -3
View File
@@ -23,6 +23,7 @@ import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/trade_legacy.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/locales/locale.dart';
import "package:cake_wallet/new-ui/widgets/money/money_settings_provider.dart";
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/reactions/bootstrap.dart';
import 'package:cake_wallet/router.dart' as Router;
@@ -315,7 +316,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 67,
initialMigrationVersion: 70,
);
}
@@ -425,8 +426,9 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
child: child!),
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
child: MoneySettingsProvider(settingsStore: settingsStore, child: child!),
),
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
+8
View File
@@ -236,12 +236,14 @@ class CWMonero extends Monero {
required String password,
required int height,
required HardwareWalletService hardwareWalletService,
required String? passphrase,
}) =>
MoneroRestoreWalletFromHardwareCredentials(
name: name,
password: password,
height: height,
hardwareWalletService: hardwareWalletService,
passphrase: passphrase,
);
@override
@@ -418,6 +420,12 @@ class CWMonero extends Monero {
return moneroWallet.needExportOutputs(amount);
}
@override
bool hasUnknownKeyImages(Object wallet) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.hasUnknownKeyImages();
}
@override
void monerocCheck() => checkIfMoneroCIsFine();
+12 -8
View File
@@ -90,14 +90,18 @@ class _NewDashboardState extends State<NewDashboard> {
),
),
),
SafeArea(
bottom: !(Platform.isIOS),
child: SizedBox(
width: double.infinity,
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
child: AbsorbPointer(
absorbing: true,
child: Container(color: Colors.transparent),
// Invisible pointer-absorbing strip behind the nav bar: it must not
// be reachable by screen-reader traversal either.
ExcludeSemantics(
child: SafeArea(
bottom: !(Platform.isIOS),
child: SizedBox(
width: double.infinity,
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
child: AbsorbPointer(
absorbing: true,
child: Container(color: Colors.transparent),
),
),
),
),
+1
View File
@@ -56,6 +56,7 @@ class _AboutPageState extends State<AboutPage> {
topBar: ModalTopBar(
title: S.of(context).about,
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
content: Container(
+70 -44
View File
@@ -128,8 +128,10 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
ModalTopBar(
title: S.of(context).wallet_accounts,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: Icon(Icons.refresh),
trailingSemanticLabel: S.of(context).reset,
onTrailingPressed: showResetDialog,
),
Padding(
@@ -172,17 +174,29 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
itemCount: _items.length,
itemBuilder: (BuildContext context, int index) {
final card = _items[index].card;
// The stack is ordered bottom to top, so the last item
// is the account currently in front — the selected one.
final selectedItemIndex = _items.length - 1;
return Container(
key: ValueKey(index),
child: GestureDetector(
onTap: () {
reorder(index, _items.length);
},
child: Align(
alignment: Alignment.topCenter,
heightFactor: _kStackVisibleFactor,
child: card,
// One labeled, selectable node per account; the card's own
// texts stay reachable underneath it.
child: Semantics(
button: true,
selected: selectedItemIndex == index,
label: _items[index].accountListItem.label,
onTap: () => reorder(index, _items.length),
child: GestureDetector(
excludeFromSemantics: true,
onTap: () {
reorder(index, _items.length);
},
child: Align(
alignment: Alignment.topCenter,
heightFactor: _kStackVisibleFactor,
child: card,
),
),
),
);
@@ -197,31 +211,36 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(999999),
onTap: _showAddAccountModal,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.add,
size: 28,
color: Theme.of(context).colorScheme.primary,
),
Text(
S.of(context).add_account,
style: TextStyle(
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
borderRadius: BorderRadius.circular(999999),
onTap: _showAddAccountModal,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.add,
size: 28,
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500),
)
],
),
Text(
S.of(context).add_account,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500),
)
],
),
),
),
),
),
@@ -404,6 +423,8 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
final TextEditingController _controller = TextEditingController();
bool _loading = false;
Future<void> _generateAccountName() async => _controller.text = await generateName();
@override
Widget build(BuildContext context) {
return Container(
@@ -439,18 +460,23 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
),
Padding(
padding: const EdgeInsets.all(12.0),
child: GestureDetector(
onTap: () async {
_controller.text = await generateName();
},
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(5)),
child: CakeImageWidget(
imageUrl: "assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
child: Semantics(
button: true,
label: S.of(context).generate_name,
onTap: _generateAccountName,
child: ExcludeSemantics(
child: GestureDetector(
onTap: _generateAccountName,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(5)),
child: CakeImageWidget(
imageUrl: "assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
),
),
),
),
+178 -138
View File
@@ -20,6 +20,7 @@ import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/utils/list_item.dart';
@@ -105,6 +106,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
ModalTopBar(
title: widget.showHidden ? S.of(context).hidden_addresses : S.of(context).addresses,
leadingIcon: Icon(Icons.arrow_back),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {}),
Expanded(
@@ -212,24 +214,31 @@ class AddressSearchBox extends StatelessWidget {
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainerHighest, width: 1),
borderRadius: BorderRadius.circular(99999)),
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: S.of(context).search,
hintStyle: TextStyle(
fontWeight: FontWeight.w600,
// The hint names the field only while it is empty, so the label is
// supplied once there is text to keep exactly one announcement.
child: MergeSemantics(
child: Semantics(
label: controller.text.isEmpty ? null : S.of(context).search,
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: S.of(context).search,
hintStyle: TextStyle(
fontWeight: FontWeight.w600,
),
prefixIcon: ExcludeSemantics(child: Icon(Icons.search)),
filled: false,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(99999),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(99999),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(vertical: 8),
),
),
prefixIcon: Icon(Icons.search),
filled: false,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(99999),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(99999),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(vertical: 8),
),
),
),
@@ -333,116 +342,139 @@ class AddressRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hasLabel = item.name != null && item.name!.isNotEmpty;
final hideLabel = item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address;
return GestureDetector(
onTap: onSelect,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: LongPressPopupBuilder(
popup: LongPressMenu(
items: [
LongPressMenuItem(
label: S.of(context).set_label,
iconPath: "assets/new-ui/address_set_label.svg",
onSelected: () async {
Navigator.of(context, rootNavigator: true).pop();
final res = await showPopUp(
context: context,
builder: (context) => getIt.get<AddressLabelInputPopup>(param1: item));
Future<void> editLabel() async {
final res = await showPopUp(
context: context, builder: (context) => getIt.get<AddressLabelInputPopup>(param1: item));
if (res != null) {
onLabelChanged();
}
}),
LongPressMenuItem(
label: item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address,
iconPath: "assets/new-ui/address_hide.svg",
onSelected: () {
Navigator.of(context, rootNavigator: true).pop();
onAddressHidden();
},
color: Theme.of(context).colorScheme.error),
LongPressMenuItem(
label: 'Info',
iconPath: "assets/images/info_icon.svg",
onSelected: () async {
Navigator.of(context, rootNavigator: true).pop();
await showPopUp(
context: context,
builder: (context) => getIt.get<AddressInfoPopup>(param1: item));
}),
],
),
child: AnimatedContainer(
duration: Duration(milliseconds: 150),
decoration: BoxDecoration(
color: selected
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: Radius.circular(first ? 16 : 0),
bottom: Radius.circular(last ? 16 : 0),
if (res != null) {
onLabelChanged();
}
}
Future<void> showInfo() async {
await showPopUp(
context: context, builder: (context) => getIt.get<AddressInfoPopup>(param1: item));
}
// The row is a single control: selection on tap, and the actions that are
// otherwise reachable only by long press exposed as custom actions.
return MergeSemantics(
child: Semantics(
button: true,
selected: selected,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
CustomSemanticsAction(label: S.of(context).set_label): editLabel,
CustomSemanticsAction(label: hideLabel): onAddressHidden,
CustomSemanticsAction(label: S.of(context).info): showInfo,
},
child: GestureDetector(
onTap: onSelect,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: LongPressPopupBuilder(
popup: LongPressMenu(
items: [
LongPressMenuItem(
label: S.of(context).set_label,
iconPath: "assets/new-ui/address_set_label.svg",
onSelected: () {
Navigator.of(context, rootNavigator: true).pop();
editLabel();
}),
LongPressMenuItem(
label: hideLabel,
iconPath: "assets/new-ui/address_hide.svg",
onSelected: () {
Navigator.of(context, rootNavigator: true).pop();
onAddressHidden();
},
color: Theme.of(context).colorScheme.error),
LongPressMenuItem(
label: S.of(context).info,
iconPath: "assets/images/info_icon.svg",
onSelected: () {
Navigator.of(context, rootNavigator: true).pop();
showInfo();
}),
],
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
spacing: 8,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
children: [
Row(
spacing: 4,
child: AnimatedContainer(
duration: Duration(milliseconds: 150),
decoration: BoxDecoration(
color: selected
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: Radius.circular(first ? 16 : 0),
bottom: Radius.circular(last ? 16 : 0),
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
spacing: 8,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
children: [
if (hasLabel)
Text(
item.name!,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500),
),
Expanded(
child: AddressFormatter.buildSegmentedAddress(
address: item.address,
walletType: walletType,
textAlign: TextAlign.left,
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
Row(
spacing: 4,
children: [
if (hasLabel)
Text(
item.name!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: hasLabel
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onSurface,
),
shouldTruncate: (hasLabel)),
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500),
),
Expanded(
child: AddressFormatter.buildSegmentedAddress(
address: item.address,
walletType: walletType,
textAlign: TextAlign.left,
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
fontWeight: FontWeight.w400,
color: hasLabel
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onSurface,
),
shouldTruncate: (hasLabel)),
),
],
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${S.of(context).transactions}: ${item.txCount}",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text(
"${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant)),
],
)
],
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${S.of(context).transactions}: ${item.txCount}",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text(
"${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant)),
],
)
],
),
),
// The row's selected state carries this information.
if (selected)
ExcludeSemantics(
child: CakeImageWidget(imageUrl: "assets/new-ui/checkmark.svg"))
],
),
if (selected) CakeImageWidget(imageUrl: "assets/new-ui/checkmark.svg")
],
),
),
),
),
@@ -462,26 +494,34 @@ class ShowHiddenButton extends StatelessWidget {
child: Column(
children: [
Material(
child: InkWell(
onTap: () {
Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true);
},
child: Container(
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).show_hidden_addresses),
RotatedBox(
quarterTurns: 1,
child: CakeImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg"))
],
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
onTap: () {
Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true);
},
child: Container(
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).show_hidden_addresses),
ExcludeSemantics(
child: RotatedBox(
quarterTurns: 1,
child:
CakeImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg")),
)
],
),
),
),
),
),
@@ -116,8 +116,10 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
ModalTopBar(
title: S.of(context).enter_amount,
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context, rootNavigator: true).pop(),
trailingIcon: Icon(Icons.history),
trailingSemanticLabel: S.of(context).history,
onTrailingPressed: () {
Navigator.pushNamed(context, Routes.bridgeHistoryPage,
arguments: widget.bridgeHistoryViewModel);
@@ -274,6 +276,7 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
backgroundColor: theme.colorScheme.primary,
iconColor: theme.colorScheme.onPrimary,
icon: Icon(Icons.arrow_forward, size: 20),
semanticLabel: S.of(context).continue_text,
onPressed: () {
Navigator.pushNamed(
context,
@@ -87,6 +87,7 @@ class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
],
),
trailingIcon: const Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => Navigator.of(context).maybePop(),
);
},
@@ -46,6 +46,7 @@ class BridgeDetailPage extends StatelessWidget {
ModalTopBar(
title: "Bridge Transfer Details",
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
Expanded(
@@ -1,4 +1,5 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
@@ -25,6 +26,7 @@ class BridgeHistoryPage extends StatelessWidget {
ModalTopBar(
title: "Bridge history",
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
Expanded(
@@ -1,4 +1,5 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
@@ -30,6 +31,7 @@ class BridgeNetworkPage extends StatelessWidget {
ModalTopBar(
title: 'Destination Network',
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
SizedBox(height: 48),
@@ -55,6 +55,7 @@ class _BridgeReceiveAddressInputPageState extends State<BridgeReceiveAddressInpu
ModalTopBar(
title: 'Input Receive Address',
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
SizedBox(height: 48),
@@ -1,4 +1,5 @@
import 'package:cake_wallet/view_model/bridge/bridge_receiving_wallet_option.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receive_address_input_page.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
@@ -42,6 +43,7 @@ class _BridgeReceivingWalletPageState extends State<BridgeReceivingWalletPage> {
ModalTopBar(
title: 'Receiving Wallet',
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
const SizedBox(height: 48),
+1
View File
@@ -84,6 +84,7 @@ class _CardCustomizerState extends State<CardCustomizer> {
ModalTopBar(
title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
// trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
onLeadingPressed: () => Navigator.of(context).maybePop(),
// onTrailingPressed: () {},
+22
View File
@@ -13,10 +13,14 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart';
import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/transaction_direction.dart";
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -48,6 +52,24 @@ class _NewHomePageState extends State<NewHomePage> {
_lightningMode = false;
});
});
reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
if (val && !widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed) {
if(!context.mounted) {
return;
}
widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed = true;
showMaterialModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ZcashMigrationModal(
hasContinue: true,
balance: widget.dashboardViewModel.wallet.balance.values.first.unavailable
.toStringWithSymbol(),
),
);
}
});
}
void _setAccountViewModel() {
@@ -68,6 +68,7 @@ class _LightningUsernamePageState extends State<LightningUsernamePage> {
ModalTopBar(
title: "Lightning ${S.of(context).username}",
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
+138 -116
View File
@@ -1,54 +1,54 @@
import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart';
import 'package:cake_wallet/utils/share_util.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/routes.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import "package:cake_wallet/anonpay/anonpay_donation_link_info.dart";
import "package:cake_wallet/bitcoin/bitcoin.dart";
import "package:cake_wallet/core/utilities.dart";
import "package:cake_wallet/di.dart";
import "package:cake_wallet/entities/auto_generate_subaddress_status.dart";
import "package:cake_wallet/entities/preferences_key.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/routes.dart";
import "package:cake_wallet/src/screens/receive/anonpay_receive_page.dart";
import "package:cake_wallet/utils/share_util.dart";
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
import "package:cake_wallet/view_model/dashboard/receive_option_view_model.dart";
import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart";
import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart";
import "package:cake_wallet/zcash/zcash.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/payment_uris.dart";
import "package:cw_core/receive_page_option.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:mobx/mobx.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
import "package:shared_preferences/shared_preferences.dart";
class NewReceivePage extends StatefulWidget {
NewReceivePage(
{super.key,
required this.addressListViewModel,
required this.receiveOptionViewModel,
required this.dashboardViewModel,
required this.lightningMode,
CryptoCurrency? initialCurrency}) {
NewReceivePage({
required this.addressListViewModel,
required this.receiveOptionViewModel,
required this.dashboardViewModel,
required this.lightningMode,
super.key,
CryptoCurrency? initialCurrency,
}) {
if (initialCurrency != null && initialCurrency != addressListViewModel.selectedCurrency) {
addressListViewModel.setTokenCurrency(initialCurrency);
}
@@ -78,31 +78,33 @@ class _NewReceivePageState extends State<NewReceivePage> {
.firstWhereOrNull((item) => item.value.contains("Lightning")) ??
ReceivePageOption.mainnet,
);
widget.addressListViewModel.setTokenCurrency(CryptoCurrency.btcln);
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln;
} else if (widget.addressListViewModel.wallet.type == WalletType.bitcoin) {
widget.receiveOptionViewModel.selectReceiveOption(
widget.receiveOptionViewModel.options
.firstWhereOrNull((item) => item.value.contains("Standard")) ??
ReceivePageOption.mainnet,
);
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc;
}
});
reaction((_) => widget.addressListViewModel.uri, (newAddress) {
_reloadAddressWithLabel(newAddress);
});
reaction((_) => widget.addressListViewModel.uri, _reloadAddressWithLabel);
_addressItemWithLabel =
widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull((item) {
return (item is WalletAddressListItem &&
item.address == widget.addressListViewModel.uri.address);
}) as WalletAddressListItem?;
_addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
(item) =>
item is WalletAddressListItem && item.address == widget.addressListViewModel.uri.address,
) as WalletAddressListItem?;
reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption,
(ReceivePageOption option) {
reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption, (option) {
if (widget.dashboardViewModel.type == WalletType.bitcoin &&
bitcoin!.isBitcoinReceivePageOption(option)) {
widget.addressListViewModel.setAddressType(bitcoin!.getOptionToType(option));
if (option.value.contains("Lightning")) {
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln;
} else {
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc;
}
return;
}
if (widget.dashboardViewModel.type == WalletType.zcash) {
@@ -166,16 +168,18 @@ class _NewReceivePageState extends State<NewReceivePage> {
final hasAddressTypeSelector = widget.receiveOptionViewModel.options.length > 1;
final hasLabel = _addressItemWithLabel?.name != null && _addressItemWithLabel!.name!.isNotEmpty;
final infoboxDismissed = widget.addressListViewModel.wallet.walletInfo.receiveInfoboxDismissed;
final infobox = ReceiveInfoBox.forWalletType(widget.addressListViewModel.type,
supportedCurrencies: widget.addressListViewModel.tokenCurrencies
.whereType<CryptoCurrency>()
.toList(), onDismissed: () {
widget.addressListViewModel.dismissInfobox();
setState(() {});
},
autoGenerateSubaddressStatus: widget.lightningMode
? AutoGenerateSubaddressStatus.disabled
: widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus);
final infobox = ReceiveInfoBox.forWalletType(
widget.addressListViewModel.type,
supportedCurrencies:
widget.addressListViewModel.tokenCurrencies.whereType<CryptoCurrency>().toList(),
onDismissed: () {
widget.addressListViewModel.dismissInfobox();
setState(() {});
},
autoGenerateSubaddressStatus: widget.lightningMode
? AutoGenerateSubaddressStatus.disabled
: widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus,
);
return Container(
decoration: BoxDecoration(
@@ -187,9 +191,7 @@ class _NewReceivePageState extends State<NewReceivePage> {
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.vertical(
top: Radius.circular(24),
),
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: SafeArea(
child: Column(
@@ -198,13 +200,14 @@ class _NewReceivePageState extends State<NewReceivePage> {
children: [
ModalTopBar(
title: _largeQrMode ? "" : S.of(context).receive,
leadingIcon: Icon(Icons.close),
leadingIcon: const Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
trailingWidget: Observer(
builder: (_) => AnimatedSwitcher(
duration: Duration(milliseconds: 300),
duration: const Duration(milliseconds: 300),
child: _largeQrMode ||
widget.addressListViewModel.hasAddressRotation
/* TODO rotating is broken on mweb, disabling for now, fix after mvp*/
@@ -217,10 +220,13 @@ class _NewReceivePageState extends State<NewReceivePage> {
key: ValueKey(_largeQrMode),
size: 36,
icon: _largeQrMode
? Icon(Icons.share)
? const Icon(Icons.share)
: widget.addressListViewModel.isRotatingAddress
? CupertinoActivityIndicator()
: Icon(Icons.refresh),
? const CupertinoActivityIndicator()
: const Icon(Icons.refresh),
semanticLabel: _largeQrMode
? S.of(context).share_address
: S.of(context).rotate_address,
onPressed: () {
if (_largeQrMode) {
ShareUtil.share(
@@ -232,8 +238,9 @@ class _NewReceivePageState extends State<NewReceivePage> {
widget.addressListViewModel.rotateAddress();
}
}
})
: SizedBox.shrink(),
},
)
: const SizedBox.shrink(),
),
),
),
@@ -268,12 +275,25 @@ class _NewReceivePageState extends State<NewReceivePage> {
ReceiveAddressWidget(
addressListViewModel: widget.addressListViewModel,
),
GestureDetector(
onTap: _showLabelModal,
child: ReceiveLabelWidget(
name: _addressItemWithLabel?.name ?? "",
largeQrMode: _largeQrMode,
)),
// The label chip animates to zero height when there is no
// label (or in large QR mode); keep it out of the semantics
// tree entirely while it is collapsed.
ExcludeSemantics(
excluding: _largeQrMode || !hasLabel,
child: MergeSemantics(
child: Semantics(
button: true,
hint: S.of(context).set_label,
child: GestureDetector(
onTap: _showLabelModal,
child: ReceiveLabelWidget(
name: _addressItemWithLabel?.name ?? "",
largeQrMode: _largeQrMode,
),
),
),
),
),
Observer(
builder: (_) => ReceiveBottomButtons(
key: const ValueKey(0),
@@ -285,14 +305,16 @@ class _NewReceivePageState extends State<NewReceivePage> {
: ClipboardData(
text: widget.addressListViewModel.displayAmount.isEmpty
? widget.addressListViewModel.uri.address
: widget.addressListViewModel.uri.toString()),
: widget.addressListViewModel.uri.toString(),
),
onCopyButtonPressed: () {
if (widget.addressListViewModel.hasPayjoin) {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) =>
PayjoinCopyModal(uri: widget.addressListViewModel.uri));
isScrollControlled: true,
context: context,
builder: (context) =>
PayjoinCopyModal(uri: widget.addressListViewModel.uri),
);
}
},
onAmountButtonPressed: () {
@@ -300,12 +322,10 @@ class _NewReceivePageState extends State<NewReceivePage> {
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withAlpha(80),
builder: (context) {
return ReceiveAmountModal(
walletAddressListViewModel: widget.addressListViewModel,
onSubmitted: (amount) {},
);
},
builder: (context) => ReceiveAmountModal(
walletAddressListViewModel: widget.addressListViewModel,
onSubmitted: (amount) {},
),
);
},
onLabelButtonPressed: _showLabelModal,
@@ -318,22 +338,25 @@ class _NewReceivePageState extends State<NewReceivePage> {
),
),
ReceiveLargeAmountPreview(
amount: widget.addressListViewModel.displayAmount,
currency: widget.addressListViewModel.cryptoCurrencySymbol,
largeQrMode: _largeQrMode),
amount: widget.addressListViewModel.displayAmount,
currency: widget.addressListViewModel.cryptoCurrencySymbol,
largeQrMode: _largeQrMode,
),
if (infobox != null && !widget.addressListViewModel.isLightning)
ClipRect(
child: AnimatedAlign(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
heightFactor: infoboxDismissed ? 0 : 1,
alignment: Alignment.center,
child: AnimatedOpacity(
child: AnimatedAlign(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
heightFactor: infoboxDismissed ? 0 : 1,
alignment: Alignment.center,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: infoboxDismissed ? 0 : 1,
curve: Curves.easeOutCubic,
child: infobox),
))
child: infobox,
),
),
),
],
),
),
@@ -345,12 +368,11 @@ class _NewReceivePageState extends State<NewReceivePage> {
void _showLabelModal() {
showMaterialModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withAlpha(80),
builder: (context) {
return getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel);
}).then((value) {
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withAlpha(80),
builder: (_) => getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel),
).then((value) {
_reloadAddressWithLabel(widget.addressListViewModel.uri);
});
}
@@ -359,8 +381,8 @@ class _NewReceivePageState extends State<NewReceivePage> {
// FIXME: viewmodel doesn't want to load address name here, so we make it. investigate why later
setState(() {
_addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
(item) => (item is WalletAddressListItem && item.address == newAddress.address))
as WalletAddressListItem?;
(item) => item is WalletAddressListItem && item.address == newAddress.address,
) as WalletAddressListItem?;
});
}
}
+130 -88
View File
@@ -76,46 +76,50 @@ class _ScanPageState extends State<ScanPage> {
controller: controller,
onDetect: _handleBarcode,
),
// A full screen tap target would otherwise show up as one giant unlabeled
// node; leaving text mode is also possible with the back button.
Positioned.fill(
child: GestureDetector(
onTap: () => setState(() {
_textInputMode = false;
}),
child: RepaintBoundary(
child: AnimatedSwitcher(
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInQuad,
duration: textModeSwitchDuration,
child: _textInputMode
? BackdropFilter(
key: ValueKey(1),
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
child: Container(
color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153),
),
)
: TweenAnimationBuilder<double>(
key: const ValueKey(0),
tween: Tween<double>(begin: 0.0, end: targetRadius),
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
child: BackdropFilter(
child: ExcludeSemantics(
child: GestureDetector(
onTap: () => setState(() {
_textInputMode = false;
}),
child: RepaintBoundary(
child: AnimatedSwitcher(
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInQuad,
duration: textModeSwitchDuration,
child: _textInputMode
? BackdropFilter(
key: ValueKey(1),
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
child: Container(
color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153),
),
),
builder: (context, radius, child) {
return ClipPath(
clipper: HoleClipper(
width: cutoutSize,
height: cutoutSize,
radius: radius,
)
: TweenAnimationBuilder<double>(
key: const ValueKey(0),
tween: Tween<double>(begin: 0.0, end: targetRadius),
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
child: Container(
color: _frontFlashMode ? Colors.white : Colors.black.withAlpha(153),
),
child: child,
);
},
),
),
builder: (context, radius, child) {
return ClipPath(
clipper: HoleClipper(
width: cutoutSize,
height: cutoutSize,
radius: radius,
),
child: child,
);
},
),
),
),
),
),
@@ -163,6 +167,7 @@ class _ScanPageState extends State<ScanPage> {
},
iconColor: buttonIconColor,
backgroundColor: buttonColor,
semanticLabel: S.of(context).switch_camera,
),
ModernButton(
size: 36,
@@ -171,6 +176,10 @@ class _ScanPageState extends State<ScanPage> {
(controller.value.torchState == TorchState.on || _frontFlashMode)
? Icons.flash_off_outlined
: Icons.flash_on),
semanticLabel:
(controller.value.torchState == TorchState.on || _frontFlashMode)
? S.of(context).turn_flash_off
: S.of(context).turn_flash_on,
onPressed: () {
if (controller.value.cameraDirection == CameraFacing.front) {
setState(() {
@@ -194,6 +203,7 @@ class _ScanPageState extends State<ScanPage> {
size: 36,
iconSize: 18,
icon: Icon(Icons.arrow_back_ios_new),
semanticLabel: S.of(context).seed_alert_back,
onPressed: () {
if (_textInputMode) {
setState(() {
@@ -235,20 +245,25 @@ class _ScanPageState extends State<ScanPage> {
spacing: 10,
children: [
Expanded(
child: TextField(
enabled: _textInputMode,
controller: textController,
focusNode: textFocusNode,
onSubmitted: (val) {
if (val.isNotEmpty) {
Navigator.of(context).pop(val);
} else {
setState(() {
_textInputMode = false;
});
}
},
decoration: InputDecoration(hintText: S.of(context).enter_code),
child: MergeSemantics(
child: Semantics(
label: S.of(context).enter_code,
child: TextField(
enabled: _textInputMode,
controller: textController,
focusNode: textFocusNode,
onSubmitted: (val) {
if (val.isNotEmpty) {
Navigator.of(context).pop(val);
} else {
setState(() {
_textInputMode = false;
});
}
},
decoration: InputDecoration(hintText: S.of(context).enter_code),
),
),
),
),
FloatingIconButton(
@@ -330,6 +345,7 @@ class _ScanPageState extends State<ScanPage> {
}
},
icon: Icons.question_mark,
semanticsLabel: S.of(context).help,
buttonColor: buttonColor,
buttonIconColor: buttonIconColor)
],
@@ -360,27 +376,39 @@ class _ScanPageState extends State<ScanPage> {
child: AnimatedOpacity(
duration: Duration(milliseconds: 500),
opacity: isScanningURQR ? 1 : 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${decoder.processedPartsCount()}",
style: TextStyle(
fontSize: 45,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary),
// Three separate digits mean nothing on their own: announce the progress
// as one node, and only while a multi part code is actually being scanned.
child: ExcludeSemantics(
excluding: !isScanningURQR,
child: Semantics(
container: true,
liveRegion: true,
label: S.of(context).qr_parts_scanned(
"${decoder.processedPartsCount()}", "${decoder.expectedPartCount() ?? 0}"),
excludeSemantics: true,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${decoder.processedPartsCount()}",
style: TextStyle(
fontSize: 45,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary),
),
Text(
"/",
style: TextStyle(
fontSize: 45, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text(
"${decoder.expectedPartCount()}",
style: TextStyle(
fontSize: 45, color: Theme.of(context).colorScheme.onSurface),
),
],
),
Text(
"/",
style: TextStyle(
fontSize: 45, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text(
"${decoder.expectedPartCount()}",
style:
TextStyle(fontSize: 45, color: Theme.of(context).colorScheme.onSurface),
),
],
),
),
),
),
@@ -400,7 +428,7 @@ class _ScanPageState extends State<ScanPage> {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).error_dialog_content,
buttonText: 'ok',
buttonText: S.of(context).ok,
buttonAction: () {
Navigator.of(context).pop();
},
@@ -453,37 +481,51 @@ class ScanPageButton extends StatelessWidget {
required this.onTap,
required this.icon,
this.label,
this.semanticsLabel,
required this.buttonColor,
required this.buttonIconColor});
final VoidCallback onTap;
final IconData icon;
final String? label;
/// Name for the icon-only variant, which has no visible [label].
final String? semanticsLabel;
final Color buttonColor;
final Color buttonIconColor;
@override
Widget build(BuildContext context) {
return GestureDetector(
return Semantics(
button: true,
enabled: true,
label: semanticsLabel ?? label,
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Container(
decoration: BoxDecoration(color: buttonColor, borderRadius: BorderRadius.circular(99999)),
child: Padding(
padding: EdgeInsets.only(
top: 10, bottom: 10, left: label == null ? 10 : 16, right: label == null ? 10 : 20),
child: Row(
spacing: 10,
children: [
Icon(icon, size: 28, color: buttonIconColor),
if (label != null)
Text(label!,
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500, color: buttonIconColor))
],
),
),
));
excludeSemantics: true,
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Container(
decoration:
BoxDecoration(color: buttonColor, borderRadius: BorderRadius.circular(99999)),
child: Padding(
padding: EdgeInsets.only(
top: 10,
bottom: 10,
left: label == null ? 10 : 16,
right: label == null ? 10 : 20),
child: Row(
spacing: 10,
children: [
Icon(icon, size: 28, color: buttonIconColor),
if (label != null)
Text(label!,
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w500, color: buttonIconColor))
],
),
),
)));
}
}
File diff suppressed because it is too large Load Diff
+26 -17
View File
@@ -9,10 +9,9 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import "package:cw_core/wallet_type.dart";
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
bool _trueFunc(DashboardViewModel _) => true;
@@ -26,10 +25,21 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
bool _isCupcake(DashboardViewModel vm) =>
vm.wallet.hardwareWalletType == HardwareWalletType.cupcake;
bool _requiresKeyImageSync(DashboardViewModel vm) =>
vm.wallet.type == WalletType.monero &&
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
class SettingsListItem {
const SettingsListItem(
this.iconPath,
this.title,
this.route, {
this.requireAuth = false,
this.use2fa = _falseFunc,
this.condition = _trueFunc,
this.routeArgs,
});
final String iconPath;
final String title;
final String route;
@@ -37,21 +47,15 @@ class SettingsListItem {
final bool requireAuth;
final bool Function(DashboardViewModel) use2fa;
final bool Function(DashboardViewModel) condition;
const SettingsListItem(this.iconPath, this.title, this.route,
{this.routeArgs = null,
this.requireAuth = false,
this.use2fa = _falseFunc,
this.condition = _trueFunc});
}
class SettingsSectionData {
const SettingsSectionData(this.title, this.titleIconPath, this.items);
final String title;
final String titleIconPath;
final List<SettingsListItem> items;
const SettingsSectionData(this.title, this.titleIconPath, this.items);
static SettingsSectionData walletSettings =
SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [
SettingsListItem(
@@ -71,9 +75,13 @@ class SettingsSectionData {
condition: _hasWalletConnect),
//SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc),
//SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb),
SettingsListItem("assets/new-ui/settings_row_icons/cupcake.svg", S.current.export_outputs,
Routes.urqrAnimatedPage,
routeArgs: {'export-outputs': 'export-outputs'}, condition: _isCupcake),
SettingsListItem(
"assets/new-ui/settings_row_icons/sync-balance.svg",
S.current.resync_device,
Routes.syncKeyImagesDevices,
routeArgs: {'export-outputs': 'export-outputs'},
condition: _requiresKeyImageSync,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage),
]);
@@ -152,8 +160,9 @@ class SettingsMainPage extends StatelessWidget {
color: Theme.of(context).colorScheme.surface,
child: Column(children: [
ModalTopBar(
title: "Settings",
title: S.of(context).settings_title,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context, rootNavigator: true).pop,
onTrailingPressed: () {},
),
+8 -2
View File
@@ -429,8 +429,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
final parsedAddress = await widget.adrResService
.resolve(query: domain, wallet: widget.exchangeViewModel.wallet, currency: currency);
return parsedAddress.isNotEmpty
? parsedAddress.first.parsedAddressByCurrencyMap[currency] ?? ''
: '';
? (parsedAddress.first.parsedAddressByCurrencyMap[currency] ?? domain)
: domain;
}
void _showFeeAlert(BuildContext context) async {
@@ -472,12 +472,14 @@ class _NewSwapPageState extends State<NewSwapPage> {
ModalTopBar(
title: S.of(context).swap,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: CakeImageWidget(
imageUrl: "assets/new-ui/options.svg",
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
trailingSemanticLabel: S.of(context).configure,
onTrailingPressed: () {
Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => Material(
@@ -576,6 +578,7 @@ class _NewSwapPageState extends State<NewSwapPage> {
iconSize: 24,
svgPath: "assets/new-ui/swap_amounts.svg",
onPressed: widget.exchangeViewModel.reverseSwapDirection,
semanticLabel: S.of(context).swap_reverse_direction,
),
],
),
@@ -1174,6 +1177,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
size: 36,
iconSize: 18,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
semanticLabel: S.of(context).refund_address,
),
if (widget.isReceiverCard &&
widget.exchangeViewModel.receiveAddress.isEmpty) ...[
@@ -1183,6 +1187,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
size: 36,
iconSize: 20,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
semanticLabel: S.of(context).paste,
),
ModernButton.svg(
svgPath: "assets/new-ui/scan.svg",
@@ -1190,6 +1195,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
size: 36,
iconSize: 20,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
semanticLabel: S.of(context).scan,
),
]
],
+79 -57
View File
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/core/theme_extension.dart';
import 'package:flutter/material.dart';
@@ -60,71 +61,86 @@ class AppsWidget extends StatelessWidget {
end: Alignment.bottomCenter,
),
),
child: TextButton(
onPressed: onTap,
style: TextButton.styleFrom(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(
width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
),
padding: EdgeInsets.all(24),
),
child: Column(
children: [
Row(
// The card is one node; the arrow glyph's "leaves the app" meaning
// becomes a hint instead of an unlabeled icon.
child: MergeSemantics(
child: Semantics(
hint: isLink == true ? S.of(context).opens_externally : null,
child: TextButton(
onPressed: onTap,
style: TextButton.styleFrom(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(
width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
),
padding: EdgeInsets.all(24),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(right: 20),
child: CakeImageWidget(imageUrl: image, height: 54, width: 54),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 6.0,
Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 20),
child: ExcludeSemantics(
child: CakeImageWidget(imageUrl: image, height: 54, width: 54),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 6.0,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w800,
fontSize: 20,
),
softWrap: true,
),
isCake == true
? ExcludeSemantics(
child: CakeImageWidget(
imageUrl: "assets/new-ui/cakelabs-icon.svg",
color: Theme.of(context)
.colorScheme
.onSurfaceVariant),
)
: SizedBox(),
],
),
SizedBox(height: 5),
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w800,
fontSize: 20,
subTitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
softWrap: true,
),
isCake == true
? CakeImageWidget(
imageUrl: "assets/new-ui/cakelabs-icon.svg",
color: Theme.of(context).colorScheme.onSurfaceVariant)
: SizedBox(),
],
),
SizedBox(height: 5),
Text(
subTitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
softWrap: true,
),
ExcludeSemantics(
child: Icon(
isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
),
],
),
)
],
),
Icon(
isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
)
if (hint != null) ...[
SizedBox(height: 10),
hint!,
]
],
),
if (hint != null) ...[
SizedBox(height: 10),
hint!,
]
],
),
),
),
),
@@ -132,10 +148,16 @@ class AppsWidget extends StatelessWidget {
Positioned(
top: 10,
right: 10,
child: IconButton(
icon: Icon(Icons.close),
onPressed: onClose,
//color: Theme.of(context).colorScheme.onSurface,
// Label the icon-only button without adding a visible tooltip.
child: MergeSemantics(
child: Semantics(
label: S.of(context).close,
child: IconButton(
icon: Icon(Icons.close),
onPressed: onClose,
//color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
+1 -1
View File
@@ -107,7 +107,7 @@ class _ChangelogModalState extends State<ChangelogModal> {
NewPrimaryButton(
onPressed: () {
try {
launchUrl(Uri.https("blog.cakewallet.com"));
launchUrl(Uri.https("docs.cakewallet.com", "/release-notes"));
} catch (_) {}
},
text: S.of(context).view_more_info,
@@ -23,47 +23,57 @@ class CoinActionButton extends StatelessWidget {
final size = MediaQuery.of(context).size.width * sizeFactor;
final double effectiveSize = min(size, 80);
return Column(
children: [
Container(
width: effectiveSize,
height: effectiveSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
context.customColors.cardGradientColorPrimary,
context.customColors.cardGradientColorSecondary
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
width: 1,
),
),
child: IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
HapticFeedback.mediumImpact();
action();
},
icon: icon,
color: Theme.of(context).colorScheme.primary,
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
// The caption below the circle is the accessible name, so it is excluded
// from the semantics tree to keep this a single button node.
return MergeSemantics(
child: Semantics(
button: true,
label: label,
child: Column(
children: [
Container(
width: effectiveSize,
height: effectiveSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
context.customColors.cardGradientColorPrimary,
context.customColors.cardGradientColorSecondary
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
label,
),
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
width: 1,
),
),
child: IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
HapticFeedback.mediumImpact();
action();
},
icon: ExcludeSemantics(child: icon),
color: Theme.of(context).colorScheme.primary,
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: ExcludeSemantics(
child: Text(
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
label,
),
),
),
],
),
],
),
);
}
}
@@ -62,6 +62,7 @@ class AssetDetailsModal extends StatelessWidget {
ModalTopBar(
title: "",
trailingIcon: Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: Navigator.of(context).pop,
padding: EdgeInsets.only(top: 12, right: 18),
),
@@ -356,17 +357,22 @@ class AssetDetailsModalBottomButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
spacing: 8,
children: [
ModernButton.svg(
onPressed: onPressed,
svgPath: iconPath,
size: 60,
iconSize: 32,
),
Text(title)
],
// The caption is the button's accessible name, so it is excluded from the
// semantics tree to keep the whole control a single button node.
return MergeSemantics(
child: Column(
spacing: 8,
children: [
ModernButton.svg(
onPressed: onPressed,
svgPath: iconPath,
size: 60,
iconSize: 32,
semanticLabel: title,
),
ExcludeSemantics(child: Text(title))
],
),
);
}
}
@@ -39,125 +39,138 @@ class AssetTile extends StatelessWidget {
Widget build(BuildContext context) {
final iconPath = balance.asset.iconPath ?? "";
return GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return AssetDetailsModal(
showSwap: showSwap,
showBridgeButton: showBridgeButton,
asset: balance.asset,
title: title ?? balance.asset.fullName ?? balance.asset.name,
chainTitle: "",
subtitle: trailingText ?? _getChainTitle(),
amount: showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
currencyTitle: balance.asset.title,
fiatAmount: showSecondary
? balance.fiatSecondAvailableBalance
: balance.fiatAvailableBalance,
iconPath: balance.asset.iconPath ?? "",
chainIconPath: chainIconPath,
mode: modalMode,
wallet: wallet,
);
});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Container(
width: double.infinity,
height: 72,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: isFirst ? Radius.circular(18) : Radius.zero,
bottom: isLast ? Radius.circular(18) : Radius.zero,
),
),
// The row is one control: name, amount and fiat value merge into a single
// button node that opens the asset details sheet.
return MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return AssetDetailsModal(
showSwap: showSwap,
showBridgeButton: showBridgeButton,
asset: balance.asset,
title: title ?? balance.asset.fullName ?? balance.asset.name,
chainTitle: "",
subtitle: trailingText ?? _getChainTitle(),
amount:
showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
currencyTitle: balance.asset.title,
fiatAmount: showSecondary
? balance.fiatSecondAvailableBalance
: balance.fiatAvailableBalance,
iconPath: balance.asset.iconPath ?? "",
chainIconPath: chainIconPath,
mode: modalMode,
wallet: wallet,
);
});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
iconPath.isNotEmpty
? TokenImageWidget(
imageUrl: iconPath,
size: 36,
)
: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
balance.asset.name
.substring(0, min(2, balance.asset.name.length)),
style: TextStyle(
fontSize: 20,
color: Theme.of(context).colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Container(
width: double.infinity,
height: 72,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: isFirst ? Radius.circular(18) : Radius.zero,
bottom: isLast ? Radius.circular(18) : Radius.zero,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Decorative: the asset name is already in the row text.
ExcludeSemantics(
child: iconPath.isNotEmpty
? TokenImageWidget(
imageUrl: iconPath,
size: 36,
)
: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
balance.asset.name
.substring(0, min(2, balance.asset.name.length)),
style: TextStyle(
fontSize: 20,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
),
),
),
),
SizedBox(width: 12.0),
Expanded(
child: Column(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 4,
),
SizedBox(width: 12.0),
Expanded(
child: Column(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title ?? balance.asset.fullName ?? balance.asset.name,
style: TextStyle(fontWeight: FontWeight.w500),
Row(
spacing: 4,
children: [
Text(
title ?? balance.asset.fullName ?? balance.asset.name,
style: TextStyle(fontWeight: FontWeight.w500),
),
if (trailingText != null)
Text(
trailingText!,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
if (trailingText != null)
Text(
trailingText!,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant),
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
"${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
maxLines: 1,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
],
),
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
"${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
maxLines: 1,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
],
),
),
],
),
],
),
),
Text(
showSecondary
? balance.fiatSecondAvailableBalance
: balance.fiatAvailableBalance,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
Text(
showSecondary ? balance.fiatSecondAvailableBalance : balance.fiatAvailableBalance,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
),
),
@@ -59,14 +59,13 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
AssetsSection(
dashboardViewModel: widget.dashboardViewModel,
),
hasAssetsButton ?
AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
() {
Navigator.of(context).pushNamed(
hasAssetsButton ? AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
() {Navigator.of(context).pushNamed(
Routes.homeSettings,
arguments: widget.dashboardViewModel.balanceViewModel,
);
}) : null),
}): null),
AssetsHistorySectionTab(
S.current.history,
HistorySection(
@@ -22,6 +22,8 @@ class AssetsTopBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final actionButton = tabs[selectedTab].actionButton;
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 32.0, bottom: 0.0, left: 12.0, right: 18.0),
@@ -49,40 +51,49 @@ class AssetsTopBar extends StatelessWidget {
key: ValueKey(selectedTab),
spacing: 8,
children: [
Opacity(
opacity: tabs[selectedTab].actionButton != null ? 1 : 0,
child: GestureDetector(
onTap: () {
if (tabs[selectedTab].actionButton != null) {
tabs[selectedTab].actionButton?.onPressed();
}
},
child: Container(
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999999),
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
spacing: 6,
children: [
if ((tabs[selectedTab].actionButton?.title ?? "").isNotEmpty)
Text(
tabs[selectedTab].actionButton?.title ?? "",
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
CakeImageWidget(
imageUrl: tabs[selectedTab].actionButton?.iconPath,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn)),
],
// Built conditionally rather than rendered at opacity 0: an
// invisible chip stayed focusable for screen readers. The
// SizedBox keeps the 40px header height the invisible chip
// used to occupy.
if (actionButton == null)
const SizedBox(height: 40)
else
MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(
onTap: actionButton.onPressed,
child: Container(
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999999),
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
spacing: 6,
children: [
if (actionButton.title.isNotEmpty)
Text(
actionButton.title,
style:
TextStyle(color: Theme.of(context).colorScheme.primary),
),
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: actionButton.iconPath,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn)),
),
],
),
),
),
),
),
),
),
],
),
),
@@ -27,6 +27,7 @@ class HistoryFiltersPage extends StatelessWidget {
ModalTopBar(
title: S.of(context).filters,
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
@@ -25,11 +25,13 @@ class HistoryModal extends StatelessWidget {
ModalTopBar(
title: S.of(context).history,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: CakeImageWidget(
imageUrl: "assets/new-ui/tx_export.svg",
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
trailingSemanticLabel: S.of(context).export_csv,
onTrailingPressed: () =>
CsvExportService().exportToCsv(dashboardViewModel.items, context),
),
@@ -36,6 +36,15 @@ class HistorySection extends StatelessWidget {
final bool roundedTopSection;
final bool detailsAsPage;
/// A history row is a single button node: every text inside it (direction,
/// date, amounts) merges into one label.
Widget _historyRow({required VoidCallback onTap, required Widget child}) => MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(onTap: onTap, child: child),
),
);
@override
Widget build(BuildContext context) {
return SliverPadding(
@@ -90,7 +99,7 @@ class HistorySection extends StatelessWidget {
else
asset = item.assetOfTransaction;
return GestureDetector(
return _historyRow(
onTap: () {
final page =
getIt.get<TransactionDetailsModal>(param1: transaction);
@@ -125,7 +134,7 @@ class HistorySection extends StatelessWidget {
final tradeFrom = trade.from;
final tradeTo = trade.to;
return GestureDetector(
return _historyRow(
onTap: () => Navigator.of(context)
.pushNamed(Routes.tradeDetails, arguments: trade),
child: HistoryTradeTile(
@@ -161,7 +170,7 @@ class HistorySection extends StatelessWidget {
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant)));
} else if (item is OrderListItem) {
return GestureDetector(
return _historyRow(
onTap: () => Navigator.of(context)
.pushNamed(Routes.orderDetails, arguments: item.order),
child: HistoryOrderTile(
@@ -176,7 +185,7 @@ class HistorySection extends StatelessWidget {
} else if (item is PayjoinTransactionListItem) {
final session = item.session;
return GestureDetector(
return _historyRow(
onTap: () => Navigator.of(context).pushNamed(
Routes.payjoinDetails,
arguments: [item.sessionId, item.transaction],
@@ -196,7 +205,7 @@ class HistorySection extends StatelessWidget {
} else if (item is AnonpayTransactionListItem) {
final transactionInfo = item.transaction;
return GestureDetector(
return _historyRow(
onTap: () => Navigator.of(context).pushNamed(
Routes.anonPayDetailsPage,
arguments: transactionInfo),
@@ -24,6 +24,7 @@ class HistorySwapProvidersPage extends StatelessWidget {
ModalTopBar(
title: S.of(context).filter_swap_providers,
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
@@ -142,7 +142,8 @@ class HistoryTile extends StatelessWidget {
date: date,
amount: amount,
amountFiat: amountFiat,
leadingIcon: _getLeadingIcon(context),
// Decorative: `title` already reads out sent/received/pending.
leadingIcon: ExcludeSemantics(child: _getLeadingIcon(context)),
primaryTextColor: _getPrimaryTextColor(),
roundedTop: roundedTop,
roundedBottom: roundedBottom,
@@ -11,46 +11,53 @@ class HistoryTopBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: GestureDetector(
child: Semantics(
button: true,
label: S.of(context).history,
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular(18),
bottom: roundedBottom ? Radius.circular(18) : Radius.zero),
color: Theme.of(context).colorScheme.surfaceContainer),
child: ExcludeSemantics(
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 12,
),
child: Column(
spacing: 12,
children: [
SizedBox.shrink(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular(18),
bottom: roundedBottom ? Radius.circular(18) : Radius.zero),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 12,
),
child: Column(
spacing: 12,
children: [
Text(S.of(context).history),
CakeImageWidget(
imageUrl: "assets/new-ui/arrow_right.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
)
SizedBox.shrink(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).history),
CakeImageWidget(
imageUrl: "assets/new-ui/arrow_right.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
)
],
),
if (!roundedBottom)
Container(
height: 1,
width: double.infinity,
color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175),
)
else
Container(height: 2)
],
),
if (!roundedBottom)
Container(
height: 1,
width: double.infinity,
color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175),
)
else
Container(height: 2)
],
),
),
),
),
@@ -1,24 +1,28 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/address_list_item.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
import "package:cake_wallet/new-ui/widgets/copy_wrapper.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/routes.dart";
import "package:cake_wallet/src/screens/transaction_details/address_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart";
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
import "package:cake_wallet/utils/address_formatter.dart";
import "package:cake_wallet/view_model/transaction_details_view_model.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class TransactionDetailsModal extends StatefulWidget {
const TransactionDetailsModal(
{super.key, required this.transactionDetailsViewModel, this.highlightNoteField = false});
{
required this.transactionDetailsViewModel,
this.highlightNoteField = false,
super.key,
});
final TransactionDetailsViewModel transactionDetailsViewModel;
final bool highlightNoteField;
@@ -48,190 +52,230 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
}
@override
Widget build(BuildContext context) {
final transactionInfoAmount = widget.transactionDetailsViewModel.transactionInfo.amount;
return SafeArea(
bottom: false,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: GestureDetector(
onTap: FocusScope.of(context).unfocus,
child: Container(
decoration: BoxDecoration(
Widget build(BuildContext context) => SafeArea(
bottom: false,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: GestureDetector(
onTap: FocusScope.of(context).unfocus,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(25))),
child: Column(
children: [
ModalTopBar(
title: S.of(context).transaction,
leadingIcon: Icon(Icons.close),
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
child: SingleChildScrollView(
controller: ModalScrollController.of(context),
child: Column(
children: [
TokenImageWidget(
imageUrl:
widget.transactionDetailsViewModel.transactionAsset.iconPath ?? "",
size: 64,
),
SizedBox(height: 10),
Text(
widget.transactionDetailsViewModel.formattedTitle +
widget.transactionDetailsViewModel.formattedStatus,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
CopyWrapper(
requireLongPress: true,
data: ClipboardData(text: transactionInfoAmount.toString()),
builder: (context, copied) => AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: Text(
key: ValueKey(copied),
copied
? S.of(context).copied
: transactionInfoAmount.toStringWithSymbol(),
style: TextStyle(
fontSize: 28,
color: copied
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
borderRadius: const BorderRadius.vertical(top: Radius.circular(25)),
),
child: Column(
children: [
ModalTopBar(
title: S.of(context).transaction,
leadingIcon: const Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
child: SingleChildScrollView(
controller: ModalScrollController.of(context),
child: Column(
children: [
TokenImageWidget(
imageUrl:
widget.transactionDetailsViewModel.transactionAsset.iconPath ?? "",
size: 64,
),
const SizedBox(height: 10),
Text(
widget.transactionDetailsViewModel.formattedTitle +
widget.transactionDetailsViewModel.formattedStatus,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
Observer(
builder: (_) => CopyWrapper(
requireLongPress: true,
data: ClipboardData(
text: widget.transactionDetailsViewModel.transactionCopyAmount,
),
builder: (context, copied) => AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
key: ValueKey(copied),
copied
? S.of(context).copied
: widget.transactionDetailsViewModel.transactionAmount,
style: TextStyle(
fontSize: 28,
color: copied
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 12,
children: [
NewListSections(sections: {
"": widget.transactionDetailsViewModel.items
.map((item) {
if (item.value.isEmpty) return null;
Padding(
padding: const EdgeInsets.all(16),
child: Column(
spacing: 12,
children: [
NewListSections(
sections: {
"": widget.transactionDetailsViewModel.items
.map((item) {
if (item.value.isEmpty) {
return null;
}
final shouldBuildBottomWidget = item.value.length > 25;
final shouldBuildBottomWidget = item.value.length > 25;
return ListItemRegularRow(
copyableText: item.value,
showArrow: false,
keyValue: ((item.key as ValueKey?)?.value as String?) ??
item.title,
label: item.title,
trailingWidget: shouldBuildBottomWidget
? null
: _buildTrailingWIdget(item),
bottomWidget: shouldBuildBottomWidget
? _buildBottomWidget(item)
: null);
})
.whereType<ListItem>()
.toList(),
}),
Container(
decoration: BoxDecoration(
return ListItemRegularRow(
copyableText: item.value,
showArrow: false,
keyValue: ((item.key as ValueKey?)?.value as String?) ??
item.title,
label: item.title,
trailingWidget: shouldBuildBottomWidget
? null
: _buildTrailingWidget(item),
bottomWidget: shouldBuildBottomWidget
? _buildBottomWidget(item)
: null,
);
})
.whereType<ListItem>()
.toList(),
},
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).note),
TextField(
focusNode: noteFocusNode,
controller: noteController,
decoration: InputDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).note),
TextField(
focusNode: noteFocusNode,
controller: noteController,
decoration: InputDecoration(
hintText: S.of(context).add_a_note,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true),
)
],
isDense: true,
),
),
],
),
),
),
),
Observer(
builder: (_) => NewListSections(sections: {
"view tx": [
ListItemRegularRow(
keyValue: "view tx on",
label:
widget.transactionDetailsViewModel.explorerDescription,
onTap: widget.transactionDetailsViewModel.launchExplorer,
foregroundColor: Theme.of(context).colorScheme.primary,
trailingIconPath: "assets/new-ui/link_arrow.svg",
trailingIconSize: 8)
],
if (widget.transactionDetailsViewModel.canReplaceByFee)
"rbf": [
ListItemRegularRow(
keyValue: "replace by fee",
label: S.of(context).bump_fee,
onTap: () {
Navigator.of(context)
.pushNamed(Routes.bumpFeePage, arguments: [
widget.transactionDetailsViewModel.transactionInfo,
widget.transactionDetailsViewModel.rawTransaction
]);
})
]
}),
)
],
Observer(
builder: (_) => NewListSections(
sections: {
"view tx": [
ListItemRegularRow(
keyValue: "view tx on",
label: widget
.transactionDetailsViewModel.explorerDescription,
onTap: widget.transactionDetailsViewModel.launchExplorer,
foregroundColor: Theme.of(context).colorScheme.primary,
trailingIconPath: "assets/new-ui/link_arrow.svg",
trailingIconSize: 8,
),
],
if (widget.transactionDetailsViewModel.canReplaceByFee)
"rbf": [
ListItemRegularRow(
keyValue: "replace by fee",
label: S.of(context).bump_fee,
onTap: () {
Navigator.of(context).pushNamed(
Routes.bumpFeePage,
arguments: [
widget
.transactionDetailsViewModel.transactionInfo,
widget.transactionDetailsViewModel.rawTransaction,
],
);
},
),
],
},
),
),
],
),
),
),
SizedBox(height: MediaQuery.of(context).viewPadding.bottom)
],
SizedBox(height: MediaQuery.of(context).viewPadding.bottom),
],
),
),
),
)
],
],
),
),
),
),
),
);
}
);
Widget _buildTrailingWIdget(TransactionDetailsListItem item) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: switch (item.runtimeType) {
ConfirmationsListItem => Row(
children: [
Text((item as ConfirmationsListItem).current.toString(),
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
if (item.needed > 0)
Text("/${item.needed}",
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant))
],
),
_ => Text(
item.value,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
)
},
);
}
Widget _buildTrailingWidget(TransactionDetailsListItem item) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: switch (item.runtimeType) {
ConfirmationsListItem => Row(
children: [
Text(
(item as ConfirmationsListItem).current.toString(),
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
if (item.needed > 0)
Text(
"/${item.needed}",
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
_ => Text(
item.value,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
)
},
);
Widget _buildBottomWidget(TransactionDetailsListItem item) {
return switch (item.runtimeType) {
AddressListItem => AddressFormatter.buildSegmentedAddress(
address: item.value,
evenTextStyle: TextStyle(
fontSize: 12,
fontFamily: "IBM Plex Mono",
color: Theme.of(context).colorScheme.onSurface)),
AddressListItem => _segmentedAddressList(item.value),
_ => Text(
item.value,
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
)
};
}
Widget _segmentedAddressList(String value) {
final style = TextStyle(
fontSize: 12,
fontFamily: "IBM Plex Mono",
color: Theme.of(context).colorScheme.onSurface,
);
final lines = value
.split(RegExp(r'[,\n]'))
.map((final line) => line.trim())
.where((final line) => line.isNotEmpty)
.toList();
if (lines.length <= 1) {
return AddressFormatter.buildSegmentedAddress(
address: lines.isNotEmpty ? lines.first : value.trim(),
evenTextStyle: style,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final line in lines)
AddressFormatter.buildSegmentedAddress(address: line, evenTextStyle: style),
],
);
}
}
@@ -91,11 +91,14 @@ class BalanceCard extends StatelessWidget {
? ClipRSuperellipse(
borderRadius: BorderRadius.circular(borderRadius),
key: ValueKey(design.imagePath),
child: CakeImageWidget(
imageUrl: design.imagePath,
width: width,
height: height,
fit: BoxFit.fill,
// Purely decorative card artwork.
child: ExcludeSemantics(
child: CakeImageWidget(
imageUrl: design.imagePath,
width: width,
height: height,
fit: BoxFit.fill,
),
),
)
: const SizedBox.shrink(
@@ -146,9 +149,14 @@ class BalanceCard extends StatelessWidget {
AnimatedOpacity(
opacity: selected ? 0 : 1,
duration: textFadeDuration,
child: Text(
accountBalance,
style: TextStyle(color: design.colors.textColor, fontSize: 14),
// Opacity alone keeps the text readable by screen
// readers, so drop it while it is invisible.
child: ExcludeSemantics(
excluding: selected,
child: Text(
accountBalance,
style: TextStyle(color: design.colors.textColor, fontSize: 14),
),
),
),
],
@@ -156,40 +164,45 @@ class BalanceCard extends StatelessWidget {
AnimatedOpacity(
opacity: selected ? 1 : 0,
duration: textFadeDuration,
child: AnimatedSwitcher(
duration: designSwitchDuration,
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
// Only the selected card's balance is visible, so only it
// may be announced.
child: ExcludeSemantics(
excluding: !selected,
child: AnimatedSwitcher(
duration: designSwitchDuration,
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
child: Row(
key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
spacing: 8.0,
children: [
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context).style.copyWith(
color: design.colors.textColor,
fontSize: 28,
fontWeight: FontWeight.w500,
letterSpacing: -0.4),
child: Text(fiatFirst ? fiatBalance : balance),
),
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context).style.copyWith(
color: design.colors.textColorSecondary,
fontSize: 28,
fontWeight: FontWeight.w400,
letterSpacing: -0.4),
child: Text(resolvedAssetName),
),
],
);
},
child: Row(
key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
spacing: 8.0,
children: [
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context).style.copyWith(
color: design.colors.textColor,
fontSize: 28,
fontWeight: FontWeight.w500,
letterSpacing: -0.4),
child: Text(fiatFirst ? fiatBalance : balance),
),
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context).style.copyWith(
color: design.colors.textColorSecondary,
fontSize: 28,
fontWeight: FontWeight.w400,
letterSpacing: -0.4),
child: Text(resolvedAssetName),
),
],
),
),
),
),
@@ -250,7 +263,10 @@ class BalanceCard extends StatelessWidget {
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: design.backgroundType == CardDesignBackgroundTypes.svgIcon
? _CornerSvgIcon(design: design, iconWidth: iconWidth)
// Purely decorative card artwork.
? ExcludeSemantics(
child: _CornerSvgIcon(design: design, iconWidth: iconWidth),
)
: const SizedBox.shrink(
key: ValueKey('svgIconOff'),
),
@@ -266,18 +282,28 @@ class BalanceCard extends StatelessWidget {
child: AnimatedOpacity(
duration: designSwitchDuration,
opacity: onCustomizeTapped == null ? 0 : 1,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onCustomizeTapped,
child: Container(
height: 40,
width: 40,
child: Center(
child: CakeImageWidget(
imageUrl: "assets/new-ui/3dots_vertical.svg",
alignment: Alignment.topRight,
colorFilter:
ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
// Faded out and inert: must not be a focusable phantom control.
child: ExcludeSemantics(
excluding: onCustomizeTapped == null,
child: Semantics(
button: true,
label: S.of(context).wallet_menu,
onTap: onCustomizeTapped,
child: GestureDetector(
excludeFromSemantics: true,
behavior: HitTestBehavior.opaque,
onTap: onCustomizeTapped,
child: Container(
height: 40,
width: 40,
child: Center(
child: CakeImageWidget(
imageUrl: "assets/new-ui/3dots_vertical.svg",
alignment: Alignment.topRight,
colorFilter:
ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
),
),
),
),
),
@@ -289,27 +315,34 @@ class BalanceCard extends StatelessWidget {
);
}
Widget getBalanceCardActionButton(BalanceCardAction action) => GestureDetector(
Widget getBalanceCardActionButton(BalanceCardAction action) => Semantics(
button: true,
label: action.label,
onTap: action.onTap,
child: Container(
decoration: BoxDecoration(
color: design.colors.backgroundImageColor.withAlpha(75),
borderRadius: BorderRadius.circular(10000000),
),
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(right: 6),
child: Text(
action.label,
style: TextStyle(color: design.colors.textColor, fontSize: 16),
),
child: ExcludeSemantics(
child: GestureDetector(
onTap: action.onTap,
child: Container(
decoration: BoxDecoration(
color: design.colors.backgroundImageColor.withAlpha(75),
borderRadius: BorderRadius.circular(10000000),
),
Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
],
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(right: 6),
child: Text(
action.label,
style: TextStyle(color: design.colors.textColor, fontSize: 16),
),
),
Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
],
),
),
),
),
);
@@ -77,6 +77,32 @@ class _CardsViewState extends State<CardsView> {
final left = (parentWidth - effectiveCardWidth) / 2.0;
final isSelected = _selectedIndex == visualIndex;
final accounts = widget.accountListViewModel?.accounts;
final cardLabel = (accounts != null && realIndex < accounts.length)
? accounts[realIndex].label
: S.of(context).balance;
void onCardTap() {
// printV(visualIndex);
if (compactMode && visualIndex != 0) {
widget.onCompactModeBackgroundCardsTapped();
} else if (!compactMode) {
setState(() {
if (widget.accountListViewModel != null)
widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
_selectedIndex = visualIndex;
});
}
}
void onCardLongPress() {
if (_selectedIndex == visualIndex) {
widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
}
HapticFeedback.heavyImpact();
}
return AnimatedPositioned(
key: ValueKey("$visualIndex $realIndex"),
duration: animDuration,
@@ -87,127 +113,125 @@ class _CardsViewState extends State<CardsView> {
duration: animDuration,
curve: Curves.easeOut,
scale: scale,
child: GestureDetector(
onTap: () {
// printV(visualIndex);
if (compactMode && visualIndex != 0) {
widget.onCompactModeBackgroundCardsTapped();
} else if (!compactMode) {
setState(() {
if (widget.accountListViewModel != null)
widget.accountListViewModel!
.select(widget.accountListViewModel!.accounts[realIndex]);
_selectedIndex = visualIndex;
});
}
},
onLongPress: () {
if (_selectedIndex == visualIndex) {
widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
}
;
HapticFeedback.heavyImpact();
},
child: Observer(builder: (_) {
if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
return Container();
}
final account = widget.accountListViewModel?.accounts[realIndex];
// The second balance should always be the lightning balance
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel
.getMainBalanceRecord(widget.lightningMode);
late final String walletBalance;
late final String walletFiatBalance;
if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
if (widget.dashboardViewModel.balanceViewModel.displayMode ==
BalanceDisplayMode.hiddenBalance) {
walletBalance = '●●●●●●';
walletFiatBalance = '●●●●●●';
} else {
walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
// The card is the tap target; the balances and the card's own buttons stay
// reachable as children of this node.
child: Semantics(
button: true,
selected: isSelected,
label: cardLabel,
hint: isSelected
? (widget.dashboardViewModel.balanceViewModel.displayMode ==
BalanceDisplayMode.hiddenBalance
? S.of(context).long_press_show_balance
: S.of(context).long_press_hide_balance)
: null,
onTap: onCardTap,
onLongPress: isSelected ? onCardLongPress : null,
child: GestureDetector(
excludeFromSemantics: true,
onTap: onCardTap,
onLongPress: onCardLongPress,
child: Observer(builder: (_) {
if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
return Container();
}
} else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
walletBalance = "";
walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
} else {
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
}
final account = widget.accountListViewModel?.accounts[realIndex];
// the card designs is empty if widget gets built before it loads.
// should get populated before user sees anything
final CardDesign cardDesign;
if (widget.dashboardViewModel.cardDesigns.isEmpty ||
realIndex >= widget.dashboardViewModel.cardDesigns.length)
cardDesign = CardDesign.genericDefault;
else if (widget.lightningMode)
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
else
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
// The second balance should always be the lightning balance
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel
.getMainBalanceRecord(widget.lightningMode);
final String accountName;
final String accountBalance;
if (account == null) {
accountName = "";
accountBalance = "";
} else {
accountName = account.label;
accountBalance = account.balance ?? "0.00";
}
late final String walletBalance;
late final String walletFiatBalance;
if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
if (widget.dashboardViewModel.balanceViewModel.displayMode ==
BalanceDisplayMode.hiddenBalance) {
walletBalance = '●●●●●●';
walletFiatBalance = '●●●●●●';
} else {
walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
}
} else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
walletBalance = "";
walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
} else {
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
}
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
? ""
: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
// the card designs is empty if widget gets built before it loads.
// should get populated before user sees anything
final CardDesign cardDesign;
if (widget.dashboardViewModel.cardDesigns.isEmpty ||
realIndex >= widget.dashboardViewModel.cardDesigns.length)
cardDesign = CardDesign.genericDefault;
else if (widget.lightningMode)
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
else
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
final List<BalanceCardAction> actions = widget.lightningMode
? [
BalanceCardAction(
label: S.current.bitcoin_lightning_deposit,
icon: Icons.arrow_downward,
onTap: depositToL2,
),
BalanceCardAction(
label: S.current.bitcoin_lightning_withdraw,
icon: Icons.arrow_upward,
onTap: withdrawFromL2,
)
]
: widget.dashboardViewModel.isEnabledTradeAction
? [
BalanceCardAction(
label: S.current.buy,
icon: Icons.arrow_forward_ios_rounded,
iconSize: 12,
onTap: () {
final String accountName;
final String accountBalance;
if (account == null) {
accountName = "";
accountBalance = "";
} else {
accountName = account.label;
accountBalance = account.balance ?? "0.00";
}
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
? ""
: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
final List<BalanceCardAction> actions = widget.lightningMode
? [
BalanceCardAction(
label: S.current.bitcoin_lightning_deposit,
icon: Icons.arrow_downward,
onTap: depositToL2,
),
BalanceCardAction(
label: S.current.bitcoin_lightning_withdraw,
icon: Icons.arrow_upward,
onTap: withdrawFromL2,
)
]
: widget.dashboardViewModel.isEnabledTradeAction
? [
BalanceCardAction(
label: S.current.buy,
icon: Icons.arrow_forward_ios_rounded,
iconSize: 12,
onTap: () {
showModalBottomSheet(
context: context, builder: (context) => BuySellSelectorModal());
},
)
]
: [];
)
]
: [];
return BalanceCard(
width: effectiveCardWidth,
accountName: accountName,
accountBalance: accountBalance,
designSwitchDuration: Duration(milliseconds: 150),
assetName: assetName,
capitalizeAssetName: _shouldCapitalizeAssetName(),
balance: walletBalance,
fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ??
widget.dashboardViewModel.settingsStore.fiatCurrency.title,
fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
fiatBalance: walletFiatBalance,
selected: _selectedIndex == visualIndex,
onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
design: cardDesign,
actions: actions,
);
}),
return BalanceCard(
width: effectiveCardWidth,
accountName: accountName,
accountBalance: accountBalance,
designSwitchDuration: Duration(milliseconds: 150),
assetName: assetName,
capitalizeAssetName: _shouldCapitalizeAssetName(),
balance: walletBalance,
fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ??
widget.dashboardViewModel.settingsStore.fiatCurrency.title,
fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
fiatBalance: walletFiatBalance,
selected: _selectedIndex == visualIndex,
onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
design: cardDesign,
actions: actions,
);
}),
),
),
),
);
+51 -39
View File
@@ -21,51 +21,63 @@ class MwebAd extends StatelessWidget {
child: Column(
spacing: 12,
children: [
GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
child: Container(
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/settings_row_icons/mweb.svg",
width: 24,
height: 24,
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(
S.of(context).mweb_ad,
softWrap: true,
style: TextStyle(fontSize: 12),
MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
child: Container(
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: "assets/new-ui/settings_row_icons/mweb.svg",
width: 24,
height: 24,
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(
S.of(context).mweb_ad,
softWrap: true,
style: TextStyle(fontSize: 12),
),
),
),
Icon(
size: 16,
Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.primary,
)
],
),
Icon(
size: 16,
Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.primary,
)
],
),
),
),
),
),
GestureDetector(
onTap: () => dashboardViewModel.dismissMwebAd(false),
child: Text(
S.of(context).do_not_show_anymore,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary),
MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(
onTap: () => dashboardViewModel.dismissMwebAd(false),
child: Text(
S.of(context).do_not_show_anymore,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary),
),
),
),
)
],
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
@@ -27,10 +28,16 @@ class ChainIcon extends StatelessWidget {
AnimatedOpacity(
duration: Duration(milliseconds: 100),
opacity: done ? 0 : 1,
child: CircularProgressIndicator(
value: progress,
color: Color(0xFFFFB84E),
strokeWidth: 2,
// Faded out means "nothing to report", so it must leave the tree too.
child: ExcludeSemantics(
excluding: done,
child: CircularProgressIndicator(
value: progress,
color: Color(0xFFFFB84E),
strokeWidth: 2,
semanticsLabel: S.of(context).synchronizing,
semanticsValue: "${(progress * 100).round()}%",
),
),
),
AnimatedScale(
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -12,76 +13,84 @@ class LightningSwitcher extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
child: InkWell(
onTap: () {
HapticFeedback.mediumImpact();
onLightningSwitchPress();
},
child: Container(
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
color: Theme.of(context).colorScheme.surfaceContainer),
width: 70,
height: 36,
padding: EdgeInsets.symmetric(vertical: 2),
child: Stack(
children: [
AnimatedContainer(
alignment: Alignment.centerRight,
margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
duration: Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
width: 32,
height: 32,
// height: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
color: Theme.of(context).colorScheme.primary),
),
Container(
child: Row(
spacing: 2.0,
children: [
SizedBox(),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
key: ValueKey(lightningMode),
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
BlendMode.srcIn,
),
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/switcher-lightning.svg',
key: ValueKey(lightningMode),
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
],
// One toggle node: the knob position and the coloured glyphs are the only
// visual cue for which mode is active.
return Semantics(
button: true,
toggled: lightningMode,
label: S.of(context).lightning_mode,
child: SizedBox(
child: InkWell(
onTap: () {
HapticFeedback.mediumImpact();
onLightningSwitchPress();
},
child: Container(
decoration: ShapeDecoration(
shape:
RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
color: Theme.of(context).colorScheme.surfaceContainer),
width: 70,
height: 36,
padding: EdgeInsets.symmetric(vertical: 2),
child: Stack(
children: [
AnimatedContainer(
alignment: Alignment.centerRight,
margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
duration: Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
width: 32,
height: 32,
// height: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
color: Theme.of(context).colorScheme.primary),
),
),
],
Container(
child: Row(
spacing: 2.0,
children: [
SizedBox(),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
key: ValueKey(lightningMode),
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
BlendMode.srcIn,
),
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/switcher-lightning.svg',
key: ValueKey(lightningMode),
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
],
),
),
],
),
),
),
),
@@ -1,5 +1,6 @@
import 'package:cake_wallet/core/sync_status_title.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
@@ -47,65 +48,55 @@ class SyncBar extends StatelessWidget {
child: Stack(
alignment: Alignment.centerLeft,
children: [
if (!_showFullBar())
Row(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
if (dashboardViewModel.isTorEnabled)
CakeImageWidget(
imageUrl: "assets/new-ui/tor.svg",
width: 20,
height: 20,
),
if (_showDot()) PulsingDot(),
],
),
if (!_showFullBar()) _buildCompactBar(context),
if (_showFullBar())
GestureDetector(
onTap: () {
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
child: Material(
child: getIt.get<ManageNodesPage>(param1: false),
)));
},
child: AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: Container(
key: ValueKey(status.runtimeType),
height: 36,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999),
border: _getBorder(context, status.runtimeType),
color: _getBackgroundColor(context, status.runtimeType),
),
child: Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
if (icon != null) icon,
// if (dashboardViewModel.silentPaymentsScanningActive &&
// progressStatuses.contains(status.runtimeType)) ...[
// Text(
// "${(status.progress() * 100).toInt()}%",
// style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
// ),
// Text(
// "·",
// style: TextStyle(fontSize: 12),
// )
// ],
Text(
syncStatusTitle(
status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
style: _getTextStyle(context, status.runtimeType),
// A single node: the localized status text (plus any active
// Tor/MWEB/Silent Payments badge) is the label, and the hint says
// where tapping leads. Everything inside is redundant with it.
Semantics(
button: true,
label: _statusSemanticsLabel(context, status),
hint: S.of(context).manage_nodes,
onTap: () => _openNodeManagement(context),
child: ExcludeSemantics(
child: GestureDetector(
onTap: () => _openNodeManagement(context),
child: AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: Container(
key: ValueKey(status.runtimeType),
height: 36,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999),
border: _getBorder(context, status.runtimeType),
color: _getBackgroundColor(context, status.runtimeType),
),
],
child: Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
if (icon != null) icon,
// if (dashboardViewModel.silentPaymentsScanningActive &&
// progressStatuses.contains(status.runtimeType)) ...[
// Text(
// "${(status.progress() * 100).toInt()}%",
// style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
// ),
// Text(
// "·",
// style: TextStyle(fontSize: 12),
// )
// ],
Text(
syncStatusTitle(status,
dashboardViewModel.settingsStore.syncStatusDisplayMode),
style: _getTextStyle(context, status.runtimeType),
),
],
),
),
),
),
),
@@ -117,6 +108,55 @@ class SyncBar extends StatelessWidget {
);
}
void _openNodeManagement(BuildContext context) =>
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
child: Material(
child: getIt.get<ManageNodesPage>(param1: false),
)));
/// Compact mode shows sync state with a pulsing dot (and a Tor glyph) only, so
/// the whole row needs a text equivalent.
Widget _buildCompactBar(BuildContext context) {
final row = Row(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
if (dashboardViewModel.isTorEnabled)
CakeImageWidget(
imageUrl: "assets/new-ui/tor.svg",
width: 20,
height: 20,
),
if (_showDot()) PulsingDot(),
],
);
final label = _joinLabels([
if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
if (_showDot()) S.of(context).synchronizing,
]);
if (label.isEmpty) return row;
return Semantics(label: label, child: ExcludeSemantics(child: row));
}
String _statusSemanticsLabel(BuildContext context, SyncStatus status) {
final isFailure = failStatuses.contains(status.runtimeType);
return _joinLabels([
syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
if (!isFailure && dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
if (!isFailure && dashboardViewModel.hasMweb) S.of(context).litecoin_mweb,
if (!isFailure && dashboardViewModel.hasSilentPayments) S.of(context).silent_payments,
]);
}
String _joinLabels(List<String> parts) => parts.where((part) => part.isNotEmpty).join(", ");
Color? _getBackgroundColor(BuildContext context, Type status) {
if (failStatuses.contains(status)) {
return Theme.of(context).colorScheme.errorContainer.withAlpha(64);
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart';
@@ -53,6 +54,7 @@ class TopBar extends StatelessWidget {
onSettingsButtonPress();
},
svgPath: "assets/new-ui/top-settings.svg",
semanticLabel: S.of(context).settings_title,
),
],
),
@@ -1,8 +1,10 @@
import 'package:cake_wallet/generated/i18n.dart';
import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
@@ -21,6 +23,7 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
return Observer(builder: (_) {
final show = dashboardViewModel.balanceViewModel
.hasAdditionalBalance(dashboardViewModel.wallet.currency);
final isIronwoodMigration = dashboardViewModel.isMigratingToIronwood;
return AnimatedSize(
duration: const Duration(milliseconds: 300),
@@ -44,13 +47,17 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
color: Colors.transparent,
child: InkWell(
onTap: () {
final modal = isIronwoodMigration
? ZcashMigrationModal(
hasContinue: false, balance: "${balance} ${currency.symbol}")
: UnconfirmedBalanceModal(
balance: balance.toStringWithSymbol(),
currencyIconPath: currency.iconPath ?? "",
);
showMaterialModalBottomSheet(
backgroundColor: Colors.transparent,
context: context,
builder: (context) => UnconfirmedBalanceModal(
balance: balance.toStringWithSymbol(),
currencyIconPath: currency.iconPath ?? "",
));
builder: (context) => modal);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
@@ -63,8 +70,12 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
child: isIronwoodMigration
? CupertinoActivityIndicator(
color: Theme.of(context).colorScheme.primary,
)
: CircularProgressIndicator(
strokeWidth: 2,
backgroundColor:
Theme.of(context).colorScheme.primary.withAlpha(50),
color: Theme.of(context).colorScheme.primary,
@@ -80,7 +91,7 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
color: Theme.of(context).colorScheme.primary,
),
),
Text(S.of(context).confirming),
Text(isIronwoodMigration ? S.of(context).migrating_to_ironwood : S.of(context).confirming),
],
)
],
@@ -125,6 +136,7 @@ class UnconfirmedBalanceModal extends StatelessWidget {
ModalTopBar(
title: S.of(context).balance_confirmation,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
@@ -171,3 +183,4 @@ class UnconfirmedBalanceModal extends StatelessWidget {
);
}
}
+29 -13
View File
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/wallet_info.dart';
@@ -20,15 +21,22 @@ class WalletInfoBar extends StatelessWidget {
final bool hasCustomize;
final VoidCallback onCustomizeButtonTap;
void _openAccountCustomizer() {
if (hasCustomize) {
onCustomizeButtonTap();
HapticFeedback.mediumImpact();
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (hasCustomize) {
onCustomizeButtonTap();
HapticFeedback.mediumImpact();
}
},
// The row, the hardware-wallet glyph and the inner accounts button are one
// control for a screen reader: a single labeled node opening the customizer.
final semanticsLabel =
hardwareWalletType == null ? name : "$name, ${S.of(context).hardware_wallet}";
final row = GestureDetector(
onTap: _openAccountCustomizer,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
@@ -69,18 +77,26 @@ class WalletInfoBar extends StatelessWidget {
SizedBox(width: 8),
ModernButton.svg(
size: 24,
onPressed: () {
if (hasCustomize) {
onCustomizeButtonTap();
HapticFeedback.mediumImpact();
}
},
onPressed: _openAccountCustomizer,
svgPath: "assets/new-ui/icon-accounts.svg",
semanticLabel: S.of(context).wallet_accounts,
)
]
],
),
);
if (!hasCustomize) {
return Semantics(label: semanticsLabel, child: ExcludeSemantics(child: row));
}
return Semantics(
button: true,
label: semanticsLabel,
hint: S.of(context).wallet_accounts,
onTap: _openAccountCustomizer,
child: ExcludeSemantics(child: row),
);
}
String? get hardwareWalletIcon {
@@ -0,0 +1,152 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:flutter/material.dart";
import "package:url_launcher/url_launcher.dart";
class ZcashMigrationModal extends StatelessWidget {
const ZcashMigrationModal({required this.hasContinue, required this.balance, super.key});
final String balance;
final bool hasContinue;
@override
Widget build(BuildContext context) => SafeArea(
bottom: false,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),),
child: SafeArea(
top: false,
child: Column(
children: [
ModalTopBar(
title: S.of(context).zcash_network_upgrade,
trailingIcon: const Icon(Icons.close),
onTrailingPressed: Navigator.of(context).pop,
trailingSemanticLabel: S.of(context).close,
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Expanded(
child: Column(
spacing: 40,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 146,
height: 79,
child: Stack(
children: [
const Positioned(
left: 67,
top: 0,
bottom: 0,
child: CakeImageWidget(
imageUrl: "assets/new-ui/ironwood_migration.svg",
width: 75,
height: 75,
),
),
Positioned(
left: 0,
child: Container(
width: 79,
height: 79,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(99999999),
border: Border.all(
color: Theme.of(context).colorScheme.surface,
width: 4,),),
child: const CakeImageWidget(
imageUrl: "assets/new-ui/crypto_full_icons/zcash.svg",
height: 75,
width: 75,
),
),
),
],
),
),
Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surfaceContainer,),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4,
children: [
Text(
balance,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,),
),
Text(S.of(context).is_currently_being_migrated),
],
),
),),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
TextSpan(
text: "${S.of(context).zcash_network_upgrade_desc_1}\n\n",
style: Theme.of(context).textTheme.bodyMedium,),
TextSpan(
text: "${S.of(context).zcash_network_upgrade_desc_2}\n\n",
style: Theme.of(context).textTheme.bodyMedium,),
TextSpan(
text: "${S.of(context).zcash_network_upgrade_desc_3}\n\n",
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: const Color(0xFFF9C03C),
),
),
],
),
),
],
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
spacing: 12,
children: [
NewPrimaryButton(
onPressed: _launchDocs,
text: S.of(context).learn_more,
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.primary,),
if (hasContinue)
NewPrimaryButton(
onPressed: Navigator.of(context).pop,
text: S.of(context).continue_text,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,),
],
),
),
],
),
),
),
);
void _launchDocs() {
try {
launchUrl(Uri.parse("https://docs.cakewallet.com/cryptos/zcash/ironwood-migration"));
} catch (_) {}
}
}
+57 -8
View File
@@ -1,8 +1,10 @@
import 'dart:io';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
class CopyWrapper extends StatefulWidget {
@@ -11,13 +13,25 @@ class CopyWrapper extends StatefulWidget {
this.data,
this.requireLongPress = false,
this.isSensitive = false,
required this.builder,
this.duration = const Duration(milliseconds: 1200)});
this.builder,
this.controlBuilder,
this.duration = const Duration(milliseconds: 1200)})
: assert((builder == null) != (controlBuilder == null),
"Provide exactly one of builder or controlBuilder");
final ClipboardData? data;
final bool isSensitive;
final bool requireLongPress;
final Widget Function(BuildContext, bool) builder;
/// Builds content that this widget makes copyable with its own gesture
/// detector. Receives the transient "copied" state.
final Widget Function(BuildContext, bool)? builder;
/// Builds a control that performs the copy itself, so no extra gesture
/// detector or actionable semantics node is stacked on top of it. [onCopy] is
/// null when there is nothing to copy.
final Widget Function(BuildContext context, bool copied, VoidCallback? onCopy)? controlBuilder;
final Duration duration;
@override
@@ -31,6 +45,11 @@ class _CopyWrapperState extends State<CopyWrapper> {
if (widget.data == null) return;
ClipboardUtil.setSensitiveDataToClipboard(widget.data!, isSensitive: widget.isSensitive);
HapticFeedback.mediumImpact();
// Screen readers learn about the copy from the transient "copied" state
// below, which build() marks as a live region, instead of from a push
// announcement: those are deprecated on Android and make TalkBack drop
// whatever it was saying. Where we don't render that state, android 13+
// already shows its own clipboard confirmation.
if (await shouldShowCopied()) {
setState(() => copied = true);
Future.delayed(widget.duration, () {
@@ -41,11 +60,41 @@ class _CopyWrapperState extends State<CopyWrapper> {
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: widget.requireLongPress ? null : handleCopy,
onLongPress: !widget.requireLongPress ? null : handleCopy,
child: widget.builder(context, copied),
final VoidCallback? onCopy = widget.data == null ? null : handleCopy;
// The control's own label carries the copied state (Copy -> Copied), so
// merging it into one node and marking that node a live region while copied
// is enough for a screen reader to pick the change up.
if (widget.controlBuilder != null) {
return MergeSemantics(
child: Semantics(
liveRegion: copied,
child: widget.controlBuilder!(context, copied, onCopy),
),
);
}
final content = widget.builder!(context, copied);
// Nothing to copy: don't leave behind a gesture detector that a screen
// reader would present as an actionable target that does nothing.
if (onCopy == null) return content;
return MergeSemantics(
child: Semantics(
button: !widget.requireLongPress,
liveRegion: copied,
hint: widget.requireLongPress ? S.of(context).long_press_to_copy : S.of(context).copy,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
CustomSemanticsAction(label: S.of(context).copy): onCopy,
},
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: widget.requireLongPress ? null : onCopy,
onLongPress: !widget.requireLongPress ? null : onCopy,
child: content,
),
),
);
}
@@ -56,24 +56,31 @@ class _ChainChip extends StatelessWidget {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsetsDirectional.only(end: 8),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(80),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: isSelected ? colors.primary : colors.surfaceContainer,
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: isSelected ? 14 : 12,
letterSpacing: -0.07,
color: isSelected ? colors.onPrimary : colors.primary,
),
child: MergeSemantics(
child: Semantics(
button: true,
selected: isSelected,
inMutuallyExclusiveGroup: true,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(80),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: isSelected ? colors.primary : colors.surfaceContainer,
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: isSelected ? 14 : 12,
letterSpacing: -0.07,
color: isSelected ? colors.onPrimary : colors.primary,
),
),
),
),
),
),
@@ -24,55 +24,65 @@ class CurrencyPickerRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: Row(
children: [
_IconWithBadge(currency: currency, badgePath: chainBadgePath),
const SizedBox(width: 12),
Expanded(
child: Row(
children: [
Flexible(
child: Text(
currency.fullName ?? currency.title,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
return MergeSemantics(
child: Semantics(
button: true,
selected: isSelected,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: Row(
children: [
ExcludeSemantics(
child: _IconWithBadge(currency: currency, badgePath: chainBadgePath),
),
const SizedBox(width: 12),
Expanded(
child: Row(
children: [
Flexible(
child: Text(
currency.fullName ?? currency.title,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
if (chainPillLabel != null) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
chainPillLabel!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: colors.onSecondaryContainer,
),
),
),
],
],
),
if (chainPillLabel != null) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
),
child: Text(
chainPillLabel!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: colors.onSecondaryContainer,
),
),
),
],
],
),
),
const SizedBox(width: 8),
trailing,
const SizedBox(width: 8),
ExcludeSemantics(
child: Icon(
Icons.chevron_right,
size: 20,
color: isSelected ? colors.primary : colors.onSurfaceVariant,
),
),
],
),
const SizedBox(width: 8),
trailing,
const SizedBox(width: 8),
Icon(
Icons.chevron_right,
size: 20,
color: isSelected ? colors.primary : colors.onSurfaceVariant,
),
],
),
),
),
);
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
@@ -27,30 +28,41 @@ class CurrencyPickerSearchField extends StatelessWidget {
),
child: Row(
children: [
Icon(Icons.search, size: 20, color: colors.primary),
ExcludeSemantics(child: Icon(Icons.search, size: 20, color: colors.primary)),
const SizedBox(width: 10),
Expanded(
child: TextFormField(
controller: controller,
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
isCollapsed: true,
border: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: hintText,
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
// The hint names the field only while it is empty, so the label is
// supplied once there is text to keep exactly one announcement.
child: MergeSemantics(
child: Semantics(
label: controller.text.isEmpty ? null : hintText,
child: TextFormField(
controller: controller,
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
isCollapsed: true,
border: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: hintText,
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
),
),
),
),
),
if (controller.text.isNotEmpty)
InkWell(
onTap: controller.clear,
customBorder: const CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(Icons.close, size: 16, color: colors.onSurfaceVariant),
Semantics(
button: true,
label: S.of(context).clear,
child: InkWell(
onTap: controller.clear,
customBorder: const CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(Icons.close, size: 16, color: colors.onSurfaceVariant),
),
),
),
],
@@ -43,6 +43,7 @@ class CurrencyPickerSheet extends StatelessWidget {
ModalTopBar(
title: S.of(context).select_asset,
leadingIcon: const Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: () => Navigator.of(context).maybePop(),
),
Expanded(
@@ -117,6 +117,7 @@ class _FiatCurrencyPickerSheetState extends State<FiatCurrencyPickerSheet> {
ModalTopBar(
title: S.of(context).select_fiat_currency_title,
leadingIcon: const Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: () => Navigator.of(context).maybePop(),
),
Expanded(
@@ -35,14 +35,18 @@ class SelectNetworkPage extends StatelessWidget {
ModalTopBar(
title: S.of(context).select_network_title,
leadingIcon: const Icon(Icons.arrow_back),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).maybePop(),
),
const SizedBox(height: 24),
CakeImageWidget(
imageUrl: assetIconPath,
width: 75,
height: 75,
fit: BoxFit.cover,
// Illustration of the asset named by the title below it.
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: assetIconPath,
width: 75,
height: 75,
fit: BoxFit.cover,
),
),
const SizedBox(height: 12),
Text(
@@ -106,33 +110,42 @@ class _NetworkRow extends StatelessWidget {
final networkIconPath =
wt != null ? getCryptoCurrencyIconForWalletListItem(wt) : variant.iconPath;
final networkName = chainNameForCurrency(variant);
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
child: Row(
children: [
CakeImageWidget(
imageUrl: networkIconPath,
width: 28,
height: 28,
fit: BoxFit.cover,
return MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
child: Row(
children: [
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: networkIconPath,
width: 28,
height: 28,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
networkName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
ExcludeSemantics(
child: Icon(
Icons.chevron_right,
size: 20,
color: colors.onSurfaceVariant,
),
),
],
),
const SizedBox(width: 12),
Expanded(
child: Text(
networkName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
Icon(
Icons.chevron_right,
size: 20,
color: colors.onSurfaceVariant,
),
],
),
),
),
);
+42 -46
View File
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import "dart:io";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
class DigitInputController {
String _text = "";
@@ -19,38 +21,37 @@ class DigitInputController {
}
class DigitInputPill extends StatelessWidget {
const DigitInputPill({super.key, this.digit, required this.highlighted});
const DigitInputPill({required this.highlighted, super.key, this.digit});
final String? digit;
final bool highlighted;
@override
Widget build(BuildContext context) {
return Container(
width: 51,
height: 84,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
border:
highlighted ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2) : null,
),
child: Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text(digit ?? " ", style: TextStyle(fontSize: 40)),
Widget build(BuildContext context) => Container(
width: 51,
height: 84,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
border: highlighted
? Border.all(color: Theme.of(context).colorScheme.primary, width: 2)
: null,
),
),
);
}
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(digit ?? " ", style: const TextStyle(fontSize: 40)),
),
),
);
}
class DigitInput extends StatefulWidget {
const DigitInput({
super.key,
required this.controller,
required this.desiredLength,
this.breakAt = 3,
super.key,
});
final DigitInputController controller;
@@ -74,12 +75,9 @@ class _DigitInputState extends State<DigitInput> implements TextInputClient {
if (_textInputConnection == null || !_textInputConnection!.attached) {
_textInputConnection = TextInput.attach(
this,
const TextInputConfiguration(
inputType: TextInputType.numberWithOptions(
signed: false,
decimal: false,
),
inputAction: TextInputAction.none,
TextInputConfiguration(
inputType: TextInputType.number,
inputAction: Platform.isIOS ? TextInputAction.done : TextInputAction.none,
),
);
}
@@ -102,30 +100,28 @@ class _DigitInputState extends State<DigitInput> implements TextInputClient {
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _openVirtualKeyboard,
child: SizedBox(
height: 84,
child: ListView.separated(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => DigitInputPill(
digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
highlighted: widget.controller.text.length == index,
Widget build(BuildContext context) => GestureDetector(
onTap: _openVirtualKeyboard,
child: SizedBox(
height: 84,
child: ListView.separated(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => DigitInputPill(
digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
highlighted: widget.controller.text.length == index,
),
separatorBuilder: (context, index) =>
SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
itemCount: widget.desiredLength,
),
separatorBuilder: (context, index) =>
SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
itemCount: widget.desiredLength,
),
),
);
}
);
@override
void updateEditingValue(TextEditingValue value) {
final RegExp numberRegExp = RegExp(r'^\d{0,6}$');
final RegExp numberRegExp = RegExp(r"^\d{0,6}$");
if (numberRegExp.hasMatch(value.text)) {
setState(() => widget.controller.text = value.text);
+38 -31
View File
@@ -11,38 +11,45 @@ class DropdownRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: Radius.circular(18),
bottom: expanded ? Radius.zero : Radius.circular(18),
return Semantics(
button: true,
expanded: expanded,
label: text,
onTap: onTap,
excludeSemantics: true,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: Radius.circular(18),
bottom: expanded ? Radius.zero : Radius.circular(18),
),
),
),
height: 48,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
text,
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.primary),
),
AnimatedRotation(
duration: Duration(milliseconds: 300),
turns: expanded ? 0.0 : 0.5,
curve: Curves.easeOut,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg",
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
))
],
height: 48,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
text,
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.primary),
),
AnimatedRotation(
duration: Duration(milliseconds: 300),
turns: expanded ? 0.0 : 0.5,
curve: Curves.easeOut,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
))
],
),
),
),
),
@@ -106,6 +106,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
Icons.close,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => showPopUp(
context: context,
builder: (context) => AlertWithTwoActions(
@@ -143,6 +144,7 @@ class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProce
iconColor: Theme.of(context).colorScheme.onPrimary,
size: 36,
icon: Icon(Icons.arrow_forward),
semanticLabel: S.of(context).confirm,
onPressed: () =>
widget.trezorConnectVM.setParingPin(_controller.text.trim()),
),
@@ -0,0 +1,224 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart";
import "package:cake_wallet/routes.dart";
import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
import "package:cake_wallet/src/widgets/base_alert_dialog.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/src/widgets/primary_button.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_info.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
class SyncKeyImagesSheet extends StatefulWidget {
const SyncKeyImagesSheet({
required this.appStore,
required this.trezorConnectVM,
super.key,
});
final AppStore appStore;
final TrezorConnectViewModelBase trezorConnectVM;
WalletBase get wallet => appStore.wallet!;
@override
State<SyncKeyImagesSheet> createState() => _HardwareWalletProceedOnDeviceSheetState();
}
class _HardwareWalletProceedOnDeviceSheetState extends State<SyncKeyImagesSheet> {
_KeyImageSyncState _state = _KeyImageSyncState.initial;
@override
Widget build(BuildContext context) => PopScope(
canPop: false,
child: SafeArea(
bottom: false,
child: Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
children: [
ModalTopBar(
title: S.of(context).resync_device,
leadingIcon: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
onLeadingPressed: () => showPopUp(
context: context,
builder: (context) => AlertWithTwoActions(
alertTitle: S.of(context).are_you_sure_exit,
alertContent: S.of(context).resync_device_cancel_warning_desc,
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).yes_exit,
rightAlertButtonStyle: AlertButtonStyle.error(context),
actionLeftButton: Navigator.of(context).pop,
actionRightButton: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
),
),
),
Expanded(
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CakeImageWidget(
imageUrl: hardwareWalletIcon,
width: 100,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
DirectionalAnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: content,
),
],
),
),
Padding(
padding: const EdgeInsets.all(24),
child: AnimatedOpacity(
curve: Curves.easeOutQuad,
opacity: _state == _KeyImageSyncState.initial ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: PrimaryButton(
text: S.of(context).continue_text,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
onPressed: _onContinuePressed,
),
),
),
],
),
),
),
),
),
);
Widget get content {
if (_state == _KeyImageSyncState.syncing) {
return _syncingKeyImages();
}
return _initial();
}
String? get hardwareWalletIcon {
switch (widget.wallet.hardwareWalletType) {
case HardwareWalletType.bitbox:
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
case HardwareWalletType.ledger:
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
case HardwareWalletType.trezor:
return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
case HardwareWalletType.cupcake:
return "assets/images/cupcake.svg";
case HardwareWalletType.coldcard:
case HardwareWalletType.seedsigner:
case HardwareWalletType.keystone:
case null:
return "assets/images/hardware_wallet/device_qr.svg";
}
}
Widget _initial() => Column(
key: const ValueKey(0),
spacing: 12,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
S.of(context).resync_device_desc,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
);
Widget _syncingKeyImages() => Column(
key: const ValueKey(1),
spacing: 12,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
S.of(context).proceed_on_device,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 20,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text(
S.of(context).proceed_on_device_description,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
)
],
);
Future<void> _onContinuePressed() async {
setState(() => _state = _KeyImageSyncState.syncing);
if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) {
await Navigator.of(context).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: widget.wallet.type,
hardwareWalletType: widget.wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (_, __) {
widget.trezorConnectVM.initWallet(widget.wallet);
Navigator.of(context).pop();
},
isReconnect: false,
),
);
// Recheck to handle tap-backs
if (!widget.trezorConnectVM.isConnected(widget.wallet.type)) {
setState(() => _state = _KeyImageSyncState.initial);
return;
}
} else {
await widget.trezorConnectVM.initWallet(widget.wallet);
}
final result = await widget.trezorConnectVM.syncKeyImages(widget.wallet);
if (result && context.mounted) {
Navigator.of(context).pop();
}
}
}
enum _KeyImageSyncState {
initial,
syncing;
}
@@ -32,6 +32,7 @@ class _HardwareWalletTrezorParingSheetState extends State<HardwareWalletTrezorPa
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {},
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
),
Padding(
padding: const EdgeInsets.all(18.0),
+33 -24
View File
@@ -93,33 +93,42 @@ class _LineTabSwitcherState extends State<LineTabSwitcher> {
children: widget.tabs.map((item) {
final index = widget.tabs.indexOf(item);
return GestureDetector(
onTap: () {
widget.onTabChange(index);
},
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedDefaultTextStyle(
duration: Duration(milliseconds: 150),
style: DefaultTextStyle.of(context).style.copyWith(
inherit: true,
fontSize: 16,
fontWeight: FontWeight.w500,
color: widget.selectedTab == index
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurfaceVariant,
// One tab = one node: the visible label merges into a button node
// that also carries the selected state of this tab.
return MergeSemantics(
child: Semantics(
button: true,
selected: widget.selectedTab == index,
inMutuallyExclusiveGroup: true,
child: GestureDetector(
onTap: () {
widget.onTabChange(index);
},
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedDefaultTextStyle(
duration: Duration(milliseconds: 150),
style: DefaultTextStyle.of(context).style.copyWith(
inherit: true,
fontSize: 16,
fontWeight: FontWeight.w500,
color: widget.selectedTab == index
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurfaceVariant,
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: itemPadding / 2),
child: Text(
item,
key: textWidgetKeys[index],
),
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: itemPadding / 2),
child: Text(
item,
key: textWidgetKeys[index],
),
),
],
),
],
),
),
);
}).toList())),
+31 -24
View File
@@ -60,31 +60,38 @@ class _LongPressMenuState extends State<LongPressMenu> {
final color = item.color ?? Theme.of(context).colorScheme.onSurface;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: item.onSelected,
child: Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 12,
),
child: Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 8,
children: [
CakeImageWidget(
imageUrl: item.iconPath,
height: 20,
width: 20,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
onTap: item.onSelected,
child: Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 12,
),
child: Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 8,
children: [
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: item.iconPath,
height: 20,
width: 20,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
),
),
Text(item.label,
style: TextStyle(
color: color, fontSize: 14, fontWeight: FontWeight.w500)),
],
),
Text(item.label,
style: TextStyle(
color: color, fontSize: 14, fontWeight: FontWeight.w500)),
],
),
),
),
),
+44 -26
View File
@@ -11,6 +11,11 @@ class ModernButton extends StatelessWidget {
final Color? backgroundColor;
final double? iconSize;
final String? label;
/// Accessible name announced by screen readers. Falls back to [label] (the
/// visible caption) when omitted, so one of the two is required. Must be
/// localized by the caller.
final String? semanticLabel;
static const iconSvgSizeRatio = 2 / 3;
const ModernButton(
@@ -21,7 +26,8 @@ class ModernButton extends StatelessWidget {
this.iconSize,
this.iconColor,
this.backgroundColor,
this.label})
this.label,
this.semanticLabel})
: svgPath = null;
const ModernButton.svg(
@@ -32,7 +38,8 @@ class ModernButton extends StatelessWidget {
this.iconSize,
this.iconColor,
this.backgroundColor,
this.label})
this.label,
this.semanticLabel})
: icon = null;
@override
@@ -56,31 +63,42 @@ class ModernButton extends StatelessWidget {
child: icon!,
);
return Column(
spacing: 8,
children: [
Container(
decoration: BoxDecoration(
color: resolvedBackgroundColor,
borderRadius: BorderRadius.circular(size),
),
width: size,
height: size,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: onPressed,
icon: resolvedIcon,
),
// A single accessibility node for the whole control: the visible caption
// duplicates the accessible name, so it is excluded to avoid a second,
// detached stop for screen readers.
return MergeSemantics(
child: Semantics(
button: true,
label: semanticLabel ?? label,
child: Column(
spacing: 8,
children: [
Container(
decoration: BoxDecoration(
color: resolvedBackgroundColor,
borderRadius: BorderRadius.circular(size),
),
width: size,
height: size,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: onPressed,
icon: ExcludeSemantics(child: resolvedIcon),
),
),
if (label != null && label!.isNotEmpty)
ExcludeSemantics(
child: Text(
label!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface),
),
)
],
),
if (label != null && label!.isNotEmpty)
Text(
label!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface),
)
],
),
);
}
}
@@ -0,0 +1,165 @@
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/currency.dart";
import "package:flutter/widgets.dart";
import "package:flutter_bloc/flutter_bloc.dart";
/// The [CurrencySymbolText] widget displays [Currency.symbol] based on
/// usersettings as a formated string of text with single style.
class CurrencySymbolText extends StatelessWidget {
const CurrencySymbolText(
this.currency, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaler,
this.maxLines,
this.semanticsLabel,
this.semanticsIdentifier,
this.textWidthBasis,
this.textHeightBehavior,
this.selectionColor,
this.useBaseUnit,
});
/// The currency to display.
final Currency currency;
/// If non-null, the style to use for this text.
///
/// If the style's "inherit" property is true, the style will be merged with
/// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
/// replace the closest enclosing [DefaultTextStyle].
///
/// The user or platform may override this [style]'s [TextStyle.fontWeight],
/// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing]
/// via a [MediaQuery] ancestor's [MediaQueryData.boldText],
/// [MediaQueryData.lineHeightScaleFactorOverride],
/// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride]
/// regardless of its [TextStyle.inherit] value.
final TextStyle? style;
/// The user or platform may override this [strutStyle]'s [StrutStyle.height]
/// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride].
final StrutStyle? strutStyle;
/// How the text should be aligned horizontally.
final TextAlign? textAlign;
/// The directionality of the text.
///
/// This decides how [textAlign] values like [TextAlign.start] and
/// [TextAlign.end] are interpreted.
///
/// This is also used to disambiguate how to render bidirectional text. For
/// example, if the [data] is an English phrase followed by a Hebrew phrase,
/// in a [TextDirection.ltr] context the English phrase will be on the left
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
/// context, the English phrase will be on the right and the Hebrew phrase on
/// its left.
///
/// Defaults to the ambient [Directionality], if any.
final TextDirection? textDirection;
/// Used to select a font when the same Unicode character can
/// be rendered differently, depending on the locale.
///
/// It's rarely necessary to set this property. By default its value
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
///
/// See [RenderParagraph.locale] for more information.
final Locale? locale;
/// Whether the text should break at soft line breaks.
///
/// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
final bool? softWrap;
/// How visual overflow should be handled.
///
/// If this is null [TextStyle.overflow] will be used, otherwise the value
/// from the nearest [DefaultTextStyle] ancestor will be used.
final TextOverflow? overflow;
final TextScaler? textScaler;
/// An optional maximum number of lines for the text to span, wrapping if necessary.
/// If the text exceeds the given number of lines, it will be truncated according
/// to [overflow].
///
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
/// edge of the box.
///
/// If this is null, but there is an ambient [DefaultTextStyle] that specifies
/// an explicit number for its [DefaultTextStyle.maxLines], then the
/// [DefaultTextStyle] value will take precedence. You can use a [RichText]
/// widget directly to entirely override the [DefaultTextStyle].
final int? maxLines;
/// An alternative semantics label for this text.
///
/// If present, the semantics of this widget will contain this value instead
/// of the actual text. This will overwrite any of the semantics labels applied
/// directly to the [TextSpan]s.
///
/// This is useful for replacing abbreviations or shorthands with the full
/// text value:
///
/// ```dart
/// const Text(r'$$', semanticsLabel: 'Double dollars')
/// ```
final String? semanticsLabel;
/// A unique identifier for the semantics node for this widget.
///
/// This is useful for cases where the text widget needs to have a uniquely
/// identifiable ID that is recognized through the automation tools without
/// having a dependency on the actual content of the text that can possibly be
/// dynamic in nature.
final String? semanticsIdentifier;
final TextWidthBasis? textWidthBasis;
final TextHeightBehavior? textHeightBehavior;
/// The color to use when painting the selection.
///
/// This is ignored if [SelectionContainer.maybeOf] returns null
/// in the [BuildContext] of the [Text] widget.
///
/// If null, the ambient [DefaultSelectionStyle] is used (if any); failing
/// that, the selection color defaults to [DefaultSelectionStyle.defaultColor]
/// (semi-transparent grey).
final Color? selectionColor;
/// Show the amount in the base unit format of [Money.currency]
///
/// If null, the displayAmountsInSatoshi setting is used.
final bool? useBaseUnit;
@override
Widget build(BuildContext context) => BlocBuilder<MoneySettingsCubit, MoneySettingsState>(
builder: (context, state) => Text(
state.getSymbol(currency, overrideSettings: useBaseUnit),
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textDirection: textDirection,
locale: locale,
softWrap: softWrap,
overflow: overflow,
textScaler: textScaler,
maxLines: maxLines,
semanticsLabel: semanticsLabel,
semanticsIdentifier: semanticsIdentifier,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
selectionColor: selectionColor,
),
);
}
@@ -0,0 +1,78 @@
import "package:bloc/bloc.dart";
import "package:cake_wallet/entities/balance_display_mode.dart";
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
import "package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart";
import "package:cake_wallet/store/settings_store.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency.dart";
import "package:mobx/mobx.dart";
class MoneySettingsCubit extends Cubit<MoneySettingsState> {
MoneySettingsCubit(SettingsStore _settingsStore)
: super(
MoneySettingsState(
bitcoinAmountDisplayMode: _settingsStore.displayAmountsInSatoshi,
displayMode: _settingsStore.balanceDisplayMode,
),
) {
_bitcoinAmountDisplayModeDisposer = reaction(
(_) => _settingsStore.displayAmountsInSatoshi,
(displayMode) => emit(state.copyWith(bitcoinAmountDisplayMode: displayMode)),
);
_displayModeDisposer = reaction(
(_) => _settingsStore.balanceDisplayMode,
(displayMode) => emit(state.copyWith(displayMode: displayMode)),
);
}
late final ReactionDisposer _bitcoinAmountDisplayModeDisposer;
late final ReactionDisposer _displayModeDisposer;
@override
Future<void> close() {
if (!_bitcoinAmountDisplayModeDisposer.reaction.isDisposed) {
_bitcoinAmountDisplayModeDisposer.reaction.dispose();
}
if (!_displayModeDisposer.reaction.isDisposed) {
_displayModeDisposer.reaction.dispose();
}
return super.close();
}
}
class MoneySettingsState {
const MoneySettingsState({required this.bitcoinAmountDisplayMode, required this.displayMode});
final BitcoinAmountDisplayMode bitcoinAmountDisplayMode;
final BalanceDisplayMode displayMode;
bool useBaseUnit(Currency currency) =>
([CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency) &&
bitcoinAmountDisplayMode == BitcoinAmountDisplayMode.satoshi) ||
(CryptoCurrency.btcln == currency &&
bitcoinAmountDisplayMode == BitcoinAmountDisplayMode.satoshiForLightning);
bool get isHidden => displayMode == BalanceDisplayMode.hiddenBalance;
String getSymbol(Currency currency, {bool? overrideSettings}) {
if (overrideSettings == null) {
return useBaseUnit(currency) ? "sats" : currency.symbol.safeSubString(0, 8);
}
if (overrideSettings == true && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) {
return "sats";
}
return currency.symbol.safeSubString(0, 8);
}
MoneySettingsState copyWith({
BitcoinAmountDisplayMode? bitcoinAmountDisplayMode,
BalanceDisplayMode? displayMode,
}) =>
MoneySettingsState(
bitcoinAmountDisplayMode: bitcoinAmountDisplayMode ?? this.bitcoinAmountDisplayMode,
displayMode: displayMode ?? this.displayMode,
);
}
@@ -0,0 +1,15 @@
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
import "package:cake_wallet/store/settings_store.dart";
import "package:flutter/material.dart";
import "package:flutter_bloc/flutter_bloc.dart";
class MoneySettingsProvider extends StatelessWidget {
const MoneySettingsProvider({required this.settingsStore, this.child, super.key});
final Widget? child;
final SettingsStore settingsStore;
@override
Widget build(BuildContext context) =>
BlocProvider(create: (_) => MoneySettingsCubit(settingsStore), child: child);
}
+256
View File
@@ -0,0 +1,256 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
import "package:cw_core/amount/money.dart";
import "package:flutter/widgets.dart";
import "package:flutter_bloc/flutter_bloc.dart";
/// The [MoneyText] widget displays [Money] as a formated string of text with single style.
class MoneyText extends StatelessWidget {
const MoneyText(
this.amount, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaler,
this.maxLines,
this.semanticsLabel,
this.semanticsIdentifier,
this.textWidthBasis,
this.textHeightBehavior,
this.selectionColor,
this.isHiddenAmount,
this.useBaseUnit,
this.fractionalDigits = 8,
this.showSymbol = true,
this.withSymbolPrefix = false,
this.trimZeros = true,
});
/// The [MoneyText.optional] returns a widget displaying [Money] as a
/// formated string or an [SizedBox.shrink].
static Widget optional(
Money? amount, {
Key? key,
TextStyle? style,
StrutStyle? strutStyle,
TextAlign? textAlign,
TextDirection? textDirection,
Locale? locale,
bool? softWrap,
TextOverflow? overflow,
TextScaler? textScaler,
int? maxLines,
String? semanticsLabel,
String? semanticsIdentifier,
TextWidthBasis? textWidthBasis,
TextHeightBehavior? textHeightBehavior,
Color? selectionColor,
bool? isHiddenAmount,
bool? useBaseUnit,
int fractionalDigits = 8,
bool showSymbol = true,
bool withSymbolPrefix = false,
bool trimZeros = true,
}) =>
amount != null
? MoneyText(
amount,
key: key,
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textDirection: textDirection,
locale: locale,
softWrap: softWrap,
overflow: overflow,
textScaler: textScaler,
maxLines: maxLines,
semanticsLabel: semanticsLabel,
semanticsIdentifier: semanticsIdentifier,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
selectionColor: selectionColor,
isHiddenAmount: isHiddenAmount,
useBaseUnit: useBaseUnit,
fractionalDigits: fractionalDigits,
showSymbol: showSymbol,
withSymbolPrefix: withSymbolPrefix,
trimZeros: trimZeros,
)
: const SizedBox.shrink();
/// The amount to display.
final Money amount;
/// If non-null, the style to use for this text.
///
/// If the style's "inherit" property is true, the style will be merged with
/// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
/// replace the closest enclosing [DefaultTextStyle].
///
/// The user or platform may override this [style]'s [TextStyle.fontWeight],
/// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing]
/// via a [MediaQuery] ancestor's [MediaQueryData.boldText],
/// [MediaQueryData.lineHeightScaleFactorOverride],
/// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride]
/// regardless of its [TextStyle.inherit] value.
final TextStyle? style;
/// The user or platform may override this [strutStyle]'s [StrutStyle.height]
/// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride].
final StrutStyle? strutStyle;
/// How the text should be aligned horizontally.
final TextAlign? textAlign;
/// The directionality of the text.
///
/// This decides how [textAlign] values like [TextAlign.start] and
/// [TextAlign.end] are interpreted.
///
/// This is also used to disambiguate how to render bidirectional text. For
/// example, if the [data] is an English phrase followed by a Hebrew phrase,
/// in a [TextDirection.ltr] context the English phrase will be on the left
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
/// context, the English phrase will be on the right and the Hebrew phrase on
/// its left.
///
/// Defaults to the ambient [Directionality], if any.
final TextDirection? textDirection;
/// Used to select a font when the same Unicode character can
/// be rendered differently, depending on the locale.
///
/// It's rarely necessary to set this property. By default its value
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
///
/// See [RenderParagraph.locale] for more information.
final Locale? locale;
/// Whether the text should break at soft line breaks.
///
/// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
final bool? softWrap;
/// How visual overflow should be handled.
///
/// If this is null [TextStyle.overflow] will be used, otherwise the value
/// from the nearest [DefaultTextStyle] ancestor will be used.
final TextOverflow? overflow;
final TextScaler? textScaler;
/// An optional maximum number of lines for the text to span, wrapping if necessary.
/// If the text exceeds the given number of lines, it will be truncated according
/// to [overflow].
///
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
/// edge of the box.
///
/// If this is null, but there is an ambient [DefaultTextStyle] that specifies
/// an explicit number for its [DefaultTextStyle.maxLines], then the
/// [DefaultTextStyle] value will take precedence. You can use a [RichText]
/// widget directly to entirely override the [DefaultTextStyle].
final int? maxLines;
/// An alternative semantics label for this text.
///
/// If present, the semantics of this widget will contain this value instead
/// of the actual text. This will overwrite any of the semantics labels applied
/// directly to the [TextSpan]s.
///
/// This is useful for replacing abbreviations or shorthands with the full
/// text value:
///
/// ```dart
/// const Text(r'$$', semanticsLabel: 'Double dollars')
/// ```
final String? semanticsLabel;
/// A unique identifier for the semantics node for this widget.
///
/// This is useful for cases where the text widget needs to have a uniquely
/// identifiable ID that is recognized through the automation tools without
/// having a dependency on the actual content of the text that can possibly be
/// dynamic in nature.
final String? semanticsIdentifier;
final TextWidthBasis? textWidthBasis;
final TextHeightBehavior? textHeightBehavior;
/// The color to use when painting the selection.
///
/// This is ignored if [SelectionContainer.maybeOf] returns null
/// in the [BuildContext] of the [Text] widget.
///
/// If null, the ambient [DefaultSelectionStyle] is used (if any); failing
/// that, the selection color defaults to [DefaultSelectionStyle.defaultColor]
/// (semi-transparent grey).
final Color? selectionColor;
/// Show the amount in the base unit format of [Money.currency]
///
/// If null, the displayAmountsInSatoshi setting is used.
final bool? useBaseUnit;
/// A limit for the display of the fractional digits of the amount
final int fractionalDigits;
/// Hide the amount
///
/// If null, the value from the balanceDisplayMode setting is used.
final bool? isHiddenAmount;
/// Show the currency symbol
final bool showSymbol;
/// Prefix the amount with the currency symbol if [showSymbol] is true
final bool withSymbolPrefix;
/// Trim the zeros at the end of an amount
final bool trimZeros;
@override
Widget build(BuildContext context) => BlocBuilder<MoneySettingsCubit, MoneySettingsState>(
builder: (context, state) => Text(
isHiddenAmount ?? state.isHidden
? "●●●●●●"
: showSymbol
? amount.toLocalStringWithSymbol(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit ?? state.useBaseUnit(amount.currency),
withSymbolPrefix: withSymbolPrefix,
locale: (locale ?? Localizations.localeOf(context)).toString(),
)
: amount.toLocalStringWithPrecision(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit ?? state.useBaseUnit(amount.currency),
locale: (locale ?? Localizations.localeOf(context)).toString(),
),
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textDirection: textDirection,
locale: locale,
softWrap: softWrap,
overflow: overflow,
textScaler: textScaler,
maxLines: maxLines,
semanticsLabel: isHiddenAmount ?? state.isHidden
? (semanticsLabel ?? S.of(context).amount_hidden)
: semanticsLabel,
semanticsIdentifier: semanticsIdentifier,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
selectionColor: selectionColor,
),
);
}
@@ -0,0 +1,57 @@
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:flutter/material.dart";
import "package:flutter_svg/flutter_svg.dart";
class NewFuturePrimaryButton extends StatefulWidget {
const NewFuturePrimaryButton({
required this.onPressed,
required this.text,
required this.color,
required this.textColor,
this.borderColor = Colors.transparent,
this.disabled = false,
this.image,
super.key,
});
final Future<void> Function() onPressed;
final bool disabled;
final SvgPicture? image;
final Color color;
final Color textColor;
final Color borderColor;
final String text;
@override
State<StatefulWidget> createState() => _NewFuturePrimaryButtonState();
}
class _NewFuturePrimaryButtonState extends State<NewFuturePrimaryButton> {
bool isLoading = false;
Future<void> _onPressed() async {
if (isLoading) {
return;
}
setState(() => isLoading = true);
try {
await widget.onPressed.call();
} finally {
if (mounted) {
setState(() => isLoading = false);
}
}
}
@override
Widget build(BuildContext context) => NewPrimaryButton(
onPressed: _onPressed,
image: widget.image,
text: widget.text,
color: widget.color,
textColor: widget.textColor,
isLoading: isLoading,
borderColor: widget.borderColor,
disabled: widget.disabled,
);
}
+3
View File
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
@@ -77,6 +78,7 @@ class _NewPickerState<Item> extends State<NewPicker<Item>> {
ModalTopBar(
title: widget.title,
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).maybePop,
),
Padding(
@@ -387,6 +389,7 @@ class _PickerSliderPageState<Item> extends State<PickerSliderPage<Item>> {
ModalTopBar(
title: widget.title,
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
@@ -4,6 +4,7 @@ import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
class PayjoinCopyModal extends StatelessWidget {
@@ -25,6 +26,7 @@ class PayjoinCopyModal extends StatelessWidget {
ModalTopBar(
title: S.of(context).select_address_to_copy,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
@@ -32,7 +34,7 @@ class PayjoinCopyModal extends StatelessWidget {
"": [
ListItemRegularRow(
keyValue: "btc",
label: "Standard",
label: S.of(context).standard,
iconPath: "assets/new-ui/pjmodal_btc.svg",
onTap: () {
Clipboard.setData(
@@ -41,6 +43,7 @@ class PayjoinCopyModal extends StatelessWidget {
? BitcoinURI(amount: uri.amount, address: uri.address).toString()
: uri.address),
);
_announceCopied(context);
Navigator.of(context).pop();
}),
ListItemRegularRow(
@@ -51,6 +54,7 @@ class PayjoinCopyModal extends StatelessWidget {
Clipboard.setData(
ClipboardData(text: uri.toString()),
);
_announceCopied(context);
Navigator.of(context).pop();
})
]
@@ -62,4 +66,15 @@ class PayjoinCopyModal extends StatelessWidget {
),
);
}
// Copying pops this sheet right away, so no widget survives to carry a
// semantics state change: this path still needs a direct announcement. It is
// skipped on platforms that deprecate announcements (android, where they
// clear TalkBack's speech queue and the system shows its own clipboard
// confirmation anyway).
void _announceCopied(BuildContext context) {
if (!MediaQuery.supportsAnnounceOf(context)) return;
SemanticsService.sendAnnouncement(
View.of(context), S.of(context).copied, Directionality.of(context));
}
}
@@ -42,52 +42,64 @@ class ReceiveAddressTypeDisplay extends StatelessWidget {
iconPath = "assets/new-ui/address-type-picker-icons/litecoin.svg";
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _showPicker(context),
child: Row(
key: ValueKey("$text$largeQrMode"),
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12.0,
children: [
if (iconPath != null)
CakeImageWidget(
imageUrl: iconPath,
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
Text(
text,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.primary,
),
),
if (!largeQrMode)
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999),
),
child: IconButton(
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
onPressed: () => _showPicker(context),
icon: (Icon(
// The row and the chevron button trigger the same picker, so only the
// row is exposed and the chevron is treated as decoration.
return MergeSemantics(
child: Semantics(
button: true,
label: S.of(context).address_type,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _showPicker(context),
child: Row(
key: ValueKey("$text$largeQrMode"),
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12.0,
children: [
if (iconPath != null)
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: iconPath,
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
Text(
text,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.primary,
size: 20,
Icons.keyboard_arrow_down,
)),
),
),
),
],
if (!largeQrMode)
ExcludeSemantics(
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999),
),
child: IconButton(
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
onPressed: () => _showPicker(context),
icon: (Icon(
color: Theme.of(context).colorScheme.primary,
size: 20,
Icons.keyboard_arrow_down,
)),
),
),
),
],
),
),
),
);
},
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/widgets/section_divider.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
@@ -34,6 +35,7 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
@override
Widget build(BuildContext context) {
return Observer(builder: (final _) {
final commonOptions = widget.receiveOptionViewModel.options
.where((element) =>
element.isCommon ||
@@ -71,6 +73,7 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
ModalTopBar(
title: S.of(context).address_type,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {}),
SizedBox(height: 24),
@@ -131,38 +134,49 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
bottom: _otherOptionsExpanded ? Radius.zero : Radius.circular(20)),
child: InkWell(
highlightColor: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
bottom: !_otherOptionsExpanded ? Radius.zero : Radius.circular(20)),
onTap: () {
setState(() {
_otherOptionsExpanded = !_otherOptionsExpanded;
});
},
child: Container(
height: 64.0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).more_options,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.primary),
child: MergeSemantics(
child: Semantics(
button: true,
expanded: _otherOptionsExpanded,
child: InkWell(
highlightColor:
Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
bottom: !_otherOptionsExpanded
? Radius.zero
: Radius.circular(20)),
onTap: () {
setState(() {
_otherOptionsExpanded = !_otherOptionsExpanded;
});
},
child: Container(
height: 64.0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).more_options,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.primary),
),
ExcludeSemantics(
child: AnimatedRotation(
duration: ReceiveAddressTypeSelector
.otherOptionsExpandDuration,
turns: _otherOptionsExpanded ? 0.0 : 0.5,
curve: Curves.easeOut,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg")),
)
],
),
AnimatedRotation(
duration:
ReceiveAddressTypeSelector.otherOptionsExpandDuration,
turns: _otherOptionsExpanded ? 0.0 : 0.5,
curve: Curves.easeOut,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg"))
],
),
),
),
),
@@ -177,40 +191,45 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
duration: ReceiveAddressTypeSelector.otherOptionsExpandDuration,
curve: Curves.easeOut,
alignment: Alignment.topCenter,
child: Container(
height: _otherOptionsExpanded ? null : 0,
child: ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: NeverScrollableScrollPhysics(),
itemCount: otherOptions.length,
itemBuilder: (context, index) {
final opt = otherOptions[index];
// Collapsed to zero height: keep the options out of
// the semantics tree so they cannot be focused.
child: ExcludeSemantics(
excluding: !_otherOptionsExpanded,
child: Container(
height: _otherOptionsExpanded ? null : 0,
child: ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: NeverScrollableScrollPhysics(),
itemCount: otherOptions.length,
itemBuilder: (context, index) {
final opt = otherOptions[index];
return ReceiveAddressTypeRow(
option: opt,
roundedTop: false,
roundedBottom: index == otherOptions.length - 1,
selected:
widget.receiveOptionViewModel.selectedReceiveOption == opt,
onItemTap: () {
Navigator.of(context).pop(opt);
},
receiveOptionViewModel: widget.receiveOptionViewModel,
);
},
separatorBuilder: (context, index) {
if ((widget.receiveOptionViewModel.selectedReceiveOption ==
otherOptions[index]) ||
(index != otherOptions.length - 1 &&
widget.receiveOptionViewModel.selectedReceiveOption ==
otherOptions[index + 1])) return Container();
return ReceiveAddressTypeRow(
option: opt,
roundedTop: false,
roundedBottom: index == otherOptions.length - 1,
selected:
widget.receiveOptionViewModel.selectedReceiveOption == opt,
onItemTap: () {
Navigator.of(context).pop(opt);
},
receiveOptionViewModel: widget.receiveOptionViewModel,
);
},
separatorBuilder: (context, index) {
if ((widget.receiveOptionViewModel.selectedReceiveOption ==
otherOptions[index]) ||
(index != otherOptions.length - 1 &&
widget.receiveOptionViewModel.selectedReceiveOption ==
otherOptions[index + 1])) return Container();
return Padding(
padding: const EdgeInsets.only(left: 44.0, right: 36.0),
child: HorizontalSectionDivider(),
);
},
return Padding(
padding: const EdgeInsets.only(left: 44.0, right: 36.0),
child: HorizontalSectionDivider(),
);
},
),
),
),
),
@@ -223,6 +242,7 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
],
)),
);
});
}
}
@@ -260,65 +280,75 @@ class ReceiveAddressTypeRow extends StatelessWidget {
top: roundedTop ? Radius.circular(20) : Radius.zero,
bottom: roundedBottom ? Radius.circular(20) : Radius.zero,
),
child: InkWell(
onTap: onItemTap,
child: Container(
height: rowHeight,
decoration: BoxDecoration(
color: selected
? Theme.of(context).colorScheme.surfaceContainerHigh
: Colors.transparent,
borderRadius: BorderRadius.vertical(
top: roundedTop ? Radius.circular(20) : Radius.zero,
bottom: roundedBottom ? Radius.circular(20) : Radius.zero,
)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Row(
// The row itself carries the selection state; the checkbox next to it is
// only a visual echo of it and must not be announced separately.
child: MergeSemantics(
child: Semantics(
selected: selected,
inMutuallyExclusiveGroup: true,
child: InkWell(
onTap: onItemTap,
child: Container(
height: rowHeight,
decoration: BoxDecoration(
color: selected
? Theme.of(context).colorScheme.surfaceContainerHigh
: Colors.transparent,
borderRadius: BorderRadius.vertical(
top: roundedTop ? Radius.circular(20) : Radius.zero,
bottom: roundedBottom ? Radius.circular(20) : Radius.zero,
)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
if (iconPath != null)
CakeImageWidget(
imageUrl: iconPath!,
width: iconSize,
height: iconSize,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
)
else
Container(width: iconSize, height: iconSize),
Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
option.value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface),
Row(
children: [
if (iconPath != null)
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: iconPath!,
width: iconSize,
height: iconSize,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
)
else
Container(width: iconSize, height: iconSize),
Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
option.value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface),
),
if (option.description != null)
Text(
option.description!,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant),
)
],
),
if (option.description != null)
Text(
option.description!,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant),
)
],
),
)
)
],
),
ExcludeSemantics(child: RoundedCheckbox(value: selected))
],
),
RoundedCheckbox(value: selected)
],
),
),
),
),
@@ -22,50 +22,54 @@ class ReceiveAmountDisplay extends StatelessWidget {
curve: Curves.easeOutCubic,
heightFactor: largeQrMode || walletAddressListViewModel.displayAmount.isEmpty ? 0 : 1,
alignment: Alignment.topCenter,
child: Container(
width: double.infinity,
child: Row(
spacing: 4,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
spacing: 8.0,
children: [
Text(
walletAddressListViewModel.displayAmount,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 16,
fontWeight: FontWeight.w500),
),
Text(walletAddressListViewModel.cryptoCurrencySymbol,
// Amount, currency symbol and fiat equivalent describe one value, so
// they are announced as a single node.
child: MergeSemantics(
child: Container(
width: double.infinity,
child: Row(
spacing: 4,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
spacing: 8.0,
children: [
Text(
walletAddressListViewModel.displayAmount,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 16,
fontWeight: FontWeight.w500))
],
fontWeight: FontWeight.w500),
),
Text(walletAddressListViewModel.cryptoCurrencySymbol,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 16,
fontWeight: FontWeight.w500))
],
),
),
),
),
if (!walletAddressListViewModel.isFiatDisabled)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
_getFiatAmount(),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 16,
fontWeight: FontWeight.w500),
),
)
],
if (!walletAddressListViewModel.isFiatDisabled)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
_getFiatAmount(),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 16,
fontWeight: FontWeight.w500),
),
)
],
),
),
),
),
@@ -56,6 +56,7 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {},
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
),
Padding(
padding: const EdgeInsets.all(18.0),
@@ -65,54 +66,68 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
spacing: 12,
children: [
if (widget.walletAddressListViewModel.hasTokensList) ...[
Text("Token"),
GestureDetector(
onTap: () {
_presentTokenCurrencyPicker(context);
},
child: Observer(
builder: (_) => Container(
height: 60,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
spacing: 8,
children: [
TokenImageWidget(
imageUrl: widget.walletAddressListViewModel.tokenCurrency
?.iconPath ??
widget.walletAddressListViewModel.currencies.first
.iconPath ??
"",
size: 32,
),
Text((widget.walletAddressListViewModel.tokenCurrency ??
widget.walletAddressListViewModel.currencies.first
as CryptoCurrency)
.title
.toUpperCase())
],
),
RotatedBox(
quarterTurns: 2,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg"))
],
// The caption is reused as the picker's semantics label,
// so it must not be announced as a separate node.
ExcludeSemantics(child: Text(S.of(context).token)),
MergeSemantics(
child: Semantics(
button: true,
label: S.of(context).select_token,
child: GestureDetector(
onTap: () {
_presentTokenCurrencyPicker(context);
},
child: Observer(
builder: (_) => Container(
height: 60,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
spacing: 8,
children: [
ExcludeSemantics(
child: TokenImageWidget(
imageUrl: widget.walletAddressListViewModel
.tokenCurrency?.iconPath ??
widget.walletAddressListViewModel.currencies.first
.iconPath ??
"",
size: 32,
),
),
Text((widget.walletAddressListViewModel.tokenCurrency ??
widget.walletAddressListViewModel.currencies.first
as CryptoCurrency)
.title
.toUpperCase())
],
),
ExcludeSemantics(
child: RotatedBox(
quarterTurns: 2,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg")),
)
],
),
),
),
),
)),
),
),
),
],
SizedBox(),
Text(S.of(context).amount),
// The caption is reused as the field's semantics label.
ExcludeSemantics(child: Text(S.of(context).amount)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -132,74 +147,91 @@ class _ReceiveAmountModalState extends State<ReceiveAmountModal> {
width: 2,
),
),
child: TextField(
textAlign: TextAlign.left,
textAlignVertical: TextAlignVertical.center,
controller: _amountController,
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal:
widget.walletAddressListViewModel.selectedCurrencyDecimals > 0,
),
inputFormatters: [
DecimalInputFormatter(
maxDecimals:
widget.walletAddressListViewModel.selectedCurrencyDecimals,
),
],
decoration: InputDecoration(
hint: Text(
widget.walletAddressListViewModel.useSatoshi ? "0" : "0.00000000",
child: MergeSemantics(
child: Semantics(
label: S.of(context).amount,
child: TextField(
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 16,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.5),
textAlignVertical: TextAlignVertical.center,
controller: _amountController,
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal:
widget.walletAddressListViewModel.selectedCurrencyDecimals >
0,
),
inputFormatters: [
DecimalInputFormatter(
maxDecimals:
widget.walletAddressListViewModel.selectedCurrencyDecimals,
),
],
decoration: InputDecoration(
hint: Text(
widget.walletAddressListViewModel.useSatoshi
? "0"
: "0.00000000",
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 16,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.5),
),
),
border: InputBorder.none,
filled: true,
fillColor: Colors.transparent,
),
style:
TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
border: InputBorder.none,
filled: true,
fillColor: Colors.transparent,
),
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
),
),
Expanded(
flex: 25,
child: GestureDetector(
onTap: () {
_presentFiatCurrencyPicker(context);
},
child: Container(
height: 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(0),
bottomLeft: Radius.circular(0),
topRight: Radius.circular(18),
bottomRight: Radius.circular(18),
),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4.0,
children: [
Observer(
builder: (_) => Text(
widget.walletAddressListViewModel.selectedCurrencySymbol,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
child: MergeSemantics(
child: Semantics(
button: true,
label: S.of(context).select_fiat_currency_title,
child: GestureDetector(
onTap: () {
_presentFiatCurrencyPicker(context);
},
child: Container(
height: 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(0),
bottomLeft: Radius.circular(0),
topRight: Radius.circular(18),
bottomRight: Radius.circular(18),
),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
Icon(
Icons.keyboard_arrow_down,
color: Theme.of(context).colorScheme.primary,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4.0,
children: [
Observer(
builder: (_) => Text(
widget.walletAddressListViewModel.selectedCurrencySymbol,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
ExcludeSemantics(
child: Icon(
Icons.keyboard_arrow_down,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
],
),
),
),
),
@@ -1,22 +1,10 @@
import 'dart:io';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart';
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/copy_wrapper.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
class ReceiveBottomButtons extends StatefulWidget {
final bool largeQrMode;
final ClipboardData? copyData;
final VoidCallback onCopyButtonPressed;
final VoidCallback onAmountButtonPressed;
final VoidCallback onLabelButtonPressed;
final VoidCallback onAccountsButtonPressed;
final bool showLabelButton;
final bool showAccountsButton;
const ReceiveBottomButtons({
required this.largeQrMode,
required this.onCopyButtonPressed,
@@ -29,6 +17,15 @@ class ReceiveBottomButtons extends StatefulWidget {
super.key,
});
final bool largeQrMode;
final ClipboardData? copyData;
final VoidCallback onCopyButtonPressed;
final VoidCallback onAmountButtonPressed;
final VoidCallback onLabelButtonPressed;
final VoidCallback onAccountsButtonPressed;
final bool showLabelButton;
final bool showAccountsButton;
@override
State<ReceiveBottomButtons> createState() => _ReceiveBottomButtonsState();
}
@@ -49,60 +46,57 @@ class _ReceiveBottomButtonsState extends State<ReceiveBottomButtons> {
opacity: targetOpacity,
curve: Curves.easeOut,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 16,
children: [
GestureDetector(
onTap: widget.copyData == null ? widget.onCopyButtonPressed : null,
child: IgnorePointer(
ignoring: widget.copyData == null,
child: CopyWrapper(
data: widget.copyData,
builder: (context, copied) => AnimatedSwitcher(
duration: Duration(milliseconds: 200),
child: IgnorePointer(
child: ModernButton.svg(
key: ValueKey(copied),
size: 60,
iconSize: 32,
svgPath: "assets/new-ui/copy.svg",
onPressed: () {},
label: copied ? S.of(context).copied : S.of(context).copy,
iconColor: copied
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
backgroundColor: copied
? Theme.of(context).colorScheme.surfaceContainerHighest
: Theme.of(context).colorScheme.primary,
),
),
),
// The button itself is the only control: it copies when there
// is data to copy, otherwise it opens the payjoin copy modal.
CopyWrapper(
data: widget.copyData,
controlBuilder: (context, copied, onCopy) => AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: ModernButton.svg(
key: ValueKey(copied),
size: 60,
iconSize: 32,
svgPath: "assets/new-ui/copy.svg",
onPressed: onCopy ?? widget.onCopyButtonPressed,
label: copied ? S.of(context).copied : S.of(context).copy,
iconColor: copied
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
backgroundColor: copied
? Theme.of(context).colorScheme.surfaceContainerHighest
: Theme.of(context).colorScheme.primary,
),
),
),
ModernButton.svg(
size: 60,
iconSize: 32,
svgPath: "assets/new-ui/set-amount.svg",
onPressed: widget.onAmountButtonPressed,
label: S.of(context).set_amount),
size: 60,
iconSize: 32,
svgPath: "assets/new-ui/set-amount.svg",
onPressed: widget.onAmountButtonPressed,
label: S.of(context).set_amount,
),
if (widget.showLabelButton)
ModernButton.svg(
size: 60,
iconSize: 32,
svgPath: "assets/new-ui/add-label.svg",
onPressed: widget.onLabelButtonPressed,
label: S.of(context).label),
size: 60,
iconSize: 32,
svgPath: "assets/new-ui/add-label.svg",
onPressed: widget.onLabelButtonPressed,
label: S.of(context).label,
),
if (widget.showAccountsButton)
ModernButton.svg(
size: 60,
iconSize: 32,
svgPath: "assets/new-ui/addr-book.svg",
onPressed: widget.onAccountsButtonPressed,
label: S.of(context).addresses),
size: 60,
iconSize: 32,
svgPath: "assets/new-ui/addr-book.svg",
onPressed: widget.onAccountsButtonPressed,
label: S.of(context).addresses,
),
],
),
),
@@ -71,12 +71,14 @@ class ReceiveInfoBox extends StatelessWidget {
spacing: 10,
children: [
if (iconPath.isNotEmpty)
CakeImageWidget(
imageUrl: iconPath,
width: 16,
height: 16,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: iconPath,
width: 16,
height: 16,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
),
Flexible(
child: Column(
@@ -96,15 +98,20 @@ class ReceiveInfoBox extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (bottomWidget != null) bottomWidget!,
GestureDetector(
onTap: onDismissed,
child: Text(
S.of(context).dismiss,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 12,
fontWeight: FontWeight.w300),
)),
MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(
onTap: onDismissed,
child: Text(
S.of(context).dismiss,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 12,
fontWeight: FontWeight.w300),
)),
),
),
],
)
]),
@@ -136,65 +143,69 @@ class InfoboxCurrencyRow extends StatelessWidget {
final double stackWidth = iconSize + (overlap * (currenciesLimited.length));
return Row(
spacing: 8,
children: [
CakeImageWidget(
imageUrl: chainIconPath,
width: 20,
height: 20,
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
Container(
height: 28,
width: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
SizedBox(
height: iconSize + iconBorder * 2,
width: stackWidth,
child: Stack(
children: [
Positioned(
top: iconBorder,
left: overlap * currenciesLimited.length,
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Colors.white.withAlpha(25),
borderRadius: BorderRadius.circular(9999999)),
child: Icon(
Icons.add,
size: 16,
color: Colors.white.withAlpha(128),
// Purely illustrative: the meaning ("receive any token on <chain>") is
// carried by the infobox message text next to it.
return ExcludeSemantics(
child: Row(
spacing: 8,
children: [
CakeImageWidget(
imageUrl: chainIconPath,
width: 20,
height: 20,
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
Container(
height: 28,
width: 1,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
SizedBox(
height: iconSize + iconBorder * 2,
width: stackWidth,
child: Stack(
children: [
Positioned(
top: iconBorder,
left: overlap * currenciesLimited.length,
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Colors.white.withAlpha(25),
borderRadius: BorderRadius.circular(9999999)),
child: Icon(
Icons.add,
size: 16,
color: Colors.white.withAlpha(128),
),
),
),
),
...currenciesLimited
.asMap()
.entries
.map((entry) => Positioned(
left: 16.0 * entry.key,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainer,
width: iconBorder),
borderRadius: BorderRadius.circular(9999999)),
child: TokenImageWidget(
imageUrl: entry.value.iconPath ?? '',
size: 24,
...currenciesLimited
.asMap()
.entries
.map((entry) => Positioned(
left: 16.0 * entry.key,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainer,
width: iconBorder),
borderRadius: BorderRadius.circular(9999999)),
child: TokenImageWidget(
imageUrl: entry.value.iconPath ?? '',
size: 24,
),
),
),
))
.toList()
.reversed,
],
),
)
],
))
.toList()
.reversed,
],
),
)
],
),
);
}
}
@@ -66,6 +66,7 @@ class _ReceiveLabelModalState extends State<ReceiveLabelModal> {
ModalTopBar(
title: S.of(context).label_address,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {}),
Padding(
@@ -91,27 +92,32 @@ class _ReceiveLabelModalState extends State<ReceiveLabelModal> {
return SizedBox(width: 8);
},
itemBuilder: (context, index) {
return Container(
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(999)),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(999),
child: InkWell(
borderRadius: BorderRadius.circular(999),
onTap: () {
_controller.text = defaultLabels[index];
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Center(
child: Text(
defaultLabels[index],
style: TextStyle(
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.primary),
return MergeSemantics(
child: Semantics(
button: true,
child: Container(
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(999)),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(999),
child: InkWell(
borderRadius: BorderRadius.circular(999),
onTap: () {
_controller.text = defaultLabels[index];
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Center(
child: Text(
defaultLabels[index],
style: TextStyle(
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.primary),
),
),
),
),
),
@@ -22,14 +22,16 @@ class ReceiveLabelWidget extends StatelessWidget {
spacing: 8,
mainAxisSize: MainAxisSize.min,
children: [
ClipRect(
child: CakeImageWidget(
imageUrl: "assets/new-ui/label.svg",
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
)),
ExcludeSemantics(
child: ClipRect(
child: CakeImageWidget(
imageUrl: "assets/new-ui/label.svg",
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
)),
),
Text(name,
style: TextStyle(
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)),
@@ -15,7 +15,9 @@ class ModalTopBar extends StatelessWidget {
this.padding,
this.bottomText,
this.leadingWidget,
this.trailingWidget}) {
this.trailingWidget,
this.leadingSemanticLabel,
this.trailingSemanticLabel}) {
if (leadingIcon != null && leadingWidget != null) {
throw Exception("Cannot have both leadingIcon and leadingWidget");
}
@@ -35,6 +37,17 @@ class ModalTopBar extends StatelessWidget {
final Widget? leadingWidget;
final Widget? trailingWidget;
/// Accessible name for the leading chrome button. Required (and must be
/// non-empty) whenever a [leadingIcon] is supplied, because the icon alone
/// does not say whether it closes the modal or goes back. Must be localized
/// by the caller.
final String? leadingSemanticLabel;
/// Accessible name for the trailing chrome button. Required (and must be
/// non-empty) whenever a [trailingIcon] is supplied. Must be localized by
/// the caller.
final String? trailingSemanticLabel;
static const buttonSize = 36.0;
@override
@@ -56,21 +69,27 @@ class ModalTopBar extends StatelessWidget {
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(
title,
key: ValueKey(title),
child: Semantics(
key: ValueKey(title),
header: title.isNotEmpty,
// Android reads the heading from headingLevel since the
// Flutter 3.41 engine; header: alone only covers iOS.
headingLevel: title.isNotEmpty ? 1 : null,
child: Text(
title,
style: TextStyle(
fontSize: hasBottomText ? 16 : 18, fontWeight: FontWeight.w600),
),
),
),
if (hasBottomText)
Text(
bottomText!,
style: TextStyle(
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
)
],
),
),
],
),
if (subtitle != null && subtitle!.isNotEmpty)
Text(subtitle!,
style: Theme.of(context)
@@ -91,6 +110,7 @@ class ModalTopBar extends StatelessWidget {
size: buttonSize,
onPressed: onLeadingPressed,
icon: leadingIcon!,
semanticLabel: leadingSemanticLabel,
iconColor: Theme.of(context).colorScheme.onSurfaceVariant)
: leadingWidget!
else
@@ -104,6 +124,7 @@ class ModalTopBar extends StatelessWidget {
size: buttonSize,
onPressed: onTrailingPressed,
icon: trailingIcon!,
semanticLabel: trailingSemanticLabel,
)
: trailingWidget!,
)
+17 -2
View File
@@ -11,6 +11,20 @@ import 'package:url_launcher/url_launcher.dart';
class ScanPageNetworkList extends StatelessWidget {
const ScanPageNetworkList({super.key});
/// Same icon the row would render from `trailingIconPath`, but it now carries the
/// information that following the row leaves the app.
Widget _externalLinkIcon(BuildContext context) => Semantics(
label: S.of(context).opens_in_browser,
excludeSemantics: true,
child: CakeImageWidget(
imageUrl: "assets/new-ui/external_link.svg",
height: 18,
width: 18,
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
);
@override
Widget build(BuildContext context) {
return Container(
@@ -22,6 +36,7 @@ class ScanPageNetworkList extends StatelessWidget {
ModalTopBar(
title: S.of(context).compatible_services,
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Padding(
@@ -80,7 +95,7 @@ class ScanPageNetworkList extends StatelessWidget {
keyValue: "wc",
label: "WalletConnect",
subtitle: S.of(context).wc_desc,
trailingIconPath: "assets/new-ui/external_link.svg",
trailingWidget: _externalLinkIcon(context),
onTap: () {
launchUrl(Uri.https("walletconnect.com"));
}),
@@ -89,7 +104,7 @@ class ScanPageNetworkList extends StatelessWidget {
keyValue: "ocp",
label: "OpenCryptoPay",
subtitle: S.of(context).ocp_desc,
trailingIconPath: "assets/new-ui/external_link.svg",
trailingWidget: _externalLinkIcon(context),
onTap: () {
launchUrl(Uri.https("opencryptopay.io"));
})
@@ -1,23 +1,24 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:flutter/material.dart';
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
import "package:cw_core/crypto_amount_format.dart";
import "package:flutter/material.dart";
class FiatAmountBar extends StatelessWidget {
const FiatAmountBar(
{super.key,
required this.fiatInputMode,
required this.onSwitchButtonPressed,
this.onAllButtonPressed,
required this.cryptoAmount,
required this.fiatAmount,
required this.cryptoCurrencySymbol,
required this.fiatCurrencySymbol,
this.allAmount,
this.foregroundElementColor,
this.textColor,
this.allAmountColor,
this.allAmountTextColor});
const FiatAmountBar({
required this.fiatInputMode,
required this.onSwitchButtonPressed,
required this.cryptoAmount,
required this.fiatAmount,
required this.cryptoCurrencySymbol,
required this.fiatCurrencySymbol,
super.key,
this.onAllButtonPressed,
this.allAmount,
this.foregroundElementColor,
this.textColor,
this.allAmountColor,
this.allAmountTextColor,
});
final bool fiatInputMode;
final VoidCallback onSwitchButtonPressed;
@@ -35,6 +36,10 @@ class FiatAmountBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final convertedAmount = fiatInputMode
? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} $cryptoCurrencySymbol"
: "${fiatAmount.isEmpty ? "0" : fiatAmount} $fiatCurrencySymbol";
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -47,14 +52,19 @@ class FiatAmountBar extends StatelessWidget {
svgPath: "assets/new-ui/switch.svg",
iconSize: 18,
onPressed: onSwitchButtonPressed,
semanticLabel: S.of(context).switch_input_currency,
),
GestureDetector(
onTap: onSwitchButtonPressed,
child: Text(
fiatInputMode
? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} $cryptoCurrencySymbol"
: "${fiatAmount.isEmpty ? "0" : fiatAmount} $fiatCurrencySymbol",
style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface),
// Announced as the converted value only: the switch button next to it already
// exposes the same action, so this must not become a second control.
Semantics(
label: convertedAmount,
excludeSemantics: true,
child: GestureDetector(
onTap: onSwitchButtonPressed,
child: Text(
convertedAmount,
style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface),
),
),
),
],
@@ -63,37 +73,50 @@ class FiatAmountBar extends StatelessWidget {
Row(
spacing: 8,
children: [
Text(
"${S.of(context).max}.",
style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface),
// The caption is part of the chip's label below.
ExcludeSemantics(
child: Text(
"${S.of(context).max}.",
style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface),
),
),
Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(999999)),
child: Material(
color: allAmountColor ??
foregroundElementColor ??
Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(99999),
color: allAmountColor ??
foregroundElementColor ??
Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(99999),
child: Semantics(
button: true,
enabled: onAllButtonPressed != null,
label: S.of(context).max,
value: _formatAmount(allAmount!),
onTap: onAllButtonPressed,
excludeSemantics: true,
child: InkWell(
borderRadius: BorderRadius.circular(99999),
onTap: onAllButtonPressed,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
formatAmount(allAmount!),
_formatAmount(allAmount!),
style: TextStyle(
color: allAmountTextColor ?? Theme.of(context).colorScheme.primary),
color: allAmountTextColor ?? Theme.of(context).colorScheme.primary,
),
),
),
)),
)
),
),
),
),
],
)
),
],
);
}
String formatAmount(String amount) {
String _formatAmount(String amount) {
try {
return double.parse(amount).toStringAsPrecision(8).replaceFirst(RegExp(r"\.?0+$"), "");
} catch (e) {
@@ -1,41 +1,41 @@
import 'dart:async';
import "dart:async";
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/send_page/l2_send_external_modal.dart';
import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import "package:cake_wallet/bitcoin/bitcoin.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/main.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/new-ui/widgets/send_page/l2_send_external_modal.dart";
import "package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart";
import "package:cake_wallet/view_model/contact_list/contact_list_view_model.dart";
import "package:cake_wallet/view_model/send/send_view_model.dart";
import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
enum l2actions { deposit, withdraw }
enum L2Actions { deposit, withdraw }
class L2ActionWalletSelector extends StatefulWidget {
const L2ActionWalletSelector({
super.key,
required this.showOtherWallets,
required this.sendViewModel,
required this.action,
required this.onSendInitiated,
required this.contactListViewModel,
required this.walletSwitcherViewModel,
super.key,
});
final bool showOtherWallets;
final SendViewModel sendViewModel;
final l2actions action;
final L2Actions action;
final VoidCallback onSendInitiated;
final ContactListViewModel contactListViewModel;
final WalletSwitcherViewModel walletSwitcherViewModel;
@@ -56,12 +56,18 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
super.initState();
if (widget.showOtherWallets) {
() async {
items.addAll((await WalletInfo.getAll()).where((item) =>
item.type == widget.sendViewModel.walletType && item.hardwareWalletType == null));
items.addAll(
(await WalletInfo.getAll()).where(
(item) =>
item.type == widget.sendViewModel.walletType && item.hardwareWalletType == null,
),
);
items.sort((a, b) {
if (a.name == widget.sendViewModel.wallet.name)
if (a.name == widget.sendViewModel.wallet.name) {
return -1;
else if (b.name == widget.sendViewModel.wallet.name) return 1;
} else if (b.name == widget.sendViewModel.wallet.name) {
return 1;
}
return 0;
});
setState(() {});
@@ -80,16 +86,17 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
mainAxisSize: MainAxisSize.min,
children: [
ModalTopBar(
title: widget.action == l2actions.deposit
title: widget.action == L2Actions.deposit
? "${S.of(context).send_from}..."
: "${S.of(context).receive_to}...",
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingIcon: const Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
Flexible(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -123,29 +130,17 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
itemBuilder: (context, index) {
final item = items[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
padding: const EdgeInsets.symmetric(vertical: 4),
child: WalletRow(
currencyIconPath: getCryptoCurrencyIconForWalletListItem(item.type),
walletName: item.name,
isCurrent: item.name == widget.sendViewModel.wallet.name,
isSelected: _selectedWalletIndex == index && !textEntered,
onTap: () async {
onTap: () {
setState(() {
addressController.text = "";
_selectedWalletIndex = index;
});
// if (widget.action == l2actions.withdraw) {
// widget.sendViewModel.outputs.first.address = item.address;
// widget.onSendInitiated();
// } else if (widget.action == l2actions.deposit) {
// setState(() {
// loadingWalletName = item.name;
// });
// await _handleChangeWallet(item);
// widget.onSendInitiated();
// setState(() {
// loadingWalletName = null;
// });
},
),
);
@@ -198,7 +193,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
// ),
// ),
// ),
if (widget.action == l2actions.withdraw)
if (widget.action == L2Actions.withdraw)
Row(
children: [
Flexible(
@@ -215,12 +210,12 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
Column(
spacing: 8,
children: [
if (widget.action == l2actions.deposit && widget.showOtherWallets) ...[
if (widget.action == L2Actions.deposit && widget.showOtherWallets) ...[
Container(
height: 1,
color: Theme.of(context).colorScheme.onSurfaceVariant.withAlpha(64),
),
SizedBox(),
const SizedBox(),
Container(
height: 52,
decoration: BoxDecoration(
@@ -232,7 +227,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () async {
onTap: () {
Navigator.of(context, rootNavigator: true).pop();
showCupertinoModalBottomSheet(
context: navigatorKey.currentContext ?? context,
@@ -246,7 +241,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
@@ -275,19 +270,22 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
onPressed: () async {
if (widget.sendViewModel.wallet.type == WalletType.bitcoin ||
widget.sendViewModel.wallet.type == WalletType.litecoin) {
if (widget.action == l2actions.withdraw) {
if (widget.action == L2Actions.withdraw) {
widget.sendViewModel.outputs.first.address =
bitcoin!.getUnusedSegwitAddress(widget.sendViewModel.wallet)!;
}
if (widget.showOtherWallets) {
if (widget.action == l2actions.deposit) {
if (widget.action == L2Actions.deposit) {
await _handleChangeWallet(items[_selectedWalletIndex]);
} else {
widget.sendViewModel.outputs.first.address =
addressController.text.isNotEmpty
? addressController.text
: items[_selectedWalletIndex].address;
if (items[_selectedWalletIndex].name !=
widget.sendViewModel.wallet.name) {
widget.sendViewModel.outputs.first.address =
addressController.text.isNotEmpty
? addressController.text
: items[_selectedWalletIndex].address;
}
}
}
widget.onSendInitiated();
@@ -298,7 +296,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: _isLoading,
),
SizedBox()
const SizedBox()
],
),
],
@@ -339,13 +337,13 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
class WalletRow extends StatelessWidget {
const WalletRow({
super.key,
required this.currencyIconPath,
required this.walletName,
required this.onTap,
this.isLoading = false,
this.isCurrent = false,
this.isSelected,
super.key,
});
final String currencyIconPath;
@@ -370,7 +368,7 @@ class WalletRow extends StatelessWidget {
borderRadius: BorderRadius.circular(16),
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -73,6 +73,7 @@ class _L2SendExternalModalState extends State<L2SendExternalModal> {
],
),
trailingIcon: Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: Navigator.of(context).pop,
),
SafeArea(
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@@ -179,32 +180,44 @@ class _RecipientDotState extends State<RecipientDot> {
curve: Curves.easeOutCubic,
top: _created ? 0 : widget.size,
child: Material(
child: AnimatedSwitcher(
duration: Duration(milliseconds: 150),
child: GestureDetector(
key: ValueKey(widget.selected),
// Dots scaled all the way down are not visible, so they must not be
// reachable by a screen reader either.
child: ExcludeSemantics(
excluding: widget.textOpacity <= 0,
child: Semantics(
button: true,
selected: widget.selected,
label: S.of(context).recipient_number("${widget.index + 1}"),
onTap: widget.onTap,
child: Container(
width: widget.size,
height: widget.size,
margin: EdgeInsets.symmetric(horizontal: widget.spacing / 2),
decoration: BoxDecoration(
color: widget.selected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Opacity(
opacity: widget.textOpacity,
child: Text(
(widget.index + 1).toString(),
style: TextStyle(
color: widget.selected
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurface,
fontSize: 14,
fontWeight: FontWeight.w500),
excludeSemantics: true,
child: AnimatedSwitcher(
duration: Duration(milliseconds: 150),
child: GestureDetector(
key: ValueKey(widget.selected),
onTap: widget.onTap,
child: Container(
width: widget.size,
height: widget.size,
margin: EdgeInsets.symmetric(horizontal: widget.spacing / 2),
decoration: BoxDecoration(
color: widget.selected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Opacity(
opacity: widget.textOpacity,
child: Text(
(widget.index + 1).toString(),
style: TextStyle(
color: widget.selected
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurface,
fontSize: 14,
fontWeight: FontWeight.w500),
),
),
),
),
),
@@ -86,22 +86,27 @@ class _NewSendAddressInputState extends State<NewSendAddressInput> {
Expanded(
child: Stack(
children: [
TextField(
focusNode: widget.focusNode,
autocorrect: false,
enableSuggestions: false,
onSubmitted: (val) => FocusScope.of(context).unfocus(),
onChanged: state.didChange,
onEditingComplete: () {
widget.onEditingComplete();
},
onTapOutside: (_) {
widget.onEditingComplete();
},
controller: widget.addressController,
decoration: InputDecoration(
hintText: widget.hintText ?? S.of(context).search_or_enter,
errorMaxLines: 3,
MergeSemantics(
child: Semantics(
label: _fieldSemanticsLabel(context),
child: TextField(
focusNode: widget.focusNode,
autocorrect: false,
enableSuggestions: false,
onSubmitted: (val) => FocusScope.of(context).unfocus(),
onChanged: state.didChange,
onEditingComplete: () {
widget.onEditingComplete();
},
onTapOutside: (_) {
widget.onEditingComplete();
},
controller: widget.addressController,
decoration: InputDecoration(
hintText: widget.hintText ?? S.of(context).search_or_enter,
errorMaxLines: 3,
),
),
),
),
Positioned.fill(
@@ -113,9 +118,13 @@ class _NewSendAddressInputState extends State<NewSendAddressInput> {
widget.addressController.text.isEmpty)
? 0
: 1,
child: SendAddressOverlay(
address: widget.addressController.text,
displayName: widget.displayName,
// Purely visual copy of the field content; announcing it again
// would read the address twice.
child: ExcludeSemantics(
child: SendAddressOverlay(
address: widget.addressController.text,
displayName: widget.displayName,
),
)),
),
),
@@ -150,9 +159,15 @@ class _NewSendAddressInputState extends State<NewSendAddressInput> {
if (state.hasError)
Padding(
padding: EdgeInsets.only(top: 6, left: 8),
child: Text(
state.errorText!,
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error),
child: Semantics(
container: true,
liveRegion: true,
label: "${S.of(context).address_or_alias}, ${state.errorText!}",
excludeSemantics: true,
child: Text(
state.errorText!,
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error),
),
),
)
],
@@ -161,6 +176,19 @@ class _NewSendAddressInputState extends State<NewSendAddressInput> {
);
}
String _fieldSemanticsLabel(BuildContext context) {
final label = S.of(context).address_or_alias;
final displayName = widget.displayName;
if (displayName == null ||
displayName.isEmpty ||
displayName == widget.addressController.text) {
return label;
}
return "$label, $displayName";
}
Future<void> _presentQRScanner(BuildContext context) async {
bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
@@ -1,20 +1,21 @@
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/utils/decimal_input_formatter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
import "package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/utils/decimal_input_formatter.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_mobx/flutter_mobx.dart";
class NewSendAmountInput extends StatefulWidget {
const NewSendAmountInput({
super.key,
required this.currency,
required this.maxDecimals,
required this.hasPicker,
required this.onPickerClicked,
required this.currencyIconPath,
required this.amountController,
super.key,
this.validator,
});
@@ -41,31 +42,40 @@ class _NewSendAmountInputState extends State<NewSendAmountInput> {
}
@override
Widget build(BuildContext context) {
return FormField<String>(
key: formFieldKey,
initialValue: widget.amountController.text,
validator: widget.validator,
builder: (state) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
Widget build(BuildContext context) => FormField<String>(
key: formFieldKey,
initialValue: widget.amountController.text,
validator: widget.validator,
builder: (state) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: widget.hasPicker
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18)),
color: widget.hasPicker
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.horizontal(left: Radius.circular(18))),
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.horizontal(left: Radius.circular(18)),
),
child: Row(
mainAxisSize: MainAxisSize.max,
spacing: 8,
children: [
// Deliberately unwrapped. Naming the field with
// MergeSemantics > Semantics(label:) made Android announce
// the amount twice: FormField's own wrapper node reflects
// its descendants' text, so the authored label came back a
// second time on the container. The visible "Amount:"
// caption on the send page carries the name instead, and it
// must stay in the semantics tree for this field to be
// named at all.
Expanded(
child: TextField(
keyboardType: TextInputType.numberWithOptions(
@@ -79,82 +89,107 @@ class _NewSendAmountInputState extends State<NewSendAmountInput> {
],
controller: widget.amountController,
decoration: InputDecoration(
hintText: widget.maxDecimals > 0 ? "0" : "0.00",
hintText: widget.maxDecimals == 0 ? "0" : "0.00",
errorMaxLines: 3,
),
onChanged: state.didChange,
),
),
FloatingIconButton(
iconPath: "assets/new-ui/paste.svg",
onPressed: () async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
final text = data.text!;
widget.amountController.value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
);
}
}),
iconPath: "assets/new-ui/paste.svg",
onPressed: () async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
final text = data.text!;
widget.amountController.value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
);
}
},
),
],
),
),
),
IntrinsicWidth(
child: Observer(
builder: (_) {
return GestureDetector(
// `container: true` is load bearing. FormField wraps this
// builder in its own non-container Semantics annotation, so
// without a container of its own this configuration is
// absorbed into that wrapper node — which then parents the
// amount TextField and hides it from screen readers.
builder: (_) => Semantics(
container: true,
button: widget.hasPicker ? true : null,
enabled: widget.hasPicker ? true : null,
label: widget.hasPicker ? S.of(context).select_asset : widget.currency,
value: widget.hasPicker ? widget.currency : null,
onTap: widget.hasPicker ? widget.onPickerClicked : null,
excludeSemantics: true,
child: GestureDetector(
onTap: widget.onPickerClicked,
child: Container(
alignment: Alignment.center,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(18),
bottomRight: Radius.circular(18)),
color: widget.hasPicker
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainer,
alignment: Alignment.center,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(18),
bottomRight: Radius.circular(18),
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
spacing: 8,
children: [
if (widget.hasPicker && widget.currencyIconPath.isNotEmpty)
TokenImageWidget(
imageUrl: widget.currencyIconPath, size: 24),
Text(widget.currency),
if (widget.hasPicker)
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
width: 12,
height: 12,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn),
)
],
))),
);
},
color: widget.hasPicker
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainer,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
spacing: 8,
children: [
if (widget.hasPicker && widget.currencyIconPath.isNotEmpty)
TokenImageWidget(
imageUrl: widget.currencyIconPath,
size: 24,
),
Text(widget.currency),
if (widget.hasPicker)
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
width: 12,
height: 12,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
],
),
),
),
),
),
),
),
],
)),
if (state.hasError)
Padding(
padding: EdgeInsets.only(top: 6, left: 8),
child: Text(
state.errorText!,
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error),
),
)
],
),
);
}
),
if (state.hasError)
Padding(
padding: const EdgeInsets.only(top: 6, left: 8),
child: Semantics(
container: true,
liveRegion: true,
label: "${S.of(context).amount}${state.errorText!}",
excludeSemantics: true,
child: Text(
state.errorText!,
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error),
),
),
),
],
),
);
}
@@ -80,18 +80,25 @@ class LoadingBottomWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12,
children: [
CupertinoActivityIndicator(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
Text(
text,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
)
],
// One node per state so each step of the send is announced exactly once.
return Semantics(
container: true,
liveRegion: true,
label: text,
excludeSemantics: true,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12,
children: [
CupertinoActivityIndicator(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
Text(
text,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
)
],
),
);
}
}
@@ -106,45 +113,51 @@ class TransactionErrorActions extends StatelessWidget {
return Column(
spacing: 12,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer.withAlpha(64),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 12,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/warning.svg",
height: 24,
width: 24,
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn),
),
Text(
S.of(context).transaction_error,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.error),
)
],
),
Text(
errorText,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.error),
textAlign: TextAlign.center,
),
],
Semantics(
container: true,
liveRegion: true,
label: "${S.of(context).transaction_error}\n$errorText",
excludeSemantics: true,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer.withAlpha(64),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 12,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/warning.svg",
height: 24,
width: 24,
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn),
),
Text(
S.of(context).transaction_error,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.error),
)
],
),
Text(
errorText,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.error),
textAlign: TextAlign.center,
),
],
),
),
),
),
@@ -87,11 +87,15 @@ class _SendConfirmSheetState extends State<SendConfirmSheet> {
offset: _committed ? const Offset(-1, 0) : Offset.zero,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
child: SendTransactionDetails(
sendViewModel: widget.sendViewModel,
isPage: widget.isPage,
title: widget.title,
iconPath: widget.iconPath,
// Both screens stay mounted; only the visible one may be reachable.
child: ExcludeSemantics(
excluding: _committed,
child: SendTransactionDetails(
sendViewModel: widget.sendViewModel,
isPage: widget.isPage,
title: widget.title,
iconPath: widget.iconPath,
),
),
),
),
@@ -102,8 +106,11 @@ class _SendConfirmSheetState extends State<SendConfirmSheet> {
offset: _committed ? Offset.zero : const Offset(1, 0),
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
child: TransactionCommitedScreen(
sendViewModel: widget.sendViewModel,
child: ExcludeSemantics(
excluding: !_committed,
child: TransactionCommitedScreen(
sendViewModel: widget.sendViewModel,
),
),
),
),
@@ -154,13 +161,20 @@ class SendTransactionDetails extends StatelessWidget {
width: 28,
height: 28,
),
Text(
title ?? S.of(context).send,
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
Semantics(
header: true,
// Android reads the heading from headingLevel since the
// Flutter 3.41 engine; header: alone only covers iOS.
headingLevel: 1,
child: Text(
title ?? S.of(context).send,
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
)
],
),
trailingIcon: Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: Navigator.of(context).maybePop,
),
isPage
@@ -249,37 +263,40 @@ class SendTransactionDetails extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
spacing: 24,
children: [
Column(
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4,
children: [
Flexible(
child: Text(
amount,
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface),
// The amount being sent is the value under review: announce it as one group.
MergeSemantics(
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4,
children: [
Flexible(
child: Text(
amount,
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface),
),
),
),
Text(currencySymbol,
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant))
],
),
Text(
fiatAmount,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
Text(currencySymbol,
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant))
],
),
Text(
fiatAmount,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
),
if (outputs.length >= 1 &&
(outputs.first.extractedAddress.isNotEmpty || outputs.first.address.isNotEmpty) &&
@@ -351,32 +368,34 @@ class SendTransactionDetails extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).fee,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface)),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"${fee.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currencySymbol}",
child: MergeSemantics(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).fee,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text(fiatFee.withLocalSeperator(sendViewModel.languageCode),
color: Theme.of(context).colorScheme.onSurface)),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"${fee.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currencySymbol}",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant))
],
)
],
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text(fiatFee.withLocalSeperator(sendViewModel.languageCode),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant))
],
)
],
),
),
),
if (sendViewModel.isElectrumWallet) ...[
@@ -389,27 +408,29 @@ class SendTransactionDetails extends StatelessWidget {
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).network,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface)),
Column(
children: [
Text(
sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln
? "Lightning"
: bitcoin!.getNetworkName(sendViewModel.wallet),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant))
],
)
],
child: MergeSemantics(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).network,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface)),
Column(
children: [
Text(
sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln
? "Lightning"
: bitcoin!.getNetworkName(sendViewModel.wallet),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant))
],
)
],
),
),
)
],
@@ -458,9 +479,14 @@ class _TransactionCommitedScreenState extends State<TransactionCommitedScreen> {
SizedBox(
height: 12,
),
Text(
S.of(context).transaction_sent_new,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
// The sheet swaps its content in place, so this title becoming visible is what
// tells a screen reader that the transaction went through.
Semantics(
liveRegion: true,
child: Text(
S.of(context).transaction_sent_new,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
),
),
SizedBox(),
CakeImageWidget(width: 200, height: 200, imageUrl: "assets/new-ui/birthday_cake.svg"),
@@ -513,7 +539,8 @@ class _TransactionCommitedScreenState extends State<TransactionCommitedScreen> {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => page);
builder: (context) =>
FractionallySizedBox(heightFactor: 0.9, child: page));
}),
],
),
@@ -550,37 +577,44 @@ class TransactionCommittedScreenActionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Flexible(
child: GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
children: [
isLoading
? CupertinoActivityIndicator()
: CakeImageWidget(
imageUrl: iconPath,
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
Text(
text,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500),
)
],
),
),
)));
child: Semantics(
button: true,
enabled: !isLoading,
label: text,
value: isLoading ? S.of(context).loading : null,
onTap: isLoading ? null : onTap,
excludeSemantics: true,
child: GestureDetector(
onTap: isLoading ? null : onTap,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
children: [
isLoading
? CupertinoActivityIndicator()
: CakeImageWidget(
imageUrl: iconPath,
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
Text(
text,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500),
)
],
),
),
))));
}
}
@@ -621,16 +655,28 @@ class _MultiSendAddressPreviewState extends State<MultiSendAddressPreview> {
style: TextStyle(fontFamily: "IBM Plex Mono"),
),
if (!_expanded)
GestureDetector(
onTap: () {
setState(() {
_expanded = true;
});
},
child: Text(middleTruncate(widget.address, 8, 8),
style: TextStyle(
fontFamily: "IBM Plex Mono",
color: Theme.of(context).colorScheme.primary)))
Semantics(
button: true,
// Read the whole address rather than the truncated form.
label: widget.address,
hint: S.of(context).show_full_address,
onTap: () {
setState(() {
_expanded = true;
});
},
excludeSemantics: true,
child: GestureDetector(
onTap: () {
setState(() {
_expanded = true;
});
},
child: Text(middleTruncate(widget.address, 8, 8),
style: TextStyle(
fontFamily: "IBM Plex Mono",
color: Theme.of(context).colorScheme.primary))),
)
else
AddressFormatter.buildSegmentedAddress(
address: widget.address,

Some files were not shown because too many files have changed in this diff Show More