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

David Adegoke committed Jan 18, 2026 at 14:34 UTC 5bb779d115cd3c85333c435a15f0cca937efa8c3
6 files changed +671 -483
lib/src/screens/send/widgets/send_card.dart
+25 -6
@@ -7,7 +7,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dar
7 import 'package:cake_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart';
8 import 'package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart';
9 import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart';
10 -import 'package:cake_wallet/src/widgets/bottom_sheet/evm_payment_flow_bottom_sheet.dart';
10 +import 'package:cake_wallet/src/widgets/bottom_sheet/token_selection_bottom_sheet.dart';
11 import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
12 import 'package:cake_wallet/src/widgets/picker.dart';
13 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
@@ -174,10 +174,27 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
174 );
175 break;
176 case PaymentFlowType.evmNetworkSelection:
177 - await _showEVMPaymentFlow(
177 + await _showTokenSelectionFlow(
178 paymentViewModel,
179 walletSwitcherViewModel,
180 paymentRequest,
181 + fixedNetwork: result.walletType,
182 + );
183 + break;
184 + case PaymentFlowType.solanaTokenSelection:
185 + await _showTokenSelectionFlow(
186 + paymentViewModel,
187 + walletSwitcherViewModel,
188 + paymentRequest,
189 + fixedNetwork: WalletType.solana,
190 + );
191 + break;
192 + case PaymentFlowType.tronTokenSelection:
193 + await _showTokenSelectionFlow(
194 + paymentViewModel,
195 + walletSwitcherViewModel,
196 + paymentRequest,
197 + fixedNetwork: WalletType.tron,
198 );
199
200 break;
@@ -238,11 +255,12 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
255 );
256 }
257
241 - Future<void> _showEVMPaymentFlow(
258 + Future<void> _showTokenSelectionFlow(
259 PaymentViewModel paymentViewModel,
260 WalletSwitcherViewModel walletSwitcherViewModel,
244 - PaymentRequest paymentRequest,
245 - ) async {
261 + PaymentRequest paymentRequest, {
262 + WalletType? fixedNetwork,
263 + }) async {
264 if (!context.mounted) {
265 return;
266 }
@@ -252,9 +270,10 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
270 isDismissible: true,
271 isScrollControlled: true,
272 builder: (BuildContext context) {
255 - return EVMPaymentFlowBottomSheet(
273 + return TokenSelectionBottomSheet(
274 paymentViewModel: paymentViewModel,
275 paymentRequest: paymentRequest,
276 + fixedNetwork: fixedNetwork,
277 onNext: (PaymentFlowResult newResult) {
278 final selectedChainId = newResult.chainId;
279 final isCompatible = selectedChainId == evm!.getSelectedChainId(sendViewModel.wallet);
lib/src/widgets/bottom_sheet/evm_payment_flow_bottom_sheet.dart deleted
-331
@@ -1,331 +0,0 @@
1 -import 'package:cake_wallet/core/universal_address_detector.dart';
2 -import 'package:cake_wallet/evm/evm.dart';
3 -import 'package:cake_wallet/generated/i18n.dart';
4 -import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
5 -import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
6 -import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
7 -import 'package:cake_wallet/src/widgets/picker.dart';
8 -import 'package:cake_wallet/src/widgets/primary_button.dart';
9 -import 'package:cake_wallet/utils/payment_request.dart';
10 -import 'package:cake_wallet/utils/qr_util.dart';
11 -import 'package:cake_wallet/utils/show_pop_up.dart';
12 -import 'package:cake_wallet/utils/token_utilities.dart';
13 -import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
14 -import 'package:cw_core/crypto_currency.dart';
15 -import 'package:cw_core/currency.dart';
16 -import 'package:cw_core/utils/print_verbose.dart';
17 -import 'package:cw_core/wallet_type.dart';
18 -import 'package:flutter/material.dart';
19 -
20 -class EVMPaymentFlowBottomSheet extends BaseBottomSheet {
21 - EVMPaymentFlowBottomSheet({
22 - Key? key,
23 - required this.paymentViewModel,
24 - required this.paymentRequest,
25 - required this.onNext,
26 - }) : super(
27 - titleText: '',
28 - footerType: FooterType.none,
29 - maxHeight: 900,
30 - );
31 -
32 - final PaymentViewModel paymentViewModel;
33 - final PaymentRequest paymentRequest;
34 - final Function(PaymentFlowResult) onNext;
35 -
36 - @override
37 - Widget contentWidget(BuildContext context) {
38 - return _EVMPaymentFlowContent(
39 - paymentViewModel: paymentViewModel,
40 - paymentRequest: paymentRequest,
41 - onNext: onNext,
42 - );
43 - }
44 -}
45 -
46 -class _EVMPaymentFlowContent extends StatefulWidget {
47 - const _EVMPaymentFlowContent({
48 - required this.paymentViewModel,
49 - required this.paymentRequest,
50 - required this.onNext,
51 - });
52 -
53 - final PaymentViewModel paymentViewModel;
54 - final PaymentRequest paymentRequest;
55 - final Function(PaymentFlowResult) onNext;
56 -
57 - @override
58 - State<_EVMPaymentFlowContent> createState() => _EVMPaymentFlowContentState();
59 -}
60 -
61 -class _EVMPaymentFlowContentState extends State<_EVMPaymentFlowContent> {
62 - int? selectedChainId;
63 - CryptoCurrency? selectedToken;
64 -
65 - @override
66 - void initState() {
67 - super.initState();
68 - selectedChainId = widget.paymentViewModel.detectedChainId ?? 1;
69 - _autoSelectToken();
70 - }
71 -
72 - Future<void> _autoSelectToken() async {
73 - if (selectedChainId == null) return;
74 -
75 - try {
76 - final tokens = await TokenUtilities.getAvailableTokensForChainId(selectedChainId!);
77 - if (tokens.isNotEmpty) {
78 - setState(() {
79 - selectedToken = tokens.first;
80 - });
81 - }
82 - } catch (e) {
83 - printV('Auto-select token error: $e');
84 - }
85 - }
86 -
87 - @override
88 - Widget build(BuildContext context) {
89 - return Container(
90 - padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
91 - child: Column(
92 - mainAxisSize: MainAxisSize.min,
93 - children: [
94 - CakeImageWidget(
95 - imageUrl: 'assets/images/eth_chain_mono.svg',
96 - width: 50,
97 - height: 50,
98 - color: Theme.of(context).colorScheme.onSurface,
99 - ),
100 - const SizedBox(height: 24),
101 - Padding(
102 - padding: const EdgeInsets.symmetric(horizontal: 24),
103 - child: Text(
104 - '${S.current.ethereum_ecosystem}\n${S.current.address_detected.toLowerCase()}',
105 - style: Theme.of(context).textTheme.titleMedium!.copyWith(
106 - fontSize: 20,
107 - fontWeight: FontWeight.w900,
108 - color: Theme.of(context).colorScheme.onSurface,
109 - ),
110 - textAlign: TextAlign.center,
111 - ),
112 - ),
113 - const SizedBox(height: 36),
114 - Text(
115 - S.current.evm_ecosystem_description,
116 - textAlign: TextAlign.center,
117 - style: Theme.of(context).textTheme.bodyMedium!.copyWith(
118 - fontSize: 16,
119 - fontWeight: FontWeight.w600,
120 - color: Theme.of(context).colorScheme.onSurfaceVariant,
121 - letterSpacing: 0.0,
122 - ),
123 - ),
124 - const SizedBox(height: 32),
125 - EVMTileWidget(
126 - value: selectedChainId != null
127 - ? _getChainName(selectedChainId!)
128 - : S.current.select_network,
129 - imagePath: selectedChainId != null ? _getChainImagePath(selectedChainId!) : null,
130 - color: selectedChainId != null ? Theme.of(context).colorScheme.primary : null,
131 - enabled: true,
132 - onTap: () => _showNetworkSelection(context),
133 - ),
134 - const SizedBox(height: 16),
135 - EVMTileWidget(
136 - imagePath: selectedToken != null ? selectedToken!.iconPath : null,
137 - value: selectedToken != null ? selectedToken!.title : S.current.select_token,
138 - enabled: selectedChainId != null,
139 - onTap: () => _showTokenSelection(context),
140 - color: selectedToken == null ? Theme.of(context).colorScheme.primary : null,
141 - ),
142 - const SizedBox(height: 32),
143 - PrimaryButton(
144 - text: S.current.restore_next,
145 - color: Theme.of(context).colorScheme.primary,
146 - textColor: Theme.of(context).colorScheme.onPrimary,
147 - onPressed: selectedChainId != null && selectedToken != null
148 - ? () async => await _handleNext(context)
149 - : null,
150 - ),
151 - const SizedBox(height: 48),
152 - ],
153 - ),
154 - );
155 - }
156 -
157 - void _showNetworkSelection(BuildContext context) async {
158 - final allChains = evm!.getAllChains();
159 - final chainIds = allChains.map((chainInfo) => chainInfo.chainId).toList();
160 -
161 - final selectedIndex = selectedChainId != null ? chainIds.indexOf(selectedChainId!) : 0;
162 -
163 - await showPopUp<void>(
164 - context: context,
165 - builder: (BuildContext context) {
166 - return Picker(
167 - items: chainIds,
168 - displayItem: (int chainId) => _getChainName(chainId),
169 - selectedAtIndex: selectedIndex >= 0 ? selectedIndex : 0,
170 - title: S.current.select_network,
171 - closeOnItemSelected: true,
172 - hasTitleSpacing: true,
173 - images: chainIds.map((chainId) {
174 - final imagePath = _getChainImagePath(chainId);
175 - return imagePath != null
176 - ? CakeImageWidget(
177 - imageUrl: imagePath,
178 - width: 20,
179 - height: 20,
180 - color: Theme.of(context).colorScheme.primary)
181 - : const SizedBox(width: 20, height: 20);
182 - }).toList(),
183 - onItemSelected: (int chainId) {
184 - setState(() {
185 - selectedChainId = chainId;
186 - selectedToken = null;
187 - });
188 - _autoSelectToken();
189 - },
190 - );
191 - },
192 - );
193 - }
194 -
195 - String _getChainName(int chainId) {
196 - final allChains = evm!.getAllChains();
197 - final chainInfo = allChains.firstWhere(
198 - (chain) => chain.chainId == chainId,
199 - orElse: () => ChainInfo(chainId: chainId, name: 'Unknown Network', shortCode: 'unknown'),
200 - );
201 - return chainInfo.name;
202 - }
203 -
204 - String? _getChainImagePath(int chainId) {
205 - return getChainMonoImage(WalletType.ethereum, selectedChainId: chainId);
206 - }
207 -
208 - void _showTokenSelection(BuildContext context) async {
209 - if (selectedChainId == null) return;
210 -
211 - try {
212 - final availableTokens = await TokenUtilities.getAvailableTokensForChainId(selectedChainId!);
213 -
214 - if (availableTokens.isEmpty) return;
215 -
216 - final selectedIndex = selectedToken != null ? availableTokens.indexOf(selectedToken!) : 0;
217 -
218 - await showPopUp<void>(
219 - context: context,
220 - builder: (BuildContext context) {
221 - return CurrencyPicker(
222 - selectedAtIndex: selectedIndex >= 0 ? selectedIndex : 0,
223 - items: availableTokens.cast<Currency>(),
224 - hintText: S.current.add_token,
225 - onItemSelected: (Currency currency) {
226 - setState(() {
227 - selectedToken = currency as CryptoCurrency;
228 - });
229 - },
230 - );
231 - },
232 - );
233 - } catch (e) {
234 - printV('Error showing token selection: $e');
235 - }
236 - }
237 -
238 - Future<void> _handleNext(BuildContext context) async {
239 - if (selectedChainId == null || selectedToken == null) return;
240 -
241 - Navigator.of(context).pop();
242 -
243 - final allEVMWallets = await widget.paymentViewModel.getEVMCompatibleWallets();
244 -
245 - final walletType = evm!.getWalletTypeByChainId(selectedChainId!) ?? WalletType.ethereum;
246 -
247 - final detectionResult = AddressDetectionResult(
248 - address: widget.paymentRequest.address,
249 - detectedWalletType: walletType,
250 - detectedCurrency: selectedToken!,
251 - chainId: selectedChainId,
252 - isValid: true,
253 - amount: widget.paymentRequest.amount,
254 - note: widget.paymentRequest.note,
255 - scheme: widget.paymentRequest.scheme,
256 - pjUri: widget.paymentRequest.pjUri,
257 - callbackUrl: widget.paymentRequest.callbackUrl,
258 - callbackMessage: widget.paymentRequest.callbackMessage,
259 - );
260 -
261 - final newResult = PaymentFlowResult.evmNetworkSelection(
262 - detectionResult,
263 - compatibleWallets: allEVMWallets,
264 - wallet: allEVMWallets.isNotEmpty ? allEVMWallets.first : null,
265 - );
266 -
267 - widget.paymentViewModel.detectedWalletType = walletType;
268 -
269 - widget.onNext(newResult);
270 - }
271 -}
272 -
273 -class EVMTileWidget extends StatelessWidget {
274 - const EVMTileWidget({
275 - super.key,
276 - required this.value,
277 - required this.enabled,
278 - required this.onTap,
279 - this.imagePath,
280 - this.color,
281 - });
282 -
283 - final String value;
284 - final bool enabled;
285 - final VoidCallback? onTap;
286 - final String? imagePath;
287 - final Color? color;
288 -
289 - @override
290 - Widget build(BuildContext context) {
291 - return InkWell(
292 - onTap: enabled ? onTap : null,
293 - borderRadius: BorderRadius.circular(12),
294 - child: Container(
295 - padding: const EdgeInsets.all(16),
296 - decoration: BoxDecoration(
297 - borderRadius: BorderRadius.circular(12),
298 - color: Theme.of(context).colorScheme.surfaceContainer,
299 - ),
300 - child: Row(
301 - children: [
302 - if (imagePath != null) ...[
303 - CakeImageWidget(
304 - imageUrl: imagePath!,
305 - color: color,
306 - width: 20,
307 - height: 20,
308 - ),
309 - const SizedBox(width: 16),
310 - ],
311 - Expanded(
312 - child: Text(
313 - value,
314 - style: Theme.of(context).textTheme.titleMedium!.copyWith(
315 - fontSize: 14,
316 - fontWeight: FontWeight.w400,
317 - color: Theme.of(context).colorScheme.onSurface,
318 - ),
319 - ),
320 - ),
321 - Icon(
322 - Icons.keyboard_arrow_down,
323 - size: 24,
324 - color: Theme.of(context).colorScheme.primary,
325 - ),
326 - ],
327 - ),
328 - ),
329 - );
330 - }
331 -}
lib/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart
+20 -2
@@ -89,6 +89,13 @@ class _PaymentConfirmationContent extends StatelessWidget {
89 return false;
90 }
91
92 + /// Checks if flow type is token selection (EVM, Solana, or Tron)
93 + bool _isTokenSelectionFlow(PaymentFlowType type) {
94 + return type == PaymentFlowType.evmNetworkSelection ||
95 + type == PaymentFlowType.solanaTokenSelection ||
96 + type == PaymentFlowType.tronTokenSelection;
97 + }
98 +
99 @override
100 Widget build(BuildContext context) {
101 return Observer(
@@ -127,14 +134,20 @@ class _PaymentConfirmationContent extends StatelessWidget {
134 mainAxisSize: MainAxisSize.min,
135 crossAxisAlignment: CrossAxisAlignment.center,
136 children: [
130 - if (paymentFlowResult.type == PaymentFlowType.evmNetworkSelection) ...[
137 + if (_isTokenSelectionFlow(paymentFlowResult.type)) ...[
138 Stack(
139 clipBehavior: Clip.none,
140 children: [
141 Image.asset(
135 - paymentFlowResult.addressDetectionResult!.detectedCurrency!.iconPath!,
142 + paymentFlowResult.addressDetectionResult?.detectedCurrency?.iconPath ??
143 + '',
144 width: 70,
145 height: 70,
146 + errorBuilder: (context, error, stackTrace) => Icon(
147 + Icons.currency_bitcoin,
148 + size: 70,
149 + color: Theme.of(context).colorScheme.onSurface,
150 + ),
151 ),
152 Positioned(
153 bottom: 0,
@@ -146,6 +159,11 @@ class _PaymentConfirmationContent extends StatelessWidget {
159 ).iconPath!,
160 width: 32,
161 height: 32,
162 + errorBuilder: (context, error, stackTrace) => Icon(
163 + Icons.account_balance_wallet,
164 + size: 32,
165 + color: Theme.of(context).colorScheme.onSurface,
166 + ),
167 ),
168 ),
169 ],
lib/src/widgets/bottom_sheet/token_selection_bottom_sheet.dart new
+456
@@ -0,0 +1,456 @@
1 +import 'package:cake_wallet/core/universal_address_detector.dart';
2 +import 'package:cake_wallet/generated/i18n.dart';
3 +import 'package:cake_wallet/reactions/wallet_connect.dart';
4 +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
5 +import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
6 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
7 +import 'package:cake_wallet/src/widgets/picker.dart';
8 +import 'package:cake_wallet/src/widgets/primary_button.dart';
9 +import 'package:cake_wallet/utils/payment_request.dart';
10 +import 'package:cake_wallet/utils/qr_util.dart';
11 +import 'package:cake_wallet/utils/show_pop_up.dart';
12 +import 'package:cake_wallet/utils/token_utilities.dart';
13 +import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
14 +import 'package:cake_wallet/wallet_types.g.dart';
15 +import 'package:cw_core/crypto_currency.dart';
16 +import 'package:cw_core/currency.dart';
17 +import 'package:cw_core/currency_for_wallet_type.dart';
18 +import 'package:cw_core/utils/print_verbose.dart';
19 +import 'package:cw_core/wallet_type.dart';
20 +import 'package:flutter/material.dart';
21 +
22 +class TokenSelectionBottomSheet extends BaseBottomSheet {
23 + TokenSelectionBottomSheet({
24 + Key? key,
25 + required this.paymentViewModel,
26 + required this.paymentRequest,
27 + required this.onNext,
28 + this.fixedNetwork,
29 + }) : super(
30 + titleText: '',
31 + footerType: FooterType.none,
32 + maxHeight: 900,
33 + );
34 +
35 + final PaymentViewModel paymentViewModel;
36 + final PaymentRequest paymentRequest;
37 + final Function(PaymentFlowResult) onNext;
38 + final WalletType? fixedNetwork;
39 +
40 + @override
41 + Widget contentWidget(BuildContext context) {
42 + return _TokenSelectionContent(
43 + paymentViewModel: paymentViewModel,
44 + paymentRequest: paymentRequest,
45 + onNext: onNext,
46 + fixedNetwork: fixedNetwork,
47 + );
48 + }
49 +}
50 +
51 +class _TokenSelectionContent extends StatefulWidget {
52 + const _TokenSelectionContent({
53 + required this.paymentViewModel,
54 + required this.paymentRequest,
55 + required this.onNext,
56 + this.fixedNetwork,
57 + });
58 +
59 + final PaymentViewModel paymentViewModel;
60 + final PaymentRequest paymentRequest;
61 + final Function(PaymentFlowResult) onNext;
62 + final WalletType? fixedNetwork;
63 +
64 + @override
65 + State<_TokenSelectionContent> createState() => _TokenSelectionContentState();
66 +}
67 +
68 +class _TokenSelectionContentState extends State<_TokenSelectionContent> {
69 + WalletType? selectedNetwork;
70 + CryptoCurrency? selectedToken;
71 + bool isLoadingTokens = false;
72 + String? tokenLoadError;
73 +
74 + @override
75 + void initState() {
76 + super.initState();
77 + selectedNetwork = widget.fixedNetwork ?? WalletType.ethereum;
78 + _autoSelectToken();
79 + }
80 +
81 + bool get _isNetworkSelectable => widget.fixedNetwork == null || isEVMCompatibleChain(widget.fixedNetwork!);
82 +
83 + Future<void> _autoSelectToken() async {
84 + if (selectedNetwork == null) return;
85 +
86 + setState(() {
87 + isLoadingTokens = true;
88 + tokenLoadError = null;
89 + });
90 +
91 + try {
92 + final tokens = await TokenUtilities.getAvailableTokensForNetwork(
93 + selectedNetwork!,
94 + );
95 +
96 + if (tokens.isEmpty) {
97 + setState(() {
98 + tokenLoadError = 'No tokens available';
99 + isLoadingTokens = false;
100 + });
101 + return;
102 + }
103 +
104 + setState(() {
105 + selectedToken = tokens.first;
106 + isLoadingTokens = false;
107 + });
108 + } catch (e) {
109 + printV('Auto-select token error: $e');
110 + setState(() {
111 + tokenLoadError = 'Failed to load tokens';
112 + isLoadingTokens = false;
113 + selectedToken = walletTypeToCryptoCurrency(selectedNetwork!);
114 + });
115 + }
116 + }
117 +
118 + String _getNetworkTitle() {
119 + if (selectedNetwork == null) return S.current.select_network;
120 + return walletTypeToString(selectedNetwork!);
121 + }
122 +
123 + String _getEcosystemTitle() {
124 + if (selectedNetwork == WalletType.solana) {
125 + return 'Solana\n${S.current.address_detected.toLowerCase()}';
126 + }
127 + if (selectedNetwork == WalletType.tron) {
128 + return 'Tron\n${S.current.address_detected.toLowerCase()}';
129 + }
130 + return '${S.current.ethereum_ecosystem}\n${S.current.address_detected.toLowerCase()}';
131 + }
132 +
133 + String _getEcosystemDescription() {
134 + if (selectedNetwork == WalletType.solana) {
135 + return 'Select a token to send on Solana network';
136 + }
137 + if (selectedNetwork == WalletType.tron) {
138 + return 'Select a token to send on Tron network';
139 + }
140 + return S.current.evm_ecosystem_description;
141 + }
142 +
143 + String _getNetworkIcon() {
144 + if (selectedNetwork == null) return 'assets/images/eth_chain_mono.svg';
145 + return getChainMonoImage(selectedNetwork!);
146 + }
147 +
148 + @override
149 + Widget build(BuildContext context) {
150 + return Container(
151 + padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
152 + child: Column(
153 + mainAxisSize: MainAxisSize.min,
154 + children: [
155 + CakeImageWidget(
156 + imageUrl: _getNetworkIcon(),
157 + width: 50,
158 + height: 50,
159 + color: Theme.of(context).colorScheme.onSurface,
160 + ),
161 + const SizedBox(height: 24),
162 + Padding(
163 + padding: const EdgeInsets.symmetric(horizontal: 24),
164 + child: Text(
165 + _getEcosystemTitle(),
166 + style: Theme.of(context).textTheme.titleMedium!.copyWith(
167 + fontSize: 20,
168 + fontWeight: FontWeight.w900,
169 + color: Theme.of(context).colorScheme.onSurface,
170 + ),
171 + textAlign: TextAlign.center,
172 + ),
173 + ),
174 + const SizedBox(height: 36),
175 + Text(
176 + _getEcosystemDescription(),
177 + textAlign: TextAlign.center,
178 + style: Theme.of(context).textTheme.bodyMedium!.copyWith(
179 + fontSize: 16,
180 + fontWeight: FontWeight.w600,
181 + color: Theme.of(context).colorScheme.onSurfaceVariant,
182 + letterSpacing: 0.0,
183 + ),
184 + ),
185 + const SizedBox(height: 32),
186 + TokenSelectionTileWidget(
187 + value: _getNetworkTitle(),
188 + imagePath: selectedNetwork != null ? getChainMonoImage(selectedNetwork!) : null,
189 + color: selectedNetwork != null ? Theme.of(context).colorScheme.primary : null,
190 + enabled: _isNetworkSelectable,
191 + onTap: _isNetworkSelectable ? () => _showNetworkSelection(context) : null,
192 + ),
193 + const SizedBox(height: 16),
194 + TokenSelectionTileWidget(
195 + imagePath: selectedToken?.iconPath,
196 + value: selectedToken != null ? selectedToken!.title : S.current.select_token,
197 + enabled: selectedNetwork != null && !isLoadingTokens,
198 + onTap: isLoadingTokens ? null : () => _showTokenSelection(context),
199 + color: selectedToken == null ? Theme.of(context).colorScheme.primary : null,
200 + ),
201 + if (isLoadingTokens) ...[
202 + const SizedBox(height: 16),
203 + const CircularProgressIndicator(),
204 + ],
205 + if (tokenLoadError != null) ...[
206 + const SizedBox(height: 16),
207 + Text(
208 + tokenLoadError!,
209 + style: Theme.of(context).textTheme.bodySmall!.copyWith(
210 + color: Theme.of(context).colorScheme.error,
211 + ),
212 + ),
213 + ],
214 + const SizedBox(height: 32),
215 + PrimaryButton(
216 + text: S.current.restore_next,
217 + color: Theme.of(context).colorScheme.primary,
218 + textColor: Theme.of(context).colorScheme.onPrimary,
219 + onPressed: selectedNetwork != null && selectedToken != null && !isLoadingTokens
220 + ? () async => await _handleNext(context)
221 + : null,
222 + ),
223 + const SizedBox(height: 48),
224 + ],
225 + ),
226 + );
227 + }
228 +
229 + void _showNetworkSelection(BuildContext context) async {
230 + if (!_isNetworkSelectable) return;
231 +
232 + final evmNetworks =
233 + availableWalletTypes.where((walletType) => isEVMCompatibleChain(walletType)).toList();
234 + final selectedIndex = evmNetworks.indexOf(selectedNetwork ?? WalletType.ethereum);
235 +
236 + await showPopUp<void>(
237 + context: context,
238 + builder: (BuildContext context) {
239 + return Picker(
240 + items: evmNetworks,
241 + displayItem: (WalletType network) => walletTypeToString(network),
242 + selectedAtIndex: selectedIndex,
243 + title: S.current.select_network,
244 + closeOnItemSelected: true,
245 + hasTitleSpacing: true,
246 + images: evmNetworks
247 + .map((network) => CakeImageWidget(
248 + imageUrl: getChainMonoImage(network),
249 + width: 20,
250 + height: 20,
251 + color: Theme.of(context).colorScheme.primary))
252 + .toList(),
253 + onItemSelected: (WalletType network) {
254 + setState(() {
255 + selectedNetwork = network;
256 + selectedToken = null;
257 + });
258 + _autoSelectToken();
259 + },
260 + );
261 + },
262 + );
263 + }
264 +
265 + void _showTokenSelection(BuildContext context) async {
266 + if (selectedNetwork == null || isLoadingTokens) return;
267 +
268 + setState(() {
269 + isLoadingTokens = true;
270 + });
271 +
272 + try {
273 + final availableTokens = await TokenUtilities.getAvailableTokensForNetwork(
274 + selectedNetwork!,
275 + );
276 +
277 + if (availableTokens.isEmpty) {
278 + setState(() {
279 + isLoadingTokens = false;
280 + });
281 + await showPopUp<void>(
282 + context: context,
283 + builder: (context) => AlertDialog(
284 + title: Text(S.current.error),
285 + content: const Text('No tokens available for this network'),
286 + actions: [
287 + TextButton(
288 + onPressed: () => Navigator.of(context).pop(),
289 + child: Text(S.current.ok),
290 + ),
291 + ],
292 + ),
293 + );
294 + return;
295 + }
296 +
297 + final selectedIndex = selectedToken != null ? availableTokens.indexOf(selectedToken!) : 0;
298 +
299 + setState(() {
300 + isLoadingTokens = false;
301 + });
302 +
303 + await showPopUp<void>(
304 + context: context,
305 + builder: (BuildContext context) {
306 + return CurrencyPicker(
307 + selectedAtIndex: selectedIndex >= 0 ? selectedIndex : 0,
308 + items: availableTokens.cast<Currency>(),
309 + hintText: S.current.add_token,
310 + onItemSelected: (Currency currency) {
311 + setState(() {
312 + selectedToken = currency as CryptoCurrency;
313 + });
314 + },
315 + );
316 + },
317 + );
318 + } catch (e) {
319 + printV('Error showing token selection: $e');
320 + setState(() {
321 + isLoadingTokens = false;
322 + tokenLoadError = 'Failed to load tokens';
323 + });
324 + }
325 + }
326 +
327 + Future<void> _handleNext(BuildContext context) async {
328 + if (selectedNetwork == null || selectedToken == null) return;
329 +
330 + Navigator.of(context).pop();
331 +
332 + final compatibleWallets = await widget.paymentViewModel.getWalletsByType(selectedNetwork!);
333 +
334 + PaymentFlowResult newResult;
335 +
336 + if (selectedNetwork == WalletType.solana) {
337 + newResult = PaymentFlowResult.solanaTokenSelection(
338 + AddressDetectionResult(
339 + address: widget.paymentRequest.address,
340 + detectedWalletType: WalletType.solana,
341 + detectedCurrency: selectedToken!,
342 + isValid: true,
343 + amount: widget.paymentRequest.amount,
344 + note: widget.paymentRequest.note,
345 + scheme: widget.paymentRequest.scheme,
346 + pjUri: widget.paymentRequest.pjUri,
347 + callbackUrl: widget.paymentRequest.callbackUrl,
348 + callbackMessage: widget.paymentRequest.callbackMessage,
349 + ),
350 + compatibleWallets: compatibleWallets,
351 + wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
352 + );
353 + } else if (selectedNetwork == WalletType.tron) {
354 + newResult = PaymentFlowResult.tronTokenSelection(
355 + AddressDetectionResult(
356 + address: widget.paymentRequest.address,
357 + detectedWalletType: WalletType.tron,
358 + detectedCurrency: selectedToken!,
359 + isValid: true,
360 + amount: widget.paymentRequest.amount,
361 + note: widget.paymentRequest.note,
362 + scheme: widget.paymentRequest.scheme,
363 + pjUri: widget.paymentRequest.pjUri,
364 + callbackUrl: widget.paymentRequest.callbackUrl,
365 + callbackMessage: widget.paymentRequest.callbackMessage,
366 + ),
367 + compatibleWallets: compatibleWallets,
368 + wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
369 + );
370 + } else {
371 + newResult = PaymentFlowResult.evmNetworkSelection(
372 + AddressDetectionResult(
373 + address: widget.paymentRequest.address,
374 + detectedWalletType: selectedNetwork!,
375 + detectedCurrency: selectedToken!,
376 + isValid: true,
377 + amount: widget.paymentRequest.amount,
378 + note: widget.paymentRequest.note,
379 + scheme: widget.paymentRequest.scheme,
380 + pjUri: widget.paymentRequest.pjUri,
381 + callbackUrl: widget.paymentRequest.callbackUrl,
382 + callbackMessage: widget.paymentRequest.callbackMessage,
383 + ),
384 + compatibleWallets: compatibleWallets,
385 + wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
386 + );
387 + }
388 +
389 + widget.paymentViewModel.detectedWalletType = selectedNetwork!;
390 + widget.onNext(newResult);
391 + }
392 +}
393 +
394 +class TokenSelectionTileWidget extends StatelessWidget {
395 + const TokenSelectionTileWidget({
396 + super.key,
397 + required this.value,
398 + required this.enabled,
399 + required this.onTap,
400 + this.imagePath,
401 + this.color,
402 + });
403 +
404 + final String value;
405 + final bool enabled;
406 + final VoidCallback? onTap;
407 + final String? imagePath;
408 + final Color? color;
409 +
410 + @override
411 + Widget build(BuildContext context) {
412 + return InkWell(
413 + onTap: enabled ? onTap : null,
414 + borderRadius: BorderRadius.circular(12),
415 + child: Opacity(
416 + opacity: enabled ? 1.0 : 0.6,
417 + child: Container(
418 + padding: const EdgeInsets.all(16),
419 + decoration: BoxDecoration(
420 + borderRadius: BorderRadius.circular(12),
421 + color: Theme.of(context).colorScheme.surfaceContainer,
422 + ),
423 + child: Row(
424 + children: [
425 + if (imagePath != null) ...[
426 + CakeImageWidget(
427 + imageUrl: imagePath!,
428 + color: color,
429 + width: 20,
430 + height: 20,
431 + ),
432 + const SizedBox(width: 16),
433 + ],
434 + Expanded(
435 + child: Text(
436 + value,
437 + style: Theme.of(context).textTheme.titleMedium!.copyWith(
438 + fontSize: 14,
439 + fontWeight: FontWeight.w400,
440 + color: Theme.of(context).colorScheme.onSurface,
441 + ),
442 + ),
443 + ),
444 + if (enabled)
445 + Icon(
446 + Icons.keyboard_arrow_down,
447 + size: 24,
448 + color: Theme.of(context).colorScheme.primary,
449 + ),
450 + ],
451 + ),
452 + ),
453 + ),
454 + );
455 + }
456 +}
lib/utils/token_utilities.dart
+119 -79
@@ -22,18 +22,13 @@ class TokenUtilities {
22 final unique = <Erc20Token>[];
23
24 for (final wallet in evmWallets) {
25 - final allChains = evm!.getAllChains();
25 + final chain = getTokenNameBasedOnWalletType(wallet.type);
26 + final box = await _openEvmTokensBoxFor(wallet);
27
27 - for (final chainInfo in allChains) {
28 - final chainId = chainInfo.chainId;
29 - final chain = getTokenNameBasedOnWalletType(wallet.type, chainId: chainId);
30 - final box = await _openEvmTokensBoxFor(wallet, chainId);
31 -
32 - for (final t in box.values.where((t) => t.enabled)) {
33 - final key = '$chain|${t.contractAddress.toLowerCase()}';
34 - if (seen.add(key)) {
35 - unique.add(t);
36 - }
28 + for (final t in box.values.where((t) => t.enabled)) {
29 + final key = '$chain|${t.contractAddress.toLowerCase()}';
30 + if (seen.add(key)) {
31 + unique.add(t);
32 }
33 }
34 }
@@ -93,6 +88,7 @@ class TokenUtilities {
88 case WalletType.ethereum:
89 case WalletType.polygon:
90 case WalletType.base:
91 + case WalletType.arbitrum:
92 final tokens = await loadAllUniqueEvmTokens();
93 for (final t in tokens) {
94 if (t.contractAddress.toLowerCase() == lower) return t;
@@ -115,9 +111,9 @@ class TokenUtilities {
111 }
112 }
113
118 - static Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo, int chainId) async {
114 + static Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo) async {
115 final walletKey = walletInfo.name.replaceAll(' ', '_');
120 - final boxName = _getErc20TokensBoxName(walletKey, chainId);
116 + final boxName = _getErc20TokensBoxName(walletKey, walletInfo.type);
117
118 if (CakeHive.isBoxOpen(boxName)) {
119 return CakeHive.box<Erc20Token>(boxName);
@@ -125,13 +121,13 @@ class TokenUtilities {
121 return CakeHive.openBox<Erc20Token>(boxName);
122 }
123
128 - static String _getErc20TokensBoxName(String sanitizedName, int chainId) {
129 - return switch (chainId) {
130 - 1 => "${sanitizedName}_${Erc20Token.ethereumBoxName}",
131 - 137 => "${sanitizedName}_${Erc20Token.polygonBoxName}",
132 - 8453 => "${sanitizedName}_${Erc20Token.baseBoxName}",
133 - 42161 => "${sanitizedName}_${Erc20Token.arbitrumBoxName}",
134 - _ => "${sanitizedName}_${Erc20Token.ethereumBoxName}",
124 + static String _getErc20TokensBoxName(String walletKey, WalletType walletType) {
125 + return switch (walletType) {
126 + WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}',
127 + WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}',
128 + WalletType.base => '${walletKey}_${Erc20Token.baseBoxName}',
129 + WalletType.arbitrum => '${walletKey}_${Erc20Token.arbitrumBoxName}',
130 + _ => '${walletKey}_${Erc20Token.ethereumBoxName}',
131 };
132 }
133
@@ -172,6 +168,8 @@ class TokenUtilities {
168 title == 'ethereum' ||
169 title == 'matic' ||
170 title == 'polygon' ||
171 + title == 'base' ||
172 + title == 'arbitrum' ||
173 title == 'bnb' ||
174 title == 'bsc' ||
175 title == 'avax' ||
@@ -186,14 +184,8 @@ class TokenUtilities {
184 final title = currency.title.toLowerCase();
185
186 // Only check EVM registry for currencies that might be EVM-related
189 - final isPotentialEVM = title == 'eth' ||
190 - title == 'ethereum' ||
191 - title == 'polygon' ||
192 - title == 'matic' ||
193 - title == 'base' ||
194 - title == 'arbitrum' ||
195 - (tag != null && (tag == 'ETH' || tag == 'POL' || tag == 'BASE' || tag == 'ARB')) ||
196 - isNativeToken(currency);
187 + final isPotentialEVM = isNativeToken(currency) ||
188 + (tag != null && (tag == 'ETH' || tag == 'POL' || tag == 'BASE' || tag == 'ARB'));
189
190 if (isPotentialEVM) {
191 // Try by tag first if available (e.g., 'POL', 'BASE', 'ARB')
@@ -232,23 +224,95 @@ class TokenUtilities {
224 return 1;
225 }
226
235 - static bool _shouldAddToken(
236 - List<CryptoCurrency> existingTokens,
237 - CryptoCurrency token,
238 - Set<String> addedAddresses,
239 - ) {
240 - if (token is Erc20Token) {
241 - final address = token.contractAddress.toLowerCase();
242 - if (addedAddresses.contains(address)) {
243 - return false;
227 + static Future<List<CryptoCurrency>> getAvailableTokensForNetwork(
228 + WalletType network,
229 + ) async {
230 + final baseCurrency = walletTypeToCryptoCurrency(network);
231 + final allTokens = <CryptoCurrency>[baseCurrency];
232 + final addedAddresses = <String>{};
233 +
234 + // Handle EVM networks
235 + if (isEVMCompatibleChain(network)) {
236 + // First, collect all user tokens
237 + final userTokens = await _getUserTokensForNetwork(baseCurrency);
238 + for (final token in userTokens) {
239 + if (token is Erc20Token) {
240 + final address = token.contractAddress.toLowerCase();
241 + if (addedAddresses.add(address)) {
242 + allTokens.add(token);
243 + }
244 + }
245 }
245 - if (existingTokens.any((existing) => _matchesCurrency(existing, token))) {
246 - return false;
246 +
247 + // Then add tokens from CryptoCurrency.all that don't duplicate user tokens
248 + for (final currency in CryptoCurrency.all) {
249 + // Match by tag for POL/BASE, match by title==tag for ETH
250 + final matches = (baseCurrency.tag == null && baseCurrency.title == currency.tag) ||
251 + (baseCurrency.tag != null &&
252 + currency.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase());
253 +
254 + if (matches) {
255 + if (currency is Erc20Token) {
256 + final address = currency.contractAddress.toLowerCase();
257 + if (addedAddresses.add(address)) {
258 + allTokens.add(currency);
259 + }
260 + } else if (!allTokens.any((t) => _matchesCurrency(t, currency))) {
261 + allTokens.add(currency);
262 + }
263 + }
264 }
248 - return true;
265 }
266
251 - return !existingTokens.any((existing) => _matchesCurrency(existing, token));
267 + // Handle Solana network
268 + else if (network == WalletType.solana) {
269 + final userSolTokens = await loadAllUniqueSolTokens();
270 + for (final token in userSolTokens) {
271 + final mintAddress = token.mintAddress.toLowerCase();
272 + if (addedAddresses.add(mintAddress)) {
273 + allTokens.add(token);
274 + }
275 + }
276 +
277 + for (final currency in CryptoCurrency.all) {
278 + if (currency.tag?.toLowerCase() == 'sol') {
279 + if (currency is SPLToken) {
280 + final mintAddress = currency.mintAddress.toLowerCase();
281 + if (addedAddresses.add(mintAddress)) {
282 + allTokens.add(currency);
283 + }
284 + } else if (!allTokens.any((t) => _matchesCurrency(t, currency))) {
285 + allTokens.add(currency);
286 + }
287 + }
288 + }
289 + }
290 +
291 + // Handle Tron network
292 + else if (network == WalletType.tron) {
293 + final userTronTokens = await loadAllUniqueTronTokens();
294 + for (final token in userTronTokens) {
295 + final contractAddress = token.contractAddress.toLowerCase();
296 + if (addedAddresses.add(contractAddress)) {
297 + allTokens.add(token);
298 + }
299 + }
300 +
301 + for (final currency in CryptoCurrency.all) {
302 + if (currency.tag?.toLowerCase() == 'trx') {
303 + if (currency is TronToken) {
304 + final contractAddress = currency.contractAddress.toLowerCase();
305 + if (addedAddresses.add(contractAddress)) {
306 + allTokens.add(currency);
307 + }
308 + } else if (!allTokens.any((t) => _matchesCurrency(t, currency))) {
309 + allTokens.add(currency);
310 + }
311 + }
312 + }
313 + }
314 +
315 + return allTokens;
316 }
317
318 static bool _matchesCurrency(CryptoCurrency a, CryptoCurrency b) {
@@ -256,52 +320,28 @@ class TokenUtilities {
320 (a.tag?.toUpperCase() == b.tag?.toUpperCase());
321 }
322
259 - static Future<List<CryptoCurrency>> getAvailableTokensForChainId(int chainId) async {
260 - // Get native currency for the chain
261 - final baseCurrency = getCryptoCurrencyByChainId(chainId);
262 - final allTokens = <CryptoCurrency>[];
263 - final addedAddresses = <String>{};
323 + static Future<List<CryptoCurrency>> _getUserTokensForNetwork(CryptoCurrency baseCurrency) async {
324 + final walletType = cryptoCurrencyToWalletType(baseCurrency);
325 + if (walletType == null) return [];
326
265 - allTokens.add(baseCurrency);
327 + if (isEVMCompatibleChain(walletType)) {
328 + final tokens = await TokenUtilities.loadAllUniqueEvmTokens();
329
267 - // Add currencies that match this chain
268 - for (final currency in CryptoCurrency.all) {
269 - final matches = (baseCurrency.tag == null && baseCurrency.title == currency.tag) ||
270 - (baseCurrency.tag != null &&
271 - currency.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase());
330 + return tokens.where((token) {
331 + if (baseCurrency.tag == null) return token.tag == baseCurrency.title;
332
273 - if (matches && _shouldAddToken(allTokens, currency, addedAddresses)) {
274 - allTokens.add(currency);
275 - }
333 + return token.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase();
334 + }).toList();
335 }
336
278 - // Add user tokens for this chain
279 - final userTokens = await _getUserTokensForChainId(chainId);
280 - for (final token in userTokens) {
281 - if (_shouldAddToken(allTokens, token, addedAddresses)) {
282 - allTokens.add(token);
283 - if (token is Erc20Token) {
284 - addedAddresses.add(token.contractAddress.toLowerCase());
285 - }
286 - }
337 + if (walletType == WalletType.solana) {
338 + return await loadAllUniqueSolTokens();
339 }
340
289 - return allTokens;
290 - }
291 -
292 - static Future<List<CryptoCurrency>> _getUserTokensForChainId(int chainId) async {
293 - final allWi = await WalletInfo.getAll();
294 - final evmWallets = allWi.where((w) => isEVMCompatibleChain(w.type));
295 -
296 - final tokens = <Erc20Token>[];
297 - for (final wallet in evmWallets) {
298 - final box = await _openEvmTokensBoxFor(wallet, chainId);
299 -
300 - for (final t in box.values.where((t) => t.enabled)) {
301 - tokens.add(t);
302 - }
341 + if (walletType == WalletType.tron) {
342 + return await loadAllUniqueTronTokens();
343 }
344
305 - return tokens.cast<CryptoCurrency>();
345 + return [];
346 }
347 }
lib/view_model/payment/payment_view_model.dart
+51 -65
@@ -76,70 +76,28 @@ abstract class PaymentViewModelBase with Store {
76
77 final currentWallet = appStore.wallet;
78
79 - if (isEVMCompatibleChain(detectedWalletType!)) {
80 - final isRawEvmInput = !addressData.contains(':') && _isEVMAddress(detectionResult.address);
81 -
82 - // Check if the current wallet is also EVM
83 - if (currentWallet != null && isEVMCompatibleChain(currentWallet.type)) {
84 - final currentChainId = evm!.getSelectedChainId(currentWallet);
85 - final detectedChainIdValue = this.detectedChainId;
86 -
87 - if (detectedChainIdValue != null && currentChainId != null) {
88 - // For raw EVM address input that only defaulted to chainId 1,
89 - // always force the EVM ecosystem bottom sheet so the user
90 - // can pick the actual network and token.
91 - if (isRawEvmInput && detectedChainIdValue == 1) {
92 - final allEVMWallets = await getEVMCompatibleWallets();
93 -
94 - final currentWalletInfo = currentWallet.walletInfo;
95 -
96 - final otherEVMWallets =
97 - allEVMWallets.where((w) => w.name != currentWallet.name).toList();
98 -
99 - return PaymentFlowResult.evmNetworkSelection(
100 - detectionResult,
101 - compatibleWallets: otherEVMWallets,
102 - wallet: currentWalletInfo,
103 - );
104 - }
105 -
106 - if (detectedChainIdValue == currentChainId) {
107 - return PaymentFlowResult.currentWalletCompatible();
108 - }
109 -
110 - final allEVMWallets = await getEVMCompatibleWallets();
111 -
112 - final currentWalletInfo = currentWallet.walletInfo;
113 -
114 - final otherEVMWallets =
115 - allEVMWallets.where((w) => w.name != currentWallet.name).toList();
116 -
117 - return PaymentFlowResult.evmNetworkSelection(
118 - detectionResult,
119 - compatibleWallets: otherEVMWallets,
120 - wallet: currentWalletInfo,
121 - );
122 - }
123 - }
124 -
125 - // If the current wallet is not EVM or the chainId comparison failed
126 - // We proceed with other checks
127 - if (!addressData.contains(':') && _isEVMAddress(detectionResult.address)) {
128 - final allEVMWallets = await getEVMCompatibleWallets();
129 - return PaymentFlowResult.evmNetworkSelection(
130 - detectionResult,
131 - compatibleWallets: allEVMWallets,
132 - );
133 - }
134 -
135 - // For EVM URIs, show network selection
136 - final allEVMWallets = await getEVMCompatibleWallets();
137 - return PaymentFlowResult.evmNetworkSelection(
79 + if (detectedWalletType == WalletType.solana) {
80 + final compatibleWallets = await getWalletsByType(WalletType.solana);
81 + return PaymentFlowResult.solanaTokenSelection(
82 + detectionResult,
83 + compatibleWallets: compatibleWallets,
84 + wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
85 + );
86 + }
87 +
88 + if (detectedWalletType == WalletType.tron) {
89 + final compatibleWallets = await getWalletsByType(WalletType.tron);
90 + return PaymentFlowResult.tronTokenSelection(
91 detectionResult,
139 - compatibleWallets: allEVMWallets,
92 + compatibleWallets: compatibleWallets,
93 + wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
94 );
95 }
96
97 + if (isEVMCompatibleChain(detectedWalletType!)) {
98 + return PaymentFlowResult.evmNetworkSelection(detectionResult);
99 + }
100 +
101 if (currentWallet != null && currentWallet.type == detectedWalletType) {
102 return PaymentFlowResult.currentWalletCompatible();
103 }
@@ -172,10 +130,6 @@ abstract class PaymentViewModelBase with Store {
130 await evm!.selectChain(appStore.wallet!, detectedChainId!, node: node);
131 }
132
175 - bool _isEVMAddress(String address) {
176 - return RegExp(r'^0x[a-fA-F0-9]{40}$').hasMatch(address);
177 - }
178 -
133 Future<List<WalletInfo>> getWalletsByType(WalletType walletType) async {
134 return (await WalletInfo.getAll()).where((wallet) => wallet.type == walletType).toList();
135 }
@@ -230,6 +184,34 @@ class PaymentFlowResult {
184 );
185 }
186
187 + /// Solana address detected - needs token selection
188 + factory PaymentFlowResult.solanaTokenSelection(
189 + AddressDetectionResult addressDetectionResult, {
190 + List<WalletInfo>? compatibleWallets,
191 + WalletInfo? wallet,
192 + }) =>
193 + PaymentFlowResult._(
194 + type: PaymentFlowType.solanaTokenSelection,
195 + addressDetectionResult: addressDetectionResult,
196 + walletType: WalletType.solana,
197 + wallets: compatibleWallets ?? [],
198 + wallet: wallet,
199 + );
200 +
201 + /// Tron address detected - needs token selection
202 + factory PaymentFlowResult.tronTokenSelection(
203 + AddressDetectionResult addressDetectionResult, {
204 + List<WalletInfo>? compatibleWallets,
205 + WalletInfo? wallet,
206 + }) =>
207 + PaymentFlowResult._(
208 + type: PaymentFlowType.tronTokenSelection,
209 + addressDetectionResult: addressDetectionResult,
210 + walletType: WalletType.tron,
211 + wallets: compatibleWallets ?? [],
212 + wallet: wallet,
213 + );
214 +
215 /// Current wallet is compatible
216 factory PaymentFlowResult.currentWalletCompatible() =>
217 PaymentFlowResult._(type: PaymentFlowType.currentWalletCompatible);
@@ -308,7 +290,9 @@ class PaymentFlowResult {
290 PaymentFlowResult._(type: PaymentFlowType.incompatible, message: message);
291
292 CryptoCurrency? get detectedCurrency {
311 - if (type == PaymentFlowType.evmNetworkSelection) {
293 + if (type == PaymentFlowType.evmNetworkSelection ||
294 + type == PaymentFlowType.solanaTokenSelection ||
295 + type == PaymentFlowType.tronTokenSelection) {
296 return addressDetectionResult?.detectedCurrency;
297 }
298 if (walletType != null) {
@@ -328,6 +312,8 @@ enum PaymentFlowType {
312 multipleWallets,
313 noWallets,
314 evmNetworkSelection,
315 + solanaTokenSelection,
316 + tronTokenSelection,
317 error,
318 incompatible,
319 }