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/utils/print_verbose.dart';
18
+import 'package:cw_core/wallet_type.dart';
19
+import 'package:flutter/material.dart';
20
+
21
+class EVMPaymentFlowBottomSheet extends BaseBottomSheet {
22
+ EVMPaymentFlowBottomSheet({
23
+ Key? key,
24
+ required this.paymentViewModel,
25
+ required this.paymentRequest,
26
+ required this.onNext,
27
+ }) : super(
28
+ titleText: '',
29
+ footerType: FooterType.none,
30
+ maxHeight: 900,
31
+ );
32
+
33
+ final PaymentViewModel paymentViewModel;
34
+ final PaymentRequest paymentRequest;
35
+ final Function(PaymentFlowResult) onNext;
36
+
37
+ @override
38
+ Widget contentWidget(BuildContext context) {
39
+ return _EVMPaymentFlowContent(
40
+ paymentViewModel: paymentViewModel,
41
+ paymentRequest: paymentRequest,
42
+ onNext: onNext,
43
+ );
44
+ }
45
+}
46
+
47
+class _EVMPaymentFlowContent extends StatefulWidget {
48
+ const _EVMPaymentFlowContent({
49
+ required this.paymentViewModel,
50
+ required this.paymentRequest,
51
+ required this.onNext,
52
+ });
53
+
54
+ final PaymentViewModel paymentViewModel;
55
+ final PaymentRequest paymentRequest;
56
+ final Function(PaymentFlowResult) onNext;
57
+
58
+ @override
59
+ State<_EVMPaymentFlowContent> createState() => _EVMPaymentFlowContentState();
60
+}
61
+
62
+class _EVMPaymentFlowContentState extends State<_EVMPaymentFlowContent> {
63
+ WalletType? selectedNetwork;
64
+ CryptoCurrency? selectedToken;
65
+
66
+ @override
67
+ void initState() {
68
+ super.initState();
69
+ selectedNetwork = WalletType.ethereum;
70
+ _autoSelectToken();
71
+ }
72
+
73
+ Future<void> _autoSelectToken() async {
74
+ if (selectedNetwork == null) return;
75
+
76
+ try {
77
+ final tokens = await TokenUtilities.getAvailableTokensForNetwork(selectedNetwork!);
78
+ if (tokens.isNotEmpty) {
79
+ setState(() {
80
+ selectedToken = tokens.first;
81
+ });
82
+ }
83
+ } catch (e) {
84
+ printV('Auto-select token error: $e');
85
+ }
86
+ }
87
+
88
+ @override
89
+ Widget build(BuildContext context) {
90
+ return Container(
91
+ padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
92
+ child: Column(
93
+ mainAxisSize: MainAxisSize.min,
94
+ children: [
95
+ CakeImageWidget(
96
+ imageUrl: 'assets/images/eth_chain_mono.svg',
97
+ width: 50,
98
+ height: 50,
99
+ color: Theme.of(context).colorScheme.onSurface,
100
+ ),
101
+ const SizedBox(height: 24),
102
+ Padding(
103
+ padding: const EdgeInsets.symmetric(horizontal: 24),
104
+ child: Text(
105
+ '${S.current.ethereum_ecosystem}\n${S.current.address_detected.toLowerCase()}',
106
+ style: Theme.of(context).textTheme.titleMedium!.copyWith(
107
+ fontSize: 20,
108
+ fontWeight: FontWeight.w900,
109
+ color: Theme.of(context).colorScheme.onSurface,
110
+ ),
111
+ textAlign: TextAlign.center,
112
+ ),
113
+ ),
114
+ const SizedBox(height: 36),
115
+ Text(
116
+ S.current.evm_ecosystem_description,
117
+ textAlign: TextAlign.center,
118
+ style: Theme.of(context).textTheme.bodyMedium!.copyWith(
119
+ fontSize: 16,
120
+ fontWeight: FontWeight.w600,
121
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
122
+ letterSpacing: 0.0,
123
+ ),
124
+ ),
125
+ const SizedBox(height: 32),
126
+ EVMTileWidget(
127
+ value: selectedNetwork != null
128
+ ? walletTypeToString(selectedNetwork!)
129
+ : S.current.select_network,
130
+ imagePath: selectedNetwork != null ? getChainMonoImage(selectedNetwork!) : null,
131
+ color: selectedNetwork != null ? Theme.of(context).colorScheme.primary : null,
132
+ enabled: true,
133
+ onTap: () => _showNetworkSelection(context),
134
+ ),
135
+ const SizedBox(height: 16),
136
+ EVMTileWidget(
137
+ imagePath: selectedToken != null ? selectedToken!.iconPath : null,
138
+ value: selectedToken != null ? selectedToken!.title : S.current.select_token,
139
+ enabled: selectedNetwork != null,
140
+ onTap: () => _showTokenSelection(context),
141
+ color: selectedToken == null ? Theme.of(context).colorScheme.primary : null,
142
+ ),
143
+ const SizedBox(height: 32),
144
+ PrimaryButton(
145
+ text: S.current.restore_next,
146
+ color: Theme.of(context).colorScheme.primary,
147
+ textColor: Theme.of(context).colorScheme.onPrimary,
148
+ onPressed: selectedNetwork != null && selectedToken != null
149
+ ? () async => await _handleNext(context)
150
+ : null,
151
+ ),
152
+ const SizedBox(height: 48),
153
+ ],
154
+ ),
155
+ );
156
+ }
157
+
158
+ void _showNetworkSelection(BuildContext context) async {
159
+ final evmNetworks =
160
+ availableWalletTypes.where((walletType) => isEVMCompatibleChain(walletType)).toList();
161
+ final selectedIndex = evmNetworks.indexOf(selectedNetwork ?? WalletType.ethereum);
162
+
163
+ await showPopUp<void>(
164
+ context: context,
165
+ builder: (BuildContext context) {
166
+ return Picker(
167
+ items: evmNetworks,
168
+ displayItem: (WalletType network) => walletTypeToString(network),
169
+ selectedAtIndex: selectedIndex,
170
+ title: S.current.select_network,
171
+ closeOnItemSelected: true,
172
+ hasTitleSpacing: true,
173
+ images: evmNetworks
174
+ .map((network) => CakeImageWidget(
175
+ imageUrl: getChainMonoImage(network),
176
+ width: 20,
177
+ height: 20,
178
+ color: Theme.of(context).colorScheme.primary))
179
+ .toList(),
180
+ onItemSelected: (WalletType network) {
181
+ setState(() {
182
+ selectedNetwork = network;
183
+ selectedToken = null;
184
+ });
185
+ _autoSelectToken();
186
+ },
187
+ );
188
+ },
189
+ );
190
+ }
191
+
192
+ void _showTokenSelection(BuildContext context) async {
193
+ if (selectedNetwork == null) return;
194
+
195
+ try {
196
+ final availableTokens = await TokenUtilities.getAvailableTokensForNetwork(selectedNetwork!);
197
+
198
+ if (availableTokens.isEmpty) return;
199
+
200
+ final selectedIndex = selectedToken != null ? availableTokens.indexOf(selectedToken!) : 0;
201
+
202
+ await showPopUp<void>(
203
+ context: context,
204
+ builder: (BuildContext context) {
205
+ return CurrencyPicker(
206
+ selectedAtIndex: selectedIndex >= 0 ? selectedIndex : 0,
207
+ items: availableTokens.cast<Currency>(),
208
+ hintText: S.current.add_token,
209
+ onItemSelected: (Currency currency) {
210
+ setState(() {
211
+ selectedToken = currency as CryptoCurrency;
212
+ });
213
+ },
214
+ );
215
+ },
216
+ );
217
+ } catch (e) {
218
+ printV('Error showing token selection: $e');
219
+ }
220
+ }
221
+
222
+ Future<void> _handleNext(BuildContext context) async {
223
+ if (selectedNetwork == null || selectedToken == null) return;
224
+
225
+ Navigator.of(context).pop();
226
+
227
+ final compatibleWallets = await widget.paymentViewModel.getWalletsByType(selectedNetwork!);
228
+
229
+ final newResult = PaymentFlowResult.evmNetworkSelection(
230
+ AddressDetectionResult(
231
+ address: widget.paymentRequest.address,
232
+ detectedWalletType: selectedNetwork!,
233
+ detectedCurrency: selectedToken!,
234
+ isValid: true,
235
+ amount: widget.paymentRequest.amount,
236
+ note: widget.paymentRequest.note,
237
+ scheme: widget.paymentRequest.scheme,
238
+ pjUri: widget.paymentRequest.pjUri,
239
+ callbackUrl: widget.paymentRequest.callbackUrl,
240
+ callbackMessage: widget.paymentRequest.callbackMessage,
241
+ ),
242
+ compatibleWallets: compatibleWallets,
243
+ wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
244
+ );
245
+
246
+ widget.paymentViewModel.detectedWalletType = selectedNetwork!;
247
+ widget.onNext(newResult);
248
+ }
249
+}
250
+
251
+class EVMTileWidget extends StatelessWidget {
252
+ const EVMTileWidget({
253
+ super.key,
254
+ required this.value,
255
+ required this.enabled,
256
+ required this.onTap,
257
+ this.imagePath,
258
+ this.color,
259
+ });
260
+
261
+ final String value;
262
+ final bool enabled;
263
+ final VoidCallback? onTap;
264
+ final String? imagePath;
265
+ final Color? color;
266
+
267
+ @override
268
+ Widget build(BuildContext context) {
269
+ return InkWell(
270
+ onTap: enabled ? onTap : null,
271
+ borderRadius: BorderRadius.circular(12),
272
+ child: Container(
273
+ padding: const EdgeInsets.all(16),
274
+ decoration: BoxDecoration(
275
+ borderRadius: BorderRadius.circular(12),
276
+ color: Theme.of(context).colorScheme.surfaceContainer,
277
+ ),
278
+ child: Row(
279
+ children: [
280
+ if (imagePath != null) ...[
281
+ CakeImageWidget(
282
+ imageUrl: imagePath!,
283
+ color: color,
284
+ width: 20,
285
+ height: 20,
286
+ ),
287
+ const SizedBox(width: 16),
288
+ ],
289
+ Expanded(
290
+ child: Text(
291
+ value,
292
+ style: Theme.of(context).textTheme.titleMedium!.copyWith(
293
+ fontSize: 14,
294
+ fontWeight: FontWeight.w400,
295
+ color: Theme.of(context).colorScheme.onSurface,
296
+ ),
297
+ ),
298
+ ),
299
+ Icon(
300
+ Icons.keyboard_arrow_down,
301
+ size: 24,
302
+ color: Theme.of(context).colorScheme.primary,
303
+ ),
304
+ ],
305
+ ),
306
+ ),
307
+ );
308
+ }
309
+}