dev
dart 305 lines 13.1 KB
Raw
1 import 'package:cake_wallet/evm/evm.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/utils/payment_request.dart';
4 import 'package:cw_core/currency_for_wallet_type.dart';
5 import 'package:cw_core/generate_name.dart';
6 import 'package:flutter/material.dart';
7 import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
8 import 'package:cake_wallet/src/widgets/primary_button.dart';
9 import 'package:cw_core/wallet_type.dart';
10 import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
11 import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
12 import 'package:cake_wallet/reactions/wallet_connect.dart';
13 import 'package:flutter_mobx/flutter_mobx.dart';
14
15 class PaymentConfirmationBottomSheet extends BaseBottomSheet {
16 PaymentConfirmationBottomSheet({
17 Key? key,
18 required this.paymentFlowResult,
19 required this.paymentViewModel,
20 required this.walletSwitcherViewModel,
21 required this.paymentRequest,
22 required this.onSelectWallet,
23 required this.onChangeWallet,
24 required this.onSwap,
25 this.onSwitchNetwork,
26 }) : super(
27 titleText: '',
28 footerType: FooterType.none,
29 maxHeight: 900,
30 );
31
32 final PaymentFlowResult paymentFlowResult;
33 final PaymentViewModel paymentViewModel;
34 final WalletSwitcherViewModel walletSwitcherViewModel;
35 final PaymentRequest paymentRequest;
36 final VoidCallback onSelectWallet;
37 final VoidCallback onChangeWallet;
38 final void Function(BuildContext) onSwap;
39 final VoidCallback? onSwitchNetwork;
40
41 @override
42 Widget contentWidget(BuildContext context) {
43 return _PaymentConfirmationContent(
44 paymentFlowResult: paymentFlowResult,
45 paymentViewModel: paymentViewModel,
46 walletSwitcherViewModel: walletSwitcherViewModel,
47 paymentRequest: paymentRequest,
48 onSelectWallet: onSelectWallet,
49 onChangeWallet: onChangeWallet,
50 onSwap: onSwap,
51 onSwitchNetwork: onSwitchNetwork,
52 );
53 }
54 }
55
56 class _PaymentConfirmationContent extends StatelessWidget {
57 const _PaymentConfirmationContent({
58 required this.paymentFlowResult,
59 required this.paymentViewModel,
60 required this.walletSwitcherViewModel,
61 required this.paymentRequest,
62 required this.onSelectWallet,
63 required this.onChangeWallet,
64 required this.onSwap,
65 this.onSwitchNetwork,
66 });
67
68 final PaymentFlowResult paymentFlowResult;
69 final PaymentViewModel paymentViewModel;
70 final WalletSwitcherViewModel walletSwitcherViewModel;
71 final PaymentRequest paymentRequest;
72 final VoidCallback onSelectWallet;
73 final VoidCallback onChangeWallet;
74 final void Function(BuildContext) onSwap;
75 final VoidCallback? onSwitchNetwork;
76
77 /// Checks if the given address is a MWEB or SP (Silent Payment) address
78 bool _isMwebOrSpAddress(String address) {
79 if (address.isEmpty) return false;
80
81 final lowerAddress = address.toLowerCase();
82
83 // Check for MWEB addresses (Litecoin MWEB addresses start with "ltcmweb1")
84 if (lowerAddress.startsWith('ltcmweb1')) return true;
85
86 // Check for Silent Payment addresses (Bitcoin SP addresses start with "sp1", "tsp1")
87 if (lowerAddress.startsWith('sp1') || lowerAddress.startsWith('tsp1')) return true;
88
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(
102 builder: (_) {
103 final currencyName = walletTypeToString(paymentViewModel.detectedWalletType!);
104 final currentWalletName = isEVMCompatibleChain(paymentViewModel.currentWalletType)
105 ? evm!.getChainNameByChainId(paymentViewModel.currentChainId!)
106 : walletTypeToString(paymentViewModel.currentWalletType);
107
108 final hasSingleWallet = paymentFlowResult.type == PaymentFlowType.singleWallet ||
109 paymentFlowResult.wallets.length == 1;
110
111 final hasMultipleWallets = paymentFlowResult.type == PaymentFlowType.multipleWallets ||
112 paymentFlowResult.wallets.length > 1;
113
114 final noAvailableWallets = paymentFlowResult.type == PaymentFlowType.noWallets ||
115 paymentFlowResult.wallets.isEmpty;
116
117 final hasAtLeastOneWallet = hasSingleWallet || hasMultipleWallets;
118
119 final isMwebOrSpAddress =
120 _isMwebOrSpAddress(paymentFlowResult.addressDetectionResult?.address ?? '');
121
122 /// If the wallet is EVM but the detected chainId is different from the currently selected chainId
123 final isEVMWalletButDifferentChainId =
124 paymentFlowResult.type == PaymentFlowType.evmNetworkSelection &&
125 paymentFlowResult.wallet != null &&
126 isEVMCompatibleChain(paymentViewModel.currentWalletType);
127
128 final otherWalletsCount = paymentFlowResult.wallets.length;
129 final hasMultipleOtherWallets = otherWalletsCount > 1;
130
131 return Container(
132 padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
133 child: Column(
134 mainAxisSize: MainAxisSize.min,
135 crossAxisAlignment: CrossAxisAlignment.center,
136 children: [
137 if (_isTokenSelectionFlow(paymentFlowResult.type)) ...[
138 Stack(
139 clipBehavior: Clip.none,
140 children: [
141 Image.asset(
142 paymentFlowResult.addressDetectionResult?.detectedCurrency?.iconPath ?? '',
143 width: 70,
144 height: 70,
145 errorBuilder: (context, error, stackTrace) => Icon(
146 Icons.currency_bitcoin,
147 size: 70,
148 color: Theme.of(context).colorScheme.onSurface,
149 ),
150 ),
151 Positioned(
152 bottom: 0,
153 right: 0,
154 child: Image.asset(
155 getCryptoCurrencyIconForWalletListItem(
156 paymentViewModel.detectedWalletType!,
157 chainId: paymentViewModel.detectedChainId,
158 ),
159 width: 32,
160 height: 32,
161 errorBuilder: (context, error, stackTrace) => Icon(
162 Icons.account_balance_wallet,
163 size: 32,
164 color: Theme.of(context).colorScheme.onSurface,
165 ),
166 ),
167 ),
168 ],
169 ),
170 const SizedBox(height: 20),
171 Text(
172 '${paymentFlowResult.addressDetectionResult?.detectedCurrency} (${walletTypeToString(paymentFlowResult.walletType!)})',
173 style: Theme.of(context).textTheme.titleMedium!.copyWith(
174 fontSize: 20,
175 fontWeight: FontWeight.w900,
176 color: Theme.of(context).colorScheme.onSurface,
177 letterSpacing: 0.0,
178 ),
179 textAlign: TextAlign.center,
180 ),
181 const SizedBox(height: 24),
182 Text(
183 '''Would you like to ${!noAvailableWallets ? 'switch to a $currencyName wallet or' : ''} swap ${currentWalletName.capitalized()} for ${paymentFlowResult.addressDetectionResult?.detectedCurrency} (${walletTypeToString(paymentFlowResult.walletType!)}) for this payment?''',
184 textAlign: TextAlign.center,
185 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
186 fontSize: 16,
187 fontWeight: FontWeight.w600,
188 color: Theme.of(context).colorScheme.onSurfaceVariant,
189 letterSpacing: 0.0,
190 ),
191 ),
192 ] else ...[
193 Image.asset(
194 getCryptoCurrencyIconForWalletListItem(
195 paymentViewModel.detectedWalletType!,
196 chainId: paymentViewModel.detectedChainId,
197 ),
198 width: 118,
199 height: 118,
200 ),
201 const SizedBox(height: 20),
202 Text(
203 '$currencyName ${S.current.address_detected.toLowerCase()}',
204 style: Theme.of(context).textTheme.titleMedium!.copyWith(
205 fontSize: 20,
206 fontWeight: FontWeight.w900,
207 color: Theme.of(context).colorScheme.onSurface,
208 letterSpacing: 0.0,
209 ),
210 textAlign: TextAlign.center,
211 ),
212 const SizedBox(height: 24),
213 Text(
214 '''Looks like you scanned a $currencyName address.\n\n'''
215 '''Would you like to ${!noAvailableWallets ? 'switch to a $currencyName wallet or' : ''} swap ${currentWalletName.capitalized()} for $currencyName for this payment?''',
216 textAlign: TextAlign.center,
217 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
218 fontSize: 16,
219 fontWeight: FontWeight.w600,
220 color: Theme.of(context).colorScheme.onSurfaceVariant,
221 letterSpacing: 0.0,
222 ),
223 ),
224 ],
225 const SizedBox(height: 72),
226 if (isEVMWalletButDifferentChainId) ...[
227 if (!isMwebOrSpAddress) ...[
228 PrimaryButton(
229 onPressed: () => onSwap(context),
230 text: 'Create In App Swap',
231 color: hasAtLeastOneWallet
232 ? Theme.of(context).colorScheme.surfaceContainer
233 : Theme.of(context).colorScheme.primary,
234 textColor: hasAtLeastOneWallet
235 ? Theme.of(context).colorScheme.onSecondaryContainer
236 : Theme.of(context).colorScheme.onPrimary,
237 ),
238 const SizedBox(height: 10),
239 ],
240 if (onSwitchNetwork != null) ...[
241 PrimaryButton(
242 onPressed: onSwitchNetwork,
243 text:
244 'Switch to ${evm!.getChainNameByChainId(paymentViewModel.detectedChainId!).toUpperCase()} Network',
245 color: Theme.of(context).colorScheme.surfaceContainer,
246 textColor: Theme.of(context).colorScheme.onSecondaryContainer,
247 ),
248 const SizedBox(height: 10),
249 ],
250 if (hasAtLeastOneWallet) ...[
251 PrimaryButton(
252 onPressed: hasMultipleOtherWallets ? onSelectWallet : onChangeWallet,
253 text: S.current.change_wallet_alert_title,
254 color: Theme.of(context).colorScheme.primary,
255 textColor: Theme.of(context).colorScheme.onPrimary,
256 ),
257 const SizedBox(height: 10),
258 ],
259 ] else if (hasAtLeastOneWallet) ...[
260 if (!isMwebOrSpAddress) ...[
261 PrimaryButton(
262 onPressed: () => onSwap(context),
263 text: '${S.current.swap} ${currentWalletName.capitalized()}',
264 color: Theme.of(context).colorScheme.surfaceContainer,
265 textColor: Theme.of(context).colorScheme.onSecondaryContainer,
266 ),
267 const SizedBox(height: 10),
268 ],
269 PrimaryButton(
270 onPressed: hasMultipleWallets ? onSelectWallet : onChangeWallet,
271 text: S.current.switch_wallet,
272 color: Theme.of(context).colorScheme.primary,
273 textColor: Theme.of(context).colorScheme.onPrimary,
274 ),
275 const SizedBox(height: 10),
276 ] else ...[
277 PrimaryButton(
278 onPressed: () => Navigator.of(context).pop(),
279 text: S.current.cancel,
280 color: Theme.of(context).colorScheme.surfaceContainer,
281 textColor: Theme.of(context).colorScheme.onSecondaryContainer,
282 ),
283 const SizedBox(height: 10),
284 if (!isMwebOrSpAddress) ...[
285 PrimaryButton(
286 onPressed: () => onSwap(context),
287 text: '${S.current.swap} ${currentWalletName.capitalized()}',
288 color: hasAtLeastOneWallet
289 ? Theme.of(context).colorScheme.surfaceContainer
290 : Theme.of(context).colorScheme.primary,
291 textColor: hasAtLeastOneWallet
292 ? Theme.of(context).colorScheme.onSecondaryContainer
293 : Theme.of(context).colorScheme.onPrimary,
294 ),
295 const SizedBox(height: 10),
296 ],
297 const SizedBox(height: 32),
298 ],
299 ],
300 ),
301 );
302 },
303 );
304 }
305 }