Cw 973 in app gift card redemption flow plus UI (#2285)

* refactor: cleanup cake_pay folder

* refactor: add My Cards tab on the main screen

* ui: add user card item

* refactor: cakePay purchase flow

* feat: add user card info

* refactor: auth flow

* refactor: theme data

* revert

* Update qr_widget.dart

* fix: update with new themes

* minor fixes

* feat: put redemption flow under a feature flag

* feat: implement simulated purchasing flow in debug mode

* feat: support LTC and MWEB payments

* Update savings_page.dart

* fix: UI fixes

* add Cake Pay transaction sent bottom sheet

* localization

* Update cake_pay_buy_card_page.dart

* addressing review comments

* fix marge conflicts
This commit is contained in:
Serhii
2025-07-11 23:56:35 +03:00
committed by GitHub
parent 203aff83f2
commit ef84db5874
96 changed files with 3907 additions and 2638 deletions
@@ -1,92 +0,0 @@
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/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';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class CakePayAccountPage extends BasePage {
CakePayAccountPage(this.cakePayAccountViewModel);
final CakePayAccountViewModel cakePayAccountViewModel;
@override
Widget leading(BuildContext context) {
return MergeSemantics(
child: SizedBox(
height: 37,
width: 37,
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
label: S.of(context).seed_alert_back,
child: TextButton(
style: ButtonStyle(
overlayColor: WidgetStateColor.resolveWith(
(states) => Colors.transparent),
),
onPressed: () => Navigator.pop(context),
child: backButton(context),
),
),
),
),
);
}
@override
Widget middle(BuildContext context) {
return Text(
S.current.account,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
);
}
@override
Widget body(BuildContext context) {
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.all(24),
content: Column(
children: [
SizedBox(height: 20),
Observer(
builder: (_) => Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1.0,
color: Theme.of(context).colorScheme.outlineVariant,
),
),
),
child: CakePayTile(
title: S.of(context).email_address,
subTitle: cakePayAccountViewModel.email
),
),
),
],
),
bottomSectionPadding: EdgeInsets.all(30),
bottomSection: Column(
children: [
PrimaryButton(
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
text: S.of(context).logout,
onPressed: () {
cakePayAccountViewModel.logout();
Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false);
},
),
],
),
);
}
}
@@ -1,152 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/cake_pay/cake_pay_states.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';
import 'package:cake_wallet/src/widgets/keyboard_done_button.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/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart';
class CakePayVerifyOtpPage extends BasePage {
CakePayVerifyOtpPage(this._authViewModel, this._email, this.isSignIn)
: _codeController = TextEditingController(),
_codeFocus = FocusNode() {
_codeController.addListener(() {
final otp = _codeController.text;
_authViewModel.otp = otp;
if (otp.length > 5) {
_authViewModel.otpState = CakePayOtpSendEnabled();
} else {
_authViewModel.otpState = CakePayOtpSendDisabled();
}
});
}
final CakePayAuthViewModel _authViewModel;
final bool isSignIn;
final String _email;
@override
Widget middle(BuildContext context) {
return Text(
S.current.verification,
style: textMediumSemiBold(
color: Theme.of(context).colorScheme.onSurface,
),
);
}
final TextEditingController _codeController;
final FocusNode _codeFocus;
@override
Widget body(BuildContext context) {
reaction((_) => _authViewModel.otpState, (CakePayOtpState state) {
if (state is CakePayOtpFailure) {
_onOtpFailure(context, state.error);
}
if (state is CakePayOtpSuccess) {
_onOtpSuccessful(context);
}
});
return KeyboardActions(
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).colorScheme.surface,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _codeFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
]),
child: Container(
height: 0,
color: Theme.of(context).colorScheme.surface,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.all(24),
content: Column(
children: [
BaseTextFormField(
hintText: S.of(context).enter_code,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
focusNode: _codeFocus,
controller: _codeController,
onSubmit: (_) => _verify(),
),
SizedBox(height: 14),
Text(
S.of(context).fill_code,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
SizedBox(height: 34),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(S.of(context).didnt_get_code),
SizedBox(width: 20),
InkWell(
onTap: () => _authViewModel.logIn(_email),
child: Text(
S.of(context).resend_code,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
],
),
bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24),
bottomSection: Column(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Observer(
builder: (_) => LoadingPrimaryButton(
text: S.of(context).continue_text,
onPressed: _verify,
isDisabled: _authViewModel.otpState is CakePayOtpSendDisabled,
isLoading: _authViewModel.otpState is CakePayOtpValidating,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
SizedBox(height: 20),
],
),
],
),
),
),
);
}
void _onOtpFailure(BuildContext context, String error) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.verification,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
void _onOtpSuccessful(BuildContext context) => Navigator.pop(context);
void _verify() async => await _authViewModel.verifyEmail(_codeController.text);
}
@@ -1,129 +0,0 @@
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/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/base_text_form_field.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/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class CakePayWelcomePage extends BasePage {
CakePayWelcomePage(this._authViewModel)
: _formKey = GlobalKey<FormState>(),
_emailController = TextEditingController() {
_emailController.text = _authViewModel.email;
_emailController.addListener(() => _authViewModel.email = _emailController.text);
}
final GlobalKey<FormState> _formKey;
final CakePayAuthViewModel _authViewModel;
final TextEditingController _emailController;
@override
Widget middle(BuildContext context) {
return Text(
S.current.welcome_to_cakepay,
style: textMediumSemiBold(
color: Theme.of(context).colorScheme.onSurface,
),
);
}
@override
Widget body(BuildContext context) {
reaction((_) => _authViewModel.userVerificationState, (CakePayUserVerificationState state) {
if (state is CakePayUserVerificationStateFailure) {
_onLoginUserFailure(context, state.error);
}
if (state is CakePayUserVerificationStateSuccess) {
_onLoginSuccessful(context, _authViewModel);
}
});
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.all(24),
content: Column(
children: [
SizedBox(height: 90),
Form(
key: _formKey,
child: BaseTextFormField(
hintText: S.of(context).email_address,
keyboardType: TextInputType.emailAddress,
validator: EmailValidator(),
controller: _emailController,
onSubmit: (text) => _login(),
),
),
SizedBox(height: 20),
Text(
S.of(context).about_cake_pay,
style: textLarge(
color: Theme.of(context).colorScheme.onSurface,
),
),
SizedBox(height: 20),
Text(S.of(context).cake_pay_account_note,
style: textLarge(color: Theme.of(context).colorScheme.onSurface)),
],
),
bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24),
bottomSection: Column(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Observer(
builder: (_) => LoadingPrimaryButton(
text: S.of(context).login,
onPressed: _login,
isLoading:
_authViewModel.userVerificationState is CakePayUserVerificationStateLoading,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
SizedBox(
height: 20,
),
],
),
],
),
);
}
void _onLoginUserFailure(BuildContext context, String error) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.login,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
void _onLoginSuccessful(BuildContext context, CakePayAuthViewModel authViewModel) =>
Navigator.pushReplacementNamed(
context,
Routes.cakePayVerifyOtpPage,
arguments: [authViewModel.email, true],
);
void _login() async {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return;
}
await _authViewModel.logIn(_emailController.text);
}
}
-5
View File
@@ -1,5 +0,0 @@
export 'auth/cake_pay_welcome_page.dart';
export 'auth/cake_pay_verify_otp_page.dart';
export 'cards/cake_pay_confirm_purchase_card_page.dart';
export 'cards/cake_pay_cards_page.dart';
export 'cards/cake_pay_buy_card_page.dart';
@@ -1,519 +0,0 @@
import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/cake_pay_payment_credantials.dart';
import 'package:cake_wallet/cake_pay/cake_pay_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/image_placeholder.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/link_extractor.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/number_text_fild_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
class CakePayBuyCardPage extends BasePage {
CakePayBuyCardPage(
this.cakePayBuyCardViewModel,
this.cakePayService,
) : _amountFieldFocus = FocusNode(),
_amountController = TextEditingController(),
_quantityFieldFocus = FocusNode(),
_quantityController =
TextEditingController(text: cakePayBuyCardViewModel.quantity.toString()) {
_amountController.addListener(() {
cakePayBuyCardViewModel.onAmountChanged(_amountController.text);
});
}
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
final CakePayService cakePayService;
@override
String get title => cakePayBuyCardViewModel.card.name;
@override
bool get extendBodyBehindAppBar => true;
@override
AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent;
@override
Widget? middle(BuildContext context) {
return Text(
title,
textAlign: TextAlign.center,
maxLines: 2,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
);
}
final TextEditingController _amountController;
final FocusNode _amountFieldFocus;
final TextEditingController _quantityController;
final FocusNode _quantityFieldFocus;
@override
Widget body(BuildContext context) {
final card = cakePayBuyCardViewModel.card;
final vendor = cakePayBuyCardViewModel.vendor;
return KeyboardActions(
disableScroll: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).colorScheme.surface,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _amountFieldFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
]),
child: Container(
color: Theme.of(context).colorScheme.surface,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
height: responsiveLayoutUtil.screenHeight * 0.35,
width: double.infinity,
child: Column(
children: [
Expanded(flex: 4, child: const SizedBox()),
Expanded(
flex: 7,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10)),
child: Image.network(
card.cardImageUrl ?? '',
fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) =>
CakePayCardImagePlaceholder(),
),
),
),
Expanded(child: const SizedBox()),
],
)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Container(
height: responsiveLayoutUtil.screenHeight * 0.5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24),
Expanded(
child: Text(
S.of(context).enter_amount,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 24,
fontWeight: FontWeight.w600,
),
),
),
card.denominations.isNotEmpty
? Expanded(
flex: 2,
child: _DenominationsAmountWidget(
fiatCurrency: card.fiatCurrency.title,
denominations: card.denominations,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
quantityFieldFocus: _quantityFieldFocus,
quantityController: _quantityController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
),
)
: Expanded(
flex: 2,
child: _EnterAmountWidget(
minValue: card.minValue ?? '-',
maxValue: card.maxValue ?? '-',
fiatCurrency: card.fiatCurrency.title,
amountFieldFocus: _amountFieldFocus,
amountController: _amountController,
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
),
),
Expanded(
flex: 5,
child: Column(
children: [
if (vendor.cakeWarnings != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color:
Theme.of(context).colorScheme.onPrimary.withOpacity(0.20),
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
vendor.cakeWarnings!,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.w600,
),
),
),
),
),
Expanded(
child: SingleChildScrollView(
child: ClickableLinksText(
text: card.description ?? '',
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 18,
),
),
),
),
],
),
),
],
),
),
),
],
),
bottomSection: Column(
children: [
Observer(builder: (_) {
return Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
onPressed: () => isIOSUnavailable(card)
? alertIOSAvailability(context, card)
: navigateToCakePayBuyCardDetailPage(context, card),
text: S.of(context).buy_now,
isDisabled: !cakePayBuyCardViewModel.isEnablePurchase,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
);
}),
],
),
),
),
);
}
bool isWordInCardsName(CakePayCard card, String word) {
return card.name.toLowerCase().contains(word.toLowerCase());
}
bool isIOSUnavailable(CakePayCard card) {
if (!Platform.isIOS && !Platform.isMacOS) {
return false;
}
final isDigitalGameStores = isWordInCardsName(card, 'playstation') ||
isWordInCardsName(card, 'xbox') ||
isWordInCardsName(card, 'steam') ||
isWordInCardsName(card, 'meta quest') ||
isWordInCardsName(card, 'kigso') ||
isWordInCardsName(card, 'game world') ||
isWordInCardsName(card, 'google') ||
isWordInCardsName(card, 'nintendo');
final isGCodes = isWordInCardsName(card, 'gcodes');
final isApple = isWordInCardsName(card, 'itunes') || isWordInCardsName(card, 'apple');
final isTidal = isWordInCardsName(card, 'tidal');
final isVPNServices = isWordInCardsName(card, 'nordvpn') ||
isWordInCardsName(card, 'expressvpn') ||
isWordInCardsName(card, 'surfshark') ||
isWordInCardsName(card, 'proton');
final isStreamingServices = isWordInCardsName(card, 'netflix') ||
isWordInCardsName(card, 'spotify') ||
isWordInCardsName(card, 'hulu') ||
isWordInCardsName(card, 'hbo') ||
isWordInCardsName(card, 'soundcloud') ||
isWordInCardsName(card, 'twitch');
final isDatingServices = isWordInCardsName(card, 'tinder');
return isDigitalGameStores ||
isGCodes ||
isApple ||
isTidal ||
isVPNServices ||
isStreamingServices ||
isDatingServices;
}
Future<void> alertIOSAvailability(BuildContext context, CakePayCard card) async {
return await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).cakepay_ios_not_available,
buttonText: S.of(context).ok,
buttonAction: () {
// _walletHardwareRestoreVM.error = null;
Navigator.of(context).pop();
});
});
}
Future<void> navigateToCakePayBuyCardDetailPage(BuildContext context, CakePayCard card) async {
final userName = await cakePayService.getUserEmail();
final paymentCredential = PaymentCredential(
amount: cakePayBuyCardViewModel.amount,
quantity: cakePayBuyCardViewModel.quantity,
totalAmount: cakePayBuyCardViewModel.totalAmount,
userName: userName,
fiatCurrency: card.fiatCurrency.title,
);
Navigator.pushNamed(
context,
Routes.cakePayBuyCardDetailPage,
arguments: [paymentCredential, card],
);
}
}
class _DenominationsAmountWidget extends StatelessWidget {
const _DenominationsAmountWidget({
required this.fiatCurrency,
required this.denominations,
required this.amountFieldFocus,
required this.amountController,
required this.quantityFieldFocus,
required this.quantityController,
required this.cakePayBuyCardViewModel,
required this.onAmountChanged,
required this.onQuantityChanged,
});
final String fiatCurrency;
final List<String> denominations;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final FocusNode quantityFieldFocus;
final TextEditingController quantityController;
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
final Function(String) onAmountChanged;
final Function(int?) onQuantityChanged;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 12,
child: Column(
children: [
Expanded(
child: DropdownFilterList(
items: denominations,
itemPrefix: fiatCurrency,
selectedItem: denominations.first,
textStyle: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurface),
onItemSelected: (value) {
amountController.text = value;
onAmountChanged(value);
},
caption: '',
),
),
const SizedBox(height: 4),
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: Text(S.of(context).choose_card_value + ':',
maxLines: 2,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
),
],
),
),
Expanded(child: const SizedBox()),
Expanded(
flex: 8,
child: Column(
children: [
Expanded(
child: NumberTextField(
controller: quantityController,
focusNode: quantityFieldFocus,
min: 1,
max: 99,
onChanged: (value) => onQuantityChanged(value),
),
),
const SizedBox(height: 4),
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: Text(S.of(context).quantity + ':',
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
),
],
),
),
Expanded(child: const SizedBox()),
Expanded(
flex: 12,
child: Column(
children: [
Expanded(
child: Container(
alignment: Alignment.bottomCenter,
child: Observer(
builder: (_) => AutoSizeText(
'$fiatCurrency ${cakePayBuyCardViewModel.totalAmount}',
maxLines: 1,
style: textMediumSemiBold(
color: Theme.of(context).colorScheme.onSurface)))),
),
const SizedBox(height: 4),
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: Text(S.of(context).total + ':',
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
),
],
)),
],
);
}
}
class _EnterAmountWidget extends StatelessWidget {
const _EnterAmountWidget({
required this.minValue,
required this.maxValue,
required this.fiatCurrency,
required this.amountFieldFocus,
required this.amountController,
required this.onAmountChanged,
});
final String minValue;
final String maxValue;
final String fiatCurrency;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final Function(String) onAmountChanged;
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: BaseTextFormField(
controller: amountController,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
hintText: '0.00',
maxLines: null,
prefixIcon: Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
'$fiatCurrency: ',
style: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurface),
),
),
textStyle: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurface),
placeholderTextStyle:
textMediumSemiBold(color: Theme.of(context).colorScheme.onSurfaceVariant),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')),
FilteringTextInputFormatter.allow(
RegExp(r'^\d+(\.|\,)?\d{0,2}'),
),
],
),
),
SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).min_amount(minValue) + ' $fiatCurrency',
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
Text(S.of(context).max_amount(maxValue) + ' $fiatCurrency',
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
],
),
],
);
}
}
@@ -1,409 +0,0 @@
import 'package:cake_wallet/cake_pay/cake_pay_states.dart';
import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart';
import 'package:cake_wallet/entities/country.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/card_item.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/card_menu.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/filter_widget.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/cake_scrollbar.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/debounce.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class CakePayCardsPage extends BasePage {
CakePayCardsPage(this._cardsListViewModel) : searchFocusNode = FocusNode() {
_searchController.addListener(() {
if (_searchController.text != _cardsListViewModel.searchString) {
_searchDebounce.run(() {
_cardsListViewModel.resetLoadingNextPageState();
_cardsListViewModel.getVendors(text: _searchController.text);
});
}
});
}
final FocusNode searchFocusNode;
final CakePayCardsListViewModel _cardsListViewModel;
final _searchDebounce = Debounce(Duration(milliseconds: 500));
final _searchController = TextEditingController();
@override
bool get gradientBackground => true;
@override
Widget Function(BuildContext, Widget) get rootWrapper =>
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
@override
bool get resizeToAvoidBottomInset => false;
@override
Widget get endDrawer => CardMenu();
@override
Widget middle(BuildContext context) {
return Text(
'Cake Pay',
style: textMediumSemiBold(
color: Theme.of(context).colorScheme.onSurface,
),
);
}
@override
Widget trailing(BuildContext context) {
return _TrailingIcon(
asset: 'assets/images/profile.png',
iconColor: Theme.of(context).colorScheme.onSurface,
onPressed: () {
_cardsListViewModel.isCakePayUserAuthenticated().then(
(value) {
if (value) {
Navigator.pushNamed(context, Routes.cakePayAccountPage);
return;
}
Navigator.pushNamed(context, Routes.cakePayWelcomePage);
},
);
},
);
}
@override
Widget body(BuildContext context) {
if (_cardsListViewModel.settingsStore.selectedCakePayCountry == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
reaction((_) => _cardsListViewModel.shouldShowCountryPicker,
(bool shouldShowCountryPicker) async {
if (shouldShowCountryPicker) {
_cardsListViewModel.storeInitialFilterStates();
await showCountryPicker(context, _cardsListViewModel);
if (_cardsListViewModel.hasFiltersChanged) {
_cardsListViewModel.resetLoadingNextPageState();
_cardsListViewModel.getVendors();
}
_cardsListViewModel.settingsStore.selectedCakePayCountry =
_cardsListViewModel.selectedCountry;
}
});
});
}
final filterButton = Semantics(
label: S.of(context).filter_by,
child: GestureDetector(
onTap: () async {
_cardsListViewModel.storeInitialFilterStates();
await showFilterWidget(context);
if (_cardsListViewModel.hasFiltersChanged) {
_cardsListViewModel.resetLoadingNextPageState();
_cardsListViewModel.getVendors();
}
},
child: Container(
width: 32,
padding: EdgeInsets.only(top: 7, bottom: 7),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border.all(
color: Colors.transparent,
),
borderRadius: BorderRadius.circular(10),
),
child: Image.asset(
'assets/images/filter_icon.png',
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
);
final _countryPicker = Semantics(
label: S.of(context).filter_by,
child: GestureDetector(
onTap: () async {
_cardsListViewModel.storeInitialFilterStates();
await showCountryPicker(context, _cardsListViewModel);
if (_cardsListViewModel.hasFiltersChanged) {
_cardsListViewModel.resetLoadingNextPageState();
_cardsListViewModel.getVendors();
}
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border.all(color: Colors.transparent),
borderRadius: BorderRadius.circular(10),
),
child: Container(
margin: EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Image.asset(
_cardsListViewModel.selectedCountry.iconPath,
width: 24,
height: 24,
errorBuilder: (context, error, stackTrace) => Container(
width: 24,
height: 24,
),
),
SizedBox(width: 6),
Text(
_cardsListViewModel.selectedCountry.countryCode,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
),
);
return Padding(
padding: const EdgeInsets.all(14.0),
child: Column(
children: [
Container(
padding: EdgeInsets.only(left: 2, right: 22),
height: 32,
child: Row(
children: [
Expanded(
child: _SearchWidget(
controller: _searchController,
focusNode: searchFocusNode,
),
),
SizedBox(width: 5),
filterButton,
SizedBox(width: 5),
_countryPicker
],
),
),
SizedBox(height: 8),
Expanded(child: CakePayCardsPageBody(cardsListViewModel: _cardsListViewModel))
],
),
);
}
Future<void> showFilterWidget(BuildContext context) async {
return showPopUp<void>(
context: context,
builder: (BuildContext context) {
return FilterWidget(filterItems: _cardsListViewModel.createFilterItems);
},
);
}
}
Future<void> showCountryPicker(
BuildContext context, CakePayCardsListViewModel cardsListViewModel) async {
await showPopUp<void>(
context: context,
builder: (_) => Picker(
title: S.of(context).select_your_country,
items: cardsListViewModel.availableCountries,
images: cardsListViewModel.availableCountries
.map((e) => Image.asset(
e.iconPath,
errorBuilder: (context, error, stackTrace) => Container(
width: 58,
height: 58,
),
))
.toList(),
selectedAtIndex:
cardsListViewModel.availableCountries.indexOf(cardsListViewModel.selectedCountry),
onItemSelected: (Country country) => cardsListViewModel.setSelectedCountry(country),
isSeparated: false,
hintText: S.of(context).search,
matchingCriteria: (Country country, String searchText) =>
country.fullName.toLowerCase().contains(searchText.toLowerCase())));
}
class CakePayCardsPageBody extends StatefulWidget {
const CakePayCardsPageBody({
Key? key,
required this.cardsListViewModel,
}) : super(key: key);
final CakePayCardsListViewModel cardsListViewModel;
@override
_CakePayCardsPageBodyState createState() => _CakePayCardsPageBodyState();
}
class _CakePayCardsPageBodyState extends State<CakePayCardsPageBody> {
double get backgroundHeight => MediaQuery.of(context).size.height * 0.75;
double thumbHeight = 72;
bool get isAlwaysShowScrollThumb => merchantsList.isEmpty ? false : merchantsList.length > 3;
List<CakePayVendor> get merchantsList => widget.cardsListViewModel.cakePayVendors;
final _scrollController = ScrollController();
@override
void initState() {
_scrollController.addListener(() {
final scrollOffsetFromTop = _scrollController.hasClients
? (_scrollController.offset /
_scrollController.position.maxScrollExtent *
(backgroundHeight - thumbHeight))
: 0.0;
widget.cardsListViewModel.setScrollOffsetFromTop(scrollOffsetFromTop);
double threshold = 200.0;
bool isNearBottom =
_scrollController.offset >= _scrollController.position.maxScrollExtent - threshold;
if (isNearBottom && !_scrollController.position.outOfRange) {
widget.cardsListViewModel.fetchNextPage();
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Observer(builder: (_) {
final vendorsState = widget.cardsListViewModel.vendorsState;
if (vendorsState is CakePayVendorLoadedState) {
bool isLoadingMore = widget.cardsListViewModel.isLoadingNextPage;
final vendors = widget.cardsListViewModel.cakePayVendors;
if (vendors.isEmpty) {
return Center(child: Text(S.of(context).no_cards_found));
}
return Stack(children: [
GridView.builder(
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 2 : 1,
childAspectRatio: 5,
crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
),
padding: EdgeInsets.only(left: 2, right: 22),
itemCount: vendors.length + (isLoadingMore ? 1 : 0),
itemBuilder: (_, index) {
if (index >= vendors.length) {
return _VendorLoadedIndicator();
}
final vendor = vendors[index];
return CardItem(
logoUrl: vendor.card?.cardImageUrl,
onTap: () {
Navigator.of(context).pushNamed(Routes.cakePayBuyCardPage, arguments: [vendor]);
},
title: vendor.name,
subTitle: vendor.card?.description ?? '',
backgroundColor: Theme.of(context).colorScheme.surface,
titleColor: Theme.of(context).colorScheme.onSurface,
subtitleColor: Theme.of(context).colorScheme.onSecondary,
discount: 0.0,
);
},
),
isAlwaysShowScrollThumb
? CakeScrollbar(
backgroundHeight: backgroundHeight,
thumbHeight: thumbHeight,
rightOffset: 1,
width: 3,
backgroundColor: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.05),
thumbColor: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5),
fromTop: widget.cardsListViewModel.scrollOffsetFromTop,
)
: Offstage()
]);
}
return _VendorLoadedIndicator();
});
}
}
class _VendorLoadedIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).colorScheme.onSurface,
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).colorScheme.primary),
),
);
}
}
class _SearchWidget extends StatelessWidget {
const _SearchWidget({
Key? key,
required this.controller,
required this.focusNode,
}) : super(key: key);
final TextEditingController controller;
final FocusNode focusNode;
@override
Widget build(BuildContext context) {
return BaseTextFormField(
focusNode: focusNode,
textStyle: Theme.of(context).textTheme.bodyMedium,
controller: controller,
contentPadding: EdgeInsets.only(top: 8, left: 8),
hintText: S.of(context).search,
placeholderTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
alignLabelWithHint: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
suffixIcon: ExcludeSemantics(
child: Icon(
Icons.search,
color: Theme.of(context).colorScheme.primary,
),
),
);
}
}
class _TrailingIcon extends StatelessWidget {
const _TrailingIcon({required this.asset, this.onPressed, required this.iconColor});
final String asset;
final VoidCallback? onPressed;
final Color iconColor;
@override
Widget build(BuildContext context) {
return Semantics(
label: S.of(context).profile,
child: Material(
color: Colors.transparent,
child: IconButton(
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
highlightColor: Colors.transparent,
onPressed: onPressed,
icon: ImageIcon(AssetImage(asset), size: 25, color: iconColor),
),
));
}
}
@@ -1,703 +0,0 @@
import 'package:cake_wallet/cake_pay/cake_pay_card.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/cake_pay_alert_modal.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/image_placeholder.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/link_extractor.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/text_icon_button.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_purchase_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher.dart';
class CakePayBuyCardDetailPage extends BasePage {
CakePayBuyCardDetailPage(this.cakePayPurchaseViewModel);
final CakePayPurchaseViewModel cakePayPurchaseViewModel;
@override
String get title => cakePayPurchaseViewModel.card.name;
@override
Widget? middle(BuildContext context) {
return Text(
title,
textAlign: TextAlign.center,
maxLines: 2,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
);
}
@override
Widget? trailing(BuildContext context) => null;
bool _effectsInstalled = false;
@override
Widget body(BuildContext context) {
_setEffects(context);
final card = cakePayPurchaseViewModel.card;
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.zero,
content: Observer(builder: (_) {
return Column(
children: [
SizedBox(height: 36),
ClipRRect(
borderRadius:
BorderRadius.horizontal(left: Radius.circular(20), right: Radius.circular(20)),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.20),
),
),
child: Row(
children: [
Expanded(
child: Container(
child: ClipRRect(
borderRadius: BorderRadius.horizontal(
left: Radius.circular(20), right: Radius.circular(20)),
child: Image.network(
card.cardImageUrl ?? '',
fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) =>
CakePayCardImagePlaceholder(),
),
)),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(children: [
Row(
children: [
Text(
S.of(context).value + ':',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
SizedBox(width: 8),
Text(
'${cakePayPurchaseViewModel.amount.toStringAsFixed(2)} ${cakePayPurchaseViewModel.fiatCurrency}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
SizedBox(height: 16),
Row(
children: [
Text(
S.of(context).quantity + ':',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
SizedBox(width: 8),
Text(
'${cakePayPurchaseViewModel.quantity}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
SizedBox(height: 16),
Row(
children: [
Text(
S.of(context).total + ':',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
SizedBox(width: 8),
Text(
'${cakePayPurchaseViewModel.totalAmount.toStringAsFixed(2)} ${cakePayPurchaseViewModel.fiatCurrency}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
]),
),
)
],
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: TextIconButton(
label: S.of(context).how_to_use_card,
onTap: () => _showHowToUseCard(context, card),
),
),
SizedBox(height: 20),
if (card.expiryAndValidity != null && card.expiryAndValidity!.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).expiry_and_validity + ':',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
)),
SizedBox(height: 10),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.20),
width: 1,
),
),
),
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
card.expiryAndValidity ?? '',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
],
),
),
],
);
}),
bottomSection: Column(
children: [
Padding(
padding: EdgeInsets.only(bottom: 12),
child: Observer(builder: (_) {
return LoadingPrimaryButton(
isDisabled: cakePayPurchaseViewModel.isPurchasing,
isLoading: cakePayPurchaseViewModel.isPurchasing ||
cakePayPurchaseViewModel.sendViewModel.state is IsExecutingState,
onPressed: () => confirmPurchaseFirst(context),
text: S.of(context).purchase_gift_card,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
);
}),
),
SizedBox(height: 8),
InkWell(
onTap: () => _showTermsAndCondition(context, card.termsAndConditions),
child: Text(
S.of(context).settings_terms_and_conditions,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(height: 16)
],
),
);
}
void _showTermsAndCondition(BuildContext context, String? termsAndConditions) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return CakePayAlertModal(
title: S.of(context).settings_terms_and_conditions,
content: Align(
alignment: Alignment.bottomLeft,
child: ClickableLinksText(
text: termsAndConditions ?? '',
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 18,
) ??
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 18,
),
),
),
actionTitle: S.of(context).agree,
showCloseButton: false,
heightFactor: 0.6,
);
});
}
Future<void> _showconfirmPurchaseFirstAlert(BuildContext context) async {
if (!cakePayPurchaseViewModel.confirmsNoVpn ||
!cakePayPurchaseViewModel.confirmsVoidedRefund ||
!cakePayPurchaseViewModel.confirmsTermsAgreed) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) => ThreeCheckboxAlert(
alertTitle: S.of(context).cakepay_confirm_purchase,
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).confirm,
actionLeftButton: () {
cakePayPurchaseViewModel.isPurchasing = false;
Navigator.of(context).pop();
},
actionRightButton: (confirmsNoVpn, confirmsVoidedRefund, confirmsTermsAgreed) {
cakePayPurchaseViewModel.confirmsNoVpn = confirmsNoVpn;
cakePayPurchaseViewModel.confirmsVoidedRefund = confirmsVoidedRefund;
cakePayPurchaseViewModel.confirmsTermsAgreed = confirmsTermsAgreed;
Navigator.of(context).pop();
},
),
);
}
if (cakePayPurchaseViewModel.confirmsNoVpn &&
cakePayPurchaseViewModel.confirmsVoidedRefund &&
cakePayPurchaseViewModel.confirmsTermsAgreed) {
await purchaseCard(context);
}
}
Future<void> confirmPurchaseFirst(BuildContext context) async {
bool isLogged = await cakePayPurchaseViewModel.cakePayService.isLogged();
if (!isLogged) {
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
} else {
cakePayPurchaseViewModel.isPurchasing = true;
await _showconfirmPurchaseFirstAlert(context);
}
}
Future<void> purchaseCard(BuildContext context) async {
bool isLogged = await cakePayPurchaseViewModel.cakePayService.isLogged();
if (!isLogged) {
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
} else {
try {
await cakePayPurchaseViewModel.createOrder();
} catch (_) {
await cakePayPurchaseViewModel.cakePayService.logout();
}
}
cakePayPurchaseViewModel.isPurchasing = false;
}
void _showHowToUseCard(
BuildContext context,
CakePayCard card,
) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return CakePayAlertModal(
title: S.of(context).how_to_use_card,
content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Padding(
padding: EdgeInsets.all(10),
child: Text(
card.name,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
)),
ClickableLinksText(
text: card.howToUse ?? '',
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
) ??
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
linkStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontStyle: FontStyle.italic,
) ??
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontStyle: FontStyle.italic,
),
),
]),
actionTitle: S.current.got_it,
);
});
}
Future<void> _showConfirmSendingAlert(BuildContext context) async {
if (cakePayPurchaseViewModel.order == null) {
return;
}
ReactionDisposer? disposer;
disposer = reaction((_) => cakePayPurchaseViewModel.isOrderExpired, (bool isExpired) {
if (isExpired) {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
if (disposer != null) {
disposer();
}
}
});
final order = cakePayPurchaseViewModel.order;
showModalBottomSheet<void>(
context: context,
isDismissible: false,
isScrollControlled: true,
builder: (BuildContext popupContext) {
return ConfirmSendingBottomSheet(
key: ValueKey('send_page_confirm_sending_dialog_key'),
currentTheme: currentTheme,
walletType: cakePayPurchaseViewModel.sendViewModel.walletType,
paymentId: S.of(popupContext).payment_id,
paymentIdValue: order?.orderId,
expirationTime: cakePayPurchaseViewModel.formattedRemainingTime,
titleText: S.of(popupContext).confirm_transaction,
titleIconPath: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
currency: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency,
amount: S.of(popupContext).send_amount,
amountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.amountFormatted,
fiatAmountValue:
cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(popupContext).send_fee,
feeValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount:
cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: cakePayPurchaseViewModel.sendViewModel.outputs,
onSlideComplete: () async {
Navigator.of(popupContext).pop();
cakePayPurchaseViewModel.sendViewModel.commitTransaction(context);
},
);
},
);
}
BuildContext? loadingBottomSheetContext;
void _setEffects(BuildContext context) {
if (_effectsInstalled) {
return;
}
reaction((_) => cakePayPurchaseViewModel.sendViewModel.state, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) showStateAlert(context, S.of(context).error, state.error);
});
}
if (state is! IsExecutingState &&
loadingBottomSheetContext != null &&
loadingBottomSheetContext!.mounted) {
Navigator.of(loadingBottomSheetContext!).pop();
}
if (state is IsExecutingState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (BuildContext context) {
loadingBottomSheetContext = context;
return LoadingBottomSheet(
titleText: S.of(context).generating_transaction,
);
},
);
}
});
}
if (state is ExecutedSuccessfullyState) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _showConfirmSendingAlert(context);
});
}
if (state is TransactionCommitted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
cakePayPurchaseViewModel.sendViewModel.clearOutputs();
if (context.mounted) showSentAlert(context);
});
}
});
_effectsInstalled = true;
}
void showStateAlert(BuildContext context, String title, String content) {
if (context.mounted) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: title,
alertContent: content,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
}
Future<void> showSentAlert(BuildContext context) async {
if (!context.mounted) {
return;
}
final order = cakePayPurchaseViewModel.order!.orderId;
final isCopy = await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).transaction_sent,
alertContent: S.of(context).cake_pay_save_order + '\n${order}',
leftButtonText: S.of(context).ignor,
rightButtonText: S.of(context).copy,
actionLeftButton: () => Navigator.of(context).pop(false),
actionRightButton: () => Navigator.of(context).pop(true));
}) ??
false;
if (isCopy) {
await Clipboard.setData(ClipboardData(text: order));
}
}
void _handleDispose(ReactionDisposer? disposer) {
cakePayPurchaseViewModel.dispose();
if (disposer != null) {
disposer();
}
}
}
class ThreeCheckboxAlert extends BaseAlertDialog {
ThreeCheckboxAlert({
required this.alertTitle,
required this.leftButtonText,
required this.rightButtonText,
required this.actionLeftButton,
required this.actionRightButton,
this.alertBarrierDismissible = true,
Key? key,
});
final String alertTitle;
final String leftButtonText;
final String rightButtonText;
final VoidCallback actionLeftButton;
final Function(bool, bool, bool) actionRightButton;
final bool alertBarrierDismissible;
bool checkbox1 = false;
void toggleCheckbox1() => checkbox1 = !checkbox1;
bool checkbox2 = false;
void toggleCheckbox2() => checkbox2 = !checkbox2;
bool checkbox3 = false;
void toggleCheckbox3() => checkbox3 = !checkbox3;
bool showValidationMessage = true;
@override
String get titleText => alertTitle;
@override
bool get isDividerExists => true;
@override
String get leftActionButtonText => leftButtonText;
@override
String get rightActionButtonText => rightButtonText;
@override
VoidCallback get actionLeft => actionLeftButton;
@override
VoidCallback get actionRight => () {
actionRightButton(checkbox1, checkbox2, checkbox3);
};
@override
bool get barrierDismissible => alertBarrierDismissible;
@override
Widget content(BuildContext context) {
return ThreeCheckboxAlertContent(
checkbox1: checkbox1,
toggleCheckbox1: toggleCheckbox1,
checkbox2: checkbox2,
toggleCheckbox2: toggleCheckbox2,
checkbox3: checkbox3,
toggleCheckbox3: toggleCheckbox3,
);
}
}
class ThreeCheckboxAlertContent extends StatefulWidget {
ThreeCheckboxAlertContent({
required this.checkbox1,
required this.toggleCheckbox1,
required this.checkbox2,
required this.toggleCheckbox2,
required this.checkbox3,
required this.toggleCheckbox3,
Key? key,
}) : super(key: key);
bool checkbox1;
void Function() toggleCheckbox1;
bool checkbox2;
void Function() toggleCheckbox2;
bool checkbox3;
void Function() toggleCheckbox3;
@override
_ThreeCheckboxAlertContentState createState() => _ThreeCheckboxAlertContentState(
checkbox1: checkbox1,
toggleCheckbox1: toggleCheckbox1,
checkbox2: checkbox2,
toggleCheckbox2: toggleCheckbox2,
checkbox3: checkbox3,
toggleCheckbox3: toggleCheckbox3,
);
static _ThreeCheckboxAlertContentState? of(BuildContext context) {
return context.findAncestorStateOfType<_ThreeCheckboxAlertContentState>();
}
}
class _ThreeCheckboxAlertContentState extends State<ThreeCheckboxAlertContent> {
_ThreeCheckboxAlertContentState({
required this.checkbox1,
required this.toggleCheckbox1,
required this.checkbox2,
required this.toggleCheckbox2,
required this.checkbox3,
required this.toggleCheckbox3,
});
bool checkbox1;
void Function() toggleCheckbox1;
bool checkbox2;
void Function() toggleCheckbox2;
bool checkbox3;
void Function() toggleCheckbox3;
bool showValidationMessage = true;
bool get areAllCheckboxesChecked => checkbox1 && checkbox2 && checkbox3;
@override
Widget build(BuildContext context) {
return Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
StandardCheckbox(
value: checkbox1,
caption: S.of(context).cakepay_confirm_no_vpn,
onChanged: (bool? value) {
setState(() {
checkbox1 = value ?? false;
toggleCheckbox1();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
StandardCheckbox(
value: checkbox2,
caption: S.of(context).cakepay_confirm_voided_refund,
onChanged: (bool? value) {
setState(() {
checkbox2 = value ?? false;
toggleCheckbox2();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
StandardCheckbox(
value: checkbox3,
caption: S.of(context).cakepay_confirm_terms_agreed,
onChanged: (bool? value) {
setState(() {
checkbox3 = value ?? false;
toggleCheckbox3();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => launchUrl(
Uri.parse("https://www.cakepay.com/terms/"),
mode: LaunchMode.externalApplication,
),
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
S.of(context).settings_terms_and_conditions,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.none,
height: 1,
),
softWrap: true,
),
),
),
if (showValidationMessage)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Please confirm all checkboxes',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.errorContainer,
decoration: TextDecoration.none,
),
),
),
],
),
);
}
}
@@ -1,86 +0,0 @@
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:flutter/material.dart';
class CakePayAlertModal extends StatelessWidget {
const CakePayAlertModal({
Key? key,
required this.title,
required this.content,
required this.actionTitle,
this.heightFactor = 0.4,
this.showCloseButton = true,
}) : super(key: key);
final String title;
final Widget content;
final String actionTitle;
final bool showCloseButton;
final double heightFactor;
@override
Widget build(BuildContext context) {
return AlertBackground(
child: Material(
color: Colors.transparent,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Spacer(),
Container(
padding: EdgeInsets.only(top: 24, left: 24, right: 24),
margin: EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(30),
),
child: Column(
children: [
if (title.isNotEmpty)
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
Container(
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * heightFactor),
child: ListView(
children: [
content,
SizedBox(height: 35),
],
),
),
PrimaryButton(
onPressed: () => Navigator.pop(context),
text: actionTitle,
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.primary,
),
SizedBox(height: 21),
],
),
),
Spacer(),
if(showCloseButton)
InkWell(
onTap: () => Navigator.pop(context),
child: Container(
margin: EdgeInsets.only(bottom: 40),
child: CircleAvatar(
child: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurface,
),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
),
),
)
],
),
),
);
}
}
@@ -1,47 +0,0 @@
import 'package:flutter/material.dart';
class CakePayTile extends StatelessWidget {
const CakePayTile({
Key? key,
required this.title,
required this.subTitle,
this.onTap,
}) : super(key: key);
final VoidCallback? onTap;
final String title;
final String subTitle;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
SizedBox(height: 8),
Text(
subTitle,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
],
)
],
),
);
}
}
@@ -1,106 +0,0 @@
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,
});
final VoidCallback? onTap;
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) {
return Theme(
data: ThemeData(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: InkWell(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
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: [
if (logoUrl != null)
AspectRatio(
aspectRatio: 1.8,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10)),
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(),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
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,
),
),
],
),
),
),
],
),
),
),
);
}
}
@@ -1,11 +0,0 @@
import 'package:flutter/material.dart';
class CardMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
);
}
}
@@ -1,28 +0,0 @@
import 'package:flutter/material.dart';
class CakePayCardImagePlaceholder extends StatelessWidget {
const CakePayCardImagePlaceholder({this.text});
final String? text;
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1.8,
child: Container(
child: Center(
child: Text(
text ?? 'Image not found!',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w900,
),
),
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
),
),
);
}
}
@@ -1,65 +0,0 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class ClickableLinksText extends StatelessWidget {
const ClickableLinksText({
required this.text,
required this.textStyle,
this.linkStyle,
});
final String text;
final TextStyle textStyle;
final TextStyle? linkStyle;
@override
Widget build(BuildContext context) {
List<InlineSpan> spans = [];
RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
Iterable<Match> matches = linkRegExp.allMatches(text);
int previousEnd = 0;
matches.forEach((match) {
if (match.start > previousEnd) {
spans.add(TextSpan(text: text.substring(previousEnd, match.start), style: textStyle));
}
String url = text.substring(match.start, match.end);
if (url.toLowerCase().endsWith('.md')) {
spans.add(
TextSpan(
text: url,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontSize: 18,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
}
},
),
);
} else {
spans.add(
TextSpan(
text: url,
style: linkStyle,
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(Uri.parse(url));
},
),
);
}
previousEnd = match.end;
});
if (previousEnd < text.length) {
spans.add(TextSpan(text: text.substring(previousEnd), style: textStyle));
}
return RichText(text: TextSpan(children: spans));
}
}
@@ -1,27 +0,0 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class RoundedCheckbox extends StatelessWidget {
RoundedCheckbox({Key? key, required this.value}) : super(key: key);
final bool value;
@override
Widget build(BuildContext context) {
return value
? Container(
height: 20.0,
width: 20.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(50.0)),
color: Theme.of(context).colorScheme.primary,
),
child: Icon(
Icons.check,
color: Theme.of(context).colorScheme.surface,
size: 14.0,
))
: Offstage();
}
}
@@ -1,35 +0,0 @@
import 'package:flutter/material.dart';
class TextIconButton extends StatelessWidget {
const TextIconButton({
Key? key,
required this.label,
this.onTap,
}) : super(key: key);
final String label;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
Icon(
Icons.chevron_right_rounded,
color: Theme.of(context).colorScheme.onSurface,
),
],
),
);
}
}
@@ -78,11 +78,14 @@ class _FilterWidgetState extends State<FilterWidget> {
children: [
Padding(
padding: const EdgeInsets.all(24.0),
child: Text(
S.of(context).filter_by,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
child: Align(
alignment: Alignment.center,
child: Text(
S.of(context).filter_by,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
),
),
),
@@ -105,7 +108,7 @@ class _FilterWidgetState extends State<FilterWidget> {
final Widget sectionListView = ListView.builder(
controller: isSectionScrollable ? _scrollController : null,
padding: const EdgeInsets.symmetric(horizontal: 28.0),
padding: const EdgeInsets.symmetric(horizontal: 28.0, vertical: 8.0),
shrinkWrap: isSectionScrollable ? false : true,
physics: isSectionScrollable
? const BouncingScrollPhysics()
@@ -1,5 +1,5 @@
import 'package:cake_wallet/src/widgets/alert_close_button.dart';
import 'package:cake_wallet/src/screens/cake_pay/widgets/rounded_checkbox.dart';
import 'package:cake_wallet/src/widgets/rounded_checkbox.dart';
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
@@ -3,6 +3,7 @@ import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
@@ -274,6 +275,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
return ConfirmSendingBottomSheet(
key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'),
currentTheme: widget.currentTheme,
footerType: FooterType.slideActionButton,
walletType: widget.exchangeTradeViewModel.sendViewModel.walletType,
titleText: S.of(bottomSheetContext).confirm_transaction,
titleIconPath:
@@ -293,7 +295,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel
.pendingTransactionFeeFiatAmountFormatted,
outputs: widget.exchangeTradeViewModel.sendViewModel.outputs,
onSlideComplete: () async {
onSlideActionComplete: () async {
if (bottomSheetContext.mounted) {
Navigator.of(bottomSheetContext).pop();
}
@@ -317,11 +319,12 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
builder: (BuildContext bottomSheetContext) {
return InfoBottomSheet(
currentTheme: widget.currentTheme,
footerType: FooterType.singleActionButton,
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/birthday_cake.png',
actionButtonText: S.of(bottomSheetContext).close,
actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
actionButton: () {
singleActionButtonText: S.of(bottomSheetContext).close,
singleActionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
onSingleActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
if (mounted) {
Navigator.of(context).pushNamedAndRemoveUntil(
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/screens/integrations/deuro/widgets/interest_card
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart';
import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
@@ -166,6 +167,7 @@ class DEuroSavingsPage extends BasePage {
isScrollControlled: true,
builder: (BuildContext bottomSheetContext) => ConfirmSendingBottomSheet(
key: ValueKey('savings_page_confirm_sending_dialog_key'),
footerType: FooterType.slideActionButton,
titleText: title,
currentTheme: currentTheme,
walletType: WalletType.ethereum,
@@ -182,7 +184,7 @@ class DEuroSavingsPage extends BasePage {
feeValue: tx.feeFormatted,
feeFiatAmount: _dEuroViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: [],
onSlideComplete: () async {
onSlideActionComplete: () async {
Navigator.of(bottomSheetContext).pop(true);
dEuroViewModel.commitTransaction();
},
@@ -201,6 +203,7 @@ class DEuroSavingsPage extends BasePage {
isScrollControlled: true,
builder: (BuildContext bottomSheetContext) => ConfirmSendingBottomSheet(
key: ValueKey('savings_page_confirm_approval_dialog_key'),
footerType: FooterType.slideActionButton,
titleText: S.of(bottomSheetContext).approve_tokens,
currentTheme: currentTheme,
walletType: WalletType.ethereum,
@@ -213,7 +216,7 @@ class DEuroSavingsPage extends BasePage {
feeValue: tx.feeFormatted,
feeFiatAmount: "",
outputs: [],
onSlideComplete: () {
onSlideActionComplete: () {
Navigator.of(bottomSheetContext).pop(true);
dEuroViewModel.commitApprovalTransaction();
},
@@ -234,13 +237,14 @@ class DEuroSavingsPage extends BasePage {
context: context,
isDismissible: false,
builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
footerType: FooterType.singleActionButton,
currentTheme: currentTheme,
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/birthday_cake.png',
content: S.of(bottomSheetContext).deuro_tx_commited_content,
actionButtonText: S.of(bottomSheetContext).close,
actionButtonKey: ValueKey('savings_page_sent_dialog_ok_button_key'),
actionButton: () => Navigator.of(bottomSheetContext).pop(),
singleActionButtonText: S.of(bottomSheetContext).close,
singleActionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
),
);
});
@@ -302,13 +306,13 @@ class DEuroSavingsPage extends BasePage {
titleText: title,
titleIconPath: CryptoCurrency.deuro.iconPath,
content: content,
isTwoAction: true,
rightButtonText: S.of(context).close,
footerType: FooterType.doubleActionButton,
doubleActionRightButtonText: S.of(context).close,
rightActionButtonKey: ValueKey('deuro_page_tooltip_dialog_${key}_ok_button_key'),
actionRightButton: () => Navigator.of(bottomSheetContext).pop(),
leftButtonText: S.of(context).learn_more,
onRightActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
doubleActionLeftButtonText: S.of(context).learn_more,
leftActionButtonKey: ValueKey('deuro_page_tooltip_dialog_${key}_learn_more_button_key'),
actionLeftButton: onLearnMorePressed,
onLeftActionButtonPressed: onLearnMorePressed,
),
);
}
@@ -327,13 +331,13 @@ class DEuroSavingsPage extends BasePage {
contentImage: 'assets/images/deuro_hero.png',
contentImageSize: 200,
content: S.of(context).deuro_savings_welcome_description,
isTwoAction: true,
rightButtonText: S.of(context).close,
footerType: FooterType.doubleActionButton,
doubleActionRightButtonText: S.of(context).close,
rightActionButtonKey: ValueKey('deuro_page_tooltip_dialog_welcome_ok_button_key'),
actionRightButton: () => Navigator.of(bottomSheetContext).pop(),
leftButtonText: S.of(context).learn_more,
onRightActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(),
doubleActionLeftButtonText: S.of(context).learn_more,
leftActionButtonKey: ValueKey('deuro_page_tooltip_dialog_welcome_learn_more_button_key'),
actionLeftButton: () => launchUrlString("https://deuro.com/what-is-deuro.html"),
onLeftActionButtonPressed: () => launchUrlString("https://deuro.com/what-is-deuro.html"),
showDisclaimerText: _dEuroViewModel.isFistTime,
),
);
@@ -352,9 +356,9 @@ class DEuroSavingsPage extends BasePage {
titleIconPath: CryptoCurrency.deuro.iconPath,
contentImage: 'assets/images/deuro_not_enough_eth.png',
content: S.of(context).deuro_tooltip_no_eth,
actionButtonKey: ValueKey('deuro_page_tooltip_dialog_no_eth_ok_button_key'),
actionButtonText: S.of(context).close,
actionButton: () => Navigator.of(bottomSheetContext).pop(),
singleActionButtonKey: ValueKey('deuro_page_tooltip_dialog_no_eth_ok_button_key'),
singleActionButtonText: S.of(context).close,
onSingleActionButtonPressed: () => Navigator.of(bottomSheetContext).pop(), footerType: FooterType.singleActionButton,
),
);
}
@@ -372,6 +376,8 @@ class DEuroSavingsPage extends BasePage {
? S.of(context).deuro_savings_available_to_add
: S.of(context).deuro_savings_available_to_remove,
balance: isAdding ? _dEuroViewModel.accountBalance : _dEuroViewModel.savingsBalance,
footerType: FooterType.none,
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
);
}
@@ -14,18 +14,18 @@ class SavingsEditSheet extends BaseBottomSheet {
super.titleIconPath,
this.balance,
this.balanceTitle,
required super.footerType, required super.maxHeight,
});
@override
Widget contentWidget(BuildContext context) => SizedBox(
height: 600,
height: 500,
child: _SavingsEditBody(
balance: balance,
balanceTitle: balanceTitle,
),
);
@override
Widget footerWidget(BuildContext context) => SizedBox.shrink();
}
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dar
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/brightness_util.dart';
@@ -319,14 +320,13 @@ class QRWidget extends StatelessWidget {
titleText: S.of(context).payjoin_unavailable_sheet_title,
content: S.of(context).payjoin_unavailable_sheet_content,
currentTheme: currentTheme,
isTwoAction: true,
leftButtonText: S.of(context).learn_more,
actionLeftButton: () => launchUrl(
Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin/#payjoin"),
mode: LaunchMode.externalApplication,
),
rightButtonText: S.of(context).ok,
actionRightButton: () => Navigator.of(context).pop(),
footerType: FooterType.doubleActionButton,
doubleActionLeftButtonText: S.of(context).learn_more,
onLeftActionButtonPressed: () => launchUrl(
Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin/#payjoin"),
mode: LaunchMode.externalApplication),
doubleActionRightButtonText: S.of(context).ok,
onRightActionButtonPressed: () => Navigator.of(context).pop(),
),
);
}
+66 -42
View File
@@ -17,12 +17,14 @@ import 'package:cake_wallet/src/widgets/adaptable_page_view.dart';
import 'package:cake_wallet/src/widgets/add_template_button.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/simple_checkbox.dart';
import 'package:cake_wallet/src/widgets/template_tile.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/utils/payment_request.dart';
@@ -569,6 +571,7 @@ class SendPage extends BasePage {
key: ValueKey('send_page_confirm_sending_dialog_key'),
titleText: S.of(bottomSheetContext).confirm_transaction,
currentTheme: currentTheme,
footerType: FooterType.slideActionButton,
walletType: sendViewModel.walletType,
titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath,
currency: sendViewModel.selectedCryptoCurrency,
@@ -581,7 +584,7 @@ class SendPage extends BasePage {
feeValue: sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: sendViewModel.outputs,
onSlideComplete: () async {
onSlideActionComplete: () async {
Navigator.of(bottomSheetContext).pop(true);
sendViewModel.commitTransaction(context);
},
@@ -611,48 +614,69 @@ class SendPage extends BasePage {
bool showContactSheet = (newContactAddress != null && sendViewModel.showAddressBookPopup);
await showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (BuildContext bottomSheetContext) {
return showContactSheet && sendViewModel.ocpRequest == null
? InfoBottomSheet(
currentTheme: currentTheme,
showDontAskMeCheckbox: true,
onCheckboxChanged: (value) => sendViewModel.setShowAddressBookPopup(!value),
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/contact.png',
contentImageColor: Theme.of(context).colorScheme.onSurface,
content: S.of(bottomSheetContext).add_contact_to_address_book,
isTwoAction: true,
leftButtonText: 'No',
rightButtonText: 'Yes',
actionLeftButton: () {
Navigator.of(bottomSheetContext).pop();
if (context.mounted) {
Navigator.of(context)
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
}
RequestReviewHandler.requestReview();
newContactAddress = null;
},
actionRightButton: () {
Navigator.of(bottomSheetContext).pop();
RequestReviewHandler.requestReview();
if (context.mounted) {
Navigator.of(context).pushNamed(Routes.addressBookAddContact,
arguments: newContactAddress);
}
newContactAddress = null;
},
)
await showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (BuildContext bottomSheetContext) {
return showContactSheet && sendViewModel.ocpRequest == null
? InfoBottomSheet(
currentTheme: currentTheme,
footerType: FooterType.doubleActionButton,
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/contact.png',
contentImageColor: Theme.of(context).colorScheme.onSurface,
content: S.of(bottomSheetContext).add_contact_to_address_book,
bottomActionPanel: Padding(
padding: const EdgeInsets.only(left: 34.0),
child: Row(
children: [
SimpleCheckbox(
onChanged: (value) =>
sendViewModel.setShowAddressBookPopup(!value)),
const SizedBox(width: 8),
Text(
'Don’t ask me next time',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.titleLarge!.color,
decoration: TextDecoration.none,
),
),
],
),
),
doubleActionLeftButtonText: 'No',
doubleActionRightButtonText: 'Yes',
onLeftActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
if (context.mounted) {
Navigator.of(context)
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
}
RequestReviewHandler.requestReview();
newContactAddress = null;
},
onRightActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
RequestReviewHandler.requestReview();
if (context.mounted) {
Navigator.of(context).pushNamed(Routes.addressBookAddContact,
arguments: newContactAddress);
}
newContactAddress = null;
},
)
: InfoBottomSheet(
currentTheme: currentTheme,
footerType: FooterType.singleActionButton,
titleText: S.of(bottomSheetContext).transaction_sent,
contentImage: 'assets/images/birthday_cake.png',
actionButtonText: S.of(bottomSheetContext).close,
actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
actionButton: () {
singleActionButtonText: S.of(bottomSheetContext).close,
singleActionButtonKey: ValueKey('send_page_transaction_sent_button_key'),
onSingleActionButtonPressed: () {
Navigator.of(bottomSheetContext).pop();
Future.delayed(Duration.zero, () {
if (context.mounted) {
@@ -710,14 +734,14 @@ class SendPage extends BasePage {
dialogContext = context;
return InfoBottomSheet(
currentTheme: currentTheme,
footerType: FooterType.singleActionButton,
titleText: S.of(context).proceed_on_device,
contentImage:
'assets/images/hardware_wallet/ledger_nano_x.png',
contentImageColor: Theme.of(context).colorScheme.onSurface,
content: S.of(context).proceed_on_device_description,
isTwoAction: false,
actionButtonText: S.of(context).cancel,
actionButton: () {
singleActionButtonText: S.of(context).cancel,
onSingleActionButtonPressed: () {
sendViewModel.state = InitialExecutionState();
Navigator.of(context).pop();
},
@@ -8,6 +8,7 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_expandab
import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
@@ -200,7 +201,9 @@ class RBFDetailsPage extends BasePage {
feeValue: transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: transactionDetailsViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: transactionDetailsViewModel.sendViewModel.outputs,
onSlideComplete: () async {
footerType: FooterType.slideActionButton,
accessibleNavigationModeSlideActionButtonText: S.of(context).send,
onSlideActionComplete: () async {
Navigator.of(bottomSheetContext).pop();
await transactionDetailsViewModel.sendViewModel.commitTransaction(context);
try {