mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-20 06:08:34 +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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user