mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
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:
Binary file not shown.
|
After Width: | Height: | Size: 353 B |
@@ -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
-1
@@ -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';
|
||||
+5
-2
@@ -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);
|
||||
}
|
||||
+9
-7
@@ -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(
|
||||
+4
-4
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+15
-39
@@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -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
@@ -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
@@ -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>(),
|
||||
);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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": "شراء بطاقات مسبقة الدفع وبطاقات الهدايا في جميع أنحاء العالم",
|
||||
|
||||
@@ -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": "Купете предплатени карти и карти за подаръци в световен мащаб",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "दुनिया भर में प्रीपेड कार्ड और उपहार कार्ड खरीदें",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Գնեք համաշխարհային նախավճարային քարտեր և նվեր քարտեր",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "世界中のプリペイドカードとギフトカードを購入します",
|
||||
|
||||
@@ -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": "전 세계 선불 카드 및 기프트 카드 구매",
|
||||
|
||||
@@ -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 ယ်ပါ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Купить карты с предоплатой и подарочными картами по всему миру",
|
||||
|
||||
@@ -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": "ซื้อบัตรเติมเงินและบัตรของขวัญทั่วโลก",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Купіть у всьому світі передплачені картки та подарункові картки",
|
||||
|
||||
@@ -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": "دنیا بھر میں پری پیڈ کارڈز اور گفٹ کارڈ خریدیں",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "购买全球预付费卡和礼品卡",
|
||||
|
||||
Reference in New Issue
Block a user