Cw 973 in app gift card redemption flow plus UI (#2285)
* refactor: cleanup cake_pay folder * refactor: add My Cards tab on the main screen * ui: add user card item * refactor: cakePay purchase flow * feat: add user card info * refactor: auth flow * refactor: theme data * revert * Update qr_widget.dart * fix: update with new themes * minor fixes * feat: put redemption flow under a feature flag * feat: implement simulated purchasing flow in debug mode * feat: support LTC and MWEB payments * Update savings_page.dart * fix: UI fixes * add Cake Pay transaction sent bottom sheet * localization * Update cake_pay_buy_card_page.dart * addressing review comments * fix marge conflicts
Serhii committed
Jul 11, 2025 at 23:56 UTC
ef84db58746d5e744df06c0deca4e84011d15820
96 files changed
+3904
-2635
assets/images/envelope.png
Binary files /dev/null and b/assets/images/envelope.png differ
lib/cake_pay/cake_pay.dart
new
+5
@@ -0,0 +1,5 @@
1
+export 'src/auth/cake_pay_welcome_page.dart';
2
+export 'src/auth/cake_pay_verify_otp_page.dart';
3
+export 'src/auth/cake_pay_account_page.dart';
4
+export 'src/cards/cake_pay_cards_page.dart';
5
+export 'src/cards/cake_pay_buy_card_page.dart';
lib/cake_pay/cake_pay_payment_credantials.dart
deleted
-15
@@ -1,15 +0,0 @@
1
-class PaymentCredential {
2
- final double amount;
3
- final int quantity;
4
- final double totalAmount;
5
- final String? userName;
6
- final String fiatCurrency;
7
-
8
- PaymentCredential({
9
- required this.amount,
10
- required this.quantity,
11
- required this.totalAmount,
12
- required this.userName,
13
- required this.fiatCurrency,
14
- });
15
-}
\ No newline at end of file
lib/cake_pay/src/auth/cake_pay_account_page.dart
renamed
+1
-1
@@ -1,7 +1,7 @@
1
import 'package:cake_wallet/routes.dart';
2
import 'package:cake_wallet/src/screens/base_page.dart';
3
import 'package:cake_wallet/generated/i18n.dart';
4
-import 'package:cake_wallet/src/screens/cake_pay/widgets/cake_pay_tile.dart';
4
+import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_tile.dart';
5
import 'package:cake_wallet/src/widgets/primary_button.dart';
6
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
7
import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart';
lib/cake_pay/src/auth/cake_pay_verify_otp_page.dart
renamed
+5
-2
@@ -1,5 +1,6 @@
1
import 'package:cake_wallet/generated/i18n.dart';
2
-import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
2
+import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
3
+import 'package:cake_wallet/palette.dart';
4
import 'package:cake_wallet/src/screens/base_page.dart';
5
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
6
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
@@ -146,7 +147,9 @@ class CakePayVerifyOtpPage extends BasePage {
147
});
148
}
149
149
- void _onOtpSuccessful(BuildContext context) => Navigator.pop(context);
150
+
151
+ void _onOtpSuccessful(BuildContext context) =>
152
+ Navigator.pop(context, true);
153
154
void _verify() async => await _authViewModel.verifyEmail(_codeController.text);
155
}
lib/cake_pay/src/auth/cake_pay_welcome_page.dart
renamed
+9
-7
@@ -1,6 +1,6 @@
1
import 'package:cake_wallet/core/email_validator.dart';
2
import 'package:cake_wallet/generated/i18n.dart';
3
-import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
3
+import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
4
import 'package:cake_wallet/routes.dart';
5
import 'package:cake_wallet/src/screens/base_page.dart';
6
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
@@ -113,12 +113,14 @@ class CakePayWelcomePage extends BasePage {
113
});
114
}
115
116
- void _onLoginSuccessful(BuildContext context, CakePayAuthViewModel authViewModel) =>
117
- Navigator.pushReplacementNamed(
118
- context,
119
- Routes.cakePayVerifyOtpPage,
120
- arguments: [authViewModel.email, true],
121
- );
116
+ Future<void> _onLoginSuccessful(BuildContext context, CakePayAuthViewModel authViewModel) async {
117
+ final verified = await Navigator.pushNamed<bool>(context, Routes.cakePayVerifyOtpPage,
118
+ arguments: [authViewModel.email, true]);
119
+
120
+ if (verified == true) {
121
+ Navigator.pop(context, true);
122
+ }
123
+ }
124
125
void _login() async {
126
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
lib/cake_pay/src/cake_pay_states.dart
renamed
+10
-9
@@ -1,5 +1,3 @@
1
-import 'cake_pay_card.dart';
2
-
1
abstract class CakePayUserVerificationState {}
2
3
class CakePayUserVerificationStateInitial extends CakePayUserVerificationState {}
@@ -44,20 +42,23 @@ class CakePayCreateCardStateFailure extends CakePayCreateCardState {
42
final String error;
43
}
44
47
-class CakePayCardsState {}
45
+class UserCakePayCardsState {}
46
49
-class CakePayCardsStateNoCards extends CakePayCardsState {}
47
+class UserCakePayCardsStateInitial extends UserCakePayCardsState {}
48
51
-class CakePayCardsStateFetching extends CakePayCardsState {}
49
+class UserCakePayCardsStateNoCards extends UserCakePayCardsState {}
50
53
-class CakePayCardsStateFailure extends CakePayCardsState {}
51
+class UserCakePayCardsStateFetching extends UserCakePayCardsState {}
52
55
-class CakePayCardsStateSuccess extends CakePayCardsState {
56
- CakePayCardsStateSuccess({required this.card});
53
+class UserCakePayCardsStateFailure extends UserCakePayCardsState {
54
+ UserCakePayCardsStateFailure({required this.error});
55
58
- final CakePayCard card;
56
+ final String error;
57
}
58
59
+class UserCakePayCardsStateSuccess extends UserCakePayCardsState {}
60
+
61
+
62
abstract class CakePayVendorState {}
63
64
class InitialCakePayVendorLoadingState extends CakePayVendorState {}
lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
new
+773
@@ -0,0 +1,773 @@
1
+import 'dart:io';
2
+
3
+import 'package:cake_wallet/bitcoin/bitcoin.dart';
4
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
5
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
6
+import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
7
+import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
8
+import 'package:cake_wallet/cake_pay/src/widgets/denominations_amount_widget.dart';
9
+import 'package:cake_wallet/cake_pay/src/widgets/enter_amount_widget.dart';
10
+import 'package:cake_wallet/cake_pay/src/widgets/image_placeholder.dart';
11
+import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
12
+import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
13
+import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart';
14
+import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
15
+import 'package:cake_wallet/core/execution_state.dart';
16
+import 'package:cake_wallet/entities/parsed_address.dart';
17
+import 'package:cake_wallet/generated/i18n.dart';
18
+import 'package:cake_wallet/routes.dart';
19
+import 'package:cake_wallet/src/screens/base_page.dart';
20
+import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
21
+import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
22
+import 'package:cake_wallet/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart';
23
+import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
24
+import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
25
+import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
26
+import 'package:cake_wallet/src/widgets/primary_button.dart';
27
+import 'package:cake_wallet/typography.dart';
28
+import 'package:cake_wallet/utils/feature_flag.dart';
29
+import 'package:cake_wallet/utils/show_pop_up.dart';
30
+import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
31
+import 'package:cake_wallet/view_model/send/output.dart';
32
+import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
33
+import 'package:flutter/material.dart';
34
+import 'package:flutter/services.dart';
35
+import 'package:flutter_mobx/flutter_mobx.dart';
36
+import 'package:keyboard_actions/keyboard_actions.dart';
37
+import 'package:mobx/mobx.dart';
38
+
39
+class CakePayBuyCardPage extends BasePage {
40
+ CakePayBuyCardPage(
41
+ this.cakePayBuyCardViewModel,
42
+ this.cakePayService,
43
+ ) : _amountFieldFocus = FocusNode(),
44
+ _amountController = TextEditingController(),
45
+ _quantityFieldFocus = FocusNode(),
46
+ _quantityController =
47
+ TextEditingController(text: cakePayBuyCardViewModel.quantity.toString()) {
48
+ _amountController.addListener(() {
49
+ cakePayBuyCardViewModel.onAmountChanged(_amountController.text);
50
+ });
51
+ }
52
+
53
+ final CakePayBuyCardViewModel cakePayBuyCardViewModel;
54
+ final CakePayService cakePayService;
55
+
56
+ bool _effectsInstalled = false;
57
+ late final BuildContext _overlayCtx;
58
+
59
+ @override
60
+ String get title => cakePayBuyCardViewModel.card.name;
61
+
62
+ @override
63
+ bool get extendBodyBehindAppBar => true;
64
+
65
+ @override
66
+ bool get gradientAll => true;
67
+
68
+ @override
69
+ AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent;
70
+
71
+ @override
72
+ Widget? trailing(BuildContext context) {
73
+ return const SizedBox(
74
+ width: 54,
75
+ height: 0,
76
+ );
77
+ }
78
+
79
+ @override
80
+ Widget? middle(BuildContext context) {
81
+ return Text(
82
+ title,
83
+ textAlign: TextAlign.center,
84
+ maxLines: 2,
85
+ style: TextStyle(
86
+ fontSize: 18.0,
87
+ fontWeight: FontWeight.bold,
88
+ fontFamily: 'Lato',
89
+ color: titleColor(context)),
90
+ );
91
+ }
92
+
93
+ final TextEditingController _amountController;
94
+ final FocusNode _amountFieldFocus;
95
+ final TextEditingController _quantityController;
96
+ final FocusNode _quantityFieldFocus;
97
+
98
+ @override
99
+ Widget body(BuildContext context) {
100
+ _setEffects(context);
101
+
102
+ final card = cakePayBuyCardViewModel.card;
103
+ final vendor = cakePayBuyCardViewModel.vendor;
104
+
105
+ return KeyboardActions(
106
+ disableScroll: true,
107
+ config: KeyboardActionsConfig(
108
+ keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
109
+ keyboardBarColor: Theme.of(context).primaryColor,
110
+ nextFocus: false,
111
+ actions: [
112
+ KeyboardActionsItem(
113
+ focusNode: _amountFieldFocus,
114
+ toolbarButtons: [(_) => KeyboardDoneButton()],
115
+ ),
116
+ ]),
117
+ child: Container(
118
+ color: Colors.transparent,
119
+ child: Column(
120
+ children: [
121
+ RoundedOverlayCards(
122
+ topCardChild: Column(
123
+ children: [
124
+ Expanded(flex: 4, child: const SizedBox()),
125
+ Expanded(
126
+ flex: 7,
127
+ child: Container(
128
+ decoration: BoxDecoration(
129
+ borderRadius: BorderRadius.circular(10),
130
+ boxShadow: [
131
+ BoxShadow(
132
+ color: Colors.black.withAlpha(150),
133
+ blurRadius: 8,
134
+ offset: const Offset(0, 2))
135
+ ],
136
+ ),
137
+ child: ClipRRect(
138
+ borderRadius: BorderRadius.circular(10),
139
+ child: Image.network(
140
+ card.cardImageUrl ?? '',
141
+ fit: BoxFit.cover,
142
+ loadingBuilder: (BuildContext context, Widget child,
143
+ ImageChunkEvent? loadingProgress) {
144
+ if (loadingProgress == null) return child;
145
+ return Center(child: CircularProgressIndicator());
146
+ },
147
+ errorBuilder: (context, error, stackTrace) =>
148
+ CakePayCardImagePlaceholder(),
149
+ ),
150
+ ),
151
+ ),
152
+ ),
153
+ Expanded(child: const SizedBox()),
154
+ ],
155
+ ),
156
+ bottomCardChild: Padding(
157
+ padding: const EdgeInsets.symmetric(horizontal: 24),
158
+ child: card.denominations.isNotEmpty
159
+ ? DenominationsAmountWidget(
160
+ fiatCurrency: card.fiatCurrency.title,
161
+ denominations: card.denominations,
162
+ amountFieldFocus: _amountFieldFocus,
163
+ amountController: _amountController,
164
+ quantityFieldFocus: _quantityFieldFocus,
165
+ quantityController: _quantityController,
166
+ onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
167
+ onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
168
+ cakePayBuyCardViewModel: cakePayBuyCardViewModel)
169
+ : EnterAmountWidget(
170
+ minValue: card.minValue ?? '-',
171
+ maxValue: card.maxValue ?? '-',
172
+ fiatCurrency: card.fiatCurrency.title,
173
+ amountFieldFocus: _amountFieldFocus,
174
+ amountController: _amountController,
175
+ onAmountChanged: cakePayBuyCardViewModel.onAmountChanged))),
176
+ Expanded(
177
+ flex: 2,
178
+ child: Padding(
179
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
180
+ child: Column(
181
+ crossAxisAlignment: CrossAxisAlignment.start,
182
+ children: [
183
+ if (vendor.cakeWarnings != null)
184
+ Padding(
185
+ padding: const EdgeInsets.symmetric(vertical: 4),
186
+ child: Center(
187
+ child: Container(
188
+ decoration: BoxDecoration(
189
+ color: Theme.of(context).primaryColor,
190
+ borderRadius: BorderRadius.circular(10),
191
+ border: Border.all(color: Colors.white.withAlpha(50)),
192
+ ),
193
+ child: Padding(
194
+ padding: const EdgeInsets.all(8.0),
195
+ child: Text(
196
+ vendor.cakeWarnings!,
197
+ textAlign: TextAlign.center,
198
+ style: textSmallSemiBold(color: Colors.white),
199
+ ),
200
+ ),
201
+ ),
202
+ ),
203
+ ),
204
+ Expanded(
205
+ child: SingleChildScrollView(
206
+ primary: false,
207
+ padding: EdgeInsets.zero,
208
+ child: ClickableLinksText(
209
+ text: card.description ?? '',
210
+ textStyle: TextStyle(
211
+ color: Theme.of(context).textTheme.titleLarge!.color!,
212
+ fontSize: 14,
213
+ fontWeight: FontWeight.w400,
214
+ ),
215
+ ),
216
+ ),
217
+ ),
218
+ ],
219
+ ),
220
+ ),
221
+ ),
222
+ Expanded(
223
+ child: SingleChildScrollView(
224
+ primary: false,
225
+ child: Padding(
226
+ padding: const EdgeInsets.only(left: 24, right: 24),
227
+ child: Column(
228
+ children: [
229
+ if (card.expiryAndValidity != null && card.expiryAndValidity!.isNotEmpty)
230
+ Row(
231
+ children: [
232
+ Text(S.of(context).expiry_and_validity + ':',
233
+ style: TextStyle(
234
+ color: Theme.of(context).textTheme.titleLarge!.color!,
235
+ fontSize: 16,
236
+ fontWeight: FontWeight.w900)),
237
+ Expanded(
238
+ child: Text(card.expiryAndValidity!,
239
+ textAlign: TextAlign.center,
240
+ style: Theme.of(context).textTheme.labelMedium)),
241
+ ],
242
+ ),
243
+ SizedBox(height: 8),
244
+ TextIconButton(
245
+ label: S.of(context).how_to_use_card,
246
+ onTap: () => _showHowToUseCard(context, card)),
247
+ SizedBox(height: 8),
248
+ TextIconButton(
249
+ label: S.of(context).settings_terms_and_conditions,
250
+ onTap: () => _showTermsAndCondition(context, card.termsAndConditions)),
251
+ ],
252
+ ),
253
+ ),
254
+ ),
255
+ ),
256
+ SizedBox(height: 8),
257
+ Observer(builder: (_) {
258
+ Widget _buildPaymentMethodWidget(
259
+ List<CakePayPaymentMethod> methods, CakePayPaymentMethod selected) {
260
+ return Row(
261
+ children: [
262
+ Padding(
263
+ padding: const EdgeInsets.only(left: 24, right: 8),
264
+ child: Text(
265
+ 'Payment Method',
266
+ style: TextStyle(
267
+ color: Theme.of(context).textTheme.titleLarge!.color!,
268
+ fontSize: 16,
269
+ fontWeight: FontWeight.w900,
270
+ ),
271
+ ),
272
+ ),
273
+ Expanded(child: const SizedBox()),
274
+ if (methods.length > 1)
275
+ Padding(
276
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
277
+ child: ToggleButtons(
278
+ isSelected: methods.map((m) => m == selected).toList(),
279
+ borderRadius: BorderRadius.circular(8),
280
+ onPressed: (index) =>
281
+ cakePayBuyCardViewModel.chooseMethod(methods[index]),
282
+ children: methods
283
+ .map((m) => Padding(
284
+ padding:
285
+ const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
286
+ child: Text(m.label),
287
+ ))
288
+ .toList(),
289
+ ),
290
+ ),
291
+ ],
292
+ );
293
+ }
294
+
295
+ final methods = cakePayBuyCardViewModel.availableMethods;
296
+ final selected = cakePayBuyCardViewModel.selectedPaymentMethod ??
297
+ (methods.isNotEmpty ? methods.first : null);
298
+
299
+ return Column(
300
+ children: [
301
+ methods.length <= 1 || selected == null
302
+ ? const SizedBox.shrink()
303
+ : _buildPaymentMethodWidget(methods, selected),
304
+ if (FeatureFlag.hasDevOptions)
305
+ Padding(
306
+ padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
307
+ child: LoadingPrimaryButton(
308
+ onPressed: () {
309
+ //Request dummy node to get the focus out of the text fields
310
+ FocusScope.of(context).requestFocus(FocusNode());
311
+
312
+ cakePayBuyCardViewModel.isSimulatingFlow = true;
313
+ isIOSUnavailable(card)
314
+ ? alertIOSAvailability(context, card)
315
+ : confirmPurchaseFirst(context);
316
+ },
317
+ text: '(Dev) Simulate Purchasing Gift Card',
318
+ isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
319
+ cakePayBuyCardViewModel.isPurchasing,
320
+ isLoading:
321
+ cakePayBuyCardViewModel.sendViewModel.state is IsExecutingState ||
322
+ cakePayBuyCardViewModel.isPurchasing,
323
+ color: Theme.of(context).colorScheme.primary,
324
+ textColor: Theme.of(context).colorScheme.onPrimary,
325
+ ),
326
+ ),
327
+ Padding(
328
+ padding: EdgeInsets.only(top: 10, bottom: 34, right: 20, left: 20),
329
+ child: LoadingPrimaryButton(
330
+ onPressed: () {
331
+ //Request dummy node to get the focus out of the text fields
332
+ FocusScope.of(context).requestFocus(FocusNode());
333
+
334
+ isIOSUnavailable(card)
335
+ ? alertIOSAvailability(context, card)
336
+ : confirmPurchaseFirst(context);
337
+ },
338
+ text: S.of(context).purchase_gift_card,
339
+ isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
340
+ cakePayBuyCardViewModel.isPurchasing,
341
+ isLoading: cakePayBuyCardViewModel.sendViewModel.state is IsExecutingState ||
342
+ cakePayBuyCardViewModel.isPurchasing,
343
+ color: Theme.of(context).colorScheme.primary,
344
+ textColor: Theme.of(context).colorScheme.onPrimary,
345
+ ),
346
+ ),
347
+ ],
348
+ );
349
+ }),
350
+ ],
351
+ ),
352
+ ),
353
+ );
354
+ }
355
+
356
+ bool isWordInCardsName(CakePayCard card, String word) {
357
+ return card.name.toLowerCase().contains(word.toLowerCase());
358
+ }
359
+
360
+ bool isIOSUnavailable(CakePayCard card) {
361
+ if (!Platform.isIOS && !Platform.isMacOS) {
362
+ return false;
363
+ }
364
+
365
+ final isDigitalGameStores = isWordInCardsName(card, 'playstation') ||
366
+ isWordInCardsName(card, 'xbox') ||
367
+ isWordInCardsName(card, 'steam') ||
368
+ isWordInCardsName(card, 'meta quest') ||
369
+ isWordInCardsName(card, 'kigso') ||
370
+ isWordInCardsName(card, 'game world') ||
371
+ isWordInCardsName(card, 'google') ||
372
+ isWordInCardsName(card, 'nintendo');
373
+ final isGCodes = isWordInCardsName(card, 'gcodes');
374
+ final isApple = isWordInCardsName(card, 'itunes') || isWordInCardsName(card, 'apple');
375
+ final isTidal = isWordInCardsName(card, 'tidal');
376
+ final isVPNServices = isWordInCardsName(card, 'nordvpn') ||
377
+ isWordInCardsName(card, 'expressvpn') ||
378
+ isWordInCardsName(card, 'surfshark') ||
379
+ isWordInCardsName(card, 'proton');
380
+ final isStreamingServices = isWordInCardsName(card, 'netflix') ||
381
+ isWordInCardsName(card, 'spotify') ||
382
+ isWordInCardsName(card, 'hulu') ||
383
+ isWordInCardsName(card, 'hbo') ||
384
+ isWordInCardsName(card, 'soundcloud') ||
385
+ isWordInCardsName(card, 'twitch');
386
+ final isDatingServices = isWordInCardsName(card, 'tinder');
387
+
388
+ return isDigitalGameStores ||
389
+ isGCodes ||
390
+ isApple ||
391
+ isTidal ||
392
+ isVPNServices ||
393
+ isStreamingServices ||
394
+ isDatingServices;
395
+ }
396
+
397
+ Future<void> alertIOSAvailability(BuildContext context, CakePayCard card) async {
398
+ cakePayBuyCardViewModel.isSimulatingFlow = false;
399
+ return await showPopUp<void>(
400
+ context: context,
401
+ builder: (BuildContext context) {
402
+ return AlertWithOneAction(
403
+ alertTitle: S.of(context).error,
404
+ alertContent: S.of(context).cakepay_ios_not_available,
405
+ buttonText: S.of(context).ok,
406
+ buttonAction: () {
407
+ // _walletHardwareRestoreVM.error = null;
408
+ Navigator.of(context).pop();
409
+ });
410
+ });
411
+ }
412
+
413
+ Future<void> confirmPurchaseFirst(BuildContext context) async {
414
+ bool isLogged = await cakePayBuyCardViewModel.cakePayService.isLogged();
415
+ if (!isLogged) {
416
+ cakePayBuyCardViewModel.isSimulatingFlow = false;
417
+ Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
418
+ } else {
419
+ cakePayBuyCardViewModel.isPurchasing = true;
420
+ await _showconfirmPurchaseFirstAlert(context);
421
+ }
422
+ }
423
+
424
+ Future<void> _showconfirmPurchaseFirstAlert(BuildContext context) async {
425
+ if (!cakePayBuyCardViewModel.confirmsNoVpn ||
426
+ !cakePayBuyCardViewModel.confirmsVoidedRefund ||
427
+ !cakePayBuyCardViewModel.confirmsTermsAgreed) {
428
+ await showPopUp<void>(
429
+ context: context,
430
+ builder: (BuildContext context) => ThreeCheckboxAlert(
431
+ alertTitle: S.of(context).cakepay_confirm_purchase,
432
+ leftButtonText: S.of(context).cancel,
433
+ rightButtonText: S.of(context).confirm,
434
+ actionLeftButton: () {
435
+ cakePayBuyCardViewModel.isPurchasing = false;
436
+ cakePayBuyCardViewModel.isSimulatingFlow = false;
437
+ Navigator.of(context).pop();
438
+ },
439
+ actionRightButton: (confirmsNoVpn, confirmsVoidedRefund, confirmsTermsAgreed) {
440
+ cakePayBuyCardViewModel.confirmsNoVpn = confirmsNoVpn;
441
+ cakePayBuyCardViewModel.confirmsVoidedRefund = confirmsVoidedRefund;
442
+ cakePayBuyCardViewModel.confirmsTermsAgreed = confirmsTermsAgreed;
443
+
444
+ Navigator.of(context).pop();
445
+ },
446
+ ),
447
+ );
448
+ }
449
+
450
+ if (cakePayBuyCardViewModel.confirmsNoVpn &&
451
+ cakePayBuyCardViewModel.confirmsVoidedRefund &&
452
+ cakePayBuyCardViewModel.confirmsTermsAgreed) {
453
+ await purchaseCard(context);
454
+ } else {
455
+ cakePayBuyCardViewModel.isPurchasing = false;
456
+ cakePayBuyCardViewModel.isSimulatingFlow = false;
457
+ }
458
+ }
459
+
460
+ Future<void> purchaseCard(BuildContext context) async {
461
+ bool isLogged = await cakePayBuyCardViewModel.cakePayService.isLogged();
462
+ if (!isLogged) {
463
+ cakePayBuyCardViewModel.isSimulatingFlow = false;
464
+ Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
465
+ } else {
466
+ try {
467
+ await cakePayBuyCardViewModel.createOrder();
468
+ } catch (_) {
469
+ cakePayBuyCardViewModel.isSimulatingFlow = false;
470
+ await cakePayBuyCardViewModel.cakePayService.logout();
471
+ }
472
+ }
473
+ cakePayBuyCardViewModel.isPurchasing = false;
474
+ }
475
+
476
+ BuildContext? dialogContext;
477
+ BuildContext? loadingBottomSheetContext;
478
+ BuildContext? confirmBottomSheetContext;
479
+
480
+ void _setEffects(BuildContext context) {
481
+ if (_effectsInstalled) {
482
+ return;
483
+ }
484
+
485
+ _overlayCtx = Navigator.of(context).context;
486
+
487
+ if (cakePayBuyCardViewModel.sendViewModel.isElectrumWallet) {
488
+ bitcoin!.updateFeeRates(cakePayBuyCardViewModel.sendViewModel.wallet);
489
+ }
490
+
491
+ reaction((_) => cakePayBuyCardViewModel.sendViewModel.state, (ExecutionState state) async {
492
+ if (dialogContext != null && dialogContext!.mounted) Navigator.of(dialogContext!).pop();
493
+
494
+ if (confirmBottomSheetContext != null && confirmBottomSheetContext!.mounted) {
495
+ Navigator.of(confirmBottomSheetContext!).pop();
496
+ }
497
+
498
+ if (state is! IsExecutingState &&
499
+ loadingBottomSheetContext != null &&
500
+ loadingBottomSheetContext!.mounted) {
501
+ Navigator.of(loadingBottomSheetContext!).pop();
502
+ }
503
+
504
+ if (state is FailureState) {
505
+ WidgetsBinding.instance.addPostFrameCallback((_) {
506
+ if (context.mounted)
507
+ showPopUp<void>(
508
+ context: context,
509
+ builder: (BuildContext context) {
510
+ return AlertWithOneAction(
511
+ key: ValueKey('cake_pay_buy_page_send_failure_dialog_key'),
512
+ buttonKey: ValueKey('cake_pay_buy_page_send_failure_dialog_button_key'),
513
+ alertTitle: S.of(context).error,
514
+ alertContent: state.error,
515
+ buttonText: S.of(context).ok,
516
+ buttonAction: () => Navigator.of(context).pop());
517
+ });
518
+ });
519
+ }
520
+
521
+ if (state is IsExecutingState) {
522
+ // wait a bit to avoid showing the loading dialog if transaction is failed
523
+ await Future.delayed(const Duration(milliseconds: 300));
524
+ final currentState = cakePayBuyCardViewModel.sendViewModel.state;
525
+ if (currentState is ExecutedSuccessfullyState || currentState is FailureState) {
526
+ return;
527
+ }
528
+
529
+ WidgetsBinding.instance.addPostFrameCallback((_) {
530
+ if (context.mounted) {
531
+ showModalBottomSheet<void>(
532
+ context: context,
533
+ isDismissible: false,
534
+ builder: (BuildContext context) {
535
+ loadingBottomSheetContext = context;
536
+ return LoadingBottomSheet(
537
+ titleText: S.of(context).generating_transaction,
538
+ );
539
+ },
540
+ );
541
+ }
542
+ });
543
+ }
544
+
545
+ if (state is ExecutedSuccessfullyState) {
546
+ if (cakePayBuyCardViewModel.order == null) return;
547
+
548
+ ReactionDisposer? disposer;
549
+
550
+ disposer = reaction((_) => cakePayBuyCardViewModel.isOrderExpired, (bool isExpired) {
551
+ if (isExpired) {
552
+ cakePayBuyCardViewModel.sendViewModel.state = FailureState('Order expired');
553
+ disposer?.call();
554
+ }
555
+ });
556
+
557
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
558
+ if (context.mounted) {
559
+ final order = cakePayBuyCardViewModel.order;
560
+
561
+ final displayingOutputs = cakePayBuyCardViewModel.sendViewModel.outputs
562
+ .map((o) => o.OutputCopyWithParsedAddress(
563
+ parsedAddress: ParsedAddress(
564
+ addresses: [o.address],
565
+ name: 'Cake Pay',
566
+ profileName: order?.cards.first.cardName ?? 'Cake Pay',
567
+ profileImageUrl: order?.cards.first.cardImagePath ?? '',
568
+ ),
569
+ fiatAmount: '${order?.amountUsd.toString()} USD',
570
+ ))
571
+ .toList();
572
+
573
+ final result = await showModalBottomSheet<bool>(
574
+ context: context,
575
+ isDismissible: false,
576
+ isScrollControlled: true,
577
+ builder: (BuildContext bottomSheetContext) {
578
+ confirmBottomSheetContext = bottomSheetContext;
579
+ return ConfirmSendingBottomSheet(
580
+ key: ValueKey('cake_pay_buy_page_confirm_sending_dialog_key'),
581
+ titleText: S.of(bottomSheetContext).confirm_transaction,
582
+ currentTheme: currentTheme,
583
+ cakePayBuyCardViewModel: cakePayBuyCardViewModel,
584
+ paymentId: S.of(bottomSheetContext).payment_id,
585
+ paymentIdValue: cakePayBuyCardViewModel.order?.orderId,
586
+ expirationTime: cakePayBuyCardViewModel.formattedRemainingTime,
587
+ walletType: cakePayBuyCardViewModel.sendViewModel.walletType,
588
+ titleIconPath:
589
+ cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
590
+ currency: cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency,
591
+ amount: S.of(bottomSheetContext).send_amount,
592
+ amountValue:
593
+ cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.amountFormatted,
594
+ quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
595
+ fiatAmountValue:
596
+ cakePayBuyCardViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
597
+ fee: S.of(bottomSheetContext).send_fee,
598
+ feeValue: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.feeFormatted,
599
+ feeFiatAmount: cakePayBuyCardViewModel
600
+ .sendViewModel.pendingTransactionFeeFiatAmountFormatted,
601
+ outputs: displayingOutputs,
602
+ footerType: FooterType.slideActionButton,
603
+ slideActionButtonText:
604
+ cakePayBuyCardViewModel.isSimulating ? 'Swipe to simulate' : 'Swipe to send',
605
+ accessibleNavigationModeSlideActionButtonText:
606
+ cakePayBuyCardViewModel.isSimulating ? 'Simulate' : S.of(context).send,
607
+ onSlideActionComplete: () async {
608
+ Navigator.of(bottomSheetContext).pop(true);
609
+ cakePayBuyCardViewModel.isSimulating
610
+ ? cakePayBuyCardViewModel.simulatePayment()
611
+ : cakePayBuyCardViewModel.sendViewModel.commitTransaction(context);
612
+ },
613
+ change: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.change,
614
+ isOpenCryptoPay: cakePayBuyCardViewModel.sendViewModel.ocpRequest != null,
615
+ );
616
+ },
617
+ );
618
+
619
+ confirmBottomSheetContext = null;
620
+ cakePayBuyCardViewModel.isSimulatingFlow = false;
621
+ _handleDispose(disposer);
622
+ if (result == null) cakePayBuyCardViewModel.sendViewModel.dismissTransaction();
623
+ }
624
+ });
625
+ }
626
+
627
+ if (state is TransactionCommitted) {
628
+ final order = cakePayBuyCardViewModel.order;
629
+ final outputsCopy = List<Output>.from(cakePayBuyCardViewModel.sendViewModel.outputs);
630
+
631
+ final displayingOutputs = outputsCopy
632
+ .map((o) => o.OutputCopyWithParsedAddress(
633
+ parsedAddress: ParsedAddress(
634
+ addresses: [o.address],
635
+ name: 'Cake Pay',
636
+ profileName: order?.cards.first.cardName ?? 'Cake Pay',
637
+ profileImageUrl: order?.cards.first.cardImagePath ?? '',
638
+ ),
639
+ fiatAmount: '${order?.amountUsd ?? 0} USD',
640
+ ))
641
+ .toList();
642
+
643
+ cakePayBuyCardViewModel.sendViewModel.clearOutputs();
644
+
645
+ final bool usePageContextLater = context.mounted;
646
+
647
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
648
+ if (displayingOutputs.isEmpty) {
649
+ if (context.mounted) Navigator.of(context).pop();
650
+ return;
651
+ }
652
+
653
+ final BuildContext sheetParentCtx = usePageContextLater ? context : _overlayCtx;
654
+
655
+ await showModalBottomSheet<void>(
656
+ context: sheetParentCtx,
657
+ useRootNavigator: !usePageContextLater,
658
+ isScrollControlled: true,
659
+ isDismissible: true,
660
+ backgroundColor: Colors.transparent,
661
+ builder: (sheetCtx) {
662
+ return CakePayTransactionSentBottomSheet(
663
+ key: const ValueKey('cake_pay_buy_page_transaction_sent_bottom_sheet_key'),
664
+ titleText: S.of(sheetCtx).transaction_sent,
665
+ titleIconWidget: const CircleAvatar(
666
+ radius: 10,
667
+ backgroundColor: Colors.green,
668
+ child: Icon(Icons.check, size: 16, color: Colors.white),
669
+ ),
670
+ output: displayingOutputs.first,
671
+ currency: cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency,
672
+ amount: S.of(sheetCtx).send_amount,
673
+ amountValue:
674
+ cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.amountFormatted,
675
+ quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
676
+ fiatAmountValue:
677
+ cakePayBuyCardViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
678
+ fee: S.of(sheetCtx).send_fee,
679
+ feeValue: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.feeFormatted,
680
+ feeFiatAmount:
681
+ cakePayBuyCardViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
682
+ paymentId: 'Order ID',
683
+ paymentIdValue: order?.orderId ?? '',
684
+ onClose: () {
685
+ Navigator.of(sheetCtx).pop();
686
+ },
687
+ );
688
+ },
689
+ );
690
+ if (context.mounted) {
691
+ Navigator.of(context).pop();
692
+ }
693
+ });
694
+ }
695
+
696
+ if (state is IsAwaitingDeviceResponseState) {
697
+ WidgetsBinding.instance.addPostFrameCallback((_) {
698
+ if (!context.mounted) return;
699
+
700
+ showModalBottomSheet<void>(
701
+ context: context,
702
+ isDismissible: false,
703
+ builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
704
+ currentTheme: currentTheme,
705
+ footerType: FooterType.singleActionButton,
706
+ titleText: S.of(bottomSheetContext).proceed_on_device,
707
+ contentImage: 'assets/images/hardware_wallet/ledger_nano_x.png',
708
+ contentImageColor: Theme.of(context).textTheme.titleLarge!.color!,
709
+ content: S.of(bottomSheetContext).proceed_on_device_description,
710
+ singleActionButtonText: S.of(context).cancel,
711
+ onSingleActionButtonPressed: () {
712
+ cakePayBuyCardViewModel.sendViewModel.state = InitialExecutionState();
713
+ Navigator.of(bottomSheetContext).pop();
714
+ },
715
+ ),
716
+ );
717
+ });
718
+ }
719
+ });
720
+
721
+ _effectsInstalled = true;
722
+ }
723
+
724
+ void _handleDispose(ReactionDisposer? disposer) {
725
+ cakePayBuyCardViewModel.dispose();
726
+ if (disposer != null) {
727
+ disposer();
728
+ }
729
+ }
730
+}
731
+
732
+void _showHowToUseCard(BuildContext context, CakePayCard card) {
733
+ showPopUp<void>(
734
+ context: context,
735
+ builder: (BuildContext context) {
736
+ return CakePayAlertModal(
737
+ title: S.of(context).how_to_use_card,
738
+ dismissible: true,
739
+ content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
740
+ Padding(
741
+ padding: EdgeInsets.all(10),
742
+ child: Text(card.name, style: Theme.of(context).textTheme.headlineSmall)),
743
+ ClickableLinksText(
744
+ text: card.howToUse ?? '',
745
+ textStyle: Theme.of(context).textTheme.bodyMedium!,
746
+ linkStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
747
+ color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic))
748
+ ]),
749
+ actionTitle: S.current.got_it,
750
+ showCloseButton: false,
751
+ );
752
+ });
753
+}
754
+
755
+void _showTermsAndCondition(BuildContext context, String? termsAndConditions) {
756
+ showPopUp<void>(
757
+ context: context,
758
+ builder: (BuildContext context) {
759
+ return CakePayAlertModal(
760
+ title: S.of(context).settings_terms_and_conditions,
761
+ dismissible: true,
762
+ content: Align(
763
+ alignment: Alignment.bottomLeft,
764
+ child: ClickableLinksText(
765
+ text: termsAndConditions ?? '',
766
+ textStyle: Theme.of(context).textTheme.bodyMedium!,
767
+ linkStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
768
+ color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic))),
769
+ actionTitle: S.of(context).agree,
770
+ showCloseButton: false,
771
+ );
772
+ });
773
+}
lib/cake_pay/src/cards/cake_pay_cards_page.dart
new
+521
@@ -0,0 +1,521 @@
1
+import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
2
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
3
+import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_search_bar_widget.dart';
4
+import 'package:cake_wallet/cake_pay/src/widgets/user_card_item.dart';
5
+import 'package:cake_wallet/entities/country.dart';
6
+import 'package:cake_wallet/generated/i18n.dart';
7
+import 'package:cake_wallet/routes.dart';
8
+import 'package:cake_wallet/src/screens/base_page.dart';
9
+import 'package:cake_wallet/cake_pay/src/widgets/card_item.dart';
10
+import 'package:cake_wallet/src/screens/dashboard/widgets/filter_widget.dart';
11
+import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
12
+import 'package:cake_wallet/src/widgets/bottom_sheet/cake_pay_card_info_bottom_sheet_widget.dart';
13
+import 'package:cake_wallet/src/widgets/cake_scrollbar.dart';
14
+import 'package:cake_wallet/src/widgets/gradient_background.dart';
15
+import 'package:cake_wallet/src/widgets/picker.dart';
16
+import 'package:cake_wallet/src/widgets/tab_view_wrapper_widget.dart';
17
+import 'package:cake_wallet/themes/core/material_base_theme.dart';
18
+import 'package:cake_wallet/typography.dart';
19
+import 'package:cake_wallet/utils/debounce.dart';
20
+import 'package:cake_wallet/utils/feature_flag.dart';
21
+import 'package:cake_wallet/utils/responsive_layout_util.dart';
22
+import 'package:cake_wallet/utils/show_pop_up.dart';
23
+import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
24
+import 'package:flutter/material.dart';
25
+import 'package:flutter_mobx/flutter_mobx.dart';
26
+import 'package:mobx/mobx.dart';
27
+
28
+class CakePayCardsPage extends BasePage {
29
+ CakePayCardsPage(this._cardsListViewModel);
30
+
31
+ final CakePayCardsListViewModel _cardsListViewModel;
32
+
33
+ @override
34
+ bool get gradientBackground => true;
35
+
36
+ @override
37
+ Widget Function(BuildContext, Widget) get rootWrapper =>
38
+ (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
39
+
40
+ @override
41
+ bool get resizeToAvoidBottomInset => false;
42
+
43
+ @override
44
+ Widget middle(BuildContext context) {
45
+ return Text(
46
+ 'Cake Pay',
47
+ style: textMediumSemiBold(
48
+ color: titleColor(context),
49
+ ),
50
+ );
51
+ }
52
+
53
+ @override
54
+ Widget trailing(BuildContext context) {
55
+ return Observer(builder: (_) {
56
+ final loggedIn = _cardsListViewModel.isUserAuthenticated == true;
57
+
58
+ Future<void> _handleOnPressed() async {
59
+ if (loggedIn) {
60
+ Navigator.pushNamed(context, Routes.cakePayAccountPage);
61
+ return;
62
+ }
63
+ final success = await Navigator.pushNamed<bool>(context, Routes.cakePayWelcomePage);
64
+
65
+ if (success == true) await _cardsListViewModel.checkAuth();
66
+ }
67
+
68
+ if (!loggedIn || _cardsListViewModel.username == null) {
69
+ return _TrailingIcon(
70
+ asset: 'assets/images/profile.png',
71
+ iconColor: pageIconColor(context) ?? Colors.white,
72
+ onPressed: () async => await _handleOnPressed(),
73
+ );
74
+ }
75
+ final letter = _cardsListViewModel.username!.trim()[0].toUpperCase();
76
+ return IconButton(
77
+ padding: EdgeInsets.zero,
78
+ iconSize: 25,
79
+ onPressed: _handleOnPressed,
80
+ icon: CircleAvatar(
81
+ radius: 12,
82
+ backgroundColor: Theme.of(context).colorScheme.primary,
83
+ child: Text(
84
+ letter,
85
+ style: TextStyle(
86
+ color: Theme.of(context).colorScheme.onPrimary,
87
+ fontWeight: FontWeight.w600,
88
+ ),
89
+ ),
90
+ ),
91
+ );
92
+ });
93
+ }
94
+
95
+ @override
96
+ Widget body(BuildContext context) {
97
+ return CakePayCardsPageBody(
98
+ cardsListViewModel: _cardsListViewModel,
99
+ currentTheme: currentTheme,
100
+ titleColor: titleColor);
101
+ }
102
+}
103
+
104
+class CakePayCardsPageBody extends StatefulWidget {
105
+ const CakePayCardsPageBody({
106
+ super.key,
107
+ required CakePayCardsListViewModel cardsListViewModel,
108
+ required this.currentTheme,
109
+ required this.titleColor,
110
+ }) : _cardsListViewModel = cardsListViewModel;
111
+
112
+ final CakePayCardsListViewModel _cardsListViewModel;
113
+ final MaterialThemeBase currentTheme;
114
+ final Color? Function(BuildContext) titleColor;
115
+
116
+ @override
117
+ State<CakePayCardsPageBody> createState() => _CakePayCardsPageBodyState();
118
+}
119
+
120
+class _CakePayCardsPageBodyState extends State<CakePayCardsPageBody> {
121
+ ReactionDisposer? _countryPickerDisposer;
122
+
123
+ @override
124
+ void initState() {
125
+ super.initState();
126
+ final viewModel = widget._cardsListViewModel;
127
+
128
+ _countryPickerDisposer = when(
129
+ (_) => viewModel.shouldShowCountryPicker,
130
+ () async {
131
+ viewModel.storeInitialFilterStates();
132
+
133
+ WidgetsBinding.instance.addPostFrameCallback(
134
+ (_) async {
135
+ await showCountryPicker(context, viewModel);
136
+ if (viewModel.hasFiltersChanged) {
137
+ viewModel.resetLoadingNextPageState();
138
+ viewModel.getVendors();
139
+ }
140
+ viewModel.settingsStore.selectedCakePayCountry = viewModel.selectedCountry;
141
+ },
142
+ );
143
+ },
144
+ );
145
+ }
146
+
147
+ @override
148
+ void dispose() {
149
+ _countryPickerDisposer?.call();
150
+ super.dispose();
151
+ }
152
+
153
+ @override
154
+ Widget build(BuildContext context) {
155
+ return Observer(builder: (_) {
156
+ final isUserAuthenticated = widget._cardsListViewModel.isUserAuthenticated;
157
+
158
+ if (isUserAuthenticated == null) return const _Loading();
159
+
160
+ if (isUserAuthenticated == false || !FeatureFlag.isCakePayRedemptionFlowEnabled) {
161
+ return Padding(
162
+ padding: const EdgeInsets.symmetric(horizontal: 14),
163
+ child: _ShopTab(cardsListViewModel: widget._cardsListViewModel),
164
+ );
165
+ }
166
+
167
+ final titleColor = widget.titleColor(context);
168
+
169
+ return Padding(
170
+ padding: const EdgeInsets.symmetric(horizontal: 14),
171
+ child: Column(children: [
172
+ Expanded(
173
+ child: TabViewWrapper(
174
+ labelStyle: TextStyle(
175
+ color: titleColor,
176
+ fontFamily: 'Lato',
177
+ fontSize: 20,
178
+ fontWeight: FontWeight.w600),
179
+ unselectedLabelStyle: TextStyle(
180
+ color: titleColor?.withAlpha(150) ?? Colors.white70,
181
+ fontFamily: 'Lato',
182
+ fontSize: 20,
183
+ fontWeight: FontWeight.w400),
184
+ indicatorColor: titleColor,
185
+ tabs: const [
186
+ Tab(text: 'My Cards'),
187
+ Tab(text: 'Shop')
188
+ ],
189
+ views: [
190
+ _MyCardsTab(
191
+ cardsListViewModel: widget._cardsListViewModel,
192
+ currentTheme: widget.currentTheme),
193
+ _ShopTab(cardsListViewModel: widget._cardsListViewModel)
194
+ ]),
195
+ )
196
+ ]));
197
+ });
198
+ }
199
+}
200
+
201
+Future<void> showFilterWidget(
202
+ BuildContext context, CakePayCardsListViewModel cardsListViewModel) async {
203
+ return showPopUp<void>(
204
+ context: context,
205
+ builder: (BuildContext context) {
206
+ return FilterWidget(filterItems: cardsListViewModel.createFilterItems);
207
+ },
208
+ );
209
+}
210
+
211
+Future<void> showCountryPicker(
212
+ BuildContext context, CakePayCardsListViewModel cardsListViewModel) async {
213
+ await showPopUp<void>(
214
+ context: context,
215
+ builder: (_) => Picker(
216
+ title: S.of(context).select_your_country,
217
+ items: cardsListViewModel.availableCountries,
218
+ images: cardsListViewModel.availableCountries
219
+ .map((e) => Image.asset(
220
+ e.iconPath,
221
+ errorBuilder: (context, error, stackTrace) => Container(
222
+ width: 58,
223
+ height: 58,
224
+ ),
225
+ ))
226
+ .toList(),
227
+ selectedAtIndex:
228
+ cardsListViewModel.availableCountries.indexOf(cardsListViewModel.selectedCountry),
229
+ onItemSelected: (Country country) => cardsListViewModel.setSelectedCountry(country),
230
+ isSeparated: false,
231
+ hintText: S.of(context).search,
232
+ matchingCriteria: (Country country, String searchText) =>
233
+ country.fullName.toLowerCase().contains(searchText.toLowerCase())));
234
+}
235
+
236
+class _TrailingIcon extends StatelessWidget {
237
+ const _TrailingIcon({required this.asset, this.onPressed, required this.iconColor});
238
+
239
+ final String asset;
240
+ final VoidCallback? onPressed;
241
+ final Color iconColor;
242
+
243
+ @override
244
+ Widget build(BuildContext context) {
245
+ return Semantics(
246
+ label: S.of(context).profile,
247
+ child: Material(
248
+ color: Colors.transparent,
249
+ child: IconButton(
250
+ padding: EdgeInsets.zero,
251
+ constraints: BoxConstraints(),
252
+ highlightColor: Colors.transparent,
253
+ onPressed: onPressed,
254
+ icon: ImageIcon(AssetImage(asset), size: 25, color: iconColor),
255
+ ),
256
+ ));
257
+ }
258
+}
259
+
260
+class _MyCardsTab extends StatefulWidget {
261
+ const _MyCardsTab({required this.cardsListViewModel, required this.currentTheme});
262
+
263
+ final CakePayCardsListViewModel cardsListViewModel;
264
+ final MaterialThemeBase currentTheme;
265
+
266
+ @override
267
+ State<_MyCardsTab> createState() => _MyCardsTabState();
268
+}
269
+
270
+class _MyCardsTabState extends State<_MyCardsTab> {
271
+ late final TextEditingController _searchController;
272
+
273
+ @override
274
+ void initState() {
275
+ super.initState();
276
+
277
+ _searchController = TextEditingController(
278
+ text: widget.cardsListViewModel.searchMyCardsString,
279
+ );
280
+ }
281
+
282
+ @override
283
+ void dispose() {
284
+ super.dispose();
285
+ }
286
+
287
+ @override
288
+ Widget build(BuildContext context) {
289
+ final viewModel = widget.cardsListViewModel;
290
+ return Observer(builder: (_) {
291
+ return Column(
292
+ children: [
293
+ Padding(
294
+ padding: const EdgeInsets.fromLTRB(2, 6, 0, 6),
295
+ child: CakePaySearchBar(
296
+ initialQuery: viewModel.searchMyCardsString,
297
+ controller: _searchController,
298
+ onSearch: viewModel.setMyCardsQuery,
299
+ onFilter: () async {}, // TODO: implement filter
300
+ )),
301
+ Expanded(
302
+ child: Observer(builder: (_) {
303
+ final cards = viewModel.filteredUserCards;
304
+ if (viewModel.userCardState is UserCakePayCardsStateFetching) return const _Loading();
305
+ if (viewModel.userCardState is UserCakePayCardsStateNoCards)
306
+ return Expanded(child: Center(child: Text(S.of(context).no_cards_found)));
307
+
308
+ final showThumb = cards.length > 6;
309
+ final userCardsList = Stack(
310
+ children: [
311
+ GridView.builder(
312
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
313
+ childAspectRatio: 1.25,
314
+ crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 3 : 2,
315
+ crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
316
+ mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5),
317
+ padding: EdgeInsets.only(left: 2, right: showThumb ? 10 : 22),
318
+ itemCount: cards.length,
319
+ itemBuilder: (_, i) {
320
+ final card = cards[i];
321
+ return UserCardItem(
322
+ logoUrl: card.cardImageUrl,
323
+ title: card.name,
324
+ subTitle: '\$100',
325
+ onTap: () => _showCardInfoBottomSheet(context, card, widget.currentTheme),
326
+ );
327
+ },
328
+ ),
329
+ ],
330
+ );
331
+ return showThumb ? Scrollbar(
332
+ key: ValueKey('cake_pay_my_cards_tab_scrollbar_key'),
333
+ thumbVisibility: true,
334
+ trackVisibility: true,
335
+ child: userCardsList,
336
+ ) : userCardsList;
337
+ }),
338
+ ),
339
+ ],
340
+ );
341
+ });
342
+ }
343
+}
344
+
345
+Future<void> _showCardInfoBottomSheet(
346
+ BuildContext context, CakePayCard card, MaterialThemeBase currentTheme) async {
347
+ bool isReloadable = false; // TODO: replace with real logic
348
+ if (card.name.toLowerCase().contains('prepaid')) {
349
+ isReloadable = true;
350
+ }
351
+ await showModalBottomSheet<void>(
352
+ context: context,
353
+ isDismissible: false,
354
+ isScrollControlled: true,
355
+ backgroundColor: Colors.transparent,
356
+ builder: (BuildContext bottomSheetContext) {
357
+ return isReloadable
358
+ ? CakePayCardInfoBottomSheet(
359
+ isReloadable: isReloadable,
360
+ titleText: 'Reloadable Card',
361
+ balance: '100 USD',
362
+ howToUse: card.howToUse,
363
+ currentTheme: currentTheme,
364
+ footerType: FooterType.doubleActionButton,
365
+ applyBoxShadow: true,
366
+ contentImage: card.cardImageUrl,
367
+ leftActionButtonKey: const Key('cake_pay_cards_page_reload_card_left_button_key'),
368
+ doubleActionLeftButtonText: 'Archive',
369
+ onLeftActionButtonPressed: () {},
370
+ rightActionButtonKey: const Key('cake_pay_cards_page_reload_card_right_button_key'),
371
+ doubleActionRightButtonText: 'Top Up',
372
+ onRightActionButtonPressed: () {},
373
+ onUpdateBalancePressed: () {})
374
+ : CakePayCardInfoBottomSheet(
375
+ isReloadable: isReloadable,
376
+ titleText: card.name,
377
+ balance: '500 USD',
378
+ howToUse: card.howToUse,
379
+ currentTheme: currentTheme,
380
+ footerType: FooterType.singleActionButton,
381
+ applyBoxShadow: true,
382
+ contentImage: card.cardImageUrl,
383
+ singleActionButtonKey: const Key('cake_pay_cards_page_card_info_bottom_sheet_key'),
384
+ singleActionButtonText: 'Mark As Used',
385
+ onSingleActionButtonPressed: () {},
386
+ onUpdateBalancePressed: () {});
387
+ },
388
+ );
389
+}
390
+
391
+class _ShopTab extends StatefulWidget {
392
+ const _ShopTab({required this.cardsListViewModel});
393
+
394
+ final CakePayCardsListViewModel cardsListViewModel;
395
+
396
+ @override
397
+ State<_ShopTab> createState() => _ShopTabState();
398
+}
399
+
400
+class _ShopTabState extends State<_ShopTab> {
401
+ late final ScrollController _scroll;
402
+
403
+
404
+ @override
405
+ void initState() {
406
+ super.initState();
407
+ _scroll = ScrollController()
408
+ ..addListener(() {
409
+ if (!_scroll.hasClients) return;
410
+ final max = _scroll.position.maxScrollExtent;
411
+
412
+ final threshold = 200.0;
413
+ if (_scroll.offset >= max - threshold && !_scroll.position.outOfRange) {
414
+ widget.cardsListViewModel.fetchNextPage();
415
+ }
416
+ });
417
+ }
418
+
419
+ @override
420
+ void dispose() {
421
+ _scroll.dispose();
422
+ super.dispose();
423
+ }
424
+
425
+ @override
426
+ Widget build(BuildContext context) {
427
+ final viewModel = widget.cardsListViewModel;
428
+
429
+ return Column(
430
+ children: [
431
+ Padding(
432
+ padding: const EdgeInsets.fromLTRB(2, 6, 0, 6),
433
+ child: CakePaySearchBar(
434
+ initialQuery: viewModel.searchString,
435
+ onSearch: (String searchText) {
436
+ if (searchText != viewModel.searchString) {
437
+ viewModel.searchString = searchText;
438
+ viewModel.resetLoadingNextPageState();
439
+ viewModel.getVendors(text: searchText);
440
+ }
441
+ },
442
+ onFilter: () async {
443
+ viewModel.storeInitialFilterStates();
444
+ await showFilterWidget(context, viewModel);
445
+ if (viewModel.hasFiltersChanged) {
446
+ viewModel.resetLoadingNextPageState();
447
+ viewModel.getVendors(text: viewModel.searchString);
448
+ }
449
+ },
450
+ onCountryPick: () async {
451
+ viewModel.storeInitialFilterStates();
452
+ await showCountryPicker(context, viewModel);
453
+ if (viewModel.hasFiltersChanged) {
454
+ viewModel.resetLoadingNextPageState();
455
+ viewModel.getVendors(text: viewModel.searchString);
456
+ }
457
+ },
458
+ selectedCountry: viewModel.selectedCountry,
459
+ ),
460
+ ),
461
+ Expanded(
462
+ child: Observer(builder: (_) {
463
+ final vendors = viewModel.cakePayVendors;
464
+
465
+ if (viewModel.vendorsState is! CakePayVendorLoadedState) {
466
+ return const _Loading();
467
+ }
468
+
469
+ if (vendors.isEmpty)
470
+ return Expanded(child: Center(child: Text(S.of(context).no_cards_found)));
471
+
472
+ final loadingMore = viewModel.isLoadingNextPage;
473
+ final showThumb = vendors.length > 3;
474
+ final cardsList = Stack(
475
+ children: [
476
+ GridView.builder(
477
+ controller: _scroll,
478
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
479
+ crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 2 : 1,
480
+ childAspectRatio: 5,
481
+ crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
482
+ mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5),
483
+ padding: EdgeInsets.only(left: 2, right: showThumb ? 10 : 22),
484
+ itemCount: vendors.length + (loadingMore ? 1 : 0),
485
+ itemBuilder: (_, i) {
486
+ if (i >= vendors.length) return const _Loading();
487
+ final vendor = vendors[i];
488
+ return CardItem(
489
+ logoUrl: vendor.card?.cardImageUrl,
490
+ title: vendor.name,
491
+ subTitle: vendor.card?.description ?? '',
492
+ onTap: () => Navigator.pushNamed(context, Routes.cakePayBuyCardPage,
493
+ arguments: [vendor]));
494
+ },
495
+ ),
496
+ ],
497
+ );
498
+ return showThumb
499
+ ? Scrollbar(
500
+ key: ValueKey('cake_pay_shop_tab_scrollbar_key'),
501
+ thumbVisibility: true,
502
+ trackVisibility: true,
503
+ controller: _scroll,
504
+ child: cardsList,
505
+ )
506
+ : cardsList;
507
+ }),
508
+ ),
509
+ ],
510
+ );
511
+ }
512
+}
513
+
514
+class _Loading extends StatelessWidget {
515
+ const _Loading();
516
+
517
+ @override
518
+ Widget build(BuildContext context) => Center(
519
+ child: CircularProgressIndicator(),
520
+ );
521
+}
lib/cake_pay/src/models/cake_pay_card.dart
renamed
lib/cake_pay/src/models/cake_pay_order.dart
renamed
+27
@@ -1,3 +1,15 @@
1
+enum CakePayPaymentMethod { BTC, BTC_LN, XMR, LTC, LTC_MWEB }
2
+
3
+extension CakePayPaymentMethodLabel on CakePayPaymentMethod {
4
+ String get label => switch (this) {
5
+ CakePayPaymentMethod.BTC => 'Bitcoin',
6
+ CakePayPaymentMethod.BTC_LN => 'Bitcoin Lightning',
7
+ CakePayPaymentMethod.XMR => 'Monero',
8
+ CakePayPaymentMethod.LTC => 'Litecoin',
9
+ CakePayPaymentMethod.LTC_MWEB => 'Litecoin MWEB',
10
+ };
11
+}
12
+
13
class CakePayOrder {
14
final String orderId;
15
final List<OrderCard> cards;
@@ -37,6 +49,8 @@ class OrderCard {
49
final String price;
50
final int quantity;
51
final String currencyCode;
52
+ final String? cardName;
53
+ final String? cardImagePath;
54
55
OrderCard({
56
required this.cardId,
@@ -44,6 +58,8 @@ class OrderCard {
58
required this.price,
59
required this.quantity,
60
required this.currencyCode,
61
+ required this.cardName,
62
+ required this.cardImagePath,
63
});
64
65
factory OrderCard.fromMap(Map<String, dynamic> map) {
@@ -53,20 +69,28 @@ class OrderCard {
69
price: map['price'] as String,
70
quantity: map['quantity'] as int,
71
currencyCode: map['currency_code'] as String,
72
+ cardName: map['name'] as String?,
73
+ cardImagePath: map['card_image_url'] as String?,
74
);
75
}
76
}
77
78
class PaymentData {
79
final CryptoPaymentData btc;
80
+ final CryptoPaymentData btc_ln;
81
final CryptoPaymentData xmr;
82
+ final CryptoPaymentData ltc;
83
+ final CryptoPaymentData ltc_mweb;
84
final DateTime invoiceTime;
85
final DateTime expirationTime;
86
final int? commission;
87
88
PaymentData({
89
required this.btc,
90
+ required this.btc_ln,
91
required this.xmr,
92
+ required this.ltc,
93
+ required this.ltc_mweb,
94
required this.invoiceTime,
95
required this.expirationTime,
96
required this.commission,
@@ -75,7 +99,10 @@ class PaymentData {
99
factory PaymentData.fromMap(Map<String, dynamic> map) {
100
return PaymentData(
101
btc: CryptoPaymentData.fromMap(map['BTC'] as Map<String, dynamic>),
102
+ btc_ln: CryptoPaymentData.fromMap(map['BTC_LN'] as Map<String, dynamic>),
103
xmr: CryptoPaymentData.fromMap(map['XMR'] as Map<String, dynamic>),
104
+ ltc: CryptoPaymentData.fromMap(map['LTC'] as Map<String, dynamic>),
105
+ ltc_mweb: CryptoPaymentData.fromMap(map['LTC_MWEB'] as Map<String, dynamic>),
106
invoiceTime: DateTime.fromMillisecondsSinceEpoch(map['invoice_time'] as int),
107
expirationTime: DateTime.fromMillisecondsSinceEpoch(map['expiration_time'] as int),
108
commission: map['commission'] as int?,
lib/cake_pay/src/models/cake_pay_user_credentials.dart
renamed
lib/cake_pay/src/models/cake_pay_vendor.dart
renamed
lib/cake_pay/src/services/cake_pay_api.dart
renamed
+36
-31
@@ -1,8 +1,8 @@
1
import 'dart:convert';
2
3
-import 'package:cake_wallet/cake_pay/cake_pay_order.dart';
4
-import 'package:cake_wallet/cake_pay/cake_pay_user_credentials.dart';
5
-import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
3
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
4
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_user_credentials.dart';
5
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
6
import 'package:cw_core/utils/proxy_wrapper.dart';
7
import 'package:cw_core/utils/print_verbose.dart';
8
import 'package:cake_wallet/entities/country.dart';
@@ -42,7 +42,6 @@ class CakePayApi {
42
throw Exception('Unexpected http status: ${response.statusCode}');
43
}
44
45
-
45
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
46
47
if (bodyJson.containsKey('user') && bodyJson['user']['email'] != null) {
@@ -79,7 +78,6 @@ class CakePayApi {
78
throw Exception('Unexpected http status: ${response.statusCode}');
79
}
80
82
-
81
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
82
83
if (bodyJson.containsKey('error')) {
@@ -108,12 +106,14 @@ class CakePayApi {
106
required bool confirmsTermsAgreed,
107
}) async {
108
final uri = Uri.https(baseCakePayUri, createOrderPath);
109
+
110
final headers = {
111
'Accept': 'application/json',
112
'Content-Type': 'application/json',
113
'Authorization': 'Api-Key $apiKey',
114
};
116
- final query = <String, dynamic>{
115
+
116
+ final body = json.encode({
117
'card_id': cardId,
118
'price': price,
119
'quantity': quantity,
@@ -123,35 +123,42 @@ class CakePayApi {
123
'confirms_no_vpn': confirmsNoVpn,
124
'confirms_voided_refund': confirmsVoidedRefund,
125
'confirms_terms_agreed': confirmsTermsAgreed,
126
- };
126
+ });
127
128
- try {
129
- final response = await ProxyWrapper().post(
130
- clearnetUri: uri,
131
- headers: headers,
132
- body: json.encode(query),
133
- );
128
+ final response = await ProxyWrapper().post(
129
+ clearnetUri: uri,
130
+ headers: headers,
131
+ body: body,
132
+ );
133
+
134
+ if (response.statusCode == 201) {
135
+ final data = json.decode(response.body) as Map<String, dynamic>;
136
+ return CakePayOrder.fromMap(data);
137
+ }
138
135
- if (response.statusCode != 201) {
136
-
137
- final responseBody = json.decode(response.body);
138
- if (responseBody is List) {
139
- throw '${responseBody[0]}';
139
+ String message = 'Server error ${response.statusCode}';
140
+
141
+ final isJson = response.headers['content-type']?.contains('application/json') == true ||
142
+ response.body.trim().startsWith(RegExp(r'[\{\[]'));
143
+
144
+ if (isJson) {
145
+ try {
146
+ final decoded = json.decode(response.body);
147
+ if (decoded is List && decoded.isNotEmpty) {
148
+ message = decoded.first.toString();
149
+ } else if (decoded is Map && decoded['detail'] != null) {
150
+ message = decoded['detail'].toString();
151
} else {
141
- throw Exception('Unexpected error: $responseBody');
152
+ message = decoded.toString();
153
}
143
- }
144
-
145
-
146
- final bodyJson = json.decode(response.body) as Map<String, dynamic>;
147
- return CakePayOrder.fromMap(bodyJson);
148
- } catch (e) {
149
- throw Exception('${e}');
154
+ } on FormatException {}
155
}
156
+
157
+ throw Exception(message);
158
}
159
160
///Simulate Payment
154
- Future<void> simulatePayment(
161
+ Future<String> simulatePayment(
162
{required String CSRFToken, required String authorization, required String orderId}) async {
163
final uri = Uri.https(baseCakePayUri, simulatePaymentPath + '/$orderId');
164
@@ -162,7 +169,6 @@ class CakePayApi {
169
};
170
171
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
165
-
172
173
printV('Response: ${response.statusCode}');
174
@@ -172,7 +178,7 @@ class CakePayApi {
178
179
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
180
175
- throw Exception('You just bot a gift card with id: ${bodyJson['order_id']}');
181
+ return 'You just SIMULATED a buying of a gift card with ID: ${bodyJson['order_id']}';
182
}
183
184
/// Logout
@@ -209,7 +215,7 @@ class CakePayApi {
215
};
216
217
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
212
-
218
+
219
if (response.statusCode != 200) {
220
throw Exception('Unexpected http status: ${response.statusCode}');
221
}
@@ -256,7 +262,6 @@ class CakePayApi {
262
};
263
264
var response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
259
-
265
266
if (response.statusCode != 200) {
267
throw Exception(
lib/cake_pay/src/services/cake_pay_service.dart
renamed
+4
-4
@@ -1,7 +1,7 @@
1
import 'package:cake_wallet/.secrets.g.dart' as secrets;
2
-import 'package:cake_wallet/cake_pay/cake_pay_api.dart';
3
-import 'package:cake_wallet/cake_pay/cake_pay_order.dart';
4
-import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
2
+import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart';
3
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
4
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
5
import 'package:cake_wallet/core/secure_storage.dart';
6
import 'package:cake_wallet/entities/country.dart';
7
@@ -114,6 +114,6 @@ class CakePayService {
114
}
115
116
///Simulate Purchase Gift Card
117
- Future<void> simulatePayment({required String orderId}) async => await cakePayApi.simulatePayment(
117
+ Future<String> simulatePayment({required String orderId}) async => await cakePayApi.simulatePayment(
118
CSRFToken: CSRFToken, authorization: authorization, orderId: orderId);
119
}
lib/cake_pay/src/widgets/cake_pay_alert_modal.dart
new
+73
@@ -0,0 +1,73 @@
1
+import 'package:cake_wallet/src/widgets/alert_background.dart';
2
+import 'package:cake_wallet/src/widgets/primary_button.dart';
3
+import 'package:flutter/material.dart';
4
+
5
+class CakePayAlertModal extends StatelessWidget {
6
+ const CakePayAlertModal({
7
+ super.key,
8
+ required this.title,
9
+ required this.content,
10
+ required this.actionTitle,
11
+ this.showCloseButton = true,
12
+ this.dismissible = false,
13
+ });
14
+
15
+ final String title;
16
+ final Widget content;
17
+ final String actionTitle;
18
+ final bool showCloseButton;
19
+ final bool dismissible;
20
+
21
+ @override
22
+ Widget build(BuildContext context) {
23
+ final theme = Theme.of(context);
24
+ final maxHeight = MediaQuery.of(context).size.height * 0.8;
25
+
26
+ return AlertBackground(
27
+ dismissible: dismissible,
28
+ child: ConstrainedBox(
29
+ constraints: BoxConstraints(maxHeight: maxHeight),
30
+ child: Material(
31
+ color: Colors.transparent,
32
+ child: Padding(
33
+ padding: const EdgeInsets.symmetric(horizontal: 16),
34
+ child: Container(
35
+ decoration: BoxDecoration(
36
+ color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(30)),
37
+ padding: const EdgeInsets.all(24),
38
+ child: Column(
39
+ mainAxisSize: MainAxisSize.min,
40
+ children: [
41
+ if (title.isNotEmpty) ...[
42
+ Text(title,
43
+ style: theme.textTheme.titleLarge?.copyWith(
44
+ color: theme.colorScheme.onSurface, fontWeight: FontWeight.bold),
45
+ textAlign: TextAlign.center),
46
+ const SizedBox(height: 12),
47
+ ],
48
+ Flexible(child: SingleChildScrollView(child: content)),
49
+ const SizedBox(height: 24),
50
+ PrimaryButton(
51
+ onPressed: () => Navigator.pop(context),
52
+ text: actionTitle,
53
+ color: theme.colorScheme.surfaceContainer,
54
+ textColor: theme.colorScheme.primary),
55
+ if (showCloseButton) ...[
56
+ const SizedBox(height: 16),
57
+ InkWell(
58
+ onTap: () => Navigator.pop(context),
59
+ child: CircleAvatar(
60
+ backgroundColor: theme.colorScheme.surfaceContainer,
61
+ child: Icon(Icons.close, color: theme.colorScheme.onSurface),
62
+ ),
63
+ ),
64
+ ],
65
+ ],
66
+ ),
67
+ ),
68
+ ),
69
+ ),
70
+ ),
71
+ );
72
+ }
73
+}
lib/cake_pay/src/widgets/cake_pay_search_bar_widget.dart
new
+126
@@ -0,0 +1,126 @@
1
+import 'package:cake_wallet/entities/country.dart';
2
+import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
3
+import 'package:cake_wallet/utils/debounce.dart';
4
+import 'package:flutter/material.dart';
5
+
6
+class CakePaySearchBar extends StatefulWidget {
7
+ const CakePaySearchBar(
8
+ {required this.initialQuery,
9
+ required this.onSearch,
10
+ required this.onFilter,
11
+ this.onCountryPick,
12
+ this.controller,
13
+ this.selectedCountry});
14
+
15
+ final String initialQuery;
16
+ final ValueChanged<String> onSearch;
17
+ final VoidCallback onFilter;
18
+ final VoidCallback? onCountryPick;
19
+ final Country? selectedCountry;
20
+ final TextEditingController? controller;
21
+
22
+ @override
23
+ State<CakePaySearchBar> createState() => _CakePaySearchBarState();
24
+}
25
+
26
+class _CakePaySearchBarState extends State<CakePaySearchBar> {
27
+ late final TextEditingController _searchController =
28
+ widget.controller ?? TextEditingController(text: widget.initialQuery);
29
+ final _searchFocusNode = FocusNode();
30
+ final _debounce = Debounce(const Duration(milliseconds: 500));
31
+
32
+ @override
33
+ void initState() {
34
+ super.initState();
35
+ _searchController
36
+ .addListener(() => _debounce.run(() => widget.onSearch(_searchController.text)));
37
+ }
38
+
39
+ @override
40
+ void dispose() {
41
+ _searchController.dispose();
42
+ _searchFocusNode.dispose();
43
+ super.dispose();
44
+ }
45
+
46
+ @override
47
+ Widget build(BuildContext context) {
48
+ return SizedBox(
49
+ height: 32,
50
+ child: Row(
51
+ children: [
52
+ Expanded(child: SearchBarWidget(searchController: _searchController)),
53
+ const SizedBox(width: 5),
54
+ _CakePayFilterButton(onFilter: widget.onFilter),
55
+ if (widget.selectedCountry != null)
56
+ _CountryPickerWidget(
57
+ onTap: widget.onCountryPick,
58
+ selectedCountry: widget.selectedCountry!,
59
+ ),
60
+ ],
61
+ ),
62
+ );
63
+ }
64
+}
65
+
66
+class _CakePayFilterButton extends StatelessWidget {
67
+ const _CakePayFilterButton({required this.onFilter});
68
+
69
+ final VoidCallback onFilter;
70
+
71
+ @override
72
+ Widget build(BuildContext context) {
73
+ return GestureDetector(
74
+ onTap: onFilter,
75
+ child: Container(
76
+ width: 32,
77
+ padding: const EdgeInsets.symmetric(vertical: 7),
78
+ decoration: BoxDecoration(
79
+ color: Theme.of(context).colorScheme.surfaceContainer,
80
+ borderRadius: BorderRadius.circular(10)),
81
+ child: Image.asset('assets/images/filter_icon.png',
82
+ color: Theme.of(context).colorScheme.onSurface)),
83
+ );
84
+ }
85
+}
86
+
87
+class _CountryPickerWidget extends StatelessWidget {
88
+ const _CountryPickerWidget({required this.selectedCountry, this.onTap});
89
+
90
+ final Country selectedCountry;
91
+ final VoidCallback? onTap;
92
+
93
+ @override
94
+ Widget build(BuildContext context) {
95
+ return Padding(
96
+ padding: const EdgeInsets.only(left: 5),
97
+ child: GestureDetector(
98
+ onTap: onTap,
99
+ child: Container(
100
+ height: 32,
101
+ padding: const EdgeInsets.symmetric(horizontal: 8),
102
+ decoration: BoxDecoration(
103
+ color: Theme.of(context).colorScheme.surfaceContainer,
104
+ borderRadius: BorderRadius.circular(10)),
105
+ child: Row(
106
+ children: [
107
+ Image.asset(selectedCountry.iconPath,
108
+ width: 24,
109
+ height: 24,
110
+ errorBuilder: (_, __, ___) => const SizedBox(width: 24, height: 24)),
111
+ const SizedBox(width: 6),
112
+ Text(
113
+ selectedCountry.countryCode,
114
+ style: TextStyle(
115
+ fontSize: 16,
116
+ fontWeight: FontWeight.w700,
117
+ color: Theme.of(context).colorScheme.onSurface,
118
+ ),
119
+ ),
120
+ ],
121
+ ),
122
+ ),
123
+ ),
124
+ );
125
+ }
126
+}
lib/cake_pay/src/widgets/cake_pay_tile.dart
renamed
lib/cake_pay/src/widgets/card_item.dart
renamed
+15
-39
@@ -1,18 +1,11 @@
1
import 'package:flutter/material.dart';
2
-
2
import 'image_placeholder.dart';
3
4
class CardItem extends StatelessWidget {
5
CardItem({
6
required this.title,
7
required this.subTitle,
9
- required this.backgroundColor,
10
- required this.titleColor,
11
- required this.subtitleColor,
12
- this.hideBorder = true,
13
- this.discount = 0.0,
8
this.isAmount = false,
15
- this.discountBackground,
9
this.onTap,
10
this.logoUrl,
11
});
@@ -21,13 +14,7 @@ class CardItem extends StatelessWidget {
14
final String title;
15
final String subTitle;
16
final String? logoUrl;
24
- final double discount;
17
final bool isAmount;
26
- final bool hideBorder;
27
- final Color backgroundColor;
28
- final Color titleColor;
29
- final Color subtitleColor;
30
- final AssetImage? discountBackground;
18
19
@override
20
Widget build(BuildContext context) {
@@ -40,13 +27,8 @@ class CardItem extends StatelessWidget {
27
onTap: onTap,
28
child: Container(
29
decoration: BoxDecoration(
43
- color: backgroundColor,
30
+ color: Theme.of(context).colorScheme.surface,
31
borderRadius: BorderRadius.circular(10),
45
- border: hideBorder
46
- ? Border.all(color: Colors.transparent)
47
- : Border.all(
48
- color: Theme.of(context).colorScheme.outlineVariant.withOpacity(0.20),
49
- ),
32
),
33
child: Row(
34
children: [
@@ -73,26 +55,20 @@ class CardItem extends StatelessWidget {
55
child: Column(
56
crossAxisAlignment: CrossAxisAlignment.start,
57
children: [
76
- Text(
77
- title,
78
- maxLines: 1,
79
- overflow: TextOverflow.ellipsis,
80
- style: Theme.of(context).textTheme.bodyMedium!.copyWith(
81
- color: titleColor,
82
- fontSize: 18,
83
- fontWeight: FontWeight.w700,
84
- ),
85
- ),
86
- Text(
87
- subTitle,
88
- maxLines: 2,
89
- overflow: TextOverflow.ellipsis,
90
- style: Theme.of(context).textTheme.bodySmall!.copyWith(
91
- color: titleColor,
92
- fontSize: 10,
93
- fontWeight: FontWeight.w700,
94
- ),
95
- ),
58
+ Text(title,
59
+ maxLines: 1,
60
+ overflow: TextOverflow.ellipsis,
61
+ style: Theme.of(context)
62
+ .textTheme
63
+ .bodyMedium!
64
+ .copyWith(fontSize: 18, fontWeight: FontWeight.w700)),
65
+ Text(subTitle,
66
+ maxLines: 2,
67
+ overflow: TextOverflow.ellipsis,
68
+ style: Theme.of(context)
69
+ .textTheme
70
+ .bodySmall!
71
+ .copyWith(fontSize: 10, fontWeight: FontWeight.w700)),
72
],
73
),
74
),
lib/cake_pay/src/widgets/denominations_amount_widget.dart
new
+122
@@ -0,0 +1,122 @@
1
+import 'package:cake_wallet/generated/i18n.dart';
2
+import 'package:cake_wallet/src/widgets/number_text_fild_widget.dart';
3
+import 'package:cake_wallet/typography.dart';
4
+import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
5
+import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dart';
6
+import 'package:flutter/material.dart';
7
+import 'package:flutter_mobx/flutter_mobx.dart';
8
+
9
+class DenominationsAmountWidget extends StatelessWidget {
10
+ const DenominationsAmountWidget(
11
+ {required this.fiatCurrency,
12
+ required this.denominations,
13
+ required this.amountFieldFocus,
14
+ required this.amountController,
15
+ required this.quantityFieldFocus,
16
+ required this.quantityController,
17
+ required this.cakePayBuyCardViewModel,
18
+ required this.onAmountChanged,
19
+ required this.onQuantityChanged});
20
+
21
+ final String fiatCurrency;
22
+ final List<String> denominations;
23
+ final FocusNode amountFieldFocus;
24
+ final TextEditingController amountController;
25
+ final FocusNode quantityFieldFocus;
26
+ final TextEditingController quantityController;
27
+ final CakePayBuyCardViewModel cakePayBuyCardViewModel;
28
+ final Function(String) onAmountChanged;
29
+ final Function(int?) onQuantityChanged;
30
+
31
+ @override
32
+ Widget build(BuildContext context) {
33
+ return Container(
34
+ height: MediaQuery.of(context).size.height * 0.1,
35
+ child: Row(
36
+ crossAxisAlignment: CrossAxisAlignment.center,
37
+ children: [
38
+ Expanded(
39
+ flex: 8,
40
+ child: Column(
41
+ mainAxisSize: MainAxisSize.min,
42
+ children: [
43
+ DropdownFilterList(
44
+ items: denominations,
45
+ itemPrefix: fiatCurrency,
46
+ selectedItem: denominations.first,
47
+ onItemSelected: (value) {
48
+ amountController.text = value;
49
+ onAmountChanged(value);
50
+ }),
51
+ const SizedBox(height: 4),
52
+ Container(
53
+ width: double.infinity,
54
+ decoration: BoxDecoration(
55
+ border: Border(
56
+ top: BorderSide(
57
+ width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant)),
58
+ ),
59
+ child: Text(S.of(context).value,
60
+ maxLines: 2,
61
+ style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
62
+ ),
63
+ ],
64
+ ),
65
+ ),
66
+ Spacer(),
67
+ Expanded(
68
+ flex: 5,
69
+ child: Column(
70
+ mainAxisSize: MainAxisSize.min,
71
+ children: [
72
+ NumberTextField(
73
+ controller: quantityController,
74
+ focusNode: quantityFieldFocus,
75
+ min: 1,
76
+ max: 99,
77
+ onChanged: (value) => onQuantityChanged(value)),
78
+ const SizedBox(height: 4),
79
+ Container(
80
+ width: double.infinity,
81
+ decoration: BoxDecoration(
82
+ border: Border(
83
+ top: BorderSide(
84
+ width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
85
+ ),
86
+ ),
87
+ child: Text(S.of(context).quantity,
88
+ maxLines: 1,
89
+ style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
90
+ ),
91
+ ],
92
+ ),
93
+ ),
94
+ Spacer(),
95
+ Expanded(
96
+ flex: 8,
97
+ child: Column(
98
+ mainAxisSize: MainAxisSize.min,
99
+ children: [
100
+ Observer(
101
+ builder: (_) => Text('$fiatCurrency ${cakePayBuyCardViewModel.totalAmount}',
102
+ maxLines: 1, style: Theme.of(context).textTheme.titleMedium!)),
103
+ const SizedBox(height: 4),
104
+ Container(
105
+ width: double.infinity,
106
+ decoration: BoxDecoration(
107
+ border: Border(
108
+ top: BorderSide(
109
+ width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
110
+ ),
111
+ ),
112
+ child: Text(S.of(context).total,
113
+ maxLines: 1,
114
+ style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
115
+ ),
116
+ ],
117
+ )),
118
+ ],
119
+ ),
120
+ );
121
+ }
122
+}
lib/cake_pay/src/widgets/enter_amount_widget.dart
new
+103
@@ -0,0 +1,103 @@
1
+import 'package:cake_wallet/generated/i18n.dart';
2
+import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
3
+import 'package:cake_wallet/typography.dart';
4
+import 'package:flutter/material.dart';
5
+import 'package:flutter/services.dart';
6
+
7
+class EnterAmountWidget extends StatelessWidget {
8
+ const EnterAmountWidget(
9
+ {required this.minValue,
10
+ required this.maxValue,
11
+ required this.fiatCurrency,
12
+ required this.amountFieldFocus,
13
+ required this.amountController,
14
+ required this.onAmountChanged});
15
+
16
+ final String minValue;
17
+ final String maxValue;
18
+ final String fiatCurrency;
19
+ final FocusNode amountFieldFocus;
20
+ final TextEditingController amountController;
21
+ final Function(String) onAmountChanged;
22
+
23
+ @override
24
+ Widget build(BuildContext context) {
25
+ return Container(
26
+ height: MediaQuery.of(context).size.height * 0.1,
27
+ child: Column(
28
+ crossAxisAlignment: CrossAxisAlignment.start,
29
+ children: [
30
+ Spacer(flex: 1),
31
+ Text(
32
+ S.of(context).enter_amount,
33
+ style: Theme.of(context).textTheme.titleLarge!,
34
+ ),
35
+ Container(
36
+ decoration: BoxDecoration(
37
+ border: Border(
38
+ bottom: BorderSide(
39
+ width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant))),
40
+ child: BaseTextFormField(
41
+ isDense: true,
42
+ contentPadding: EdgeInsets.zero,
43
+ controller: amountController,
44
+ keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
45
+ hintText: '0.00',
46
+ maxLines: 1,
47
+ prefixIcon: Padding(
48
+ padding: const EdgeInsets.only(top: 2.0),
49
+ child: Text(
50
+ '$fiatCurrency: ',
51
+ style: Theme.of(context).textTheme.titleMedium!,
52
+ ),
53
+ ),
54
+ prefixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
55
+ suffixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
56
+ suffixIcon: Padding(
57
+ padding: const EdgeInsets.only(top: 2, right: 4),
58
+ child: Row(
59
+ mainAxisSize: MainAxisSize.min,
60
+ children: [
61
+ Text('QTY ', style: Theme.of(context).textTheme.titleMedium!),
62
+ Text(
63
+ '1',
64
+ style: Theme.of(context).textTheme
65
+ .titleMedium!
66
+ .copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)
67
+ ),
68
+ ],
69
+ ),
70
+ ),
71
+ textStyle: Theme.of(context).textTheme.titleMedium!,
72
+ placeholderTextStyle: Theme.of(context)
73
+ .textTheme
74
+ .titleMedium!
75
+ .copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
76
+ inputFormatters: [
77
+ FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')),
78
+ FilteringTextInputFormatter.allow(
79
+ RegExp(r'^\d+(\.|\,)?\d{0,2}'),
80
+ ),
81
+ ],
82
+ ),
83
+ ),
84
+ const SizedBox(height: 4),
85
+ Row(
86
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
87
+ children: [
88
+ Text(S.of(context).min_amount(minValue) + ' $fiatCurrency',
89
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
90
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
91
+ )),
92
+ Text(S.of(context).max_amount(maxValue) + ' $fiatCurrency',
93
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
94
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
95
+ )),
96
+ ],
97
+ ),
98
+ Spacer(flex: 1),
99
+ ],
100
+ ),
101
+ );
102
+ }
103
+}
lib/cake_pay/src/widgets/flip_card_widget.dart
new
+62
@@ -0,0 +1,62 @@
1
+import 'dart:math' as math;
2
+import 'package:flutter/material.dart';
3
+
4
+class FlipCard extends StatefulWidget {
5
+ const FlipCard({
6
+ super.key,
7
+ required this.front,
8
+ required this.back,
9
+ this.duration = const Duration(milliseconds: 400),
10
+ this.flipOnTouch = true,
11
+ });
12
+
13
+ final Widget front;
14
+ final Widget back;
15
+ final Duration duration;
16
+ final bool flipOnTouch;
17
+
18
+ @override
19
+ FlipCardState createState() => FlipCardState();
20
+}
21
+
22
+class FlipCardState extends State<FlipCard> with SingleTickerProviderStateMixin {
23
+ late final AnimationController _ctrl =
24
+ AnimationController(vsync: this, duration: widget.duration);
25
+ bool _isFront = true;
26
+
27
+ void toggleCard() {
28
+ if (_isFront) {
29
+ _ctrl.forward();
30
+ } else {
31
+ _ctrl.reverse();
32
+ }
33
+ _isFront = !_isFront;
34
+ }
35
+
36
+ @override
37
+ Widget build(BuildContext context) {
38
+ final content = AnimatedBuilder(
39
+ animation: _ctrl,
40
+ builder: (_, __) {
41
+ final angle = _ctrl.value * math.pi;
42
+ final isFront = angle < math.pi / 2;
43
+ return Transform(
44
+ alignment: Alignment.center,
45
+ transform: Matrix4.identity()
46
+ ..setEntry(3, 2, 0.001)
47
+ ..rotateY(angle),
48
+ child: isFront ? widget.front
49
+ : Transform(
50
+ alignment: Alignment.center,
51
+ transform: Matrix4.rotationY(math.pi),
52
+ child: widget.back,
53
+ ),
54
+ );
55
+ },
56
+ );
57
+
58
+ return widget.flipOnTouch
59
+ ? GestureDetector(onTap: toggleCard, child: content)
60
+ : content;
61
+ }
62
+}
lib/cake_pay/src/widgets/image_placeholder.dart
renamed
lib/cake_pay/src/widgets/link_extractor.dart
renamed
lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart
new
+47
@@ -0,0 +1,47 @@
1
+import 'package:flutter/material.dart';
2
+
3
+class RoundedOverlayCards extends StatelessWidget {
4
+ const RoundedOverlayCards({
5
+ this.topCardChild = const SizedBox(),
6
+ this.bottomCardChild = const SizedBox(),
7
+ });
8
+
9
+ final Widget topCardChild;
10
+ final Widget bottomCardChild;
11
+
12
+ @override
13
+ Widget build(BuildContext context) {
14
+ final screenHeight = MediaQuery.of(context).size.height;
15
+ return ClipRRect(
16
+ borderRadius:
17
+ BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
18
+ child: Container(
19
+ height: screenHeight * 0.50,
20
+ decoration: BoxDecoration(
21
+ borderRadius: BorderRadius.only(
22
+ bottomLeft: Radius.circular(24),
23
+ bottomRight: Radius.circular(24),
24
+ ),
25
+ color: Theme.of(context).colorScheme.surfaceContainer
26
+ ),
27
+ child: Column(
28
+ children: [
29
+ ClipRRect(
30
+ borderRadius: BorderRadius.only(
31
+ bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
32
+ child: Container(
33
+ decoration: BoxDecoration(
34
+ borderRadius: BorderRadius.only(
35
+ bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
36
+ color: Theme.of(context).colorScheme.surfaceContainerLow,
37
+ ),
38
+ height: screenHeight * 0.38,
39
+ width: double.infinity,
40
+ child: topCardChild)),
41
+ bottomCardChild,
42
+ ],
43
+ ),
44
+ ),
45
+ );
46
+ }
47
+}
\ No newline at end of file
lib/cake_pay/src/widgets/text_icon_button.dart
renamed
+6
-5
@@ -19,14 +19,15 @@ class TextIconButton extends StatelessWidget {
19
children: [
20
Text(
21
label,
22
- style: Theme.of(context).textTheme.titleMedium?.copyWith(
23
- color: Theme.of(context).colorScheme.onSurface,
24
- fontWeight: FontWeight.w600,
25
- ),
22
+ style: TextStyle(
23
+ color: Theme.of(context).textTheme.titleLarge!.color,
24
+ fontSize: 16,
25
+ fontWeight: FontWeight.w900,
26
+ ),
27
),
28
Icon(
29
Icons.chevron_right_rounded,
29
- color: Theme.of(context).colorScheme.onSurface,
30
+ color: Theme.of(context).textTheme.titleLarge!.color,
31
),
32
],
33
),
lib/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart
new
+203
@@ -0,0 +1,203 @@
1
+import 'package:cake_wallet/generated/i18n.dart';
2
+import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
3
+import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
4
+import 'package:flutter/material.dart';
5
+import 'package:url_launcher/url_launcher.dart';
6
+
7
+class ThreeCheckboxAlert extends BaseAlertDialog {
8
+ ThreeCheckboxAlert({
9
+ required this.alertTitle,
10
+ required this.leftButtonText,
11
+ required this.rightButtonText,
12
+ required this.actionLeftButton,
13
+ required this.actionRightButton,
14
+ this.alertBarrierDismissible = true,
15
+ Key? key,
16
+ });
17
+
18
+ final String alertTitle;
19
+ final String leftButtonText;
20
+ final String rightButtonText;
21
+ final VoidCallback actionLeftButton;
22
+ final Function(bool, bool, bool) actionRightButton;
23
+ final bool alertBarrierDismissible;
24
+
25
+ bool checkbox1 = false;
26
+ void toggleCheckbox1() => checkbox1 = !checkbox1;
27
+ bool checkbox2 = false;
28
+ void toggleCheckbox2() => checkbox2 = !checkbox2;
29
+ bool checkbox3 = false;
30
+ void toggleCheckbox3() => checkbox3 = !checkbox3;
31
+
32
+ bool showValidationMessage = true;
33
+
34
+ @override
35
+ String get titleText => alertTitle;
36
+
37
+ @override
38
+ bool get isDividerExists => true;
39
+
40
+ @override
41
+ String get leftActionButtonText => leftButtonText;
42
+
43
+ @override
44
+ String get rightActionButtonText => rightButtonText;
45
+
46
+ @override
47
+ VoidCallback get actionLeft => actionLeftButton;
48
+
49
+ @override
50
+ VoidCallback get actionRight => () {
51
+ actionRightButton(checkbox1, checkbox2, checkbox3);
52
+ };
53
+
54
+ @override
55
+ bool get barrierDismissible => alertBarrierDismissible;
56
+
57
+ @override
58
+ Widget content(BuildContext context) {
59
+ return ThreeCheckboxAlertContent(
60
+ checkbox1: checkbox1,
61
+ toggleCheckbox1: toggleCheckbox1,
62
+ checkbox2: checkbox2,
63
+ toggleCheckbox2: toggleCheckbox2,
64
+ checkbox3: checkbox3,
65
+ toggleCheckbox3: toggleCheckbox3,
66
+ );
67
+ }
68
+}
69
+
70
+class ThreeCheckboxAlertContent extends StatefulWidget {
71
+ ThreeCheckboxAlertContent({
72
+ required this.checkbox1,
73
+ required this.toggleCheckbox1,
74
+ required this.checkbox2,
75
+ required this.toggleCheckbox2,
76
+ required this.checkbox3,
77
+ required this.toggleCheckbox3,
78
+ Key? key,
79
+ }) : super(key: key);
80
+
81
+ bool checkbox1;
82
+ void Function() toggleCheckbox1;
83
+ bool checkbox2;
84
+ void Function() toggleCheckbox2;
85
+ bool checkbox3;
86
+ void Function() toggleCheckbox3;
87
+
88
+ @override
89
+ _ThreeCheckboxAlertContentState createState() => _ThreeCheckboxAlertContentState(
90
+ checkbox1: checkbox1,
91
+ toggleCheckbox1: toggleCheckbox1,
92
+ checkbox2: checkbox2,
93
+ toggleCheckbox2: toggleCheckbox2,
94
+ checkbox3: checkbox3,
95
+ toggleCheckbox3: toggleCheckbox3,
96
+ );
97
+
98
+ static _ThreeCheckboxAlertContentState? of(BuildContext context) {
99
+ return context.findAncestorStateOfType<_ThreeCheckboxAlertContentState>();
100
+ }
101
+}
102
+
103
+class _ThreeCheckboxAlertContentState extends State<ThreeCheckboxAlertContent> {
104
+ _ThreeCheckboxAlertContentState({
105
+ required this.checkbox1,
106
+ required this.toggleCheckbox1,
107
+ required this.checkbox2,
108
+ required this.toggleCheckbox2,
109
+ required this.checkbox3,
110
+ required this.toggleCheckbox3,
111
+ });
112
+
113
+ bool checkbox1;
114
+ void Function() toggleCheckbox1;
115
+ bool checkbox2;
116
+ void Function() toggleCheckbox2;
117
+ bool checkbox3;
118
+ void Function() toggleCheckbox3;
119
+
120
+ bool showValidationMessage = true;
121
+
122
+ bool get areAllCheckboxesChecked => checkbox1 && checkbox2 && checkbox3;
123
+
124
+ @override
125
+ Widget build(BuildContext context) {
126
+ return Form(
127
+ child: Column(
128
+ mainAxisSize: MainAxisSize.min,
129
+ children: [
130
+ StandardCheckbox(
131
+ value: checkbox1,
132
+ caption: S.of(context).cakepay_confirm_no_vpn,
133
+ onChanged: (bool? value) {
134
+ setState(() {
135
+ checkbox1 = value ?? false;
136
+ toggleCheckbox1();
137
+ showValidationMessage = !areAllCheckboxesChecked;
138
+ });
139
+ },
140
+ ),
141
+ StandardCheckbox(
142
+ value: checkbox2,
143
+ caption: S.of(context).cakepay_confirm_voided_refund,
144
+ onChanged: (bool? value) {
145
+ setState(() {
146
+ checkbox2 = value ?? false;
147
+ toggleCheckbox2();
148
+ showValidationMessage = !areAllCheckboxesChecked;
149
+ });
150
+ },
151
+ ),
152
+ StandardCheckbox(
153
+ value: checkbox3,
154
+ caption: S.of(context).cakepay_confirm_terms_agreed,
155
+ onChanged: (bool? value) {
156
+ setState(() {
157
+ checkbox3 = value ?? false;
158
+ toggleCheckbox3();
159
+ showValidationMessage = !areAllCheckboxesChecked;
160
+ });
161
+ },
162
+ ),
163
+ GestureDetector(
164
+ behavior: HitTestBehavior.opaque,
165
+ onTap: () => launchUrl(
166
+ Uri.parse("https://cakepay.com/cakepay-web-terms.txt"),
167
+ mode: LaunchMode.externalApplication,
168
+ ),
169
+ child: Padding(
170
+ padding: const EdgeInsets.only(top: 8.0),
171
+ child: Text(
172
+ S.of(context).settings_terms_and_conditions,
173
+ style: TextStyle(
174
+ fontSize: 16,
175
+ fontFamily: 'Lato',
176
+ fontWeight: FontWeight.w400,
177
+ color: Theme.of(context).primaryColor,
178
+ decoration: TextDecoration.none,
179
+ height: 1,
180
+ ),
181
+ softWrap: true,
182
+ ),
183
+ ),
184
+ ),
185
+ if (showValidationMessage)
186
+ Padding(
187
+ padding: const EdgeInsets.only(top: 8.0),
188
+ child: Text(
189
+ 'Please confirm all checkboxes',
190
+ style: TextStyle(
191
+ color: Colors.red,
192
+ fontSize: 14,
193
+ fontFamily: 'Lato',
194
+ fontWeight: FontWeight.w400,
195
+ decoration: TextDecoration.none,
196
+ ),
197
+ ),
198
+ ),
199
+ ],
200
+ ),
201
+ );
202
+ }
203
+}
lib/cake_pay/src/widgets/user_card_item.dart
new
+83
@@ -0,0 +1,83 @@
1
+import 'package:flutter/material.dart';
2
+import 'image_placeholder.dart';
3
+
4
+class UserCardItem extends StatelessWidget {
5
+ UserCardItem({
6
+ required this.title,
7
+ required this.subTitle,
8
+ this.onTap,
9
+ this.logoUrl
10
+ });
11
+
12
+ final VoidCallback? onTap;
13
+ final String title;
14
+ final String subTitle;
15
+ final String? logoUrl;
16
+
17
+ @override
18
+ Widget build(BuildContext context) {
19
+ return Theme(
20
+ data: ThemeData(
21
+ splashColor: Colors.transparent,
22
+ highlightColor: Colors.transparent,
23
+ ),
24
+ child: InkWell(
25
+ onTap: onTap,
26
+ child: Container(
27
+ decoration: BoxDecoration(
28
+ color: Theme.of(context).colorScheme.surface,
29
+ borderRadius: BorderRadius.circular(15),
30
+ ),
31
+ child: Column(
32
+ children: [
33
+ if (logoUrl != null)
34
+ Padding(
35
+ padding: const EdgeInsets.all(2.0),
36
+ child: AspectRatio(
37
+ aspectRatio: 1.65,
38
+ child: ClipRRect(
39
+ borderRadius: BorderRadius.all(Radius.circular(13)),
40
+ child: Image.network(
41
+ logoUrl!,
42
+ fit: BoxFit.cover,
43
+ loadingBuilder:
44
+ (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
45
+ if (loadingProgress == null) return child;
46
+ return Center(child: CircularProgressIndicator());
47
+ },
48
+ errorBuilder: (context, error, stackTrace) => CakePayCardImagePlaceholder(),
49
+ ),
50
+ ),
51
+ ),
52
+ ),
53
+ Padding(
54
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
55
+ child: Row(
56
+ children: [
57
+ Expanded(
58
+ child: Text(title,
59
+ maxLines: 1,
60
+ overflow: TextOverflow.ellipsis,
61
+ style: Theme.of(context)
62
+ .textTheme
63
+ .bodyMedium!
64
+ .copyWith(fontSize: 16, fontWeight: FontWeight.w700)),
65
+ ),
66
+ SizedBox(width: 8),
67
+ Text(subTitle,
68
+ maxLines: 1,
69
+ overflow: TextOverflow.clip,
70
+ style: Theme.of(context)
71
+ .textTheme
72
+ .bodyMedium!
73
+ .copyWith(fontSize: 16, fontWeight: FontWeight.w700)),
74
+ ],
75
+ ),
76
+ ),
77
+ ],
78
+ ),
79
+ ),
80
+ ),
81
+ );
82
+ }
83
+}
\ No newline at end of file
lib/di.dart
+8
-26
@@ -76,7 +76,7 @@ import 'package:cake_wallet/entities/qr_view_data.dart';
76
import 'package:cake_wallet/entities/template.dart';
77
import 'package:cake_wallet/entities/transaction_description.dart';
78
import 'package:cake_wallet/ethereum/ethereum.dart';
79
-import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
79
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
80
import 'package:cake_wallet/exchange/exchange_template.dart';
81
import 'package:cake_wallet/exchange/trade.dart';
82
import 'package:cake_wallet/monero/monero.dart';
@@ -170,14 +170,12 @@ import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
170
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
171
import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
172
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
173
-import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
174
-import 'package:cake_wallet/cake_pay/cake_pay_api.dart';
175
-import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
176
-import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart';
177
-import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
173
+import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
174
+import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart';
175
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
176
+import 'package:cake_wallet/cake_pay/cake_pay.dart';
177
import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart';
178
import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
180
-import 'package:cake_wallet/view_model/cake_pay/cake_pay_purchase_view_model.dart';
179
import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart';
180
import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
181
import 'package:cake_wallet/view_model/new_wallet_type_view_model.dart';
@@ -276,7 +274,6 @@ import 'package:shared_preferences/shared_preferences.dart';
274
import 'buy/kryptonim/kryptonim.dart';
275
import 'buy/meld/meld_buy_provider.dart';
276
import 'src/screens/buy/buy_sell_page.dart';
279
-import 'cake_pay/cake_pay_payment_credantials.dart';
277
import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart';
278
import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
279
import 'package:cake_wallet/core/trade_monitor.dart';
@@ -1390,18 +1387,11 @@ Future<void> setup({
1387
1388
getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get<CakePayService>()));
1389
1393
- getIt.registerFactoryParam<CakePayPurchaseViewModel, PaymentCredential, CakePayCard>(
1394
- (PaymentCredential paymentCredential, CakePayCard card) {
1395
- return CakePayPurchaseViewModel(
1396
- cakePayService: getIt.get<CakePayService>(),
1397
- paymentCredential: paymentCredential,
1398
- card: card,
1399
- sendViewModel: getIt.get<SendViewModel>());
1400
- });
1401
-
1390
getIt.registerFactoryParam<CakePayBuyCardViewModel, CakePayVendor, void>(
1391
(CakePayVendor vendor, _) {
1404
- return CakePayBuyCardViewModel(vendor: vendor);
1392
+ return CakePayBuyCardViewModel(vendor: vendor,
1393
+ cakePayService: getIt.get<CakePayService>(),
1394
+ sendViewModel: getIt.get<SendViewModel>());
1395
});
1396
1397
getIt.registerFactory(() => CakePayAccountViewModel(cakePayService: getIt.get<CakePayService>()));
@@ -1422,14 +1412,6 @@ Future<void> setup({
1412
getIt.get<CakePayBuyCardViewModel>(param1: vendor), getIt.get<CakePayService>());
1413
});
1414
1425
- getIt
1426
- .registerFactoryParam<CakePayBuyCardDetailPage, List<dynamic>, void>((List<dynamic> args, _) {
1427
- final paymentCredential = args.first as PaymentCredential;
1428
- final card = args[1] as CakePayCard;
1429
- return CakePayBuyCardDetailPage(
1430
- getIt.get<CakePayPurchaseViewModel>(param1: paymentCredential, param2: card));
1431
- });
1432
-
1415
getIt.registerFactory(() => CakePayCardsPage(getIt.get<CakePayCardsListViewModel>()));
1416
1417
getIt.registerFactory(() => CakePayAccountPage(getIt.get<CakePayAccountViewModel>()));
lib/router.dart
+8
-14
@@ -21,8 +21,7 @@ import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
21
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
22
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
23
import 'package:cake_wallet/src/screens/buy/webview_page.dart';
24
-import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart';
25
-import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
24
+import 'package:cake_wallet/cake_pay/cake_pay.dart';
25
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
26
import 'package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_options_page.dart';
27
import 'package:cake_wallet/src/screens/connect_device/select_hardware_wallet_account_page.dart';
@@ -153,14 +152,14 @@ import 'src/screens/dashboard/pages/nft_import_page.dart';
152
153
late RouteSettings currentRouteSettings;
154
156
-Route<dynamic> handleRouteWithPlatformAwareness(
155
+Route<T> handleRouteWithPlatformAwareness<T>(
156
Widget Function(BuildContext) builder, {
157
bool fullscreenDialog = false,
158
}) {
159
if (Platform.isIOS) {
161
- return CupertinoPageRoute<void>(builder: builder, fullscreenDialog: fullscreenDialog);
160
+ return CupertinoPageRoute<T>(builder: builder, fullscreenDialog: fullscreenDialog);
161
} else {
163
- return MaterialPageRoute<void>(builder: builder, fullscreenDialog: fullscreenDialog);
162
+ return MaterialPageRoute<T>(builder: builder, fullscreenDialog: fullscreenDialog);
163
}
164
}
165
@@ -708,25 +707,20 @@ Route<dynamic> createRoute(RouteSettings settings) {
707
(context) => getIt.get<CakePayBuyCardPage>(param1: args),
708
);
709
711
- case Routes.cakePayBuyCardDetailPage:
712
- final args = settings.arguments as List;
713
- return handleRouteWithPlatformAwareness(
714
- (context) => getIt.get<CakePayBuyCardDetailPage>(param1: args),
715
- );
716
-
710
case Routes.cakePayWelcomePage:
718
- return handleRouteWithPlatformAwareness(
711
+ return handleRouteWithPlatformAwareness<bool>(
712
(context) => getIt.get<CakePayWelcomePage>(),
713
);
714
715
case Routes.cakePayVerifyOtpPage:
716
final args = settings.arguments as List;
724
- return handleRouteWithPlatformAwareness(
717
+ return handleRouteWithPlatformAwareness<bool>(
718
(context) => getIt.get<CakePayVerifyOtpPage>(param1: args),
719
);
720
721
+
722
case Routes.cakePayAccountPage:
729
- return handleRouteWithPlatformAwareness(
723
+ return handleRouteWithPlatformAwareness<bool>(
724
(context) => getIt.get<CakePayAccountPage>(),
725
);
726
lib/routes.dart
-1
@@ -75,7 +75,6 @@ class Routes {
75
static const cakePayLoginPage = '/cake_pay_login_page';
76
static const cakePayCardsPage = '/cake_pay_cards_page';
77
static const cakePayBuyCardPage = '/cake_pay_buy_card_page';
78
- static const cakePayBuyCardDetailPage = '/cake_pay_buy_card_detail_page';
78
static const cakePayVerifyOtpPage = '/cake_pay_verify_otp_page';
79
static const cakePayAccountPage = '/cake_pay_account_page';
80
static const webViewPage = '/web_view_page';
lib/src/screens/cake_pay/cake_pay.dart
deleted
-5
@@ -1,5 +0,0 @@
1
-export 'auth/cake_pay_welcome_page.dart';
2
-export 'auth/cake_pay_verify_otp_page.dart';
3
-export 'cards/cake_pay_confirm_purchase_card_page.dart';
4
-export 'cards/cake_pay_cards_page.dart';
5
-export 'cards/cake_pay_buy_card_page.dart';
lib/src/screens/cake_pay/cards/cake_pay_buy_card_page.dart
deleted
-519
@@ -1,519 +0,0 @@
1
-import 'dart:io';
2
-
3
-import 'package:auto_size_text/auto_size_text.dart';
4
-import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
5
-import 'package:cake_wallet/cake_pay/cake_pay_payment_credantials.dart';
6
-import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
7
-import 'package:cake_wallet/generated/i18n.dart';
8
-import 'package:cake_wallet/routes.dart';
9
-import 'package:cake_wallet/src/screens/base_page.dart';
10
-import 'package:cake_wallet/src/screens/cake_pay/widgets/image_placeholder.dart';
11
-import 'package:cake_wallet/src/screens/cake_pay/widgets/link_extractor.dart';
12
-import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
13
-import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
14
-import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
15
-import 'package:cake_wallet/src/widgets/number_text_fild_widget.dart';
16
-import 'package:cake_wallet/src/widgets/primary_button.dart';
17
-import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
18
-import 'package:cake_wallet/typography.dart';
19
-import 'package:cake_wallet/utils/responsive_layout_util.dart';
20
-import 'package:cake_wallet/utils/show_pop_up.dart';
21
-import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
22
-import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dart';
23
-import 'package:flutter/material.dart';
24
-import 'package:flutter/services.dart';
25
-import 'package:flutter_mobx/flutter_mobx.dart';
26
-import 'package:keyboard_actions/keyboard_actions.dart';
27
-
28
-class CakePayBuyCardPage extends BasePage {
29
- CakePayBuyCardPage(
30
- this.cakePayBuyCardViewModel,
31
- this.cakePayService,
32
- ) : _amountFieldFocus = FocusNode(),
33
- _amountController = TextEditingController(),
34
- _quantityFieldFocus = FocusNode(),
35
- _quantityController =
36
- TextEditingController(text: cakePayBuyCardViewModel.quantity.toString()) {
37
- _amountController.addListener(() {
38
- cakePayBuyCardViewModel.onAmountChanged(_amountController.text);
39
- });
40
- }
41
-
42
- final CakePayBuyCardViewModel cakePayBuyCardViewModel;
43
- final CakePayService cakePayService;
44
-
45
- @override
46
- String get title => cakePayBuyCardViewModel.card.name;
47
-
48
- @override
49
- bool get extendBodyBehindAppBar => true;
50
-
51
- @override
52
- AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent;
53
-
54
- @override
55
- Widget? middle(BuildContext context) {
56
- return Text(
57
- title,
58
- textAlign: TextAlign.center,
59
- maxLines: 2,
60
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
61
- color: Theme.of(context).colorScheme.onSurface,
62
- fontWeight: FontWeight.w600,
63
- ),
64
- );
65
- }
66
-
67
- final TextEditingController _amountController;
68
- final FocusNode _amountFieldFocus;
69
- final TextEditingController _quantityController;
70
- final FocusNode _quantityFieldFocus;
71
-
72
- @override
73
- Widget body(BuildContext context) {
74
- final card = cakePayBuyCardViewModel.card;
75
- final vendor = cakePayBuyCardViewModel.vendor;
76
-
77
- return KeyboardActions(
78
- disableScroll: true,
79
- config: KeyboardActionsConfig(
80
- keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
81
- keyboardBarColor: Theme.of(context).colorScheme.surface,
82
- nextFocus: false,
83
- actions: [
84
- KeyboardActionsItem(
85
- focusNode: _amountFieldFocus,
86
- toolbarButtons: [(_) => KeyboardDoneButton()],
87
- ),
88
- ]),
89
- child: Container(
90
- color: Theme.of(context).colorScheme.surface,
91
- child: ScrollableWithBottomSection(
92
- contentPadding: EdgeInsets.zero,
93
- content: Column(
94
- children: [
95
- ClipRRect(
96
- borderRadius: BorderRadius.only(
97
- bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
98
- child: Container(
99
- decoration: BoxDecoration(
100
- gradient: LinearGradient(
101
- colors: [
102
- Theme.of(context).colorScheme.primary,
103
- Theme.of(context).colorScheme.secondary,
104
- ],
105
- begin: Alignment.topLeft,
106
- end: Alignment.bottomRight,
107
- ),
108
- ),
109
- height: responsiveLayoutUtil.screenHeight * 0.35,
110
- width: double.infinity,
111
- child: Column(
112
- children: [
113
- Expanded(flex: 4, child: const SizedBox()),
114
- Expanded(
115
- flex: 7,
116
- child: ClipRRect(
117
- borderRadius: BorderRadius.all(Radius.circular(10)),
118
- child: Image.network(
119
- card.cardImageUrl ?? '',
120
- fit: BoxFit.cover,
121
- loadingBuilder: (BuildContext context, Widget child,
122
- ImageChunkEvent? loadingProgress) {
123
- if (loadingProgress == null) return child;
124
- return Center(child: CircularProgressIndicator());
125
- },
126
- errorBuilder: (context, error, stackTrace) =>
127
- CakePayCardImagePlaceholder(),
128
- ),
129
- ),
130
- ),
131
- Expanded(child: const SizedBox()),
132
- ],
133
- )),
134
- ),
135
- Padding(
136
- padding: const EdgeInsets.symmetric(horizontal: 24),
137
- child: Container(
138
- height: responsiveLayoutUtil.screenHeight * 0.5,
139
- child: Column(
140
- crossAxisAlignment: CrossAxisAlignment.start,
141
- children: [
142
- SizedBox(height: 24),
143
- Expanded(
144
- child: Text(
145
- S.of(context).enter_amount,
146
- style: Theme.of(context).textTheme.bodyLarge!.copyWith(
147
- fontSize: 24,
148
- fontWeight: FontWeight.w600,
149
- ),
150
- ),
151
- ),
152
- card.denominations.isNotEmpty
153
- ? Expanded(
154
- flex: 2,
155
- child: _DenominationsAmountWidget(
156
- fiatCurrency: card.fiatCurrency.title,
157
- denominations: card.denominations,
158
- amountFieldFocus: _amountFieldFocus,
159
- amountController: _amountController,
160
- quantityFieldFocus: _quantityFieldFocus,
161
- quantityController: _quantityController,
162
- onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
163
- onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
164
- cakePayBuyCardViewModel: cakePayBuyCardViewModel,
165
- ),
166
- )
167
- : Expanded(
168
- flex: 2,
169
- child: _EnterAmountWidget(
170
- minValue: card.minValue ?? '-',
171
- maxValue: card.maxValue ?? '-',
172
- fiatCurrency: card.fiatCurrency.title,
173
- amountFieldFocus: _amountFieldFocus,
174
- amountController: _amountController,
175
- onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
176
- ),
177
- ),
178
- Expanded(
179
- flex: 5,
180
- child: Column(
181
- children: [
182
- if (vendor.cakeWarnings != null)
183
- Padding(
184
- padding: const EdgeInsets.only(bottom: 8.0),
185
- child: Container(
186
- decoration: BoxDecoration(
187
- color: Theme.of(context).colorScheme.primary,
188
- borderRadius: BorderRadius.circular(10),
189
- border: Border.all(
190
- color:
191
- Theme.of(context).colorScheme.onPrimary.withOpacity(0.20),
192
- ),
193
- ),
194
- child: Padding(
195
- padding: const EdgeInsets.all(8.0),
196
- child: Text(
197
- vendor.cakeWarnings!,
198
- textAlign: TextAlign.center,
199
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
200
- color: Theme.of(context).colorScheme.onPrimary,
201
- fontWeight: FontWeight.w600,
202
- ),
203
- ),
204
- ),
205
- ),
206
- ),
207
- Expanded(
208
- child: SingleChildScrollView(
209
- child: ClickableLinksText(
210
- text: card.description ?? '',
211
- textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
212
- color: Theme.of(context).colorScheme.onSurfaceVariant,
213
- fontSize: 18,
214
- ),
215
- ),
216
- ),
217
- ),
218
- ],
219
- ),
220
- ),
221
- ],
222
- ),
223
- ),
224
- ),
225
- ],
226
- ),
227
- bottomSection: Column(
228
- children: [
229
- Observer(builder: (_) {
230
- return Padding(
231
- padding: EdgeInsets.only(bottom: 12),
232
- child: PrimaryButton(
233
- onPressed: () => isIOSUnavailable(card)
234
- ? alertIOSAvailability(context, card)
235
- : navigateToCakePayBuyCardDetailPage(context, card),
236
- text: S.of(context).buy_now,
237
- isDisabled: !cakePayBuyCardViewModel.isEnablePurchase,
238
- color: Theme.of(context).colorScheme.primary,
239
- textColor: Theme.of(context).colorScheme.onPrimary,
240
- ),
241
- );
242
- }),
243
- ],
244
- ),
245
- ),
246
- ),
247
- );
248
- }
249
-
250
- bool isWordInCardsName(CakePayCard card, String word) {
251
- return card.name.toLowerCase().contains(word.toLowerCase());
252
- }
253
-
254
- bool isIOSUnavailable(CakePayCard card) {
255
- if (!Platform.isIOS && !Platform.isMacOS) {
256
- return false;
257
- }
258
-
259
- final isDigitalGameStores = isWordInCardsName(card, 'playstation') ||
260
- isWordInCardsName(card, 'xbox') ||
261
- isWordInCardsName(card, 'steam') ||
262
- isWordInCardsName(card, 'meta quest') ||
263
- isWordInCardsName(card, 'kigso') ||
264
- isWordInCardsName(card, 'game world') ||
265
- isWordInCardsName(card, 'google') ||
266
- isWordInCardsName(card, 'nintendo');
267
- final isGCodes = isWordInCardsName(card, 'gcodes');
268
- final isApple = isWordInCardsName(card, 'itunes') || isWordInCardsName(card, 'apple');
269
- final isTidal = isWordInCardsName(card, 'tidal');
270
- final isVPNServices = isWordInCardsName(card, 'nordvpn') ||
271
- isWordInCardsName(card, 'expressvpn') ||
272
- isWordInCardsName(card, 'surfshark') ||
273
- isWordInCardsName(card, 'proton');
274
- final isStreamingServices = isWordInCardsName(card, 'netflix') ||
275
- isWordInCardsName(card, 'spotify') ||
276
- isWordInCardsName(card, 'hulu') ||
277
- isWordInCardsName(card, 'hbo') ||
278
- isWordInCardsName(card, 'soundcloud') ||
279
- isWordInCardsName(card, 'twitch');
280
- final isDatingServices = isWordInCardsName(card, 'tinder');
281
-
282
- return isDigitalGameStores ||
283
- isGCodes ||
284
- isApple ||
285
- isTidal ||
286
- isVPNServices ||
287
- isStreamingServices ||
288
- isDatingServices;
289
- }
290
-
291
- Future<void> alertIOSAvailability(BuildContext context, CakePayCard card) async {
292
- return await showPopUp<void>(
293
- context: context,
294
- builder: (BuildContext context) {
295
- return AlertWithOneAction(
296
- alertTitle: S.of(context).error,
297
- alertContent: S.of(context).cakepay_ios_not_available,
298
- buttonText: S.of(context).ok,
299
- buttonAction: () {
300
- // _walletHardwareRestoreVM.error = null;
301
- Navigator.of(context).pop();
302
- });
303
- });
304
- }
305
-
306
- Future<void> navigateToCakePayBuyCardDetailPage(BuildContext context, CakePayCard card) async {
307
- final userName = await cakePayService.getUserEmail();
308
- final paymentCredential = PaymentCredential(
309
- amount: cakePayBuyCardViewModel.amount,
310
- quantity: cakePayBuyCardViewModel.quantity,
311
- totalAmount: cakePayBuyCardViewModel.totalAmount,
312
- userName: userName,
313
- fiatCurrency: card.fiatCurrency.title,
314
- );
315
-
316
- Navigator.pushNamed(
317
- context,
318
- Routes.cakePayBuyCardDetailPage,
319
- arguments: [paymentCredential, card],
320
- );
321
- }
322
-}
323
-
324
-class _DenominationsAmountWidget extends StatelessWidget {
325
- const _DenominationsAmountWidget({
326
- required this.fiatCurrency,
327
- required this.denominations,
328
- required this.amountFieldFocus,
329
- required this.amountController,
330
- required this.quantityFieldFocus,
331
- required this.quantityController,
332
- required this.cakePayBuyCardViewModel,
333
- required this.onAmountChanged,
334
- required this.onQuantityChanged,
335
- });
336
-
337
- final String fiatCurrency;
338
- final List<String> denominations;
339
- final FocusNode amountFieldFocus;
340
- final TextEditingController amountController;
341
- final FocusNode quantityFieldFocus;
342
- final TextEditingController quantityController;
343
- final CakePayBuyCardViewModel cakePayBuyCardViewModel;
344
- final Function(String) onAmountChanged;
345
- final Function(int?) onQuantityChanged;
346
-
347
- @override
348
- Widget build(BuildContext context) {
349
- return Row(
350
- crossAxisAlignment: CrossAxisAlignment.start,
351
- children: [
352
- Expanded(
353
- flex: 12,
354
- child: Column(
355
- children: [
356
- Expanded(
357
- child: DropdownFilterList(
358
- items: denominations,
359
- itemPrefix: fiatCurrency,
360
- selectedItem: denominations.first,
361
- textStyle: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurface),
362
- onItemSelected: (value) {
363
- amountController.text = value;
364
- onAmountChanged(value);
365
- },
366
- caption: '',
367
- ),
368
- ),
369
- const SizedBox(height: 4),
370
- Expanded(
371
- child: Container(
372
- width: double.infinity,
373
- decoration: BoxDecoration(
374
- border: Border(
375
- top: BorderSide(
376
- width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
377
- ),
378
- ),
379
- child: Text(S.of(context).choose_card_value + ':',
380
- maxLines: 2,
381
- style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
382
- ),
383
- ),
384
- ],
385
- ),
386
- ),
387
- Expanded(child: const SizedBox()),
388
- Expanded(
389
- flex: 8,
390
- child: Column(
391
- children: [
392
- Expanded(
393
- child: NumberTextField(
394
- controller: quantityController,
395
- focusNode: quantityFieldFocus,
396
- min: 1,
397
- max: 99,
398
- onChanged: (value) => onQuantityChanged(value),
399
- ),
400
- ),
401
- const SizedBox(height: 4),
402
- Expanded(
403
- child: Container(
404
- width: double.infinity,
405
- decoration: BoxDecoration(
406
- border: Border(
407
- top: BorderSide(
408
- width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
409
- ),
410
- ),
411
- child: Text(S.of(context).quantity + ':',
412
- maxLines: 1,
413
- style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
414
- ),
415
- ),
416
- ],
417
- ),
418
- ),
419
- Expanded(child: const SizedBox()),
420
- Expanded(
421
- flex: 12,
422
- child: Column(
423
- children: [
424
- Expanded(
425
- child: Container(
426
- alignment: Alignment.bottomCenter,
427
- child: Observer(
428
- builder: (_) => AutoSizeText(
429
- '$fiatCurrency ${cakePayBuyCardViewModel.totalAmount}',
430
- maxLines: 1,
431
- style: textMediumSemiBold(
432
- color: Theme.of(context).colorScheme.onSurface)))),
433
- ),
434
- const SizedBox(height: 4),
435
- Expanded(
436
- child: Container(
437
- width: double.infinity,
438
- decoration: BoxDecoration(
439
- border: Border(
440
- top: BorderSide(
441
- width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
442
- ),
443
- ),
444
- child: Text(S.of(context).total + ':',
445
- maxLines: 1,
446
- style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
447
- ),
448
- ),
449
- ],
450
- )),
451
- ],
452
- );
453
- }
454
-}
455
-
456
-class _EnterAmountWidget extends StatelessWidget {
457
- const _EnterAmountWidget({
458
- required this.minValue,
459
- required this.maxValue,
460
- required this.fiatCurrency,
461
- required this.amountFieldFocus,
462
- required this.amountController,
463
- required this.onAmountChanged,
464
- });
465
-
466
- final String minValue;
467
- final String maxValue;
468
- final String fiatCurrency;
469
- final FocusNode amountFieldFocus;
470
- final TextEditingController amountController;
471
- final Function(String) onAmountChanged;
472
-
473
- @override
474
- Widget build(BuildContext context) {
475
- return Column(
476
- children: [
477
- Container(
478
- decoration: BoxDecoration(
479
- border: Border(
480
- bottom: BorderSide(width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
481
- ),
482
- ),
483
- child: BaseTextFormField(
484
- controller: amountController,
485
- keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
486
- hintText: '0.00',
487
- maxLines: null,
488
- prefixIcon: Padding(
489
- padding: const EdgeInsets.only(top: 12),
490
- child: Text(
491
- '$fiatCurrency: ',
492
- style: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurface),
493
- ),
494
- ),
495
- textStyle: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurface),
496
- placeholderTextStyle:
497
- textMediumSemiBold(color: Theme.of(context).colorScheme.onSurfaceVariant),
498
- inputFormatters: [
499
- FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')),
500
- FilteringTextInputFormatter.allow(
501
- RegExp(r'^\d+(\.|\,)?\d{0,2}'),
502
- ),
503
- ],
504
- ),
505
- ),
506
- SizedBox(height: 4),
507
- Row(
508
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
509
- children: [
510
- Text(S.of(context).min_amount(minValue) + ' $fiatCurrency',
511
- style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
512
- Text(S.of(context).max_amount(maxValue) + ' $fiatCurrency',
513
- style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
514
- ],
515
- ),
516
- ],
517
- );
518
- }
519
-}
lib/src/screens/cake_pay/cards/cake_pay_cards_page.dart
deleted
-409
@@ -1,409 +0,0 @@
1
-import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
2
-import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
3
-import 'package:cake_wallet/entities/country.dart';
4
-import 'package:cake_wallet/generated/i18n.dart';
5
-import 'package:cake_wallet/routes.dart';
6
-import 'package:cake_wallet/src/screens/base_page.dart';
7
-import 'package:cake_wallet/src/screens/cake_pay/widgets/card_item.dart';
8
-import 'package:cake_wallet/src/screens/cake_pay/widgets/card_menu.dart';
9
-import 'package:cake_wallet/src/screens/dashboard/widgets/filter_widget.dart';
10
-import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
11
-import 'package:cake_wallet/src/widgets/cake_scrollbar.dart';
12
-import 'package:cake_wallet/src/widgets/gradient_background.dart';
13
-import 'package:cake_wallet/src/widgets/picker.dart';
14
-import 'package:cake_wallet/typography.dart';
15
-import 'package:cake_wallet/utils/debounce.dart';
16
-import 'package:cake_wallet/utils/responsive_layout_util.dart';
17
-import 'package:cake_wallet/utils/show_pop_up.dart';
18
-import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
19
-import 'package:flutter/material.dart';
20
-import 'package:flutter_mobx/flutter_mobx.dart';
21
-import 'package:mobx/mobx.dart';
22
-
23
-class CakePayCardsPage extends BasePage {
24
- CakePayCardsPage(this._cardsListViewModel) : searchFocusNode = FocusNode() {
25
- _searchController.addListener(() {
26
- if (_searchController.text != _cardsListViewModel.searchString) {
27
- _searchDebounce.run(() {
28
- _cardsListViewModel.resetLoadingNextPageState();
29
- _cardsListViewModel.getVendors(text: _searchController.text);
30
- });
31
- }
32
- });
33
- }
34
-
35
- final FocusNode searchFocusNode;
36
- final CakePayCardsListViewModel _cardsListViewModel;
37
-
38
- final _searchDebounce = Debounce(Duration(milliseconds: 500));
39
- final _searchController = TextEditingController();
40
-
41
- @override
42
- bool get gradientBackground => true;
43
-
44
- @override
45
- Widget Function(BuildContext, Widget) get rootWrapper =>
46
- (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
47
-
48
- @override
49
- bool get resizeToAvoidBottomInset => false;
50
-
51
- @override
52
- Widget get endDrawer => CardMenu();
53
-
54
- @override
55
- Widget middle(BuildContext context) {
56
- return Text(
57
- 'Cake Pay',
58
- style: textMediumSemiBold(
59
- color: Theme.of(context).colorScheme.onSurface,
60
- ),
61
- );
62
- }
63
-
64
- @override
65
- Widget trailing(BuildContext context) {
66
- return _TrailingIcon(
67
- asset: 'assets/images/profile.png',
68
- iconColor: Theme.of(context).colorScheme.onSurface,
69
- onPressed: () {
70
- _cardsListViewModel.isCakePayUserAuthenticated().then(
71
- (value) {
72
- if (value) {
73
- Navigator.pushNamed(context, Routes.cakePayAccountPage);
74
- return;
75
- }
76
- Navigator.pushNamed(context, Routes.cakePayWelcomePage);
77
- },
78
- );
79
- },
80
- );
81
- }
82
-
83
- @override
84
- Widget body(BuildContext context) {
85
- if (_cardsListViewModel.settingsStore.selectedCakePayCountry == null) {
86
- WidgetsBinding.instance.addPostFrameCallback((_) {
87
- reaction((_) => _cardsListViewModel.shouldShowCountryPicker,
88
- (bool shouldShowCountryPicker) async {
89
- if (shouldShowCountryPicker) {
90
- _cardsListViewModel.storeInitialFilterStates();
91
- await showCountryPicker(context, _cardsListViewModel);
92
- if (_cardsListViewModel.hasFiltersChanged) {
93
- _cardsListViewModel.resetLoadingNextPageState();
94
- _cardsListViewModel.getVendors();
95
- }
96
-
97
- _cardsListViewModel.settingsStore.selectedCakePayCountry =
98
- _cardsListViewModel.selectedCountry;
99
- }
100
- });
101
- });
102
- }
103
-
104
- final filterButton = Semantics(
105
- label: S.of(context).filter_by,
106
- child: GestureDetector(
107
- onTap: () async {
108
- _cardsListViewModel.storeInitialFilterStates();
109
- await showFilterWidget(context);
110
- if (_cardsListViewModel.hasFiltersChanged) {
111
- _cardsListViewModel.resetLoadingNextPageState();
112
- _cardsListViewModel.getVendors();
113
- }
114
- },
115
- child: Container(
116
- width: 32,
117
- padding: EdgeInsets.only(top: 7, bottom: 7),
118
- decoration: BoxDecoration(
119
- color: Theme.of(context).colorScheme.surface,
120
- border: Border.all(
121
- color: Colors.transparent,
122
- ),
123
- borderRadius: BorderRadius.circular(10),
124
- ),
125
- child: Image.asset(
126
- 'assets/images/filter_icon.png',
127
- color: Theme.of(context).colorScheme.onSurfaceVariant,
128
- ),
129
- ),
130
- ),
131
- );
132
- final _countryPicker = Semantics(
133
- label: S.of(context).filter_by,
134
- child: GestureDetector(
135
- onTap: () async {
136
- _cardsListViewModel.storeInitialFilterStates();
137
- await showCountryPicker(context, _cardsListViewModel);
138
- if (_cardsListViewModel.hasFiltersChanged) {
139
- _cardsListViewModel.resetLoadingNextPageState();
140
- _cardsListViewModel.getVendors();
141
- }
142
- },
143
- child: Container(
144
- padding: EdgeInsets.symmetric(horizontal: 8),
145
- decoration: BoxDecoration(
146
- color: Theme.of(context).colorScheme.surface,
147
- border: Border.all(color: Colors.transparent),
148
- borderRadius: BorderRadius.circular(10),
149
- ),
150
- child: Container(
151
- margin: EdgeInsets.symmetric(vertical: 4),
152
- child: Row(
153
- children: [
154
- Image.asset(
155
- _cardsListViewModel.selectedCountry.iconPath,
156
- width: 24,
157
- height: 24,
158
- errorBuilder: (context, error, stackTrace) => Container(
159
- width: 24,
160
- height: 24,
161
- ),
162
- ),
163
- SizedBox(width: 6),
164
- Text(
165
- _cardsListViewModel.selectedCountry.countryCode,
166
- style: Theme.of(context).textTheme.bodyMedium!.copyWith(
167
- fontSize: 16,
168
- fontWeight: FontWeight.w700,
169
- ),
170
- ),
171
- ],
172
- ),
173
- ),
174
- ),
175
- ),
176
- );
177
-
178
- return Padding(
179
- padding: const EdgeInsets.all(14.0),
180
- child: Column(
181
- children: [
182
- Container(
183
- padding: EdgeInsets.only(left: 2, right: 22),
184
- height: 32,
185
- child: Row(
186
- children: [
187
- Expanded(
188
- child: _SearchWidget(
189
- controller: _searchController,
190
- focusNode: searchFocusNode,
191
- ),
192
- ),
193
- SizedBox(width: 5),
194
- filterButton,
195
- SizedBox(width: 5),
196
- _countryPicker
197
- ],
198
- ),
199
- ),
200
- SizedBox(height: 8),
201
- Expanded(child: CakePayCardsPageBody(cardsListViewModel: _cardsListViewModel))
202
- ],
203
- ),
204
- );
205
- }
206
-
207
- Future<void> showFilterWidget(BuildContext context) async {
208
- return showPopUp<void>(
209
- context: context,
210
- builder: (BuildContext context) {
211
- return FilterWidget(filterItems: _cardsListViewModel.createFilterItems);
212
- },
213
- );
214
- }
215
-}
216
-
217
-Future<void> showCountryPicker(
218
- BuildContext context, CakePayCardsListViewModel cardsListViewModel) async {
219
- await showPopUp<void>(
220
- context: context,
221
- builder: (_) => Picker(
222
- title: S.of(context).select_your_country,
223
- items: cardsListViewModel.availableCountries,
224
- images: cardsListViewModel.availableCountries
225
- .map((e) => Image.asset(
226
- e.iconPath,
227
- errorBuilder: (context, error, stackTrace) => Container(
228
- width: 58,
229
- height: 58,
230
- ),
231
- ))
232
- .toList(),
233
- selectedAtIndex:
234
- cardsListViewModel.availableCountries.indexOf(cardsListViewModel.selectedCountry),
235
- onItemSelected: (Country country) => cardsListViewModel.setSelectedCountry(country),
236
- isSeparated: false,
237
- hintText: S.of(context).search,
238
- matchingCriteria: (Country country, String searchText) =>
239
- country.fullName.toLowerCase().contains(searchText.toLowerCase())));
240
-}
241
-
242
-class CakePayCardsPageBody extends StatefulWidget {
243
- const CakePayCardsPageBody({
244
- Key? key,
245
- required this.cardsListViewModel,
246
- }) : super(key: key);
247
-
248
- final CakePayCardsListViewModel cardsListViewModel;
249
-
250
- @override
251
- _CakePayCardsPageBodyState createState() => _CakePayCardsPageBodyState();
252
-}
253
-
254
-class _CakePayCardsPageBodyState extends State<CakePayCardsPageBody> {
255
- double get backgroundHeight => MediaQuery.of(context).size.height * 0.75;
256
- double thumbHeight = 72;
257
-
258
- bool get isAlwaysShowScrollThumb => merchantsList.isEmpty ? false : merchantsList.length > 3;
259
-
260
- List<CakePayVendor> get merchantsList => widget.cardsListViewModel.cakePayVendors;
261
-
262
- final _scrollController = ScrollController();
263
-
264
- @override
265
- void initState() {
266
- _scrollController.addListener(() {
267
- final scrollOffsetFromTop = _scrollController.hasClients
268
- ? (_scrollController.offset /
269
- _scrollController.position.maxScrollExtent *
270
- (backgroundHeight - thumbHeight))
271
- : 0.0;
272
- widget.cardsListViewModel.setScrollOffsetFromTop(scrollOffsetFromTop);
273
-
274
- double threshold = 200.0;
275
- bool isNearBottom =
276
- _scrollController.offset >= _scrollController.position.maxScrollExtent - threshold;
277
- if (isNearBottom && !_scrollController.position.outOfRange) {
278
- widget.cardsListViewModel.fetchNextPage();
279
- }
280
- });
281
- super.initState();
282
- }
283
-
284
- @override
285
- Widget build(BuildContext context) {
286
- return Observer(builder: (_) {
287
- final vendorsState = widget.cardsListViewModel.vendorsState;
288
- if (vendorsState is CakePayVendorLoadedState) {
289
- bool isLoadingMore = widget.cardsListViewModel.isLoadingNextPage;
290
- final vendors = widget.cardsListViewModel.cakePayVendors;
291
-
292
- if (vendors.isEmpty) {
293
- return Center(child: Text(S.of(context).no_cards_found));
294
- }
295
- return Stack(children: [
296
- GridView.builder(
297
- controller: _scrollController,
298
- gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
299
- crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 2 : 1,
300
- childAspectRatio: 5,
301
- crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
302
- mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
303
- ),
304
- padding: EdgeInsets.only(left: 2, right: 22),
305
- itemCount: vendors.length + (isLoadingMore ? 1 : 0),
306
- itemBuilder: (_, index) {
307
- if (index >= vendors.length) {
308
- return _VendorLoadedIndicator();
309
- }
310
- final vendor = vendors[index];
311
- return CardItem(
312
- logoUrl: vendor.card?.cardImageUrl,
313
- onTap: () {
314
- Navigator.of(context).pushNamed(Routes.cakePayBuyCardPage, arguments: [vendor]);
315
- },
316
- title: vendor.name,
317
- subTitle: vendor.card?.description ?? '',
318
- backgroundColor: Theme.of(context).colorScheme.surface,
319
- titleColor: Theme.of(context).colorScheme.onSurface,
320
- subtitleColor: Theme.of(context).colorScheme.onSecondary,
321
- discount: 0.0,
322
- );
323
- },
324
- ),
325
- isAlwaysShowScrollThumb
326
- ? CakeScrollbar(
327
- backgroundHeight: backgroundHeight,
328
- thumbHeight: thumbHeight,
329
- rightOffset: 1,
330
- width: 3,
331
- backgroundColor: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.05),
332
- thumbColor: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5),
333
- fromTop: widget.cardsListViewModel.scrollOffsetFromTop,
334
- )
335
- : Offstage()
336
- ]);
337
- }
338
- return _VendorLoadedIndicator();
339
- });
340
- }
341
-}
342
-
343
-class _VendorLoadedIndicator extends StatelessWidget {
344
- @override
345
- Widget build(BuildContext context) {
346
- return Center(
347
- child: CircularProgressIndicator(
348
- backgroundColor: Theme.of(context).colorScheme.onSurface,
349
- valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).colorScheme.primary),
350
- ),
351
- );
352
- }
353
-}
354
-
355
-class _SearchWidget extends StatelessWidget {
356
- const _SearchWidget({
357
- Key? key,
358
- required this.controller,
359
- required this.focusNode,
360
- }) : super(key: key);
361
- final TextEditingController controller;
362
- final FocusNode focusNode;
363
-
364
- @override
365
- Widget build(BuildContext context) {
366
- return BaseTextFormField(
367
- focusNode: focusNode,
368
- textStyle: Theme.of(context).textTheme.bodyMedium,
369
- controller: controller,
370
- contentPadding: EdgeInsets.only(top: 8, left: 8),
371
- hintText: S.of(context).search,
372
- placeholderTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
373
- color: Theme.of(context).colorScheme.onSurface,
374
- ),
375
- alignLabelWithHint: true,
376
- floatingLabelBehavior: FloatingLabelBehavior.never,
377
- suffixIcon: ExcludeSemantics(
378
- child: Icon(
379
- Icons.search,
380
- color: Theme.of(context).colorScheme.primary,
381
- ),
382
- ),
383
- );
384
- }
385
-}
386
-
387
-class _TrailingIcon extends StatelessWidget {
388
- const _TrailingIcon({required this.asset, this.onPressed, required this.iconColor});
389
-
390
- final String asset;
391
- final VoidCallback? onPressed;
392
- final Color iconColor;
393
-
394
- @override
395
- Widget build(BuildContext context) {
396
- return Semantics(
397
- label: S.of(context).profile,
398
- child: Material(
399
- color: Colors.transparent,
400
- child: IconButton(
401
- padding: EdgeInsets.zero,
402
- constraints: BoxConstraints(),
403
- highlightColor: Colors.transparent,
404
- onPressed: onPressed,
405
- icon: ImageIcon(AssetImage(asset), size: 25, color: iconColor),
406
- ),
407
- ));
408
- }
409
-}
lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart
deleted
-703
@@ -1,703 +0,0 @@
1
-import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
2
-import 'package:cake_wallet/core/execution_state.dart';
3
-import 'package:cake_wallet/generated/i18n.dart';
4
-import 'package:cake_wallet/routes.dart';
5
-import 'package:cake_wallet/src/screens/base_page.dart';
6
-import 'package:cake_wallet/src/screens/cake_pay/widgets/cake_pay_alert_modal.dart';
7
-import 'package:cake_wallet/src/screens/cake_pay/widgets/image_placeholder.dart';
8
-import 'package:cake_wallet/src/screens/cake_pay/widgets/link_extractor.dart';
9
-import 'package:cake_wallet/src/screens/cake_pay/widgets/text_icon_button.dart';
10
-import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
11
-import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
12
-import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
13
-import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
14
-import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
15
-import 'package:cake_wallet/src/widgets/primary_button.dart';
16
-import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
17
-import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
18
-import 'package:cake_wallet/utils/show_pop_up.dart';
19
-import 'package:cake_wallet/view_model/cake_pay/cake_pay_purchase_view_model.dart';
20
-import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
21
-import 'package:flutter/material.dart';
22
-import 'package:flutter/services.dart';
23
-import 'package:flutter_mobx/flutter_mobx.dart';
24
-import 'package:mobx/mobx.dart';
25
-import 'package:url_launcher/url_launcher.dart';
26
-
27
-class CakePayBuyCardDetailPage extends BasePage {
28
- CakePayBuyCardDetailPage(this.cakePayPurchaseViewModel);
29
-
30
- final CakePayPurchaseViewModel cakePayPurchaseViewModel;
31
-
32
- @override
33
- String get title => cakePayPurchaseViewModel.card.name;
34
-
35
- @override
36
- Widget? middle(BuildContext context) {
37
- return Text(
38
- title,
39
- textAlign: TextAlign.center,
40
- maxLines: 2,
41
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
42
- color: Theme.of(context).colorScheme.onSurface,
43
- fontWeight: FontWeight.bold,
44
- ),
45
- );
46
- }
47
-
48
- @override
49
- Widget? trailing(BuildContext context) => null;
50
-
51
- bool _effectsInstalled = false;
52
-
53
- @override
54
- Widget body(BuildContext context) {
55
- _setEffects(context);
56
-
57
- final card = cakePayPurchaseViewModel.card;
58
-
59
- return ScrollableWithBottomSection(
60
- contentPadding: EdgeInsets.zero,
61
- content: Observer(builder: (_) {
62
- return Column(
63
- children: [
64
- SizedBox(height: 36),
65
- ClipRRect(
66
- borderRadius:
67
- BorderRadius.horizontal(left: Radius.circular(20), right: Radius.circular(20)),
68
- child: Container(
69
- decoration: BoxDecoration(
70
- color: Theme.of(context).colorScheme.surfaceContainerLowest,
71
- borderRadius: BorderRadius.circular(20),
72
- border: Border.all(
73
- color: Theme.of(context).colorScheme.outline.withOpacity(0.20),
74
- ),
75
- ),
76
- child: Row(
77
- children: [
78
- Expanded(
79
- child: Container(
80
- child: ClipRRect(
81
- borderRadius: BorderRadius.horizontal(
82
- left: Radius.circular(20), right: Radius.circular(20)),
83
- child: Image.network(
84
- card.cardImageUrl ?? '',
85
- fit: BoxFit.cover,
86
- loadingBuilder: (BuildContext context, Widget child,
87
- ImageChunkEvent? loadingProgress) {
88
- if (loadingProgress == null) return child;
89
- return Center(child: CircularProgressIndicator());
90
- },
91
- errorBuilder: (context, error, stackTrace) =>
92
- CakePayCardImagePlaceholder(),
93
- ),
94
- )),
95
- ),
96
- Expanded(
97
- child: Padding(
98
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
99
- child: Column(children: [
100
- Row(
101
- children: [
102
- Text(
103
- S.of(context).value + ':',
104
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
105
- color: Theme.of(context).colorScheme.onSurface,
106
- ),
107
- ),
108
- SizedBox(width: 8),
109
- Text(
110
- '${cakePayPurchaseViewModel.amount.toStringAsFixed(2)} ${cakePayPurchaseViewModel.fiatCurrency}',
111
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
112
- color: Theme.of(context).colorScheme.onSurface,
113
- ),
114
- ),
115
- ],
116
- ),
117
- SizedBox(height: 16),
118
- Row(
119
- children: [
120
- Text(
121
- S.of(context).quantity + ':',
122
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
123
- color: Theme.of(context).colorScheme.onSurface,
124
- ),
125
- ),
126
- SizedBox(width: 8),
127
- Text(
128
- '${cakePayPurchaseViewModel.quantity}',
129
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
130
- color: Theme.of(context).colorScheme.onSurface,
131
- ),
132
- ),
133
- ],
134
- ),
135
- SizedBox(height: 16),
136
- Row(
137
- children: [
138
- Text(
139
- S.of(context).total + ':',
140
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
141
- color: Theme.of(context).colorScheme.onSurface,
142
- ),
143
- ),
144
- SizedBox(width: 8),
145
- Text(
146
- '${cakePayPurchaseViewModel.totalAmount.toStringAsFixed(2)} ${cakePayPurchaseViewModel.fiatCurrency}',
147
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
148
- color: Theme.of(context).colorScheme.onSurface,
149
- ),
150
- ),
151
- ],
152
- ),
153
- ]),
154
- ),
155
- )
156
- ],
157
- ),
158
- ),
159
- ),
160
- SizedBox(height: 20),
161
- Padding(
162
- padding: const EdgeInsets.symmetric(horizontal: 24.0),
163
- child: TextIconButton(
164
- label: S.of(context).how_to_use_card,
165
- onTap: () => _showHowToUseCard(context, card),
166
- ),
167
- ),
168
- SizedBox(height: 20),
169
- if (card.expiryAndValidity != null && card.expiryAndValidity!.isNotEmpty)
170
- Padding(
171
- padding: const EdgeInsets.symmetric(horizontal: 24.0),
172
- child: Column(
173
- crossAxisAlignment: CrossAxisAlignment.start,
174
- children: [
175
- Text(S.of(context).expiry_and_validity + ':',
176
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
177
- color: Theme.of(context).colorScheme.onSurface,
178
- fontWeight: FontWeight.w600,
179
- )),
180
- SizedBox(height: 10),
181
- Container(
182
- width: double.infinity,
183
- decoration: BoxDecoration(
184
- border: Border(
185
- bottom: BorderSide(
186
- color: Theme.of(context).colorScheme.outline.withOpacity(0.20),
187
- width: 1,
188
- ),
189
- ),
190
- ),
191
- child: Padding(
192
- padding: const EdgeInsets.only(bottom: 8.0),
193
- child: Text(
194
- card.expiryAndValidity ?? '',
195
- style: Theme.of(context).textTheme.bodyLarge?.copyWith(
196
- color: Theme.of(context).colorScheme.onSurfaceVariant,
197
- ),
198
- ),
199
- ),
200
- ),
201
- ],
202
- ),
203
- ),
204
- ],
205
- );
206
- }),
207
- bottomSection: Column(
208
- children: [
209
- Padding(
210
- padding: EdgeInsets.only(bottom: 12),
211
- child: Observer(builder: (_) {
212
- return LoadingPrimaryButton(
213
- isDisabled: cakePayPurchaseViewModel.isPurchasing,
214
- isLoading: cakePayPurchaseViewModel.isPurchasing ||
215
- cakePayPurchaseViewModel.sendViewModel.state is IsExecutingState,
216
- onPressed: () => confirmPurchaseFirst(context),
217
- text: S.of(context).purchase_gift_card,
218
- color: Theme.of(context).colorScheme.primary,
219
- textColor: Theme.of(context).colorScheme.onPrimary,
220
- );
221
- }),
222
- ),
223
- SizedBox(height: 8),
224
- InkWell(
225
- onTap: () => _showTermsAndCondition(context, card.termsAndConditions),
226
- child: Text(
227
- S.of(context).settings_terms_and_conditions,
228
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
229
- color: Theme.of(context).colorScheme.primary,
230
- fontWeight: FontWeight.w600,
231
- ),
232
- ),
233
- ),
234
- SizedBox(height: 16)
235
- ],
236
- ),
237
- );
238
- }
239
-
240
- void _showTermsAndCondition(BuildContext context, String? termsAndConditions) {
241
- showPopUp<void>(
242
- context: context,
243
- builder: (BuildContext context) {
244
- return CakePayAlertModal(
245
- title: S.of(context).settings_terms_and_conditions,
246
- content: Align(
247
- alignment: Alignment.bottomLeft,
248
- child: ClickableLinksText(
249
- text: termsAndConditions ?? '',
250
- textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
251
- color: Theme.of(context).colorScheme.onSurfaceVariant,
252
- fontSize: 18,
253
- ) ??
254
- Theme.of(context).textTheme.bodyMedium!.copyWith(
255
- color: Theme.of(context).colorScheme.onSurfaceVariant,
256
- fontSize: 18,
257
- ),
258
- ),
259
- ),
260
- actionTitle: S.of(context).agree,
261
- showCloseButton: false,
262
- heightFactor: 0.6,
263
- );
264
- });
265
- }
266
-
267
- Future<void> _showconfirmPurchaseFirstAlert(BuildContext context) async {
268
- if (!cakePayPurchaseViewModel.confirmsNoVpn ||
269
- !cakePayPurchaseViewModel.confirmsVoidedRefund ||
270
- !cakePayPurchaseViewModel.confirmsTermsAgreed) {
271
- await showPopUp<void>(
272
- context: context,
273
- builder: (BuildContext context) => ThreeCheckboxAlert(
274
- alertTitle: S.of(context).cakepay_confirm_purchase,
275
- leftButtonText: S.of(context).cancel,
276
- rightButtonText: S.of(context).confirm,
277
- actionLeftButton: () {
278
- cakePayPurchaseViewModel.isPurchasing = false;
279
- Navigator.of(context).pop();
280
- },
281
- actionRightButton: (confirmsNoVpn, confirmsVoidedRefund, confirmsTermsAgreed) {
282
- cakePayPurchaseViewModel.confirmsNoVpn = confirmsNoVpn;
283
- cakePayPurchaseViewModel.confirmsVoidedRefund = confirmsVoidedRefund;
284
- cakePayPurchaseViewModel.confirmsTermsAgreed = confirmsTermsAgreed;
285
-
286
- Navigator.of(context).pop();
287
- },
288
- ),
289
- );
290
- }
291
-
292
- if (cakePayPurchaseViewModel.confirmsNoVpn &&
293
- cakePayPurchaseViewModel.confirmsVoidedRefund &&
294
- cakePayPurchaseViewModel.confirmsTermsAgreed) {
295
- await purchaseCard(context);
296
- }
297
- }
298
-
299
- Future<void> confirmPurchaseFirst(BuildContext context) async {
300
- bool isLogged = await cakePayPurchaseViewModel.cakePayService.isLogged();
301
- if (!isLogged) {
302
- Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
303
- } else {
304
- cakePayPurchaseViewModel.isPurchasing = true;
305
- await _showconfirmPurchaseFirstAlert(context);
306
- }
307
- }
308
-
309
- Future<void> purchaseCard(BuildContext context) async {
310
- bool isLogged = await cakePayPurchaseViewModel.cakePayService.isLogged();
311
- if (!isLogged) {
312
- Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
313
- } else {
314
- try {
315
- await cakePayPurchaseViewModel.createOrder();
316
- } catch (_) {
317
- await cakePayPurchaseViewModel.cakePayService.logout();
318
- }
319
- }
320
- cakePayPurchaseViewModel.isPurchasing = false;
321
- }
322
-
323
- void _showHowToUseCard(
324
- BuildContext context,
325
- CakePayCard card,
326
- ) {
327
- showPopUp<void>(
328
- context: context,
329
- builder: (BuildContext context) {
330
- return CakePayAlertModal(
331
- title: S.of(context).how_to_use_card,
332
- content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
333
- Padding(
334
- padding: EdgeInsets.all(10),
335
- child: Text(
336
- card.name,
337
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
338
- color: Theme.of(context).colorScheme.onSurface,
339
- fontWeight: FontWeight.w600,
340
- ),
341
- )),
342
- ClickableLinksText(
343
- text: card.howToUse ?? '',
344
- textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
345
- color: Theme.of(context).colorScheme.onSurfaceVariant,
346
- ) ??
347
- Theme.of(context).textTheme.bodyMedium!.copyWith(
348
- color: Theme.of(context).colorScheme.onSurfaceVariant,
349
- ),
350
- linkStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
351
- color: Theme.of(context).colorScheme.onSurface,
352
- fontStyle: FontStyle.italic,
353
- ) ??
354
- Theme.of(context).textTheme.bodyMedium!.copyWith(
355
- color: Theme.of(context).colorScheme.onSurface,
356
- fontStyle: FontStyle.italic,
357
- ),
358
- ),
359
- ]),
360
- actionTitle: S.current.got_it,
361
- );
362
- });
363
- }
364
-
365
- Future<void> _showConfirmSendingAlert(BuildContext context) async {
366
- if (cakePayPurchaseViewModel.order == null) {
367
- return;
368
- }
369
- ReactionDisposer? disposer;
370
-
371
- disposer = reaction((_) => cakePayPurchaseViewModel.isOrderExpired, (bool isExpired) {
372
- if (isExpired) {
373
- if (Navigator.of(context).canPop()) {
374
- Navigator.of(context).pop();
375
- }
376
- if (disposer != null) {
377
- disposer();
378
- }
379
- }
380
- });
381
-
382
- final order = cakePayPurchaseViewModel.order;
383
-
384
- showModalBottomSheet<void>(
385
- context: context,
386
- isDismissible: false,
387
- isScrollControlled: true,
388
- builder: (BuildContext popupContext) {
389
- return ConfirmSendingBottomSheet(
390
- key: ValueKey('send_page_confirm_sending_dialog_key'),
391
- currentTheme: currentTheme,
392
- walletType: cakePayPurchaseViewModel.sendViewModel.walletType,
393
- paymentId: S.of(popupContext).payment_id,
394
- paymentIdValue: order?.orderId,
395
- expirationTime: cakePayPurchaseViewModel.formattedRemainingTime,
396
- titleText: S.of(popupContext).confirm_transaction,
397
- titleIconPath: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
398
- currency: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency,
399
- amount: S.of(popupContext).send_amount,
400
- amountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.amountFormatted,
401
- fiatAmountValue:
402
- cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
403
- fee: S.of(popupContext).send_fee,
404
- feeValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.feeFormatted,
405
- feeFiatAmount:
406
- cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
407
- outputs: cakePayPurchaseViewModel.sendViewModel.outputs,
408
- onSlideComplete: () async {
409
- Navigator.of(popupContext).pop();
410
- cakePayPurchaseViewModel.sendViewModel.commitTransaction(context);
411
- },
412
- );
413
- },
414
- );
415
- }
416
-
417
- BuildContext? loadingBottomSheetContext;
418
-
419
- void _setEffects(BuildContext context) {
420
- if (_effectsInstalled) {
421
- return;
422
- }
423
-
424
- reaction((_) => cakePayPurchaseViewModel.sendViewModel.state, (ExecutionState state) {
425
- if (state is FailureState) {
426
- WidgetsBinding.instance.addPostFrameCallback((_) {
427
- if (context.mounted) showStateAlert(context, S.of(context).error, state.error);
428
- });
429
- }
430
-
431
- if (state is! IsExecutingState &&
432
- loadingBottomSheetContext != null &&
433
- loadingBottomSheetContext!.mounted) {
434
- Navigator.of(loadingBottomSheetContext!).pop();
435
- }
436
-
437
- if (state is IsExecutingState) {
438
- WidgetsBinding.instance.addPostFrameCallback((_) {
439
- if (context.mounted) {
440
- showModalBottomSheet<void>(
441
- context: context,
442
- isDismissible: false,
443
- builder: (BuildContext context) {
444
- loadingBottomSheetContext = context;
445
- return LoadingBottomSheet(
446
- titleText: S.of(context).generating_transaction,
447
- );
448
- },
449
- );
450
- }
451
- });
452
- }
453
-
454
- if (state is ExecutedSuccessfullyState) {
455
- WidgetsBinding.instance.addPostFrameCallback((_) async {
456
- await _showConfirmSendingAlert(context);
457
- });
458
- }
459
-
460
- if (state is TransactionCommitted) {
461
- WidgetsBinding.instance.addPostFrameCallback((_) {
462
- cakePayPurchaseViewModel.sendViewModel.clearOutputs();
463
- if (context.mounted) showSentAlert(context);
464
- });
465
- }
466
- });
467
-
468
- _effectsInstalled = true;
469
- }
470
-
471
- void showStateAlert(BuildContext context, String title, String content) {
472
- if (context.mounted) {
473
- showPopUp<void>(
474
- context: context,
475
- builder: (BuildContext context) {
476
- return AlertWithOneAction(
477
- alertTitle: title,
478
- alertContent: content,
479
- buttonText: S.of(context).ok,
480
- buttonAction: () => Navigator.of(context).pop());
481
- });
482
- }
483
- }
484
-
485
- Future<void> showSentAlert(BuildContext context) async {
486
- if (!context.mounted) {
487
- return;
488
- }
489
- final order = cakePayPurchaseViewModel.order!.orderId;
490
- final isCopy = await showPopUp<bool>(
491
- context: context,
492
- builder: (BuildContext context) {
493
- return AlertWithTwoActions(
494
- alertTitle: S.of(context).transaction_sent,
495
- alertContent: S.of(context).cake_pay_save_order + '\n${order}',
496
- leftButtonText: S.of(context).ignor,
497
- rightButtonText: S.of(context).copy,
498
- actionLeftButton: () => Navigator.of(context).pop(false),
499
- actionRightButton: () => Navigator.of(context).pop(true));
500
- }) ??
501
- false;
502
-
503
- if (isCopy) {
504
- await Clipboard.setData(ClipboardData(text: order));
505
- }
506
- }
507
-
508
- void _handleDispose(ReactionDisposer? disposer) {
509
- cakePayPurchaseViewModel.dispose();
510
- if (disposer != null) {
511
- disposer();
512
- }
513
- }
514
-}
515
-
516
-class ThreeCheckboxAlert extends BaseAlertDialog {
517
- ThreeCheckboxAlert({
518
- required this.alertTitle,
519
- required this.leftButtonText,
520
- required this.rightButtonText,
521
- required this.actionLeftButton,
522
- required this.actionRightButton,
523
- this.alertBarrierDismissible = true,
524
- Key? key,
525
- });
526
-
527
- final String alertTitle;
528
- final String leftButtonText;
529
- final String rightButtonText;
530
- final VoidCallback actionLeftButton;
531
- final Function(bool, bool, bool) actionRightButton;
532
- final bool alertBarrierDismissible;
533
-
534
- bool checkbox1 = false;
535
- void toggleCheckbox1() => checkbox1 = !checkbox1;
536
- bool checkbox2 = false;
537
- void toggleCheckbox2() => checkbox2 = !checkbox2;
538
- bool checkbox3 = false;
539
- void toggleCheckbox3() => checkbox3 = !checkbox3;
540
-
541
- bool showValidationMessage = true;
542
-
543
- @override
544
- String get titleText => alertTitle;
545
- @override
546
- bool get isDividerExists => true;
547
-
548
- @override
549
- String get leftActionButtonText => leftButtonText;
550
- @override
551
- String get rightActionButtonText => rightButtonText;
552
- @override
553
- VoidCallback get actionLeft => actionLeftButton;
554
- @override
555
- VoidCallback get actionRight => () {
556
- actionRightButton(checkbox1, checkbox2, checkbox3);
557
- };
558
-
559
- @override
560
- bool get barrierDismissible => alertBarrierDismissible;
561
-
562
- @override
563
- Widget content(BuildContext context) {
564
- return ThreeCheckboxAlertContent(
565
- checkbox1: checkbox1,
566
- toggleCheckbox1: toggleCheckbox1,
567
- checkbox2: checkbox2,
568
- toggleCheckbox2: toggleCheckbox2,
569
- checkbox3: checkbox3,
570
- toggleCheckbox3: toggleCheckbox3,
571
- );
572
- }
573
-}
574
-
575
-class ThreeCheckboxAlertContent extends StatefulWidget {
576
- ThreeCheckboxAlertContent({
577
- required this.checkbox1,
578
- required this.toggleCheckbox1,
579
- required this.checkbox2,
580
- required this.toggleCheckbox2,
581
- required this.checkbox3,
582
- required this.toggleCheckbox3,
583
- Key? key,
584
- }) : super(key: key);
585
-
586
- bool checkbox1;
587
- void Function() toggleCheckbox1;
588
- bool checkbox2;
589
- void Function() toggleCheckbox2;
590
- bool checkbox3;
591
- void Function() toggleCheckbox3;
592
-
593
- @override
594
- _ThreeCheckboxAlertContentState createState() => _ThreeCheckboxAlertContentState(
595
- checkbox1: checkbox1,
596
- toggleCheckbox1: toggleCheckbox1,
597
- checkbox2: checkbox2,
598
- toggleCheckbox2: toggleCheckbox2,
599
- checkbox3: checkbox3,
600
- toggleCheckbox3: toggleCheckbox3,
601
- );
602
-
603
- static _ThreeCheckboxAlertContentState? of(BuildContext context) {
604
- return context.findAncestorStateOfType<_ThreeCheckboxAlertContentState>();
605
- }
606
-}
607
-
608
-class _ThreeCheckboxAlertContentState extends State<ThreeCheckboxAlertContent> {
609
- _ThreeCheckboxAlertContentState({
610
- required this.checkbox1,
611
- required this.toggleCheckbox1,
612
- required this.checkbox2,
613
- required this.toggleCheckbox2,
614
- required this.checkbox3,
615
- required this.toggleCheckbox3,
616
- });
617
-
618
- bool checkbox1;
619
- void Function() toggleCheckbox1;
620
- bool checkbox2;
621
- void Function() toggleCheckbox2;
622
- bool checkbox3;
623
- void Function() toggleCheckbox3;
624
-
625
- bool showValidationMessage = true;
626
-
627
- bool get areAllCheckboxesChecked => checkbox1 && checkbox2 && checkbox3;
628
-
629
- @override
630
- Widget build(BuildContext context) {
631
- return Form(
632
- child: Column(
633
- mainAxisSize: MainAxisSize.min,
634
- children: [
635
- StandardCheckbox(
636
- value: checkbox1,
637
- caption: S.of(context).cakepay_confirm_no_vpn,
638
- onChanged: (bool? value) {
639
- setState(() {
640
- checkbox1 = value ?? false;
641
- toggleCheckbox1();
642
- showValidationMessage = !areAllCheckboxesChecked;
643
- });
644
- },
645
- ),
646
- StandardCheckbox(
647
- value: checkbox2,
648
- caption: S.of(context).cakepay_confirm_voided_refund,
649
- onChanged: (bool? value) {
650
- setState(() {
651
- checkbox2 = value ?? false;
652
- toggleCheckbox2();
653
- showValidationMessage = !areAllCheckboxesChecked;
654
- });
655
- },
656
- ),
657
- StandardCheckbox(
658
- value: checkbox3,
659
- caption: S.of(context).cakepay_confirm_terms_agreed,
660
- onChanged: (bool? value) {
661
- setState(() {
662
- checkbox3 = value ?? false;
663
- toggleCheckbox3();
664
- showValidationMessage = !areAllCheckboxesChecked;
665
- });
666
- },
667
- ),
668
- GestureDetector(
669
- behavior: HitTestBehavior.opaque,
670
- onTap: () => launchUrl(
671
- Uri.parse("https://www.cakepay.com/terms/"),
672
- mode: LaunchMode.externalApplication,
673
- ),
674
- child: Padding(
675
- padding: const EdgeInsets.only(top: 8.0),
676
- child: Text(
677
- S.of(context).settings_terms_and_conditions,
678
- style: Theme.of(context).textTheme.bodyMedium!.copyWith(
679
- fontSize: 16,
680
- color: Theme.of(context).colorScheme.primary,
681
- decoration: TextDecoration.none,
682
- height: 1,
683
- ),
684
- softWrap: true,
685
- ),
686
- ),
687
- ),
688
- if (showValidationMessage)
689
- Padding(
690
- padding: const EdgeInsets.only(top: 8.0),
691
- child: Text(
692
- 'Please confirm all checkboxes',
693
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
694
- color: Theme.of(context).colorScheme.errorContainer,
695
- decoration: TextDecoration.none,
696
- ),
697
- ),
698
- ),
699
- ],
700
- ),
701
- );
702
- }
703
-}
lib/src/screens/cake_pay/widgets/cake_pay_alert_modal.dart
deleted
-86
@@ -1,86 +0,0 @@
1
-import 'package:cake_wallet/src/widgets/alert_background.dart';
2
-import 'package:cake_wallet/src/widgets/primary_button.dart';
3
-import 'package:flutter/material.dart';
4
-
5
-class CakePayAlertModal extends StatelessWidget {
6
- const CakePayAlertModal({
7
- Key? key,
8
- required this.title,
9
- required this.content,
10
- required this.actionTitle,
11
- this.heightFactor = 0.4,
12
- this.showCloseButton = true,
13
- }) : super(key: key);
14
-
15
- final String title;
16
- final Widget content;
17
- final String actionTitle;
18
- final bool showCloseButton;
19
- final double heightFactor;
20
-
21
- @override
22
- Widget build(BuildContext context) {
23
- return AlertBackground(
24
- child: Material(
25
- color: Colors.transparent,
26
- child: Column(
27
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
28
- children: [
29
- Spacer(),
30
- Container(
31
- padding: EdgeInsets.only(top: 24, left: 24, right: 24),
32
- margin: EdgeInsets.all(24),
33
- decoration: BoxDecoration(
34
- color: Theme.of(context).colorScheme.surface,
35
- borderRadius: BorderRadius.circular(30),
36
- ),
37
- child: Column(
38
- children: [
39
- if (title.isNotEmpty)
40
- Text(
41
- title,
42
- style: Theme.of(context).textTheme.titleLarge?.copyWith(
43
- color: Theme.of(context).colorScheme.onSurface,
44
- fontWeight: FontWeight.bold,
45
- ),
46
- ),
47
- Container(
48
- constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * heightFactor),
49
- child: ListView(
50
- children: [
51
- content,
52
- SizedBox(height: 35),
53
- ],
54
- ),
55
- ),
56
- PrimaryButton(
57
- onPressed: () => Navigator.pop(context),
58
- text: actionTitle,
59
- color: Theme.of(context).colorScheme.surfaceContainer,
60
- textColor: Theme.of(context).colorScheme.primary,
61
- ),
62
- SizedBox(height: 21),
63
- ],
64
- ),
65
- ),
66
- Spacer(),
67
- if(showCloseButton)
68
- InkWell(
69
- onTap: () => Navigator.pop(context),
70
- child: Container(
71
- margin: EdgeInsets.only(bottom: 40),
72
- child: CircleAvatar(
73
- child: Icon(
74
- Icons.close,
75
- color: Theme.of(context).colorScheme.onSurface,
76
- ),
77
- backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
78
- ),
79
- ),
80
- )
81
- ],
82
- ),
83
- ),
84
- );
85
- }
86
-}
\ No newline at end of file
lib/src/screens/cake_pay/widgets/card_menu.dart
deleted
-11
@@ -1,11 +0,0 @@
1
-import 'package:flutter/material.dart';
2
-
3
-class CardMenu extends StatelessWidget {
4
-
5
- @override
6
- Widget build(BuildContext context) {
7
- return Container(
8
-
9
- );
10
- }
11
-}
\ No newline at end of file
lib/src/screens/dashboard/widgets/filter_widget.dart
+9
-6
@@ -78,11 +78,14 @@ class _FilterWidgetState extends State<FilterWidget> {
78
children: [
79
Padding(
80
padding: const EdgeInsets.all(24.0),
81
- child: Text(
82
- S.of(context).filter_by,
83
- style: Theme.of(context).textTheme.titleMedium?.copyWith(
84
- color: Theme.of(context).colorScheme.onSurface,
85
- fontWeight: FontWeight.bold,
81
+ child: Align(
82
+ alignment: Alignment.center,
83
+ child: Text(
84
+ S.of(context).filter_by,
85
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(
86
+ color: Theme.of(context).colorScheme.onSurface,
87
+ fontWeight: FontWeight.bold,
88
+ ),
89
),
90
),
91
),
@@ -105,7 +108,7 @@ class _FilterWidgetState extends State<FilterWidget> {
108
109
final Widget sectionListView = ListView.builder(
110
controller: isSectionScrollable ? _scrollController : null,
108
- padding: const EdgeInsets.symmetric(horizontal: 28.0),
111
+ padding: const EdgeInsets.symmetric(horizontal: 28.0, vertical: 8.0),
112
shrinkWrap: isSectionScrollable ? false : true,
113
physics: isSectionScrollable
114
? const BouncingScrollPhysics()
lib/src/screens/dashboard/widgets/present_receive_option_picker.dart
+1
-1
@@ -1,5 +1,5 @@
1
import 'package:cake_wallet/src/widgets/alert_close_button.dart';
2
-import 'package:cake_wallet/src/screens/cake_pay/widgets/rounded_checkbox.dart';
2
+import 'package:cake_wallet/src/widgets/rounded_checkbox.dart';
3
import 'package:cake_wallet/src/widgets/alert_background.dart';
4
import 'package:cake_wallet/typography.dart';
5
import 'package:cake_wallet/utils/show_pop_up.dart';
lib/src/screens/exchange_trade/exchange_trade_page.dart
+7
-4
@@ -3,6 +3,7 @@ import 'package:cake_wallet/routes.dart';
3
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
4
import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
5
import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
6
+import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
7
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
8
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
9
import 'package:cake_wallet/themes/core/material_base_theme.dart';
@@ -274,6 +275,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
275
return ConfirmSendingBottomSheet(
276
key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'),
277
currentTheme: widget.currentTheme,
278
+ footerType: FooterType.slideActionButton,
279
walletType: widget.exchangeTradeViewModel.sendViewModel.walletType,
280
titleText: S.of(bottomSheetContext).confirm_transaction,
281
titleIconPath:
@@ -293,7 +295,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
295
feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel
296
.pendingTransactionFeeFiatAmountFormatted,
297
outputs: widget.exchangeTradeViewModel.sendViewModel.outputs,
296
- onSlideComplete: () async {
298
+ onSlideActionComplete: () async {
299
if (bottomSheetContext.mounted) {
300
Navigator.of(bottomSheetContext).pop();
301
}
@@ -317,11 +319,12 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
319
builder: (BuildContext bottomSheetContext) {
320
return InfoBottomSheet(
321
currentTheme: widget.currentTheme,
322
+ footerType: FooterType.singleActionButton,
323
titleText: S.of(bottomSheetContext).transaction_sent,
324
contentImage: 'assets/images/birthday_cake.png',
322
- actionButtonText: S.of(bottomSheetContext).close,
323
- actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
324
- actionButton: () {
325
+ singleActionButtonText: S.of(bottomSheetContext).close,
326
+ singleActionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
327
+ onSingleActionButtonPressed: () {
328
Navigator.of(bottomSheetContext).pop();
329
if (mounted) {
330
Navigator.of(context).pushNamedAndRemoveUntil(
lib/src/screens/integrations/deuro/savings_page.dart
+24
-18
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/screens/integrations/deuro/widgets/interest_card
6
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart';
7
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart';
8
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
9
+import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
10
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
11
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
12
import 'package:cake_wallet/src/widgets/gradient_background.dart';
@@ -166,6 +167,7 @@ class DEuroSavingsPage extends BasePage {
167
isScrollControlled: true,
168
builder: (BuildContext bottomSheetContext) => ConfirmSendingBottomSheet(
169
key: ValueKey('savings_page_confirm_sending_dialog_key'),
170
+ footerType: FooterType.slideActionButton,
171
titleText: title,
172
currentTheme: currentTheme,
173
walletType: WalletType.ethereum,
@@ -182,7 +184,7 @@ class DEuroSavingsPage extends BasePage {
184
feeValue: tx.feeFormatted,
185
feeFiatAmount: _dEuroViewModel.pendingTransactionFeeFiatAmountFormatted,
186
outputs: [],
185
- onSlideComplete: () async {
187
+ onSlideActionComplete: () async {
188
Navigator.of(bottomSheetContext).pop(true);
189
dEuroViewModel.commitTransaction();
190
},
@@ -201,6 +203,7 @@ class DEuroSavingsPage extends BasePage {
203
isScrollControlled: true,
204
builder: (BuildContext bottomSheetContext) => ConfirmSendingBottomSheet(
205
key: ValueKey('savings_page_confirm_approval_dialog_key'),
206
+ footerType: FooterType.slideActionButton,
207
titleText: S.of(bottomSheetContext).approve_tokens,
208
currentTheme: currentTheme,
209
walletType: WalletType.ethereum,
@@ -213,7 +216,7 @@ class DEuroSavingsPage extends BasePage {
216
feeValue: tx.feeFormatted,
217
feeFiatAmount: "",
218
outputs: [],
216
- onSlideComplete: () {
219
+ onSlideActionComplete: () {
220
Navigator.of(bottomSheetContext).pop(true);
221
dEuroViewModel.commitApprovalTransaction();
222
},
@@ -234,13 +237,14 @@ class DEuroSavingsPage extends BasePage {
237
context: context,
238
isDismissible: false,
239
builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
240
+ footerType: FooterType.singleActionButton,
241
currentTheme: currentTheme,
242
titleText: S.of(bottomSheetContext).transaction_sent,
243
contentImage: 'assets/images/birthday_cake.png',
244
content: S.of(bottomSheetContext).deuro_tx_commited_content,
241
- actionButtonText: S.of(bottomSheetContext).close,
242
- actionButtonKey: ValueKey('savings_page_sent_dialog_ok_button_key'),
243
- actionButton: () => Navigator.of(bottomSheetContext).pop(),
245
+ singleActionButtonText: S.of(bottomSheetContext).close,
246
+ singleActionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
247
+ onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
248
),
249
);
250
});
@@ -302,13 +306,13 @@ class DEuroSavingsPage extends BasePage {
306
titleText: title,
307
titleIconPath: CryptoCurrency.deuro.iconPath,
308
content: content,
305
- isTwoAction: true,
306
- rightButtonText: S.of(context).close,
309
+ footerType: FooterType.doubleActionButton,
310
+ doubleActionRightButtonText: S.of(context).close,
311
rightActionButtonKey: ValueKey('deuro_page_tooltip_dialog_${key}_ok_button_key'),
308
- actionRightButton: () => Navigator.of(bottomSheetContext).pop(),
309
- leftButtonText: S.of(context).learn_more,
312
+ onRightActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
313
+ doubleActionLeftButtonText: S.of(context).learn_more,
314
leftActionButtonKey: ValueKey('deuro_page_tooltip_dialog_${key}_learn_more_button_key'),
311
- actionLeftButton: onLearnMorePressed,
315
+ onLeftActionButtonPressed: onLearnMorePressed,
316
),
317
);
318
}
@@ -327,13 +331,13 @@ class DEuroSavingsPage extends BasePage {
331
contentImage: 'assets/images/deuro_hero.png',
332
contentImageSize: 200,
333
content: S.of(context).deuro_savings_welcome_description,
330
- isTwoAction: true,
331
- rightButtonText: S.of(context).close,
334
+ footerType: FooterType.doubleActionButton,
335
+ doubleActionRightButtonText: S.of(context).close,
336
rightActionButtonKey: ValueKey('deuro_page_tooltip_dialog_welcome_ok_button_key'),
333
- actionRightButton: () => Navigator.of(bottomSheetContext).pop(),
334
- leftButtonText: S.of(context).learn_more,
337
+ onRightActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
338
+ doubleActionLeftButtonText: S.of(context).learn_more,
339
leftActionButtonKey: ValueKey('deuro_page_tooltip_dialog_welcome_learn_more_button_key'),
336
- actionLeftButton: () => launchUrlString("https://deuro.com/what-is-deuro.html"),
340
+ onLeftActionButtonPressed: () => launchUrlString("https://deuro.com/what-is-deuro.html"),
341
showDisclaimerText: _dEuroViewModel.isFistTime,
342
),
343
);
@@ -352,9 +356,9 @@ class DEuroSavingsPage extends BasePage {
356
titleIconPath: CryptoCurrency.deuro.iconPath,
357
contentImage: 'assets/images/deuro_not_enough_eth.png',
358
content: S.of(context).deuro_tooltip_no_eth,
355
- actionButtonKey: ValueKey('deuro_page_tooltip_dialog_no_eth_ok_button_key'),
356
- actionButtonText: S.of(context).close,
357
- actionButton: () => Navigator.of(bottomSheetContext).pop(),
359
+ singleActionButtonKey: ValueKey('deuro_page_tooltip_dialog_no_eth_ok_button_key'),
360
+ singleActionButtonText: S.of(context).close,
361
+ onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(), footerType: FooterType.singleActionButton,
362
),
363
);
364
}
@@ -372,6 +376,8 @@ class DEuroSavingsPage extends BasePage {
376
? S.of(context).deuro_savings_available_to_add
377
: S.of(context).deuro_savings_available_to_remove,
378
balance: isAdding ? _dEuroViewModel.accountBalance : _dEuroViewModel.savingsBalance,
379
+ footerType: FooterType.none,
380
+ maxHeight: MediaQuery.of(context).size.height * 0.8,
381
),
382
);
383
}
lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart
+2
-2
@@ -14,18 +14,18 @@ class SavingsEditSheet extends BaseBottomSheet {
14
super.titleIconPath,
15
this.balance,
16
this.balanceTitle,
17
+ required super.footerType, required super.maxHeight,
18
});
19
20
@override
21
Widget contentWidget(BuildContext context) => SizedBox(
21
- height: 600,
22
+ height: 500,
23
child: _SavingsEditBody(
24
balance: balance,
25
balanceTitle: balanceTitle,
26
),
27
);
28
28
- @override
29
Widget footerWidget(BuildContext context) => SizedBox.shrink();
30
}
31
lib/src/screens/receive/widgets/qr_widget.dart
+8
-8
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dar
6
import 'package:cake_wallet/themes/core/material_base_theme.dart';
7
import 'package:cake_wallet/generated/i18n.dart';
8
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
9
+import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
10
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
11
import 'package:cake_wallet/utils/address_formatter.dart';
12
import 'package:cake_wallet/utils/brightness_util.dart';
@@ -319,14 +320,13 @@ class QRWidget extends StatelessWidget {
320
titleText: S.of(context).payjoin_unavailable_sheet_title,
321
content: S.of(context).payjoin_unavailable_sheet_content,
322
currentTheme: currentTheme,
322
- isTwoAction: true,
323
- leftButtonText: S.of(context).learn_more,
324
- actionLeftButton: () => launchUrl(
325
- Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin/#payjoin"),
326
- mode: LaunchMode.externalApplication,
327
- ),
328
- rightButtonText: S.of(context).ok,
329
- actionRightButton: () => Navigator.of(context).pop(),
323
+ footerType: FooterType.doubleActionButton,
324
+ doubleActionLeftButtonText: S.of(context).learn_more,
325
+ onLeftActionButtonPressed: () => launchUrl(
326
+ Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin/#payjoin"),
327
+ mode: LaunchMode.externalApplication),
328
+ doubleActionRightButtonText: S.of(context).ok,
329
+ onRightActionButtonPressed: () => Navigator.of(context).pop(),
330
),
331
);
332
}
lib/src/screens/send/send_page.dart
+66
-42
@@ -17,12 +17,14 @@ import 'package:cake_wallet/src/widgets/adaptable_page_view.dart';
17
import 'package:cake_wallet/src/widgets/add_template_button.dart';
18
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
19
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
20
+import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
21
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
22
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
23
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
24
import 'package:cake_wallet/src/widgets/picker.dart';
25
import 'package:cake_wallet/src/widgets/primary_button.dart';
26
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
27
+import 'package:cake_wallet/src/widgets/simple_checkbox.dart';
28
import 'package:cake_wallet/src/widgets/template_tile.dart';
29
import 'package:cake_wallet/src/widgets/trail_button.dart';
30
import 'package:cake_wallet/utils/payment_request.dart';
@@ -569,6 +571,7 @@ class SendPage extends BasePage {
571
key: ValueKey('send_page_confirm_sending_dialog_key'),
572
titleText: S.of(bottomSheetContext).confirm_transaction,
573
currentTheme: currentTheme,
574
+ footerType: FooterType.slideActionButton,
575
walletType: sendViewModel.walletType,
576
titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath,
577
currency: sendViewModel.selectedCryptoCurrency,
@@ -581,7 +584,7 @@ class SendPage extends BasePage {
584
feeValue: sendViewModel.pendingTransaction!.feeFormatted,
585
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
586
outputs: sendViewModel.outputs,
584
- onSlideComplete: () async {
587
+ onSlideActionComplete: () async {
588
Navigator.of(bottomSheetContext).pop(true);
589
sendViewModel.commitTransaction(context);
590
},
@@ -611,48 +614,69 @@ class SendPage extends BasePage {
614
615
bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup);
616
614
- await showModalBottomSheet<void>(
615
- context: context,
616
- isDismissible: false,
617
- builder: (BuildContext bottomSheetContext) {
618
- return showContactSheet && sendViewModel.ocpRequest == null
619
- ? InfoBottomSheet(
620
- currentTheme: currentTheme,
621
- showDontAskMeCheckbox: true,
622
- onCheckboxChanged: (value) => sendViewModel.setShowAddressBookPopup(!value),
623
- titleText: S.of(bottomSheetContext).transaction_sent,
624
- contentImage: 'assets/images/contact.png',
625
- contentImageColor: Theme.of(context).colorScheme.onSurface,
626
- content: S.of(bottomSheetContext).add_contact_to_address_book,
627
- isTwoAction: true,
628
- leftButtonText: 'No',
629
- rightButtonText: 'Yes',
630
- actionLeftButton: () {
631
- Navigator.of(bottomSheetContext).pop();
632
- if (context.mounted) {
633
- Navigator.of(context)
634
- .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
635
- }
636
- RequestReviewHandler.requestReview();
637
- newContactAddress = null;
638
- },
639
- actionRightButton: () {
640
- Navigator.of(bottomSheetContext).pop();
641
- RequestReviewHandler.requestReview();
642
- if (context.mounted) {
643
- Navigator.of(context).pushNamed(Routes.addressBookAddContact,
644
- arguments: newContactAddress);
645
- }
646
- newContactAddress = null;
647
- },
648
- )
617
+ await showModalBottomSheet<void>(
618
+ context: context,
619
+ isDismissible: false,
620
+ builder: (BuildContext bottomSheetContext) {
621
+ return showContactSheet && sendViewModel.ocpRequest == null
622
+ ? InfoBottomSheet(
623
+ currentTheme: currentTheme,
624
+ footerType: FooterType.doubleActionButton,
625
+ titleText: S.of(bottomSheetContext).transaction_sent,
626
+ contentImage: 'assets/images/contact.png',
627
+ contentImageColor: Theme.of(context).colorScheme.onSurface,
628
+ content: S.of(bottomSheetContext).add_contact_to_address_book,
629
+ bottomActionPanel: Padding(
630
+ padding: const EdgeInsets.only(left: 34.0),
631
+ child: Row(
632
+ children: [
633
+ SimpleCheckbox(
634
+ onChanged: (value) =>
635
+ sendViewModel.setShowAddressBookPopup(!value)),
636
+ const SizedBox(width: 8),
637
+ Text(
638
+ 'Don’t ask me next time',
639
+ textAlign: TextAlign.center,
640
+ style: TextStyle(
641
+ fontSize: 14,
642
+ fontFamily: 'Lato',
643
+ fontWeight: FontWeight.w500,
644
+ color: Theme.of(context).textTheme.titleLarge!.color,
645
+ decoration: TextDecoration.none,
646
+ ),
647
+ ),
648
+ ],
649
+ ),
650
+ ),
651
+ doubleActionLeftButtonText: 'No',
652
+ doubleActionRightButtonText: 'Yes',
653
+ onLeftActionButtonPressed: () {
654
+ Navigator.of(bottomSheetContext).pop();
655
+ if (context.mounted) {
656
+ Navigator.of(context)
657
+ .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
658
+ }
659
+ RequestReviewHandler.requestReview();
660
+ newContactAddress = null;
661
+ },
662
+ onRightActionButtonPressed: () {
663
+ Navigator.of(bottomSheetContext).pop();
664
+ RequestReviewHandler.requestReview();
665
+ if (context.mounted) {
666
+ Navigator.of(context).pushNamed(Routes.addressBookAddContact,
667
+ arguments: newContactAddress);
668
+ }
669
+ newContactAddress = null;
670
+ },
671
+ )
672
: InfoBottomSheet(
673
currentTheme: currentTheme,
674
+ footerType: FooterType.singleActionButton,
675
titleText: S.of(bottomSheetContext).transaction_sent,
676
contentImage: 'assets/images/birthday_cake.png',
653
- actionButtonText: S.of(bottomSheetContext).close,
654
- actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
655
- actionButton: () {
677
+ singleActionButtonText: S.of(bottomSheetContext).close,
678
+ singleActionButtonKey: ValueKey('send_page_transaction_sent_button_key'),
679
+ onSingleActionButtonPressed: () {
680
Navigator.of(bottomSheetContext).pop();
681
Future.delayed(Duration.zero, () {
682
if (context.mounted) {
@@ -710,14 +734,14 @@ class SendPage extends BasePage {
734
dialogContext = context;
735
return InfoBottomSheet(
736
currentTheme: currentTheme,
737
+ footerType: FooterType.singleActionButton,
738
titleText: S.of(context).proceed_on_device,
739
contentImage:
740
'assets/images/hardware_wallet/ledger_nano_x.png',
741
contentImageColor: Theme.of(context).colorScheme.onSurface,
742
content: S.of(context).proceed_on_device_description,
718
- isTwoAction: false,
719
- actionButtonText: S.of(context).cancel,
720
- actionButton: () {
743
+ singleActionButtonText: S.of(context).cancel,
744
+ onSingleActionButtonPressed: () {
745
sendViewModel.state = InitialExecutionState();
746
Navigator.of(context).pop();
747
},
lib/src/screens/transaction_details/rbf_details_page.dart
+4
-1
@@ -8,6 +8,7 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_expandab
8
import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart';
9
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
10
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
11
+import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
12
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
13
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
14
import 'package:cake_wallet/src/widgets/list_row.dart';
@@ -200,7 +201,9 @@ class RBFDetailsPage extends BasePage {
201
feeValue: transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted,
202
feeFiatAmount: transactionDetailsViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
203
outputs: transactionDetailsViewModel.sendViewModel.outputs,
203
- onSlideComplete: () async {
204
+ footerType: FooterType.slideActionButton,
205
+ accessibleNavigationModeSlideActionButtonText: S.of(context).send,
206
+ onSlideActionComplete: () async {
207
Navigator.of(bottomSheetContext).pop();
208
await transactionDetailsViewModel.sendViewModel.commitTransaction(context);
209
try {
lib/src/widgets/alert_background.dart
+14
-11
@@ -3,28 +3,31 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart';
3
import 'package:flutter/material.dart';
4
5
class AlertBackground extends StatelessWidget {
6
- AlertBackground({required this.child});
6
+ const AlertBackground({Key? key, required this.child, this.dismissible = false});
7
8
final Widget child;
9
+ final bool dismissible;
10
11
@override
12
Widget build(BuildContext context) {
13
return Scaffold(
14
resizeToAvoidBottomInset: false,
15
backgroundColor: Colors.transparent,
15
- body: Container(
16
- height: double.infinity,
17
- width: double.infinity,
18
- color: Colors.transparent,
16
+ body: GestureDetector(
17
+ behavior: HitTestBehavior.opaque,
18
+ onTap: dismissible ? () => Navigator.of(context).pop() : null,
19
child: BackdropFilter(
20
- filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
20
+ filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
21
child: Container(
22
- decoration: BoxDecoration(
23
- color: Theme.of(context).colorScheme.surface.withOpacity(0.8)),
22
+ decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface.withAlpha(200)),
23
child: Center(
25
- child: Container(
26
- width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
27
- child: child,
24
+ child: GestureDetector(
25
+ onTap: () {},
26
+ child: ConstrainedBox(
27
+ constraints:
28
+ BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
29
+ child: child,
30
+ ),
31
),
32
),
33
),
lib/src/widgets/base_text_form_field.dart
+10
-1
@@ -43,7 +43,9 @@ class BaseTextFormField extends StatelessWidget {
43
this.onFieldSubmitted,
44
this.hasUnderlineBorder = false,
45
this.borderWidth = 1.0,
46
- super.key,
46
+ this.prefixIconConstraints,
47
+ this.suffixIconConstraints,
48
+ super.key, this.suffixText,
49
});
50
51
final TextEditingController? controller;
@@ -70,6 +72,9 @@ class BaseTextFormField extends StatelessWidget {
72
final bool readOnly;
73
final bool? enableInteractiveSelection;
74
final String? initialValue;
75
+ final BoxConstraints? prefixIconConstraints;
76
+ final BoxConstraints? suffixIconConstraints;
77
+ final String? suffixText;
78
final void Function(String)? onSubmit;
79
final bool obscureText;
80
final bool? autofocus;
@@ -120,8 +125,12 @@ class BaseTextFormField extends StatelessWidget {
125
alignLabelWithHint: alignLabelWithHint,
126
contentPadding: contentPadding,
127
floatingLabelBehavior: FloatingLabelBehavior.never,
128
+ prefixIconConstraints: prefixIconConstraints ??
129
+ const BoxConstraints(minWidth: 0, minHeight: 0),
130
prefix: prefix,
131
prefixIcon: prefixIcon,
132
+ suffixIconConstraints: suffixIconConstraints ??
133
+ const BoxConstraints(minWidth: 0, minHeight: 0),
134
suffix: suffix,
135
suffixIcon: suffixIcon,
136
filled: !hasUnderlineBorder,
lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart
+136
-52
@@ -1,62 +1,53 @@
1
+import 'package:cake_wallet/src/widgets/primary_button.dart';
2
+import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
3
+import 'package:cake_wallet/themes/core/material_base_theme.dart';
4
import 'package:flutter/material.dart';
5
6
+enum FooterType { none, slideActionButton, singleActionButton, doubleActionButton }
7
+
8
abstract class BaseBottomSheet extends StatelessWidget {
9
+ const BaseBottomSheet({
10
+ super.key,
11
+ required this.titleText,
12
+ this.titleIconPath,
13
+ required this.footerType,
14
+ this.currentTheme,
15
+ this.slideActionButtonText,
16
+ this.onSlideActionComplete,
17
+ this.singleActionButtonText,
18
+ this.accessibleNavigationModeSlideActionButtonText,
19
+ this.onSingleActionButtonPressed,
20
+ this.singleActionButtonKey,
21
+ this.doubleActionLeftButtonText,
22
+ this.doubleActionRightButtonText,
23
+ this.onLeftActionButtonPressed,
24
+ this.onRightActionButtonPressed,
25
+ this.leftActionButtonKey,
26
+ this.rightActionButtonKey,
27
+ required this.maxHeight,
28
+ }) : assert(footerType == FooterType.none || currentTheme != null,
29
+ 'currentTheme is required unless footerType is none');
30
+
31
final String titleText;
32
final String? titleIconPath;
33
+ final MaterialThemeBase? currentTheme;
34
+ final FooterType footerType;
35
+ final String? slideActionButtonText;
36
+ final VoidCallback? onSlideActionComplete;
37
+ final String? singleActionButtonText;
38
+ final String? accessibleNavigationModeSlideActionButtonText;
39
+ final VoidCallback? onSingleActionButtonPressed;
40
+ final Key? singleActionButtonKey;
41
+ final String? doubleActionLeftButtonText;
42
+ final String? doubleActionRightButtonText;
43
+ final VoidCallback? onLeftActionButtonPressed;
44
+ final VoidCallback? onRightActionButtonPressed;
45
+ final Key? leftActionButtonKey;
46
+ final Key? rightActionButtonKey;
47
final double maxHeight;
48
8
- const BaseBottomSheet({required this.titleText, this.titleIconPath, this.maxHeight = 900});
9
-
10
- Widget headerWidget(BuildContext context) {
11
- return Column(
12
- children: [
13
- Padding(
14
- padding: const EdgeInsets.symmetric(vertical: 16),
15
- child: Row(
16
- children: [
17
- const Spacer(flex: 4),
18
- Expanded(
19
- flex: 2,
20
- child: Container(
21
- height: 6,
22
- decoration: BoxDecoration(
23
- borderRadius: BorderRadius.circular(4),
24
- color: Theme.of(context).colorScheme.onSurface,
25
- ),
26
- ),
27
- ),
28
- const Spacer(flex: 4),
29
- ],
30
- ),
31
- ),
32
- Row(
33
- mainAxisAlignment: MainAxisAlignment.center,
34
- children: [
35
- if (titleIconPath != null)
36
- Image.asset(titleIconPath!, height: 24, width: 24, excludeFromSemantics: true)
37
- else
38
- Container(),
39
- const SizedBox(width: 6),
40
- Text(
41
- titleText,
42
- textAlign: TextAlign.center,
43
- style: Theme.of(context).textTheme.titleLarge!.copyWith(
44
- fontSize: 20,
45
- fontWeight: FontWeight.w600,
46
- decoration: TextDecoration.none,
47
- ),
48
- ),
49
- ],
50
- ),
51
- const SizedBox(height: 24),
52
- ],
53
- );
54
- }
55
-
49
Widget contentWidget(BuildContext context);
50
58
- Widget footerWidget(BuildContext context);
59
-
51
@override
52
Widget build(BuildContext context) {
53
return ConstrainedBox(
@@ -69,13 +60,106 @@ abstract class BaseBottomSheet extends StatelessWidget {
60
child: Column(
61
mainAxisSize: MainAxisSize.min,
62
children: <Widget>[
72
- headerWidget(context),
63
+ _buildHeader(context),
64
contentWidget(context),
74
- footerWidget(context),
65
+ _buildFooter(context),
66
],
67
),
68
),
69
),
70
);
71
}
72
+
73
+ Widget _buildHeader(BuildContext context) => Column(
74
+ children: [
75
+ const SizedBox(height: 12),
76
+ Container(
77
+ width: 64,
78
+ height: 5,
79
+ decoration: BoxDecoration(
80
+ borderRadius: BorderRadius.circular(4),
81
+ color: Theme.of(context).colorScheme.onSurface,
82
+ ),
83
+ ),
84
+ const SizedBox(height: 20),
85
+ Row(
86
+ mainAxisAlignment: MainAxisAlignment.center,
87
+ children: [
88
+ if (titleIconPath != null) ...[
89
+ Image.asset(titleIconPath!, height: 24, width: 24),
90
+ const SizedBox(width: 6),
91
+ ],
92
+ Text(
93
+ titleText,
94
+ style: Theme.of(context).textTheme.titleLarge!.copyWith(
95
+ fontSize: 20,
96
+ fontWeight: FontWeight.w600,
97
+ decoration: TextDecoration.none,
98
+ ),
99
+ ),
100
+ ],
101
+ ),
102
+ const SizedBox(height: 13),
103
+ ],
104
+ );
105
+
106
+ Widget _buildFooter(BuildContext context) {
107
+ switch (footerType) {
108
+ case FooterType.none:
109
+ return const SizedBox.shrink();
110
+
111
+ case FooterType.slideActionButton:
112
+ return Padding(
113
+ padding: const EdgeInsets.fromLTRB(40, 12, 40, 34),
114
+ child: StandardSlideButton(
115
+ buttonText: slideActionButtonText ?? '',
116
+ onSlideComplete: onSlideActionComplete ?? () {},
117
+ currentTheme: currentTheme!,
118
+ accessibleNavigationModeButtonText: accessibleNavigationModeSlideActionButtonText ?? '',
119
+ ),
120
+ );
121
+
122
+ case FooterType.singleActionButton:
123
+ return Padding(
124
+ padding: const EdgeInsets.fromLTRB(16, 12, 16, 34),
125
+ child: LoadingPrimaryButton(
126
+ key: singleActionButtonKey,
127
+ text: singleActionButtonText ?? '',
128
+ onPressed: onSingleActionButtonPressed ?? () {},
129
+ color: Theme.of(context).colorScheme.primary,
130
+ textColor: Theme.of(context).colorScheme.onPrimary,
131
+ isLoading: false,
132
+ isDisabled: false,
133
+ ),
134
+ );
135
+
136
+ case FooterType.doubleActionButton:
137
+ return Padding(
138
+ padding: const EdgeInsets.fromLTRB(16, 0, 16, 34),
139
+ child: Row(
140
+ children: [
141
+ Expanded(
142
+ child: PrimaryButton(
143
+ key: leftActionButtonKey,
144
+ text: doubleActionLeftButtonText ?? '',
145
+ onPressed: onLeftActionButtonPressed,
146
+ color: Theme.of(context).colorScheme.surfaceContainer,
147
+ textColor: Theme.of(context).colorScheme.onSecondaryContainer
148
+ ),
149
+ ),
150
+ const SizedBox(width: 12),
151
+ Expanded(
152
+ child: PrimaryButton(
153
+ key: rightActionButtonKey,
154
+ text: doubleActionRightButtonText ?? '',
155
+ onPressed: onRightActionButtonPressed,
156
+ color: Theme.of(context).colorScheme.primary,
157
+ textColor: Theme.of(context).colorScheme.onPrimary,
158
+ ),
159
+ ),
160
+ ],
161
+ ),
162
+ );
163
+ }
164
+ }
165
}
lib/src/widgets/bottom_sheet/cake_pay_card_info_bottom_sheet_widget.dart
new
+352
@@ -0,0 +1,352 @@
1
+import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
2
+import 'package:cake_wallet/cake_pay/src/widgets/flip_card_widget.dart';
3
+import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
4
+import 'package:cake_wallet/generated/i18n.dart';
5
+import 'package:cake_wallet/palette.dart';
6
+import 'package:cake_wallet/themes/core/material_base_theme.dart';
7
+import 'package:cake_wallet/utils/image_utill.dart';
8
+import 'package:cake_wallet/utils/show_pop_up.dart';
9
+import 'package:flutter/material.dart';
10
+
11
+import 'base_bottom_sheet_widget.dart';
12
+
13
+class CakePayCardInfoBottomSheet extends BaseBottomSheet {
14
+ CakePayCardInfoBottomSheet({
15
+ required String titleText,
16
+ required MaterialThemeBase currentTheme,
17
+ required FooterType footerType,
18
+ String? titleIconPath,
19
+ String? singleActionButtonText,
20
+ VoidCallback? onSingleActionButtonPressed,
21
+ Key? singleActionButtonKey,
22
+ String? doubleActionLeftButtonText,
23
+ String? doubleActionRightButtonText,
24
+ VoidCallback? onLeftActionButtonPressed,
25
+ VoidCallback? onRightActionButtonPressed,
26
+ Key? rightActionButtonKey,
27
+ Key? leftActionButtonKey,
28
+ required this.onUpdateBalancePressed,
29
+ required this.isReloadable,
30
+ required this.balance,
31
+ this.contentImage,
32
+ this.howToUse,
33
+ this.applyBoxShadow = false,
34
+ Key? key,
35
+ }) : _currentTheme = currentTheme,
36
+ super(
37
+ titleText: titleText,
38
+ maxHeight: 900,
39
+ titleIconPath: titleIconPath,
40
+ currentTheme: currentTheme,
41
+ footerType: footerType,
42
+ singleActionButtonText: singleActionButtonText,
43
+ onSingleActionButtonPressed: onSingleActionButtonPressed,
44
+ singleActionButtonKey: singleActionButtonKey,
45
+ doubleActionLeftButtonText: doubleActionLeftButtonText,
46
+ doubleActionRightButtonText: doubleActionRightButtonText,
47
+ onLeftActionButtonPressed: onLeftActionButtonPressed,
48
+ onRightActionButtonPressed: onRightActionButtonPressed,
49
+ leftActionButtonKey: leftActionButtonKey,
50
+ rightActionButtonKey: rightActionButtonKey,
51
+ key: key);
52
+
53
+ final VoidCallback onUpdateBalancePressed;
54
+ final MaterialThemeBase _currentTheme;
55
+ final String? contentImage;
56
+ final String? howToUse;
57
+ final bool isReloadable;
58
+ final String balance;
59
+ final bool applyBoxShadow;
60
+
61
+ final _cardKey = GlobalKey<FlipCardState>();
62
+
63
+ @override
64
+ Widget contentWidget(BuildContext context) {
65
+ final itemTitleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
66
+ fontSize: 16,
67
+ fontWeight: FontWeight.w500,
68
+ decoration: TextDecoration.none,
69
+ );
70
+ final itemSubTitleTextStyle = Theme.of(context).textTheme.bodySmall!.copyWith(
71
+ fontWeight: FontWeight.w600,
72
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
73
+ decoration: TextDecoration.none,
74
+ );
75
+
76
+ final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
77
+
78
+ return Column(
79
+ mainAxisSize: MainAxisSize.min,
80
+ children: [
81
+ if (contentImage != null)
82
+ Padding(
83
+ padding: const EdgeInsets.symmetric(horizontal: 32),
84
+ child: Stack(
85
+ children: [
86
+ Padding(
87
+ padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 16),
88
+ child: AspectRatio(
89
+ aspectRatio: 1.6,
90
+ child: FlipCard(
91
+ key: _cardKey,
92
+ flipOnTouch: true,
93
+ front: _buildCardImage(context, contentImage!, applyBoxShadow),
94
+ back: _buildBarcodeSide(context,
95
+ cardNumber: '6006491979836784204', pin: '4782'),
96
+ ),
97
+ ),
98
+ ),
99
+ Positioned(
100
+ bottom: 0,
101
+ right: 0,
102
+ child: GestureDetector(
103
+ onTap: () => _cardKey.currentState?.toggleCard(),
104
+ child: Container(
105
+ height: 43,
106
+ width: 43,
107
+ decoration: BoxDecoration(
108
+ border: Border.all(color: Colors.white, width: 1),
109
+ color: Colors.white.withAlpha(75),
110
+ shape: BoxShape.circle,
111
+ ),
112
+ child: Transform.scale(
113
+ scale: .8,
114
+ child: const ImageIcon(
115
+ AssetImage('assets/images/transfer.png'),
116
+ color: Colors.white,
117
+ ),
118
+ )),
119
+ ),
120
+ ),
121
+ ],
122
+ ),
123
+ ),
124
+ Container(),
125
+ Text(
126
+ 'Tap card to show details',
127
+ style: itemTitleTextStyle.copyWith(fontSize: 12, fontWeight: FontWeight.w700),
128
+ ),
129
+ Padding(
130
+ padding: const EdgeInsets.symmetric(horizontal: 8),
131
+ child: Column(
132
+ children: [
133
+ const SizedBox(height: 34),
134
+ CakePayInfoTile(
135
+ isReloadable: isReloadable,
136
+ itemValue: balance,
137
+ itemTitleTextStyle: itemTitleTextStyle,
138
+ itemSubTitleTextStyle: itemSubTitleTextStyle,
139
+ tileBackgroundColor: tileBackgroundColor),
140
+ const SizedBox(height: 8),
141
+ _HowToUseTile(howToUse: howToUse ?? '', tileBackgroundColor: tileBackgroundColor),
142
+ const SizedBox(height: 40),
143
+ ],
144
+ ),
145
+ ),
146
+ ],
147
+ );
148
+ }
149
+}
150
+
151
+class _HowToUseTile extends StatelessWidget {
152
+ const _HowToUseTile({required this.howToUse, required this.tileBackgroundColor});
153
+
154
+ final String howToUse;
155
+ final Color tileBackgroundColor;
156
+
157
+ @override
158
+ Widget build(BuildContext context) {
159
+ return InkWell(
160
+ onTap: () => _showHowToUseCard(context: context, howToUse: howToUse),
161
+ child: Container(
162
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
163
+ decoration:
164
+ BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor),
165
+ child: Row(
166
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
167
+ children: [
168
+ Text(
169
+ S.of(context).how_to_use_card,
170
+ style: Theme.of(context).textTheme.bodyLarge),
171
+ Icon(
172
+ Icons.chevron_right_rounded,
173
+ color: Theme.of(context).textTheme.titleLarge!.color!,
174
+ ),
175
+ ],
176
+ )),
177
+ );
178
+ }
179
+}
180
+
181
+class CakePayInfoTile extends StatelessWidget {
182
+ const CakePayInfoTile({
183
+ super.key,
184
+ required this.isReloadable,
185
+ required this.itemValue,
186
+ required this.itemTitleTextStyle,
187
+ this.itemSubTitle,
188
+ required this.itemSubTitleTextStyle,
189
+ required this.tileBackgroundColor,
190
+ });
191
+
192
+ final bool isReloadable;
193
+ final String itemValue;
194
+ final TextStyle itemTitleTextStyle;
195
+ final String? itemSubTitle;
196
+ final TextStyle itemSubTitleTextStyle;
197
+ final Color tileBackgroundColor;
198
+
199
+ @override
200
+ Widget build(BuildContext context) {
201
+ return Semantics(
202
+ container: true,
203
+ label: isReloadable ? 'Balance' : 'Total Value',
204
+ child: Container(
205
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
206
+ decoration:
207
+ BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor),
208
+ child: Column(
209
+ crossAxisAlignment: CrossAxisAlignment.start,
210
+ children: [
211
+ Row(
212
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
213
+ children: [
214
+ Text(isReloadable ? 'Balance' : 'Total Value', style: itemTitleTextStyle),
215
+ Text(itemValue,
216
+ style: itemTitleTextStyle.copyWith(fontSize: 18, fontWeight: FontWeight.w600)),
217
+ ],
218
+ ),
219
+ const SizedBox(height: 14),
220
+ Container(
221
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
222
+ child: Text(isReloadable ? 'Top Up Balance' : 'Manually update Balance',
223
+ style: itemTitleTextStyle.copyWith(fontSize: 12, fontWeight: FontWeight.w600)),
224
+ decoration: BoxDecoration(
225
+ borderRadius: BorderRadius.circular(18),
226
+ color: Theme.of(context).dialogBackgroundColor)),
227
+ const SizedBox(height: 4),
228
+ ],
229
+ ),
230
+ ),
231
+ );
232
+ }
233
+}
234
+
235
+void _showHowToUseCard({required BuildContext context, String? howToUse}) {
236
+ showPopUp<void>(
237
+ context: context,
238
+ builder: (BuildContext context) {
239
+ return CakePayAlertModal(
240
+ title: S.of(context).how_to_use_card,
241
+ content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
242
+ ClickableLinksText(
243
+ text: howToUse ?? '',
244
+ textStyle: Theme.of(context).textTheme.bodyMedium!,
245
+ linkStyle: TextStyle(
246
+ color: Theme.of(context).textTheme.titleLarge!.color!,
247
+ fontSize: 18,
248
+ fontStyle: FontStyle.italic,
249
+ fontWeight: FontWeight.w400,
250
+ ),
251
+ ),
252
+ ]),
253
+ actionTitle: S.current.got_it,
254
+ );
255
+ });
256
+}
257
+
258
+Widget _buildCardImage(BuildContext ctx, String path, bool addShadow) {
259
+ final border = BorderRadius.circular(10);
260
+
261
+ return Container(
262
+ decoration: addShadow
263
+ ? BoxDecoration(
264
+ borderRadius: border,
265
+ boxShadow: [
266
+ BoxShadow(color: Colors.black.withAlpha(150), blurRadius: 5)
267
+ ],
268
+ )
269
+ : null,
270
+ child: ClipRRect(
271
+ borderRadius: border,
272
+ child: Stack(
273
+ fit: StackFit.expand,
274
+ children: [
275
+ Container(color: Theme.of(ctx).cardColor.withAlpha(200)),
276
+ ImageUtil.getImageFromPath(
277
+ imagePath: path,
278
+ fit: BoxFit.cover,
279
+ ),
280
+ ],
281
+ ),
282
+ ),
283
+ );
284
+}
285
+
286
+Widget _buildBarcodeSide(BuildContext context, {required String cardNumber, required String pin}) =>
287
+ SizedBox.expand(
288
+ child: Container(
289
+ padding: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 34),
290
+ decoration: BoxDecoration(
291
+ color: Theme.of(context).cardColor.withAlpha(200),
292
+ borderRadius: BorderRadius.circular(10),
293
+ boxShadow: [
294
+ BoxShadow(color: Colors.black.withAlpha(150), blurRadius: 5)
295
+ ],
296
+ ),
297
+ child: Column(
298
+ mainAxisAlignment: MainAxisAlignment.center,
299
+ children: [
300
+ Expanded(
301
+ child: Container(
302
+ color: Theme.of(context).textTheme.titleLarge!.color!.withOpacity(.1),
303
+
304
+ ),
305
+ ),
306
+ const SizedBox(height: 12),
307
+ Row(
308
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
309
+ children: [
310
+ Column(
311
+ crossAxisAlignment: CrossAxisAlignment.start,
312
+ children: [
313
+ Text('Gift Card Number',
314
+ style: TextStyle(
315
+ fontFamily: 'Lato',
316
+ fontSize: 10,
317
+ fontWeight: FontWeight.w500,
318
+ color: Color.fromRGBO(207, 207, 207, 1))),
319
+ const SizedBox(height: 4),
320
+ Text(cardNumber,
321
+ style: TextStyle(
322
+ fontFamily: 'Lato',
323
+ fontSize: 12,
324
+ fontWeight: FontWeight.w900,
325
+ color: Color.fromRGBO(146, 146, 146, 1))),
326
+ ],
327
+ ),
328
+ Column(
329
+ crossAxisAlignment: CrossAxisAlignment.start,
330
+ children: [
331
+ Text('PIN Number',
332
+ style: TextStyle(
333
+ fontFamily: 'Lato',
334
+ fontSize: 10,
335
+ fontWeight: FontWeight.w500,
336
+ color: Color.fromRGBO(207, 207, 207, 1))),
337
+ const SizedBox(height: 4),
338
+ Text(pin,
339
+ style: TextStyle(
340
+ fontFamily: 'Lato',
341
+ fontSize: 12,
342
+ fontWeight: FontWeight.w900,
343
+ color: Color.fromRGBO(146, 146, 146, 1))),
344
+ ],
345
+ ),
346
+ ],
347
+ ),
348
+
349
+ ],
350
+ ),
351
+ ),
352
+ );
lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart
new
+239
@@ -0,0 +1,239 @@
1
+import 'package:cake_wallet/generated/i18n.dart';
2
+import 'package:cake_wallet/src/widgets/primary_button.dart';
3
+import 'package:cake_wallet/utils/image_utill.dart';
4
+import 'package:cake_wallet/view_model/send/output.dart';
5
+import 'package:cw_core/crypto_currency.dart';
6
+import 'package:flutter/material.dart';
7
+
8
+class CakePayTransactionSentBottomSheet extends StatelessWidget {
9
+ const CakePayTransactionSentBottomSheet({
10
+ super.key,
11
+ required this.titleText,
12
+ required this.currency,
13
+ required this.amount,
14
+ required this.amountValue,
15
+ required this.fiatAmountValue,
16
+ required this.output,
17
+ required this.fee,
18
+ required this.feeValue,
19
+ required this.feeFiatAmount,
20
+ this.titleIconWidget,
21
+ required this.quantity,
22
+ required this.onClose,
23
+ required this.paymentId,
24
+ required this.paymentIdValue,
25
+ });
26
+
27
+ final String titleText;
28
+ final Widget? titleIconWidget;
29
+ final CryptoCurrency currency;
30
+ final String amount;
31
+ final String amountValue;
32
+ final String fiatAmountValue;
33
+ final Output output;
34
+ final String fee;
35
+ final String feeValue;
36
+ final String feeFiatAmount;
37
+ final String quantity;
38
+ final VoidCallback onClose;
39
+ final String paymentId;
40
+ final String paymentIdValue;
41
+
42
+ TextStyle _titleStyle(BuildContext ctx) => Theme.of(ctx).textTheme.bodyLarge!;
43
+
44
+ TextStyle _valueStyle(BuildContext ctx) => Theme.of(ctx).textTheme.titleMedium!.copyWith(
45
+ fontSize: 18,
46
+ fontWeight: FontWeight.w600,
47
+ );
48
+
49
+ TextStyle _subStyle(BuildContext ctx) => Theme.of(ctx).textTheme.labelSmall!.copyWith(
50
+ color: Theme.of(ctx).colorScheme.onSurfaceVariant,
51
+ );
52
+
53
+ Widget _buildHeader(BuildContext ctx) => Column(
54
+ children: [
55
+ const SizedBox(height: 12),
56
+ Container(
57
+ width: 64,
58
+ height: 5,
59
+ decoration: BoxDecoration(
60
+ borderRadius: BorderRadius.circular(4),
61
+ color: Theme.of(ctx).colorScheme.onSurface,
62
+ ),
63
+ ),
64
+ const SizedBox(height: 20),
65
+ Row(
66
+ mainAxisAlignment: MainAxisAlignment.center,
67
+ children: [
68
+ if (titleIconWidget != null) titleIconWidget!,
69
+ const SizedBox(width: 6),
70
+ Text(
71
+ titleText,
72
+ style: Theme.of(ctx).textTheme.titleLarge!.copyWith(
73
+ fontSize: 20,
74
+ fontWeight: FontWeight.w600,
75
+ decoration: TextDecoration.none,
76
+ ),
77
+ ),
78
+ ],
79
+ ),
80
+ const SizedBox(height: 12),
81
+ ],
82
+ );
83
+
84
+ Widget _buildBody(BuildContext context) => Padding(
85
+ padding: const EdgeInsets.symmetric(horizontal: 8),
86
+ child: Column(
87
+ children: [
88
+ _StandardTile(
89
+ itemTitle: amount,
90
+ titleStyle: _titleStyle(context),
91
+ itemValue: '$amountValue ${currency.title}',
92
+ itemValueStyle: _valueStyle(context),
93
+ itemSubTitle: fiatAmountValue,
94
+ itemSubTitleStyle: _subStyle(context),
95
+ ),
96
+ const SizedBox(height: 8),
97
+ _StandardTile(
98
+ itemTitle: fee,
99
+ titleStyle: _titleStyle(context),
100
+ itemValue: feeValue,
101
+ itemValueStyle: _valueStyle(context),
102
+ itemSubTitle: feeFiatAmount,
103
+ itemSubTitleStyle: _subStyle(context),
104
+ ),
105
+ const SizedBox(height: 8),
106
+ _StandardTile(
107
+ itemTitle: output.parsedAddress.profileName,
108
+ titleStyle: _titleStyle(context),
109
+ itemValue: output.fiatAmount,
110
+ itemValueStyle: _valueStyle(context),
111
+ itemSubTitle: quantity,
112
+ itemSubTitleStyle: _subStyle(context),
113
+ imagePath: output.parsedAddress.profileImageUrl,
114
+ ),
115
+ Padding(
116
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
117
+ child: Column(
118
+ children: [
119
+ Text(paymentId + ': ' + paymentIdValue,
120
+ style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center),
121
+ const SizedBox(height: 18),
122
+ Image.asset('assets/images/envelope.png'),
123
+ const SizedBox(height: 18),
124
+ Text(
125
+ S.of(context).cake_pay_card_email_delivered_message,
126
+ style: Theme.of(context).textTheme.bodyLarge,
127
+ textAlign: TextAlign.center,
128
+ ),
129
+ const SizedBox(height: 24),
130
+ PrimaryButton(
131
+ text: S.of(context).close,
132
+ color: Theme.of(context).colorScheme.primary,
133
+ textColor: Theme.of(context).colorScheme.onPrimaryContainer,
134
+ onPressed: onClose,
135
+ ),
136
+ ],
137
+ ),
138
+ ),
139
+ ],
140
+ ),
141
+ );
142
+
143
+ @override
144
+ Widget build(BuildContext context) {
145
+ final maxHeight = MediaQuery.of(context).size.height * 0.9;
146
+
147
+ return ClipRRect(
148
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
149
+ child: Material(
150
+ color: Theme.of(context).colorScheme.surface,
151
+ child: ConstrainedBox(
152
+ constraints: BoxConstraints(maxHeight: maxHeight),
153
+ child: SingleChildScrollView(
154
+ child: Column(
155
+ mainAxisSize: MainAxisSize.min,
156
+ children: [
157
+ _buildHeader(context),
158
+ _buildBody(context),
159
+ ],
160
+ ),
161
+ ),
162
+ ),
163
+ ),
164
+ );
165
+ }
166
+}
167
+
168
+class _StandardTile extends StatelessWidget {
169
+ const _StandardTile({
170
+ required this.itemTitle,
171
+ required this.titleStyle,
172
+ required this.itemValue,
173
+ required this.itemValueStyle,
174
+ this.itemSubTitle,
175
+ this.itemSubTitleStyle,
176
+ this.imagePath,
177
+ });
178
+
179
+ final String itemTitle;
180
+ final TextStyle titleStyle;
181
+ final String itemValue;
182
+ final TextStyle itemValueStyle;
183
+ final String? itemSubTitle;
184
+ final TextStyle? itemSubTitleStyle;
185
+ final String? imagePath;
186
+
187
+ @override
188
+ Widget build(BuildContext context) {
189
+ return Semantics(
190
+ container: true,
191
+ label: itemTitle,
192
+ child: Container(
193
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
194
+ decoration: BoxDecoration(
195
+ borderRadius: BorderRadius.circular(10),
196
+ color: Theme.of(context).colorScheme.surfaceContainerLowest.withAlpha(80)),
197
+ child: Row(
198
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
199
+ children: [
200
+ Expanded(
201
+ child: Row(
202
+ children: [
203
+ if (imagePath != null)
204
+ Padding(
205
+ padding: const EdgeInsets.only(right: 8),
206
+ child: ClipRRect(
207
+ borderRadius: BorderRadius.circular(12),
208
+ child: ImageUtil.getImageFromPath(
209
+ imagePath: imagePath!, height: 40, width: 40),
210
+ ),
211
+ ),
212
+ Flexible(
213
+ child: Padding(
214
+ padding: const EdgeInsets.only(right: 8),
215
+ child: Text(
216
+ itemTitle,
217
+ style: titleStyle,
218
+ overflow: TextOverflow.ellipsis,
219
+ maxLines: 1,
220
+ softWrap: false,
221
+ )),
222
+ ),
223
+ ],
224
+ ),
225
+ ),
226
+ Column(
227
+ crossAxisAlignment: CrossAxisAlignment.end,
228
+ mainAxisSize: MainAxisSize.min,
229
+ children: [
230
+ Text(itemValue, style: itemValueStyle.copyWith(height: 1.0)),
231
+ if (itemSubTitle != null) Text(itemSubTitle!, style: itemSubTitleStyle),
232
+ ],
233
+ ),
234
+ ],
235
+ ),
236
+ ),
237
+ );
238
+ }
239
+}
lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart
+131
-80
@@ -1,39 +1,29 @@
1
import 'package:cake_wallet/generated/i18n.dart';
2
import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
3
import 'package:cake_wallet/themes/core/material_base_theme.dart';
4
+import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
5
import 'package:cake_wallet/utils/address_formatter.dart';
6
+import 'package:cake_wallet/utils/image_utill.dart';
7
+import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
8
import 'package:cake_wallet/view_model/send/output.dart';
9
import 'package:cw_core/crypto_currency.dart';
10
import 'package:cw_core/pending_transaction.dart';
11
import 'package:cw_core/wallet_type.dart';
12
import 'package:flutter/material.dart';
13
+import 'package:flutter_mobx/flutter_mobx.dart';
14
15
import 'base_bottom_sheet_widget.dart';
16
17
class ConfirmSendingBottomSheet extends BaseBottomSheet {
14
- final CryptoCurrency currency;
15
- final MaterialThemeBase currentTheme;
16
- final String? paymentId;
17
- final String? paymentIdValue;
18
- final String? expirationTime;
19
- final String amount;
20
- final String amountValue;
21
- final String fiatAmountValue;
22
- final String fee;
23
- final String feeValue;
24
- final String feeFiatAmount;
25
- final String? explanation;
26
- final List<Output> outputs;
27
- final VoidCallback onSlideComplete;
28
- final WalletType walletType;
29
- final PendingChange? change;
30
- final bool isOpenCryptoPay;
31
-
18
ConfirmSendingBottomSheet({
19
required String titleText,
20
+ required MaterialThemeBase currentTheme,
21
+ required FooterType footerType,
22
String? titleIconPath,
23
+ String? slideActionButtonText,
24
+ VoidCallback? onSlideActionComplete,
25
+ String? accessibleNavigationModeSlideActionButtonText,
26
required this.currency,
36
- required this.currentTheme,
27
this.paymentId,
28
this.paymentIdValue,
29
this.expirationTime,
@@ -44,14 +34,45 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
34
required this.feeValue,
35
required this.feeFiatAmount,
36
required this.outputs,
47
- required this.onSlideComplete,
37
required this.walletType,
38
this.change,
39
this.explanation,
40
this.isOpenCryptoPay = false,
41
+ this.cakePayBuyCardViewModel,
42
+ this.quantity,
43
Key? key,
44
}) : showScrollbar = outputs.length > 3,
54
- super(titleText: titleText, titleIconPath: titleIconPath);
45
+ _currentTheme = currentTheme,
46
+ super(
47
+ titleText: titleText,
48
+ maxHeight: 900,
49
+ titleIconPath: titleIconPath,
50
+ currentTheme: currentTheme,
51
+ footerType: footerType,
52
+ slideActionButtonText: slideActionButtonText ?? 'Swipe to send',
53
+ onSlideActionComplete: onSlideActionComplete,
54
+ accessibleNavigationModeSlideActionButtonText:
55
+ accessibleNavigationModeSlideActionButtonText,
56
+ key: key);
57
+
58
+ final CryptoCurrency currency;
59
+ final MaterialThemeBase _currentTheme;
60
+ final String? paymentId;
61
+ final String? paymentIdValue;
62
+ final String? expirationTime;
63
+ final String amount;
64
+ final String amountValue;
65
+ final String fiatAmountValue;
66
+ final String fee;
67
+ final String feeValue;
68
+ final String feeFiatAmount;
69
+ final List<Output> outputs;
70
+ final WalletType walletType;
71
+ final PendingChange? change;
72
+ final bool isOpenCryptoPay;
73
+ final CakePayBuyCardViewModel? cakePayBuyCardViewModel;
74
+ final String? quantity;
75
+ final String? explanation;
76
77
final bool showScrollbar;
78
final ScrollController scrollController = ScrollController();
@@ -69,25 +90,32 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
90
decoration: TextDecoration.none,
91
);
92
72
- final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
93
+ final tileBackgroundColor = _currentTheme.isDark
94
+ ? CustomThemeColors.backgroundGradientColorDark.withAlpha(140)
95
+ : CustomThemeColors.cardGradientColorPrimaryLight;
96
97
Widget content = Padding(
98
padding: EdgeInsets.fromLTRB(8, 0, showScrollbar ? 16 : 8, 8),
99
child: Column(
100
children: [
78
- if (paymentId != null && paymentIdValue != null)
101
+ if (paymentId != null && paymentIdValue != null && cakePayBuyCardViewModel != null)
102
Padding(
103
padding: const EdgeInsets.only(bottom: 8),
81
- child: AddressTile(
82
- itemTitle: paymentId!,
83
- itemTitleTextStyle: itemTitleTextStyle,
84
- walletType: walletType,
85
- isBatchSending: false,
86
- amount: '',
87
- address: paymentIdValue!,
88
- itemSubTitleTextStyle: itemSubTitleTextStyle,
89
- tileBackgroundColor: tileBackgroundColor,
90
- ),
104
+ child: Observer(
105
+ builder: (_) => AddressTile(
106
+ itemTitle: paymentId!,
107
+ itemTitleTextStyle: itemTitleTextStyle,
108
+ amountTextStyle: itemSubTitleTextStyle,
109
+ walletType: walletType,
110
+ amount: expirationTime != null
111
+ ? S.current.offer_expires_in +
112
+ ' ${cakePayBuyCardViewModel!.formattedRemainingTime}'
113
+ : null,
114
+ address: paymentIdValue!,
115
+ itemSubTitleTextStyle: itemSubTitleTextStyle,
116
+ tileBackgroundColor: tileBackgroundColor,
117
+ applyAddressFormatting: false,
118
+ )),
119
),
120
if (explanation != null)
121
Padding(
@@ -128,12 +156,13 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
156
final bool isBatchSending = outputs.length > 1;
157
final item = outputs[index];
158
final contactName = item.parsedAddress.name;
159
+ final isCakePayName = contactName == 'Cake Pay';
160
final batchContactTitle =
161
'${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}';
162
final _address = item.isParsedAddress ? item.extractedAddress : item.address;
163
final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}';
135
- return isBatchSending || contactName.isNotEmpty
136
- ? AddressExpansionTile(
164
+ return isBatchSending || (contactName.isNotEmpty && !isCakePayName)
165
+ ? ExpansionAddressTile(
166
contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
167
name: isBatchSending ? batchContactTitle : contactName,
168
address: _address,
@@ -145,12 +174,15 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
174
tileBackgroundColor: tileBackgroundColor,
175
)
176
: AddressTile(
148
- itemTitle: S.of(context).address,
177
+ itemTitle: isCakePayName
178
+ ? item.parsedAddress.profileName
179
+ : S.of(context).address,
180
+ imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null,
181
itemTitleTextStyle: itemTitleTextStyle,
150
- isBatchSending: isBatchSending,
182
walletType: walletType,
152
- amount: _amount,
183
+ amount: isCakePayName ? item.fiatAmount : _amount,
184
address: _address,
185
+ itemSubTitle: isCakePayName ? quantity : null,
186
itemSubTitleTextStyle: itemSubTitleTextStyle,
187
tileBackgroundColor: tileBackgroundColor,
188
);
@@ -159,7 +191,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
191
if (change != null)
192
Padding(
193
padding: const EdgeInsets.only(top: 8),
162
- child: AddressExpansionTile(
194
+ child: ExpansionAddressTile(
195
contactType: 'Change',
196
name: S.of(context).send_change_to_you,
197
address: change!.address,
@@ -195,31 +227,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
227
return content;
228
}
229
}
198
-
199
- @override
200
- Widget footerWidget(BuildContext context) {
201
- return Container(
202
- padding: const EdgeInsets.fromLTRB(40, 12, 40, 34),
203
- decoration: BoxDecoration(
204
- color: Theme.of(context).colorScheme.surface,
205
- boxShadow: [
206
- if (showScrollbar)
207
- BoxShadow(
208
- color: Theme.of(context).colorScheme.outlineVariant,
209
- spreadRadius: 2,
210
- blurRadius: 10,
211
- offset: const Offset(0, 0),
212
- ),
213
- ],
214
- ),
215
- child: StandardSlideButton(
216
- onSlideComplete: onSlideComplete,
217
- buttonText: 'Swipe to send',
218
- currentTheme: currentTheme,
219
- accessibleNavigationModeButtonText: S.of(context).send,
220
- ),
221
- );
222
- }
230
}
231
232
class StandardTile extends StatelessWidget {
@@ -304,22 +311,28 @@ class AddressTile extends StatelessWidget {
311
super.key,
312
required this.itemTitle,
313
required this.itemTitleTextStyle,
307
- required this.isBatchSending,
308
- required this.amount,
314
required this.address,
315
required this.itemSubTitleTextStyle,
316
required this.tileBackgroundColor,
317
required this.walletType,
318
+ this.amountTextStyle,
319
+ this.applyAddressFormatting = true,
320
+ this.imagePath,
321
+ this.amount,
322
+ this.itemSubTitle,
323
});
324
325
final String itemTitle;
326
final TextStyle itemTitleTextStyle;
317
- final bool isBatchSending;
318
- final String amount;
327
+ final String? amount;
328
final String address;
329
final TextStyle itemSubTitleTextStyle;
330
+ final TextStyle? amountTextStyle;
331
final Color tileBackgroundColor;
332
final WalletType walletType;
333
+ final bool applyAddressFormatting;
334
+ final String? imagePath;
335
+ final String? itemSubTitle;
336
337
@override
338
Widget build(BuildContext context) {
@@ -335,26 +348,65 @@ class AddressTile extends StatelessWidget {
348
Row(
349
mainAxisAlignment: MainAxisAlignment.spaceBetween,
350
children: [
338
- Text(itemTitle, style: itemTitleTextStyle),
339
- if (isBatchSending) Text(amount, style: itemTitleTextStyle),
351
+ Expanded(
352
+ child: Row(
353
+ children: [
354
+ if (imagePath != null)
355
+ Padding(
356
+ padding: const EdgeInsets.only(right: 8),
357
+ child: ClipRRect(
358
+ borderRadius: BorderRadius.circular(12),
359
+ child: ImageUtil.getImageFromPath(
360
+ imagePath: imagePath!, height: 40, width: 40),
361
+ ),
362
+ ),
363
+ Flexible(
364
+ child: Padding(
365
+ padding: const EdgeInsets.only(right: 8),
366
+ child: Text(
367
+ itemTitle,
368
+ style: itemTitleTextStyle,
369
+ overflow: TextOverflow.ellipsis,
370
+ maxLines: 1,
371
+ softWrap: false,
372
+ )),
373
+ ),
374
+ ],
375
+ ),
376
+ ),
377
+ if (amount != null) Text(amount!, style: amountTextStyle ?? itemTitleTextStyle),
378
],
379
),
342
- AddressFormatter.buildSegmentedAddress(
343
- address: address,
344
- walletType: walletType,
345
- evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
346
- fontWeight: FontWeight.w600,
347
- decoration: TextDecoration.none,
380
+ address.isEmpty
381
+ ? Container()
382
+ : applyAddressFormatting
383
+ ? AddressFormatter.buildSegmentedAddress(
384
+ address: address,
385
+ walletType: walletType,
386
+ evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
387
+ fontWeight: FontWeight.w600,
388
+ decoration: TextDecoration.none,
389
+ ))
390
+ : Text(
391
+ address,
392
+ style: itemTitleTextStyle,
393
+ ),
394
+ itemSubTitle == null
395
+ ? Container()
396
+ : Row(
397
+ mainAxisAlignment: MainAxisAlignment.end,
398
+ children: [
399
+ Text(itemSubTitle!, style: itemSubTitleTextStyle),
400
+ ],
401
),
349
- ),
402
],
403
),
404
);
405
}
406
}
407
356
-class AddressExpansionTile extends StatelessWidget {
357
- const AddressExpansionTile({
408
+class ExpansionAddressTile extends StatelessWidget {
409
+ const ExpansionAddressTile({
410
super.key,
411
required this.contactType,
412
required this.name,
@@ -425,7 +477,6 @@ class AddressExpansionTile extends StatelessWidget {
477
walletType: walletType,
478
evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
479
fontWeight: FontWeight.w600,
428
- decoration: TextDecoration.none,
480
),
481
),
482
),
lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart
+70
-160
@@ -1,74 +1,96 @@
1
import 'package:auto_size_text/auto_size_text.dart';
2
+import 'package:cake_wallet/src/widgets/simple_checkbox.dart';
3
+
4
+
5
+
6
+import 'package:cake_wallet/utils/image_utill.dart';
7
+
8
+import 'package:cake_wallet/src/widgets/primary_button.dart';
9
+import 'package:cake_wallet/themes/core/material_base_theme.dart';
10
+
11
+
12
import 'package:cake_wallet/routes.dart';
13
import 'package:cake_wallet/src/widgets/primary_button.dart';
14
import 'package:cake_wallet/themes/core/material_base_theme.dart';
15
import 'package:flutter/gestures.dart';
16
+
17
import 'package:flutter/material.dart';
7
-import 'package:flutter_svg/svg.dart';
18
19
import 'base_bottom_sheet_widget.dart';
20
21
class LoadingBottomSheet extends BaseBottomSheet {
12
- LoadingBottomSheet({required String titleText, String? titleIconPath})
13
- : super(titleText: titleText, titleIconPath: titleIconPath);
22
+ LoadingBottomSheet(
23
+ {required String titleText, String? titleIconPath})
24
+ : super(titleText: titleText, titleIconPath: titleIconPath, footerType: FooterType.none, maxHeight: 900);
25
26
@override
27
Widget contentWidget(BuildContext context) {
28
return SizedBox(
18
- height: 200,
29
+ height: 300,
30
child: Center(child: CircularProgressIndicator()),
31
);
32
}
22
-
23
- @override
24
- Widget footerWidget(BuildContext context) => const SizedBox(height: 94);
33
}
34
35
class InfoBottomSheet extends BaseBottomSheet {
28
- final MaterialThemeBase currentTheme;
29
- final String? contentImage;
30
- final Color? contentImageColor;
31
- final String? content;
32
- final bool isTwoAction;
33
- final bool showDontAskMeCheckbox;
34
- final bool showDisclaimerText;
35
- final Function(bool)? onCheckboxChanged;
36
- final String? actionButtonText;
37
- final VoidCallback? actionButton;
38
- final Key? actionButtonKey;
39
- final String? leftButtonText;
40
- final String? rightButtonText;
41
- final VoidCallback? actionLeftButton;
42
- final VoidCallback? actionRightButton;
43
- final Key? rightActionButtonKey;
44
- final Key? leftActionButtonKey;
45
- final double height;
46
- final double? contentImageSize;
47
-
36
InfoBottomSheet({
37
required String titleText,
38
String? titleIconPath,
39
required this.currentTheme,
40
+ required this.footerType,
41
this.contentImage,
42
this.contentImageColor,
43
this.contentImageSize,
55
- this.content,
56
- this.isTwoAction = false,
57
- this.showDontAskMeCheckbox = false,
58
- this.showDisclaimerText = false,
44
this.height = 200,
60
- this.onCheckboxChanged,
61
- this.actionButtonText,
62
- this.actionButton,
63
- this.actionButtonKey,
64
- this.leftButtonText,
65
- this.rightButtonText,
66
- this.actionLeftButton,
67
- this.actionRightButton,
68
- this.rightActionButtonKey,
45
+ this.content,
46
+ this.bottomActionPanel,
47
+ this.singleActionButtonText,
48
+ this.onSingleActionButtonPressed,
49
+ this.singleActionButtonKey,
50
+ this.doubleActionLeftButtonText,
51
+ this.doubleActionRightButtonText,
52
+ this.onLeftActionButtonPressed,
53
+ this.onRightActionButtonPressed,
54
this.leftActionButtonKey,
70
- double maxHeight = 900,
71
- }) : super(titleText: titleText, titleIconPath: titleIconPath, maxHeight: maxHeight);
55
+ this.rightActionButtonKey,
56
+ this.showDisclaimerText = true,
57
+ Key? key,
58
+ }) : super(
59
+ titleText: titleText,
60
+ titleIconPath: titleIconPath,
61
+ maxHeight: 900,
62
+ currentTheme: currentTheme,
63
+ footerType: footerType,
64
+ singleActionButtonText: singleActionButtonText,
65
+ onSingleActionButtonPressed: onSingleActionButtonPressed,
66
+ singleActionButtonKey: singleActionButtonKey,
67
+ doubleActionLeftButtonText: doubleActionLeftButtonText,
68
+ doubleActionRightButtonText: doubleActionRightButtonText,
69
+ onLeftActionButtonPressed: onLeftActionButtonPressed,
70
+ onRightActionButtonPressed: onRightActionButtonPressed,
71
+ leftActionButtonKey: leftActionButtonKey,
72
+ rightActionButtonKey: rightActionButtonKey,
73
+ key: key);
74
+
75
+ final MaterialThemeBase currentTheme;
76
+ final FooterType footerType;
77
+ final String? contentImage;
78
+ final Color? contentImageColor;
79
+ final String? content;
80
+ final Widget? bottomActionPanel;
81
+ final String? singleActionButtonText;
82
+ final VoidCallback? onSingleActionButtonPressed;
83
+ final Key? singleActionButtonKey;
84
+ final String? doubleActionLeftButtonText;
85
+ final String? doubleActionRightButtonText;
86
+ final VoidCallback? onLeftActionButtonPressed;
87
+ final VoidCallback? onRightActionButtonPressed;
88
+ final Key? rightActionButtonKey;
89
+ final Key? leftActionButtonKey;
90
+ final double height;
91
+ final double? contentImageSize;
92
+ final bool showDisclaimerText;
93
+
94
95
@override
96
Widget contentWidget(BuildContext context) {
@@ -79,9 +101,11 @@ class InfoBottomSheet extends BaseBottomSheet {
101
if (contentImage != null)
102
Expanded(
103
flex: 4,
82
- child: SizedBox(
83
- width: contentImageSize,
84
- child: getImage(contentImage!, imageColor: contentImageColor),
104
+ child: ImageUtil.getImageFromPath(
105
+ imagePath: contentImage!,
106
+ svgImageColor: contentImageColor,
107
+ fit: BoxFit.contain,
108
+ borderRadius: 10,
109
),
110
)
111
else
@@ -110,6 +134,7 @@ class InfoBottomSheet extends BaseBottomSheet {
134
],
135
),
136
),
137
+ bottomActionPanel ?? const SizedBox(),
138
if (showDisclaimerText)
139
Padding(
140
padding: const EdgeInsets.only(top: 20, bottom: 10),
@@ -141,123 +166,8 @@ class InfoBottomSheet extends BaseBottomSheet {
166
),
167
),
168
),
144
- if (showDontAskMeCheckbox)
145
- Padding(
146
- padding: const EdgeInsets.only(left: 34),
147
- child: Row(
148
- children: [
149
- SimpleCheckbox(onChanged: onCheckboxChanged),
150
- const SizedBox(width: 8),
151
- Text(
152
- 'Don’t ask me next time',
153
- textAlign: TextAlign.center,
154
- style: Theme.of(context).textTheme.bodyMedium!.copyWith(
155
- fontWeight: FontWeight.w500,
156
- color: Theme.of(context).colorScheme.onSurfaceVariant,
157
- decoration: TextDecoration.none,
158
- ),
159
- ),
160
- ],
161
- ),
162
- ),
169
],
170
),
171
);
172
}
167
-
168
- @override
169
- Widget footerWidget(BuildContext context) {
170
- if (isTwoAction) {
171
- return Padding(
172
- padding: const EdgeInsets.fromLTRB(16, 0, 16, 34),
173
- child: Row(
174
- mainAxisSize: MainAxisSize.max,
175
- children: [
176
- Flexible(
177
- child: Container(
178
- padding: const EdgeInsets.only(right: 8.0, top: 8.0),
179
- child: PrimaryButton(
180
- key: leftActionButtonKey,
181
- onPressed: actionLeftButton,
182
- text: leftButtonText ?? '',
183
- color: Theme.of(context).colorScheme.surfaceContainer,
184
- textColor: Theme.of(context).colorScheme.onSecondaryContainer,
185
- ),
186
- ),
187
- ),
188
- Flexible(
189
- child: Container(
190
- padding: const EdgeInsets.only(left: 8.0, top: 8.0),
191
- child: PrimaryButton(
192
- key: rightActionButtonKey,
193
- onPressed: actionRightButton,
194
- text: rightButtonText ?? '',
195
- color: Theme.of(context).colorScheme.primary,
196
- textColor: Theme.of(context).colorScheme.onPrimary,
197
- ),
198
- ),
199
- ),
200
- ],
201
- ),
202
- );
203
- } else {
204
- return Padding(
205
- padding: const EdgeInsets.fromLTRB(16, 12, 16, 34),
206
- child: LoadingPrimaryButton(
207
- key: actionButtonKey,
208
- onPressed: actionButton ?? () {},
209
- text: actionButtonText ?? '',
210
- color: Theme.of(context).colorScheme.primary,
211
- textColor: Theme.of(context).colorScheme.onPrimary,
212
- isLoading: false,
213
- isDisabled: false,
214
- ),
215
- );
216
- }
217
- }
218
-
219
- Widget getImage(String imagePath, {Color? imageColor}) {
220
- final bool isSvg = imagePath.endsWith('.svg');
221
- if (isSvg) {
222
- return SvgPicture.asset(
223
- imagePath,
224
- colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
225
- );
226
- } else {
227
- return Image.asset(imagePath);
228
- }
229
- }
230
-}
231
-
232
-class SimpleCheckbox extends StatefulWidget {
233
- SimpleCheckbox({this.onChanged});
234
-
235
- final Function(bool)? onChanged;
236
-
237
- @override
238
- State<SimpleCheckbox> createState() => _SimpleCheckboxState();
239
-}
240
-
241
-class _SimpleCheckboxState extends State<SimpleCheckbox> {
242
- bool initialValue = false;
243
-
244
- @override
245
- Widget build(BuildContext context) {
246
- return SizedBox(
247
- height: 24.0,
248
- width: 24.0,
249
- child: Checkbox(
250
- value: initialValue,
251
- onChanged: (value) => setState(() {
252
- initialValue = value!;
253
- widget.onChanged?.call(value);
254
- }),
255
- checkColor: Theme.of(context).colorScheme.onSurfaceVariant,
256
- activeColor: Colors.transparent,
257
- materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
258
- side: WidgetStateBorderSide.resolveWith((states) =>
259
- BorderSide(color: Theme.of(context).colorScheme.onSurfaceVariant, width: 1.0)),
260
- ),
261
- );
262
- }
173
}
lib/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart
+1
-1
@@ -21,7 +21,7 @@ class InfoStepsBottomSheet extends BaseBottomSheet {
21
required this.steps,
22
String? titleIconPath,
23
required this.currentTheme,
24
- }) : super(titleText: titleText, titleIconPath: titleIconPath);
24
+ }) : super(titleText: titleText, titleIconPath: titleIconPath, footerType: FooterType.none, maxHeight: 900);
25
26
@override
27
Widget contentWidget(BuildContext context) => SizedBox(
lib/src/widgets/number_text_fild_widget.dart
+33
-53
@@ -56,67 +56,47 @@ class _NumberTextFieldState extends State<NumberTextField> {
56
57
@override
58
Widget build(BuildContext context) => TextField(
59
- style: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurfaceVariant),
60
- enableInteractiveSelection: false,
61
- textAlign: TextAlign.center,
62
- textAlignVertical: TextAlignVertical.bottom,
63
- controller: _controller,
64
- focusNode: _focusNode,
65
- textInputAction: TextInputAction.done,
66
- keyboardType: TextInputType.number,
67
- maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0),
68
- decoration: InputDecoration(
59
+ style: Theme.of(context).textTheme.titleMedium!,
60
+ enableInteractiveSelection: false,
61
+ textAlign: TextAlign.center,
62
+ textAlignVertical: TextAlignVertical.bottom,
63
+ controller: _controller,
64
+ focusNode: _focusNode,
65
+ textInputAction: TextInputAction.done,
66
+ keyboardType: TextInputType.number,
67
+ maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0),
68
+ decoration: InputDecoration(
69
border: InputBorder.none,
70
contentPadding: EdgeInsets.all(0),
71
fillColor: Colors.transparent,
72
counterText: '',
73
isDense: true,
74
filled: true,
75
- suffixIconConstraints: BoxConstraints(
76
- maxHeight: widget.arrowsHeight,
77
- maxWidth: widget.arrowsWidth + widget.contentPadding.right,
78
- ),
79
- prefixIconConstraints: BoxConstraints(
80
- maxHeight: widget.arrowsHeight,
81
- maxWidth: widget.arrowsWidth + widget.contentPadding.left,
82
- ),
75
+ suffixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
76
+ prefixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
77
prefixIcon: Material(
84
- type: MaterialType.transparency,
85
- child: InkWell(
86
- child: Container(
87
- width: widget.arrowsWidth,
88
- alignment: Alignment.bottomCenter,
89
- child: Icon(
90
- Icons.arrow_left_outlined,
91
- size: widget.arrowsWidth,
92
- ),
93
- ),
94
- onTap: _canGoDown ? () => _update(false) : null,
95
- ),
96
- ),
78
+ type: MaterialType.transparency,
79
+ child: InkWell(
80
+ child: Container(
81
+ width: widget.arrowsWidth,
82
+ alignment: Alignment.bottomCenter,
83
+ child: Icon(Icons.keyboard_arrow_left_outlined ,size: widget.arrowsWidth)),
84
+ onTap: _canGoDown ? () => _update(false) : null)),
85
suffixIcon: Material(
98
- type: MaterialType.transparency,
99
- child: InkWell(
100
- child: Container(
101
- width: widget.arrowsWidth,
102
- alignment: Alignment.bottomCenter,
103
- child: Icon(
104
- Icons.arrow_right_outlined,
105
- size: widget.arrowsWidth,
106
- ),
107
- ),
108
- onTap: _canGoUp ? () => _update(true) : null,
109
- ),
110
- ),
111
- ),
112
- maxLines: 1,
113
- onChanged: (value) {
114
- final intValue = int.tryParse(value);
115
- widget.onChanged?.call(intValue);
116
- _updateArrows(intValue);
117
- },
118
- inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)],
119
- );
86
+ type: MaterialType.transparency,
87
+ child: InkWell(
88
+ child: Container(
89
+ width: widget.arrowsWidth,
90
+ alignment: Alignment.bottomCenter,
91
+ child: Icon(Icons.keyboard_arrow_right_outlined, size: widget.arrowsWidth)),
92
+ onTap: _canGoUp ? () => _update(true) : null))),
93
+ maxLines: 1,
94
+ onChanged: (value) {
95
+ final intValue = int.tryParse(value);
96
+ widget.onChanged?.call(intValue);
97
+ _updateArrows(intValue);
98
+ },
99
+ inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)]);
100
101
void _update(bool up) {
102
var intValue = int.tryParse(_controller.text);
lib/src/widgets/rounded_checkbox.dart
renamed
lib/src/widgets/simple_checkbox.dart
new
+34
@@ -0,0 +1,34 @@
1
+import 'package:flutter/material.dart';
2
+
3
+class SimpleCheckbox extends StatefulWidget {
4
+ SimpleCheckbox({this.onChanged});
5
+
6
+ final Function(bool)? onChanged;
7
+
8
+ @override
9
+ State<SimpleCheckbox> createState() => _SimpleCheckboxState();
10
+}
11
+
12
+class _SimpleCheckboxState extends State<SimpleCheckbox> {
13
+ bool initialValue = false;
14
+
15
+ @override
16
+ Widget build(BuildContext context) {
17
+ return SizedBox(
18
+ height: 24.0,
19
+ width: 24.0,
20
+ child: Checkbox(
21
+ value: initialValue,
22
+ onChanged: (value) => setState(() {
23
+ initialValue = value!;
24
+ widget.onChanged?.call(value);
25
+ }),
26
+ checkColor: Theme.of(context).textTheme.titleLarge!.color,
27
+ activeColor: Colors.transparent,
28
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
29
+ side: WidgetStateBorderSide.resolveWith((states) => BorderSide(
30
+ color: Theme.of(context).textTheme.titleLarge!.color!, width: 1.0)),
31
+ ),
32
+ );
33
+ }
34
+}
\ No newline at end of file
lib/src/widgets/standard_slide_button_widget.dart
+5
-2
@@ -1,5 +1,6 @@
1
import 'package:cake_wallet/src/widgets/primary_button.dart';
2
import 'package:cake_wallet/themes/core/material_base_theme.dart';
3
+import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
4
import 'package:flutter/material.dart';
5
6
class StandardSlideButton extends StatefulWidget {
@@ -29,7 +30,9 @@ class _StandardSlideButtonState extends State<StandardSlideButton> {
30
Widget build(BuildContext context) {
31
final bool accessible = MediaQuery.of(context).accessibleNavigation;
32
32
- final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
33
+ final tileBackgroundColor = widget.currentTheme.isDark
34
+ ? CustomThemeColors.backgroundGradientColorDark
35
+ : CustomThemeColors.backgroundGradientColorLight;
36
37
return accessible
38
? PrimaryButton(
@@ -87,7 +90,7 @@ class _StandardSlideButtonState extends State<StandardSlideButton> {
90
height: widget.height - 8,
91
decoration: BoxDecoration(
92
borderRadius: BorderRadius.circular(10),
90
- color: Theme.of(context).colorScheme.primary,
93
+ color: Theme.of(context).colorScheme.surface,
94
),
95
alignment: Alignment.center,
96
child: Icon(
lib/src/widgets/tab_view_wrapper_widget.dart
new
+79
@@ -0,0 +1,79 @@
1
+import 'package:flutter/material.dart';
2
+
3
+class TabViewWrapper extends StatefulWidget {
4
+ const TabViewWrapper({
5
+ super.key,
6
+ required this.tabs,
7
+ required this.views,
8
+ this.tabBarPadding = const EdgeInsets.only(right: 24),
9
+ this.labelStyle,
10
+ this.unselectedLabelStyle,
11
+ this.indicatorColor,
12
+ }) : assert(tabs.length == views.length, 'Tabs and views must be of equal length.');
13
+
14
+ final List<Tab> tabs;
15
+ final List<Widget> views;
16
+ final EdgeInsets tabBarPadding;
17
+ final TextStyle? labelStyle;
18
+ final TextStyle? unselectedLabelStyle;
19
+ final Color? indicatorColor;
20
+
21
+ @override
22
+ State<TabViewWrapper> createState() => _TabViewWrapperState();
23
+}
24
+
25
+class _TabViewWrapperState extends State<TabViewWrapper> with SingleTickerProviderStateMixin {
26
+ late final TabController _tabController;
27
+
28
+ @override
29
+ void initState() {
30
+ super.initState();
31
+ _tabController = TabController(length: widget.tabs.length, vsync: this);
32
+ }
33
+
34
+ @override
35
+ void dispose() {
36
+ _tabController.dispose();
37
+ super.dispose();
38
+ }
39
+
40
+ @override
41
+ Widget build(BuildContext context) {
42
+ final textStyle = TextStyle(
43
+ fontSize: 18,
44
+ fontFamily: 'Lato',
45
+ fontWeight: FontWeight.w600,
46
+ color: Theme.of(context).colorScheme.onSurface);
47
+
48
+ return Column(
49
+ children: [
50
+ Align(
51
+ alignment: Alignment.centerLeft,
52
+ child: TabBar(
53
+ controller: _tabController,
54
+ isScrollable: true,
55
+ splashFactory: NoSplash.splashFactory,
56
+ indicatorSize: TabBarIndicatorSize.label,
57
+ labelStyle: widget.labelStyle ?? textStyle,
58
+ unselectedLabelStyle: widget.unselectedLabelStyle ??
59
+ textStyle.copyWith(color: textStyle.color?.withAlpha(150)),
60
+ labelColor: widget.labelStyle?.color ?? textStyle.color,
61
+ indicatorColor: widget.indicatorColor,
62
+ indicatorPadding: EdgeInsets.zero,
63
+ labelPadding: widget.tabBarPadding,
64
+ tabAlignment: TabAlignment.start,
65
+ dividerColor: Colors.transparent,
66
+ padding: EdgeInsets.zero,
67
+ tabs: widget.tabs,
68
+ ),
69
+ ),
70
+ Expanded(
71
+ child: TabBarView(
72
+ controller: _tabController,
73
+ children: widget.views,
74
+ ),
75
+ ),
76
+ ],
77
+ );
78
+ }
79
+}
lib/utils/feature_flag.dart
+1
@@ -3,6 +3,7 @@ import 'dart:io';
3
4
class FeatureFlag {
5
static const bool isCakePayEnabled = false;
6
+ static const bool isCakePayRedemptionFlowEnabled = false;
7
static const bool isExolixEnabled = true;
8
static const bool isBackgroundSyncEnabled = true;
9
static final bool isInAppTorEnabled = (Platform.isAndroid);
lib/utils/image_utill.dart
+55
-63
@@ -3,89 +3,81 @@ import 'package:flutter/material.dart';
3
import 'package:flutter_svg/svg.dart';
4
5
class ImageUtil {
6
- static Widget getImageFromPath({required String imagePath, double? height, double? width}) {
6
+ static Widget getImageFromPath({
7
+ required String imagePath,
8
+ double? height,
9
+ double? width,
10
+ Color? svgImageColor,
11
+ BoxFit? fit,
12
+ double? borderRadius,
13
+ }) {
14
bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
15
+
16
if (CakeTor.instance.enabled && isNetworkImage) {
17
imagePath = "assets/images/tor_logo.svg";
18
isNetworkImage = false;
19
}
12
- final bool isSvg = imagePath.endsWith('.svg');
13
- final double _height = height ?? 35;
14
- final double _width = width ?? 35;
20
+ final isSvg = imagePath.endsWith('.svg');
21
+ final bool ignoreSize = fit != null;
22
+ final double? _height = ignoreSize ? null : (height ?? 35);
23
+ final double? _width = ignoreSize ? null : (width ?? 35);
24
+
25
+ Widget img;
26
if (isNetworkImage) {
16
- return isSvg
17
- ? SvgPicture.network(
27
+ img = isSvg
28
+ ? SvgPicture.network(imagePath,
29
key: ValueKey(imagePath),
19
- imagePath,
30
height: _height,
31
width: _width,
22
- placeholderBuilder: (BuildContext context) => Container(
23
- height: _height,
24
- width: _width,
25
- child: Center(
26
- child: CircularProgressIndicator(),
27
- ),
28
- ),
29
- errorBuilder: (_, __, ___) {
30
- return Container(
31
- height: _height,
32
- width: _width,
33
- child: Center(
34
- child: Icon(Icons.error_outline, color: Colors.grey),
35
- ),
36
- );
37
- },
38
- )
39
- : Image.network(
32
+ fit: fit ?? BoxFit.contain,
33
+ placeholderBuilder: (_) => _placeholder(_height, _width),
34
+ errorBuilder: (_, __, ___) => _errorPlaceholder(_height, _width))
35
+ : Image.network(imagePath,
36
key: ValueKey(imagePath),
41
- imagePath,
37
height: _height,
38
width: _width,
44
- loadingBuilder:
45
- (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
46
- if (loadingProgress == null) {
47
- return child;
48
- }
49
- return Container(
50
- height: _height,
51
- width: _width,
52
- child: Center(
53
- child: CircularProgressIndicator(
54
- value: loadingProgress.expectedTotalBytes != null
55
- ? loadingProgress.cumulativeBytesLoaded /
56
- loadingProgress.expectedTotalBytes!
57
- : null,
58
- ),
59
- ),
60
- );
61
- },
62
- errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
63
- return Container(
64
- height: _height,
65
- width: _width,
66
- child: Center(
67
- child: Icon(Icons.error_outline, color: Colors.grey),
68
- ),
69
- );
70
- },
71
- );
39
+ fit: fit,
40
+ loadingBuilder: (_, child, progress) =>
41
+ progress == null ? child : _placeholder(_height, _width),
42
+ errorBuilder: (_, __, ___) => _errorPlaceholder(_height, _width));
43
} else {
73
- return isSvg
74
- ? SvgPicture.asset(
75
- imagePath,
44
+ img = isSvg
45
+ ? SvgPicture.asset(imagePath,
46
+ key: ValueKey(imagePath),
47
height: _height,
48
width: _width,
78
- placeholderBuilder: (_) => Icon(Icons.error),
79
- errorBuilder: (_, __, ___) => Icon(Icons.error),
80
- key: ValueKey(imagePath),
81
- )
49
+ fit: fit ?? BoxFit.contain,
50
+ colorFilter:
51
+ svgImageColor != null ? ColorFilter.mode(svgImageColor, BlendMode.srcIn) : null,
52
+ placeholderBuilder: (_) => _placeholder(_height, _width),
53
+ errorBuilder: (_, __, ___) => _errorPlaceholder(_height, _width))
54
: Image.asset(
55
imagePath,
56
+ key: ValueKey(imagePath),
57
height: _height,
58
width: _width,
86
- errorBuilder: (_, __, ___) => Icon(Icons.error),
87
- key: ValueKey(imagePath),
59
+ fit: fit,
60
+ errorBuilder: (_, __, ___) => _errorPlaceholder(_height, _width),
61
);
62
}
63
+
64
+ if (borderRadius != null && borderRadius > 0) {
65
+ img = ClipRRect(
66
+ borderRadius: BorderRadius.circular(borderRadius),
67
+ child: img,
68
+ );
69
+ }
70
+ return img;
71
}
72
+
73
+ static Widget _placeholder(double? h, double? w) => (h != null || w != null)
74
+ ? SizedBox(height: h, width: w, child: const Center(child: CircularProgressIndicator()))
75
+ : const Center(child: CircularProgressIndicator());
76
+
77
+ static Widget _errorPlaceholder(double? h, double? w) => (h != null || w != null)
78
+ ? SizedBox(
79
+ height: h,
80
+ width: w,
81
+ child: const Center(child: Icon(Icons.error_outline, color: Colors.grey)))
82
+ : const Center(child: Icon(Icons.error_outline, color: Colors.grey));
83
}
lib/view_model/cake_pay/cake_pay_account_view_model.dart
+1
-1
@@ -1,4 +1,4 @@
1
-import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
1
+import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
2
import 'package:mobx/mobx.dart';
3
4
part 'cake_pay_account_view_model.g.dart';
lib/view_model/cake_pay/cake_pay_auth_view_model.dart
+2
-2
@@ -1,5 +1,5 @@
1
-import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
2
-import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
1
+import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
2
+import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
3
import 'package:mobx/mobx.dart';
4
5
part 'cake_pay_auth_view_model.g.dart';
lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart
+199
-8
@@ -1,5 +1,14 @@
1
-import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
2
-import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
1
+import 'dart:async';
2
+
3
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
4
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
5
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
6
+import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
7
+import 'package:cake_wallet/core/execution_state.dart';
8
+import 'package:cake_wallet/utils/feature_flag.dart';
9
+import 'package:cake_wallet/view_model/send/send_view_model.dart';
10
+import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
11
+import 'package:cw_core/wallet_type.dart';
12
import 'package:mobx/mobx.dart';
13
14
part 'cake_pay_buy_card_view_model.g.dart';
@@ -7,20 +16,36 @@ part 'cake_pay_buy_card_view_model.g.dart';
16
class CakePayBuyCardViewModel = CakePayBuyCardViewModelBase with _$CakePayBuyCardViewModel;
17
18
abstract class CakePayBuyCardViewModelBase with Store {
10
- CakePayBuyCardViewModelBase({required this.vendor})
11
- : amount = vendor.card!.denominations.isNotEmpty
19
+ CakePayBuyCardViewModelBase(
20
+ {required this.vendor, required this.cakePayService, required this.sendViewModel})
21
+ : walletType = sendViewModel.walletType,
22
+ amount = vendor.card!.denominations.isNotEmpty
23
? double.parse(vendor.card!.denominations.first)
24
: 0,
25
quantity = 1,
26
min = double.parse(vendor.card!.minValue ?? '0'),
27
max = double.parse(vendor.card!.maxValue ?? '0'),
17
- card = vendor.card!;
28
+ card = vendor.card! {
29
+ selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
30
+ }
31
32
final CakePayVendor vendor;
33
+ final SendViewModel sendViewModel;
34
+ final CakePayService cakePayService;
35
+ final double max;
36
+ final double min;
37
final CakePayCard card;
38
+ final WalletType walletType;
39
22
- final double min;
23
- final double max;
40
+ CakePayOrder? order;
41
+ Timer? _timer;
42
+ DateTime? expirationTime;
43
+ Duration? remainingTime;
44
+ bool confirmsNoVpn = false;
45
+ bool confirmsVoidedRefund = false;
46
+ bool confirmsTermsAgreed = false;
47
+
48
+ String simulatedResponse = '';
49
50
bool get isDenominationSelected => card.denominations.isNotEmpty;
51
@@ -30,13 +55,48 @@ abstract class CakePayBuyCardViewModelBase with Store {
55
@observable
56
int quantity;
57
58
+ @observable
59
+ bool isPurchasing = false;
60
+
61
+ @observable
62
+ bool isSimulatingFlow = false;
63
+
64
+ @observable
65
+ bool isOrderExpired = false;
66
+
67
+ @observable
68
+ String formattedRemainingTime = '';
69
+
70
@computed
34
- bool get isEnablePurchase =>
71
+ bool get isAmountSufficient =>
72
(amount >= min && amount <= max) || (isDenominationSelected && quantity > 0);
73
74
+ @observable
75
+ CakePayPaymentMethod? selectedPaymentMethod;
76
+
77
@computed
78
double get totalAmount => amount * quantity;
79
80
+ @computed
81
+ bool get isSimulating => isSimulatingFlow && FeatureFlag.hasDevOptions;
82
+
83
+ @computed
84
+ List<CakePayPaymentMethod> get availableMethods {
85
+ switch (walletType) {
86
+ case WalletType.bitcoin:
87
+ return [CakePayPaymentMethod.BTC];
88
+ case WalletType.litecoin:
89
+ return [CakePayPaymentMethod.LTC, CakePayPaymentMethod.LTC_MWEB];
90
+ case WalletType.monero:
91
+ return [CakePayPaymentMethod.XMR];
92
+ default:
93
+ return const [];
94
+ }
95
+ }
96
+
97
+ @action
98
+ void chooseMethod(CakePayPaymentMethod method) => selectedPaymentMethod = method;
99
+
100
@action
101
void onQuantityChanged(int? input) => quantity = input ?? 1;
102
@@ -45,4 +105,135 @@ abstract class CakePayBuyCardViewModelBase with Store {
105
if (input.isEmpty) return;
106
amount = double.parse(input.replaceAll(',', '.'));
107
}
108
+
109
+ CryptoPaymentData? getPaymentDataFor(CakePayPaymentMethod? method) {
110
+ if (order == null || method == null) return null;
111
+
112
+
113
+ final data = switch (method) {
114
+ CakePayPaymentMethod.BTC => order?.paymentData.btc,
115
+ CakePayPaymentMethod.XMR => order?.paymentData.xmr,
116
+ CakePayPaymentMethod.LTC => order?.paymentData.ltc,
117
+ CakePayPaymentMethod.LTC_MWEB => order?.paymentData.ltc_mweb,
118
+ _ => null
119
+ };
120
+
121
+ if (data == null) return null;
122
+
123
+ final bip21 = data.paymentUrls?.bip21;
124
+ if (bip21 != null && bip21.isNotEmpty) {
125
+ final uri = Uri.parse(bip21);
126
+ final addr = uri.path;
127
+ final price = uri.queryParameters['amount'] ?? data.price;
128
+
129
+ return CryptoPaymentData(price: price, address: addr);
130
+ }
131
+
132
+ return data;
133
+ }
134
+
135
+ @action
136
+ Future<void> createOrder() async {
137
+ if (walletType != WalletType.bitcoin &&
138
+ walletType != WalletType.monero &&
139
+ walletType != WalletType.litecoin) {
140
+ sendViewModel.state =
141
+ FailureState('Unsupported wallet type, please use Bitcoin, Monero, or Litecoin.');
142
+ }
143
+ try {
144
+ order = await cakePayService.createOrder(
145
+ cardId: card.id,
146
+ price: amount.toString(),
147
+ quantity: quantity,
148
+ confirmsNoVpn: confirmsNoVpn,
149
+ confirmsVoidedRefund: confirmsVoidedRefund,
150
+ confirmsTermsAgreed: confirmsTermsAgreed,
151
+ );
152
+ await confirmSending();
153
+ expirationTime = order!.paymentData.expirationTime;
154
+ updateRemainingTime();
155
+ _startExpirationTimer();
156
+ } catch (e) {
157
+ sendViewModel.state = FailureState(
158
+ sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
159
+ }
160
+ }
161
+
162
+ @action
163
+ Future<void> confirmSending() async {
164
+ final cryptoPaymentData = getPaymentDataFor(selectedPaymentMethod);
165
+ if (order == null || cryptoPaymentData == null) return;
166
+
167
+ try {
168
+ sendViewModel.clearOutputs();
169
+ final output = sendViewModel.outputs.first;
170
+ output.address = cryptoPaymentData.address;
171
+ output.setCryptoAmount(cryptoPaymentData.price);
172
+
173
+ await sendViewModel.createTransaction();
174
+ } catch (e) {
175
+ throw e;
176
+ }
177
+ }
178
+
179
+ @action
180
+ Future<void> simulatePayment() async {
181
+ if (order == null) {
182
+ throw Exception('Order is not created yet.');
183
+ }
184
+
185
+ try {
186
+ simulatedResponse = await cakePayService.simulatePayment(orderId: order!.orderId);
187
+ sendViewModel.state = TransactionCommitted();
188
+
189
+ } catch (e) {
190
+ sendViewModel.state = FailureState(
191
+ sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
192
+ }
193
+ }
194
+
195
+ @action
196
+ void updateRemainingTime() {
197
+ if (expirationTime == null) {
198
+ formattedRemainingTime = '';
199
+ return;
200
+ }
201
+
202
+ remainingTime = expirationTime!.difference(DateTime.now());
203
+
204
+ isOrderExpired = remainingTime!.isNegative;
205
+
206
+ if (isOrderExpired) {
207
+ disposeExpirationTimer();
208
+ sendViewModel.state = FailureState('Order has expired.');
209
+ } else {
210
+ formattedRemainingTime = formatDuration(remainingTime!);
211
+ }
212
+ }
213
+
214
+ void _startExpirationTimer() {
215
+ _timer?.cancel();
216
+ _timer = Timer.periodic(Duration(seconds: 1), (_) {
217
+ updateRemainingTime();
218
+ });
219
+ }
220
+
221
+ String formatDuration(Duration duration) {
222
+ final hours = duration.inHours;
223
+ final minutes = duration.inMinutes.remainder(60);
224
+ final seconds = duration.inSeconds.remainder(60);
225
+ return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds
226
+ .toString().padLeft(2, '0')}';
227
+ }
228
+
229
+ void disposeExpirationTimer() {
230
+ _timer?.cancel();
231
+ remainingTime = null;
232
+ formattedRemainingTime = '';
233
+ expirationTime = null;
234
+ }
235
+
236
+ void dispose() {
237
+ disposeExpirationTimer();
238
+ }
239
}
lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart
+80
-9
@@ -1,6 +1,9 @@
1
-import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
2
-import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
3
-import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
1
+import 'dart:async';
2
+
3
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
4
+import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
5
+import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
6
+import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
7
import 'package:cake_wallet/entities/country.dart';
8
import 'package:cake_wallet/entities/fiat_currency.dart';
9
import 'package:cake_wallet/generated/i18n.dart';
@@ -17,8 +20,8 @@ abstract class CakePayCardsListViewModelBase with Store {
20
CakePayCardsListViewModelBase({
21
required this.cakePayService,
22
required this.settingsStore,
20
- }) : cardState = CakePayCardsStateNoCards(),
21
- cakePayVendors = [],
23
+ }) : cakePayVendors = [],
24
+ userCards = [],
25
availableCountries = [],
26
page = 1,
27
displayPrepaidCards = true,
@@ -28,8 +31,11 @@ abstract class CakePayCardsListViewModelBase with Store {
31
scrollOffsetFromTop = 0.0,
32
vendorsState = InitialCakePayVendorLoadingState(),
33
createCardState = CakePayCreateCardState(),
34
+ userCardState = UserCakePayCardsStateInitial(),
35
searchString = '',
36
+ searchMyCardsString = '',
37
CakePayVendorList = <CakePayVendor>[] {
38
+ checkAuth();
39
initialization();
40
}
41
@@ -43,6 +49,7 @@ abstract class CakePayCardsListViewModelBase with Store {
49
void initialization() async {
50
await getCountries();
51
getVendors();
52
+ getUserCards();
53
}
54
55
final CakePayService cakePayService;
@@ -51,7 +58,7 @@ abstract class CakePayCardsListViewModelBase with Store {
58
List<CakePayVendor> CakePayVendorList;
59
60
Map<String, List<FilterItem>> get createFilterItems => {
54
- S.current.filter_by: [
61
+ 'Card Type': [
62
FilterItem(
63
value: () => displayPrepaidCards,
64
caption: S.current.prepaid_cards,
@@ -74,6 +81,7 @@ abstract class CakePayCardsListViewModelBase with Store {
81
};
82
83
String searchString;
84
+ String? username;
85
int page;
86
87
late Country _initialSelectedCountry;
@@ -82,6 +90,12 @@ abstract class CakePayCardsListViewModelBase with Store {
90
late bool _initialDisplayDenominationsCards;
91
late bool _initialDisplayCustomValueCards;
92
93
+ @observable
94
+ List<CakePayCard> userCards;
95
+
96
+ @observable
97
+ String searchMyCardsString;
98
+
99
@observable
100
double scrollOffsetFromTop;
101
@@ -89,7 +103,7 @@ abstract class CakePayCardsListViewModelBase with Store {
103
CakePayCreateCardState createCardState;
104
105
@observable
92
- CakePayCardsState cardState;
106
+ UserCakePayCardsState userCardState;
107
108
@observable
109
CakePayVendorState vendorsState;
@@ -118,12 +132,29 @@ abstract class CakePayCardsListViewModelBase with Store {
132
@observable
133
bool displayCustomValueCards;
134
135
+ @observable
136
+ ObservableFuture<bool>? authFuture;
137
+
138
+ @computed
139
+ bool? get isUserAuthenticated =>
140
+ authFuture?.status == FutureStatus.fulfilled ? authFuture?.value : null;
141
+
142
@computed
143
Country get selectedCountry =>
144
settingsStore.selectedCakePayCountry ?? _getInitialCountry(settingsStore.fiatCurrency);
145
146
@computed
126
- bool get shouldShowCountryPicker => settingsStore.selectedCakePayCountry == null && availableCountries.isNotEmpty;
147
+ bool get shouldShowCountryPicker =>
148
+ settingsStore.selectedCakePayCountry == null && availableCountries.isNotEmpty;
149
+
150
+ @computed
151
+ List<CakePayCard> get filteredUserCards {
152
+ final query = searchMyCardsString.trim().toLowerCase();
153
+ if (query.isEmpty) return userCards;
154
+ return userCards
155
+ .where((card) => card.name.toLowerCase().contains(query))
156
+ .toList(growable: false);
157
+ }
158
159
160
bool get hasFiltersChanged {
@@ -134,7 +165,6 @@ abstract class CakePayCardsListViewModelBase with Store {
165
displayCustomValueCards != _initialDisplayCustomValueCards;
166
}
167
137
-
168
Future<void> getCountries() async {
169
try {
170
availableCountries = await cakePayService.getCountries();
@@ -143,6 +173,44 @@ abstract class CakePayCardsListViewModelBase with Store {
173
}
174
}
175
176
+ Future<void> getUserCards() async {
177
+ //Dummy user cards // TODO: fetch from API
178
+ userCardState = UserCakePayCardsStateFetching();
179
+ try {
180
+ await Future.delayed(const Duration(seconds: 2));
181
+ final vendorsCard = cakePayVendors
182
+ .where((vendor) => vendor.card != null)
183
+ .map((vendor) => vendor.card!)
184
+ .toList();
185
+ userCards = vendorsCard.sublist(0, 10);
186
+
187
+ vendorsCard.forEach((card) {
188
+ if (card.name.toLowerCase().contains('amazon.com')) {
189
+ userCards.add(card);
190
+ }
191
+ });
192
+
193
+ userCardState = UserCakePayCardsStateSuccess();
194
+ if (userCards.isEmpty) {
195
+ userCardState = UserCakePayCardsStateNoCards();
196
+ }
197
+ } catch (e) {
198
+ userCardState = UserCakePayCardsStateFailure(
199
+ error: e.toString(),
200
+ );
201
+ }
202
+ }
203
+
204
+ @action
205
+ Future<void> checkAuth() async {
206
+ authFuture = ObservableFuture(cakePayService.isLogged());
207
+
208
+ final logged = await authFuture!;
209
+ if (logged) {
210
+ username = await cakePayService.getUserEmail();
211
+ }
212
+ }
213
+
214
@action
215
Future<void> getVendors({
216
String? text,
@@ -219,6 +287,9 @@ abstract class CakePayCardsListViewModelBase with Store {
287
settingsStore.selectedCakePayCountry = country;
288
}
289
290
+ @action
291
+ void setMyCardsQuery(String text) => searchMyCardsString = text;
292
+
293
@action
294
void togglePrepaidCards() => displayPrepaidCards = !displayPrepaidCards;
295
lib/view_model/cake_pay/cake_pay_purchase_view_model.dart
deleted
-173
@@ -1,173 +0,0 @@
1
-import 'dart:async';
2
-
3
-import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
4
-import 'package:cake_wallet/cake_pay/cake_pay_order.dart';
5
-import 'package:cake_wallet/cake_pay/cake_pay_payment_credantials.dart';
6
-import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
7
-import 'package:cake_wallet/core/execution_state.dart';
8
-import 'package:cake_wallet/view_model/send/send_view_model.dart';
9
-import 'package:cw_core/wallet_type.dart';
10
-import 'package:mobx/mobx.dart';
11
-
12
-part 'cake_pay_purchase_view_model.g.dart';
13
-
14
-class CakePayPurchaseViewModel = CakePayPurchaseViewModelBase with _$CakePayPurchaseViewModel;
15
-
16
-abstract class CakePayPurchaseViewModelBase with Store {
17
- CakePayPurchaseViewModelBase({
18
- required this.cakePayService,
19
- required this.paymentCredential,
20
- required this.card,
21
- required this.sendViewModel,
22
- }) : walletType = sendViewModel.walletType;
23
-
24
- final WalletType walletType;
25
-
26
- final PaymentCredential paymentCredential;
27
-
28
- final CakePayCard card;
29
-
30
- final SendViewModel sendViewModel;
31
-
32
- final CakePayService cakePayService;
33
-
34
- CakePayOrder? order;
35
-
36
- Timer? _timer;
37
-
38
- DateTime? expirationTime;
39
-
40
- Duration? remainingTime;
41
-
42
- String? get userName => paymentCredential.userName;
43
-
44
- double get amount => paymentCredential.amount;
45
-
46
- int get quantity => paymentCredential.quantity;
47
-
48
- double get totalAmount => paymentCredential.totalAmount;
49
-
50
- String get fiatCurrency => paymentCredential.fiatCurrency;
51
-
52
- bool confirmsNoVpn = false;
53
- bool confirmsVoidedRefund = false;
54
- bool confirmsTermsAgreed = false;
55
-
56
- @observable
57
- bool isPurchasing = false;
58
-
59
- CryptoPaymentData? get cryptoPaymentData {
60
- if (order == null) return null;
61
-
62
- if (WalletType.monero == walletType) {
63
- return order!.paymentData.xmr;
64
- }
65
-
66
- if (WalletType.bitcoin == walletType) {
67
- final paymentUrls = order!.paymentData.btc.paymentUrls!.bip21;
68
-
69
- final uri = Uri.parse(paymentUrls!);
70
-
71
- final address = uri.path;
72
- final price = uri.queryParameters['amount'];
73
-
74
- return CryptoPaymentData(
75
- address: address,
76
- price: price ?? '0',
77
- );
78
- }
79
-
80
- return null;
81
- }
82
-
83
- @observable
84
- bool isOrderExpired = false;
85
-
86
- @observable
87
- String formattedRemainingTime = '';
88
-
89
- @action
90
- Future<void> createOrder() async {
91
- if (walletType != WalletType.bitcoin && walletType != WalletType.monero) {
92
- sendViewModel.state = FailureState('Unsupported wallet type, please use Bitcoin or Monero.');
93
- }
94
- try {
95
- order = await cakePayService.createOrder(
96
- cardId: card.id,
97
- price: paymentCredential.amount.toString(),
98
- quantity: paymentCredential.quantity,
99
- confirmsNoVpn: confirmsNoVpn,
100
- confirmsVoidedRefund: confirmsVoidedRefund,
101
- confirmsTermsAgreed: confirmsTermsAgreed,
102
- );
103
- await confirmSending();
104
- expirationTime = order!.paymentData.expirationTime;
105
- updateRemainingTime();
106
- _startExpirationTimer();
107
- } catch (e) {
108
- sendViewModel.state = FailureState(
109
- sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
110
- }
111
- }
112
-
113
- @action
114
- Future<void> confirmSending() async {
115
- final cryptoPaymentData = this.cryptoPaymentData;
116
- try {
117
- if (order == null || cryptoPaymentData == null) return;
118
-
119
- sendViewModel.clearOutputs();
120
- final output = sendViewModel.outputs.first;
121
- output.address = cryptoPaymentData.address;
122
- output.setCryptoAmount(cryptoPaymentData.price);
123
-
124
- await sendViewModel.createTransaction();
125
- } catch (e) {
126
- throw e;
127
- }
128
- }
129
-
130
- @action
131
- void updateRemainingTime() {
132
- if (expirationTime == null) {
133
- formattedRemainingTime = '';
134
- return;
135
- }
136
-
137
- remainingTime = expirationTime!.difference(DateTime.now());
138
-
139
- isOrderExpired = remainingTime!.isNegative;
140
-
141
- if (isOrderExpired) {
142
- disposeExpirationTimer();
143
- sendViewModel.state = FailureState('Order has expired.');
144
- } else {
145
- formattedRemainingTime = formatDuration(remainingTime!);
146
- }
147
- }
148
-
149
- void _startExpirationTimer() {
150
- _timer?.cancel();
151
- _timer = Timer.periodic(Duration(seconds: 1), (_) {
152
- updateRemainingTime();
153
- });
154
- }
155
-
156
- String formatDuration(Duration duration) {
157
- final hours = duration.inHours;
158
- final minutes = duration.inMinutes.remainder(60);
159
- final seconds = duration.inSeconds.remainder(60);
160
- return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
161
- }
162
-
163
- void disposeExpirationTimer() {
164
- _timer?.cancel();
165
- remainingTime = null;
166
- formattedRemainingTime = '';
167
- expirationTime = null;
168
- }
169
-
170
- void dispose() {
171
- disposeExpirationTimer();
172
- }
173
-}
lib/view_model/dashboard/cake_features_view_model.dart
+1
-1
@@ -1,4 +1,4 @@
1
-import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
1
+import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
2
import 'package:mobx/mobx.dart';
3
4
part 'cake_features_view_model.g.dart';
lib/view_model/dashboard/dropdown_filter_item_widget.dart
+37
-47
@@ -1,67 +1,57 @@
1
-import 'package:auto_size_text/auto_size_text.dart';
1
import 'package:flutter/material.dart';
2
4
-class DropdownFilterList extends StatefulWidget {
5
- DropdownFilterList({
3
+class DropdownFilterList extends StatelessWidget {
4
+ const DropdownFilterList({
5
Key? key,
6
required this.items,
8
- this.itemPrefix,
9
- this.textStyle,
10
- required this.caption,
7
required this.selectedItem,
8
required this.onItemSelected,
9
+ this.itemPrefix,
10
}) : super(key: key);
11
12
final List<String> items;
16
- final String? itemPrefix;
17
- final TextStyle? textStyle;
18
- final String caption;
13
final String selectedItem;
20
- final Function(String) onItemSelected;
21
-
22
- @override
23
- _DropdownFilterListState createState() => _DropdownFilterListState();
24
-}
25
-
26
-class _DropdownFilterListState extends State<DropdownFilterList> {
27
- String? selectedValue;
28
-
29
- @override
30
- void initState() {
31
- super.initState();
32
- selectedValue = widget.selectedItem;
33
- }
14
+ final String? itemPrefix;
15
+ final ValueChanged<String> onItemSelected;
16
17
@override
18
Widget build(BuildContext context) {
19
return DropdownButtonHideUnderline(
38
- child: Container(
39
- child: DropdownButton<String>(
40
- isExpanded: true,
41
- icon: Container(
42
- child: Column(
43
- mainAxisAlignment: MainAxisAlignment.end,
44
- children: [
45
- Icon(Icons.arrow_drop_down, color: Theme.of(context).colorScheme.onSurfaceVariant),
46
- ],
47
- ),
48
- ),
49
- dropdownColor: Theme.of(context).colorScheme.surfaceContainerHighest,
20
+ child: DropdownButton<String>(
21
+ isDense: true,
22
+ dropdownColor: Theme.of(context).primaryColor,
23
borderRadius: BorderRadius.circular(10),
51
- items: widget.items
52
- .map((item) => DropdownMenuItem<String>(
53
- alignment: Alignment.bottomCenter,
54
- value: item,
55
- child: AutoSizeText('${widget.itemPrefix ?? ''} $item', style: widget.textStyle),
56
- ))
24
+ selectedItemBuilder: (context) => items
25
+ .map(
26
+ (item) => Text(
27
+ '${itemPrefix ?? ''} $item',
28
+ style: Theme.of(context).textTheme.titleMedium!,
29
+ maxLines: 1,
30
+ ),
31
+ )
32
+ .toList(),
33
+ items: items
34
+ .map(
35
+ (item) => DropdownMenuItem<String>(
36
+ value: item,
37
+ alignment: Alignment.centerLeft,
38
+ child: Text(
39
+ '${itemPrefix ?? ''} $item',
40
+ style: (const TextStyle()).copyWith(
41
+ color: Colors.white,
42
+ fontWeight: FontWeight.w600,
43
+ ),
44
+ maxLines: 1,
45
+ ),
46
+ ),
47
+ )
48
.toList(),
58
- value: selectedValue,
59
- onChanged: (newValue) {
60
- setState(() => selectedValue = newValue);
61
- widget.onItemSelected(newValue!);
49
+ value: selectedItem,
50
+ onChanged: (value) {
51
+ if (value != null) onItemSelected(value);
52
},
63
- ),
64
- ),
53
+ icon: Icon(Icons.keyboard_arrow_down_outlined,
54
+ color: Theme.of(context).colorScheme.onSurfaceVariant)),
55
);
56
}
57
}
lib/view_model/send/output.dart
+26
@@ -335,3 +335,29 @@ abstract class OutputBase with Store {
335
note = parsedAddress.description;
336
}
337
}
338
+
339
+extension OutputCopyWith on Output {
340
+ Output OutputCopyWithParsedAddress({
341
+ ParsedAddress? parsedAddress,
342
+ String? fiatAmount,
343
+ }) {
344
+ final clone = Output(
345
+ _wallet,
346
+ _settingsStore,
347
+ _fiatConversationStore,
348
+ cryptoCurrencyHandler,
349
+ );
350
+
351
+ clone
352
+ ..cryptoAmount = cryptoAmount
353
+ ..cryptoFullBalance = cryptoFullBalance
354
+ ..note = note
355
+ ..sendAll = sendAll
356
+ ..memo = memo
357
+ ..stealthAddress = stealthAddress
358
+ ..parsedAddress = parsedAddress ?? this.parsedAddress
359
+ ..fiatAmount = fiatAmount ?? this.fiatAmount;
360
+
361
+ return clone;
362
+ }
363
+}
\ No newline at end of file
res/values/strings_ar.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": " كعكة 2FA مسبقا",
109
"cake_dark_theme": "موضوع الكعكة الظلام",
110
"cake_pay_account_note": "قم بالتسجيل باستخدام عنوان بريد إلكتروني فقط لمشاهدة البطاقات وشرائها. حتى أن بعضها متوفر بسعر مخفض!",
111
+ "cake_pay_card_email_delivered_message": "سيتم تسليم بطاقة الهدايا الخاصة بك عبر البريد الإلكتروني بعد التأكيدات اللازمة.",
112
"cake_pay_learn_more": "شراء واسترداد بطاقات الهدايا على الفور في التطبيق!\nاسحب من اليسار إلى اليمين لمعرفة المزيد.",
113
"cake_pay_save_order": "يجب إرسال البطاقة إلى بريدك الإلكتروني خلال يوم عمل واحد \n حفظ معرف الطلب الخاص بك:",
114
"cake_pay_subtitle": "شراء بطاقات مسبقة الدفع وبطاقات الهدايا في جميع أنحاء العالم",
res/values/strings_bg.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Торта 2FA Preset",
109
"cake_dark_theme": "Торта тъмна тема",
110
"cake_pay_account_note": "Регистрайте се само с един имейл, за да виждате и купувате карти. За някои има дори и отстъпка!",
111
+ "cake_pay_card_email_delivered_message": "Вашата карта за подарък ще бъде доставена по имейл след необходимите потвърждения.",
112
"cake_pay_learn_more": "Купете и използвайте гифткарти директно в приложението!\nПлъзнете отляво надясно, за да научите още.",
113
"cake_pay_save_order": "Картата трябва да бъде изпратена до вашия имейл в рамките на 1 работен ден \n Запазете вашия идентификационен номер на поръчката:",
114
"cake_pay_subtitle": "Купете предплатени карти и карти за подаръци в световен мащаб",
res/values/strings_cs.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Předvolba Cake 2FA",
109
"cake_dark_theme": "Dort tmavé téma",
110
"cake_pay_account_note": "Přihlaste se svou e-mailovou adresou pro zobrazení a nákup karet. Některé jsou dostupné ve slevě!",
111
+ "cake_pay_card_email_delivered_message": "Vaše dárková karta bude doručena e -mailem po nezbytných potvrzeních.",
112
"cake_pay_learn_more": "Okamžitý nákup a uplatnění dárkových karet v aplikaci!\nPřejeďte prstem zleva doprava pro další informace.",
113
"cake_pay_save_order": "Karta by měla být odeslána do vašeho e-mailu do 1 pracovního dne \n Uložit ID objednávky:",
114
"cake_pay_subtitle": "Kupte si celosvětové předplacené karty a dárkové karty",
res/values/strings_de.arb
+3
-2
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Cake 2FA-Voreinstellung",
109
"cake_dark_theme": "Cake Dark Thema",
110
"cake_pay_account_note": "Melden Sie sich nur mit einer E-Mail-Adresse an, um Karten anzuzeigen und zu kaufen. Einige sind sogar mit Rabatt erhältlich!",
111
+ "cake_pay_card_email_delivered_message": "Ihre Geschenkkarte wird nach den erforderlichen Bestätigungen per E -Mail geliefert.",
112
"cake_pay_learn_more": "Kaufen und lösen Sie Geschenkkarten sofort in der App ein!\nWischen Sie von links nach rechts, um mehr zu erfahren.",
113
"cake_pay_save_order": "Die Karte sollte innerhalb von 1 Werktag an Ihre E-Mail gesendet werden, \n Ihre Bestell-ID zu speichern:",
114
"cake_pay_subtitle": "Kaufen Sie weltweite Prepaid-Karten und Geschenkkarten",
@@ -605,8 +606,8 @@
606
"please_choose_one": "Bitte wählen Sie einen",
607
"please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist",
608
"please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.",
608
- "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.",
609
"please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.",
610
+ "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.",
611
"please_select": "Bitte auswählen:",
612
"please_select_backup_file": "Bitte wählen Sie die Sicherungsdatei und geben Sie das Sicherungskennwort ein.",
613
"please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Knoten zu verbinden",
@@ -1127,4 +1128,4 @@
1128
"you_will_send": "Konvertieren von",
1129
"youCanGoBackToYourDapp": "Sie können jetzt zu Ihrem Dapp zurückkehren",
1130
"yy": "YY"
1130
-}
1131
+}
\ No newline at end of file
res/values/strings_en.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Cake 2FA Preset",
109
"cake_dark_theme": "Cake Dark Theme",
110
"cake_pay_account_note": "Sign up with just an email address to see and purchase cards. Some are even available at a discount!",
111
+ "cake_pay_card_email_delivered_message": "Your gift card will be delivered via email after the necessary confirmations.",
112
"cake_pay_learn_more": "Instantly purchase and redeem gift cards in the app!\nSwipe left to right to learn more.",
113
"cake_pay_save_order": "The card should be sent to your e-mail within 1 business day \n Save your Order ID:",
114
"cake_pay_subtitle": "Buy worldwide prepaid cards and gift cards",
res/values/strings_es.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Pastel 2FA preestablecido",
109
"cake_dark_theme": "Tema oscuro",
110
"cake_pay_account_note": "Regístrate con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!",
111
+ "cake_pay_card_email_delivered_message": "Su tarjeta de regalo se entregará por correo electrónico después de las confirmaciones necesarias.",
112
"cake_pay_learn_more": "¡Compra y canjea tarjetas de regalo al instante en la aplicación!\nDesliza el dedo de izquierda a derecha para obtener más información.",
113
"cake_pay_save_order": "La tarjeta debe enviarse a tu correo electrónico dentro de 1 día hábil \n Guardar su ID de pedido:",
114
"cake_pay_subtitle": "Compra tarjetas prepagadas y tarjetas de regalo en todo el mundo",
res/values/strings_fr.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Cake 2FA prédéfini",
109
"cake_dark_theme": "Thème sombre Cake",
110
"cake_pay_account_note": "Inscrivez-vous avec juste une adresse e-mail pour voir et acheter des cartes. Certaines sont même disponibles à prix réduit !",
111
+ "cake_pay_card_email_delivered_message": "Votre carte-cadeau sera livrée par e-mail après les confirmations nécessaires.",
112
"cake_pay_learn_more": "Achetez et utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.",
113
"cake_pay_save_order": "La carte doit être envoyée à votre e-mail dans un jour ouvrable \n Enregistrez votre identifiant de commande:",
114
"cake_pay_subtitle": "Achetez des cartes et des cartes-cadeaux prépayées mondiales",
res/values/strings_ha.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Cake 2FA saiti",
109
"cake_dark_theme": "Cake Dark Jigo",
110
"cake_pay_account_note": "Yi rajista tare da adireshin imel kawai don gani da siyan katunan. Wasu ma suna samuwa a rangwame!",
111
+ "cake_pay_card_email_delivered_message": "Za a kawo katin kyautar ku ta hanyar imel bayan abubuwan da suka wajaba.",
112
"cake_pay_learn_more": "Nan take siya ku kwaso katunan kyaututtuka a cikin app!\nTake hagu zuwa dama don ƙarin koyo.",
113
"cake_pay_save_order": "Ya kamata a aika katin zuwa e-mail ɗinku a cikin rana 1 na kasuwanci \n Ajiye id ku:",
114
"cake_pay_subtitle": "Sayi katunan shirye-shiryen duniya da katunan kyauta",
res/values/strings_hi.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "केक 2एफए प्रीसेट",
109
"cake_dark_theme": "केक डार्क थीम",
110
"cake_pay_account_note": "कार्ड देखने और खरीदने के लिए केवल एक ईमेल पते के साथ साइन अप करें। कुछ छूट पर भी उपलब्ध हैं!",
111
+ "cake_pay_card_email_delivered_message": "आवश्यक पुष्टि के बाद आपका उपहार कार्ड ईमेल के माध्यम से दिया जाएगा।",
112
"cake_pay_learn_more": "ऐप में उपहार कार्ड तुरंत खरीदें और रिडीम करें!\nअधिक जानने के लिए बाएं से दाएं स्वाइप करें।",
113
"cake_pay_save_order": "कार्ड को आपके ई-मेल को 1 व्यावसायिक दिन के भीतर भेजा जाना चाहिए \n आपकी ऑर्डर आईडी सहेजें:",
114
"cake_pay_subtitle": "दुनिया भर में प्रीपेड कार्ड और उपहार कार्ड खरीदें",
res/values/strings_hr.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Cake 2FA Preset",
109
"cake_dark_theme": "TOKA DARKA TEMA",
110
"cake_pay_account_note": "Prijavite se samo s adresom e-pošte da biste vidjeli i kupili kartice. Neke su čak dostupne uz popust!",
111
+ "cake_pay_card_email_delivered_message": "Vaša poklon kartica bit će isporučena putem e -pošte nakon potrebnih potvrda.",
112
"cake_pay_learn_more": "Azonnal vásárolhat és válthat be ajándékutalványokat az alkalmazásban!\nTovábbi információért csúsztassa balról jobbra az ujját.",
113
"cake_pay_save_order": "Karticu treba poslati na vašu e-poštu u roku od 1 radnog dana \n Spremi ID narudžbe:",
114
"cake_pay_subtitle": "Kupite svjetske unaprijed plaćene kartice i poklon kartice",
res/values/strings_hy.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Cake 2FA նախապես կանխորոշված",
109
"cake_dark_theme": "Cake մութ տեսք",
110
"cake_pay_account_note": "Գրանցվեք միայն էլ. փոստի միջոցով, որպեսզի տեսնեք և գնեք քարտեր: Որոշ քարտեր հասանելի են նույնիսկ զեղչով:",
111
+ "cake_pay_card_email_delivered_message": "Ձեր նվեր քարտը կուղարկվի էլփոստի միջոցով անհրաժեշտ հաստատումներից հետո:",
112
"cake_pay_learn_more": "Վայրկյանապես գնեք և փոխանակեք նվեր քարտերը հավելվածում:\nՍահեցրեք ձախից աջ՝ ավելին իմանալու համար:",
113
"cake_pay_save_order": "Քարտը պետք է ուղարկված լինի ձեր էլ. փոստին 1 աշխատանքային օրվա ընթացքում \n Պահպանեք Ձեր պատվերի համարը՝",
114
"cake_pay_subtitle": "Գնեք համաշխարհային նախավճարային քարտեր և նվեր քարտեր",
res/values/strings_id.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Preset Kue 2FA",
109
"cake_dark_theme": "Tema Kue Gelap",
110
"cake_pay_account_note": "Daftar hanya dengan alamat email untuk melihat dan membeli kartu. Beberapa di antaranya bahkan tersedia dengan diskon!",
111
+ "cake_pay_card_email_delivered_message": "Kartu hadiah Anda akan dikirimkan melalui email setelah konfirmasi yang diperlukan.",
112
"cake_pay_learn_more": "Beli dan tukar kartu hadiah secara instan di aplikasi!\nGeser ke kanan untuk informasi lebih lanjut.",
113
"cake_pay_save_order": "Kartu harus dikirim ke email Anda dalam 1 hari kerja \n Simpan ID pesanan Anda:",
114
"cake_pay_subtitle": "Beli kartu prabayar di seluruh dunia dan kartu hadiah",
res/values/strings_it.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Preset Cake 2FA",
109
"cake_dark_theme": "Tema scuro Cake",
110
"cake_pay_account_note": "Iscriviti solamente con un indirizzo email per vedere e acquistare le carte. Alcune sono anche disponibili con uno sconto!",
111
+ "cake_pay_card_email_delivered_message": "La tua carta regalo verrà consegnata via e -mail dopo le conferme necessarie.",
112
"cake_pay_learn_more": "Acquista e riscatta istantaneamente carte regalo nell'app!\nScorri da sinistra a destra per saperne di più.",
113
"cake_pay_save_order": "La carta deve essere inviata alla tua e-mail entro 1 giorno lavorativo \n Salva l'ID del tuo ordine:",
114
"cake_pay_subtitle": "Acquista carte prepagate e carte regalo internazionali",
res/values/strings_ja.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "ケーキ 2FA プリセット",
109
"cake_dark_theme": "ケーキ暗いテーマ",
110
"cake_pay_account_note": "メールアドレスだけでサインアップして、カードを表示して購入できます。割引価格で利用できるカードもあります!",
111
+ "cake_pay_card_email_delivered_message": "ギフトカードは、必要な確認の後に電子メールで配信されます。",
112
"cake_pay_learn_more": "アプリですぐにギフトカードを購入して引き換えましょう!\n左から右にスワイプして詳細をご覧ください。",
113
"cake_pay_save_order": "カードは1営業日以内に電子メールに送信する必要があります\n注文IDを保存します。",
114
"cake_pay_subtitle": "世界中のプリペイドカードとギフトカードを購入します",
res/values/strings_ko.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Cake 2FA 사전 설정",
109
"cake_dark_theme": "Cake 다크 테마",
110
"cake_pay_account_note": "이메일 주소만으로 가입하여 카드를 확인하고 구매하세요. 일부는 할인된 가격으로도 이용 가능합니다!",
111
+ "cake_pay_card_email_delivered_message": "기프트 카드는 필요한 확인 후 이메일을 통해 전달됩니다.",
112
"cake_pay_learn_more": "앱에서 즉시 기프트 카드를 구매하고 사용하세요!\n자세히 알아보려면 왼쪽에서 오른쪽으로 스와이프하세요.",
113
"cake_pay_save_order": "카드는 영업일 기준 1일 이내에 이메일로 발송됩니다.\n주문 ID를 저장하세요:",
114
"cake_pay_subtitle": "전 세계 선불 카드 및 기프트 카드 구매",
res/values/strings_my.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "ကိတ်မုန့် 2FA ကြိုတင်သတ်မှတ်",
109
"cake_dark_theme": "ကိတ်မုန့် Dark Theme",
110
"cake_pay_account_note": "ကတ်များကြည့်ရှုဝယ်ယူရန် အီးမေးလ်လိပ်စာတစ်ခုဖြင့် စာရင်းသွင်းပါ။ အချို့ကို လျှော့ဈေးဖြင့်ပင် ရနိုင်သည်။",
111
+ "cake_pay_card_email_delivered_message": "လိုအပ်သောအတည်ပြုချက်များအပြီးတွင်သင်၏လက်ဆောင်ကဒ်ကိုအီးမေးလ်ဖြင့်ပေးပို့လိမ့်မည်။",
112
"cake_pay_learn_more": "အက်ပ်ရှိ လက်ဆောင်ကတ်များကို ချက်ချင်းဝယ်ယူပြီး ကူပွန်ဖြင့် လဲလှယ်ပါ။\nပိုမိုလေ့လာရန် ဘယ်မှညာသို့ ပွတ်ဆွဲပါ။",
113
"cake_pay_save_order": "ကဒ်ကိုသင်၏အီးမေးလ်သို့ပေးပို့သင့်သည်။ သင်၏အမှာစာ ID ကိုသိမ်းပါ။",
114
"cake_pay_subtitle": "Worldwide ကြိုတင်ငွေဖြည့်ကဒ်များနှင့်လက်ဆောင်ကဒ်များကို 0 ယ်ပါ",
res/values/strings_nl.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Taart 2FA Voorinstelling",
109
"cake_dark_theme": "Cake Dark Theme",
110
"cake_pay_account_note": "Meld u aan met alleen een e-mailadres om kaarten te bekijken en te kopen. Sommige zijn zelfs met korting verkrijgbaar!",
111
+ "cake_pay_card_email_delivered_message": "Uw cadeaubon wordt na de nodige bevestigingen via e -mail afgeleverd.",
112
"cake_pay_learn_more": "Koop en wissel cadeaubonnen direct in de app in!\nSwipe van links naar rechts voor meer informatie.",
113
"cake_pay_save_order": "De kaart moet binnen 1 werkdag naar uw e-mail worden verzonden.",
114
"cake_pay_subtitle": "Koop wereldwijde prepaid -kaarten en cadeaubonnen",
res/values/strings_pl.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Cake 2FA Preset",
109
"cake_dark_theme": "Cake Dark",
110
"cake_pay_account_note": "Zarejestruj się, używając tylko adresu e-mail, aby przeglądać i kupować karty. Niektóre są nawet dostępne ze zniżką!",
111
+ "cake_pay_card_email_delivered_message": "Twoja karta podarunkowa zostanie dostarczona pocztą elektroniczną po niezbędnych potwierdzeniach.",
112
"cake_pay_learn_more": "Kupuj i wykorzystuj karty podarunkowe od razu w aplikacji!\nPrzesuń od lewej do prawej, aby dowiedzieć się więcej.",
113
"cake_pay_save_order": "Karta powinna zostać wysłana na adres e-mail w ciągu 1 dnia roboczego \n Zapisz identyfikator zamówienia:",
114
"cake_pay_subtitle": "Kup na całym świecie karty przedpłacone i karty podarunkowe",
res/values/strings_pt.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Predefinição de bolo 2FA",
109
"cake_dark_theme": "Bolo tema escuro",
110
"cake_pay_account_note": "Inscreva-se com apenas um endereço de e-mail para ver e comprar cartões. Alguns estão até com desconto!",
111
+ "cake_pay_card_email_delivered_message": "Seu cartão -presente será entregue por e -mail após as confirmações necessárias.",
112
"cake_pay_learn_more": "Compre e resgate vales-presente instantaneamente no app!\nDeslize da esquerda para a direita para saber mais.",
113
"cake_pay_save_order": "O cartão deve ser enviado ao seu e-mail dentro de 1 dia útil \n Salvar seu ID do pedido:",
114
"cake_pay_subtitle": "Compre cartões pré -pagos em todo o mundo e cartões -presente",
res/values/strings_ru.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Торт 2FA Preset",
109
"cake_dark_theme": "Тейт темная тема",
110
"cake_pay_account_note": "Зарегистрируйтесь, указав только адрес электронной почты, чтобы просматривать и покупать карты. Некоторые даже доступны со скидкой!",
111
+ "cake_pay_card_email_delivered_message": "Ваша подарочная карта будет доставлена по электронной почте после необходимых подтверждений.",
112
"cake_pay_learn_more": "Мгновенно покупайте и используйте подарочные карты в приложении!\nПроведите по экрану слева направо, чтобы узнать больше.",
113
"cake_pay_save_order": "Карта должна быть отправлена на ваше электронное письмо в течение 1 рабочего дня \n Сохраните свой идентификатор заказа:",
114
"cake_pay_subtitle": "Купить карты с предоплатой и подарочными картами по всему миру",
res/values/strings_th.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "เค้ก 2FA ที่ตั้งไว้ล่วงหน้า",
109
"cake_dark_theme": "ธีมเค้กมืด",
110
"cake_pay_account_note": "ลงทะเบียนด้วยอีเมลเพียงอย่างเดียวเพื่อดูและซื้อบัตร บางบัตรอาจมีส่วนลด!",
111
+ "cake_pay_card_email_delivered_message": "บัตรของขวัญของคุณจะถูกส่งทางอีเมลหลังจากการยืนยันที่จำเป็น",
112
"cake_pay_learn_more": "ซื้อและเบิกบัตรของขวัญในแอพพลิเคชันทันที!\nกระแทกขวาไปซ้ายเพื่อเรียนรู้เพิ่มเติม",
113
"cake_pay_save_order": "บัตรควรส่งไปยังอีเมลของคุณภายใน 1 วันทำการ \n บันทึกรหัสคำสั่งซื้อของคุณ:",
114
"cake_pay_subtitle": "ซื้อบัตรเติมเงินและบัตรของขวัญทั่วโลก",
res/values/strings_tl.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Cake 2FA Preset",
109
"cake_dark_theme": "Cake Dark Theme",
110
"cake_pay_account_note": "Mag-sign up na may isang email address lamang upang makita at bumili ng mga kard. Ang ilan ay magagamit kahit sa isang diskwento!",
111
+ "cake_pay_card_email_delivered_message": "Ang iyong gift card ay maihatid sa pamamagitan ng email pagkatapos ng kinakailangang mga kumpirmasyon.",
112
"cake_pay_learn_more": "Agad na bumili at tubusin ang mga kard ng regalo sa app!\nMag-swipe pakaliwa sa kanan upang matuto nang higit pa.",
113
"cake_pay_save_order": "Ang card ay dapat ipadala sa iyong email sa loob ng 1 araw ng negosyo \n I-save ang iyong order ID:",
114
"cake_pay_subtitle": "Bumili ng mga pandaigdigang prepaid card at gift card",
res/values/strings_tr.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Kek 2FA Ön Ayarı",
109
"cake_dark_theme": "Kek Koyu Tema",
110
"cake_pay_account_note": "Kartları görmek ve satın almak için sadece bir e-posta adresiyle kaydolun. Hatta bazıları indirimli olarak bile mevcut!",
111
+ "cake_pay_card_email_delivered_message": "Hediye kartınız gerekli onaylardan sonra e -posta yoluyla teslim edilecektir.",
112
"cake_pay_learn_more": "Uygulamada anında hediye kartları satın alın ve harcayın!\nDaha fazla öğrenmek için soldan sağa kaydır.",
113
"cake_pay_save_order": "Kart, 1 İş Günü içinde e-postanıza gönderilmelidir \n Sipariş Kimliğinizi Kaydet:",
114
"cake_pay_subtitle": "Dünya çapında ön ödemeli kartlar ve hediye kartları satın alın",
res/values/strings_uk.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Торт 2FA Preset",
109
"cake_dark_theme": "Темна тема торта",
110
"cake_pay_account_note": "Зареєструйтеся, використовуючи лише адресу електронної пошти, щоб переглядати та купувати картки. Деякі навіть доступні зі знижкою!",
111
+ "cake_pay_card_email_delivered_message": "Ваша подарункова картка буде доставлена електронною поштою після необхідних підтверджень.",
112
"cake_pay_learn_more": "Миттєво купуйте та активуйте подарункові картки в додатку!\nПроведіть пальцем зліва направо, щоб дізнатися більше.",
113
"cake_pay_save_order": "Картка повинна бути надіслана на вашу електронну пошту протягом 1 робочого дня \n Зберегти ідентифікатор замовлення:",
114
"cake_pay_subtitle": "Купіть у всьому світі передплачені картки та подарункові картки",
res/values/strings_ur.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "کیک 2FA پیش سیٹ",
109
"cake_dark_theme": "کیک ڈارک تھیم",
110
"cake_pay_account_note": "کارڈز دیکھنے اور خریدنے کے لیے صرف ایک ای میل ایڈریس کے ساتھ سائن اپ کریں۔ کچھ رعایت پر بھی دستیاب ہیں!",
111
+ "cake_pay_card_email_delivered_message": "آپ کا گفٹ کارڈ ضروری تصدیقوں کے بعد ای میل کے ذریعے فراہم کیا جائے گا۔",
112
"cake_pay_learn_more": "ایپ میں فوری طور پر گفٹ کارڈز خریدیں اور بھنائیں!\\nمزید جاننے کے لیے بائیں سے دائیں سوائپ کریں۔",
113
"cake_pay_save_order": "کارڈ 1 کاروباری دن کے اندر آپ کے ای میل پر بھیجا جانا چاہئے \n اپنے آرڈر کی شناخت کو بچائیں:",
114
"cake_pay_subtitle": "دنیا بھر میں پری پیڈ کارڈز اور گفٹ کارڈ خریدیں",
res/values/strings_vi.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Thiết lập sẵn Cake 2FA",
109
"cake_dark_theme": "Chủ đề Cake tối",
110
"cake_pay_account_note": "Đăng ký chỉ với một địa chỉ email để xem và mua thẻ. Một số thẻ còn được giảm giá!",
111
+ "cake_pay_card_email_delivered_message": "Thẻ quà tặng của bạn sẽ được gửi qua email sau khi xác nhận cần thiết.",
112
"cake_pay_learn_more": "Mua và đổi thẻ quà tặng ngay trong ứng dụng!\nVuốt từ trái sang phải để tìm hiểu thêm.",
113
"cake_pay_save_order": "Thẻ sẽ được gửi đến email của bạn trong vòng 1 ngày làm việc \n Lưu mã Đơn hàng của bạn:",
114
"cake_pay_subtitle": "Mua thẻ trả trước toàn cầu và thẻ quà tặng",
res/values/strings_yo.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "Cake 2FA Tito",
109
"cake_dark_theme": "Akara oyinbo dudu koko",
110
"cake_pay_account_note": "Ẹ fi àdírẹ́sì ímeèlì nìkan forúkọ sílẹ̀ k'ẹ́ rí àti ra àwọn káàdì. Ẹ lè fi owó tó kéré jù ra àwọn káàdì kan!",
111
+ "cake_pay_card_email_delivered_message": "Kaadi Ẹbun Rẹ yoo fi jiṣẹ nipasẹ imeeli lẹhin awọn ijẹrisi pataki.",
112
"cake_pay_learn_more": "Láìpẹ́ ra àti lo àwọn káàdí ìrajà t'á lò nínú irú kan ìtajà nínú áàpù!\nẸ tẹ̀ òsì de ọ̀tún láti kọ́ jù.",
113
"cake_pay_save_order": "Kaadi yẹ ki o firanṣẹ si imeeli rẹ laarin ọjọ iṣowo 1 \n Fipamọ aṣẹ rẹ:",
114
"cake_pay_subtitle": "Ra awọn kaadi ti a san ni agbaye ati awọn kaadi ẹbun",
res/values/strings_zh.arb
+1
@@ -108,6 +108,7 @@
108
"cake_2fa_preset": "蛋糕 2FA 预设",
109
"cake_dark_theme": "蛋糕黑暗主题",
110
"cake_pay_account_note": "只需使用電子郵件地址註冊即可查看和購買卡片。有些甚至可以打折!",
111
+ "cake_pay_card_email_delivered_message": "您的礼品卡将在必要的确认后通过电子邮件发送。",
112
"cake_pay_learn_more": "立即在应用中购买和兑换礼品卡!\n从左向右滑动以了解详情。",
113
"cake_pay_save_order": "该卡应在1个工作日内发送到您的电子邮件\n保存您的订单ID:",
114
"cake_pay_subtitle": "购买全球预付费卡和礼品卡",