mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-11-12 08:53:57 +01:00
CW-1523-AnyPay Redesign (#3383)
* feat: Add network selector and compatible networks screen to send page for EVMs * feat: add redesigned payanything flow and handle cases for evms, solana and tron * feat: adding the swap part to the entire flow * refactor: update navigation and error handling in send and swap flows * adjust structuring and use the chain badge icons, also fix crypto and currency values not consistent on single network picker * fix: modify guard for non-evm chains and minor cleanup * use multipicker layout for receive section, streamline send to available balances and adjust colors as needed * handle btc case for picker, update style and images across * fix all pending issues with scanning qr and network handling for both evm and non evm, plus token and amount resolving * fix all pending issues with scanning qr and network handling for both evm and non evm, plus token and amount resolving * fix: expand fetch for tokens across networks and fix other issues regarding scanning from homepage * auto-reformat * fix conflict * handle evm qr scan from homepage and add guards for swap and switch flows * chore: Cleanup * chore: set estimated fee to 0 when switching wallet * fix merge conflicts * fix to review comments and reformatting for lints * fix: root cause for token bug reported * chore: cleanup * feat: add improved anypay swap screen which focuses on the receive amount fix: fiat input toggle erro (also present on prod) reformat: anypay related widgets * feat: add alert for unsupported networks in payment requests * fix: prevent rate and provider from showing until amount is calculated and loaded fix: remove send icon in rate widget and align to center fix: adjust network badge in swap confirm widget * feat: embed tokens for solana and tron in QR generated for swap from external * test: add unit tests for Tron, Solana, and ERC681 URI handling and detection * fix: review comments * fix: handle more edgecases * feat: enhance ERC681URI to support raw token amounts and decimals * refactor: streamline EVM chain ID retrieval in send page and recipient network row * fix: minor review comments --------- Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
This commit is contained in:
co-authored by
Robert Malikowski
parent
931b6fb9d2
commit
90194f7136
+601
-304
File diff suppressed because it is too large
Load Diff
+514
-892
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.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";
|
||||
|
||||
class EvmAddressDetectedSheet extends StatelessWidget {
|
||||
const EvmAddressDetectedSheet({super.key, required this.networks});
|
||||
|
||||
final List<RecipientNetworkItem> networks;
|
||||
|
||||
static Future<int?> show({
|
||||
required BuildContext context,
|
||||
required List<RecipientNetworkItem> networks,
|
||||
}) {
|
||||
return showModalBottomSheet<int>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => EvmAddressDetectedSheet(networks: networks),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
color: colors.surface,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
StackedNetworkIcons(iconPaths: networks.map((n) => n.iconPath).toList()),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).evm_address_detected,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: -0.1),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
S.of(context).select_recipient_network_for_tx,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 64),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: NetworkListCard(
|
||||
rows: [
|
||||
for (final network in networks)
|
||||
RecipientNetworkListRow(
|
||||
item: network,
|
||||
imageOverride: network.chainBadgeIconPath,
|
||||
onTap: () => Navigator.of(context).pop(network.chainId),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StackedNetworkIcons extends StatelessWidget {
|
||||
const StackedNetworkIcons({
|
||||
required this.iconPaths,
|
||||
this.size = 50,
|
||||
this.step = 34,
|
||||
});
|
||||
|
||||
final List<String> iconPaths;
|
||||
final double size;
|
||||
final double step;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (iconPaths.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final width = size + (iconPaths.length - 1) * step;
|
||||
|
||||
return SizedBox(
|
||||
height: size,
|
||||
width: width,
|
||||
child: Stack(
|
||||
children: [
|
||||
for (int i = 0; i < iconPaths.length; i++)
|
||||
Positioned(
|
||||
left: i * step,
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colors.surfaceContainer,
|
||||
border: Border.all(color: colors.surface, width: 2),
|
||||
),
|
||||
child: CakeImageWidget(imageUrl: iconPaths[i], fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import "package:cake_wallet/evm/evm.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class RecipientNetworkSelector extends StatelessWidget {
|
||||
const RecipientNetworkSelector({
|
||||
super.key,
|
||||
required this.wallet,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final WalletBase wallet;
|
||||
final void Function(ChainInfo currentChain) onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (evm == null) return const SizedBox.shrink();
|
||||
|
||||
final currentChainId =
|
||||
evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
|
||||
final currentChain = evm!.getChainInfoByChainId(currentChainId);
|
||||
if (currentChain == null) {
|
||||
printV("RecipientNetworkSelector: no chain info for chainId $currentChainId");
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return _RecipientNetworkRow(
|
||||
networkName: currentChain.name,
|
||||
networkIconPath: symbolIconPathForWalletType(wallet.type) ?? "",
|
||||
onTap: () => onTap(currentChain),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecipientNetworkRow extends StatelessWidget {
|
||||
const _RecipientNetworkRow({
|
||||
required this.networkName,
|
||||
required this.networkIconPath,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String networkName;
|
||||
final String networkIconPath;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).network_prefix_on,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
CakeImageWidget(
|
||||
imageUrl: networkIconPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
fit: BoxFit.cover,
|
||||
color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
networkName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: -0.07,
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
size: 18,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import "package:cake_wallet/generated/i18n.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:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class RecipientNetworkItem {
|
||||
const RecipientNetworkItem({
|
||||
required this.chainId,
|
||||
required this.name,
|
||||
required this.iconPath,
|
||||
this.chainBadgeIconPath,
|
||||
});
|
||||
|
||||
final int chainId;
|
||||
final String name;
|
||||
final String iconPath;
|
||||
final String? chainBadgeIconPath;
|
||||
}
|
||||
|
||||
class SelectRecipientNetworkSheet extends StatelessWidget {
|
||||
const SelectRecipientNetworkSheet({
|
||||
super.key,
|
||||
required this.networks,
|
||||
required this.currentChainId,
|
||||
});
|
||||
|
||||
final List<RecipientNetworkItem> networks;
|
||||
final int currentChainId;
|
||||
|
||||
static Future<int?> show({
|
||||
required BuildContext context,
|
||||
required List<RecipientNetworkItem> networks,
|
||||
required int currentChainId,
|
||||
}) {
|
||||
return showModalBottomSheet<int>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
enableDrag: false,
|
||||
isDismissible: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => SelectRecipientNetworkSheet(
|
||||
networks: networks,
|
||||
currentChainId: currentChainId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
RecipientNetworkItem? current;
|
||||
final compatible = <RecipientNetworkItem>[];
|
||||
for (final network in networks) {
|
||||
if (network.chainId == currentChainId && current == null) {
|
||||
current = network;
|
||||
} else {
|
||||
compatible.add(network);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
color: colors.surface,
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/network_cube.svg",
|
||||
width: 75,
|
||||
height: 75,
|
||||
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).select_recipient_network,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: -0.1),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
S.of(context).select_recipient_network_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (current != null) ...[
|
||||
NetworkListCard(
|
||||
rows: [
|
||||
RecipientNetworkListRow(
|
||||
item: current,
|
||||
subtitle: S.of(context).current_network,
|
||||
onTap: () => Navigator.of(context).pop(current!.chainId),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
if (compatible.isNotEmpty) ...[
|
||||
Text(
|
||||
S.of(context).compatible_networks,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.06),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
NetworkListCard(
|
||||
rows: [
|
||||
for (final network in compatible)
|
||||
RecipientNetworkListRow(
|
||||
item: network,
|
||||
onTap: () => Navigator.of(context).pop(network.chainId),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkListCard extends StatelessWidget {
|
||||
const NetworkListCard({required this.rows});
|
||||
|
||||
final List<Widget> rows;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 0; i < rows.length; i++) ...[
|
||||
rows[i],
|
||||
if (i != rows.length - 1)
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: colors.surfaceContainerHigh,
|
||||
indent: 48,
|
||||
endIndent: 12,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RecipientNetworkListRow extends StatelessWidget {
|
||||
const RecipientNetworkListRow(
|
||||
{super.key, required this.item, required this.onTap, this.subtitle, this.imageOverride});
|
||||
|
||||
final String? subtitle;
|
||||
final String? imageOverride;
|
||||
final VoidCallback onTap;
|
||||
final RecipientNetworkItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: imageOverride ?? item.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
fit: BoxFit.cover,
|
||||
color: isMonochromeSymbolIcon(imageOverride ?? item.iconPath) ? colors.primary : null,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
item.name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w400, letterSpacing: -0.07),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colors.onSurfaceVariant, letterSpacing: -0.06, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, size: 16, color: colors.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SendToNetworkPage extends StatelessWidget {
|
||||
const SendToNetworkPage({
|
||||
required this.title,
|
||||
required this.destinationNetworkName,
|
||||
required this.destinationIconPath,
|
||||
required this.currentNetworkName,
|
||||
required this.onSwitchWallet,
|
||||
required this.onSwap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String destinationNetworkName;
|
||||
final String destinationIconPath;
|
||||
final String currentNetworkName;
|
||||
final VoidCallback onSwitchWallet;
|
||||
final VoidCallback onSwap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: colors.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: destinationIconPath,
|
||||
width: 75,
|
||||
height: 75,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
S.of(context).send_to_network_description(
|
||||
destinationNetworkName,
|
||||
currentNetworkName,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
NewPrimaryButton(
|
||||
onPressed: onSwitchWallet,
|
||||
image: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/wallet_filled.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
|
||||
),
|
||||
text: S.of(context).switch_to_x_wallet(destinationNetworkName),
|
||||
color: colors.primary,
|
||||
textColor: colors.onPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
NewPrimaryButton(
|
||||
onPressed: onSwap,
|
||||
image: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/swap_arrows.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
|
||||
),
|
||||
text: S.of(context).swap_from_network(currentNetworkName),
|
||||
color: colors.surfaceContainer,
|
||||
textColor: colors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
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:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SwapFromNetworkPage extends StatelessWidget {
|
||||
const SwapFromNetworkPage({
|
||||
required this.title,
|
||||
required this.primaryButtonText,
|
||||
required this.destinationNetworkName,
|
||||
required this.destinationNetworkIconPath,
|
||||
required this.currentNetworkName,
|
||||
required this.currentNetworkIconPath,
|
||||
required this.onProceed,
|
||||
super.key,
|
||||
this.primaryHasSwapIcon = false,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String primaryButtonText;
|
||||
final bool primaryHasSwapIcon;
|
||||
final String destinationNetworkName;
|
||||
final String destinationNetworkIconPath;
|
||||
final String currentNetworkName;
|
||||
final String currentNetworkIconPath;
|
||||
final VoidCallback onProceed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: colors.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
trailingSemanticLabel: S.of(context).close,
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: currentNetworkIconPath,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(currentNetworkIconPath)
|
||||
? colors.primary
|
||||
: null,
|
||||
),
|
||||
Icon(Icons.arrow_forward, color: colors.primary, size: 28),
|
||||
CakeImageWidget(
|
||||
imageUrl: destinationNetworkIconPath,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(destinationNetworkIconPath)
|
||||
? colors.primary
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
S.of(context).swap_from_network_description(
|
||||
destinationNetworkName,
|
||||
currentNetworkName,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
NewPrimaryButton(
|
||||
onPressed: onProceed,
|
||||
image: primaryHasSwapIcon
|
||||
? CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/swap_arrows.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
|
||||
)
|
||||
: null,
|
||||
text: primaryButtonText,
|
||||
color: colors.primary,
|
||||
textColor: colors.onPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
NewPrimaryButton(
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
text: S.of(context).cancel,
|
||||
color: colors.surfaceContainer,
|
||||
textColor: colors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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/l2_action_wallet_selector.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SwitchNetworkWalletPage extends StatelessWidget {
|
||||
const SwitchNetworkWalletPage({
|
||||
required this.wallets,
|
||||
required this.networkName,
|
||||
required this.destinationIconPath,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String networkName;
|
||||
final String destinationIconPath;
|
||||
final List<WalletInfo> wallets;
|
||||
|
||||
static Future<WalletInfo?> push({
|
||||
required BuildContext context,
|
||||
required String networkName,
|
||||
required String targetIconPath,
|
||||
required List<WalletInfo> wallets,
|
||||
}) =>
|
||||
Navigator.of(context).push<WalletInfo>(
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => SwitchNetworkWalletPage(
|
||||
networkName: networkName,
|
||||
destinationIconPath: targetIconPath,
|
||||
wallets: wallets,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: colors.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
const SizedBox(height: 124),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.arrow_forward, color: colors.primary, size: 32),
|
||||
const SizedBox(width: 8),
|
||||
CakeImageWidget(
|
||||
imageUrl: destinationIconPath,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).switch_to_x_wallet(networkName),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: -0.1),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
S.of(context).select_compatible_wallet,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: wallets.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (_, index) {
|
||||
final wallet = wallets[index];
|
||||
return WalletRow(
|
||||
currencyIconPath: getCryptoCurrencyIconForWalletListItem(wallet.type),
|
||||
walletName: wallet.name,
|
||||
onTap: () => Navigator.of(context).pop(wallet),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,12 @@ class HistoryTradeTile extends StatelessWidget {
|
||||
to!.title,
|
||||
),
|
||||
if (toChainIcon?.isNotEmpty ?? false)
|
||||
CakeImageWidget(imageUrl: toChainIcon, width: 12, height: 12)
|
||||
CakeImageWidget(
|
||||
imageUrl: toChainIcon,
|
||||
width: 12,
|
||||
height: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
)
|
||||
],
|
||||
),
|
||||
leadingIcon: _getLeadingStack(context),
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/currency_groups.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import "package:cake_wallet/evm/evm.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/currency_groups.dart";
|
||||
import "package:cw_core/erc20_token.dart";
|
||||
import "package:cw_core/spl_token.dart";
|
||||
import "package:cw_core/tron_token.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
|
||||
String chainNameForCurrency(CryptoCurrency c) {
|
||||
if (c == CryptoCurrency.btcln) return 'Lightning';
|
||||
if (c == CryptoCurrency.btcln) {
|
||||
return "Lightning";
|
||||
}
|
||||
final wt = cryptoCurrencyOrTokenToWalletType(c);
|
||||
return wt != null ? walletTypeToString(wt) : (c.tag ?? '');
|
||||
return wt != null ? walletTypeToString(wt) : (c.tag ?? "");
|
||||
}
|
||||
|
||||
final Set<String> _stablecoinSymbols = {
|
||||
@@ -27,19 +32,25 @@ const _kEvmDefaultTokenNatives = <CryptoCurrency>[
|
||||
];
|
||||
|
||||
void appendEvmDefaultTokens(List<CryptoCurrency> into) {
|
||||
if (evm == null) return;
|
||||
if (evm == null) {
|
||||
return;
|
||||
}
|
||||
String keyFor(CryptoCurrency c) {
|
||||
final wt = cryptoCurrencyOrTokenToWalletType(c);
|
||||
final chain = wt?.toString() ?? (c.tag ?? '').toUpperCase();
|
||||
return '${c.title.toUpperCase()}|$chain';
|
||||
final chain = wt?.toString() ?? (c.tag ?? "").toUpperCase();
|
||||
return "${c.title.toUpperCase()}|$chain";
|
||||
}
|
||||
|
||||
final seen = <String>{for (final c in into) keyFor(c)};
|
||||
for (final native in _kEvmDefaultTokenNatives) {
|
||||
final chainId = getChainIdByCryptoCurrency(native);
|
||||
if (chainId == null) continue;
|
||||
if (chainId == null) {
|
||||
continue;
|
||||
}
|
||||
for (final t in evm!.getDefaultTokensByChainId(chainId)) {
|
||||
if (seen.add(keyFor(t))) into.add(t);
|
||||
if (seen.add(keyFor(t))) {
|
||||
into.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +63,55 @@ class CurrencyPickerBalance {
|
||||
final double? fiatValue;
|
||||
}
|
||||
|
||||
String? _assetAddressKey(CryptoCurrency c) {
|
||||
if (c is Erc20Token) {
|
||||
return c.contractAddress.toLowerCase();
|
||||
}
|
||||
if (c is TronToken) {
|
||||
return c.contractAddress.toLowerCase();
|
||||
}
|
||||
if (c is SPLToken) {
|
||||
return c.mintAddress.toLowerCase();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
CurrencyPickerBalance? balanceForAsset(
|
||||
Map<CryptoCurrency, CurrencyPickerBalance>? balances,
|
||||
CryptoCurrency asset,
|
||||
) {
|
||||
if (balances == null || balances.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final direct = balances[asset];
|
||||
if (direct != null) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
final addressKey = _assetAddressKey(asset);
|
||||
if (addressKey != null) {
|
||||
for (final entry in balances.entries) {
|
||||
if (_assetAddressKey(entry.key) == addressKey) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final title = asset.title.toUpperCase();
|
||||
CurrencyPickerBalance? byTitle;
|
||||
for (final entry in balances.entries) {
|
||||
if (entry.key.title.toUpperCase() == title) {
|
||||
if (byTitle != null) {
|
||||
return null;
|
||||
}
|
||||
byTitle = entry.value;
|
||||
}
|
||||
}
|
||||
return byTitle;
|
||||
}
|
||||
|
||||
enum RecentsSource { none, trades, orders }
|
||||
|
||||
class CurrencyPickerArgs {
|
||||
@@ -63,6 +123,7 @@ class CurrencyPickerArgs {
|
||||
required this.onSelected,
|
||||
required this.symbolResolver,
|
||||
this.recentsSource = RecentsSource.none,
|
||||
this.useSingleNetworkLayout = false,
|
||||
});
|
||||
|
||||
final CryptoCurrency? selected;
|
||||
@@ -72,6 +133,5 @@ class CurrencyPickerArgs {
|
||||
final String Function(CryptoCurrency) symbolResolver;
|
||||
final Map<CryptoCurrency, CurrencyPickerBalance>? balanceByAsset;
|
||||
final RecentsSource recentsSource;
|
||||
|
||||
bool get isPreFiltered => filterByNetwork != null;
|
||||
final bool useSingleNetworkLayout;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class CurrencyPickerSheet extends StatelessWidget {
|
||||
onLeadingPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
Expanded(
|
||||
child: args.isPreFiltered
|
||||
child: args.useSingleNetworkLayout
|
||||
? SingleNetworkCurrencyPicker(args: args)
|
||||
: MultiNetworkCurrencyPicker(args: args),
|
||||
),
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_row.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/currency_picker/picker_section_header.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_utils.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_row.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/picker_section_header.dart";
|
||||
import "package:cake_wallet/reactions/wallet_utils.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SingleNetworkCurrencyPicker extends StatefulWidget {
|
||||
const SingleNetworkCurrencyPicker({super.key, required this.args});
|
||||
const SingleNetworkCurrencyPicker({
|
||||
required this.args,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final CurrencyPickerArgs args;
|
||||
|
||||
@@ -41,24 +44,36 @@ class _SingleNetworkCurrencyPickerState extends State<SingleNetworkCurrencyPicke
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
|
||||
double _fiatValueFor(CryptoCurrency c) => _args.balanceByAsset?[c]?.fiatValue ?? 0;
|
||||
CurrencyPickerBalance? _balanceFor(CryptoCurrency c) => balanceForAsset(_args.balanceByAsset, c);
|
||||
|
||||
double _fiatValueFor(CryptoCurrency c) => _balanceFor(c)?.fiatValue ?? 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final native = walletTypeToCryptoCurrency(_network);
|
||||
final query = _searchController.text.trim();
|
||||
final tokens =
|
||||
_args.items.where((c) => c != native).where((c) => currencyMatchesQuery(c, query)).toList();
|
||||
final tokens = _args.items
|
||||
.where(
|
||||
(c) =>
|
||||
c != native &&
|
||||
cryptoCurrencyOrTokenToWalletType(c) == _network &&
|
||||
currencyMatchesQuery(c, query),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final insertOrder = {for (var i = 0; i < tokens.length; i++) tokens[i]: i};
|
||||
tokens.sort((a, b) {
|
||||
final av = _fiatValueFor(a);
|
||||
final bv = _fiatValueFor(b);
|
||||
if (bv != av) return bv.compareTo(av);
|
||||
if (bv != av) {
|
||||
return bv.compareTo(av);
|
||||
}
|
||||
return (insertOrder[a] ?? 0).compareTo(insertOrder[b] ?? 0);
|
||||
});
|
||||
final nativeMatches = currencyMatchesQuery(native, query);
|
||||
|
||||
final showSectionHeaders = _network != WalletType.bitcoin;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
@@ -66,40 +81,61 @@ class _SingleNetworkCurrencyPickerState extends State<SingleNetworkCurrencyPicke
|
||||
child: (nativeMatches || tokens.isNotEmpty)
|
||||
? ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
children: [
|
||||
if (nativeMatches) ...[
|
||||
PickerSectionHeader(title: S.of(context).picker_section_gas_token),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
CurrencyPickerRow(
|
||||
currency: native,
|
||||
isSelected: _args.selected == native,
|
||||
trailing: _BalanceTrailing(balance: _args.balanceByAsset?[native]),
|
||||
onTap: () => _selectCurrency(native),
|
||||
children: showSectionHeaders
|
||||
? [
|
||||
if (nativeMatches) ...[
|
||||
PickerSectionHeader(title: S.of(context).picker_section_gas_token),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
CurrencyPickerRow(
|
||||
currency: native,
|
||||
isSelected: _args.selected == native,
|
||||
trailing: _BalanceTrailing(balance: _balanceFor(native)),
|
||||
onTap: () => _selectCurrency(native),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (tokens.isNotEmpty) ...[
|
||||
PickerSectionHeader(
|
||||
title: S
|
||||
.of(context)
|
||||
.picker_section_tokens_standard(tokenStandardFor(_network)),
|
||||
),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final t in tokens)
|
||||
CurrencyPickerRow(
|
||||
currency: t,
|
||||
isSelected: _args.selected == t,
|
||||
trailing: _BalanceTrailing(balance: _balanceFor(t)),
|
||||
onTap: () => _selectCurrency(t),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
]
|
||||
: [
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
if (nativeMatches)
|
||||
CurrencyPickerRow(
|
||||
currency: native,
|
||||
isSelected: _args.selected == native,
|
||||
trailing: _BalanceTrailing(balance: _balanceFor(native)),
|
||||
onTap: () => _selectCurrency(native),
|
||||
),
|
||||
for (final t in tokens)
|
||||
CurrencyPickerRow(
|
||||
currency: t,
|
||||
isSelected: _args.selected == t,
|
||||
trailing: _BalanceTrailing(balance: _balanceFor(t)),
|
||||
onTap: () => _selectCurrency(t),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (tokens.isNotEmpty) ...[
|
||||
PickerSectionHeader(
|
||||
title: S
|
||||
.of(context)
|
||||
.picker_section_tokens_standard(tokenStandardFor(_network)),
|
||||
),
|
||||
CurrencyPickerListContainer(
|
||||
rows: [
|
||||
for (final t in tokens)
|
||||
CurrencyPickerRow(
|
||||
currency: t,
|
||||
isSelected: _args.selected == t,
|
||||
trailing: _BalanceTrailing(balance: _args.balanceByAsset?[t]),
|
||||
onTap: () => _selectCurrency(t),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: Padding(
|
||||
@@ -131,7 +167,7 @@ class _BalanceTrailing extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final amount = balance?.amount ?? '—';
|
||||
final amount = balance?.amount ?? "—";
|
||||
final fiat = balance?.fiat;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class NewPrimaryButton extends StatelessWidget {
|
||||
const NewPrimaryButton({
|
||||
required this.onPressed,
|
||||
this.image,
|
||||
required this.text,
|
||||
required this.color,
|
||||
required this.textColor,
|
||||
this.image,
|
||||
this.isLoading = false,
|
||||
this.borderColor = Colors.transparent,
|
||||
this.disabled = false,
|
||||
@@ -18,48 +17,46 @@ class NewPrimaryButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
final bool isLoading;
|
||||
final bool disabled;
|
||||
final SvgPicture? image;
|
||||
final Widget? image;
|
||||
final Color color;
|
||||
final Color textColor;
|
||||
final Color borderColor;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 52.0,
|
||||
child: TextButton(
|
||||
onPressed: disabled ? null : onPressed,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
disabled ? color.withAlpha(128) : color,
|
||||
),
|
||||
shape: WidgetStateProperty.all<RoundedSuperellipseBorder>(
|
||||
RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
height: 52,
|
||||
child: TextButton(
|
||||
onPressed: disabled ? null : onPressed,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
disabled ? color.withAlpha(128) : color,
|
||||
),
|
||||
shape: WidgetStateProperty.all<RoundedSuperellipseBorder>(
|
||||
RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: isLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: Row(
|
||||
spacing: 10,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (image != null) image!,
|
||||
Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: isLoading
|
||||
? CupertinoActivityIndicator()
|
||||
: Row(
|
||||
spacing: 10,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (image != null) image!,
|
||||
Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import "package:auto_size_text/auto_size_text.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/swap_page/provider_selector_page.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class AnyPaySwapFooter extends StatelessWidget {
|
||||
const AnyPaySwapFooter({required this.exchangeViewModel, super.key});
|
||||
|
||||
final ExchangeViewModel exchangeViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
if (exchangeViewModel.depositAmount.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final provider = exchangeViewModel.forcedProvider ?? exchangeViewModel.providerDisplay;
|
||||
final depositAmount = exchangeViewModel.depositAmount;
|
||||
final depositSymbol =
|
||||
exchangeViewModel.amountParsingProxy.getCryptoSymbol(exchangeViewModel.depositCurrency);
|
||||
final depositFiat = exchangeViewModel.roundedDepositAmountFiat(2);
|
||||
final showFiat = !exchangeViewModel.isFiatDisabled && depositFiat.isNotEmpty;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: colors.primary),
|
||||
),
|
||||
child: AutoSizeText.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: "$depositAmount $depositSymbol"),
|
||||
if (showFiat)
|
||||
TextSpan(
|
||||
text: " ≈ ${exchangeViewModel.fiat.symbol}$depositFiat",
|
||||
style: TextStyle(color: colors.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
maxLines: 1,
|
||||
minFontSize: 10,
|
||||
style: textTheme.bodySmall
|
||||
?.copyWith(fontWeight: FontWeight.w500, letterSpacing: -0.06),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: provider == null
|
||||
? null
|
||||
: () => Navigator.of(context).push(
|
||||
CupertinoPageRoute<void>(
|
||||
builder: (context) => Material(
|
||||
child: ProviderSelectorPage(exchangeViewModel: exchangeViewModel),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (provider != null)
|
||||
CakeImageWidget(imageUrl: provider.description.image, width: 24, height: 24)
|
||||
else
|
||||
const CupertinoActivityIndicator(),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
provider?.title ?? "${S.of(context).finding_provider}...",
|
||||
style: textTheme.bodyMedium?.copyWith(letterSpacing: -0.07),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/chooser.svg",
|
||||
width: 12,
|
||||
height: 12,
|
||||
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,789 @@
|
||||
import "package:cake_wallet/core/amount_validator.dart";
|
||||
import "package:cake_wallet/entities/qr_scanner.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/swap_page/refund_address_modal.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/swap_page/swap_address_selection_modal.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/utils/decimal_input_formatter.dart";
|
||||
import "package:cake_wallet/utils/permission_handler.dart";
|
||||
import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currencies_with_memo.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
import "package:mobx/mobx.dart" as mobx;
|
||||
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
|
||||
import "package:permission_handler/permission_handler.dart";
|
||||
|
||||
class SwapAmountBox extends StatefulWidget {
|
||||
SwapAmountBox({
|
||||
required this.exchangeViewModel,
|
||||
required this.isReceiverCard,
|
||||
required this.walletSwitcherViewModel,
|
||||
required this.currency,
|
||||
required this.currencies,
|
||||
required this.onCurrencySelected,
|
||||
this.currencyValueValidator,
|
||||
this.addressTextFieldValidator,
|
||||
this.title = "",
|
||||
this.hasRefundAddress = false,
|
||||
this.hasAllAmount = false,
|
||||
this.useBaseUnit = false,
|
||||
this.allAmount,
|
||||
this.onPushPasteButton,
|
||||
this.onPushAddressBookButton,
|
||||
this.filteredNetwork,
|
||||
this.sourceSelectorMode = false,
|
||||
this.walletName,
|
||||
this.balanceByAsset,
|
||||
this.useSingleNetworkLayout = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<Currency> currencies;
|
||||
final Function(Currency) onCurrencySelected;
|
||||
final String title;
|
||||
final bool isReceiverCard;
|
||||
final Currency currency;
|
||||
final bool useBaseUnit;
|
||||
final bool hasRefundAddress;
|
||||
final FormFieldValidator<String>? currencyValueValidator;
|
||||
final FormFieldValidator<String>? addressTextFieldValidator;
|
||||
final FormFieldValidator<String> allAmountValidator = AllAmountValidator();
|
||||
final bool hasAllAmount;
|
||||
final VoidCallback? allAmount;
|
||||
final void Function(BuildContext context)? onPushPasteButton;
|
||||
final void Function(BuildContext context)? onPushAddressBookButton;
|
||||
final ExchangeViewModel exchangeViewModel;
|
||||
final WalletSwitcherViewModel walletSwitcherViewModel;
|
||||
final WalletType? filteredNetwork;
|
||||
final bool sourceSelectorMode;
|
||||
final String? walletName;
|
||||
final Map<CryptoCurrency, CurrencyPickerBalance>? balanceByAsset;
|
||||
final bool useSingleNetworkLayout;
|
||||
|
||||
@override
|
||||
State<SwapAmountBox> createState() => SwapAmountBoxState();
|
||||
}
|
||||
|
||||
class SwapAmountBoxState extends State<SwapAmountBox> {
|
||||
final addressController = TextEditingController();
|
||||
final amountController = TextEditingController();
|
||||
final fiatAmountController = TextEditingController();
|
||||
final amountFocusNode = FocusNode();
|
||||
final memoController = TextEditingController();
|
||||
mobx.ReactionDisposer? _memoReactionDisposer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (widget.isReceiverCard) {
|
||||
memoController.text = widget.exchangeViewModel.receiveAddressExtraId;
|
||||
|
||||
memoController.addListener(() {
|
||||
if (widget.exchangeViewModel.receiveAddressExtraId != memoController.text) {
|
||||
widget.exchangeViewModel.receiveAddressExtraId = memoController.text;
|
||||
}
|
||||
});
|
||||
|
||||
_memoReactionDisposer =
|
||||
mobx.reaction((_) => widget.exchangeViewModel.receiveAddressExtraId, (String value) {
|
||||
if (memoController.text != value) {
|
||||
memoController.text = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_memoReactionDisposer?.call();
|
||||
memoController.dispose();
|
||||
addressController.dispose();
|
||||
amountController.dispose();
|
||||
fiatAmountController.dispose();
|
||||
amountFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _fiatInputMode = false;
|
||||
|
||||
/// Whether the amount field is currently taking fiat input, the page's
|
||||
/// best-rate reaction needs this to know which controller holds the amount.
|
||||
bool get fiatInputMode => _fiatInputMode;
|
||||
|
||||
CurrencyPickerBalance? _sourceBalance() {
|
||||
final selected = widget.currency;
|
||||
if (selected is! CryptoCurrency) {
|
||||
return null;
|
||||
}
|
||||
return balanceForAsset(widget.balanceByAsset, selected);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currencyToShow = (widget.currency is CryptoCurrency)
|
||||
? widget.exchangeViewModel.amountParsingProxy
|
||||
.getCryptoSymbol(widget.currency as CryptoCurrency)
|
||||
: widget.currency.name.toUpperCase();
|
||||
|
||||
final chainIconPath = (widget.currency is CryptoCurrency)
|
||||
? _getCurrencyChainIconPath(widget.currency as CryptoCurrency)
|
||||
: null;
|
||||
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
if (widget.sourceSelectorMode) {
|
||||
final available = _sourceBalance()?.amount.split(" ").first ?? "—";
|
||||
return Column(
|
||||
spacing: 12,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _presentCurrencyPicker,
|
||||
child: Container(
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: widget.currency.iconPath ?? "",
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
currencyToShow,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(letterSpacing: -0.08),
|
||||
),
|
||||
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
|
||||
const SizedBox(width: 4),
|
||||
CakeImageWidget(
|
||||
imageUrl: chainIconPath,
|
||||
width: 12,
|
||||
height: 12,
|
||||
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/chooser.svg",
|
||||
width: 12,
|
||||
height: 12,
|
||||
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/wallet_filled.svg",
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.walletName ?? "",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.06,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"${S.of(context).avl}: $available",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.06,
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
spacing: 12,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.title.isNotEmpty)
|
||||
Text(
|
||||
widget.title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: ShapeDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(24)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: amountFocusNode.requestFocus,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: IntrinsicWidth(
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final showFetching = !widget.isReceiverCard &&
|
||||
widget.exchangeViewModel.isFixedRateMode &&
|
||||
widget.exchangeViewModel.receiveAmount.isNotEmpty &&
|
||||
widget.exchangeViewModel.depositAmount.isEmpty;
|
||||
return TextFormField(
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false,
|
||||
decimal: !widget.useBaseUnit,
|
||||
),
|
||||
validator:
|
||||
_fiatInputMode ? null : widget.currencyValueValidator,
|
||||
controller:
|
||||
_fiatInputMode ? fiatAmountController : amountController,
|
||||
focusNode: amountFocusNode,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
hintText: showFetching ? S.of(context).fetching : "0",
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: Colors.transparent,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
DecimalInputFormatter(
|
||||
maxDecimals:
|
||||
widget.useBaseUnit ? 0 : widget.currency.decimals,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_fiatInputMode)
|
||||
Center(
|
||||
child: Text(
|
||||
widget.exchangeViewModel.fiat.title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _presentCurrencyPicker,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(999999),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 4, right: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: widget.currency.iconPath ?? "",
|
||||
width: 28,
|
||||
height: 28,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
currencyToShow,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
|
||||
const SizedBox(width: 4),
|
||||
CakeImageWidget(
|
||||
imageUrl: chainIconPath,
|
||||
width: 12,
|
||||
height: 12,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
] else
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(9999999999),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg",
|
||||
width: 4,
|
||||
height: 4,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
final hasAllAmount = !(widget.isReceiverCard ||
|
||||
widget.exchangeViewModel.isSendFromExternal ||
|
||||
!widget.exchangeViewModel.hasAllAmount);
|
||||
|
||||
return FiatAmountBar(
|
||||
foregroundElementColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
textColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
allAmountColor: hasAllAmount ? null : Colors.transparent,
|
||||
allAmountTextColor:
|
||||
hasAllAmount ? null : Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fiatInputMode: _fiatInputMode,
|
||||
allAmount: !hasAllAmount
|
||||
? widget.isReceiverCard
|
||||
? null
|
||||
: widget.exchangeViewModel.balanceDisplay
|
||||
: widget.exchangeViewModel.depositAvailableAmount,
|
||||
onSwitchButtonPressed: () {
|
||||
setState(() => _fiatInputMode = !_fiatInputMode);
|
||||
if (_fiatInputMode) {
|
||||
updateFiatAmount();
|
||||
} else {
|
||||
amountController.text = widget.isReceiverCard
|
||||
? widget.exchangeViewModel.receiveAmount
|
||||
: widget.exchangeViewModel.depositAmount;
|
||||
}
|
||||
},
|
||||
onAllButtonPressed: () {
|
||||
setState(() {
|
||||
_fiatInputMode = false;
|
||||
});
|
||||
widget.allAmount?.call();
|
||||
},
|
||||
cryptoAmount: widget.isReceiverCard
|
||||
? widget.exchangeViewModel.roundedReceiveAmount(6)
|
||||
: widget.exchangeViewModel.roundedDepositAmount(6),
|
||||
fiatAmount: widget.isReceiverCard
|
||||
? widget.exchangeViewModel.roundedReceiveAmountFiat(6)
|
||||
: widget.exchangeViewModel.roundedDepositAmountFiat(6),
|
||||
cryptoCurrencySymbol: currencyToShow,
|
||||
fiatCurrencySymbol: widget.exchangeViewModel.fiat.symbol,
|
||||
);
|
||||
},
|
||||
),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
final addressEmpty = (widget.isReceiverCard &&
|
||||
widget.exchangeViewModel.receiveAddress.isEmpty) ||
|
||||
(!widget.isReceiverCard && widget.exchangeViewModel.depositAddress.isEmpty);
|
||||
final addressPickerText = widget.isReceiverCard
|
||||
? (addressEmpty ? S.of(context).select_receiver : S.of(context).to)
|
||||
: S.of(context).from;
|
||||
final addressDescription = widget.isReceiverCard
|
||||
? widget.exchangeViewModel.receiveAddressDisplayName ??
|
||||
_middleTruncate(widget.exchangeViewModel.receiveAddress, 8, 8)
|
||||
: widget.exchangeViewModel.isSendFromExternal
|
||||
? S.of(context).external
|
||||
: widget.exchangeViewModel.wallet.name;
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
onTap: _presentWalletPicker,
|
||||
child: Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: (addressEmpty && widget.isReceiverCard)
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(9999),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Observer(
|
||||
builder: (_) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Text(
|
||||
addressPickerText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: (addressEmpty && widget.isReceiverCard)
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
addressDescription,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/dropdown_arrow.svg",
|
||||
colorFilter: ColorFilter.mode(
|
||||
(addressEmpty && widget.isReceiverCard)
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!widget.isReceiverCard &&
|
||||
widget.exchangeViewModel.isSendFromExternal &&
|
||||
widget.exchangeViewModel.depositAddress.isEmpty)
|
||||
ModernButton.svg(
|
||||
svgPath: "assets/new-ui/refund_address.svg",
|
||||
onPressed: askForRefundAddress,
|
||||
size: 36,
|
||||
iconSize: 18,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
semanticLabel: S.of(context).refund_address,
|
||||
),
|
||||
if (widget.isReceiverCard &&
|
||||
widget.exchangeViewModel.receiveAddress.isEmpty) ...[
|
||||
ModernButton.svg(
|
||||
svgPath: "assets/new-ui/paste.svg",
|
||||
onPressed: () => widget.onPushPasteButton?.call(context),
|
||||
size: 36,
|
||||
iconSize: 20,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
semanticLabel: S.of(context).paste,
|
||||
),
|
||||
ModernButton.svg(
|
||||
svgPath: "assets/new-ui/scan.svg",
|
||||
onPressed: () => _presentQRScanner(context),
|
||||
size: 36,
|
||||
iconSize: 20,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
semanticLabel: S.of(context).scan,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.isReceiverCard)
|
||||
Observer(
|
||||
builder: (_) {
|
||||
final selected = widget.exchangeViewModel.receiveCurrency;
|
||||
final labelType = memoLabelTypeFor(selected);
|
||||
if (labelType == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isDestinationTag = labelType == MemoLabelType.destinationTag;
|
||||
final hint = isDestinationTag
|
||||
? S.of(context).destination_tag_optional
|
||||
: S.of(context).memo_optional;
|
||||
final disclaimer = isDestinationTag
|
||||
? S.of(context).destination_tag_swap_disclaimer
|
||||
: S.of(context).memo_swap_disclaimer;
|
||||
|
||||
return NewSendMemoInput(
|
||||
memoController: memoController,
|
||||
maxMemoLength: isDestinationTag ? 20 : 256,
|
||||
memoLength: memoController.text.length,
|
||||
hintText: hint,
|
||||
disclaimerText: disclaimer,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void changeAddress({required String address}) {
|
||||
setState(() => addressController.text = _normalizeAddressFormat(address));
|
||||
}
|
||||
|
||||
void changeAmount({required String amount}) {
|
||||
setState(() => amountController.text = amount);
|
||||
}
|
||||
|
||||
String _normalizeAddressFormat(String address) {
|
||||
if (address.startsWith("bitcoincash:")) {
|
||||
address = address.substring(12);
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
void _presentCurrencyPicker() {
|
||||
final rawCurrencies = widget.isReceiverCard
|
||||
? widget.exchangeViewModel.receiveCurrencies
|
||||
: widget.exchangeViewModel.depositCurrencies;
|
||||
final currencies = rawCurrencies.whereType<CryptoCurrency>().toList();
|
||||
final restrictToCurrentBalances = !widget.isReceiverCard &&
|
||||
widget.balanceByAsset != null &&
|
||||
widget.balanceByAsset!.isNotEmpty;
|
||||
if (!restrictToCurrentBalances) {
|
||||
appendEvmDefaultTokens(currencies);
|
||||
}
|
||||
if (widget.exchangeViewModel.wallet.type == WalletType.bitcoin) {
|
||||
currencies.sort((a, b) {
|
||||
if (a == CryptoCurrency.btcln) {
|
||||
return -1;
|
||||
}
|
||||
if (b == CryptoCurrency.btcln) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
List<CryptoCurrency> items = currencies;
|
||||
if (restrictToCurrentBalances) {
|
||||
final allowed = {
|
||||
for (final asset in widget.balanceByAsset!.keys) asset.title.toUpperCase(),
|
||||
};
|
||||
items = currencies.where((c) => allowed.contains(c.title.toUpperCase())).toList();
|
||||
}
|
||||
|
||||
if (widget.isReceiverCard && widget.filteredNetwork != null) {
|
||||
final network = widget.filteredNetwork!;
|
||||
items = items.where((c) => cryptoCurrencyOrTokenToWalletType(c) == network).toList();
|
||||
}
|
||||
|
||||
if (items.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final selected = widget.isReceiverCard
|
||||
? widget.exchangeViewModel.receiveCurrency
|
||||
: widget.exchangeViewModel.depositCurrency;
|
||||
|
||||
CurrencyPickerSheet.show(
|
||||
context: context,
|
||||
args: CurrencyPickerArgs(
|
||||
items: items,
|
||||
selected: selected,
|
||||
filterByNetwork: widget.filteredNetwork,
|
||||
balanceByAsset: widget.balanceByAsset,
|
||||
useSingleNetworkLayout: widget.useSingleNetworkLayout,
|
||||
recentsSource: RecentsSource.trades,
|
||||
onSelected: widget.onCurrencySelected,
|
||||
symbolResolver: widget.exchangeViewModel.amountParsingProxy.getCryptoSymbol,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _presentQRScanner(BuildContext context) async {
|
||||
final isCameraPermissionGranted =
|
||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||
if (!isCameraPermissionGranted) {
|
||||
return;
|
||||
}
|
||||
final code = await presentQRScanner(context);
|
||||
if (code == null) {
|
||||
return;
|
||||
}
|
||||
if (code.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(code);
|
||||
widget.exchangeViewModel.receiveAddress = uri.path;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _presentWalletPicker() async {
|
||||
final res = await showMaterialModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
||||
),
|
||||
child: SwapAddressSelectionModal(
|
||||
isSelectingReceiver: widget.isReceiverCard,
|
||||
exchangeViewModel: widget.exchangeViewModel,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (res != null && res is SwapAddressSelectionResult) {
|
||||
if (widget.isReceiverCard) {
|
||||
widget.exchangeViewModel.selectedAddressBookWallet = res.walletInfo;
|
||||
widget.exchangeViewModel.receiveAddress = res.address!;
|
||||
if (res.walletInfo?.name != null) {
|
||||
if (res.accountName != null) {
|
||||
widget.exchangeViewModel.receiveAddressDisplayName =
|
||||
"${res.walletInfo!.name} → ${res.accountName}";
|
||||
} else {
|
||||
widget.exchangeViewModel.receiveAddressDisplayName = res.walletInfo!.name;
|
||||
}
|
||||
}
|
||||
} else if (res.address == null || res.address!.isEmpty) {
|
||||
widget.exchangeViewModel.isSendFromExternal = true;
|
||||
askForRefundAddress();
|
||||
} else {
|
||||
widget.exchangeViewModel.isSendFromExternal = false;
|
||||
switchToDepositWallet(res.walletInfo!.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void switchToDepositWallet(String walletName) async {
|
||||
final walletType = cryptoCurrencyOrTokenToWalletType(widget.exchangeViewModel.depositCurrency);
|
||||
if (walletType == null) {
|
||||
return;
|
||||
}
|
||||
final wallet = await WalletInfo.get(walletName, walletType);
|
||||
if (wallet == null) {
|
||||
return;
|
||||
}
|
||||
widget.exchangeViewModel.depositAddress = wallet.address;
|
||||
addressController.text = _normalizeAddressFormat(wallet.address);
|
||||
widget.walletSwitcherViewModel.selectWallet(wallet);
|
||||
await widget.walletSwitcherViewModel.switchToSelectedWallet();
|
||||
}
|
||||
|
||||
void askForRefundAddress() async {
|
||||
final refundAddress = await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => RefundAddressModal(
|
||||
selectedCurrency: widget.exchangeViewModel.depositCurrency,
|
||||
isFromWalletSelection: true,
|
||||
),
|
||||
);
|
||||
if (refundAddress != null && refundAddress is String) {
|
||||
widget.exchangeViewModel.depositAddress = refundAddress;
|
||||
} else {
|
||||
widget.exchangeViewModel.depositAddress = "";
|
||||
}
|
||||
}
|
||||
|
||||
void updateFiatAmount() {
|
||||
final newText = widget.isReceiverCard
|
||||
? widget.exchangeViewModel.receiveAmountFiat
|
||||
: widget.exchangeViewModel.depositAmountFiat;
|
||||
|
||||
if (double.tryParse(fiatAmountController.text) != double.tryParse(newText)) {
|
||||
if (newText == "0.00") {
|
||||
fiatAmountController.text = "";
|
||||
} else {
|
||||
fiatAmountController.text =
|
||||
newText.replaceAll(RegExp(r"(?<=\.\d*)0+$"), "").replaceAll(RegExp(r"\.$"), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _middleTruncate(String s, int head, int tail) {
|
||||
if (s.length <= head + tail + 3) {
|
||||
return s;
|
||||
}
|
||||
return s.substring(0, head) + "..." + s.substring(s.length - tail);
|
||||
}
|
||||
|
||||
String? _getCurrencyChainIconPath(CryptoCurrency curr) {
|
||||
try {
|
||||
if (curr.chainIconPath != null) {
|
||||
return curr.chainIconPath!;
|
||||
}
|
||||
|
||||
if (curr.tag != null) {
|
||||
final currencyFromTag = CryptoCurrency.fromString(curr.tag!);
|
||||
|
||||
if (currencyFromTag.chainIconPath != null) {
|
||||
return currencyFromTag.chainIconPath!;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
|
||||
class SwapFromSendArgs {
|
||||
const SwapFromSendArgs({
|
||||
required this.recipientAddress,
|
||||
required this.receiveCurrency,
|
||||
required this.targetWalletType,
|
||||
this.depositBalanceByAsset,
|
||||
this.receiveAmount,
|
||||
});
|
||||
|
||||
final String recipientAddress;
|
||||
final CryptoCurrency receiveCurrency;
|
||||
final WalletType targetWalletType;
|
||||
final Map<CryptoCurrency, CurrencyPickerBalance>? depositBalanceByAsset;
|
||||
final Money? receiveAmount;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cw_core/currency_for_wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SwapSectionHeader extends StatelessWidget {
|
||||
const SwapSectionHeader({
|
||||
required this.label,
|
||||
required this.networkName,
|
||||
required this.networkIconPath,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String networkName;
|
||||
final String networkIconPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style:
|
||||
textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500, letterSpacing: -0.07),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CakeImageWidget(
|
||||
imageUrl: networkIconPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
networkName,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.07,
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,17 @@ String walletConnectCompatibleChainsLabel() {
|
||||
return '$head, and ${names.last}';
|
||||
}
|
||||
|
||||
String networkDisplayName(WalletType type, int? chainId) {
|
||||
if (evm != null && isEVMCompatibleChain(type)) {
|
||||
final id = chainId ?? evm!.getChainIdByWalletType(type);
|
||||
final info = evm!.getChainInfoByChainId(id);
|
||||
if (info != null) {
|
||||
return info.name;
|
||||
}
|
||||
}
|
||||
return walletTypeToString(type);
|
||||
}
|
||||
|
||||
bool isEVMCompatibleChain(WalletType walletType) {
|
||||
switch (walletType) {
|
||||
case WalletType.polygon:
|
||||
|
||||
@@ -381,7 +381,7 @@ class _TokenSelectionContentState extends State<_TokenSelectionContent> {
|
||||
detectedWalletType: WalletType.solana,
|
||||
detectedCurrency: selectedToken!,
|
||||
isValid: true,
|
||||
amount: widget.paymentRequest.amount,
|
||||
amount: widget.paymentRequest.resolveTokenAmount(selectedToken!),
|
||||
note: widget.paymentRequest.note,
|
||||
scheme: widget.paymentRequest.scheme,
|
||||
pjUri: widget.paymentRequest.pjUri,
|
||||
@@ -398,7 +398,7 @@ class _TokenSelectionContentState extends State<_TokenSelectionContent> {
|
||||
detectedWalletType: WalletType.tron,
|
||||
detectedCurrency: selectedToken!,
|
||||
isValid: true,
|
||||
amount: widget.paymentRequest.amount,
|
||||
amount: widget.paymentRequest.resolveTokenAmount(selectedToken!),
|
||||
note: widget.paymentRequest.note,
|
||||
scheme: widget.paymentRequest.scheme,
|
||||
pjUri: widget.paymentRequest.pjUri,
|
||||
@@ -415,7 +415,7 @@ class _TokenSelectionContentState extends State<_TokenSelectionContent> {
|
||||
detectedWalletType: selectedNetwork!,
|
||||
detectedCurrency: selectedToken!,
|
||||
isValid: true,
|
||||
amount: widget.paymentRequest.amount,
|
||||
amount: widget.paymentRequest.resolveTokenAmount(selectedToken!),
|
||||
note: widget.paymentRequest.note,
|
||||
scheme: widget.paymentRequest.scheme,
|
||||
pjUri: widget.paymentRequest.pjUri,
|
||||
|
||||
@@ -89,16 +89,28 @@ class ListItemRegularRowWidget extends StatelessWidget {
|
||||
children: [
|
||||
leadingIcon!,
|
||||
Positioned(
|
||||
right: -3,
|
||||
bottom: -3,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: badgeIconPath!,
|
||||
width: badgeIconSize ?? 10,
|
||||
height: badgeIconSize ?? 10,
|
||||
errorWidget: leadingIconErrorWidget,
|
||||
colorFilter: iconColor == null
|
||||
? null
|
||||
: ColorFilter.mode(iconColor!, BlendMode.srcIn),
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
width: 2,
|
||||
),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: badgeIconPath!,
|
||||
width: badgeIconSize ?? 12,
|
||||
height: badgeIconSize ?? 12,
|
||||
fit: BoxFit.cover,
|
||||
color: theme.colorScheme.surface,
|
||||
errorWidget: leadingIconErrorWidget,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -11,4 +11,9 @@ class Debounce {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(duration, action);
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,41 @@
|
||||
import 'package:cw_core/amount/amount_sanitizer.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/lnurl.dart';
|
||||
import 'package:cw_core/payment_uris.dart';
|
||||
import 'package:cake_wallet/nano/nano.dart';
|
||||
import "package:cake_wallet/nano/nano.dart";
|
||||
import "package:cw_core/amount/amount_sanitizer.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/format_fixed.dart";
|
||||
import "package:cw_core/lnurl.dart";
|
||||
import "package:cw_core/payment_uris.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
|
||||
class PaymentRequest {
|
||||
PaymentRequest(this.address, this.amount, this.note, this.scheme, this.pjUri,
|
||||
{this.callbackUrl, this.callbackMessage, this.contractAddress});
|
||||
PaymentRequest(
|
||||
this.address,
|
||||
this.amount,
|
||||
this.note,
|
||||
this.scheme,
|
||||
this.pjUri, {
|
||||
this.callbackUrl,
|
||||
this.callbackMessage,
|
||||
this.contractAddress,
|
||||
this.chainId,
|
||||
this.rawTokenAmount,
|
||||
});
|
||||
|
||||
factory PaymentRequest.fromString(String input) {
|
||||
try {
|
||||
return PaymentRequest.fromBolt11(input);
|
||||
} catch (_) {
|
||||
return PaymentRequest.fromUri(Uri.parse(input));
|
||||
try {
|
||||
return PaymentRequest.fromUri(Uri.parse(input));
|
||||
} catch (_) {
|
||||
return PaymentRequest(input, "", "", "", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
factory PaymentRequest.fromBolt11(String invoice) {
|
||||
final amount = getBolt11Amount(invoice) ?? Money.zero(CryptoCurrency.btcln);
|
||||
return PaymentRequest(invoice, amount.toString(), '', 'lightning', null);
|
||||
return PaymentRequest(invoice, amount.toString(), "", "lightning", null);
|
||||
}
|
||||
|
||||
factory PaymentRequest.fromUri(Uri? uri) {
|
||||
@@ -32,13 +48,15 @@ class PaymentRequest {
|
||||
String? callbackMessage;
|
||||
String? pjUri;
|
||||
String? contractAddress;
|
||||
int? chainId;
|
||||
String? rawTokenAmount;
|
||||
|
||||
if (uri != null) {
|
||||
if (uri.queryParameters['pj'] != null) {
|
||||
if (uri.queryParameters["pj"] != null) {
|
||||
pjUri = uri.toString();
|
||||
}
|
||||
|
||||
address = uri.queryParameters['address'] ?? uri.path;
|
||||
address = uri.queryParameters["address"] ?? uri.path;
|
||||
try {
|
||||
final lnAmount = getBolt11Amount(uri.path) ?? Money.zero(CryptoCurrency.btcln);
|
||||
|
||||
@@ -47,27 +65,30 @@ class PaymentRequest {
|
||||
}
|
||||
} catch (_) {}
|
||||
if (amount.isEmpty) {
|
||||
amount = uri.queryParameters['tx_amount'] ?? uri.queryParameters['amount'] ?? "";
|
||||
amount = uri.queryParameters["tx_amount"] ?? uri.queryParameters["amount"] ?? "";
|
||||
}
|
||||
note = uri.queryParameters['tx_description'] ?? uri.queryParameters['message'] ?? "";
|
||||
note = uri.queryParameters["tx_description"] ?? uri.queryParameters["message"] ?? "";
|
||||
scheme = uri.scheme;
|
||||
callbackUrl = uri.queryParameters['callback'];
|
||||
callbackMessage = uri.queryParameters['callbackMessage'];
|
||||
walletType = uri.queryParameters['type'];
|
||||
callbackUrl = uri.queryParameters["callback"];
|
||||
callbackMessage = uri.queryParameters["callbackMessage"];
|
||||
walletType = uri.queryParameters["type"];
|
||||
|
||||
if (scheme == "ethereum") {
|
||||
final paymentUri = ERC681URI.fromUri(uri);
|
||||
final hasExplicitChainId = uri.path.contains("@");
|
||||
|
||||
address = paymentUri.address;
|
||||
amount = paymentUri.amount;
|
||||
contractAddress = paymentUri.contractAddress;
|
||||
chainId = hasExplicitChainId ? paymentUri.chainId : null;
|
||||
rawTokenAmount = paymentUri.rawTokenAmount;
|
||||
} else if (scheme == "tron") {
|
||||
final token = uri.queryParameters['token'];
|
||||
final token = uri.queryParameters["token"];
|
||||
if (token != null && token.isNotEmpty) {
|
||||
contractAddress = token;
|
||||
}
|
||||
} else if (scheme == "solana") {
|
||||
final splToken = uri.queryParameters['spl-token'];
|
||||
final splToken = uri.queryParameters["spl-token"];
|
||||
if (splToken != null && splToken.isNotEmpty) {
|
||||
contractAddress = splToken;
|
||||
}
|
||||
@@ -99,6 +120,8 @@ class PaymentRequest {
|
||||
callbackUrl: callbackUrl,
|
||||
callbackMessage: callbackMessage,
|
||||
contractAddress: contractAddress,
|
||||
chainId: chainId,
|
||||
rawTokenAmount: rawTokenAmount,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,23 +133,53 @@ class PaymentRequest {
|
||||
final String? callbackUrl;
|
||||
final String? callbackMessage;
|
||||
final String? contractAddress;
|
||||
final int? chainId;
|
||||
final String? rawTokenAmount;
|
||||
|
||||
String? resolveTokenAmount(CryptoCurrency token) {
|
||||
if (amount.isNotEmpty) {
|
||||
return amount;
|
||||
}
|
||||
|
||||
if (rawTokenAmount == null || rawTokenAmount!.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final raw = BigInt.tryParse(rawTokenAmount!);
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1e9 whole tokens is beyond any real payment, so a larger result means the QR
|
||||
// came from an old app version that encoded with 18 decimals regardless of token.
|
||||
if (token.decimals != 18 && raw > BigInt.from(10).pow(token.decimals + 9)) {
|
||||
printV("resolveTokenAmount: reading $rawTokenAmount with legacy 18 decimals");
|
||||
return formatFixed(raw, 18);
|
||||
}
|
||||
|
||||
return formatFixed(raw, token.decimals);
|
||||
}
|
||||
|
||||
/// Checks if the amount string is already in a usable format (e.g., "123.45") and doesn't need to be converted from raw format.
|
||||
///
|
||||
/// This was causing an error for us when we scan Nano QRs with amounts in them, the amounts are already in usable format so when the parsing was done, it returns 0 wrongly.
|
||||
static bool _isAlreadyUsableAmount(String amount) {
|
||||
if (amount.isEmpty) return false;
|
||||
if (amount.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final parsed = double.tryParse(amount.sanitized());
|
||||
if (parsed == null) return false;
|
||||
|
||||
// Check if the amount contains a decimal point and is a reasonable number,
|
||||
// it's likely already in usable format rather than raw format
|
||||
// Raw amounts are typically very large integers without decimal points
|
||||
if (amount.contains('.') && parsed > 0 && parsed < 1000000000) return true;
|
||||
if (parsed == null) {
|
||||
return false;
|
||||
}
|
||||
if (amount.contains(".") && parsed > 0 && parsed < 1000000000) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's a small integer (less than 1 billion), it's likely already usable
|
||||
if (parsed == parsed.toInt() && parsed < 1000000000) return true;
|
||||
if (parsed == parsed.toInt() && parsed < 1000000000) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -222,6 +222,24 @@ class TokenUtilities {
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? findSolanaTokenMint(CryptoCurrency currency) {
|
||||
if (currency is SPLToken) return currency.mintAddress;
|
||||
|
||||
for (final token in loadDefaultSolTokensForSwap()) {
|
||||
if (_matchesCurrency(token, currency)) return token.mintAddress;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? findTronTokenContract(CryptoCurrency currency) {
|
||||
if (currency is TronToken) return currency.contractAddress;
|
||||
|
||||
for (final token in loadDefaultTronTokensForSwap()) {
|
||||
if (_matchesCurrency(token, currency)) return token.contractAddress;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool isNativeToken(CryptoCurrency currency) {
|
||||
final title = currency.title.toLowerCase();
|
||||
final tag = currency.tag?.toLowerCase();
|
||||
@@ -289,9 +307,7 @@ class TokenUtilities {
|
||||
final allTokens = <CryptoCurrency>[baseCurrency];
|
||||
final addedAddresses = <String>{};
|
||||
|
||||
// Handle EVM networks
|
||||
if (isEVMCompatibleChain(network)) {
|
||||
// First, collect all user tokens
|
||||
final userTokens = await _getUserTokensForNetwork(baseCurrency);
|
||||
for (final token in userTokens) {
|
||||
if (token is Erc20Token) {
|
||||
@@ -302,7 +318,17 @@ class TokenUtilities {
|
||||
}
|
||||
}
|
||||
|
||||
// Then add tokens from CryptoCurrency.all that don't duplicate user tokens
|
||||
if (evm != null) {
|
||||
final chainId = evm!.getChainIdByWalletType(network);
|
||||
for (final token in evm!.getDefaultTokensByChainId(chainId)) {
|
||||
final address = token.contractAddress.toLowerCase();
|
||||
if (addedAddresses.add(address)) {
|
||||
allTokens.add(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add tokens from CryptoCurrency.all that don't duplicate user tokens
|
||||
for (final currency in CryptoCurrency.all) {
|
||||
// Match by tag for POL/BASE, match by title==tag for ETH
|
||||
final matches = (baseCurrency.tag == null && baseCurrency.title == currency.tag) ||
|
||||
@@ -320,10 +346,7 @@ class TokenUtilities {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Solana network
|
||||
else if (network == WalletType.solana) {
|
||||
} else if (network == WalletType.solana) {
|
||||
final userSolTokens = await loadAllUniqueSolTokens();
|
||||
for (final token in userSolTokens) {
|
||||
final mintAddress = token.mintAddress.toLowerCase();
|
||||
@@ -332,6 +355,13 @@ class TokenUtilities {
|
||||
}
|
||||
}
|
||||
|
||||
for (final token in loadDefaultSolTokensForSwap()) {
|
||||
final mintAddress = token.mintAddress.toLowerCase();
|
||||
if (addedAddresses.add(mintAddress)) {
|
||||
allTokens.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
for (final currency in CryptoCurrency.all) {
|
||||
if (currency.tag?.toLowerCase() == 'sol') {
|
||||
if (currency is SPLToken) {
|
||||
@@ -356,6 +386,13 @@ class TokenUtilities {
|
||||
}
|
||||
}
|
||||
|
||||
for (final token in loadDefaultTronTokensForSwap()) {
|
||||
final contractAddress = token.contractAddress.toLowerCase();
|
||||
if (addedAddresses.add(contractAddress)) {
|
||||
allTokens.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
for (final currency in CryptoCurrency.all) {
|
||||
if (currency.tag?.toLowerCase() == 'trx') {
|
||||
if (currency is TronToken) {
|
||||
|
||||
@@ -448,9 +448,17 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
case WalletType.bsc:
|
||||
return _createERC681URI(fromCurrency, inputAddress, amount);
|
||||
case WalletType.solana:
|
||||
return SolanaURI(amount: amount, address: inputAddress);
|
||||
return SolanaURI(
|
||||
amount: amount,
|
||||
address: inputAddress,
|
||||
contractAddress: TokenUtilities.findSolanaTokenMint(fromCurrency),
|
||||
);
|
||||
case WalletType.tron:
|
||||
return TronURI(amount: amount, address: inputAddress);
|
||||
return TronURI(
|
||||
amount: amount,
|
||||
address: inputAddress,
|
||||
contractAddress: TokenUtilities.findTronTokenContract(fromCurrency),
|
||||
);
|
||||
case WalletType.monero:
|
||||
return MoneroURI(address: inputAddress, amount: amount);
|
||||
case WalletType.wownero:
|
||||
@@ -494,6 +502,7 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
address: address,
|
||||
amount: amount,
|
||||
contractAddress: erc20Token.contractAddress,
|
||||
tokenDecimals: erc20Token.decimal,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -146,6 +146,13 @@ abstract class PaymentViewModelBase with Store {
|
||||
await evm!.selectChain(appStore.wallet!, detectedChainId!, node: node);
|
||||
}
|
||||
|
||||
void applyManualEvmSelection(AddressDetectionResult detection) {
|
||||
runInAction(() {
|
||||
_lastDetectionResult = detection;
|
||||
detectedWalletType = detection.detectedWalletType;
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<WalletInfo>> getWalletsByType(WalletType walletType) async {
|
||||
return (await WalletInfo.getAll()).where((wallet) => wallet.type == walletType).toList();
|
||||
}
|
||||
@@ -245,7 +252,8 @@ class PaymentFlowResult {
|
||||
if (chainId == null && addressDetectionResult.detectedCurrency != null) {
|
||||
chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!);
|
||||
}
|
||||
if (chainId == null) {
|
||||
|
||||
if (chainId == null && isEVMCompatibleChain(wallet.type)) {
|
||||
chainId = evm?.getChainIdByWalletType(wallet.type);
|
||||
}
|
||||
|
||||
@@ -267,7 +275,7 @@ class PaymentFlowResult {
|
||||
if (chainId == null && addressDetectionResult.detectedCurrency != null) {
|
||||
chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!);
|
||||
}
|
||||
if (chainId == null) {
|
||||
if (chainId == null && isEVMCompatibleChain(wallets.first.type)) {
|
||||
chainId = evm?.getChainIdByWalletType(wallets.first.type);
|
||||
}
|
||||
|
||||
@@ -289,7 +297,7 @@ class PaymentFlowResult {
|
||||
if (chainId == null && addressDetectionResult.detectedCurrency != null) {
|
||||
chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!);
|
||||
}
|
||||
if (chainId == null) {
|
||||
if (chainId == null && isEVMCompatibleChain(walletType)) {
|
||||
chainId = evm?.getChainIdByWalletType(walletType);
|
||||
}
|
||||
|
||||
|
||||
@@ -263,6 +263,7 @@ abstract class OutputBase with Store {
|
||||
void updateWallet(
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> newWallet) {
|
||||
_wallet = newWallet;
|
||||
estimatedFee = Money.zero(cryptoCurrencyHandler());
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -138,9 +138,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
return null;
|
||||
}, (_) async {
|
||||
// When chain changes, update currencies and selected currency
|
||||
final selectionAtChainChange = selectedCryptoCurrency;
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
currencies = wallet.balance.keys.toList();
|
||||
selectedCryptoCurrency = wallet.currency;
|
||||
if (selectedCryptoCurrency == selectionAtChainChange) {
|
||||
selectedCryptoCurrency = wallet.currency;
|
||||
}
|
||||
updateSendingBalance();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,11 +218,13 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
||||
@computed
|
||||
PaymentURI get uri {
|
||||
if (tokenCurrency != null && isEVMCompatibleChain(wallet.type)) {
|
||||
final erc20Token = tokenCurrency! as Erc20Token;
|
||||
return ERC681URI(
|
||||
chainId: wallet.chainId ?? 1,
|
||||
address: wallet.walletAddresses.address,
|
||||
amount: _amount,
|
||||
contractAddress: (tokenCurrency! as Erc20Token).contractAddress,
|
||||
contractAddress: erc20Token.contractAddress,
|
||||
tokenDecimals: erc20Token.decimal,
|
||||
);
|
||||
}
|
||||
if (tokenCurrency is TronToken && wallet.type == WalletType.tron) {
|
||||
|
||||
Reference in New Issue
Block a user