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
}