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
This commit is contained in:
Serhii
2025-07-11 23:56:35 +03:00
committed by GitHub
parent 203aff83f2
commit ef84db5874
96 changed files with 3907 additions and 2638 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

+5
View File
@@ -0,0 +1,5 @@
export 'src/auth/cake_pay_welcome_page.dart';
export 'src/auth/cake_pay_verify_otp_page.dart';
export 'src/auth/cake_pay_account_page.dart';
export 'src/cards/cake_pay_cards_page.dart';
export 'src/cards/cake_pay_buy_card_page.dart';
@@ -1,15 +0,0 @@
class PaymentCredential {
final double amount;
final int quantity;
final double totalAmount;
final String? userName;
final String fiatCurrency;
PaymentCredential({
required this.amount,
required this.quantity,
required this.totalAmount,
required this.userName,
required this.fiatCurrency,
});
}
@@ -1,7 +1,7 @@
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/cake_pay_tile.dart';
import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_tile.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart';
@@ -1,5 +1,6 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
@@ -146,7 +147,9 @@ class CakePayVerifyOtpPage extends BasePage {
});
}
void _onOtpSuccessful(BuildContext context) => Navigator.pop(context);
void _onOtpSuccessful(BuildContext context) =>
Navigator.pop(context, true);
void _verify() async => await _authViewModel.verifyEmail(_codeController.text);
}
@@ -1,6 +1,6 @@
import 'package:cake_wallet/core/email_validator.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
@@ -113,12 +113,14 @@ class CakePayWelcomePage extends BasePage {
});
}
void _onLoginSuccessful(BuildContext context, CakePayAuthViewModel authViewModel) =>
Navigator.pushReplacementNamed(
context,
Routes.cakePayVerifyOtpPage,
arguments: [authViewModel.email, true],
);
Future<void> _onLoginSuccessful(BuildContext context, CakePayAuthViewModel authViewModel) async {
final verified = await Navigator.pushNamed<bool>(context, Routes.cakePayVerifyOtpPage,
arguments: [authViewModel.email, true]);
if (verified == true) {
Navigator.pop(context, true);
}
}
void _login() async {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
@@ -1,5 +1,3 @@
import 'cake_pay_card.dart';
abstract class CakePayUserVerificationState {}
class CakePayUserVerificationStateInitial extends CakePayUserVerificationState {}
@@ -44,20 +42,23 @@ class CakePayCreateCardStateFailure extends CakePayCreateCardState {
final String error;
}
class CakePayCardsState {}
class UserCakePayCardsState {}
class CakePayCardsStateNoCards extends CakePayCardsState {}
class UserCakePayCardsStateInitial extends UserCakePayCardsState {}
class CakePayCardsStateFetching extends CakePayCardsState {}
class UserCakePayCardsStateNoCards extends UserCakePayCardsState {}
class CakePayCardsStateFailure extends CakePayCardsState {}
class UserCakePayCardsStateFetching extends UserCakePayCardsState {}
class CakePayCardsStateSuccess extends CakePayCardsState {
CakePayCardsStateSuccess({required this.card});
class UserCakePayCardsStateFailure extends UserCakePayCardsState {
UserCakePayCardsStateFailure({required this.error});
final CakePayCard card;
final String error;
}
class UserCakePayCardsStateSuccess extends UserCakePayCardsState {}
abstract class CakePayVendorState {}
class InitialCakePayVendorLoadingState extends CakePayVendorState {}
@@ -0,0 +1,773 @@
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
import 'package:cake_wallet/cake_pay/src/widgets/denominations_amount_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/enter_amount_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/image_placeholder.dart';
import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart';
import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart';
class CakePayBuyCardPage extends BasePage {
CakePayBuyCardPage(
this.cakePayBuyCardViewModel,
this.cakePayService,
) : _amountFieldFocus = FocusNode(),
_amountController = TextEditingController(),
_quantityFieldFocus = FocusNode(),
_quantityController =
TextEditingController(text: cakePayBuyCardViewModel.quantity.toString()) {
_amountController.addListener(() {
cakePayBuyCardViewModel.onAmountChanged(_amountController.text);
});
}
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
final CakePayService cakePayService;
bool _effectsInstalled = false;
late final BuildContext _overlayCtx;
@override
String get title => cakePayBuyCardViewModel.card.name;
@override
bool get extendBodyBehindAppBar => true;
@override
bool get gradientAll => true;
@override
AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent;
@override
Widget? trailing(BuildContext context) {
return const SizedBox(
width: 54,
height: 0,
);
}
@override
Widget? middle(BuildContext context) {
return Text(
title,
textAlign: TextAlign.center,
maxLines: 2,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
fontFamily: 'Lato',
color: titleColor(context)),
);
}
final TextEditingController _amountController;
final FocusNode _amountFieldFocus;
final TextEditingController _quantityController;
final FocusNode _quantityFieldFocus;
@override
Widget body(BuildContext context) {
_setEffects(context);
final card = cakePayBuyCardViewModel.card;
final vendor = cakePayBuyCardViewModel.vendor;
return KeyboardActions(
disableScroll: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).primaryColor,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _amountFieldFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
]),
child: Container(
color: Colors.transparent,
child: Column(
children: [
RoundedOverlayCards(
topCardChild: Column(
children: [
Expanded(flex: 4, child: const SizedBox()),
Expanded(
flex: 7,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(150),
blurRadius: 8,
offset: const Offset(0, 2))
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
card.cardImageUrl ?? '',
fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) =>
CakePayCardImagePlaceholder(),
),
),
),
),
Expanded(child: const SizedBox()),
],
),
bottomCardChild: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: card.denominations.isNotEmpty
? DenominationsAmountWidget(
fiatCurrency: card.fiatCurrency.title,
denominations: card.denominations,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
quantityFieldFocus: _quantityFieldFocus,
quantityController: _quantityController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
cakePayBuyCardViewModel: cakePayBuyCardViewModel)
: EnterAmountWidget(
minValue: card.minValue ?? '-',
maxValue: card.maxValue ?? '-',
fiatCurrency: card.fiatCurrency.title,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged))),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (vendor.cakeWarnings != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Center(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white.withAlpha(50)),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
vendor.cakeWarnings!,
textAlign: TextAlign.center,
style: textSmallSemiBold(color: Colors.white),
),
),
),
),
),
Expanded(
child: SingleChildScrollView(
primary: false,
padding: EdgeInsets.zero,
child: ClickableLinksText(
text: card.description ?? '',
textStyle: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color!,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
),
),
],
),
),
),
Expanded(
child: SingleChildScrollView(
primary: false,
child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24),
child: Column(
children: [
if (card.expiryAndValidity != null && card.expiryAndValidity!.isNotEmpty)
Row(
children: [
Text(S.of(context).expiry_and_validity + ':',
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color!,
fontSize: 16,
fontWeight: FontWeight.w900)),
Expanded(
child: Text(card.expiryAndValidity!,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.labelMedium)),
],
),
SizedBox(height: 8),
TextIconButton(
label: S.of(context).how_to_use_card,
onTap: () => _showHowToUseCard(context, card)),
SizedBox(height: 8),
TextIconButton(
label: S.of(context).settings_terms_and_conditions,
onTap: () => _showTermsAndCondition(context, card.termsAndConditions)),
],
),
),
),
),
SizedBox(height: 8),
Observer(builder: (_) {
Widget _buildPaymentMethodWidget(
List<CakePayPaymentMethod> methods, CakePayPaymentMethod selected) {
return Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 24, right: 8),
child: Text(
'Payment Method',
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color!,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
),
Expanded(child: const SizedBox()),
if (methods.length > 1)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
child: ToggleButtons(
isSelected: methods.map((m) => m == selected).toList(),
borderRadius: BorderRadius.circular(8),
onPressed: (index) =>
cakePayBuyCardViewModel.chooseMethod(methods[index]),
children: methods
.map((m) => Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Text(m.label),
))
.toList(),
),
),
],
);
}
final methods = cakePayBuyCardViewModel.availableMethods;
final selected = cakePayBuyCardViewModel.selectedPaymentMethod ??
(methods.isNotEmpty ? methods.first : null);
return Column(
children: [
methods.length <= 1 || selected == null
? const SizedBox.shrink()
: _buildPaymentMethodWidget(methods, selected),
if (FeatureFlag.hasDevOptions)
Padding(
padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
child: LoadingPrimaryButton(
onPressed: () {
//Request dummy node to get the focus out of the text fields
FocusScope.of(context).requestFocus(FocusNode());
cakePayBuyCardViewModel.isSimulatingFlow = true;
isIOSUnavailable(card)
? alertIOSAvailability(context, card)
: confirmPurchaseFirst(context);
},
text: '(Dev) Simulate Purchasing Gift Card',
isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
cakePayBuyCardViewModel.isPurchasing,
isLoading:
cakePayBuyCardViewModel.sendViewModel.state is IsExecutingState ||
cakePayBuyCardViewModel.isPurchasing,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
Padding(
padding: EdgeInsets.only(top: 10, bottom: 34, right: 20, left: 20),
child: LoadingPrimaryButton(
onPressed: () {
//Request dummy node to get the focus out of the text fields
FocusScope.of(context).requestFocus(FocusNode());
isIOSUnavailable(card)
? alertIOSAvailability(context, card)
: confirmPurchaseFirst(context);
},
text: S.of(context).purchase_gift_card,
isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
cakePayBuyCardViewModel.isPurchasing,
isLoading: cakePayBuyCardViewModel.sendViewModel.state is IsExecutingState ||
cakePayBuyCardViewModel.isPurchasing,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
],
);
}),
],
),
),
);
}
bool isWordInCardsName(CakePayCard card, String word) {
return card.name.toLowerCase().contains(word.toLowerCase());
}
bool isIOSUnavailable(CakePayCard card) {
if (!Platform.isIOS && !Platform.isMacOS) {
return false;
}
final isDigitalGameStores = isWordInCardsName(card, 'playstation') ||
isWordInCardsName(card, 'xbox') ||
isWordInCardsName(card, 'steam') ||
isWordInCardsName(card, 'meta quest') ||
isWordInCardsName(card, 'kigso') ||
isWordInCardsName(card, 'game world') ||
isWordInCardsName(card, 'google') ||
isWordInCardsName(card, 'nintendo');
final isGCodes = isWordInCardsName(card, 'gcodes');
final isApple = isWordInCardsName(card, 'itunes') || isWordInCardsName(card, 'apple');
final isTidal = isWordInCardsName(card, 'tidal');
final isVPNServices = isWordInCardsName(card, 'nordvpn') ||
isWordInCardsName(card, 'expressvpn') ||
isWordInCardsName(card, 'surfshark') ||
isWordInCardsName(card, 'proton');
final isStreamingServices = isWordInCardsName(card, 'netflix') ||
isWordInCardsName(card, 'spotify') ||
isWordInCardsName(card, 'hulu') ||
isWordInCardsName(card, 'hbo') ||
isWordInCardsName(card, 'soundcloud') ||
isWordInCardsName(card, 'twitch');
final isDatingServices = isWordInCardsName(card, 'tinder');
return isDigitalGameStores ||
isGCodes ||
isApple ||
isTidal ||
isVPNServices ||
isStreamingServices ||
isDatingServices;
}
Future<void> alertIOSAvailability(BuildContext context, CakePayCard card) async {
cakePayBuyCardViewModel.isSimulatingFlow = false;
return await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).cakepay_ios_not_available,
buttonText: S.of(context).ok,
buttonAction: () {
// _walletHardwareRestoreVM.error = null;
Navigator.of(context).pop();
});
});
}
Future<void> confirmPurchaseFirst(BuildContext context) async {
bool isLogged = await cakePayBuyCardViewModel.cakePayService.isLogged();
if (!isLogged) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
} else {
cakePayBuyCardViewModel.isPurchasing = true;
await _showconfirmPurchaseFirstAlert(context);
}
}
Future<void> _showconfirmPurchaseFirstAlert(BuildContext context) async {
if (!cakePayBuyCardViewModel.confirmsNoVpn ||
!cakePayBuyCardViewModel.confirmsVoidedRefund ||
!cakePayBuyCardViewModel.confirmsTermsAgreed) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) => ThreeCheckboxAlert(
alertTitle: S.of(context).cakepay_confirm_purchase,
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).confirm,
actionLeftButton: () {
cakePayBuyCardViewModel.isPurchasing = false;
cakePayBuyCardViewModel.isSimulatingFlow = false;
Navigator.of(context).pop();
},
actionRightButton: (confirmsNoVpn, confirmsVoidedRefund, confirmsTermsAgreed) {
cakePayBuyCardViewModel.confirmsNoVpn = confirmsNoVpn;
cakePayBuyCardViewModel.confirmsVoidedRefund = confirmsVoidedRefund;
cakePayBuyCardViewModel.confirmsTermsAgreed = confirmsTermsAgreed;
Navigator.of(context).pop();
},
),
);
}
if (cakePayBuyCardViewModel.confirmsNoVpn &&
cakePayBuyCardViewModel.confirmsVoidedRefund &&
cakePayBuyCardViewModel.confirmsTermsAgreed) {
await purchaseCard(context);
} else {
cakePayBuyCardViewModel.isPurchasing = false;
cakePayBuyCardViewModel.isSimulatingFlow = false;
}
}
Future<void> purchaseCard(BuildContext context) async {
bool isLogged = await cakePayBuyCardViewModel.cakePayService.isLogged();
if (!isLogged) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
} else {
try {
await cakePayBuyCardViewModel.createOrder();
} catch (_) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
await cakePayBuyCardViewModel.cakePayService.logout();
}
}
cakePayBuyCardViewModel.isPurchasing = false;
}
BuildContext? dialogContext;
BuildContext? loadingBottomSheetContext;
BuildContext? confirmBottomSheetContext;
void _setEffects(BuildContext context) {
if (_effectsInstalled) {
return;
}
_overlayCtx = Navigator.of(context).context;
if (cakePayBuyCardViewModel.sendViewModel.isElectrumWallet) {
bitcoin!.updateFeeRates(cakePayBuyCardViewModel.sendViewModel.wallet);
}
reaction((_) => cakePayBuyCardViewModel.sendViewModel.state, (ExecutionState state) async {
if (dialogContext != null && dialogContext!.mounted) Navigator.of(dialogContext!).pop();
if (confirmBottomSheetContext != null && confirmBottomSheetContext!.mounted) {
Navigator.of(confirmBottomSheetContext!).pop();
}
if (state is! IsExecutingState &&
loadingBottomSheetContext != null &&
loadingBottomSheetContext!.mounted) {
Navigator.of(loadingBottomSheetContext!).pop();
}
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted)
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
key: ValueKey('cake_pay_buy_page_send_failure_dialog_key'),
buttonKey: ValueKey('cake_pay_buy_page_send_failure_dialog_button_key'),
alertTitle: S.of(context).error,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
});
}
if (state is IsExecutingState) {
// wait a bit to avoid showing the loading dialog if transaction is failed
await Future.delayed(const Duration(milliseconds: 300));
final currentState = cakePayBuyCardViewModel.sendViewModel.state;
if (currentState is ExecutedSuccessfullyState || currentState is FailureState) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (BuildContext context) {
loadingBottomSheetContext = context;
return LoadingBottomSheet(
titleText: S.of(context).generating_transaction,
);
},
);
}
});
}
if (state is ExecutedSuccessfullyState) {
if (cakePayBuyCardViewModel.order == null) return;
ReactionDisposer? disposer;
disposer = reaction((_) => cakePayBuyCardViewModel.isOrderExpired, (bool isExpired) {
if (isExpired) {
cakePayBuyCardViewModel.sendViewModel.state = FailureState('Order expired');
disposer?.call();
}
});
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (context.mounted) {
final order = cakePayBuyCardViewModel.order;
final displayingOutputs = cakePayBuyCardViewModel.sendViewModel.outputs
.map((o) => o.OutputCopyWithParsedAddress(
parsedAddress: ParsedAddress(
addresses: [o.address],
name: 'Cake Pay',
profileName: order?.cards.first.cardName ?? 'Cake Pay',
profileImageUrl: order?.cards.first.cardImagePath ?? '',
),
fiatAmount: '${order?.amountUsd.toString()} USD',
))
.toList();
final result = await showModalBottomSheet<bool>(
context: context,
isDismissible: false,
isScrollControlled: true,
builder: (BuildContext bottomSheetContext) {
confirmBottomSheetContext = bottomSheetContext;
return ConfirmSendingBottomSheet(
key: ValueKey('cake_pay_buy_page_confirm_sending_dialog_key'),
titleText: S.of(bottomSheetContext).confirm_transaction,
currentTheme: currentTheme,
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
paymentId: S.of(bottomSheetContext).payment_id,
paymentIdValue: cakePayBuyCardViewModel.order?.orderId,
expirationTime: cakePayBuyCardViewModel.formattedRemainingTime,
walletType: cakePayBuyCardViewModel.sendViewModel.walletType,
titleIconPath:
cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
currency: cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency,
amount: S.of(bottomSheetContext).send_amount,
amountValue:
cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.amountFormatted,
quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
fiatAmountValue:
cakePayBuyCardViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(bottomSheetContext).send_fee,
feeValue: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: cakePayBuyCardViewModel
.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: displayingOutputs,
footerType: FooterType.slideActionButton,
slideActionButtonText:
cakePayBuyCardViewModel.isSimulating ? 'Swipe to simulate' : 'Swipe to send',
accessibleNavigationModeSlideActionButtonText:
cakePayBuyCardViewModel.isSimulating ? 'Simulate' : S.of(context).send,
onSlideActionComplete: () async {
Navigator.of(bottomSheetContext).pop(true);
cakePayBuyCardViewModel.isSimulating
? cakePayBuyCardViewModel.simulatePayment()
: cakePayBuyCardViewModel.sendViewModel.commitTransaction(context);
},
change: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.change,
isOpenCryptoPay: cakePayBuyCardViewModel.sendViewModel.ocpRequest != null,
);
},
);
confirmBottomSheetContext = null;
cakePayBuyCardViewModel.isSimulatingFlow = false;
_handleDispose(disposer);
if (result == null) cakePayBuyCardViewModel.sendViewModel.dismissTransaction();
}
});
}
if (state is TransactionCommitted) {
final order = cakePayBuyCardViewModel.order;
final outputsCopy = List<Output>.from(cakePayBuyCardViewModel.sendViewModel.outputs);
final displayingOutputs = outputsCopy
.map((o) => o.OutputCopyWithParsedAddress(
parsedAddress: ParsedAddress(
addresses: [o.address],
name: 'Cake Pay',
profileName: order?.cards.first.cardName ?? 'Cake Pay',
profileImageUrl: order?.cards.first.cardImagePath ?? '',
),
fiatAmount: '${order?.amountUsd ?? 0} USD',
))
.toList();
cakePayBuyCardViewModel.sendViewModel.clearOutputs();
final bool usePageContextLater = context.mounted;
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (displayingOutputs.isEmpty) {
if (context.mounted) Navigator.of(context).pop();
return;
}
final BuildContext sheetParentCtx = usePageContextLater ? context : _overlayCtx;
await showModalBottomSheet<void>(
context: sheetParentCtx,
useRootNavigator: !usePageContextLater,
isScrollControlled: true,
isDismissible: true,
backgroundColor: Colors.transparent,
builder: (sheetCtx) {
return CakePayTransactionSentBottomSheet(
key: const ValueKey('cake_pay_buy_page_transaction_sent_bottom_sheet_key'),
titleText: S.of(sheetCtx).transaction_sent,
titleIconWidget: const CircleAvatar(
radius: 10,
backgroundColor: Colors.green,
child: Icon(Icons.check, size: 16, color: Colors.white),
),
output: displayingOutputs.first,
currency: cakePayBuyCardViewModel.sendViewModel.selectedCryptoCurrency,
amount: S.of(sheetCtx).send_amount,
amountValue:
cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.amountFormatted,
quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
fiatAmountValue:
cakePayBuyCardViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(sheetCtx).send_fee,
feeValue: cakePayBuyCardViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount:
cakePayBuyCardViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
paymentId: 'Order ID',
paymentIdValue: order?.orderId ?? '',
onClose: () {
Navigator.of(sheetCtx).pop();
},
);
},
);
if (context.mounted) {
Navigator.of(context).pop();
}
});
}
if (state is IsAwaitingDeviceResponseState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!context.mounted) return;
showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
currentTheme: currentTheme,
footerType: FooterType.singleActionButton,
titleText: S.of(bottomSheetContext).proceed_on_device,
contentImage: 'assets/images/hardware_wallet/ledger_nano_x.png',
contentImageColor: Theme.of(context).textTheme.titleLarge!.color!,
content: S.of(bottomSheetContext).proceed_on_device_description,
singleActionButtonText: S.of(context).cancel,
onSingleActionButtonPressed: () {
cakePayBuyCardViewModel.sendViewModel.state = InitialExecutionState();
Navigator.of(bottomSheetContext).pop();
},
),
);
});
}
});
_effectsInstalled = true;
}
void _handleDispose(ReactionDisposer? disposer) {
cakePayBuyCardViewModel.dispose();
if (disposer != null) {
disposer();
}
}
}
void _showHowToUseCard(BuildContext context, CakePayCard card) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return CakePayAlertModal(
title: S.of(context).how_to_use_card,
dismissible: true,
content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Padding(
padding: EdgeInsets.all(10),
child: Text(card.name, style: Theme.of(context).textTheme.headlineSmall)),
ClickableLinksText(
text: card.howToUse ?? '',
textStyle: Theme.of(context).textTheme.bodyMedium!,
linkStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic))
]),
actionTitle: S.current.got_it,
showCloseButton: false,
);
});
}
void _showTermsAndCondition(BuildContext context, String? termsAndConditions) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return CakePayAlertModal(
title: S.of(context).settings_terms_and_conditions,
dismissible: true,
content: Align(
alignment: Alignment.bottomLeft,
child: ClickableLinksText(
text: termsAndConditions ?? '',
textStyle: Theme.of(context).textTheme.bodyMedium!,
linkStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic))),
actionTitle: S.of(context).agree,
showCloseButton: false,
);
});
}
@@ -0,0 +1,521 @@
import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_search_bar_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/user_card_item.dart';
import 'package:cake_wallet/entities/country.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/cake_pay/src/widgets/card_item.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/filter_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/cake_pay_card_info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/cake_scrollbar.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/src/widgets/tab_view_wrapper_widget.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/debounce.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class CakePayCardsPage extends BasePage {
CakePayCardsPage(this._cardsListViewModel);
final CakePayCardsListViewModel _cardsListViewModel;
@override
bool get gradientBackground => true;
@override
Widget Function(BuildContext, Widget) get rootWrapper =>
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
@override
bool get resizeToAvoidBottomInset => false;
@override
Widget middle(BuildContext context) {
return Text(
'Cake Pay',
style: textMediumSemiBold(
color: titleColor(context),
),
);
}
@override
Widget trailing(BuildContext context) {
return Observer(builder: (_) {
final loggedIn = _cardsListViewModel.isUserAuthenticated == true;
Future<void> _handleOnPressed() async {
if (loggedIn) {
Navigator.pushNamed(context, Routes.cakePayAccountPage);
return;
}
final success = await Navigator.pushNamed<bool>(context, Routes.cakePayWelcomePage);
if (success == true) await _cardsListViewModel.checkAuth();
}
if (!loggedIn || _cardsListViewModel.username == null) {
return _TrailingIcon(
asset: 'assets/images/profile.png',
iconColor: pageIconColor(context) ?? Colors.white,
onPressed: () async => await _handleOnPressed(),
);
}
final letter = _cardsListViewModel.username!.trim()[0].toUpperCase();
return IconButton(
padding: EdgeInsets.zero,
iconSize: 25,
onPressed: _handleOnPressed,
icon: CircleAvatar(
radius: 12,
backgroundColor: Theme.of(context).colorScheme.primary,
child: Text(
letter,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.w600,
),
),
),
);
});
}
@override
Widget body(BuildContext context) {
return CakePayCardsPageBody(
cardsListViewModel: _cardsListViewModel,
currentTheme: currentTheme,
titleColor: titleColor);
}
}
class CakePayCardsPageBody extends StatefulWidget {
const CakePayCardsPageBody({
super.key,
required CakePayCardsListViewModel cardsListViewModel,
required this.currentTheme,
required this.titleColor,
}) : _cardsListViewModel = cardsListViewModel;
final CakePayCardsListViewModel _cardsListViewModel;
final MaterialThemeBase currentTheme;
final Color? Function(BuildContext) titleColor;
@override
State<CakePayCardsPageBody> createState() => _CakePayCardsPageBodyState();
}
class _CakePayCardsPageBodyState extends State<CakePayCardsPageBody> {
ReactionDisposer? _countryPickerDisposer;
@override
void initState() {
super.initState();
final viewModel = widget._cardsListViewModel;
_countryPickerDisposer = when(
(_) => viewModel.shouldShowCountryPicker,
() async {
viewModel.storeInitialFilterStates();
WidgetsBinding.instance.addPostFrameCallback(
(_) async {
await showCountryPicker(context, viewModel);
if (viewModel.hasFiltersChanged) {
viewModel.resetLoadingNextPageState();
viewModel.getVendors();
}
viewModel.settingsStore.selectedCakePayCountry = viewModel.selectedCountry;
},
);
},
);
}
@override
void dispose() {
_countryPickerDisposer?.call();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Observer(builder: (_) {
final isUserAuthenticated = widget._cardsListViewModel.isUserAuthenticated;
if (isUserAuthenticated == null) return const _Loading();
if (isUserAuthenticated == false || !FeatureFlag.isCakePayRedemptionFlowEnabled) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: _ShopTab(cardsListViewModel: widget._cardsListViewModel),
);
}
final titleColor = widget.titleColor(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Column(children: [
Expanded(
child: TabViewWrapper(
labelStyle: TextStyle(
color: titleColor,
fontFamily: 'Lato',
fontSize: 20,
fontWeight: FontWeight.w600),
unselectedLabelStyle: TextStyle(
color: titleColor?.withAlpha(150) ?? Colors.white70,
fontFamily: 'Lato',
fontSize: 20,
fontWeight: FontWeight.w400),
indicatorColor: titleColor,
tabs: const [
Tab(text: 'My Cards'),
Tab(text: 'Shop')
],
views: [
_MyCardsTab(
cardsListViewModel: widget._cardsListViewModel,
currentTheme: widget.currentTheme),
_ShopTab(cardsListViewModel: widget._cardsListViewModel)
]),
)
]));
});
}
}
Future<void> showFilterWidget(
BuildContext context, CakePayCardsListViewModel cardsListViewModel) async {
return showPopUp<void>(
context: context,
builder: (BuildContext context) {
return FilterWidget(filterItems: cardsListViewModel.createFilterItems);
},
);
}
Future<void> showCountryPicker(
BuildContext context, CakePayCardsListViewModel cardsListViewModel) async {
await showPopUp<void>(
context: context,
builder: (_) => Picker(
title: S.of(context).select_your_country,
items: cardsListViewModel.availableCountries,
images: cardsListViewModel.availableCountries
.map((e) => Image.asset(
e.iconPath,
errorBuilder: (context, error, stackTrace) => Container(
width: 58,
height: 58,
),
))
.toList(),
selectedAtIndex:
cardsListViewModel.availableCountries.indexOf(cardsListViewModel.selectedCountry),
onItemSelected: (Country country) => cardsListViewModel.setSelectedCountry(country),
isSeparated: false,
hintText: S.of(context).search,
matchingCriteria: (Country country, String searchText) =>
country.fullName.toLowerCase().contains(searchText.toLowerCase())));
}
class _TrailingIcon extends StatelessWidget {
const _TrailingIcon({required this.asset, this.onPressed, required this.iconColor});
final String asset;
final VoidCallback? onPressed;
final Color iconColor;
@override
Widget build(BuildContext context) {
return Semantics(
label: S.of(context).profile,
child: Material(
color: Colors.transparent,
child: IconButton(
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
highlightColor: Colors.transparent,
onPressed: onPressed,
icon: ImageIcon(AssetImage(asset), size: 25, color: iconColor),
),
));
}
}
class _MyCardsTab extends StatefulWidget {
const _MyCardsTab({required this.cardsListViewModel, required this.currentTheme});
final CakePayCardsListViewModel cardsListViewModel;
final MaterialThemeBase currentTheme;
@override
State<_MyCardsTab> createState() => _MyCardsTabState();
}
class _MyCardsTabState extends State<_MyCardsTab> {
late final TextEditingController _searchController;
@override
void initState() {
super.initState();
_searchController = TextEditingController(
text: widget.cardsListViewModel.searchMyCardsString,
);
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
final viewModel = widget.cardsListViewModel;
return Observer(builder: (_) {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(2, 6, 0, 6),
child: CakePaySearchBar(
initialQuery: viewModel.searchMyCardsString,
controller: _searchController,
onSearch: viewModel.setMyCardsQuery,
onFilter: () async {}, // TODO: implement filter
)),
Expanded(
child: Observer(builder: (_) {
final cards = viewModel.filteredUserCards;
if (viewModel.userCardState is UserCakePayCardsStateFetching) return const _Loading();
if (viewModel.userCardState is UserCakePayCardsStateNoCards)
return Expanded(child: Center(child: Text(S.of(context).no_cards_found)));
final showThumb = cards.length > 6;
final userCardsList = Stack(
children: [
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 1.25,
crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 3 : 2,
crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5),
padding: EdgeInsets.only(left: 2, right: showThumb ? 10 : 22),
itemCount: cards.length,
itemBuilder: (_, i) {
final card = cards[i];
return UserCardItem(
logoUrl: card.cardImageUrl,
title: card.name,
subTitle: '\$100',
onTap: () => _showCardInfoBottomSheet(context, card, widget.currentTheme),
);
},
),
],
);
return showThumb ? Scrollbar(
key: ValueKey('cake_pay_my_cards_tab_scrollbar_key'),
thumbVisibility: true,
trackVisibility: true,
child: userCardsList,
) : userCardsList;
}),
),
],
);
});
}
}
Future<void> _showCardInfoBottomSheet(
BuildContext context, CakePayCard card, MaterialThemeBase currentTheme) async {
bool isReloadable = false; // TODO: replace with real logic
if (card.name.toLowerCase().contains('prepaid')) {
isReloadable = true;
}
await showModalBottomSheet<void>(
context: context,
isDismissible: false,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext bottomSheetContext) {
return isReloadable
? CakePayCardInfoBottomSheet(
isReloadable: isReloadable,
titleText: 'Reloadable Card',
balance: '100 USD',
howToUse: card.howToUse,
currentTheme: currentTheme,
footerType: FooterType.doubleActionButton,
applyBoxShadow: true,
contentImage: card.cardImageUrl,
leftActionButtonKey: const Key('cake_pay_cards_page_reload_card_left_button_key'),
doubleActionLeftButtonText: 'Archive',
onLeftActionButtonPressed: () {},
rightActionButtonKey: const Key('cake_pay_cards_page_reload_card_right_button_key'),
doubleActionRightButtonText: 'Top Up',
onRightActionButtonPressed: () {},
onUpdateBalancePressed: () {})
: CakePayCardInfoBottomSheet(
isReloadable: isReloadable,
titleText: card.name,
balance: '500 USD',
howToUse: card.howToUse,
currentTheme: currentTheme,
footerType: FooterType.singleActionButton,
applyBoxShadow: true,
contentImage: card.cardImageUrl,
singleActionButtonKey: const Key('cake_pay_cards_page_card_info_bottom_sheet_key'),
singleActionButtonText: 'Mark As Used',
onSingleActionButtonPressed: () {},
onUpdateBalancePressed: () {});
},
);
}
class _ShopTab extends StatefulWidget {
const _ShopTab({required this.cardsListViewModel});
final CakePayCardsListViewModel cardsListViewModel;
@override
State<_ShopTab> createState() => _ShopTabState();
}
class _ShopTabState extends State<_ShopTab> {
late final ScrollController _scroll;
@override
void initState() {
super.initState();
_scroll = ScrollController()
..addListener(() {
if (!_scroll.hasClients) return;
final max = _scroll.position.maxScrollExtent;
final threshold = 200.0;
if (_scroll.offset >= max - threshold && !_scroll.position.outOfRange) {
widget.cardsListViewModel.fetchNextPage();
}
});
}
@override
void dispose() {
_scroll.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final viewModel = widget.cardsListViewModel;
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(2, 6, 0, 6),
child: CakePaySearchBar(
initialQuery: viewModel.searchString,
onSearch: (String searchText) {
if (searchText != viewModel.searchString) {
viewModel.searchString = searchText;
viewModel.resetLoadingNextPageState();
viewModel.getVendors(text: searchText);
}
},
onFilter: () async {
viewModel.storeInitialFilterStates();
await showFilterWidget(context, viewModel);
if (viewModel.hasFiltersChanged) {
viewModel.resetLoadingNextPageState();
viewModel.getVendors(text: viewModel.searchString);
}
},
onCountryPick: () async {
viewModel.storeInitialFilterStates();
await showCountryPicker(context, viewModel);
if (viewModel.hasFiltersChanged) {
viewModel.resetLoadingNextPageState();
viewModel.getVendors(text: viewModel.searchString);
}
},
selectedCountry: viewModel.selectedCountry,
),
),
Expanded(
child: Observer(builder: (_) {
final vendors = viewModel.cakePayVendors;
if (viewModel.vendorsState is! CakePayVendorLoadedState) {
return const _Loading();
}
if (vendors.isEmpty)
return Expanded(child: Center(child: Text(S.of(context).no_cards_found)));
final loadingMore = viewModel.isLoadingNextPage;
final showThumb = vendors.length > 3;
final cardsList = Stack(
children: [
GridView.builder(
controller: _scroll,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 2 : 1,
childAspectRatio: 5,
crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5),
padding: EdgeInsets.only(left: 2, right: showThumb ? 10 : 22),
itemCount: vendors.length + (loadingMore ? 1 : 0),
itemBuilder: (_, i) {
if (i >= vendors.length) return const _Loading();
final vendor = vendors[i];
return CardItem(
logoUrl: vendor.card?.cardImageUrl,
title: vendor.name,
subTitle: vendor.card?.description ?? '',
onTap: () => Navigator.pushNamed(context, Routes.cakePayBuyCardPage,
arguments: [vendor]));
},
),
],
);
return showThumb
? Scrollbar(
key: ValueKey('cake_pay_shop_tab_scrollbar_key'),
thumbVisibility: true,
trackVisibility: true,
controller: _scroll,
child: cardsList,
)
: cardsList;
}),
),
],
);
}
}
class _Loading extends StatelessWidget {
const _Loading();
@override
Widget build(BuildContext context) => Center(
child: CircularProgressIndicator(),
);
}
@@ -1,3 +1,15 @@
enum CakePayPaymentMethod { BTC, BTC_LN, XMR, LTC, LTC_MWEB }
extension CakePayPaymentMethodLabel on CakePayPaymentMethod {
String get label => switch (this) {
CakePayPaymentMethod.BTC => 'Bitcoin',
CakePayPaymentMethod.BTC_LN => 'Bitcoin Lightning',
CakePayPaymentMethod.XMR => 'Monero',
CakePayPaymentMethod.LTC => 'Litecoin',
CakePayPaymentMethod.LTC_MWEB => 'Litecoin MWEB',
};
}
class CakePayOrder {
final String orderId;
final List<OrderCard> cards;
@@ -37,6 +49,8 @@ class OrderCard {
final String price;
final int quantity;
final String currencyCode;
final String? cardName;
final String? cardImagePath;
OrderCard({
required this.cardId,
@@ -44,6 +58,8 @@ class OrderCard {
required this.price,
required this.quantity,
required this.currencyCode,
required this.cardName,
required this.cardImagePath,
});
factory OrderCard.fromMap(Map<String, dynamic> map) {
@@ -53,20 +69,28 @@ class OrderCard {
price: map['price'] as String,
quantity: map['quantity'] as int,
currencyCode: map['currency_code'] as String,
cardName: map['name'] as String?,
cardImagePath: map['card_image_url'] as String?,
);
}
}
class PaymentData {
final CryptoPaymentData btc;
final CryptoPaymentData btc_ln;
final CryptoPaymentData xmr;
final CryptoPaymentData ltc;
final CryptoPaymentData ltc_mweb;
final DateTime invoiceTime;
final DateTime expirationTime;
final int? commission;
PaymentData({
required this.btc,
required this.btc_ln,
required this.xmr,
required this.ltc,
required this.ltc_mweb,
required this.invoiceTime,
required this.expirationTime,
required this.commission,
@@ -75,7 +99,10 @@ class PaymentData {
factory PaymentData.fromMap(Map<String, dynamic> map) {
return PaymentData(
btc: CryptoPaymentData.fromMap(map['BTC'] as Map<String, dynamic>),
btc_ln: CryptoPaymentData.fromMap(map['BTC_LN'] as Map<String, dynamic>),
xmr: CryptoPaymentData.fromMap(map['XMR'] as Map<String, dynamic>),
ltc: CryptoPaymentData.fromMap(map['LTC'] as Map<String, dynamic>),
ltc_mweb: CryptoPaymentData.fromMap(map['LTC_MWEB'] as Map<String, dynamic>),
invoiceTime: DateTime.fromMillisecondsSinceEpoch(map['invoice_time'] as int),
expirationTime: DateTime.fromMillisecondsSinceEpoch(map['expiration_time'] as int),
commission: map['commission'] as int?,
@@ -1,8 +1,8 @@
import 'dart:convert';
import 'package:cake_wallet/cake_pay/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/cake_pay_user_credentials.dart';
import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_user_credentials.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cake_wallet/entities/country.dart';
@@ -42,7 +42,6 @@ class CakePayApi {
throw Exception('Unexpected http status: ${response.statusCode}');
}
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
if (bodyJson.containsKey('user') && bodyJson['user']['email'] != null) {
@@ -79,7 +78,6 @@ class CakePayApi {
throw Exception('Unexpected http status: ${response.statusCode}');
}
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
if (bodyJson.containsKey('error')) {
@@ -108,12 +106,14 @@ class CakePayApi {
required bool confirmsTermsAgreed,
}) async {
final uri = Uri.https(baseCakePayUri, createOrderPath);
final headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Api-Key $apiKey',
};
final query = <String, dynamic>{
final body = json.encode({
'card_id': cardId,
'price': price,
'quantity': quantity,
@@ -123,35 +123,42 @@ class CakePayApi {
'confirms_no_vpn': confirmsNoVpn,
'confirms_voided_refund': confirmsVoidedRefund,
'confirms_terms_agreed': confirmsTermsAgreed,
};
});
try {
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: json.encode(query),
);
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: headers,
body: body,
);
if (response.statusCode != 201) {
final responseBody = json.decode(response.body);
if (responseBody is List) {
throw '${responseBody[0]}';
} else {
throw Exception('Unexpected error: $responseBody');
}
}
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
return CakePayOrder.fromMap(bodyJson);
} catch (e) {
throw Exception('${e}');
if (response.statusCode == 201) {
final data = json.decode(response.body) as Map<String, dynamic>;
return CakePayOrder.fromMap(data);
}
String message = 'Server error ${response.statusCode}';
final isJson = response.headers['content-type']?.contains('application/json') == true ||
response.body.trim().startsWith(RegExp(r'[\{\[]'));
if (isJson) {
try {
final decoded = json.decode(response.body);
if (decoded is List && decoded.isNotEmpty) {
message = decoded.first.toString();
} else if (decoded is Map && decoded['detail'] != null) {
message = decoded['detail'].toString();
} else {
message = decoded.toString();
}
} on FormatException {}
}
throw Exception(message);
}
///Simulate Payment
Future<void> simulatePayment(
Future<String> simulatePayment(
{required String CSRFToken, required String authorization, required String orderId}) async {
final uri = Uri.https(baseCakePayUri, simulatePaymentPath + '/$orderId');
@@ -162,7 +169,6 @@ class CakePayApi {
};
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
printV('Response: ${response.statusCode}');
@@ -172,7 +178,7 @@ class CakePayApi {
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
throw Exception('You just bot a gift card with id: ${bodyJson['order_id']}');
return 'You just SIMULATED a buying of a gift card with ID: ${bodyJson['order_id']}';
}
/// Logout
@@ -209,7 +215,7 @@ class CakePayApi {
};
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
@@ -256,7 +262,6 @@ class CakePayApi {
};
var response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
if (response.statusCode != 200) {
throw Exception(
@@ -1,7 +1,7 @@
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/cake_pay/cake_pay_api.dart';
import 'package:cake_wallet/cake_pay/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/country.dart';
@@ -114,6 +114,6 @@ class CakePayService {
}
///Simulate Purchase Gift Card
Future<void> simulatePayment({required String orderId}) async => await cakePayApi.simulatePayment(
Future<String> simulatePayment({required String orderId}) async => await cakePayApi.simulatePayment(
CSRFToken: CSRFToken, authorization: authorization, orderId: orderId);
}
@@ -0,0 +1,73 @@
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:flutter/material.dart';
class CakePayAlertModal extends StatelessWidget {
const CakePayAlertModal({
super.key,
required this.title,
required this.content,
required this.actionTitle,
this.showCloseButton = true,
this.dismissible = false,
});
final String title;
final Widget content;
final String actionTitle;
final bool showCloseButton;
final bool dismissible;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final maxHeight = MediaQuery.of(context).size.height * 0.8;
return AlertBackground(
dismissible: dismissible,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight),
child: Material(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(30)),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (title.isNotEmpty) ...[
Text(title,
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface, fontWeight: FontWeight.bold),
textAlign: TextAlign.center),
const SizedBox(height: 12),
],
Flexible(child: SingleChildScrollView(child: content)),
const SizedBox(height: 24),
PrimaryButton(
onPressed: () => Navigator.pop(context),
text: actionTitle,
color: theme.colorScheme.surfaceContainer,
textColor: theme.colorScheme.primary),
if (showCloseButton) ...[
const SizedBox(height: 16),
InkWell(
onTap: () => Navigator.pop(context),
child: CircleAvatar(
backgroundColor: theme.colorScheme.surfaceContainer,
child: Icon(Icons.close, color: theme.colorScheme.onSurface),
),
),
],
],
),
),
),
),
),
);
}
}
@@ -0,0 +1,126 @@
import 'package:cake_wallet/entities/country.dart';
import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
import 'package:cake_wallet/utils/debounce.dart';
import 'package:flutter/material.dart';
class CakePaySearchBar extends StatefulWidget {
const CakePaySearchBar(
{required this.initialQuery,
required this.onSearch,
required this.onFilter,
this.onCountryPick,
this.controller,
this.selectedCountry});
final String initialQuery;
final ValueChanged<String> onSearch;
final VoidCallback onFilter;
final VoidCallback? onCountryPick;
final Country? selectedCountry;
final TextEditingController? controller;
@override
State<CakePaySearchBar> createState() => _CakePaySearchBarState();
}
class _CakePaySearchBarState extends State<CakePaySearchBar> {
late final TextEditingController _searchController =
widget.controller ?? TextEditingController(text: widget.initialQuery);
final _searchFocusNode = FocusNode();
final _debounce = Debounce(const Duration(milliseconds: 500));
@override
void initState() {
super.initState();
_searchController
.addListener(() => _debounce.run(() => widget.onSearch(_searchController.text)));
}
@override
void dispose() {
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 32,
child: Row(
children: [
Expanded(child: SearchBarWidget(searchController: _searchController)),
const SizedBox(width: 5),
_CakePayFilterButton(onFilter: widget.onFilter),
if (widget.selectedCountry != null)
_CountryPickerWidget(
onTap: widget.onCountryPick,
selectedCountry: widget.selectedCountry!,
),
],
),
);
}
}
class _CakePayFilterButton extends StatelessWidget {
const _CakePayFilterButton({required this.onFilter});
final VoidCallback onFilter;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onFilter,
child: Container(
width: 32,
padding: const EdgeInsets.symmetric(vertical: 7),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(10)),
child: Image.asset('assets/images/filter_icon.png',
color: Theme.of(context).colorScheme.onSurface)),
);
}
}
class _CountryPickerWidget extends StatelessWidget {
const _CountryPickerWidget({required this.selectedCountry, this.onTap});
final Country selectedCountry;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 5),
child: GestureDetector(
onTap: onTap,
child: Container(
height: 32,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(10)),
child: Row(
children: [
Image.asset(selectedCountry.iconPath,
width: 24,
height: 24,
errorBuilder: (_, __, ___) => const SizedBox(width: 24, height: 24)),
const SizedBox(width: 6),
Text(
selectedCountry.countryCode,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
),
);
}
}
@@ -1,18 +1,11 @@
import 'package:flutter/material.dart';
import 'image_placeholder.dart';
class CardItem extends StatelessWidget {
CardItem({
required this.title,
required this.subTitle,
required this.backgroundColor,
required this.titleColor,
required this.subtitleColor,
this.hideBorder = true,
this.discount = 0.0,
this.isAmount = false,
this.discountBackground,
this.onTap,
this.logoUrl,
});
@@ -21,13 +14,7 @@ class CardItem extends StatelessWidget {
final String title;
final String subTitle;
final String? logoUrl;
final double discount;
final bool isAmount;
final bool hideBorder;
final Color backgroundColor;
final Color titleColor;
final Color subtitleColor;
final AssetImage? discountBackground;
@override
Widget build(BuildContext context) {
@@ -40,13 +27,8 @@ class CardItem extends StatelessWidget {
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(10),
border: hideBorder
? Border.all(color: Colors.transparent)
: Border.all(
color: Theme.of(context).colorScheme.outlineVariant.withOpacity(0.20),
),
),
child: Row(
children: [
@@ -73,26 +55,20 @@ class CardItem extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: titleColor,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
Text(
subTitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: titleColor,
fontSize: 10,
fontWeight: FontWeight.w700,
),
),
Text(title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 18, fontWeight: FontWeight.w700)),
Text(subTitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(fontSize: 10, fontWeight: FontWeight.w700)),
],
),
),
@@ -0,0 +1,122 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/number_text_fild_widget.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class DenominationsAmountWidget extends StatelessWidget {
const DenominationsAmountWidget(
{required this.fiatCurrency,
required this.denominations,
required this.amountFieldFocus,
required this.amountController,
required this.quantityFieldFocus,
required this.quantityController,
required this.cakePayBuyCardViewModel,
required this.onAmountChanged,
required this.onQuantityChanged});
final String fiatCurrency;
final List<String> denominations;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final FocusNode quantityFieldFocus;
final TextEditingController quantityController;
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
final Function(String) onAmountChanged;
final Function(int?) onQuantityChanged;
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.1,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 8,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownFilterList(
items: denominations,
itemPrefix: fiatCurrency,
selectedItem: denominations.first,
onItemSelected: (value) {
amountController.text = value;
onAmountChanged(value);
}),
const SizedBox(height: 4),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
child: Text(S.of(context).value,
maxLines: 2,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
],
),
),
Spacer(),
Expanded(
flex: 5,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NumberTextField(
controller: quantityController,
focusNode: quantityFieldFocus,
min: 1,
max: 99,
onChanged: (value) => onQuantityChanged(value)),
const SizedBox(height: 4),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: Text(S.of(context).quantity,
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
],
),
),
Spacer(),
Expanded(
flex: 8,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Observer(
builder: (_) => Text('$fiatCurrency ${cakePayBuyCardViewModel.totalAmount}',
maxLines: 1, style: Theme.of(context).textTheme.titleMedium!)),
const SizedBox(height: 4),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: Text(S.of(context).total,
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
],
)),
],
),
);
}
}
@@ -0,0 +1,103 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/typography.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class EnterAmountWidget extends StatelessWidget {
const EnterAmountWidget(
{required this.minValue,
required this.maxValue,
required this.fiatCurrency,
required this.amountFieldFocus,
required this.amountController,
required this.onAmountChanged});
final String minValue;
final String maxValue;
final String fiatCurrency;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final Function(String) onAmountChanged;
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Spacer(flex: 1),
Text(
S.of(context).enter_amount,
style: Theme.of(context).textTheme.titleLarge!,
),
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant))),
child: BaseTextFormField(
isDense: true,
contentPadding: EdgeInsets.zero,
controller: amountController,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
hintText: '0.00',
maxLines: 1,
prefixIcon: Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
'$fiatCurrency: ',
style: Theme.of(context).textTheme.titleMedium!,
),
),
prefixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
suffixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
suffixIcon: Padding(
padding: const EdgeInsets.only(top: 2, right: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('QTY ', style: Theme.of(context).textTheme.titleMedium!),
Text(
'1',
style: Theme.of(context).textTheme
.titleMedium!
.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)
),
],
),
),
textStyle: Theme.of(context).textTheme.titleMedium!,
placeholderTextStyle: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')),
FilteringTextInputFormatter.allow(
RegExp(r'^\d+(\.|\,)?\d{0,2}'),
),
],
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).min_amount(minValue) + ' $fiatCurrency',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
)),
Text(S.of(context).max_amount(maxValue) + ' $fiatCurrency',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
)),
],
),
Spacer(flex: 1),
],
),
);
}
}
@@ -0,0 +1,62 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
class FlipCard extends StatefulWidget {
const FlipCard({
super.key,
required this.front,
required this.back,
this.duration = const Duration(milliseconds: 400),
this.flipOnTouch = true,
});
final Widget front;
final Widget back;
final Duration duration;
final bool flipOnTouch;
@override
FlipCardState createState() => FlipCardState();
}
class FlipCardState extends State<FlipCard> with SingleTickerProviderStateMixin {
late final AnimationController _ctrl =
AnimationController(vsync: this, duration: widget.duration);
bool _isFront = true;
void toggleCard() {
if (_isFront) {
_ctrl.forward();
} else {
_ctrl.reverse();
}
_isFront = !_isFront;
}
@override
Widget build(BuildContext context) {
final content = AnimatedBuilder(
animation: _ctrl,
builder: (_, __) {
final angle = _ctrl.value * math.pi;
final isFront = angle < math.pi / 2;
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(angle),
child: isFront ? widget.front
: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi),
child: widget.back,
),
);
},
);
return widget.flipOnTouch
? GestureDetector(onTap: toggleCard, child: content)
: content;
}
}
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
class RoundedOverlayCards extends StatelessWidget {
const RoundedOverlayCards({
this.topCardChild = const SizedBox(),
this.bottomCardChild = const SizedBox(),
});
final Widget topCardChild;
final Widget bottomCardChild;
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
return ClipRRect(
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
child: Container(
height: screenHeight * 0.50,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
color: Theme.of(context).colorScheme.surfaceContainer
),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
color: Theme.of(context).colorScheme.surfaceContainerLow,
),
height: screenHeight * 0.38,
width: double.infinity,
child: topCardChild)),
bottomCardChild,
],
),
),
);
}
}
@@ -19,14 +19,15 @@ class TextIconButton extends StatelessWidget {
children: [
Text(
label,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
Icon(
Icons.chevron_right_rounded,
color: Theme.of(context).colorScheme.onSurface,
color: Theme.of(context).textTheme.titleLarge!.color,
),
],
),
@@ -0,0 +1,203 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class ThreeCheckboxAlert extends BaseAlertDialog {
ThreeCheckboxAlert({
required this.alertTitle,
required this.leftButtonText,
required this.rightButtonText,
required this.actionLeftButton,
required this.actionRightButton,
this.alertBarrierDismissible = true,
Key? key,
});
final String alertTitle;
final String leftButtonText;
final String rightButtonText;
final VoidCallback actionLeftButton;
final Function(bool, bool, bool) actionRightButton;
final bool alertBarrierDismissible;
bool checkbox1 = false;
void toggleCheckbox1() => checkbox1 = !checkbox1;
bool checkbox2 = false;
void toggleCheckbox2() => checkbox2 = !checkbox2;
bool checkbox3 = false;
void toggleCheckbox3() => checkbox3 = !checkbox3;
bool showValidationMessage = true;
@override
String get titleText => alertTitle;
@override
bool get isDividerExists => true;
@override
String get leftActionButtonText => leftButtonText;
@override
String get rightActionButtonText => rightButtonText;
@override
VoidCallback get actionLeft => actionLeftButton;
@override
VoidCallback get actionRight => () {
actionRightButton(checkbox1, checkbox2, checkbox3);
};
@override
bool get barrierDismissible => alertBarrierDismissible;
@override
Widget content(BuildContext context) {
return ThreeCheckboxAlertContent(
checkbox1: checkbox1,
toggleCheckbox1: toggleCheckbox1,
checkbox2: checkbox2,
toggleCheckbox2: toggleCheckbox2,
checkbox3: checkbox3,
toggleCheckbox3: toggleCheckbox3,
);
}
}
class ThreeCheckboxAlertContent extends StatefulWidget {
ThreeCheckboxAlertContent({
required this.checkbox1,
required this.toggleCheckbox1,
required this.checkbox2,
required this.toggleCheckbox2,
required this.checkbox3,
required this.toggleCheckbox3,
Key? key,
}) : super(key: key);
bool checkbox1;
void Function() toggleCheckbox1;
bool checkbox2;
void Function() toggleCheckbox2;
bool checkbox3;
void Function() toggleCheckbox3;
@override
_ThreeCheckboxAlertContentState createState() => _ThreeCheckboxAlertContentState(
checkbox1: checkbox1,
toggleCheckbox1: toggleCheckbox1,
checkbox2: checkbox2,
toggleCheckbox2: toggleCheckbox2,
checkbox3: checkbox3,
toggleCheckbox3: toggleCheckbox3,
);
static _ThreeCheckboxAlertContentState? of(BuildContext context) {
return context.findAncestorStateOfType<_ThreeCheckboxAlertContentState>();
}
}
class _ThreeCheckboxAlertContentState extends State<ThreeCheckboxAlertContent> {
_ThreeCheckboxAlertContentState({
required this.checkbox1,
required this.toggleCheckbox1,
required this.checkbox2,
required this.toggleCheckbox2,
required this.checkbox3,
required this.toggleCheckbox3,
});
bool checkbox1;
void Function() toggleCheckbox1;
bool checkbox2;
void Function() toggleCheckbox2;
bool checkbox3;
void Function() toggleCheckbox3;
bool showValidationMessage = true;
bool get areAllCheckboxesChecked => checkbox1 && checkbox2 && checkbox3;
@override
Widget build(BuildContext context) {
return Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
StandardCheckbox(
value: checkbox1,
caption: S.of(context).cakepay_confirm_no_vpn,
onChanged: (bool? value) {
setState(() {
checkbox1 = value ?? false;
toggleCheckbox1();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
StandardCheckbox(
value: checkbox2,
caption: S.of(context).cakepay_confirm_voided_refund,
onChanged: (bool? value) {
setState(() {
checkbox2 = value ?? false;
toggleCheckbox2();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
StandardCheckbox(
value: checkbox3,
caption: S.of(context).cakepay_confirm_terms_agreed,
onChanged: (bool? value) {
setState(() {
checkbox3 = value ?? false;
toggleCheckbox3();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => launchUrl(
Uri.parse("https://cakepay.com/cakepay-web-terms.txt"),
mode: LaunchMode.externalApplication,
),
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
S.of(context).settings_terms_and_conditions,
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).primaryColor,
decoration: TextDecoration.none,
height: 1,
),
softWrap: true,
),
),
),
if (showValidationMessage)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Please confirm all checkboxes',
style: TextStyle(
color: Colors.red,
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
decoration: TextDecoration.none,
),
),
),
],
),
);
}
}
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'image_placeholder.dart';
class UserCardItem extends StatelessWidget {
UserCardItem({
required this.title,
required this.subTitle,
this.onTap,
this.logoUrl
});
final VoidCallback? onTap;
final String title;
final String subTitle;
final String? logoUrl;
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: InkWell(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
if (logoUrl != null)
Padding(
padding: const EdgeInsets.all(2.0),
child: AspectRatio(
aspectRatio: 1.65,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(13)),
child: Image.network(
logoUrl!,
fit: BoxFit.cover,
loadingBuilder:
(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) => CakePayCardImagePlaceholder(),
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Row(
children: [
Expanded(
child: Text(title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 16, fontWeight: FontWeight.w700)),
),
SizedBox(width: 8),
Text(subTitle,
maxLines: 1,
overflow: TextOverflow.clip,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 16, fontWeight: FontWeight.w700)),
],
),
),
],
),
),
),
);
}
}
+8 -26
View File
@@ -76,7 +76,7 @@ import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/monero/monero.dart';
@@ -170,14 +170,12 @@ import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
import 'package:cake_wallet/cake_pay/cake_pay_api.dart';
import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
import 'package:cake_wallet/cake_pay/cake_pay.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_purchase_view_model.dart';
import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
import 'package:cake_wallet/view_model/new_wallet_type_view_model.dart';
@@ -276,7 +274,6 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'buy/kryptonim/kryptonim.dart';
import 'buy/meld/meld_buy_provider.dart';
import 'src/screens/buy/buy_sell_page.dart';
import 'cake_pay/cake_pay_payment_credantials.dart';
import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart';
import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
@@ -1390,18 +1387,11 @@ Future<void> setup({
getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get<CakePayService>()));
getIt.registerFactoryParam<CakePayPurchaseViewModel, PaymentCredential, CakePayCard>(
(PaymentCredential paymentCredential, CakePayCard card) {
return CakePayPurchaseViewModel(
cakePayService: getIt.get<CakePayService>(),
paymentCredential: paymentCredential,
card: card,
sendViewModel: getIt.get<SendViewModel>());
});
getIt.registerFactoryParam<CakePayBuyCardViewModel, CakePayVendor, void>(
(CakePayVendor vendor, _) {
return CakePayBuyCardViewModel(vendor: vendor);
return CakePayBuyCardViewModel(vendor: vendor,
cakePayService: getIt.get<CakePayService>(),
sendViewModel: getIt.get<SendViewModel>());
});
getIt.registerFactory(() => CakePayAccountViewModel(cakePayService: getIt.get<CakePayService>()));
@@ -1422,14 +1412,6 @@ Future<void> setup({
getIt.get<CakePayBuyCardViewModel>(param1: vendor), getIt.get<CakePayService>());
});
getIt
.registerFactoryParam<CakePayBuyCardDetailPage, List<dynamic>, void>((List<dynamic> args, _) {
final paymentCredential = args.first as PaymentCredential;
final card = args[1] as CakePayCard;
return CakePayBuyCardDetailPage(
getIt.get<CakePayPurchaseViewModel>(param1: paymentCredential, param2: card));
});
getIt.registerFactory(() => CakePayCardsPage(getIt.get<CakePayCardsListViewModel>()));
getIt.registerFactory(() => CakePayAccountPage(getIt.get<CakePayAccountViewModel>()));
+8 -14
View File
@@ -21,8 +21,7 @@ import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/buy/webview_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
import 'package:cake_wallet/cake_pay/cake_pay.dart';
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
import 'package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_options_page.dart';
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';
late RouteSettings currentRouteSettings;
Route<dynamic> handleRouteWithPlatformAwareness(
Route<T> handleRouteWithPlatformAwareness<T>(
Widget Function(BuildContext) builder, {
bool fullscreenDialog = false,
}) {
if (Platform.isIOS) {
return CupertinoPageRoute<void>(builder: builder, fullscreenDialog: fullscreenDialog);
return CupertinoPageRoute<T>(builder: builder, fullscreenDialog: fullscreenDialog);
} else {
return MaterialPageRoute<void>(builder: builder, fullscreenDialog: fullscreenDialog);
return MaterialPageRoute<T>(builder: builder, fullscreenDialog: fullscreenDialog);
}
}
@@ -708,25 +707,20 @@ Route<dynamic> createRoute(RouteSettings settings) {
(context) => getIt.get<CakePayBuyCardPage>(param1: args),
);
case Routes.cakePayBuyCardDetailPage:
final args = settings.arguments as List;
return handleRouteWithPlatformAwareness(
(context) => getIt.get<CakePayBuyCardDetailPage>(param1: args),
);
case Routes.cakePayWelcomePage:
return handleRouteWithPlatformAwareness(
return handleRouteWithPlatformAwareness<bool>(
(context) => getIt.get<CakePayWelcomePage>(),
);
case Routes.cakePayVerifyOtpPage:
final args = settings.arguments as List;
return handleRouteWithPlatformAwareness(
return handleRouteWithPlatformAwareness<bool>(
(context) => getIt.get<CakePayVerifyOtpPage>(param1: args),
);
case Routes.cakePayAccountPage:
return handleRouteWithPlatformAwareness(
return handleRouteWithPlatformAwareness<bool>(
(context) => getIt.get<CakePayAccountPage>(),
);
-1
View File
@@ -75,7 +75,6 @@ class Routes {
static const cakePayLoginPage = '/cake_pay_login_page';
static const cakePayCardsPage = '/cake_pay_cards_page';
static const cakePayBuyCardPage = '/cake_pay_buy_card_page';
static const cakePayBuyCardDetailPage = '/cake_pay_buy_card_detail_page';
static const cakePayVerifyOtpPage = '/cake_pay_verify_otp_page';
static const cakePayAccountPage = '/cake_pay_account_page';
static const webViewPage = '/web_view_page';
-5
View File
@@ -1,5 +0,0 @@
export 'auth/cake_pay_welcome_page.dart';
export 'auth/cake_pay_verify_otp_page.dart';
export 'cards/cake_pay_confirm_purchase_card_page.dart';
export 'cards/cake_pay_cards_page.dart';
export 'cards/cake_pay_buy_card_page.dart';
@@ -1,519 +0,0 @@
import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/cake_pay_payment_credantials.dart';
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/image_placeholder.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/link_extractor.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/number_text_fild_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
class CakePayBuyCardPage extends BasePage {
CakePayBuyCardPage(
this.cakePayBuyCardViewModel,
this.cakePayService,
) : _amountFieldFocus = FocusNode(),
_amountController = TextEditingController(),
_quantityFieldFocus = FocusNode(),
_quantityController =
TextEditingController(text: cakePayBuyCardViewModel.quantity.toString()) {
_amountController.addListener(() {
cakePayBuyCardViewModel.onAmountChanged(_amountController.text);
});
}
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
final CakePayService cakePayService;
@override
String get title => cakePayBuyCardViewModel.card.name;
@override
bool get extendBodyBehindAppBar => true;
@override
AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent;
@override
Widget? middle(BuildContext context) {
return Text(
title,
textAlign: TextAlign.center,
maxLines: 2,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
);
}
final TextEditingController _amountController;
final FocusNode _amountFieldFocus;
final TextEditingController _quantityController;
final FocusNode _quantityFieldFocus;
@override
Widget body(BuildContext context) {
final card = cakePayBuyCardViewModel.card;
final vendor = cakePayBuyCardViewModel.vendor;
return KeyboardActions(
disableScroll: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).colorScheme.surface,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _amountFieldFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
]),
child: Container(
color: Theme.of(context).colorScheme.surface,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
height: responsiveLayoutUtil.screenHeight * 0.35,
width: double.infinity,
child: Column(
children: [
Expanded(flex: 4, child: const SizedBox()),
Expanded(
flex: 7,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10)),
child: Image.network(
card.cardImageUrl ?? '',
fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) =>
CakePayCardImagePlaceholder(),
),
),
),
Expanded(child: const SizedBox()),
],
)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Container(
height: responsiveLayoutUtil.screenHeight * 0.5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24),
Expanded(
child: Text(
S.of(context).enter_amount,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 24,
fontWeight: FontWeight.w600,
),
),
),
card.denominations.isNotEmpty
? Expanded(
flex: 2,
child: _DenominationsAmountWidget(
fiatCurrency: card.fiatCurrency.title,
denominations: card.denominations,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
quantityFieldFocus: _quantityFieldFocus,
quantityController: _quantityController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
),
)
: Expanded(
flex: 2,
child: _EnterAmountWidget(
minValue: card.minValue ?? '-',
maxValue: card.maxValue ?? '-',
fiatCurrency: card.fiatCurrency.title,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
),
),
Expanded(
flex: 5,
child: Column(
children: [
if (vendor.cakeWarnings != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color:
Theme.of(context).colorScheme.onPrimary.withOpacity(0.20),
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
vendor.cakeWarnings!,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.w600,
),
),
),
),
),
Expanded(
child: SingleChildScrollView(
child: ClickableLinksText(
text: card.description ?? '',
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 18,
),
),
),
),
],
),
),
],
),
),
),
],
),
bottomSection: Column(
children: [
Observer(builder: (_) {
return Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
onPressed: () => isIOSUnavailable(card)
? alertIOSAvailability(context, card)
: navigateToCakePayBuyCardDetailPage(context, card),
text: S.of(context).buy_now,
isDisabled: !cakePayBuyCardViewModel.isEnablePurchase,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
);
}),
],
),
),
),
);
}
bool isWordInCardsName(CakePayCard card, String word) {
return card.name.toLowerCase().contains(word.toLowerCase());
}
bool isIOSUnavailable(CakePayCard card) {
if (!Platform.isIOS && !Platform.isMacOS) {
return false;
}
final isDigitalGameStores = isWordInCardsName(card, 'playstation') ||
isWordInCardsName(card, 'xbox') ||
isWordInCardsName(card, 'steam') ||
isWordInCardsName(card, 'meta quest') ||
isWordInCardsName(card, 'kigso') ||
isWordInCardsName(card, 'game world') ||
isWordInCardsName(card, 'google') ||
isWordInCardsName(card, 'nintendo');
final isGCodes = isWordInCardsName(card, 'gcodes');
final isApple = isWordInCardsName(card, 'itunes') || isWordInCardsName(card, 'apple');
final isTidal = isWordInCardsName(card, 'tidal');
final isVPNServices = isWordInCardsName(card, 'nordvpn') ||
isWordInCardsName(card, 'expressvpn') ||
isWordInCardsName(card, 'surfshark') ||
isWordInCardsName(card, 'proton');
final isStreamingServices = isWordInCardsName(card, 'netflix') ||
isWordInCardsName(card, 'spotify') ||
isWordInCardsName(card, 'hulu') ||
isWordInCardsName(card, 'hbo') ||
isWordInCardsName(card, 'soundcloud') ||
isWordInCardsName(card, 'twitch');
final isDatingServices = isWordInCardsName(card, 'tinder');
return isDigitalGameStores ||
isGCodes ||
isApple ||
isTidal ||
isVPNServices ||
isStreamingServices ||
isDatingServices;
}
Future<void> alertIOSAvailability(BuildContext context, CakePayCard card) async {
return await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).cakepay_ios_not_available,
buttonText: S.of(context).ok,
buttonAction: () {
// _walletHardwareRestoreVM.error = null;
Navigator.of(context).pop();
});
});
}
Future<void> navigateToCakePayBuyCardDetailPage(BuildContext context, CakePayCard card) async {
final userName = await cakePayService.getUserEmail();
final paymentCredential = PaymentCredential(
amount: cakePayBuyCardViewModel.amount,
quantity: cakePayBuyCardViewModel.quantity,
totalAmount: cakePayBuyCardViewModel.totalAmount,
userName: userName,
fiatCurrency: card.fiatCurrency.title,
);
Navigator.pushNamed(
context,
Routes.cakePayBuyCardDetailPage,
arguments: [paymentCredential, card],
);
}
}
class _DenominationsAmountWidget extends StatelessWidget {
const _DenominationsAmountWidget({
required this.fiatCurrency,
required this.denominations,
required this.amountFieldFocus,
required this.amountController,
required this.quantityFieldFocus,
required this.quantityController,
required this.cakePayBuyCardViewModel,
required this.onAmountChanged,
required this.onQuantityChanged,
});
final String fiatCurrency;
final List<String> denominations;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final FocusNode quantityFieldFocus;
final TextEditingController quantityController;
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
final Function(String) onAmountChanged;
final Function(int?) onQuantityChanged;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 12,
child: Column(
children: [
Expanded(
child: DropdownFilterList(
items: denominations,
itemPrefix: fiatCurrency,
selectedItem: denominations.first,
textStyle: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurface),
onItemSelected: (value) {
amountController.text = value;
onAmountChanged(value);
},
caption: '',
),
),
const SizedBox(height: 4),
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: Text(S.of(context).choose_card_value + ':',
maxLines: 2,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
),
],
),
),
Expanded(child: const SizedBox()),
Expanded(
flex: 8,
child: Column(
children: [
Expanded(
child: NumberTextField(
controller: quantityController,
focusNode: quantityFieldFocus,
min: 1,
max: 99,
onChanged: (value) => onQuantityChanged(value),
),
),
const SizedBox(height: 4),
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: Text(S.of(context).quantity + ':',
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
),
],
),
),
Expanded(child: const SizedBox()),
Expanded(
flex: 12,
child: Column(
children: [
Expanded(
child: Container(
alignment: Alignment.bottomCenter,
child: Observer(
builder: (_) => AutoSizeText(
'$fiatCurrency ${cakePayBuyCardViewModel.totalAmount}',
maxLines: 1,
style: textMediumSemiBold(
color: Theme.of(context).colorScheme.onSurface)))),
),
const SizedBox(height: 4),
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: Text(S.of(context).total + ':',
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
),
],
)),
],
);
}
}
class _EnterAmountWidget extends StatelessWidget {
const _EnterAmountWidget({
required this.minValue,
required this.maxValue,
required this.fiatCurrency,
required this.amountFieldFocus,
required this.amountController,
required this.onAmountChanged,
});
final String minValue;
final String maxValue;
final String fiatCurrency;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final Function(String) onAmountChanged;
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: BaseTextFormField(
controller: amountController,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
hintText: '0.00',
maxLines: null,
prefixIcon: Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
'$fiatCurrency: ',
style: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurface),
),
),
textStyle: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurface),
placeholderTextStyle:
textMediumSemiBold(color: Theme.of(context).colorScheme.onSurfaceVariant),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')),
FilteringTextInputFormatter.allow(
RegExp(r'^\d+(\.|\,)?\d{0,2}'),
),
],
),
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).min_amount(minValue) + ' $fiatCurrency',
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
Text(S.of(context).max_amount(maxValue) + ' $fiatCurrency',
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
],
),
],
);
}
}
@@ -1,409 +0,0 @@
import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
import 'package:cake_wallet/entities/country.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/card_item.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/card_menu.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/filter_widget.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/cake_scrollbar.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/debounce.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class CakePayCardsPage extends BasePage {
CakePayCardsPage(this._cardsListViewModel) : searchFocusNode = FocusNode() {
_searchController.addListener(() {
if (_searchController.text != _cardsListViewModel.searchString) {
_searchDebounce.run(() {
_cardsListViewModel.resetLoadingNextPageState();
_cardsListViewModel.getVendors(text: _searchController.text);
});
}
});
}
final FocusNode searchFocusNode;
final CakePayCardsListViewModel _cardsListViewModel;
final _searchDebounce = Debounce(Duration(milliseconds: 500));
final _searchController = TextEditingController();
@override
bool get gradientBackground => true;
@override
Widget Function(BuildContext, Widget) get rootWrapper =>
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
@override
bool get resizeToAvoidBottomInset => false;
@override
Widget get endDrawer => CardMenu();
@override
Widget middle(BuildContext context) {
return Text(
'Cake Pay',
style: textMediumSemiBold(
color: Theme.of(context).colorScheme.onSurface,
),
);
}
@override
Widget trailing(BuildContext context) {
return _TrailingIcon(
asset: 'assets/images/profile.png',
iconColor: Theme.of(context).colorScheme.onSurface,
onPressed: () {
_cardsListViewModel.isCakePayUserAuthenticated().then(
(value) {
if (value) {
Navigator.pushNamed(context, Routes.cakePayAccountPage);
return;
}
Navigator.pushNamed(context, Routes.cakePayWelcomePage);
},
);
},
);
}
@override
Widget body(BuildContext context) {
if (_cardsListViewModel.settingsStore.selectedCakePayCountry == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
reaction((_) => _cardsListViewModel.shouldShowCountryPicker,
(bool shouldShowCountryPicker) async {
if (shouldShowCountryPicker) {
_cardsListViewModel.storeInitialFilterStates();
await showCountryPicker(context, _cardsListViewModel);
if (_cardsListViewModel.hasFiltersChanged) {
_cardsListViewModel.resetLoadingNextPageState();
_cardsListViewModel.getVendors();
}
_cardsListViewModel.settingsStore.selectedCakePayCountry =
_cardsListViewModel.selectedCountry;
}
});
});
}
final filterButton = Semantics(
label: S.of(context).filter_by,
child: GestureDetector(
onTap: () async {
_cardsListViewModel.storeInitialFilterStates();
await showFilterWidget(context);
if (_cardsListViewModel.hasFiltersChanged) {
_cardsListViewModel.resetLoadingNextPageState();
_cardsListViewModel.getVendors();
}
},
child: Container(
width: 32,
padding: EdgeInsets.only(top: 7, bottom: 7),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border.all(
color: Colors.transparent,
),
borderRadius: BorderRadius.circular(10),
),
child: Image.asset(
'assets/images/filter_icon.png',
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
);
final _countryPicker = Semantics(
label: S.of(context).filter_by,
child: GestureDetector(
onTap: () async {
_cardsListViewModel.storeInitialFilterStates();
await showCountryPicker(context, _cardsListViewModel);
if (_cardsListViewModel.hasFiltersChanged) {
_cardsListViewModel.resetLoadingNextPageState();
_cardsListViewModel.getVendors();
}
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border.all(color: Colors.transparent),
borderRadius: BorderRadius.circular(10),
),
child: Container(
margin: EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Image.asset(
_cardsListViewModel.selectedCountry.iconPath,
width: 24,
height: 24,
errorBuilder: (context, error, stackTrace) => Container(
width: 24,
height: 24,
),
),
SizedBox(width: 6),
Text(
_cardsListViewModel.selectedCountry.countryCode,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
),
);
return Padding(
padding: const EdgeInsets.all(14.0),
child: Column(
children: [
Container(
padding: EdgeInsets.only(left: 2, right: 22),
height: 32,
child: Row(
children: [
Expanded(
child: _SearchWidget(
controller: _searchController,
focusNode: searchFocusNode,
),
),
SizedBox(width: 5),
filterButton,
SizedBox(width: 5),
_countryPicker
],
),
),
SizedBox(height: 8),
Expanded(child: CakePayCardsPageBody(cardsListViewModel: _cardsListViewModel))
],
),
);
}
Future<void> showFilterWidget(BuildContext context) async {
return showPopUp<void>(
context: context,
builder: (BuildContext context) {
return FilterWidget(filterItems: _cardsListViewModel.createFilterItems);
},
);
}
}
Future<void> showCountryPicker(
BuildContext context, CakePayCardsListViewModel cardsListViewModel) async {
await showPopUp<void>(
context: context,
builder: (_) => Picker(
title: S.of(context).select_your_country,
items: cardsListViewModel.availableCountries,
images: cardsListViewModel.availableCountries
.map((e) => Image.asset(
e.iconPath,
errorBuilder: (context, error, stackTrace) => Container(
width: 58,
height: 58,
),
))
.toList(),
selectedAtIndex:
cardsListViewModel.availableCountries.indexOf(cardsListViewModel.selectedCountry),
onItemSelected: (Country country) => cardsListViewModel.setSelectedCountry(country),
isSeparated: false,
hintText: S.of(context).search,
matchingCriteria: (Country country, String searchText) =>
country.fullName.toLowerCase().contains(searchText.toLowerCase())));
}
class CakePayCardsPageBody extends StatefulWidget {
const CakePayCardsPageBody({
Key? key,
required this.cardsListViewModel,
}) : super(key: key);
final CakePayCardsListViewModel cardsListViewModel;
@override
_CakePayCardsPageBodyState createState() => _CakePayCardsPageBodyState();
}
class _CakePayCardsPageBodyState extends State<CakePayCardsPageBody> {
double get backgroundHeight => MediaQuery.of(context).size.height * 0.75;
double thumbHeight = 72;
bool get isAlwaysShowScrollThumb => merchantsList.isEmpty ? false : merchantsList.length > 3;
List<CakePayVendor> get merchantsList => widget.cardsListViewModel.cakePayVendors;
final _scrollController = ScrollController();
@override
void initState() {
_scrollController.addListener(() {
final scrollOffsetFromTop = _scrollController.hasClients
? (_scrollController.offset /
_scrollController.position.maxScrollExtent *
(backgroundHeight - thumbHeight))
: 0.0;
widget.cardsListViewModel.setScrollOffsetFromTop(scrollOffsetFromTop);
double threshold = 200.0;
bool isNearBottom =
_scrollController.offset >= _scrollController.position.maxScrollExtent - threshold;
if (isNearBottom && !_scrollController.position.outOfRange) {
widget.cardsListViewModel.fetchNextPage();
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Observer(builder: (_) {
final vendorsState = widget.cardsListViewModel.vendorsState;
if (vendorsState is CakePayVendorLoadedState) {
bool isLoadingMore = widget.cardsListViewModel.isLoadingNextPage;
final vendors = widget.cardsListViewModel.cakePayVendors;
if (vendors.isEmpty) {
return Center(child: Text(S.of(context).no_cards_found));
}
return Stack(children: [
GridView.builder(
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 2 : 1,
childAspectRatio: 5,
crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
),
padding: EdgeInsets.only(left: 2, right: 22),
itemCount: vendors.length + (isLoadingMore ? 1 : 0),
itemBuilder: (_, index) {
if (index >= vendors.length) {
return _VendorLoadedIndicator();
}
final vendor = vendors[index];
return CardItem(
logoUrl: vendor.card?.cardImageUrl,
onTap: () {
Navigator.of(context).pushNamed(Routes.cakePayBuyCardPage, arguments: [vendor]);
},
title: vendor.name,
subTitle: vendor.card?.description ?? '',
backgroundColor: Theme.of(context).colorScheme.surface,
titleColor: Theme.of(context).colorScheme.onSurface,
subtitleColor: Theme.of(context).colorScheme.onSecondary,
discount: 0.0,
);
},
),
isAlwaysShowScrollThumb
? CakeScrollbar(
backgroundHeight: backgroundHeight,
thumbHeight: thumbHeight,
rightOffset: 1,
width: 3,
backgroundColor: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.05),
thumbColor: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5),
fromTop: widget.cardsListViewModel.scrollOffsetFromTop,
)
: Offstage()
]);
}
return _VendorLoadedIndicator();
});
}
}
class _VendorLoadedIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).colorScheme.onSurface,
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).colorScheme.primary),
),
);
}
}
class _SearchWidget extends StatelessWidget {
const _SearchWidget({
Key? key,
required this.controller,
required this.focusNode,
}) : super(key: key);
final TextEditingController controller;
final FocusNode focusNode;
@override
Widget build(BuildContext context) {
return BaseTextFormField(
focusNode: focusNode,
textStyle: Theme.of(context).textTheme.bodyMedium,
controller: controller,
contentPadding: EdgeInsets.only(top: 8, left: 8),
hintText: S.of(context).search,
placeholderTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
alignLabelWithHint: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
suffixIcon: ExcludeSemantics(
child: Icon(
Icons.search,
color: Theme.of(context).colorScheme.primary,
),
),
);
}
}
class _TrailingIcon extends StatelessWidget {
const _TrailingIcon({required this.asset, this.onPressed, required this.iconColor});
final String asset;
final VoidCallback? onPressed;
final Color iconColor;
@override
Widget build(BuildContext context) {
return Semantics(
label: S.of(context).profile,
child: Material(
color: Colors.transparent,
child: IconButton(
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
highlightColor: Colors.transparent,
onPressed: onPressed,
icon: ImageIcon(AssetImage(asset), size: 25, color: iconColor),
),
));
}
}
@@ -1,703 +0,0 @@
import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/cake_pay_alert_modal.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/image_placeholder.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/link_extractor.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/text_icon_button.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_purchase_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher.dart';
class CakePayBuyCardDetailPage extends BasePage {
CakePayBuyCardDetailPage(this.cakePayPurchaseViewModel);
final CakePayPurchaseViewModel cakePayPurchaseViewModel;
@override
String get title => cakePayPurchaseViewModel.card.name;
@override
Widget? middle(BuildContext context) {
return Text(
title,
textAlign: TextAlign.center,
maxLines: 2,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
);
}
@override
Widget? trailing(BuildContext context) => null;
bool _effectsInstalled = false;
@override
Widget body(BuildContext context) {
_setEffects(context);
final card = cakePayPurchaseViewModel.card;
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Observer(builder: (_) {
return Column(
children: [
SizedBox(height: 36),
ClipRRect(
borderRadius:
BorderRadius.horizontal(left: Radius.circular(20), right: Radius.circular(20)),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.20),
),
),
child: Row(
children: [
Expanded(
child: Container(
child: ClipRRect(
borderRadius: BorderRadius.horizontal(
left: Radius.circular(20), right: Radius.circular(20)),
child: Image.network(
card.cardImageUrl ?? '',
fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) =>
CakePayCardImagePlaceholder(),
),
)),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(children: [
Row(
children: [
Text(
S.of(context).value + ':',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
SizedBox(width: 8),
Text(
'${cakePayPurchaseViewModel.amount.toStringAsFixed(2)} ${cakePayPurchaseViewModel.fiatCurrency}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
SizedBox(height: 16),
Row(
children: [
Text(
S.of(context).quantity + ':',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
SizedBox(width: 8),
Text(
'${cakePayPurchaseViewModel.quantity}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
SizedBox(height: 16),
Row(
children: [
Text(
S.of(context).total + ':',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
SizedBox(width: 8),
Text(
'${cakePayPurchaseViewModel.totalAmount.toStringAsFixed(2)} ${cakePayPurchaseViewModel.fiatCurrency}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
]),
),
)
],
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: TextIconButton(
label: S.of(context).how_to_use_card,
onTap: () => _showHowToUseCard(context, card),
),
),
SizedBox(height: 20),
if (card.expiryAndValidity != null && card.expiryAndValidity!.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).expiry_and_validity + ':',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
)),
SizedBox(height: 10),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.20),
width: 1,
),
),
),
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
card.expiryAndValidity ?? '',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
],
),
),
],
);
}),
bottomSection: Column(
children: [
Padding(
padding: EdgeInsets.only(bottom: 12),
child: Observer(builder: (_) {
return LoadingPrimaryButton(
isDisabled: cakePayPurchaseViewModel.isPurchasing,
isLoading: cakePayPurchaseViewModel.isPurchasing ||
cakePayPurchaseViewModel.sendViewModel.state is IsExecutingState,
onPressed: () => confirmPurchaseFirst(context),
text: S.of(context).purchase_gift_card,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
);
}),
),
SizedBox(height: 8),
InkWell(
onTap: () => _showTermsAndCondition(context, card.termsAndConditions),
child: Text(
S.of(context).settings_terms_and_conditions,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(height: 16)
],
),
);
}
void _showTermsAndCondition(BuildContext context, String? termsAndConditions) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return CakePayAlertModal(
title: S.of(context).settings_terms_and_conditions,
content: Align(
alignment: Alignment.bottomLeft,
child: ClickableLinksText(
text: termsAndConditions ?? '',
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 18,
) ??
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 18,
),
),
),
actionTitle: S.of(context).agree,
showCloseButton: false,
heightFactor: 0.6,
);
});
}
Future<void> _showconfirmPurchaseFirstAlert(BuildContext context) async {
if (!cakePayPurchaseViewModel.confirmsNoVpn ||
!cakePayPurchaseViewModel.confirmsVoidedRefund ||
!cakePayPurchaseViewModel.confirmsTermsAgreed) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) => ThreeCheckboxAlert(
alertTitle: S.of(context).cakepay_confirm_purchase,
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).confirm,
actionLeftButton: () {
cakePayPurchaseViewModel.isPurchasing = false;
Navigator.of(context).pop();
},
actionRightButton: (confirmsNoVpn, confirmsVoidedRefund, confirmsTermsAgreed) {
cakePayPurchaseViewModel.confirmsNoVpn = confirmsNoVpn;
cakePayPurchaseViewModel.confirmsVoidedRefund = confirmsVoidedRefund;
cakePayPurchaseViewModel.confirmsTermsAgreed = confirmsTermsAgreed;
Navigator.of(context).pop();
},
),
);
}
if (cakePayPurchaseViewModel.confirmsNoVpn &&
cakePayPurchaseViewModel.confirmsVoidedRefund &&
cakePayPurchaseViewModel.confirmsTermsAgreed) {
await purchaseCard(context);
}
}
Future<void> confirmPurchaseFirst(BuildContext context) async {
bool isLogged = await cakePayPurchaseViewModel.cakePayService.isLogged();
if (!isLogged) {
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
} else {
cakePayPurchaseViewModel.isPurchasing = true;
await _showconfirmPurchaseFirstAlert(context);
}
}
Future<void> purchaseCard(BuildContext context) async {
bool isLogged = await cakePayPurchaseViewModel.cakePayService.isLogged();
if (!isLogged) {
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
} else {
try {
await cakePayPurchaseViewModel.createOrder();
} catch (_) {
await cakePayPurchaseViewModel.cakePayService.logout();
}
}
cakePayPurchaseViewModel.isPurchasing = false;
}
void _showHowToUseCard(
BuildContext context,
CakePayCard card,
) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return CakePayAlertModal(
title: S.of(context).how_to_use_card,
content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Padding(
padding: EdgeInsets.all(10),
child: Text(
card.name,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
)),
ClickableLinksText(
text: card.howToUse ?? '',
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
) ??
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
linkStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontStyle: FontStyle.italic,
) ??
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontStyle: FontStyle.italic,
),
),
]),
actionTitle: S.current.got_it,
);
});
}
Future<void> _showConfirmSendingAlert(BuildContext context) async {
if (cakePayPurchaseViewModel.order == null) {
return;
}
ReactionDisposer? disposer;
disposer = reaction((_) => cakePayPurchaseViewModel.isOrderExpired, (bool isExpired) {
if (isExpired) {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
if (disposer != null) {
disposer();
}
}
});
final order = cakePayPurchaseViewModel.order;
showModalBottomSheet<void>(
context: context,
isDismissible: false,
isScrollControlled: true,
builder: (BuildContext popupContext) {
return ConfirmSendingBottomSheet(
key: ValueKey('send_page_confirm_sending_dialog_key'),
currentTheme: currentTheme,
walletType: cakePayPurchaseViewModel.sendViewModel.walletType,
paymentId: S.of(popupContext).payment_id,
paymentIdValue: order?.orderId,
expirationTime: cakePayPurchaseViewModel.formattedRemainingTime,
titleText: S.of(popupContext).confirm_transaction,
titleIconPath: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
currency: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency,
amount: S.of(popupContext).send_amount,
amountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.amountFormatted,
fiatAmountValue:
cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(popupContext).send_fee,
feeValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount:
cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: cakePayPurchaseViewModel.sendViewModel.outputs,
onSlideComplete: () async {
Navigator.of(popupContext).pop();
cakePayPurchaseViewModel.sendViewModel.commitTransaction(context);
},
);
},
);
}
BuildContext? loadingBottomSheetContext;
void _setEffects(BuildContext context) {
if (_effectsInstalled) {
return;
}
reaction((_) => cakePayPurchaseViewModel.sendViewModel.state, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) showStateAlert(context, S.of(context).error, state.error);
});
}
if (state is! IsExecutingState &&
loadingBottomSheetContext != null &&
loadingBottomSheetContext!.mounted) {
Navigator.of(loadingBottomSheetContext!).pop();
}
if (state is IsExecutingState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (BuildContext context) {
loadingBottomSheetContext = context;
return LoadingBottomSheet(
titleText: S.of(context).generating_transaction,
);
},
);
}
});
}
if (state is ExecutedSuccessfullyState) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _showConfirmSendingAlert(context);
});
}
if (state is TransactionCommitted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
cakePayPurchaseViewModel.sendViewModel.clearOutputs();
if (context.mounted) showSentAlert(context);
});
}
});
_effectsInstalled = true;
}
void showStateAlert(BuildContext context, String title, String content) {
if (context.mounted) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: title,
alertContent: content,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
}
Future<void> showSentAlert(BuildContext context) async {
if (!context.mounted) {
return;
}
final order = cakePayPurchaseViewModel.order!.orderId;
final isCopy = await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).transaction_sent,
alertContent: S.of(context).cake_pay_save_order + '\n${order}',
leftButtonText: S.of(context).ignor,
rightButtonText: S.of(context).copy,
actionLeftButton: () => Navigator.of(context).pop(false),
actionRightButton: () => Navigator.of(context).pop(true));
}) ??
false;
if (isCopy) {
await Clipboard.setData(ClipboardData(text: order));
}
}
void _handleDispose(ReactionDisposer? disposer) {
cakePayPurchaseViewModel.dispose();
if (disposer != null) {
disposer();
}
}
}
class ThreeCheckboxAlert extends BaseAlertDialog {
ThreeCheckboxAlert({
required this.alertTitle,
required this.leftButtonText,
required this.rightButtonText,
required this.actionLeftButton,
required this.actionRightButton,
this.alertBarrierDismissible = true,
Key? key,
});
final String alertTitle;
final String leftButtonText;
final String rightButtonText;
final VoidCallback actionLeftButton;
final Function(bool, bool, bool) actionRightButton;
final bool alertBarrierDismissible;
bool checkbox1 = false;
void toggleCheckbox1() => checkbox1 = !checkbox1;
bool checkbox2 = false;
void toggleCheckbox2() => checkbox2 = !checkbox2;
bool checkbox3 = false;
void toggleCheckbox3() => checkbox3 = !checkbox3;
bool showValidationMessage = true;
@override
String get titleText => alertTitle;
@override
bool get isDividerExists => true;
@override
String get leftActionButtonText => leftButtonText;
@override
String get rightActionButtonText => rightButtonText;
@override
VoidCallback get actionLeft => actionLeftButton;
@override
VoidCallback get actionRight => () {
actionRightButton(checkbox1, checkbox2, checkbox3);
};
@override
bool get barrierDismissible => alertBarrierDismissible;
@override
Widget content(BuildContext context) {
return ThreeCheckboxAlertContent(
checkbox1: checkbox1,
toggleCheckbox1: toggleCheckbox1,
checkbox2: checkbox2,
toggleCheckbox2: toggleCheckbox2,
checkbox3: checkbox3,
toggleCheckbox3: toggleCheckbox3,
);
}
}
class ThreeCheckboxAlertContent extends StatefulWidget {
ThreeCheckboxAlertContent({
required this.checkbox1,
required this.toggleCheckbox1,
required this.checkbox2,
required this.toggleCheckbox2,
required this.checkbox3,
required this.toggleCheckbox3,
Key? key,
}) : super(key: key);
bool checkbox1;
void Function() toggleCheckbox1;
bool checkbox2;
void Function() toggleCheckbox2;
bool checkbox3;
void Function() toggleCheckbox3;
@override
_ThreeCheckboxAlertContentState createState() => _ThreeCheckboxAlertContentState(
checkbox1: checkbox1,
toggleCheckbox1: toggleCheckbox1,
checkbox2: checkbox2,
toggleCheckbox2: toggleCheckbox2,
checkbox3: checkbox3,
toggleCheckbox3: toggleCheckbox3,
);
static _ThreeCheckboxAlertContentState? of(BuildContext context) {
return context.findAncestorStateOfType<_ThreeCheckboxAlertContentState>();
}
}
class _ThreeCheckboxAlertContentState extends State<ThreeCheckboxAlertContent> {
_ThreeCheckboxAlertContentState({
required this.checkbox1,
required this.toggleCheckbox1,
required this.checkbox2,
required this.toggleCheckbox2,
required this.checkbox3,
required this.toggleCheckbox3,
});
bool checkbox1;
void Function() toggleCheckbox1;
bool checkbox2;
void Function() toggleCheckbox2;
bool checkbox3;
void Function() toggleCheckbox3;
bool showValidationMessage = true;
bool get areAllCheckboxesChecked => checkbox1 && checkbox2 && checkbox3;
@override
Widget build(BuildContext context) {
return Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
StandardCheckbox(
value: checkbox1,
caption: S.of(context).cakepay_confirm_no_vpn,
onChanged: (bool? value) {
setState(() {
checkbox1 = value ?? false;
toggleCheckbox1();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
StandardCheckbox(
value: checkbox2,
caption: S.of(context).cakepay_confirm_voided_refund,
onChanged: (bool? value) {
setState(() {
checkbox2 = value ?? false;
toggleCheckbox2();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
StandardCheckbox(
value: checkbox3,
caption: S.of(context).cakepay_confirm_terms_agreed,
onChanged: (bool? value) {
setState(() {
checkbox3 = value ?? false;
toggleCheckbox3();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => launchUrl(
Uri.parse("https://www.cakepay.com/terms/"),
mode: LaunchMode.externalApplication,
),
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
S.of(context).settings_terms_and_conditions,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.none,
height: 1,
),
softWrap: true,
),
),
),
if (showValidationMessage)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Please confirm all checkboxes',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.errorContainer,
decoration: TextDecoration.none,
),
),
),
],
),
);
}
}
@@ -1,86 +0,0 @@
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:flutter/material.dart';
class CakePayAlertModal extends StatelessWidget {
const CakePayAlertModal({
Key? key,
required this.title,
required this.content,
required this.actionTitle,
this.heightFactor = 0.4,
this.showCloseButton = true,
}) : super(key: key);
final String title;
final Widget content;
final String actionTitle;
final bool showCloseButton;
final double heightFactor;
@override
Widget build(BuildContext context) {
return AlertBackground(
child: Material(
color: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Spacer(),
Container(
padding: EdgeInsets.only(top: 24, left: 24, right: 24),
margin: EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(30),
),
child: Column(
children: [
if (title.isNotEmpty)
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
Container(
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * heightFactor),
child: ListView(
children: [
content,
SizedBox(height: 35),
],
),
),
PrimaryButton(
onPressed: () => Navigator.pop(context),
text: actionTitle,
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.primary,
),
SizedBox(height: 21),
],
),
),
Spacer(),
if(showCloseButton)
InkWell(
onTap: () => Navigator.pop(context),
child: Container(
margin: EdgeInsets.only(bottom: 40),
child: CircleAvatar(
child: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurface,
),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
),
),
)
],
),
),
);
}
}
@@ -1,11 +0,0 @@
import 'package:flutter/material.dart';
class CardMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
);
}
}
@@ -78,11 +78,14 @@ class _FilterWidgetState extends State<FilterWidget> {
children: [
Padding(
padding: const EdgeInsets.all(24.0),
child: Text(
S.of(context).filter_by,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
child: Align(
alignment: Alignment.center,
child: Text(
S.of(context).filter_by,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
),
),
@@ -105,7 +108,7 @@ class _FilterWidgetState extends State<FilterWidget> {
final Widget sectionListView = ListView.builder(
controller: isSectionScrollable ? _scrollController : null,
padding: const EdgeInsets.symmetric(horizontal: 28.0),
padding: const EdgeInsets.symmetric(horizontal: 28.0, vertical: 8.0),
shrinkWrap: isSectionScrollable ? false : true,
physics: isSectionScrollable
? const BouncingScrollPhysics()
@@ -1,5 +1,5 @@
import 'package:cake_wallet/src/widgets/alert_close_button.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/rounded_checkbox.dart';
import 'package:cake_wallet/src/widgets/rounded_checkbox.dart';
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
@@ -3,6 +3,7 @@ import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
@@ -274,6 +275,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
return ConfirmSendingBottomSheet(
key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'),
currentTheme: widget.currentTheme,
footerType: FooterType.slideActionButton,
walletType: widget.exchangeTradeViewModel.sendViewModel.walletType,
titleText: S.of(bottomSheetContext).confirm_transaction,
titleIconPath:
@@ -293,7 +295,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel
.pendingTransactionFeeFiatAmountFormatted,
outputs: widget.exchangeTradeViewModel.sendViewModel.outputs,
onSlideComplete: () async {
onSlideActionComplete: () async {
if (bottomSheetContext.mounted) {
Navigator.of(bottomSheetContext).pop();
}
@@ -317,11 +319,12 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
builder: (BuildContext bottomSheetContext) {
return InfoBottomSheet(
currentTheme: widget.currentTheme,
footerType: FooterType.singleActionButton,
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/birthday_cake.png',
actionButtonText: S.of(bottomSheetContext).close,
actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
actionButton: () {
singleActionButtonText: S.of(bottomSheetContext).close,
singleActionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
onSingleActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
if (mounted) {
Navigator.of(context).pushNamedAndRemoveUntil(
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/screens/integrations/deuro/widgets/interest_card
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart';
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
@@ -166,6 +167,7 @@ class DEuroSavingsPage extends BasePage {
isScrollControlled: true,
builder: (BuildContext bottomSheetContext) => ConfirmSendingBottomSheet(
key: ValueKey('savings_page_confirm_sending_dialog_key'),
footerType: FooterType.slideActionButton,
titleText: title,
currentTheme: currentTheme,
walletType: WalletType.ethereum,
@@ -182,7 +184,7 @@ class DEuroSavingsPage extends BasePage {
feeValue: tx.feeFormatted,
feeFiatAmount: _dEuroViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: [],
onSlideComplete: () async {
onSlideActionComplete: () async {
Navigator.of(bottomSheetContext).pop(true);
dEuroViewModel.commitTransaction();
},
@@ -201,6 +203,7 @@ class DEuroSavingsPage extends BasePage {
isScrollControlled: true,
builder: (BuildContext bottomSheetContext) => ConfirmSendingBottomSheet(
key: ValueKey('savings_page_confirm_approval_dialog_key'),
footerType: FooterType.slideActionButton,
titleText: S.of(bottomSheetContext).approve_tokens,
currentTheme: currentTheme,
walletType: WalletType.ethereum,
@@ -213,7 +216,7 @@ class DEuroSavingsPage extends BasePage {
feeValue: tx.feeFormatted,
feeFiatAmount: "",
outputs: [],
onSlideComplete: () {
onSlideActionComplete: () {
Navigator.of(bottomSheetContext).pop(true);
dEuroViewModel.commitApprovalTransaction();
},
@@ -234,13 +237,14 @@ class DEuroSavingsPage extends BasePage {
context: context,
isDismissible: false,
builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
footerType: FooterType.singleActionButton,
currentTheme: currentTheme,
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/birthday_cake.png',
content: S.of(bottomSheetContext).deuro_tx_commited_content,
actionButtonText: S.of(bottomSheetContext).close,
actionButtonKey: ValueKey('savings_page_sent_dialog_ok_button_key'),
actionButton: () => Navigator.of(bottomSheetContext).pop(),
singleActionButtonText: S.of(bottomSheetContext).close,
singleActionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
),
);
});
@@ -302,13 +306,13 @@ class DEuroSavingsPage extends BasePage {
titleText: title,
titleIconPath: CryptoCurrency.deuro.iconPath,
content: content,
isTwoAction: true,
rightButtonText: S.of(context).close,
footerType: FooterType.doubleActionButton,
doubleActionRightButtonText: S.of(context).close,
rightActionButtonKey: ValueKey('deuro_page_tooltip_dialog_${key}_ok_button_key'),
actionRightButton: () => Navigator.of(bottomSheetContext).pop(),
leftButtonText: S.of(context).learn_more,
onRightActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
doubleActionLeftButtonText: S.of(context).learn_more,
leftActionButtonKey: ValueKey('deuro_page_tooltip_dialog_${key}_learn_more_button_key'),
actionLeftButton: onLearnMorePressed,
onLeftActionButtonPressed: onLearnMorePressed,
),
);
}
@@ -327,13 +331,13 @@ class DEuroSavingsPage extends BasePage {
contentImage: 'assets/images/deuro_hero.png',
contentImageSize: 200,
content: S.of(context).deuro_savings_welcome_description,
isTwoAction: true,
rightButtonText: S.of(context).close,
footerType: FooterType.doubleActionButton,
doubleActionRightButtonText: S.of(context).close,
rightActionButtonKey: ValueKey('deuro_page_tooltip_dialog_welcome_ok_button_key'),
actionRightButton: () => Navigator.of(bottomSheetContext).pop(),
leftButtonText: S.of(context).learn_more,
onRightActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
doubleActionLeftButtonText: S.of(context).learn_more,
leftActionButtonKey: ValueKey('deuro_page_tooltip_dialog_welcome_learn_more_button_key'),
actionLeftButton: () => launchUrlString("https://deuro.com/what-is-deuro.html"),
onLeftActionButtonPressed: () => launchUrlString("https://deuro.com/what-is-deuro.html"),
showDisclaimerText: _dEuroViewModel.isFistTime,
),
);
@@ -352,9 +356,9 @@ class DEuroSavingsPage extends BasePage {
titleIconPath: CryptoCurrency.deuro.iconPath,
contentImage: 'assets/images/deuro_not_enough_eth.png',
content: S.of(context).deuro_tooltip_no_eth,
actionButtonKey: ValueKey('deuro_page_tooltip_dialog_no_eth_ok_button_key'),
actionButtonText: S.of(context).close,
actionButton: () => Navigator.of(bottomSheetContext).pop(),
singleActionButtonKey: ValueKey('deuro_page_tooltip_dialog_no_eth_ok_button_key'),
singleActionButtonText: S.of(context).close,
onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(), footerType: FooterType.singleActionButton,
),
);
}
@@ -372,6 +376,8 @@ class DEuroSavingsPage extends BasePage {
? S.of(context).deuro_savings_available_to_add
: S.of(context).deuro_savings_available_to_remove,
balance: isAdding ? _dEuroViewModel.accountBalance : _dEuroViewModel.savingsBalance,
footerType: FooterType.none,
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
);
}
@@ -14,18 +14,18 @@ class SavingsEditSheet extends BaseBottomSheet {
super.titleIconPath,
this.balance,
this.balanceTitle,
required super.footerType, required super.maxHeight,
});
@override
Widget contentWidget(BuildContext context) => SizedBox(
height: 600,
height: 500,
child: _SavingsEditBody(
balance: balance,
balanceTitle: balanceTitle,
),
);
@override
Widget footerWidget(BuildContext context) => SizedBox.shrink();
}
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dar
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/brightness_util.dart';
@@ -319,14 +320,13 @@ class QRWidget extends StatelessWidget {
titleText: S.of(context).payjoin_unavailable_sheet_title,
content: S.of(context).payjoin_unavailable_sheet_content,
currentTheme: currentTheme,
isTwoAction: true,
leftButtonText: S.of(context).learn_more,
actionLeftButton: () => launchUrl(
Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin/#payjoin"),
mode: LaunchMode.externalApplication,
),
rightButtonText: S.of(context).ok,
actionRightButton: () => Navigator.of(context).pop(),
footerType: FooterType.doubleActionButton,
doubleActionLeftButtonText: S.of(context).learn_more,
onLeftActionButtonPressed: () => launchUrl(
Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin/#payjoin"),
mode: LaunchMode.externalApplication),
doubleActionRightButtonText: S.of(context).ok,
onRightActionButtonPressed: () => Navigator.of(context).pop(),
),
);
}
+66 -42
View File
@@ -17,12 +17,14 @@ import 'package:cake_wallet/src/widgets/adaptable_page_view.dart';
import 'package:cake_wallet/src/widgets/add_template_button.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/simple_checkbox.dart';
import 'package:cake_wallet/src/widgets/template_tile.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/utils/payment_request.dart';
@@ -569,6 +571,7 @@ class SendPage extends BasePage {
key: ValueKey('send_page_confirm_sending_dialog_key'),
titleText: S.of(bottomSheetContext).confirm_transaction,
currentTheme: currentTheme,
footerType: FooterType.slideActionButton,
walletType: sendViewModel.walletType,
titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath,
currency: sendViewModel.selectedCryptoCurrency,
@@ -581,7 +584,7 @@ class SendPage extends BasePage {
feeValue: sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: sendViewModel.outputs,
onSlideComplete: () async {
onSlideActionComplete: () async {
Navigator.of(bottomSheetContext).pop(true);
sendViewModel.commitTransaction(context);
},
@@ -611,48 +614,69 @@ class SendPage extends BasePage {
bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup);
await showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (BuildContext bottomSheetContext) {
return showContactSheet && sendViewModel.ocpRequest == null
? InfoBottomSheet(
currentTheme: currentTheme,
showDontAskMeCheckbox: true,
onCheckboxChanged: (value) => sendViewModel.setShowAddressBookPopup(!value),
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/contact.png',
contentImageColor: Theme.of(context).colorScheme.onSurface,
content: S.of(bottomSheetContext).add_contact_to_address_book,
isTwoAction: true,
leftButtonText: 'No',
rightButtonText: 'Yes',
actionLeftButton: () {
Navigator.of(bottomSheetContext).pop();
if (context.mounted) {
Navigator.of(context)
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
}
RequestReviewHandler.requestReview();
newContactAddress = null;
},
actionRightButton: () {
Navigator.of(bottomSheetContext).pop();
RequestReviewHandler.requestReview();
if (context.mounted) {
Navigator.of(context).pushNamed(Routes.addressBookAddContact,
arguments: newContactAddress);
}
newContactAddress = null;
},
)
await showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (BuildContext bottomSheetContext) {
return showContactSheet && sendViewModel.ocpRequest == null
? InfoBottomSheet(
currentTheme: currentTheme,
footerType: FooterType.doubleActionButton,
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/contact.png',
contentImageColor: Theme.of(context).colorScheme.onSurface,
content: S.of(bottomSheetContext).add_contact_to_address_book,
bottomActionPanel: Padding(
padding: const EdgeInsets.only(left: 34.0),
child: Row(
children: [
SimpleCheckbox(
onChanged: (value) =>
sendViewModel.setShowAddressBookPopup(!value)),
const SizedBox(width: 8),
Text(
'Don’t ask me next time',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.titleLarge!.color,
decoration: TextDecoration.none,
),
),
],
),
),
doubleActionLeftButtonText: 'No',
doubleActionRightButtonText: 'Yes',
onLeftActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
if (context.mounted) {
Navigator.of(context)
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
}
RequestReviewHandler.requestReview();
newContactAddress = null;
},
onRightActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
RequestReviewHandler.requestReview();
if (context.mounted) {
Navigator.of(context).pushNamed(Routes.addressBookAddContact,
arguments: newContactAddress);
}
newContactAddress = null;
},
)
: InfoBottomSheet(
currentTheme: currentTheme,
footerType: FooterType.singleActionButton,
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/birthday_cake.png',
actionButtonText: S.of(bottomSheetContext).close,
actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
actionButton: () {
singleActionButtonText: S.of(bottomSheetContext).close,
singleActionButtonKey: ValueKey('send_page_transaction_sent_button_key'),
onSingleActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
Future.delayed(Duration.zero, () {
if (context.mounted) {
@@ -710,14 +734,14 @@ class SendPage extends BasePage {
dialogContext = context;
return InfoBottomSheet(
currentTheme: currentTheme,
footerType: FooterType.singleActionButton,
titleText: S.of(context).proceed_on_device,
contentImage:
'assets/images/hardware_wallet/ledger_nano_x.png',
contentImageColor: Theme.of(context).colorScheme.onSurface,
content: S.of(context).proceed_on_device_description,
isTwoAction: false,
actionButtonText: S.of(context).cancel,
actionButton: () {
singleActionButtonText: S.of(context).cancel,
onSingleActionButtonPressed: () {
sendViewModel.state = InitialExecutionState();
Navigator.of(context).pop();
},
@@ -8,6 +8,7 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_expandab
import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
@@ -200,7 +201,9 @@ class RBFDetailsPage extends BasePage {
feeValue: transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: transactionDetailsViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: transactionDetailsViewModel.sendViewModel.outputs,
onSlideComplete: () async {
footerType: FooterType.slideActionButton,
accessibleNavigationModeSlideActionButtonText: S.of(context).send,
onSlideActionComplete: () async {
Navigator.of(bottomSheetContext).pop();
await transactionDetailsViewModel.sendViewModel.commitTransaction(context);
try {
+14 -11
View File
@@ -3,28 +3,31 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:flutter/material.dart';
class AlertBackground extends StatelessWidget {
AlertBackground({required this.child});
const AlertBackground({Key? key, required this.child, this.dismissible = false});
final Widget child;
final bool dismissible;
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.transparent,
body: Container(
height: double.infinity,
width: double.infinity,
color: Colors.transparent,
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: dismissible ? () => Navigator.of(context).pop() : null,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface.withOpacity(0.8)),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface.withAlpha(200)),
child: Center(
child: Container(
width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
child: child,
child: GestureDetector(
onTap: () {},
child: ConstrainedBox(
constraints:
BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
child: child,
),
),
),
),
+10 -1
View File
@@ -43,7 +43,9 @@ class BaseTextFormField extends StatelessWidget {
this.onFieldSubmitted,
this.hasUnderlineBorder = false,
this.borderWidth = 1.0,
super.key,
this.prefixIconConstraints,
this.suffixIconConstraints,
super.key, this.suffixText,
});
final TextEditingController? controller;
@@ -70,6 +72,9 @@ class BaseTextFormField extends StatelessWidget {
final bool readOnly;
final bool? enableInteractiveSelection;
final String? initialValue;
final BoxConstraints? prefixIconConstraints;
final BoxConstraints? suffixIconConstraints;
final String? suffixText;
final void Function(String)? onSubmit;
final bool obscureText;
final bool? autofocus;
@@ -120,8 +125,12 @@ class BaseTextFormField extends StatelessWidget {
alignLabelWithHint: alignLabelWithHint,
contentPadding: contentPadding,
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIconConstraints: prefixIconConstraints ??
const BoxConstraints(minWidth: 0, minHeight: 0),
prefix: prefix,
prefixIcon: prefixIcon,
suffixIconConstraints: suffixIconConstraints ??
const BoxConstraints(minWidth: 0, minHeight: 0),
suffix: suffix,
suffixIcon: suffixIcon,
filled: !hasUnderlineBorder,
@@ -1,62 +1,53 @@
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:flutter/material.dart';
enum FooterType { none, slideActionButton, singleActionButton, doubleActionButton }
abstract class BaseBottomSheet extends StatelessWidget {
const BaseBottomSheet({
super.key,
required this.titleText,
this.titleIconPath,
required this.footerType,
this.currentTheme,
this.slideActionButtonText,
this.onSlideActionComplete,
this.singleActionButtonText,
this.accessibleNavigationModeSlideActionButtonText,
this.onSingleActionButtonPressed,
this.singleActionButtonKey,
this.doubleActionLeftButtonText,
this.doubleActionRightButtonText,
this.onLeftActionButtonPressed,
this.onRightActionButtonPressed,
this.leftActionButtonKey,
this.rightActionButtonKey,
required this.maxHeight,
}) : assert(footerType == FooterType.none || currentTheme != null,
'currentTheme is required unless footerType is none');
final String titleText;
final String? titleIconPath;
final MaterialThemeBase? currentTheme;
final FooterType footerType;
final String? slideActionButtonText;
final VoidCallback? onSlideActionComplete;
final String? singleActionButtonText;
final String? accessibleNavigationModeSlideActionButtonText;
final VoidCallback? onSingleActionButtonPressed;
final Key? singleActionButtonKey;
final String? doubleActionLeftButtonText;
final String? doubleActionRightButtonText;
final VoidCallback? onLeftActionButtonPressed;
final VoidCallback? onRightActionButtonPressed;
final Key? leftActionButtonKey;
final Key? rightActionButtonKey;
final double maxHeight;
const BaseBottomSheet({required this.titleText, this.titleIconPath, this.maxHeight = 900});
Widget headerWidget(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
const Spacer(flex: 4),
Expanded(
flex: 2,
child: Container(
height: 6,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).colorScheme.onSurface,
),
),
),
const Spacer(flex: 4),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (titleIconPath != null)
Image.asset(titleIconPath!, height: 24, width: 24, excludeFromSemantics: true)
else
Container(),
const SizedBox(width: 6),
Text(
titleText,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
fontSize: 20,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
],
),
const SizedBox(height: 24),
],
);
}
Widget contentWidget(BuildContext context);
Widget footerWidget(BuildContext context);
@override
Widget build(BuildContext context) {
return ConstrainedBox(
@@ -69,13 +60,106 @@ abstract class BaseBottomSheet extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
headerWidget(context),
_buildHeader(context),
contentWidget(context),
footerWidget(context),
_buildFooter(context),
],
),
),
),
);
}
Widget _buildHeader(BuildContext context) => Column(
children: [
const SizedBox(height: 12),
Container(
width: 64,
height: 5,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (titleIconPath != null) ...[
Image.asset(titleIconPath!, height: 24, width: 24),
const SizedBox(width: 6),
],
Text(
titleText,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
fontSize: 20,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
],
),
const SizedBox(height: 13),
],
);
Widget _buildFooter(BuildContext context) {
switch (footerType) {
case FooterType.none:
return const SizedBox.shrink();
case FooterType.slideActionButton:
return Padding(
padding: const EdgeInsets.fromLTRB(40, 12, 40, 34),
child: StandardSlideButton(
buttonText: slideActionButtonText ?? '',
onSlideComplete: onSlideActionComplete ?? () {},
currentTheme: currentTheme!,
accessibleNavigationModeButtonText: accessibleNavigationModeSlideActionButtonText ?? '',
),
);
case FooterType.singleActionButton:
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 34),
child: LoadingPrimaryButton(
key: singleActionButtonKey,
text: singleActionButtonText ?? '',
onPressed: onSingleActionButtonPressed ?? () {},
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: false,
isDisabled: false,
),
);
case FooterType.doubleActionButton:
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 34),
child: Row(
children: [
Expanded(
child: PrimaryButton(
key: leftActionButtonKey,
text: doubleActionLeftButtonText ?? '',
onPressed: onLeftActionButtonPressed,
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.onSecondaryContainer
),
),
const SizedBox(width: 12),
Expanded(
child: PrimaryButton(
key: rightActionButtonKey,
text: doubleActionRightButtonText ?? '',
onPressed: onRightActionButtonPressed,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
],
),
);
}
}
}
@@ -0,0 +1,352 @@
import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
import 'package:cake_wallet/cake_pay/src/widgets/flip_card_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
import 'base_bottom_sheet_widget.dart';
class CakePayCardInfoBottomSheet extends BaseBottomSheet {
CakePayCardInfoBottomSheet({
required String titleText,
required MaterialThemeBase currentTheme,
required FooterType footerType,
String? titleIconPath,
String? singleActionButtonText,
VoidCallback? onSingleActionButtonPressed,
Key? singleActionButtonKey,
String? doubleActionLeftButtonText,
String? doubleActionRightButtonText,
VoidCallback? onLeftActionButtonPressed,
VoidCallback? onRightActionButtonPressed,
Key? rightActionButtonKey,
Key? leftActionButtonKey,
required this.onUpdateBalancePressed,
required this.isReloadable,
required this.balance,
this.contentImage,
this.howToUse,
this.applyBoxShadow = false,
Key? key,
}) : _currentTheme = currentTheme,
super(
titleText: titleText,
maxHeight: 900,
titleIconPath: titleIconPath,
currentTheme: currentTheme,
footerType: footerType,
singleActionButtonText: singleActionButtonText,
onSingleActionButtonPressed: onSingleActionButtonPressed,
singleActionButtonKey: singleActionButtonKey,
doubleActionLeftButtonText: doubleActionLeftButtonText,
doubleActionRightButtonText: doubleActionRightButtonText,
onLeftActionButtonPressed: onLeftActionButtonPressed,
onRightActionButtonPressed: onRightActionButtonPressed,
leftActionButtonKey: leftActionButtonKey,
rightActionButtonKey: rightActionButtonKey,
key: key);
final VoidCallback onUpdateBalancePressed;
final MaterialThemeBase _currentTheme;
final String? contentImage;
final String? howToUse;
final bool isReloadable;
final String balance;
final bool applyBoxShadow;
final _cardKey = GlobalKey<FlipCardState>();
@override
Widget contentWidget(BuildContext context) {
final itemTitleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
);
final itemSubTitleTextStyle = Theme.of(context).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurfaceVariant,
decoration: TextDecoration.none,
);
final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (contentImage != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 16),
child: AspectRatio(
aspectRatio: 1.6,
child: FlipCard(
key: _cardKey,
flipOnTouch: true,
front: _buildCardImage(context, contentImage!, applyBoxShadow),
back: _buildBarcodeSide(context,
cardNumber: '6006491979836784204', pin: '4782'),
),
),
),
Positioned(
bottom: 0,
right: 0,
child: GestureDetector(
onTap: () => _cardKey.currentState?.toggleCard(),
child: Container(
height: 43,
width: 43,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 1),
color: Colors.white.withAlpha(75),
shape: BoxShape.circle,
),
child: Transform.scale(
scale: .8,
child: const ImageIcon(
AssetImage('assets/images/transfer.png'),
color: Colors.white,
),
)),
),
),
],
),
),
Container(),
Text(
'Tap card to show details',
style: itemTitleTextStyle.copyWith(fontSize: 12, fontWeight: FontWeight.w700),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
children: [
const SizedBox(height: 34),
CakePayInfoTile(
isReloadable: isReloadable,
itemValue: balance,
itemTitleTextStyle: itemTitleTextStyle,
itemSubTitleTextStyle: itemSubTitleTextStyle,
tileBackgroundColor: tileBackgroundColor),
const SizedBox(height: 8),
_HowToUseTile(howToUse: howToUse ?? '', tileBackgroundColor: tileBackgroundColor),
const SizedBox(height: 40),
],
),
),
],
);
}
}
class _HowToUseTile extends StatelessWidget {
const _HowToUseTile({required this.howToUse, required this.tileBackgroundColor});
final String howToUse;
final Color tileBackgroundColor;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => _showHowToUseCard(context: context, howToUse: howToUse),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).how_to_use_card,
style: Theme.of(context).textTheme.bodyLarge),
Icon(
Icons.chevron_right_rounded,
color: Theme.of(context).textTheme.titleLarge!.color!,
),
],
)),
);
}
}
class CakePayInfoTile extends StatelessWidget {
const CakePayInfoTile({
super.key,
required this.isReloadable,
required this.itemValue,
required this.itemTitleTextStyle,
this.itemSubTitle,
required this.itemSubTitleTextStyle,
required this.tileBackgroundColor,
});
final bool isReloadable;
final String itemValue;
final TextStyle itemTitleTextStyle;
final String? itemSubTitle;
final TextStyle itemSubTitleTextStyle;
final Color tileBackgroundColor;
@override
Widget build(BuildContext context) {
return Semantics(
container: true,
label: isReloadable ? 'Balance' : 'Total Value',
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(isReloadable ? 'Balance' : 'Total Value', style: itemTitleTextStyle),
Text(itemValue,
style: itemTitleTextStyle.copyWith(fontSize: 18, fontWeight: FontWeight.w600)),
],
),
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Text(isReloadable ? 'Top Up Balance' : 'Manually update Balance',
style: itemTitleTextStyle.copyWith(fontSize: 12, fontWeight: FontWeight.w600)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
color: Theme.of(context).dialogBackgroundColor)),
const SizedBox(height: 4),
],
),
),
);
}
}
void _showHowToUseCard({required BuildContext context, String? howToUse}) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return CakePayAlertModal(
title: S.of(context).how_to_use_card,
content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
ClickableLinksText(
text: howToUse ?? '',
textStyle: Theme.of(context).textTheme.bodyMedium!,
linkStyle: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color!,
fontSize: 18,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.w400,
),
),
]),
actionTitle: S.current.got_it,
);
});
}
Widget _buildCardImage(BuildContext ctx, String path, bool addShadow) {
final border = BorderRadius.circular(10);
return Container(
decoration: addShadow
? BoxDecoration(
borderRadius: border,
boxShadow: [
BoxShadow(color: Colors.black.withAlpha(150), blurRadius: 5)
],
)
: null,
child: ClipRRect(
borderRadius: border,
child: Stack(
fit: StackFit.expand,
children: [
Container(color: Theme.of(ctx).cardColor.withAlpha(200)),
ImageUtil.getImageFromPath(
imagePath: path,
fit: BoxFit.cover,
),
],
),
),
);
}
Widget _buildBarcodeSide(BuildContext context, {required String cardNumber, required String pin}) =>
SizedBox.expand(
child: Container(
padding: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 34),
decoration: BoxDecoration(
color: Theme.of(context).cardColor.withAlpha(200),
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(color: Colors.black.withAlpha(150), blurRadius: 5)
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
color: Theme.of(context).textTheme.titleLarge!.color!.withOpacity(.1),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Gift Card Number',
style: TextStyle(
fontFamily: 'Lato',
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(207, 207, 207, 1))),
const SizedBox(height: 4),
Text(cardNumber,
style: TextStyle(
fontFamily: 'Lato',
fontSize: 12,
fontWeight: FontWeight.w900,
color: Color.fromRGBO(146, 146, 146, 1))),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('PIN Number',
style: TextStyle(
fontFamily: 'Lato',
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(207, 207, 207, 1))),
const SizedBox(height: 4),
Text(pin,
style: TextStyle(
fontFamily: 'Lato',
fontSize: 12,
fontWeight: FontWeight.w900,
color: Color.fromRGBO(146, 146, 146, 1))),
],
),
],
),
],
),
),
);
@@ -0,0 +1,239 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
class CakePayTransactionSentBottomSheet extends StatelessWidget {
const CakePayTransactionSentBottomSheet({
super.key,
required this.titleText,
required this.currency,
required this.amount,
required this.amountValue,
required this.fiatAmountValue,
required this.output,
required this.fee,
required this.feeValue,
required this.feeFiatAmount,
this.titleIconWidget,
required this.quantity,
required this.onClose,
required this.paymentId,
required this.paymentIdValue,
});
final String titleText;
final Widget? titleIconWidget;
final CryptoCurrency currency;
final String amount;
final String amountValue;
final String fiatAmountValue;
final Output output;
final String fee;
final String feeValue;
final String feeFiatAmount;
final String quantity;
final VoidCallback onClose;
final String paymentId;
final String paymentIdValue;
TextStyle _titleStyle(BuildContext ctx) => Theme.of(ctx).textTheme.bodyLarge!;
TextStyle _valueStyle(BuildContext ctx) => Theme.of(ctx).textTheme.titleMedium!.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
);
TextStyle _subStyle(BuildContext ctx) => Theme.of(ctx).textTheme.labelSmall!.copyWith(
color: Theme.of(ctx).colorScheme.onSurfaceVariant,
);
Widget _buildHeader(BuildContext ctx) => Column(
children: [
const SizedBox(height: 12),
Container(
width: 64,
height: 5,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Theme.of(ctx).colorScheme.onSurface,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (titleIconWidget != null) titleIconWidget!,
const SizedBox(width: 6),
Text(
titleText,
style: Theme.of(ctx).textTheme.titleLarge!.copyWith(
fontSize: 20,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
],
),
const SizedBox(height: 12),
],
);
Widget _buildBody(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
children: [
_StandardTile(
itemTitle: amount,
titleStyle: _titleStyle(context),
itemValue: '$amountValue ${currency.title}',
itemValueStyle: _valueStyle(context),
itemSubTitle: fiatAmountValue,
itemSubTitleStyle: _subStyle(context),
),
const SizedBox(height: 8),
_StandardTile(
itemTitle: fee,
titleStyle: _titleStyle(context),
itemValue: feeValue,
itemValueStyle: _valueStyle(context),
itemSubTitle: feeFiatAmount,
itemSubTitleStyle: _subStyle(context),
),
const SizedBox(height: 8),
_StandardTile(
itemTitle: output.parsedAddress.profileName,
titleStyle: _titleStyle(context),
itemValue: output.fiatAmount,
itemValueStyle: _valueStyle(context),
itemSubTitle: quantity,
itemSubTitleStyle: _subStyle(context),
imagePath: output.parsedAddress.profileImageUrl,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Column(
children: [
Text(paymentId + ': ' + paymentIdValue,
style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center),
const SizedBox(height: 18),
Image.asset('assets/images/envelope.png'),
const SizedBox(height: 18),
Text(
S.of(context).cake_pay_card_email_delivered_message,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
PrimaryButton(
text: S.of(context).close,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimaryContainer,
onPressed: onClose,
),
],
),
),
],
),
);
@override
Widget build(BuildContext context) {
final maxHeight = MediaQuery.of(context).size.height * 0.9;
return ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: Material(
color: Theme.of(context).colorScheme.surface,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildHeader(context),
_buildBody(context),
],
),
),
),
),
);
}
}
class _StandardTile extends StatelessWidget {
const _StandardTile({
required this.itemTitle,
required this.titleStyle,
required this.itemValue,
required this.itemValueStyle,
this.itemSubTitle,
this.itemSubTitleStyle,
this.imagePath,
});
final String itemTitle;
final TextStyle titleStyle;
final String itemValue;
final TextStyle itemValueStyle;
final String? itemSubTitle;
final TextStyle? itemSubTitleStyle;
final String? imagePath;
@override
Widget build(BuildContext context) {
return Semantics(
container: true,
label: itemTitle,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).colorScheme.surfaceContainerLowest.withAlpha(80)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
if (imagePath != null)
Padding(
padding: const EdgeInsets.only(right: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: ImageUtil.getImageFromPath(
imagePath: imagePath!, height: 40, width: 40),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
itemTitle,
style: titleStyle,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
)),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(itemValue, style: itemValueStyle.copyWith(height: 1.0)),
if (itemSubTitle != null) Text(itemSubTitle!, style: itemSubTitleStyle),
],
),
],
),
),
);
}
}
@@ -1,39 +1,29 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'base_bottom_sheet_widget.dart';
class ConfirmSendingBottomSheet extends BaseBottomSheet {
final CryptoCurrency currency;
final MaterialThemeBase currentTheme;
final String? paymentId;
final String? paymentIdValue;
final String? expirationTime;
final String amount;
final String amountValue;
final String fiatAmountValue;
final String fee;
final String feeValue;
final String feeFiatAmount;
final String? explanation;
final List<Output> outputs;
final VoidCallback onSlideComplete;
final WalletType walletType;
final PendingChange? change;
final bool isOpenCryptoPay;
ConfirmSendingBottomSheet({
required String titleText,
required MaterialThemeBase currentTheme,
required FooterType footerType,
String? titleIconPath,
String? slideActionButtonText,
VoidCallback? onSlideActionComplete,
String? accessibleNavigationModeSlideActionButtonText,
required this.currency,
required this.currentTheme,
this.paymentId,
this.paymentIdValue,
this.expirationTime,
@@ -44,14 +34,45 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
required this.feeValue,
required this.feeFiatAmount,
required this.outputs,
required this.onSlideComplete,
required this.walletType,
this.change,
this.explanation,
this.isOpenCryptoPay = false,
this.cakePayBuyCardViewModel,
this.quantity,
Key? key,
}) : showScrollbar = outputs.length > 3,
super(titleText: titleText, titleIconPath: titleIconPath);
_currentTheme = currentTheme,
super(
titleText: titleText,
maxHeight: 900,
titleIconPath: titleIconPath,
currentTheme: currentTheme,
footerType: footerType,
slideActionButtonText: slideActionButtonText ?? 'Swipe to send',
onSlideActionComplete: onSlideActionComplete,
accessibleNavigationModeSlideActionButtonText:
accessibleNavigationModeSlideActionButtonText,
key: key);
final CryptoCurrency currency;
final MaterialThemeBase _currentTheme;
final String? paymentId;
final String? paymentIdValue;
final String? expirationTime;
final String amount;
final String amountValue;
final String fiatAmountValue;
final String fee;
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
final WalletType walletType;
final PendingChange? change;
final bool isOpenCryptoPay;
final CakePayBuyCardViewModel? cakePayBuyCardViewModel;
final String? quantity;
final String? explanation;
final bool showScrollbar;
final ScrollController scrollController = ScrollController();
@@ -69,25 +90,32 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
decoration: TextDecoration.none,
);
final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
final tileBackgroundColor = _currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark.withAlpha(140)
: CustomThemeColors.cardGradientColorPrimaryLight;
Widget content = Padding(
padding: EdgeInsets.fromLTRB(8, 0, showScrollbar ? 16 : 8, 8),
child: Column(
children: [
if (paymentId != null && paymentIdValue != null)
if (paymentId != null && paymentIdValue != null && cakePayBuyCardViewModel != null)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: AddressTile(
itemTitle: paymentId!,
itemTitleTextStyle: itemTitleTextStyle,
walletType: walletType,
isBatchSending: false,
amount: '',
address: paymentIdValue!,
itemSubTitleTextStyle: itemSubTitleTextStyle,
tileBackgroundColor: tileBackgroundColor,
),
child: Observer(
builder: (_) => AddressTile(
itemTitle: paymentId!,
itemTitleTextStyle: itemTitleTextStyle,
amountTextStyle: itemSubTitleTextStyle,
walletType: walletType,
amount: expirationTime != null
? S.current.offer_expires_in +
' ${cakePayBuyCardViewModel!.formattedRemainingTime}'
: null,
address: paymentIdValue!,
itemSubTitleTextStyle: itemSubTitleTextStyle,
tileBackgroundColor: tileBackgroundColor,
applyAddressFormatting: false,
)),
),
if (explanation != null)
Padding(
@@ -128,12 +156,13 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
final bool isBatchSending = outputs.length > 1;
final item = outputs[index];
final contactName = item.parsedAddress.name;
final isCakePayName = contactName == 'Cake Pay';
final batchContactTitle =
'${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}';
final _address = item.isParsedAddress ? item.extractedAddress : item.address;
final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}';
return isBatchSending || contactName.isNotEmpty
? AddressExpansionTile(
return isBatchSending || (contactName.isNotEmpty && !isCakePayName)
? ExpansionAddressTile(
contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
name: isBatchSending ? batchContactTitle : contactName,
address: _address,
@@ -145,12 +174,15 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
tileBackgroundColor: tileBackgroundColor,
)
: AddressTile(
itemTitle: S.of(context).address,
itemTitle: isCakePayName
? item.parsedAddress.profileName
: S.of(context).address,
imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null,
itemTitleTextStyle: itemTitleTextStyle,
isBatchSending: isBatchSending,
walletType: walletType,
amount: _amount,
amount: isCakePayName ? item.fiatAmount : _amount,
address: _address,
itemSubTitle: isCakePayName ? quantity : null,
itemSubTitleTextStyle: itemSubTitleTextStyle,
tileBackgroundColor: tileBackgroundColor,
);
@@ -159,7 +191,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
if (change != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: AddressExpansionTile(
child: ExpansionAddressTile(
contactType: 'Change',
name: S.of(context).send_change_to_you,
address: change!.address,
@@ -195,31 +227,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
return content;
}
}
@override
Widget footerWidget(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(40, 12, 40, 34),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
if (showScrollbar)
BoxShadow(
color: Theme.of(context).colorScheme.outlineVariant,
spreadRadius: 2,
blurRadius: 10,
offset: const Offset(0, 0),
),
],
),
child: StandardSlideButton(
onSlideComplete: onSlideComplete,
buttonText: 'Swipe to send',
currentTheme: currentTheme,
accessibleNavigationModeButtonText: S.of(context).send,
),
);
}
}
class StandardTile extends StatelessWidget {
@@ -304,22 +311,28 @@ class AddressTile extends StatelessWidget {
super.key,
required this.itemTitle,
required this.itemTitleTextStyle,
required this.isBatchSending,
required this.amount,
required this.address,
required this.itemSubTitleTextStyle,
required this.tileBackgroundColor,
required this.walletType,
this.amountTextStyle,
this.applyAddressFormatting = true,
this.imagePath,
this.amount,
this.itemSubTitle,
});
final String itemTitle;
final TextStyle itemTitleTextStyle;
final bool isBatchSending;
final String amount;
final String? amount;
final String address;
final TextStyle itemSubTitleTextStyle;
final TextStyle? amountTextStyle;
final Color tileBackgroundColor;
final WalletType walletType;
final bool applyAddressFormatting;
final String? imagePath;
final String? itemSubTitle;
@override
Widget build(BuildContext context) {
@@ -335,26 +348,65 @@ class AddressTile extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(itemTitle, style: itemTitleTextStyle),
if (isBatchSending) Text(amount, style: itemTitleTextStyle),
Expanded(
child: Row(
children: [
if (imagePath != null)
Padding(
padding: const EdgeInsets.only(right: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: ImageUtil.getImageFromPath(
imagePath: imagePath!, height: 40, width: 40),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
itemTitle,
style: itemTitleTextStyle,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
)),
),
],
),
),
if (amount != null) Text(amount!, style: amountTextStyle ?? itemTitleTextStyle),
],
),
AddressFormatter.buildSegmentedAddress(
address: address,
walletType: walletType,
evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
address.isEmpty
? Container()
: applyAddressFormatting
? AddressFormatter.buildSegmentedAddress(
address: address,
walletType: walletType,
evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
))
: Text(
address,
style: itemTitleTextStyle,
),
itemSubTitle == null
? Container()
: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(itemSubTitle!, style: itemSubTitleTextStyle),
],
),
),
],
),
);
}
}
class AddressExpansionTile extends StatelessWidget {
const AddressExpansionTile({
class ExpansionAddressTile extends StatelessWidget {
const ExpansionAddressTile({
super.key,
required this.contactType,
required this.name,
@@ -425,7 +477,6 @@ class AddressExpansionTile extends StatelessWidget {
walletType: walletType,
evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
),
@@ -1,74 +1,96 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cake_wallet/src/widgets/simple_checkbox.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'base_bottom_sheet_widget.dart';
class LoadingBottomSheet extends BaseBottomSheet {
LoadingBottomSheet({required String titleText, String? titleIconPath})
: super(titleText: titleText, titleIconPath: titleIconPath);
LoadingBottomSheet(
{required String titleText, String? titleIconPath})
: super(titleText: titleText, titleIconPath: titleIconPath, footerType: FooterType.none, maxHeight: 900);
@override
Widget contentWidget(BuildContext context) {
return SizedBox(
height: 200,
height: 300,
child: Center(child: CircularProgressIndicator()),
);
}
@override
Widget footerWidget(BuildContext context) => const SizedBox(height: 94);
}
class InfoBottomSheet extends BaseBottomSheet {
final MaterialThemeBase currentTheme;
final String? contentImage;
final Color? contentImageColor;
final String? content;
final bool isTwoAction;
final bool showDontAskMeCheckbox;
final bool showDisclaimerText;
final Function(bool)? onCheckboxChanged;
final String? actionButtonText;
final VoidCallback? actionButton;
final Key? actionButtonKey;
final String? leftButtonText;
final String? rightButtonText;
final VoidCallback? actionLeftButton;
final VoidCallback? actionRightButton;
final Key? rightActionButtonKey;
final Key? leftActionButtonKey;
final double height;
final double? contentImageSize;
InfoBottomSheet({
required String titleText,
String? titleIconPath,
required this.currentTheme,
required this.footerType,
this.contentImage,
this.contentImageColor,
this.contentImageSize,
this.content,
this.isTwoAction = false,
this.showDontAskMeCheckbox = false,
this.showDisclaimerText = false,
this.height = 200,
this.onCheckboxChanged,
this.actionButtonText,
this.actionButton,
this.actionButtonKey,
this.leftButtonText,
this.rightButtonText,
this.actionLeftButton,
this.actionRightButton,
this.rightActionButtonKey,
this.content,
this.bottomActionPanel,
this.singleActionButtonText,
this.onSingleActionButtonPressed,
this.singleActionButtonKey,
this.doubleActionLeftButtonText,
this.doubleActionRightButtonText,
this.onLeftActionButtonPressed,
this.onRightActionButtonPressed,
this.leftActionButtonKey,
double maxHeight = 900,
}) : super(titleText: titleText, titleIconPath: titleIconPath, maxHeight: maxHeight);
this.rightActionButtonKey,
this.showDisclaimerText = true,
Key? key,
}) : super(
titleText: titleText,
titleIconPath: titleIconPath,
maxHeight: 900,
currentTheme: currentTheme,
footerType: footerType,
singleActionButtonText: singleActionButtonText,
onSingleActionButtonPressed: onSingleActionButtonPressed,
singleActionButtonKey: singleActionButtonKey,
doubleActionLeftButtonText: doubleActionLeftButtonText,
doubleActionRightButtonText: doubleActionRightButtonText,
onLeftActionButtonPressed: onLeftActionButtonPressed,
onRightActionButtonPressed: onRightActionButtonPressed,
leftActionButtonKey: leftActionButtonKey,
rightActionButtonKey: rightActionButtonKey,
key: key);
final MaterialThemeBase currentTheme;
final FooterType footerType;
final String? contentImage;
final Color? contentImageColor;
final String? content;
final Widget? bottomActionPanel;
final String? singleActionButtonText;
final VoidCallback? onSingleActionButtonPressed;
final Key? singleActionButtonKey;
final String? doubleActionLeftButtonText;
final String? doubleActionRightButtonText;
final VoidCallback? onLeftActionButtonPressed;
final VoidCallback? onRightActionButtonPressed;
final Key? rightActionButtonKey;
final Key? leftActionButtonKey;
final double height;
final double? contentImageSize;
final bool showDisclaimerText;
@override
Widget contentWidget(BuildContext context) {
@@ -79,9 +101,11 @@ class InfoBottomSheet extends BaseBottomSheet {
if (contentImage != null)
Expanded(
flex: 4,
child: SizedBox(
width: contentImageSize,
child: getImage(contentImage!, imageColor: contentImageColor),
child: ImageUtil.getImageFromPath(
imagePath: contentImage!,
svgImageColor: contentImageColor,
fit: BoxFit.contain,
borderRadius: 10,
),
)
else
@@ -110,6 +134,7 @@ class InfoBottomSheet extends BaseBottomSheet {
],
),
),
bottomActionPanel ?? const SizedBox(),
if (showDisclaimerText)
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 10),
@@ -141,123 +166,8 @@ class InfoBottomSheet extends BaseBottomSheet {
),
),
),
if (showDontAskMeCheckbox)
Padding(
padding: const EdgeInsets.only(left: 34),
child: Row(
children: [
SimpleCheckbox(onChanged: onCheckboxChanged),
const SizedBox(width: 8),
Text(
'Don’t ask me next time',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
decoration: TextDecoration.none,
),
),
],
),
),
],
),
);
}
@override
Widget footerWidget(BuildContext context) {
if (isTwoAction) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 34),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
child: Container(
padding: const EdgeInsets.only(right: 8.0, top: 8.0),
child: PrimaryButton(
key: leftActionButtonKey,
onPressed: actionLeftButton,
text: leftButtonText ?? '',
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
),
Flexible(
child: Container(
padding: const EdgeInsets.only(left: 8.0, top: 8.0),
child: PrimaryButton(
key: rightActionButtonKey,
onPressed: actionRightButton,
text: rightButtonText ?? '',
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
),
);
} else {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 34),
child: LoadingPrimaryButton(
key: actionButtonKey,
onPressed: actionButton ?? () {},
text: actionButtonText ?? '',
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: false,
isDisabled: false,
),
);
}
}
Widget getImage(String imagePath, {Color? imageColor}) {
final bool isSvg = imagePath.endsWith('.svg');
if (isSvg) {
return SvgPicture.asset(
imagePath,
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
);
} else {
return Image.asset(imagePath);
}
}
}
class SimpleCheckbox extends StatefulWidget {
SimpleCheckbox({this.onChanged});
final Function(bool)? onChanged;
@override
State<SimpleCheckbox> createState() => _SimpleCheckboxState();
}
class _SimpleCheckboxState extends State<SimpleCheckbox> {
bool initialValue = false;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 24.0,
width: 24.0,
child: Checkbox(
value: initialValue,
onChanged: (value) => setState(() {
initialValue = value!;
widget.onChanged?.call(value);
}),
checkColor: Theme.of(context).colorScheme.onSurfaceVariant,
activeColor: Colors.transparent,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: WidgetStateBorderSide.resolveWith((states) =>
BorderSide(color: Theme.of(context).colorScheme.onSurfaceVariant, width: 1.0)),
),
);
}
}
@@ -21,7 +21,7 @@ class InfoStepsBottomSheet extends BaseBottomSheet {
required this.steps,
String? titleIconPath,
required this.currentTheme,
}) : super(titleText: titleText, titleIconPath: titleIconPath);
}) : super(titleText: titleText, titleIconPath: titleIconPath, footerType: FooterType.none, maxHeight: 900);
@override
Widget contentWidget(BuildContext context) => SizedBox(
+33 -53
View File
@@ -56,67 +56,47 @@ class _NumberTextFieldState extends State<NumberTextField> {
@override
Widget build(BuildContext context) => TextField(
style: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurfaceVariant),
enableInteractiveSelection: false,
textAlign: TextAlign.center,
textAlignVertical: TextAlignVertical.bottom,
controller: _controller,
focusNode: _focusNode,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0),
decoration: InputDecoration(
style: Theme.of(context).textTheme.titleMedium!,
enableInteractiveSelection: false,
textAlign: TextAlign.center,
textAlignVertical: TextAlignVertical.bottom,
controller: _controller,
focusNode: _focusNode,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0),
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.all(0),
fillColor: Colors.transparent,
counterText: '',
isDense: true,
filled: true,
suffixIconConstraints: BoxConstraints(
maxHeight: widget.arrowsHeight,
maxWidth: widget.arrowsWidth + widget.contentPadding.right,
),
prefixIconConstraints: BoxConstraints(
maxHeight: widget.arrowsHeight,
maxWidth: widget.arrowsWidth + widget.contentPadding.left,
),
suffixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
prefixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
prefixIcon: Material(
type: MaterialType.transparency,
child: InkWell(
child: Container(
width: widget.arrowsWidth,
alignment: Alignment.bottomCenter,
child: Icon(
Icons.arrow_left_outlined,
size: widget.arrowsWidth,
),
),
onTap: _canGoDown ? () => _update(false) : null,
),
),
type: MaterialType.transparency,
child: InkWell(
child: Container(
width: widget.arrowsWidth,
alignment: Alignment.bottomCenter,
child: Icon(Icons.keyboard_arrow_left_outlined ,size: widget.arrowsWidth)),
onTap: _canGoDown ? () => _update(false) : null)),
suffixIcon: Material(
type: MaterialType.transparency,
child: InkWell(
child: Container(
width: widget.arrowsWidth,
alignment: Alignment.bottomCenter,
child: Icon(
Icons.arrow_right_outlined,
size: widget.arrowsWidth,
),
),
onTap: _canGoUp ? () => _update(true) : null,
),
),
),
maxLines: 1,
onChanged: (value) {
final intValue = int.tryParse(value);
widget.onChanged?.call(intValue);
_updateArrows(intValue);
},
inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)],
);
type: MaterialType.transparency,
child: InkWell(
child: Container(
width: widget.arrowsWidth,
alignment: Alignment.bottomCenter,
child: Icon(Icons.keyboard_arrow_right_outlined, size: widget.arrowsWidth)),
onTap: _canGoUp ? () => _update(true) : null))),
maxLines: 1,
onChanged: (value) {
final intValue = int.tryParse(value);
widget.onChanged?.call(intValue);
_updateArrows(intValue);
},
inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)]);
void _update(bool up) {
var intValue = int.tryParse(_controller.text);
+34
View File
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class SimpleCheckbox extends StatefulWidget {
SimpleCheckbox({this.onChanged});
final Function(bool)? onChanged;
@override
State<SimpleCheckbox> createState() => _SimpleCheckboxState();
}
class _SimpleCheckboxState extends State<SimpleCheckbox> {
bool initialValue = false;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 24.0,
width: 24.0,
child: Checkbox(
value: initialValue,
onChanged: (value) => setState(() {
initialValue = value!;
widget.onChanged?.call(value);
}),
checkColor: Theme.of(context).textTheme.titleLarge!.color,
activeColor: Colors.transparent,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: WidgetStateBorderSide.resolveWith((states) => BorderSide(
color: Theme.of(context).textTheme.titleLarge!.color!, width: 1.0)),
),
);
}
}
@@ -1,5 +1,6 @@
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:flutter/material.dart';
class StandardSlideButton extends StatefulWidget {
@@ -29,7 +30,9 @@ class _StandardSlideButtonState extends State<StandardSlideButton> {
Widget build(BuildContext context) {
final bool accessible = MediaQuery.of(context).accessibleNavigation;
final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
final tileBackgroundColor = widget.currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark
: CustomThemeColors.backgroundGradientColorLight;
return accessible
? PrimaryButton(
@@ -87,7 +90,7 @@ class _StandardSlideButtonState extends State<StandardSlideButton> {
height: widget.height - 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.surface,
),
alignment: Alignment.center,
child: Icon(
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
class TabViewWrapper extends StatefulWidget {
const TabViewWrapper({
super.key,
required this.tabs,
required this.views,
this.tabBarPadding = const EdgeInsets.only(right: 24),
this.labelStyle,
this.unselectedLabelStyle,
this.indicatorColor,
}) : assert(tabs.length == views.length, 'Tabs and views must be of equal length.');
final List<Tab> tabs;
final List<Widget> views;
final EdgeInsets tabBarPadding;
final TextStyle? labelStyle;
final TextStyle? unselectedLabelStyle;
final Color? indicatorColor;
@override
State<TabViewWrapper> createState() => _TabViewWrapperState();
}
class _TabViewWrapperState extends State<TabViewWrapper> with SingleTickerProviderStateMixin {
late final TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: widget.tabs.length, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final textStyle = TextStyle(
fontSize: 18,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface);
return Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: TabBar(
controller: _tabController,
isScrollable: true,
splashFactory: NoSplash.splashFactory,
indicatorSize: TabBarIndicatorSize.label,
labelStyle: widget.labelStyle ?? textStyle,
unselectedLabelStyle: widget.unselectedLabelStyle ??
textStyle.copyWith(color: textStyle.color?.withAlpha(150)),
labelColor: widget.labelStyle?.color ?? textStyle.color,
indicatorColor: widget.indicatorColor,
indicatorPadding: EdgeInsets.zero,
labelPadding: widget.tabBarPadding,
tabAlignment: TabAlignment.start,
dividerColor: Colors.transparent,
padding: EdgeInsets.zero,
tabs: widget.tabs,
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: widget.views,
),
),
],
);
}
}
+1
View File
@@ -3,6 +3,7 @@ import 'dart:io';
class FeatureFlag {
static const bool isCakePayEnabled = false;
static const bool isCakePayRedemptionFlowEnabled = false;
static const bool isExolixEnabled = true;
static const bool isBackgroundSyncEnabled = true;
static final bool isInAppTorEnabled = (Platform.isAndroid);
+55 -63
View File
@@ -3,89 +3,81 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class ImageUtil {
static Widget getImageFromPath({required String imagePath, double? height, double? width}) {
static Widget getImageFromPath({
required String imagePath,
double? height,
double? width,
Color? svgImageColor,
BoxFit? fit,
double? borderRadius,
}) {
bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
if (CakeTor.instance.enabled && isNetworkImage) {
imagePath = "assets/images/tor_logo.svg";
isNetworkImage = false;
}
final bool isSvg = imagePath.endsWith('.svg');
final double _height = height ?? 35;
final double _width = width ?? 35;
final isSvg = imagePath.endsWith('.svg');
final bool ignoreSize = fit != null;
final double? _height = ignoreSize ? null : (height ?? 35);
final double? _width = ignoreSize ? null : (width ?? 35);
Widget img;
if (isNetworkImage) {
return isSvg
? SvgPicture.network(
img = isSvg
? SvgPicture.network(imagePath,
key: ValueKey(imagePath),
imagePath,
height: _height,
width: _width,
placeholderBuilder: (BuildContext context) => Container(
height: _height,
width: _width,
child: Center(
child: CircularProgressIndicator(),
),
),
errorBuilder: (_, __, ___) {
return Container(
height: _height,
width: _width,
child: Center(
child: Icon(Icons.error_outline, color: Colors.grey),
),
);
},
)
: Image.network(
fit: fit ?? BoxFit.contain,
placeholderBuilder: (_) => _placeholder(_height, _width),
errorBuilder: (_, __, ___) => _errorPlaceholder(_height, _width))
: Image.network(imagePath,
key: ValueKey(imagePath),
imagePath,
height: _height,
width: _width,
loadingBuilder:
(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
return child;
}
return Container(
height: _height,
width: _width,
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
return Container(
height: _height,
width: _width,
child: Center(
child: Icon(Icons.error_outline, color: Colors.grey),
),
);
},
);
fit: fit,
loadingBuilder: (_, child, progress) =>
progress == null ? child : _placeholder(_height, _width),
errorBuilder: (_, __, ___) => _errorPlaceholder(_height, _width));
} else {
return isSvg
? SvgPicture.asset(
imagePath,
img = isSvg
? SvgPicture.asset(imagePath,
key: ValueKey(imagePath),
height: _height,
width: _width,
placeholderBuilder: (_) => Icon(Icons.error),
errorBuilder: (_, __, ___) => Icon(Icons.error),
key: ValueKey(imagePath),
)
fit: fit ?? BoxFit.contain,
colorFilter:
svgImageColor != null ? ColorFilter.mode(svgImageColor, BlendMode.srcIn) : null,
placeholderBuilder: (_) => _placeholder(_height, _width),
errorBuilder: (_, __, ___) => _errorPlaceholder(_height, _width))
: Image.asset(
imagePath,
key: ValueKey(imagePath),
height: _height,
width: _width,
errorBuilder: (_, __, ___) => Icon(Icons.error),
key: ValueKey(imagePath),
fit: fit,
errorBuilder: (_, __, ___) => _errorPlaceholder(_height, _width),
);
}
if (borderRadius != null && borderRadius > 0) {
img = ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: img,
);
}
return img;
}
static Widget _placeholder(double? h, double? w) => (h != null || w != null)
? SizedBox(height: h, width: w, child: const Center(child: CircularProgressIndicator()))
: const Center(child: CircularProgressIndicator());
static Widget _errorPlaceholder(double? h, double? w) => (h != null || w != null)
? SizedBox(
height: h,
width: w,
child: const Center(child: Icon(Icons.error_outline, color: Colors.grey)))
: const Center(child: Icon(Icons.error_outline, color: Colors.grey));
}
@@ -1,4 +1,4 @@
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:mobx/mobx.dart';
part 'cake_pay_account_view_model.g.dart';
@@ -1,5 +1,5 @@
import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:mobx/mobx.dart';
part 'cake_pay_auth_view_model.g.dart';
@@ -1,5 +1,14 @@
import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
import 'dart:async';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
part 'cake_pay_buy_card_view_model.g.dart';
@@ -7,20 +16,36 @@ part 'cake_pay_buy_card_view_model.g.dart';
class CakePayBuyCardViewModel = CakePayBuyCardViewModelBase with _$CakePayBuyCardViewModel;
abstract class CakePayBuyCardViewModelBase with Store {
CakePayBuyCardViewModelBase({required this.vendor})
: amount = vendor.card!.denominations.isNotEmpty
CakePayBuyCardViewModelBase(
{required this.vendor, required this.cakePayService, required this.sendViewModel})
: walletType = sendViewModel.walletType,
amount = vendor.card!.denominations.isNotEmpty
? double.parse(vendor.card!.denominations.first)
: 0,
quantity = 1,
min = double.parse(vendor.card!.minValue ?? '0'),
max = double.parse(vendor.card!.maxValue ?? '0'),
card = vendor.card!;
card = vendor.card! {
selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
}
final CakePayVendor vendor;
final CakePayCard card;
final double min;
final SendViewModel sendViewModel;
final CakePayService cakePayService;
final double max;
final double min;
final CakePayCard card;
final WalletType walletType;
CakePayOrder? order;
Timer? _timer;
DateTime? expirationTime;
Duration? remainingTime;
bool confirmsNoVpn = false;
bool confirmsVoidedRefund = false;
bool confirmsTermsAgreed = false;
String simulatedResponse = '';
bool get isDenominationSelected => card.denominations.isNotEmpty;
@@ -30,13 +55,48 @@ abstract class CakePayBuyCardViewModelBase with Store {
@observable
int quantity;
@observable
bool isPurchasing = false;
@observable
bool isSimulatingFlow = false;
@observable
bool isOrderExpired = false;
@observable
String formattedRemainingTime = '';
@computed
bool get isEnablePurchase =>
bool get isAmountSufficient =>
(amount >= min && amount <= max) || (isDenominationSelected && quantity > 0);
@observable
CakePayPaymentMethod? selectedPaymentMethod;
@computed
double get totalAmount => amount * quantity;
@computed
bool get isSimulating => isSimulatingFlow && FeatureFlag.hasDevOptions;
@computed
List<CakePayPaymentMethod> get availableMethods {
switch (walletType) {
case WalletType.bitcoin:
return [CakePayPaymentMethod.BTC];
case WalletType.litecoin:
return [CakePayPaymentMethod.LTC, CakePayPaymentMethod.LTC_MWEB];
case WalletType.monero:
return [CakePayPaymentMethod.XMR];
default:
return const [];
}
}
@action
void chooseMethod(CakePayPaymentMethod method) => selectedPaymentMethod = method;
@action
void onQuantityChanged(int? input) => quantity = input ?? 1;
@@ -45,4 +105,135 @@ abstract class CakePayBuyCardViewModelBase with Store {
if (input.isEmpty) return;
amount = double.parse(input.replaceAll(',', '.'));
}
CryptoPaymentData? getPaymentDataFor(CakePayPaymentMethod? method) {
if (order == null || method == null) return null;
final data = switch (method) {
CakePayPaymentMethod.BTC => order?.paymentData.btc,
CakePayPaymentMethod.XMR => order?.paymentData.xmr,
CakePayPaymentMethod.LTC => order?.paymentData.ltc,
CakePayPaymentMethod.LTC_MWEB => order?.paymentData.ltc_mweb,
_ => null
};
if (data == null) return null;
final bip21 = data.paymentUrls?.bip21;
if (bip21 != null && bip21.isNotEmpty) {
final uri = Uri.parse(bip21);
final addr = uri.path;
final price = uri.queryParameters['amount'] ?? data.price;
return CryptoPaymentData(price: price, address: addr);
}
return data;
}
@action
Future<void> createOrder() async {
if (walletType != WalletType.bitcoin &&
walletType != WalletType.monero &&
walletType != WalletType.litecoin) {
sendViewModel.state =
FailureState('Unsupported wallet type, please use Bitcoin, Monero, or Litecoin.');
}
try {
order = await cakePayService.createOrder(
cardId: card.id,
price: amount.toString(),
quantity: quantity,
confirmsNoVpn: confirmsNoVpn,
confirmsVoidedRefund: confirmsVoidedRefund,
confirmsTermsAgreed: confirmsTermsAgreed,
);
await confirmSending();
expirationTime = order!.paymentData.expirationTime;
updateRemainingTime();
_startExpirationTimer();
} catch (e) {
sendViewModel.state = FailureState(
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
}
}
@action
Future<void> confirmSending() async {
final cryptoPaymentData = getPaymentDataFor(selectedPaymentMethod);
if (order == null || cryptoPaymentData == null) return;
try {
sendViewModel.clearOutputs();
final output = sendViewModel.outputs.first;
output.address = cryptoPaymentData.address;
output.setCryptoAmount(cryptoPaymentData.price);
await sendViewModel.createTransaction();
} catch (e) {
throw e;
}
}
@action
Future<void> simulatePayment() async {
if (order == null) {
throw Exception('Order is not created yet.');
}
try {
simulatedResponse = await cakePayService.simulatePayment(orderId: order!.orderId);
sendViewModel.state = TransactionCommitted();
} catch (e) {
sendViewModel.state = FailureState(
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
}
}
@action
void updateRemainingTime() {
if (expirationTime == null) {
formattedRemainingTime = '';
return;
}
remainingTime = expirationTime!.difference(DateTime.now());
isOrderExpired = remainingTime!.isNegative;
if (isOrderExpired) {
disposeExpirationTimer();
sendViewModel.state = FailureState('Order has expired.');
} else {
formattedRemainingTime = formatDuration(remainingTime!);
}
}
void _startExpirationTimer() {
_timer?.cancel();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
updateRemainingTime();
});
}
String formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds
.toString().padLeft(2, '0')}';
}
void disposeExpirationTimer() {
_timer?.cancel();
remainingTime = null;
formattedRemainingTime = '';
expirationTime = null;
}
void dispose() {
disposeExpirationTimer();
}
}
@@ -1,6 +1,9 @@
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
import 'dart:async';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
import 'package:cake_wallet/entities/country.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -17,8 +20,8 @@ abstract class CakePayCardsListViewModelBase with Store {
CakePayCardsListViewModelBase({
required this.cakePayService,
required this.settingsStore,
}) : cardState = CakePayCardsStateNoCards(),
cakePayVendors = [],
}) : cakePayVendors = [],
userCards = [],
availableCountries = [],
page = 1,
displayPrepaidCards = true,
@@ -28,8 +31,11 @@ abstract class CakePayCardsListViewModelBase with Store {
scrollOffsetFromTop = 0.0,
vendorsState = InitialCakePayVendorLoadingState(),
createCardState = CakePayCreateCardState(),
userCardState = UserCakePayCardsStateInitial(),
searchString = '',
searchMyCardsString = '',
CakePayVendorList = <CakePayVendor>[] {
checkAuth();
initialization();
}
@@ -43,6 +49,7 @@ abstract class CakePayCardsListViewModelBase with Store {
void initialization() async {
await getCountries();
getVendors();
getUserCards();
}
final CakePayService cakePayService;
@@ -51,7 +58,7 @@ abstract class CakePayCardsListViewModelBase with Store {
List<CakePayVendor> CakePayVendorList;
Map<String, List<FilterItem>> get createFilterItems => {
S.current.filter_by: [
'Card Type': [
FilterItem(
value: () => displayPrepaidCards,
caption: S.current.prepaid_cards,
@@ -74,6 +81,7 @@ abstract class CakePayCardsListViewModelBase with Store {
};
String searchString;
String? username;
int page;
late Country _initialSelectedCountry;
@@ -82,6 +90,12 @@ abstract class CakePayCardsListViewModelBase with Store {
late bool _initialDisplayDenominationsCards;
late bool _initialDisplayCustomValueCards;
@observable
List<CakePayCard> userCards;
@observable
String searchMyCardsString;
@observable
double scrollOffsetFromTop;
@@ -89,7 +103,7 @@ abstract class CakePayCardsListViewModelBase with Store {
CakePayCreateCardState createCardState;
@observable
CakePayCardsState cardState;
UserCakePayCardsState userCardState;
@observable
CakePayVendorState vendorsState;
@@ -118,12 +132,29 @@ abstract class CakePayCardsListViewModelBase with Store {
@observable
bool displayCustomValueCards;
@observable
ObservableFuture<bool>? authFuture;
@computed
bool? get isUserAuthenticated =>
authFuture?.status == FutureStatus.fulfilled ? authFuture?.value : null;
@computed
Country get selectedCountry =>
settingsStore.selectedCakePayCountry ?? _getInitialCountry(settingsStore.fiatCurrency);
@computed
bool get shouldShowCountryPicker => settingsStore.selectedCakePayCountry == null && availableCountries.isNotEmpty;
bool get shouldShowCountryPicker =>
settingsStore.selectedCakePayCountry == null && availableCountries.isNotEmpty;
@computed
List<CakePayCard> get filteredUserCards {
final query = searchMyCardsString.trim().toLowerCase();
if (query.isEmpty) return userCards;
return userCards
.where((card) => card.name.toLowerCase().contains(query))
.toList(growable: false);
}
bool get hasFiltersChanged {
@@ -134,7 +165,6 @@ abstract class CakePayCardsListViewModelBase with Store {
displayCustomValueCards != _initialDisplayCustomValueCards;
}
Future<void> getCountries() async {
try {
availableCountries = await cakePayService.getCountries();
@@ -143,6 +173,44 @@ abstract class CakePayCardsListViewModelBase with Store {
}
}
Future<void> getUserCards() async {
//Dummy user cards // TODO: fetch from API
userCardState = UserCakePayCardsStateFetching();
try {
await Future.delayed(const Duration(seconds: 2));
final vendorsCard = cakePayVendors
.where((vendor) => vendor.card != null)
.map((vendor) => vendor.card!)
.toList();
userCards = vendorsCard.sublist(0, 10);
vendorsCard.forEach((card) {
if (card.name.toLowerCase().contains('amazon.com')) {
userCards.add(card);
}
});
userCardState = UserCakePayCardsStateSuccess();
if (userCards.isEmpty) {
userCardState = UserCakePayCardsStateNoCards();
}
} catch (e) {
userCardState = UserCakePayCardsStateFailure(
error: e.toString(),
);
}
}
@action
Future<void> checkAuth() async {
authFuture = ObservableFuture(cakePayService.isLogged());
final logged = await authFuture!;
if (logged) {
username = await cakePayService.getUserEmail();
}
}
@action
Future<void> getVendors({
String? text,
@@ -219,6 +287,9 @@ abstract class CakePayCardsListViewModelBase with Store {
settingsStore.selectedCakePayCountry = country;
}
@action
void setMyCardsQuery(String text) => searchMyCardsString = text;
@action
void togglePrepaidCards() => displayPrepaidCards = !displayPrepaidCards;
@@ -1,173 +0,0 @@
import 'dart:async';
import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/cake_pay_payment_credantials.dart';
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
part 'cake_pay_purchase_view_model.g.dart';
class CakePayPurchaseViewModel = CakePayPurchaseViewModelBase with _$CakePayPurchaseViewModel;
abstract class CakePayPurchaseViewModelBase with Store {
CakePayPurchaseViewModelBase({
required this.cakePayService,
required this.paymentCredential,
required this.card,
required this.sendViewModel,
}) : walletType = sendViewModel.walletType;
final WalletType walletType;
final PaymentCredential paymentCredential;
final CakePayCard card;
final SendViewModel sendViewModel;
final CakePayService cakePayService;
CakePayOrder? order;
Timer? _timer;
DateTime? expirationTime;
Duration? remainingTime;
String? get userName => paymentCredential.userName;
double get amount => paymentCredential.amount;
int get quantity => paymentCredential.quantity;
double get totalAmount => paymentCredential.totalAmount;
String get fiatCurrency => paymentCredential.fiatCurrency;
bool confirmsNoVpn = false;
bool confirmsVoidedRefund = false;
bool confirmsTermsAgreed = false;
@observable
bool isPurchasing = false;
CryptoPaymentData? get cryptoPaymentData {
if (order == null) return null;
if (WalletType.monero == walletType) {
return order!.paymentData.xmr;
}
if (WalletType.bitcoin == walletType) {
final paymentUrls = order!.paymentData.btc.paymentUrls!.bip21;
final uri = Uri.parse(paymentUrls!);
final address = uri.path;
final price = uri.queryParameters['amount'];
return CryptoPaymentData(
address: address,
price: price ?? '0',
);
}
return null;
}
@observable
bool isOrderExpired = false;
@observable
String formattedRemainingTime = '';
@action
Future<void> createOrder() async {
if (walletType != WalletType.bitcoin && walletType != WalletType.monero) {
sendViewModel.state = FailureState('Unsupported wallet type, please use Bitcoin or Monero.');
}
try {
order = await cakePayService.createOrder(
cardId: card.id,
price: paymentCredential.amount.toString(),
quantity: paymentCredential.quantity,
confirmsNoVpn: confirmsNoVpn,
confirmsVoidedRefund: confirmsVoidedRefund,
confirmsTermsAgreed: confirmsTermsAgreed,
);
await confirmSending();
expirationTime = order!.paymentData.expirationTime;
updateRemainingTime();
_startExpirationTimer();
} catch (e) {
sendViewModel.state = FailureState(
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
}
}
@action
Future<void> confirmSending() async {
final cryptoPaymentData = this.cryptoPaymentData;
try {
if (order == null || cryptoPaymentData == null) return;
sendViewModel.clearOutputs();
final output = sendViewModel.outputs.first;
output.address = cryptoPaymentData.address;
output.setCryptoAmount(cryptoPaymentData.price);
await sendViewModel.createTransaction();
} catch (e) {
throw e;
}
}
@action
void updateRemainingTime() {
if (expirationTime == null) {
formattedRemainingTime = '';
return;
}
remainingTime = expirationTime!.difference(DateTime.now());
isOrderExpired = remainingTime!.isNegative;
if (isOrderExpired) {
disposeExpirationTimer();
sendViewModel.state = FailureState('Order has expired.');
} else {
formattedRemainingTime = formatDuration(remainingTime!);
}
}
void _startExpirationTimer() {
_timer?.cancel();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
updateRemainingTime();
});
}
String formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
void disposeExpirationTimer() {
_timer?.cancel();
remainingTime = null;
formattedRemainingTime = '';
expirationTime = null;
}
void dispose() {
disposeExpirationTimer();
}
}
@@ -1,4 +1,4 @@
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:mobx/mobx.dart';
part 'cake_features_view_model.g.dart';
@@ -1,67 +1,57 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
class DropdownFilterList extends StatefulWidget {
DropdownFilterList({
class DropdownFilterList extends StatelessWidget {
const DropdownFilterList({
Key? key,
required this.items,
this.itemPrefix,
this.textStyle,
required this.caption,
required this.selectedItem,
required this.onItemSelected,
this.itemPrefix,
}) : super(key: key);
final List<String> items;
final String? itemPrefix;
final TextStyle? textStyle;
final String caption;
final String selectedItem;
final Function(String) onItemSelected;
@override
_DropdownFilterListState createState() => _DropdownFilterListState();
}
class _DropdownFilterListState extends State<DropdownFilterList> {
String? selectedValue;
@override
void initState() {
super.initState();
selectedValue = widget.selectedItem;
}
final String? itemPrefix;
final ValueChanged<String> onItemSelected;
@override
Widget build(BuildContext context) {
return DropdownButtonHideUnderline(
child: Container(
child: DropdownButton<String>(
isExpanded: true,
icon: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(Icons.arrow_drop_down, color: Theme.of(context).colorScheme.onSurfaceVariant),
],
),
),
dropdownColor: Theme.of(context).colorScheme.surfaceContainerHighest,
child: DropdownButton<String>(
isDense: true,
dropdownColor: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(10),
items: widget.items
.map((item) => DropdownMenuItem<String>(
alignment: Alignment.bottomCenter,
value: item,
child: AutoSizeText('${widget.itemPrefix ?? ''} $item', style: widget.textStyle),
))
selectedItemBuilder: (context) => items
.map(
(item) => Text(
'${itemPrefix ?? ''} $item',
style: Theme.of(context).textTheme.titleMedium!,
maxLines: 1,
),
)
.toList(),
value: selectedValue,
onChanged: (newValue) {
setState(() => selectedValue = newValue);
widget.onItemSelected(newValue!);
items: items
.map(
(item) => DropdownMenuItem<String>(
value: item,
alignment: Alignment.centerLeft,
child: Text(
'${itemPrefix ?? ''} $item',
style: (const TextStyle()).copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
maxLines: 1,
),
),
)
.toList(),
value: selectedItem,
onChanged: (value) {
if (value != null) onItemSelected(value);
},
),
),
icon: Icon(Icons.keyboard_arrow_down_outlined,
color: Theme.of(context).colorScheme.onSurfaceVariant)),
);
}
}
+26
View File
@@ -335,3 +335,29 @@ abstract class OutputBase with Store {
note = parsedAddress.description;
}
}
extension OutputCopyWith on Output {
Output OutputCopyWithParsedAddress({
ParsedAddress? parsedAddress,
String? fiatAmount,
}) {
final clone = Output(
_wallet,
_settingsStore,
_fiatConversationStore,
cryptoCurrencyHandler,
);
clone
..cryptoAmount = cryptoAmount
..cryptoFullBalance = cryptoFullBalance
..note = note
..sendAll = sendAll
..memo = memo
..stealthAddress = stealthAddress
..parsedAddress = parsedAddress ?? this.parsedAddress
..fiatAmount = fiatAmount ?? this.fiatAmount;
return clone;
}
}
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": " كعكة 2FA مسبقا",
"cake_dark_theme": "موضوع الكعكة الظلام",
"cake_pay_account_note": "قم بالتسجيل باستخدام عنوان بريد إلكتروني فقط لمشاهدة البطاقات وشرائها. حتى أن بعضها متوفر بسعر مخفض!",
"cake_pay_card_email_delivered_message": "سيتم تسليم بطاقة الهدايا الخاصة بك عبر البريد الإلكتروني بعد التأكيدات اللازمة.",
"cake_pay_learn_more": "شراء واسترداد بطاقات الهدايا على الفور في التطبيق!\nاسحب من اليسار إلى اليمين لمعرفة المزيد.",
"cake_pay_save_order": "يجب إرسال البطاقة إلى بريدك الإلكتروني خلال يوم عمل واحد \n حفظ معرف الطلب الخاص بك:",
"cake_pay_subtitle": "شراء بطاقات مسبقة الدفع وبطاقات الهدايا في جميع أنحاء العالم",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Торта 2FA Preset",
"cake_dark_theme": "Торта тъмна тема",
"cake_pay_account_note": "Регистрайте се само с един имейл, за да виждате и купувате карти. За някои има дори и отстъпка!",
"cake_pay_card_email_delivered_message": "Вашата карта за подарък ще бъде доставена по имейл след необходимите потвърждения.",
"cake_pay_learn_more": "Купете и използвайте гифткарти директно в приложението!\nПлъзнете отляво надясно, за да научите още.",
"cake_pay_save_order": "Картата трябва да бъде изпратена до вашия имейл в рамките на 1 работен ден \n Запазете вашия идентификационен номер на поръчката:",
"cake_pay_subtitle": "Купете предплатени карти и карти за подаръци в световен мащаб",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Předvolba Cake 2FA",
"cake_dark_theme": "Dort tmavé téma",
"cake_pay_account_note": "Přihlaste se svou e-mailovou adresou pro zobrazení a nákup karet. Některé jsou dostupné ve slevě!",
"cake_pay_card_email_delivered_message": "Vaše dárková karta bude doručena e -mailem po nezbytných potvrzeních.",
"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.",
"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:",
"cake_pay_subtitle": "Kupte si celosvětové předplacené karty a dárkové karty",
+3 -2
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Cake 2FA-Voreinstellung",
"cake_dark_theme": "Cake Dark Thema",
"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!",
"cake_pay_card_email_delivered_message": "Ihre Geschenkkarte wird nach den erforderlichen Bestätigungen per E -Mail geliefert.",
"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.",
"cake_pay_save_order": "Die Karte sollte innerhalb von 1 Werktag an Ihre E-Mail gesendet werden, \n Ihre Bestell-ID zu speichern:",
"cake_pay_subtitle": "Kaufen Sie weltweite Prepaid-Karten und Geschenkkarten",
@@ -605,8 +606,8 @@
"please_choose_one": "Bitte wählen Sie einen",
"please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist",
"please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.",
"Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.",
"please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.",
"Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.",
"please_select": "Bitte auswählen:",
"please_select_backup_file": "Bitte wählen Sie die Sicherungsdatei und geben Sie das Sicherungskennwort ein.",
"please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Knoten zu verbinden",
@@ -1127,4 +1128,4 @@
"you_will_send": "Konvertieren von",
"youCanGoBackToYourDapp": "Sie können jetzt zu Ihrem Dapp zurückkehren",
"yy": "YY"
}
}
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Cake 2FA Preset",
"cake_dark_theme": "Cake Dark Theme",
"cake_pay_account_note": "Sign up with just an email address to see and purchase cards. Some are even available at a discount!",
"cake_pay_card_email_delivered_message": "Your gift card will be delivered via email after the necessary confirmations.",
"cake_pay_learn_more": "Instantly purchase and redeem gift cards in the app!\nSwipe left to right to learn more.",
"cake_pay_save_order": "The card should be sent to your e-mail within 1 business day \n Save your Order ID:",
"cake_pay_subtitle": "Buy worldwide prepaid cards and gift cards",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Pastel 2FA preestablecido",
"cake_dark_theme": "Tema oscuro",
"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!",
"cake_pay_card_email_delivered_message": "Su tarjeta de regalo se entregará por correo electrónico después de las confirmaciones necesarias.",
"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.",
"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:",
"cake_pay_subtitle": "Compra tarjetas prepagadas y tarjetas de regalo en todo el mundo",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Cake 2FA prédéfini",
"cake_dark_theme": "Thème sombre Cake",
"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 !",
"cake_pay_card_email_delivered_message": "Votre carte-cadeau sera livrée par e-mail après les confirmations nécessaires.",
"cake_pay_learn_more": "Achetez et utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.",
"cake_pay_save_order": "La carte doit être envoyée à votre e-mail dans un jour ouvrable \n Enregistrez votre identifiant de commande:",
"cake_pay_subtitle": "Achetez des cartes et des cartes-cadeaux prépayées mondiales",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Cake 2FA saiti",
"cake_dark_theme": "Cake Dark Jigo",
"cake_pay_account_note": "Yi rajista tare da adireshin imel kawai don gani da siyan katunan. Wasu ma suna samuwa a rangwame!",
"cake_pay_card_email_delivered_message": "Za a kawo katin kyautar ku ta hanyar imel bayan abubuwan da suka wajaba.",
"cake_pay_learn_more": "Nan take siya ku kwaso katunan kyaututtuka a cikin app!\nTake hagu zuwa dama don ƙarin koyo.",
"cake_pay_save_order": "Ya kamata a aika katin zuwa e-mail ɗinku a cikin rana 1 na kasuwanci \n Ajiye id ku:",
"cake_pay_subtitle": "Sayi katunan shirye-shiryen duniya da katunan kyauta",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "केक 2एफए प्रीसेट",
"cake_dark_theme": "केक डार्क थीम",
"cake_pay_account_note": "कार्ड देखने और खरीदने के लिए केवल एक ईमेल पते के साथ साइन अप करें। कुछ छूट पर भी उपलब्ध हैं!",
"cake_pay_card_email_delivered_message": "आवश्यक पुष्टि के बाद आपका उपहार कार्ड ईमेल के माध्यम से दिया जाएगा।",
"cake_pay_learn_more": "ऐप में उपहार कार्ड तुरंत खरीदें और रिडीम करें!\nअधिक जानने के लिए बाएं से दाएं स्वाइप करें।",
"cake_pay_save_order": "कार्ड को आपके ई-मेल को 1 व्यावसायिक दिन के भीतर भेजा जाना चाहिए \n आपकी ऑर्डर आईडी सहेजें:",
"cake_pay_subtitle": "दुनिया भर में प्रीपेड कार्ड और उपहार कार्ड खरीदें",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Cake 2FA Preset",
"cake_dark_theme": "TOKA DARKA TEMA",
"cake_pay_account_note": "Prijavite se samo s adresom e-pošte da biste vidjeli i kupili kartice. Neke su čak dostupne uz popust!",
"cake_pay_card_email_delivered_message": "Vaša poklon kartica bit će isporučena putem e -pošte nakon potrebnih potvrda.",
"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.",
"cake_pay_save_order": "Karticu treba poslati na vašu e-poštu u roku od 1 radnog dana \n Spremi ID narudžbe:",
"cake_pay_subtitle": "Kupite svjetske unaprijed plaćene kartice i poklon kartice",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Cake 2FA նախապես կանխորոշված",
"cake_dark_theme": "Cake մութ տեսք",
"cake_pay_account_note": "Գրանցվեք միայն էլ. փոստի միջոցով, որպեսզի տեսնեք և գնեք քարտեր: Որոշ քարտեր հասանելի են նույնիսկ զեղչով:",
"cake_pay_card_email_delivered_message": "Ձեր նվեր քարտը կուղարկվի էլփոստի միջոցով անհրաժեշտ հաստատումներից հետո:",
"cake_pay_learn_more": "Վայրկյանապես գնեք և փոխանակեք նվեր քարտերը հավելվածում:\nՍահեցրեք ձախից աջ՝ ավելին իմանալու համար:",
"cake_pay_save_order": "Քարտը պետք է ուղարկված լինի ձեր էլ. փոստին 1 աշխատանքային օրվա ընթացքում \n Պահպանեք Ձեր պատվերի համարը՝",
"cake_pay_subtitle": "Գնեք համաշխարհային նախավճարային քարտեր և նվեր քարտեր",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Preset Kue 2FA",
"cake_dark_theme": "Tema Kue Gelap",
"cake_pay_account_note": "Daftar hanya dengan alamat email untuk melihat dan membeli kartu. Beberapa di antaranya bahkan tersedia dengan diskon!",
"cake_pay_card_email_delivered_message": "Kartu hadiah Anda akan dikirimkan melalui email setelah konfirmasi yang diperlukan.",
"cake_pay_learn_more": "Beli dan tukar kartu hadiah secara instan di aplikasi!\nGeser ke kanan untuk informasi lebih lanjut.",
"cake_pay_save_order": "Kartu harus dikirim ke email Anda dalam 1 hari kerja \n Simpan ID pesanan Anda:",
"cake_pay_subtitle": "Beli kartu prabayar di seluruh dunia dan kartu hadiah",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Preset Cake 2FA",
"cake_dark_theme": "Tema scuro Cake",
"cake_pay_account_note": "Iscriviti solamente con un indirizzo email per vedere e acquistare le carte. Alcune sono anche disponibili con uno sconto!",
"cake_pay_card_email_delivered_message": "La tua carta regalo verrà consegnata via e -mail dopo le conferme necessarie.",
"cake_pay_learn_more": "Acquista e riscatta istantaneamente carte regalo nell'app!\nScorri da sinistra a destra per saperne di più.",
"cake_pay_save_order": "La carta deve essere inviata alla tua e-mail entro 1 giorno lavorativo \n Salva l'ID del tuo ordine:",
"cake_pay_subtitle": "Acquista carte prepagate e carte regalo internazionali",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "ケーキ 2FA プリセット",
"cake_dark_theme": "ケーキ暗いテーマ",
"cake_pay_account_note": "メールアドレスだけでサインアップして、カードを表示して購入できます。割引価格で利用できるカードもあります!",
"cake_pay_card_email_delivered_message": "ギフトカードは、必要な確認の後に電子メールで配信されます。",
"cake_pay_learn_more": "アプリですぐにギフトカードを購入して引き換えましょう!\n左から右にスワイプして詳細をご覧ください。",
"cake_pay_save_order": "カードは1営業日以内に電子メールに送信する必要があります\n注文IDを保存します。",
"cake_pay_subtitle": "世界中のプリペイドカードとギフトカードを購入します",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Cake 2FA 사전 설정",
"cake_dark_theme": "Cake 다크 테마",
"cake_pay_account_note": "이메일 주소만으로 가입하여 카드를 확인하고 구매하세요. 일부는 할인된 가격으로도 이용 가능합니다!",
"cake_pay_card_email_delivered_message": "기프트 카드는 필요한 확인 후 이메일을 통해 전달됩니다.",
"cake_pay_learn_more": "앱에서 즉시 기프트 카드를 구매하고 사용하세요!\n자세히 알아보려면 왼쪽에서 오른쪽으로 스와이프하세요.",
"cake_pay_save_order": "카드는 영업일 기준 1일 이내에 이메일로 발송됩니다.\n주문 ID를 저장하세요:",
"cake_pay_subtitle": "전 세계 선불 카드 및 기프트 카드 구매",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "ကိတ်မုန့် 2FA ကြိုတင်သတ်မှတ်",
"cake_dark_theme": "ကိတ်မုန့် Dark Theme",
"cake_pay_account_note": "ကတ်များကြည့်ရှုဝယ်ယူရန် အီးမေးလ်လိပ်စာတစ်ခုဖြင့် စာရင်းသွင်းပါ။ အချို့ကို လျှော့ဈေးဖြင့်ပင် ရနိုင်သည်။",
"cake_pay_card_email_delivered_message": "လိုအပ်သောအတည်ပြုချက်များအပြီးတွင်သင်၏လက်ဆောင်ကဒ်ကိုအီးမေးလ်ဖြင့်ပေးပို့လိမ့်မည်။",
"cake_pay_learn_more": "အက်ပ်ရှိ လက်ဆောင်ကတ်များကို ချက်ချင်းဝယ်ယူပြီး ကူပွန်ဖြင့် လဲလှယ်ပါ။\nပိုမိုလေ့လာရန် ဘယ်မှညာသို့ ပွတ်ဆွဲပါ။",
"cake_pay_save_order": "ကဒ်ကိုသင်၏အီးမေးလ်သို့ပေးပို့သင့်သည်။ သင်၏အမှာစာ ID ကိုသိမ်းပါ။",
"cake_pay_subtitle": "Worldwide ကြိုတင်ငွေဖြည့်ကဒ်များနှင့်လက်ဆောင်ကဒ်များကို 0 ယ်ပါ",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Taart 2FA Voorinstelling",
"cake_dark_theme": "Cake Dark Theme",
"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!",
"cake_pay_card_email_delivered_message": "Uw cadeaubon wordt na de nodige bevestigingen via e -mail afgeleverd.",
"cake_pay_learn_more": "Koop en wissel cadeaubonnen direct in de app in!\nSwipe van links naar rechts voor meer informatie.",
"cake_pay_save_order": "De kaart moet binnen 1 werkdag naar uw e-mail worden verzonden.",
"cake_pay_subtitle": "Koop wereldwijde prepaid -kaarten en cadeaubonnen",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Cake 2FA Preset",
"cake_dark_theme": "Cake Dark",
"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ą!",
"cake_pay_card_email_delivered_message": "Twoja karta podarunkowa zostanie dostarczona pocztą elektroniczną po niezbędnych potwierdzeniach.",
"cake_pay_learn_more": "Kupuj i wykorzystuj karty podarunkowe od razu w aplikacji!\nPrzesuń od lewej do prawej, aby dowiedzieć się więcej.",
"cake_pay_save_order": "Karta powinna zostać wysłana na adres e-mail w ciągu 1 dnia roboczego \n Zapisz identyfikator zamówienia:",
"cake_pay_subtitle": "Kup na całym świecie karty przedpłacone i karty podarunkowe",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Predefinição de bolo 2FA",
"cake_dark_theme": "Bolo tema escuro",
"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!",
"cake_pay_card_email_delivered_message": "Seu cartão -presente será entregue por e -mail após as confirmações necessárias.",
"cake_pay_learn_more": "Compre e resgate vales-presente instantaneamente no app!\nDeslize da esquerda para a direita para saber mais.",
"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:",
"cake_pay_subtitle": "Compre cartões pré -pagos em todo o mundo e cartões -presente",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Торт 2FA Preset",
"cake_dark_theme": "Тейт темная тема",
"cake_pay_account_note": "Зарегистрируйтесь, указав только адрес электронной почты, чтобы просматривать и покупать карты. Некоторые даже доступны со скидкой!",
"cake_pay_card_email_delivered_message": "Ваша подарочная карта будет доставлена ​​по электронной почте после необходимых подтверждений.",
"cake_pay_learn_more": "Мгновенно покупайте и используйте подарочные карты в приложении!\nПроведите по экрану слева направо, чтобы узнать больше.",
"cake_pay_save_order": "Карта должна быть отправлена ​​на ваше электронное письмо в течение 1 рабочего дня \n Сохраните свой идентификатор заказа:",
"cake_pay_subtitle": "Купить карты с предоплатой и подарочными картами по всему миру",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "เค้ก 2FA ที่ตั้งไว้ล่วงหน้า",
"cake_dark_theme": "ธีมเค้กมืด",
"cake_pay_account_note": "ลงทะเบียนด้วยอีเมลเพียงอย่างเดียวเพื่อดูและซื้อบัตร บางบัตรอาจมีส่วนลด!",
"cake_pay_card_email_delivered_message": "บัตรของขวัญของคุณจะถูกส่งทางอีเมลหลังจากการยืนยันที่จำเป็น",
"cake_pay_learn_more": "ซื้อและเบิกบัตรของขวัญในแอพพลิเคชันทันที!\nกระแทกขวาไปซ้ายเพื่อเรียนรู้เพิ่มเติม",
"cake_pay_save_order": "บัตรควรส่งไปยังอีเมลของคุณภายใน 1 วันทำการ \n บันทึกรหัสคำสั่งซื้อของคุณ:",
"cake_pay_subtitle": "ซื้อบัตรเติมเงินและบัตรของขวัญทั่วโลก",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Cake 2FA Preset",
"cake_dark_theme": "Cake Dark Theme",
"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!",
"cake_pay_card_email_delivered_message": "Ang iyong gift card ay maihatid sa pamamagitan ng email pagkatapos ng kinakailangang mga kumpirmasyon.",
"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.",
"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:",
"cake_pay_subtitle": "Bumili ng mga pandaigdigang prepaid card at gift card",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Kek 2FA Ön Ayarı",
"cake_dark_theme": "Kek Koyu Tema",
"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!",
"cake_pay_card_email_delivered_message": "Hediye kartınız gerekli onaylardan sonra e -posta yoluyla teslim edilecektir.",
"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.",
"cake_pay_save_order": "Kart, 1 İş Günü içinde e-postanıza gönderilmelidir \n Sipariş Kimliğinizi Kaydet:",
"cake_pay_subtitle": "Dünya çapında ön ödemeli kartlar ve hediye kartları satın alın",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Торт 2FA Preset",
"cake_dark_theme": "Темна тема торта",
"cake_pay_account_note": "Зареєструйтеся, використовуючи лише адресу електронної пошти, щоб переглядати та купувати картки. Деякі навіть доступні зі знижкою!",
"cake_pay_card_email_delivered_message": "Ваша подарункова картка буде доставлена ​​електронною поштою після необхідних підтверджень.",
"cake_pay_learn_more": "Миттєво купуйте та активуйте подарункові картки в додатку!\nПроведіть пальцем зліва направо, щоб дізнатися більше.",
"cake_pay_save_order": "Картка повинна бути надіслана на вашу електронну пошту протягом 1 робочого дня \n Зберегти ідентифікатор замовлення:",
"cake_pay_subtitle": "Купіть у всьому світі передплачені картки та подарункові картки",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "کیک 2FA پیش سیٹ",
"cake_dark_theme": "کیک ڈارک تھیم",
"cake_pay_account_note": "کارڈز دیکھنے اور خریدنے کے لیے صرف ایک ای میل ایڈریس کے ساتھ سائن اپ کریں۔ کچھ رعایت پر بھی دستیاب ہیں!",
"cake_pay_card_email_delivered_message": "آپ کا گفٹ کارڈ ضروری تصدیقوں کے بعد ای میل کے ذریعے فراہم کیا جائے گا۔",
"cake_pay_learn_more": "ایپ میں فوری طور پر گفٹ کارڈز خریدیں اور بھنائیں!\\nمزید جاننے کے لیے بائیں سے دائیں سوائپ کریں۔",
"cake_pay_save_order": "کارڈ 1 کاروباری دن کے اندر آپ کے ای میل پر بھیجا جانا چاہئے \n اپنے آرڈر کی شناخت کو بچائیں:",
"cake_pay_subtitle": "دنیا بھر میں پری پیڈ کارڈز اور گفٹ کارڈ خریدیں",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Thiết lập sẵn Cake 2FA",
"cake_dark_theme": "Chủ đề Cake tối",
"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á!",
"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.",
"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.",
"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:",
"cake_pay_subtitle": "Mua thẻ trả trước toàn cầu và thẻ quà tặng",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "Cake 2FA Tito",
"cake_dark_theme": "Akara oyinbo dudu koko",
"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!",
"cake_pay_card_email_delivered_message": "Kaadi Ẹbun Rẹ yoo fi jiṣẹ nipasẹ imeeli lẹhin awọn ijẹrisi pataki.",
"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ù.",
"cake_pay_save_order": "Kaadi yẹ ki o firanṣẹ si imeeli rẹ laarin ọjọ iṣowo 1 \n Fipamọ aṣẹ rẹ:",
"cake_pay_subtitle": "Ra awọn kaadi ti a san ni agbaye ati awọn kaadi ẹbun",
+1
View File
@@ -108,6 +108,7 @@
"cake_2fa_preset": "蛋糕 2FA 预设",
"cake_dark_theme": "蛋糕黑暗主题",
"cake_pay_account_note": "只需使用電子郵件地址註冊即可查看和購買卡片。有些甚至可以打折!",
"cake_pay_card_email_delivered_message": "您的礼品卡将在必要的确认后通过电子邮件发送。",
"cake_pay_learn_more": "立即在应用中购买和兑换礼品卡!\n从左向右滑动以了解详情。",
"cake_pay_save_order": "该卡应在1个工作日内发送到您的电子邮件\n保存您的订单ID:",
"cake_pay_subtitle": "购买全球预付费卡和礼品卡",