mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
CW-1277: Expand PayAnything flow to cover Solana and Tron networks (#2805)
* feat: Expand PayAnything flow to cover Solana and Tron networks * fix: refactor utility method to prevent duplicates
This commit is contained in:
@@ -7,7 +7,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dar
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/evm_payment_flow_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/token_selection_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
|
||||
@@ -174,10 +174,27 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
);
|
||||
break;
|
||||
case PaymentFlowType.evmNetworkSelection:
|
||||
await _showEVMPaymentFlow(
|
||||
await _showTokenSelectionFlow(
|
||||
paymentViewModel,
|
||||
walletSwitcherViewModel,
|
||||
paymentRequest,
|
||||
fixedNetwork: result.walletType,
|
||||
);
|
||||
break;
|
||||
case PaymentFlowType.solanaTokenSelection:
|
||||
await _showTokenSelectionFlow(
|
||||
paymentViewModel,
|
||||
walletSwitcherViewModel,
|
||||
paymentRequest,
|
||||
fixedNetwork: WalletType.solana,
|
||||
);
|
||||
break;
|
||||
case PaymentFlowType.tronTokenSelection:
|
||||
await _showTokenSelectionFlow(
|
||||
paymentViewModel,
|
||||
walletSwitcherViewModel,
|
||||
paymentRequest,
|
||||
fixedNetwork: WalletType.tron,
|
||||
);
|
||||
|
||||
break;
|
||||
@@ -238,11 +255,12 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showEVMPaymentFlow(
|
||||
Future<void> _showTokenSelectionFlow(
|
||||
PaymentViewModel paymentViewModel,
|
||||
WalletSwitcherViewModel walletSwitcherViewModel,
|
||||
PaymentRequest paymentRequest,
|
||||
) async {
|
||||
PaymentRequest paymentRequest, {
|
||||
WalletType? fixedNetwork,
|
||||
}) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
@@ -252,9 +270,10 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
isDismissible: true,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return EVMPaymentFlowBottomSheet(
|
||||
return TokenSelectionBottomSheet(
|
||||
paymentViewModel: paymentViewModel,
|
||||
paymentRequest: paymentRequest,
|
||||
fixedNetwork: fixedNetwork,
|
||||
onNext: (PaymentFlowResult newResult) {
|
||||
final selectedChainId = newResult.chainId;
|
||||
final isCompatible = selectedChainId == evm!.getSelectedChainId(sendViewModel.wallet);
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
import 'package:cake_wallet/core/universal_address_detector.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/qr_util.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/utils/token_utilities.dart';
|
||||
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EVMPaymentFlowBottomSheet extends BaseBottomSheet {
|
||||
EVMPaymentFlowBottomSheet({
|
||||
Key? key,
|
||||
required this.paymentViewModel,
|
||||
required this.paymentRequest,
|
||||
required this.onNext,
|
||||
}) : super(
|
||||
titleText: '',
|
||||
footerType: FooterType.none,
|
||||
maxHeight: 900,
|
||||
);
|
||||
|
||||
final PaymentViewModel paymentViewModel;
|
||||
final PaymentRequest paymentRequest;
|
||||
final Function(PaymentFlowResult) onNext;
|
||||
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) {
|
||||
return _EVMPaymentFlowContent(
|
||||
paymentViewModel: paymentViewModel,
|
||||
paymentRequest: paymentRequest,
|
||||
onNext: onNext,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EVMPaymentFlowContent extends StatefulWidget {
|
||||
const _EVMPaymentFlowContent({
|
||||
required this.paymentViewModel,
|
||||
required this.paymentRequest,
|
||||
required this.onNext,
|
||||
});
|
||||
|
||||
final PaymentViewModel paymentViewModel;
|
||||
final PaymentRequest paymentRequest;
|
||||
final Function(PaymentFlowResult) onNext;
|
||||
|
||||
@override
|
||||
State<_EVMPaymentFlowContent> createState() => _EVMPaymentFlowContentState();
|
||||
}
|
||||
|
||||
class _EVMPaymentFlowContentState extends State<_EVMPaymentFlowContent> {
|
||||
int? selectedChainId;
|
||||
CryptoCurrency? selectedToken;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedChainId = widget.paymentViewModel.detectedChainId ?? 1;
|
||||
_autoSelectToken();
|
||||
}
|
||||
|
||||
Future<void> _autoSelectToken() async {
|
||||
if (selectedChainId == null) return;
|
||||
|
||||
try {
|
||||
final tokens = await TokenUtilities.getAvailableTokensForChainId(selectedChainId!);
|
||||
if (tokens.isNotEmpty) {
|
||||
setState(() {
|
||||
selectedToken = tokens.first;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Auto-select token error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: 'assets/images/eth_chain_mono.svg',
|
||||
width: 50,
|
||||
height: 50,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text(
|
||||
'${S.current.ethereum_ecosystem}\n${S.current.address_detected.toLowerCase()}',
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 36),
|
||||
Text(
|
||||
S.current.evm_ecosystem_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
EVMTileWidget(
|
||||
value: selectedChainId != null
|
||||
? _getChainName(selectedChainId!)
|
||||
: S.current.select_network,
|
||||
imagePath: selectedChainId != null ? _getChainImagePath(selectedChainId!) : null,
|
||||
color: selectedChainId != null ? Theme.of(context).colorScheme.primary : null,
|
||||
enabled: true,
|
||||
onTap: () => _showNetworkSelection(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
EVMTileWidget(
|
||||
imagePath: selectedToken != null ? selectedToken!.iconPath : null,
|
||||
value: selectedToken != null ? selectedToken!.title : S.current.select_token,
|
||||
enabled: selectedChainId != null,
|
||||
onTap: () => _showTokenSelection(context),
|
||||
color: selectedToken == null ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
PrimaryButton(
|
||||
text: S.current.restore_next,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: selectedChainId != null && selectedToken != null
|
||||
? () async => await _handleNext(context)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showNetworkSelection(BuildContext context) async {
|
||||
final allChains = evm!.getAllChains();
|
||||
final chainIds = allChains.map((chainInfo) => chainInfo.chainId).toList();
|
||||
|
||||
final selectedIndex = selectedChainId != null ? chainIds.indexOf(selectedChainId!) : 0;
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Picker(
|
||||
items: chainIds,
|
||||
displayItem: (int chainId) => _getChainName(chainId),
|
||||
selectedAtIndex: selectedIndex >= 0 ? selectedIndex : 0,
|
||||
title: S.current.select_network,
|
||||
closeOnItemSelected: true,
|
||||
hasTitleSpacing: true,
|
||||
images: chainIds.map((chainId) {
|
||||
final imagePath = _getChainImagePath(chainId);
|
||||
return imagePath != null
|
||||
? CakeImageWidget(
|
||||
imageUrl: imagePath,
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: Theme.of(context).colorScheme.primary)
|
||||
: const SizedBox(width: 20, height: 20);
|
||||
}).toList(),
|
||||
onItemSelected: (int chainId) {
|
||||
setState(() {
|
||||
selectedChainId = chainId;
|
||||
selectedToken = null;
|
||||
});
|
||||
_autoSelectToken();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _getChainName(int chainId) {
|
||||
final allChains = evm!.getAllChains();
|
||||
final chainInfo = allChains.firstWhere(
|
||||
(chain) => chain.chainId == chainId,
|
||||
orElse: () => ChainInfo(chainId: chainId, name: 'Unknown Network', shortCode: 'unknown'),
|
||||
);
|
||||
return chainInfo.name;
|
||||
}
|
||||
|
||||
String? _getChainImagePath(int chainId) {
|
||||
return getChainMonoImage(WalletType.ethereum, selectedChainId: chainId);
|
||||
}
|
||||
|
||||
void _showTokenSelection(BuildContext context) async {
|
||||
if (selectedChainId == null) return;
|
||||
|
||||
try {
|
||||
final availableTokens = await TokenUtilities.getAvailableTokensForChainId(selectedChainId!);
|
||||
|
||||
if (availableTokens.isEmpty) return;
|
||||
|
||||
final selectedIndex = selectedToken != null ? availableTokens.indexOf(selectedToken!) : 0;
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CurrencyPicker(
|
||||
selectedAtIndex: selectedIndex >= 0 ? selectedIndex : 0,
|
||||
items: availableTokens.cast<Currency>(),
|
||||
hintText: S.current.add_token,
|
||||
onItemSelected: (Currency currency) {
|
||||
setState(() {
|
||||
selectedToken = currency as CryptoCurrency;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
printV('Error showing token selection: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleNext(BuildContext context) async {
|
||||
if (selectedChainId == null || selectedToken == null) return;
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
final allEVMWallets = await widget.paymentViewModel.getEVMCompatibleWallets();
|
||||
|
||||
final walletType = evm!.getWalletTypeByChainId(selectedChainId!) ?? WalletType.ethereum;
|
||||
|
||||
final detectionResult = AddressDetectionResult(
|
||||
address: widget.paymentRequest.address,
|
||||
detectedWalletType: walletType,
|
||||
detectedCurrency: selectedToken!,
|
||||
chainId: selectedChainId,
|
||||
isValid: true,
|
||||
amount: widget.paymentRequest.amount,
|
||||
note: widget.paymentRequest.note,
|
||||
scheme: widget.paymentRequest.scheme,
|
||||
pjUri: widget.paymentRequest.pjUri,
|
||||
callbackUrl: widget.paymentRequest.callbackUrl,
|
||||
callbackMessage: widget.paymentRequest.callbackMessage,
|
||||
);
|
||||
|
||||
final newResult = PaymentFlowResult.evmNetworkSelection(
|
||||
detectionResult,
|
||||
compatibleWallets: allEVMWallets,
|
||||
wallet: allEVMWallets.isNotEmpty ? allEVMWallets.first : null,
|
||||
);
|
||||
|
||||
widget.paymentViewModel.detectedWalletType = walletType;
|
||||
|
||||
widget.onNext(newResult);
|
||||
}
|
||||
}
|
||||
|
||||
class EVMTileWidget extends StatelessWidget {
|
||||
const EVMTileWidget({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.enabled,
|
||||
required this.onTap,
|
||||
this.imagePath,
|
||||
this.color,
|
||||
});
|
||||
|
||||
final String value;
|
||||
final bool enabled;
|
||||
final VoidCallback? onTap;
|
||||
final String? imagePath;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (imagePath != null) ...[
|
||||
CakeImageWidget(
|
||||
imageUrl: imagePath!,
|
||||
color: color,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
size: 24,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,13 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Checks if flow type is token selection (EVM, Solana, or Tron)
|
||||
bool _isTokenSelectionFlow(PaymentFlowType type) {
|
||||
return type == PaymentFlowType.evmNetworkSelection ||
|
||||
type == PaymentFlowType.solanaTokenSelection ||
|
||||
type == PaymentFlowType.tronTokenSelection;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
@@ -127,14 +134,20 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (paymentFlowResult.type == PaymentFlowType.evmNetworkSelection) ...[
|
||||
if (_isTokenSelectionFlow(paymentFlowResult.type)) ...[
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Image.asset(
|
||||
paymentFlowResult.addressDetectionResult!.detectedCurrency!.iconPath!,
|
||||
paymentFlowResult.addressDetectionResult?.detectedCurrency?.iconPath ??
|
||||
'',
|
||||
width: 70,
|
||||
height: 70,
|
||||
errorBuilder: (context, error, stackTrace) => Icon(
|
||||
Icons.currency_bitcoin,
|
||||
size: 70,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
@@ -146,6 +159,11 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
).iconPath!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
errorBuilder: (context, error, stackTrace) => Icon(
|
||||
Icons.account_balance_wallet,
|
||||
size: 32,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
import 'package:cake_wallet/core/universal_address_detector.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/qr_util.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/utils/token_utilities.dart';
|
||||
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
|
||||
import 'package:cake_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TokenSelectionBottomSheet extends BaseBottomSheet {
|
||||
TokenSelectionBottomSheet({
|
||||
Key? key,
|
||||
required this.paymentViewModel,
|
||||
required this.paymentRequest,
|
||||
required this.onNext,
|
||||
this.fixedNetwork,
|
||||
}) : super(
|
||||
titleText: '',
|
||||
footerType: FooterType.none,
|
||||
maxHeight: 900,
|
||||
);
|
||||
|
||||
final PaymentViewModel paymentViewModel;
|
||||
final PaymentRequest paymentRequest;
|
||||
final Function(PaymentFlowResult) onNext;
|
||||
final WalletType? fixedNetwork;
|
||||
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) {
|
||||
return _TokenSelectionContent(
|
||||
paymentViewModel: paymentViewModel,
|
||||
paymentRequest: paymentRequest,
|
||||
onNext: onNext,
|
||||
fixedNetwork: fixedNetwork,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TokenSelectionContent extends StatefulWidget {
|
||||
const _TokenSelectionContent({
|
||||
required this.paymentViewModel,
|
||||
required this.paymentRequest,
|
||||
required this.onNext,
|
||||
this.fixedNetwork,
|
||||
});
|
||||
|
||||
final PaymentViewModel paymentViewModel;
|
||||
final PaymentRequest paymentRequest;
|
||||
final Function(PaymentFlowResult) onNext;
|
||||
final WalletType? fixedNetwork;
|
||||
|
||||
@override
|
||||
State<_TokenSelectionContent> createState() => _TokenSelectionContentState();
|
||||
}
|
||||
|
||||
class _TokenSelectionContentState extends State<_TokenSelectionContent> {
|
||||
WalletType? selectedNetwork;
|
||||
CryptoCurrency? selectedToken;
|
||||
bool isLoadingTokens = false;
|
||||
String? tokenLoadError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedNetwork = widget.fixedNetwork ?? WalletType.ethereum;
|
||||
_autoSelectToken();
|
||||
}
|
||||
|
||||
bool get _isNetworkSelectable => widget.fixedNetwork == null || isEVMCompatibleChain(widget.fixedNetwork!);
|
||||
|
||||
Future<void> _autoSelectToken() async {
|
||||
if (selectedNetwork == null) return;
|
||||
|
||||
setState(() {
|
||||
isLoadingTokens = true;
|
||||
tokenLoadError = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final tokens = await TokenUtilities.getAvailableTokensForNetwork(
|
||||
selectedNetwork!,
|
||||
);
|
||||
|
||||
if (tokens.isEmpty) {
|
||||
setState(() {
|
||||
tokenLoadError = 'No tokens available';
|
||||
isLoadingTokens = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
selectedToken = tokens.first;
|
||||
isLoadingTokens = false;
|
||||
});
|
||||
} catch (e) {
|
||||
printV('Auto-select token error: $e');
|
||||
setState(() {
|
||||
tokenLoadError = 'Failed to load tokens';
|
||||
isLoadingTokens = false;
|
||||
selectedToken = walletTypeToCryptoCurrency(selectedNetwork!);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _getNetworkTitle() {
|
||||
if (selectedNetwork == null) return S.current.select_network;
|
||||
return walletTypeToString(selectedNetwork!);
|
||||
}
|
||||
|
||||
String _getEcosystemTitle() {
|
||||
if (selectedNetwork == WalletType.solana) {
|
||||
return 'Solana\n${S.current.address_detected.toLowerCase()}';
|
||||
}
|
||||
if (selectedNetwork == WalletType.tron) {
|
||||
return 'Tron\n${S.current.address_detected.toLowerCase()}';
|
||||
}
|
||||
return '${S.current.ethereum_ecosystem}\n${S.current.address_detected.toLowerCase()}';
|
||||
}
|
||||
|
||||
String _getEcosystemDescription() {
|
||||
if (selectedNetwork == WalletType.solana) {
|
||||
return 'Select a token to send on Solana network';
|
||||
}
|
||||
if (selectedNetwork == WalletType.tron) {
|
||||
return 'Select a token to send on Tron network';
|
||||
}
|
||||
return S.current.evm_ecosystem_description;
|
||||
}
|
||||
|
||||
String _getNetworkIcon() {
|
||||
if (selectedNetwork == null) return 'assets/images/eth_chain_mono.svg';
|
||||
return getChainMonoImage(selectedNetwork!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: _getNetworkIcon(),
|
||||
width: 50,
|
||||
height: 50,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text(
|
||||
_getEcosystemTitle(),
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 36),
|
||||
Text(
|
||||
_getEcosystemDescription(),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TokenSelectionTileWidget(
|
||||
value: _getNetworkTitle(),
|
||||
imagePath: selectedNetwork != null ? getChainMonoImage(selectedNetwork!) : null,
|
||||
color: selectedNetwork != null ? Theme.of(context).colorScheme.primary : null,
|
||||
enabled: _isNetworkSelectable,
|
||||
onTap: _isNetworkSelectable ? () => _showNetworkSelection(context) : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TokenSelectionTileWidget(
|
||||
imagePath: selectedToken?.iconPath,
|
||||
value: selectedToken != null ? selectedToken!.title : S.current.select_token,
|
||||
enabled: selectedNetwork != null && !isLoadingTokens,
|
||||
onTap: isLoadingTokens ? null : () => _showTokenSelection(context),
|
||||
color: selectedToken == null ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
if (isLoadingTokens) ...[
|
||||
const SizedBox(height: 16),
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
if (tokenLoadError != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
tokenLoadError!,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
PrimaryButton(
|
||||
text: S.current.restore_next,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: selectedNetwork != null && selectedToken != null && !isLoadingTokens
|
||||
? () async => await _handleNext(context)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showNetworkSelection(BuildContext context) async {
|
||||
if (!_isNetworkSelectable) return;
|
||||
|
||||
final evmNetworks =
|
||||
availableWalletTypes.where((walletType) => isEVMCompatibleChain(walletType)).toList();
|
||||
final selectedIndex = evmNetworks.indexOf(selectedNetwork ?? WalletType.ethereum);
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Picker(
|
||||
items: evmNetworks,
|
||||
displayItem: (WalletType network) => walletTypeToString(network),
|
||||
selectedAtIndex: selectedIndex,
|
||||
title: S.current.select_network,
|
||||
closeOnItemSelected: true,
|
||||
hasTitleSpacing: true,
|
||||
images: evmNetworks
|
||||
.map((network) => CakeImageWidget(
|
||||
imageUrl: getChainMonoImage(network),
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: Theme.of(context).colorScheme.primary))
|
||||
.toList(),
|
||||
onItemSelected: (WalletType network) {
|
||||
setState(() {
|
||||
selectedNetwork = network;
|
||||
selectedToken = null;
|
||||
});
|
||||
_autoSelectToken();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showTokenSelection(BuildContext context) async {
|
||||
if (selectedNetwork == null || isLoadingTokens) return;
|
||||
|
||||
setState(() {
|
||||
isLoadingTokens = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final availableTokens = await TokenUtilities.getAvailableTokensForNetwork(
|
||||
selectedNetwork!,
|
||||
);
|
||||
|
||||
if (availableTokens.isEmpty) {
|
||||
setState(() {
|
||||
isLoadingTokens = false;
|
||||
});
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(S.current.error),
|
||||
content: const Text('No tokens available for this network'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(S.current.ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedIndex = selectedToken != null ? availableTokens.indexOf(selectedToken!) : 0;
|
||||
|
||||
setState(() {
|
||||
isLoadingTokens = false;
|
||||
});
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CurrencyPicker(
|
||||
selectedAtIndex: selectedIndex >= 0 ? selectedIndex : 0,
|
||||
items: availableTokens.cast<Currency>(),
|
||||
hintText: S.current.add_token,
|
||||
onItemSelected: (Currency currency) {
|
||||
setState(() {
|
||||
selectedToken = currency as CryptoCurrency;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
printV('Error showing token selection: $e');
|
||||
setState(() {
|
||||
isLoadingTokens = false;
|
||||
tokenLoadError = 'Failed to load tokens';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleNext(BuildContext context) async {
|
||||
if (selectedNetwork == null || selectedToken == null) return;
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
final compatibleWallets = await widget.paymentViewModel.getWalletsByType(selectedNetwork!);
|
||||
|
||||
PaymentFlowResult newResult;
|
||||
|
||||
if (selectedNetwork == WalletType.solana) {
|
||||
newResult = PaymentFlowResult.solanaTokenSelection(
|
||||
AddressDetectionResult(
|
||||
address: widget.paymentRequest.address,
|
||||
detectedWalletType: WalletType.solana,
|
||||
detectedCurrency: selectedToken!,
|
||||
isValid: true,
|
||||
amount: widget.paymentRequest.amount,
|
||||
note: widget.paymentRequest.note,
|
||||
scheme: widget.paymentRequest.scheme,
|
||||
pjUri: widget.paymentRequest.pjUri,
|
||||
callbackUrl: widget.paymentRequest.callbackUrl,
|
||||
callbackMessage: widget.paymentRequest.callbackMessage,
|
||||
),
|
||||
compatibleWallets: compatibleWallets,
|
||||
wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
|
||||
);
|
||||
} else if (selectedNetwork == WalletType.tron) {
|
||||
newResult = PaymentFlowResult.tronTokenSelection(
|
||||
AddressDetectionResult(
|
||||
address: widget.paymentRequest.address,
|
||||
detectedWalletType: WalletType.tron,
|
||||
detectedCurrency: selectedToken!,
|
||||
isValid: true,
|
||||
amount: widget.paymentRequest.amount,
|
||||
note: widget.paymentRequest.note,
|
||||
scheme: widget.paymentRequest.scheme,
|
||||
pjUri: widget.paymentRequest.pjUri,
|
||||
callbackUrl: widget.paymentRequest.callbackUrl,
|
||||
callbackMessage: widget.paymentRequest.callbackMessage,
|
||||
),
|
||||
compatibleWallets: compatibleWallets,
|
||||
wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
|
||||
);
|
||||
} else {
|
||||
newResult = PaymentFlowResult.evmNetworkSelection(
|
||||
AddressDetectionResult(
|
||||
address: widget.paymentRequest.address,
|
||||
detectedWalletType: selectedNetwork!,
|
||||
detectedCurrency: selectedToken!,
|
||||
isValid: true,
|
||||
amount: widget.paymentRequest.amount,
|
||||
note: widget.paymentRequest.note,
|
||||
scheme: widget.paymentRequest.scheme,
|
||||
pjUri: widget.paymentRequest.pjUri,
|
||||
callbackUrl: widget.paymentRequest.callbackUrl,
|
||||
callbackMessage: widget.paymentRequest.callbackMessage,
|
||||
),
|
||||
compatibleWallets: compatibleWallets,
|
||||
wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
|
||||
);
|
||||
}
|
||||
|
||||
widget.paymentViewModel.detectedWalletType = selectedNetwork!;
|
||||
widget.onNext(newResult);
|
||||
}
|
||||
}
|
||||
|
||||
class TokenSelectionTileWidget extends StatelessWidget {
|
||||
const TokenSelectionTileWidget({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.enabled,
|
||||
required this.onTap,
|
||||
this.imagePath,
|
||||
this.color,
|
||||
});
|
||||
|
||||
final String value;
|
||||
final bool enabled;
|
||||
final VoidCallback? onTap;
|
||||
final String? imagePath;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Opacity(
|
||||
opacity: enabled ? 1.0 : 0.6,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (imagePath != null) ...[
|
||||
CakeImageWidget(
|
||||
imageUrl: imagePath!,
|
||||
color: color,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (enabled)
|
||||
Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
size: 24,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+121
-81
@@ -22,18 +22,13 @@ class TokenUtilities {
|
||||
final unique = <Erc20Token>[];
|
||||
|
||||
for (final wallet in evmWallets) {
|
||||
final allChains = evm!.getAllChains();
|
||||
final chain = getTokenNameBasedOnWalletType(wallet.type);
|
||||
final box = await _openEvmTokensBoxFor(wallet);
|
||||
|
||||
for (final chainInfo in allChains) {
|
||||
final chainId = chainInfo.chainId;
|
||||
final chain = getTokenNameBasedOnWalletType(wallet.type, chainId: chainId);
|
||||
final box = await _openEvmTokensBoxFor(wallet, chainId);
|
||||
|
||||
for (final t in box.values.where((t) => t.enabled)) {
|
||||
final key = '$chain|${t.contractAddress.toLowerCase()}';
|
||||
if (seen.add(key)) {
|
||||
unique.add(t);
|
||||
}
|
||||
for (final t in box.values.where((t) => t.enabled)) {
|
||||
final key = '$chain|${t.contractAddress.toLowerCase()}';
|
||||
if (seen.add(key)) {
|
||||
unique.add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +88,7 @@ class TokenUtilities {
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.base:
|
||||
case WalletType.arbitrum:
|
||||
final tokens = await loadAllUniqueEvmTokens();
|
||||
for (final t in tokens) {
|
||||
if (t.contractAddress.toLowerCase() == lower) return t;
|
||||
@@ -115,9 +111,9 @@ class TokenUtilities {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo, int chainId) async {
|
||||
static Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo) async {
|
||||
final walletKey = walletInfo.name.replaceAll(' ', '_');
|
||||
final boxName = _getErc20TokensBoxName(walletKey, chainId);
|
||||
final boxName = _getErc20TokensBoxName(walletKey, walletInfo.type);
|
||||
|
||||
if (CakeHive.isBoxOpen(boxName)) {
|
||||
return CakeHive.box<Erc20Token>(boxName);
|
||||
@@ -125,13 +121,13 @@ class TokenUtilities {
|
||||
return CakeHive.openBox<Erc20Token>(boxName);
|
||||
}
|
||||
|
||||
static String _getErc20TokensBoxName(String sanitizedName, int chainId) {
|
||||
return switch (chainId) {
|
||||
1 => "${sanitizedName}_${Erc20Token.ethereumBoxName}",
|
||||
137 => "${sanitizedName}_${Erc20Token.polygonBoxName}",
|
||||
8453 => "${sanitizedName}_${Erc20Token.baseBoxName}",
|
||||
42161 => "${sanitizedName}_${Erc20Token.arbitrumBoxName}",
|
||||
_ => "${sanitizedName}_${Erc20Token.ethereumBoxName}",
|
||||
static String _getErc20TokensBoxName(String walletKey, WalletType walletType) {
|
||||
return switch (walletType) {
|
||||
WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}',
|
||||
WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}',
|
||||
WalletType.base => '${walletKey}_${Erc20Token.baseBoxName}',
|
||||
WalletType.arbitrum => '${walletKey}_${Erc20Token.arbitrumBoxName}',
|
||||
_ => '${walletKey}_${Erc20Token.ethereumBoxName}',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -172,6 +168,8 @@ class TokenUtilities {
|
||||
title == 'ethereum' ||
|
||||
title == 'matic' ||
|
||||
title == 'polygon' ||
|
||||
title == 'base' ||
|
||||
title == 'arbitrum' ||
|
||||
title == 'bnb' ||
|
||||
title == 'bsc' ||
|
||||
title == 'avax' ||
|
||||
@@ -186,14 +184,8 @@ class TokenUtilities {
|
||||
final title = currency.title.toLowerCase();
|
||||
|
||||
// Only check EVM registry for currencies that might be EVM-related
|
||||
final isPotentialEVM = title == 'eth' ||
|
||||
title == 'ethereum' ||
|
||||
title == 'polygon' ||
|
||||
title == 'matic' ||
|
||||
title == 'base' ||
|
||||
title == 'arbitrum' ||
|
||||
(tag != null && (tag == 'ETH' || tag == 'POL' || tag == 'BASE' || tag == 'ARB')) ||
|
||||
isNativeToken(currency);
|
||||
final isPotentialEVM = isNativeToken(currency) ||
|
||||
(tag != null && (tag == 'ETH' || tag == 'POL' || tag == 'BASE' || tag == 'ARB'));
|
||||
|
||||
if (isPotentialEVM) {
|
||||
// Try by tag first if available (e.g., 'POL', 'BASE', 'ARB')
|
||||
@@ -232,56 +224,90 @@ class TokenUtilities {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static bool _shouldAddToken(
|
||||
List<CryptoCurrency> existingTokens,
|
||||
CryptoCurrency token,
|
||||
Set<String> addedAddresses,
|
||||
) {
|
||||
if (token is Erc20Token) {
|
||||
final address = token.contractAddress.toLowerCase();
|
||||
if (addedAddresses.contains(address)) {
|
||||
return false;
|
||||
}
|
||||
if (existingTokens.any((existing) => _matchesCurrency(existing, token))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return !existingTokens.any((existing) => _matchesCurrency(existing, token));
|
||||
}
|
||||
|
||||
static bool _matchesCurrency(CryptoCurrency a, CryptoCurrency b) {
|
||||
return a.title.toUpperCase() == b.title.toUpperCase() &&
|
||||
(a.tag?.toUpperCase() == b.tag?.toUpperCase());
|
||||
}
|
||||
|
||||
static Future<List<CryptoCurrency>> getAvailableTokensForChainId(int chainId) async {
|
||||
// Get native currency for the chain
|
||||
final baseCurrency = getCryptoCurrencyByChainId(chainId);
|
||||
final allTokens = <CryptoCurrency>[];
|
||||
static Future<List<CryptoCurrency>> getAvailableTokensForNetwork(
|
||||
WalletType network,
|
||||
) async {
|
||||
final baseCurrency = walletTypeToCryptoCurrency(network);
|
||||
final allTokens = <CryptoCurrency>[baseCurrency];
|
||||
final addedAddresses = <String>{};
|
||||
|
||||
allTokens.add(baseCurrency);
|
||||
// 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) {
|
||||
final address = token.contractAddress.toLowerCase();
|
||||
if (addedAddresses.add(address)) {
|
||||
allTokens.add(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add currencies that match this chain
|
||||
for (final currency in CryptoCurrency.all) {
|
||||
final matches = (baseCurrency.tag == null && baseCurrency.title == currency.tag) ||
|
||||
(baseCurrency.tag != null &&
|
||||
currency.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase());
|
||||
// Then 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) ||
|
||||
(baseCurrency.tag != null &&
|
||||
currency.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase());
|
||||
|
||||
if (matches && _shouldAddToken(allTokens, currency, addedAddresses)) {
|
||||
allTokens.add(currency);
|
||||
if (matches) {
|
||||
if (currency is Erc20Token) {
|
||||
final address = currency.contractAddress.toLowerCase();
|
||||
if (addedAddresses.add(address)) {
|
||||
allTokens.add(currency);
|
||||
}
|
||||
} else if (!allTokens.any((t) => _matchesCurrency(t, currency))) {
|
||||
allTokens.add(currency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add user tokens for this chain
|
||||
final userTokens = await _getUserTokensForChainId(chainId);
|
||||
for (final token in userTokens) {
|
||||
if (_shouldAddToken(allTokens, token, addedAddresses)) {
|
||||
allTokens.add(token);
|
||||
if (token is Erc20Token) {
|
||||
addedAddresses.add(token.contractAddress.toLowerCase());
|
||||
// Handle Solana network
|
||||
else if (network == WalletType.solana) {
|
||||
final userSolTokens = await loadAllUniqueSolTokens();
|
||||
for (final token in userSolTokens) {
|
||||
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) {
|
||||
final mintAddress = currency.mintAddress.toLowerCase();
|
||||
if (addedAddresses.add(mintAddress)) {
|
||||
allTokens.add(currency);
|
||||
}
|
||||
} else if (!allTokens.any((t) => _matchesCurrency(t, currency))) {
|
||||
allTokens.add(currency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Tron network
|
||||
else if (network == WalletType.tron) {
|
||||
final userTronTokens = await loadAllUniqueTronTokens();
|
||||
for (final token in userTronTokens) {
|
||||
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) {
|
||||
final contractAddress = currency.contractAddress.toLowerCase();
|
||||
if (addedAddresses.add(contractAddress)) {
|
||||
allTokens.add(currency);
|
||||
}
|
||||
} else if (!allTokens.any((t) => _matchesCurrency(t, currency))) {
|
||||
allTokens.add(currency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,19 +315,33 @@ class TokenUtilities {
|
||||
return allTokens;
|
||||
}
|
||||
|
||||
static Future<List<CryptoCurrency>> _getUserTokensForChainId(int chainId) async {
|
||||
final allWi = await WalletInfo.getAll();
|
||||
final evmWallets = allWi.where((w) => isEVMCompatibleChain(w.type));
|
||||
static bool _matchesCurrency(CryptoCurrency a, CryptoCurrency b) {
|
||||
return a.title.toUpperCase() == b.title.toUpperCase() &&
|
||||
(a.tag?.toUpperCase() == b.tag?.toUpperCase());
|
||||
}
|
||||
|
||||
final tokens = <Erc20Token>[];
|
||||
for (final wallet in evmWallets) {
|
||||
final box = await _openEvmTokensBoxFor(wallet, chainId);
|
||||
static Future<List<CryptoCurrency>> _getUserTokensForNetwork(CryptoCurrency baseCurrency) async {
|
||||
final walletType = cryptoCurrencyToWalletType(baseCurrency);
|
||||
if (walletType == null) return [];
|
||||
|
||||
for (final t in box.values.where((t) => t.enabled)) {
|
||||
tokens.add(t);
|
||||
}
|
||||
if (isEVMCompatibleChain(walletType)) {
|
||||
final tokens = await TokenUtilities.loadAllUniqueEvmTokens();
|
||||
|
||||
return tokens.where((token) {
|
||||
if (baseCurrency.tag == null) return token.tag == baseCurrency.title;
|
||||
|
||||
return token.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase();
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return tokens.cast<CryptoCurrency>();
|
||||
if (walletType == WalletType.solana) {
|
||||
return await loadAllUniqueSolTokens();
|
||||
}
|
||||
|
||||
if (walletType == WalletType.tron) {
|
||||
return await loadAllUniqueTronTokens();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,70 +76,28 @@ abstract class PaymentViewModelBase with Store {
|
||||
|
||||
final currentWallet = appStore.wallet;
|
||||
|
||||
if (isEVMCompatibleChain(detectedWalletType!)) {
|
||||
final isRawEvmInput = !addressData.contains(':') && _isEVMAddress(detectionResult.address);
|
||||
|
||||
// Check if the current wallet is also EVM
|
||||
if (currentWallet != null && isEVMCompatibleChain(currentWallet.type)) {
|
||||
final currentChainId = evm!.getSelectedChainId(currentWallet);
|
||||
final detectedChainIdValue = this.detectedChainId;
|
||||
|
||||
if (detectedChainIdValue != null && currentChainId != null) {
|
||||
// For raw EVM address input that only defaulted to chainId 1,
|
||||
// always force the EVM ecosystem bottom sheet so the user
|
||||
// can pick the actual network and token.
|
||||
if (isRawEvmInput && detectedChainIdValue == 1) {
|
||||
final allEVMWallets = await getEVMCompatibleWallets();
|
||||
|
||||
final currentWalletInfo = currentWallet.walletInfo;
|
||||
|
||||
final otherEVMWallets =
|
||||
allEVMWallets.where((w) => w.name != currentWallet.name).toList();
|
||||
|
||||
return PaymentFlowResult.evmNetworkSelection(
|
||||
detectionResult,
|
||||
compatibleWallets: otherEVMWallets,
|
||||
wallet: currentWalletInfo,
|
||||
);
|
||||
}
|
||||
|
||||
if (detectedChainIdValue == currentChainId) {
|
||||
return PaymentFlowResult.currentWalletCompatible();
|
||||
}
|
||||
|
||||
final allEVMWallets = await getEVMCompatibleWallets();
|
||||
|
||||
final currentWalletInfo = currentWallet.walletInfo;
|
||||
|
||||
final otherEVMWallets =
|
||||
allEVMWallets.where((w) => w.name != currentWallet.name).toList();
|
||||
|
||||
return PaymentFlowResult.evmNetworkSelection(
|
||||
detectionResult,
|
||||
compatibleWallets: otherEVMWallets,
|
||||
wallet: currentWalletInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If the current wallet is not EVM or the chainId comparison failed
|
||||
// We proceed with other checks
|
||||
if (!addressData.contains(':') && _isEVMAddress(detectionResult.address)) {
|
||||
final allEVMWallets = await getEVMCompatibleWallets();
|
||||
return PaymentFlowResult.evmNetworkSelection(
|
||||
detectionResult,
|
||||
compatibleWallets: allEVMWallets,
|
||||
);
|
||||
}
|
||||
|
||||
// For EVM URIs, show network selection
|
||||
final allEVMWallets = await getEVMCompatibleWallets();
|
||||
return PaymentFlowResult.evmNetworkSelection(
|
||||
if (detectedWalletType == WalletType.solana) {
|
||||
final compatibleWallets = await getWalletsByType(WalletType.solana);
|
||||
return PaymentFlowResult.solanaTokenSelection(
|
||||
detectionResult,
|
||||
compatibleWallets: allEVMWallets,
|
||||
compatibleWallets: compatibleWallets,
|
||||
wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
|
||||
);
|
||||
}
|
||||
|
||||
if (detectedWalletType == WalletType.tron) {
|
||||
final compatibleWallets = await getWalletsByType(WalletType.tron);
|
||||
return PaymentFlowResult.tronTokenSelection(
|
||||
detectionResult,
|
||||
compatibleWallets: compatibleWallets,
|
||||
wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
|
||||
);
|
||||
}
|
||||
|
||||
if (isEVMCompatibleChain(detectedWalletType!)) {
|
||||
return PaymentFlowResult.evmNetworkSelection(detectionResult);
|
||||
}
|
||||
|
||||
if (currentWallet != null && currentWallet.type == detectedWalletType) {
|
||||
return PaymentFlowResult.currentWalletCompatible();
|
||||
}
|
||||
@@ -172,10 +130,6 @@ abstract class PaymentViewModelBase with Store {
|
||||
await evm!.selectChain(appStore.wallet!, detectedChainId!, node: node);
|
||||
}
|
||||
|
||||
bool _isEVMAddress(String address) {
|
||||
return RegExp(r'^0x[a-fA-F0-9]{40}$').hasMatch(address);
|
||||
}
|
||||
|
||||
Future<List<WalletInfo>> getWalletsByType(WalletType walletType) async {
|
||||
return (await WalletInfo.getAll()).where((wallet) => wallet.type == walletType).toList();
|
||||
}
|
||||
@@ -230,6 +184,34 @@ class PaymentFlowResult {
|
||||
);
|
||||
}
|
||||
|
||||
/// Solana address detected - needs token selection
|
||||
factory PaymentFlowResult.solanaTokenSelection(
|
||||
AddressDetectionResult addressDetectionResult, {
|
||||
List<WalletInfo>? compatibleWallets,
|
||||
WalletInfo? wallet,
|
||||
}) =>
|
||||
PaymentFlowResult._(
|
||||
type: PaymentFlowType.solanaTokenSelection,
|
||||
addressDetectionResult: addressDetectionResult,
|
||||
walletType: WalletType.solana,
|
||||
wallets: compatibleWallets ?? [],
|
||||
wallet: wallet,
|
||||
);
|
||||
|
||||
/// Tron address detected - needs token selection
|
||||
factory PaymentFlowResult.tronTokenSelection(
|
||||
AddressDetectionResult addressDetectionResult, {
|
||||
List<WalletInfo>? compatibleWallets,
|
||||
WalletInfo? wallet,
|
||||
}) =>
|
||||
PaymentFlowResult._(
|
||||
type: PaymentFlowType.tronTokenSelection,
|
||||
addressDetectionResult: addressDetectionResult,
|
||||
walletType: WalletType.tron,
|
||||
wallets: compatibleWallets ?? [],
|
||||
wallet: wallet,
|
||||
);
|
||||
|
||||
/// Current wallet is compatible
|
||||
factory PaymentFlowResult.currentWalletCompatible() =>
|
||||
PaymentFlowResult._(type: PaymentFlowType.currentWalletCompatible);
|
||||
@@ -308,7 +290,9 @@ class PaymentFlowResult {
|
||||
PaymentFlowResult._(type: PaymentFlowType.incompatible, message: message);
|
||||
|
||||
CryptoCurrency? get detectedCurrency {
|
||||
if (type == PaymentFlowType.evmNetworkSelection) {
|
||||
if (type == PaymentFlowType.evmNetworkSelection ||
|
||||
type == PaymentFlowType.solanaTokenSelection ||
|
||||
type == PaymentFlowType.tronTokenSelection) {
|
||||
return addressDetectionResult?.detectedCurrency;
|
||||
}
|
||||
if (walletType != null) {
|
||||
@@ -328,6 +312,8 @@ enum PaymentFlowType {
|
||||
multipleWallets,
|
||||
noWallets,
|
||||
evmNetworkSelection,
|
||||
solanaTokenSelection,
|
||||
tronTokenSelection,
|
||||
error,
|
||||
incompatible,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user