Merge branch 'cake-tech:dev' into dev

This commit is contained in:
BSN ∞/21M
2026-08-04 09:05:32 +02:00
committed by GitHub
22 changed files with 1092 additions and 787 deletions
@@ -34,7 +34,7 @@ class ZcashTaddressRotation {
static bool _isStarted = false;
static zkool_coin.Coin get c => ZcashWalletBase.c;
static const int _sweepThreshold = 30000;
static const int _minSpendableNote = 5000;
static const int minSpendableNote = 5000;
static const int _lookahead = 5;
// Matches transparentLimit in ZcashWalletBase._oneshotSync.
static const int _transparentSyncLimit = 100;
@@ -415,7 +415,7 @@ class ZcashTaddressRotation {
if (note.pool != NotePool.transparent.index || note.locked) {
continue;
}
if (note.value < BigInt.from(_minSpendableNote)) {
if (note.value < BigInt.from(minSpendableNote)) {
continue;
}
if (note.height > height) {
+68 -34
View File
@@ -83,6 +83,12 @@ abstract class ZcashWalletBase
static const int _autoShieldMinSweep = 30000;
// zkool's migrate::MIN_SD.
static const int _ironwoodMigrateMinNote = 500000;
static int _minSweepThreshold({required final bool ironwood}) =>
ironwood ? _ironwoodMigrateMinNote : _autoShieldMinSweep;
Money _feeFromTxPlan(
final zkool_pay.PcztPackage txPlan,
final TransactionPriority priority,
@@ -469,8 +475,7 @@ abstract class ZcashWalletBase
}
// pools parameter: bitmask for which pools to use for sending
// 1=Transparent, 2=Sapling, 4=Orchard, 7=All pools
// Using 7 (all pools) allows spending from any pool type
// 1=Transparent, 2=Sapling, 4=Orchard, 8=Ironwood
try {
return await runWithCoin(
accountId: accountId,
@@ -479,7 +484,7 @@ abstract class ZcashWalletBase
final txPlan = await zkool_pay.prepare(
recipients: recipients,
options: zkool_pay.PaymentOptions(
srcPools: ironwood ? 15 : 7,
srcPools: ironwood ? 8 : 4,
recipientPaysFee: receipientPaysFee,
smartTransparent: false,
mode: 0,
@@ -1175,6 +1180,40 @@ abstract class ZcashWalletBase
}
}
/// Total of transparent + sapling notes at or above the per-note spendable floor.
static Future<BigInt> _sweepableTotal(final zkool_coin.Coin coin) async {
final notes = await zkool_account.listNotes(c: coin);
BigInt sweepable = BigInt.zero;
for (int i = 0; i < notes.length; i++) {
final note = notes[i];
if (note.pool < 0 || note.pool >= NotePool.values.length) {
continue;
}
final noteType = NotePool.values[note.pool];
if ((noteType == NotePool.transparent || noteType == NotePool.sapling) &&
note.value >= BigInt.from(ZcashTaddressRotation.minSpendableNote)) {
sweepable += note.value;
}
}
return sweepable;
}
/// Orchard notes that migration will actually split or move to Ironwood.
static Future<BigInt> _migratableOrchardTotal(final zkool_coin.Coin coin) async {
final notes = await zkool_account.listNotes(c: coin);
BigInt migratable = BigInt.zero;
for (int i = 0; i < notes.length; i++) {
final note = notes[i];
if (note.pool != NotePool.orchard.index || note.locked) {
continue;
}
if (note.value >= BigInt.from(_ironwoodMigrateMinNote)) {
migratable += note.value;
}
}
return migratable;
}
Future<void> _$autoShield() async {
if (syncStatus is! SyncedSyncStatus) {
return;
@@ -1182,24 +1221,12 @@ abstract class ZcashWalletBase
final txId = await runWithCoin(
accountId: accountId,
func: (coin) async {
final _notes = await zkool_account.listNotes(c: coin);
BigInt sweepable = BigInt.zero;
for (int i = 0; i < _notes.length; i++) {
final note = _notes[i];
if (note.pool < 0 || note.pool >= NotePool.values.length) {
continue;
}
final noteType = NotePool.values[note.pool];
if (noteType == NotePool.transparent || noteType == NotePool.sapling) {
sweepable += note.value;
}
}
final sweepable = await _sweepableTotal(coin);
final ironwood = await zkool_network.isIronwoodActive(c: coin);
if (sweepable <= BigInt.from(_autoShieldMinSweep)) {
if (sweepable <= BigInt.from(_minSweepThreshold(ironwood: ironwood))) {
return null;
}
final ironwood = await zkool_network.isIronwoodActive(c: coin);
final txPlan = await zkool_pay.prepare(
recipients: [
zkool_paydart.Recipient(
@@ -1332,17 +1359,6 @@ abstract class ZcashWalletBase
}) async {
try {
await _updateIronwoodActive();
final bal = await runWithCoin(
accountId: accountId,
func: (final coin) async => zkool_sync.balance(c: coin),
);
// 0 - transparent, 1 - sapling, 2 - orchard, 3 - ironwood
final transparent = bal.field0[0];
final sapling = bal.field0.length > 1 ? bal.field0[1] : BigInt.zero;
final orchard = bal.field0.length > 2 ? bal.field0[2] : BigInt.zero;
final ironwood = bal.field0.length > 3 ? bal.field0[3] : BigInt.zero;
if (runAutoShield) {
await _autoShield();
}
@@ -1350,18 +1366,36 @@ abstract class ZcashWalletBase
await _ironwoodMigrate();
}
final (bal, sweepable, migratableOrchard) = await runWithCoin(
accountId: accountId,
func: (final coin) async => (
await zkool_sync.balance(c: coin),
await _sweepableTotal(coin),
await _migratableOrchardTotal(coin),
),
);
// 0 - transparent, 1 - sapling, 2 - orchard, 3 - ironwood
final orchard = bal.field0.length > 2 ? bal.field0[2] : BigInt.zero;
final ironwood = bal.field0.length > 3 ? bal.field0[3] : BigInt.zero;
// After NU6.3, Orchard notes are migrated to Ironwood - show them as unconfirmed.
// Unavailable uses the same per-note totals and thresholds as auto-shield/migration guards.
final BigInt availableAmount;
final BigInt unavailableAmount;
if (ironwoodActive == true && orchard > BigInt.zero) {
availableAmount = sapling + ironwood;
unavailableAmount = transparent + orchard;
final sweepableUnavailable = sweepable <= BigInt.from(_ironwoodMigrateMinNote)
? BigInt.zero
: sweepable;
availableAmount = ironwood;
unavailableAmount = migratableOrchard + sweepableUnavailable;
} else {
availableAmount = sapling + orchard + ironwood;
unavailableAmount = transparent;
final minSweep = _minSweepThreshold(ironwood: ironwoodActive == true);
availableAmount = orchard + ironwood;
unavailableAmount = sweepable <= BigInt.from(minSweep) ? BigInt.zero : sweepable;
}
balance[CryptoCurrency.zec] = ZcashBalance(
balance[currency] = ZcashBalance(
Money(availableAmount, currency),
Money(unavailableAmount, currency),
frozen: Money.zero(currency),
+12 -1
View File
@@ -30,13 +30,24 @@ security channel, so reports will not be missed.
### What to include
- A clear description of the issue and its security impact.
- Step-by-step reproduction, ideally with a proof of concept.
- A working proof of concept is required. It must run directly against Cake
Wallet itself, using a release build or locally built version, and demonstrate
the reported behavior and security impact. Standalone Python code,
mathematical examples, or simulations that only reproduce the theory without
exercising Cake Wallet do not satisfy this requirement.
- Step-by-step reproduction instructions.
- Affected platforms (iOS, Android, macOS, Linux, Windows) and app version.
- Affected wallet types / chains, if applicable.
- Any relevant logs, addresses, or transaction IDs (for on-chain issues).
If you used AI tooling to find or write up the report, please say so.
### Communication expectations
AI should not be used to generate comments when communicating with maintainers
and other contributors. Comments are expected to be written by humans. Comments
that are believed to be written by AI may be moderated.
## Our commitment (safe harbor)
We consider security research conducted in good faith under this policy to be
+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),
),
),
),
),
+68 -44
View File
@@ -174,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,
),
),
),
);
@@ -199,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),
)
],
),
),
),
),
),
@@ -406,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(
@@ -441,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),
),
),
),
),
),
+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,
),
),
),
),
],
@@ -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,
),
),
],
),
),
),
),
@@ -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)),
),
],
),
),
),
),
),
),
),
],
),
),
@@ -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),
@@ -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)
],
),
),
),
),
@@ -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),
],
),
),
),
),
);
@@ -76,6 +76,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,
@@ -86,124 +112,122 @@ 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: () => Navigator.of(context).pushNamed(Routes.buySellPage),
)
]
: [];
final String accountName;
final String accountBalance;
if (account == null) {
accountName = "";
accountBalance = "";
} else {
accountName = account.label;
accountBalance = account.balance ?? "0.00";
}
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,
);
}),
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: () => Navigator.of(context).pushNamed(Routes.buySellPage),
)
]
: [];
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);
+27 -13
View File
@@ -21,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,
@@ -70,12 +77,7 @@ 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,
)
@@ -83,6 +85,18 @@ class WalletInfoBar extends StatelessWidget {
],
),
);
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 {
+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)),
],
),
),
),
),
@@ -187,50 +187,56 @@ class _NEWNewMainNavBarState extends State<NewMainNavBar> {
? iconHorizontalPadding / 100
: 0),
curve: Curves.easeOutCubic,
child: InkWell(
splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
borderRadius: BorderRadius.circular(pillBorderRadius),
onTap: () => _onItemTap(i),
child: AnimatedContainer(
duration:
_firstFrame ? Duration.zero : inactiveIconMoveDuration,
curve: Curves.easeOutCubic,
width: i == widget.selectedIndex ? pillWidth : iconBoxWidth,
alignment: Alignment.center,
child: AnimatedAlign(
duration: inactiveIconFadeDuration,
child: Semantics(
button: true,
selected: i == widget.selectedIndex,
inMutuallyExclusiveGroup: true,
label: visibleActions[i].name(context),
child: InkWell(
splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
borderRadius: BorderRadius.circular(pillBorderRadius),
onTap: () => _onItemTap(i),
child: AnimatedContainer(
duration:
_firstFrame ? Duration.zero : inactiveIconMoveDuration,
curve: Curves.easeOutCubic,
width: i == widget.selectedIndex ? pillWidth : iconBoxWidth,
alignment: Alignment.center,
child: AnimatedScale(
duration: inactiveIconAppearDuration,
child: AnimatedAlign(
duration: inactiveIconFadeDuration,
curve: Curves.easeOutCubic,
scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
child: TweenAnimationBuilder<Color?>(
tween: ColorTween(
begin: (i == widget.selectedIndex)
? inactiveColor
: activeColor,
end: (i == widget.selectedIndex)
? activeColor
: inactiveColor,
),
duration: iconColorChangeDuration,
builder: (context, value, child) {
return Container(
height: NewMainNavBar.barHeight,
child: CakeImageWidget(
imageUrl: visibleActions[i].image,
width: iconWidth,
height: iconHeight,
//fit: BoxFit.scaleDown,
colorFilter: ColorFilter.mode(
value ?? inactiveColor,
BlendMode.srcIn,
alignment: Alignment.center,
child: AnimatedScale(
duration: inactiveIconAppearDuration,
curve: Curves.easeOutCubic,
scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
child: TweenAnimationBuilder<Color?>(
tween: ColorTween(
begin: (i == widget.selectedIndex)
? inactiveColor
: activeColor,
end: (i == widget.selectedIndex)
? activeColor
: inactiveColor,
),
duration: iconColorChangeDuration,
builder: (context, value, child) {
return Container(
height: NewMainNavBar.barHeight,
child: CakeImageWidget(
imageUrl: visibleActions[i].image,
width: iconWidth,
height: iconHeight,
//fit: BoxFit.scaleDown,
colorFilter: ColorFilter.mode(
value ?? inactiveColor,
BlendMode.srcIn,
),
),
),
);
}),
);
}),
),
),
),
),
@@ -306,11 +312,15 @@ class AnimatedPill extends StatelessWidget {
children: [
Padding(
padding: EdgeInsets.only(left: pillIconWidth + 2),
child: Text(
currentAction.name(context),
style: pillTextStyle.copyWith(color: contentColor),
overflow: TextOverflow.fade,
softWrap: false,
// The selected tab's InkWell already announces this name, so the
// pill text must not become a second stop for screen readers.
child: ExcludeSemantics(
child: Text(
currentAction.name(context),
style: pillTextStyle.copyWith(color: contentColor),
overflow: TextOverflow.fade,
softWrap: false,
),
),
),
],
+4
View File
@@ -528,6 +528,7 @@
"got_it": "Got it",
"gross_balance": "Gross Balance",
"group_by_type": "Group by type",
"hardware_wallet": "Hardware wallet",
"haven_app": "Haven by Cake Wallet",
"haven_app_wallet_text": "Awesome wallet for Haven",
"help": "help",
@@ -588,6 +589,7 @@
"lightning_deposit_desc": "When you deposit to Lightning, you are swapping your on-chain Bitcoin from this wallet to your Lightning account.",
"lightning_deposit_disclaimer": "The new Lightning balance will not be available until the Bitcoin transaction is fully confirmed.",
"lightning_external_disclaimer": "Don't close this page until you send the funds, or else the deposit won't proceed.",
"lightning_mode": "Lightning mode",
"lightning_username_desc": "Easily receive Lightning payments with a Lightning username by choosing a custom name or using a randomly generated one below.",
"lightning_username_desc_completed": "You can use this username to receive payments from any Lightning wallet.",
"lightning_username_setup_later": "If you skip now, you can set this up later in the Settings.",
@@ -618,6 +620,7 @@
"login": "Login",
"logout": "Logout",
"long_press_edit_address": "Long press to edit address",
"long_press_hide_balance": "Long press card to hide balance",
"long_press_show_balance": "Long press card to show balance",
"low_fee": "Low fee",
"low_fee_alert": "You currently are using a low network fee priority. This could cause long waits, different rates, or canceled trades. We recommend setting a higher fee for a better experience.",
@@ -726,6 +729,7 @@
"onramper_option_description": "Quickly buy crypto with many payment methods. Available in most countries. Spreads and fees vary.",
"open_gift_card": "Open Gift Card",
"open_wallet": "Open Wallet",
"opens_externally": "Opens externally",
"optional_description": "Optional description",
"optional_email_hint": "Optional payee notification email",
"optional_name": "Optional recipient name",