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": "购买全球预付费卡和礼品卡",