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,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 {
+14 -11
View File
@@ -3,28 +3,31 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:flutter/material.dart';
class AlertBackground extends StatelessWidget {
AlertBackground({required this.child});
const AlertBackground({Key? key, required this.child, this.dismissible = false});
final Widget child;
final bool dismissible;
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.transparent,
body: Container(
height: double.infinity,
width: double.infinity,
color: Colors.transparent,
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: dismissible ? () => Navigator.of(context).pop() : null,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface.withOpacity(0.8)),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface.withAlpha(200)),
child: Center(
child: Container(
width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
child: child,
child: GestureDetector(
onTap: () {},
child: ConstrainedBox(
constraints:
BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
child: child,
),
),
),
),
+10 -1
View File
@@ -43,7 +43,9 @@ class BaseTextFormField extends StatelessWidget {
this.onFieldSubmitted,
this.hasUnderlineBorder = false,
this.borderWidth = 1.0,
super.key,
this.prefixIconConstraints,
this.suffixIconConstraints,
super.key, this.suffixText,
});
final TextEditingController? controller;
@@ -70,6 +72,9 @@ class BaseTextFormField extends StatelessWidget {
final bool readOnly;
final bool? enableInteractiveSelection;
final String? initialValue;
final BoxConstraints? prefixIconConstraints;
final BoxConstraints? suffixIconConstraints;
final String? suffixText;
final void Function(String)? onSubmit;
final bool obscureText;
final bool? autofocus;
@@ -120,8 +125,12 @@ class BaseTextFormField extends StatelessWidget {
alignLabelWithHint: alignLabelWithHint,
contentPadding: contentPadding,
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIconConstraints: prefixIconConstraints ??
const BoxConstraints(minWidth: 0, minHeight: 0),
prefix: prefix,
prefixIcon: prefixIcon,
suffixIconConstraints: suffixIconConstraints ??
const BoxConstraints(minWidth: 0, minHeight: 0),
suffix: suffix,
suffixIcon: suffixIcon,
filled: !hasUnderlineBorder,
@@ -1,62 +1,53 @@
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:flutter/material.dart';
enum FooterType { none, slideActionButton, singleActionButton, doubleActionButton }
abstract class BaseBottomSheet extends StatelessWidget {
const BaseBottomSheet({
super.key,
required this.titleText,
this.titleIconPath,
required this.footerType,
this.currentTheme,
this.slideActionButtonText,
this.onSlideActionComplete,
this.singleActionButtonText,
this.accessibleNavigationModeSlideActionButtonText,
this.onSingleActionButtonPressed,
this.singleActionButtonKey,
this.doubleActionLeftButtonText,
this.doubleActionRightButtonText,
this.onLeftActionButtonPressed,
this.onRightActionButtonPressed,
this.leftActionButtonKey,
this.rightActionButtonKey,
required this.maxHeight,
}) : assert(footerType == FooterType.none || currentTheme != null,
'currentTheme is required unless footerType is none');
final String titleText;
final String? titleIconPath;
final MaterialThemeBase? currentTheme;
final FooterType footerType;
final String? slideActionButtonText;
final VoidCallback? onSlideActionComplete;
final String? singleActionButtonText;
final String? accessibleNavigationModeSlideActionButtonText;
final VoidCallback? onSingleActionButtonPressed;
final Key? singleActionButtonKey;
final String? doubleActionLeftButtonText;
final String? doubleActionRightButtonText;
final VoidCallback? onLeftActionButtonPressed;
final VoidCallback? onRightActionButtonPressed;
final Key? leftActionButtonKey;
final Key? rightActionButtonKey;
final double maxHeight;
const BaseBottomSheet({required this.titleText, this.titleIconPath, this.maxHeight = 900});
Widget headerWidget(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
const Spacer(flex: 4),
Expanded(
flex: 2,
child: Container(
height: 6,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).colorScheme.onSurface,
),
),
),
const Spacer(flex: 4),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (titleIconPath != null)
Image.asset(titleIconPath!, height: 24, width: 24, excludeFromSemantics: true)
else
Container(),
const SizedBox(width: 6),
Text(
titleText,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
fontSize: 20,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
],
),
const SizedBox(height: 24),
],
);
}
Widget contentWidget(BuildContext context);
Widget footerWidget(BuildContext context);
@override
Widget build(BuildContext context) {
return ConstrainedBox(
@@ -69,13 +60,106 @@ abstract class BaseBottomSheet extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
headerWidget(context),
_buildHeader(context),
contentWidget(context),
footerWidget(context),
_buildFooter(context),
],
),
),
),
);
}
Widget _buildHeader(BuildContext context) => Column(
children: [
const SizedBox(height: 12),
Container(
width: 64,
height: 5,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (titleIconPath != null) ...[
Image.asset(titleIconPath!, height: 24, width: 24),
const SizedBox(width: 6),
],
Text(
titleText,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
fontSize: 20,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
],
),
const SizedBox(height: 13),
],
);
Widget _buildFooter(BuildContext context) {
switch (footerType) {
case FooterType.none:
return const SizedBox.shrink();
case FooterType.slideActionButton:
return Padding(
padding: const EdgeInsets.fromLTRB(40, 12, 40, 34),
child: StandardSlideButton(
buttonText: slideActionButtonText ?? '',
onSlideComplete: onSlideActionComplete ?? () {},
currentTheme: currentTheme!,
accessibleNavigationModeButtonText: accessibleNavigationModeSlideActionButtonText ?? '',
),
);
case FooterType.singleActionButton:
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 34),
child: LoadingPrimaryButton(
key: singleActionButtonKey,
text: singleActionButtonText ?? '',
onPressed: onSingleActionButtonPressed ?? () {},
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: false,
isDisabled: false,
),
);
case FooterType.doubleActionButton:
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 34),
child: Row(
children: [
Expanded(
child: PrimaryButton(
key: leftActionButtonKey,
text: doubleActionLeftButtonText ?? '',
onPressed: onLeftActionButtonPressed,
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.onSecondaryContainer
),
),
const SizedBox(width: 12),
Expanded(
child: PrimaryButton(
key: rightActionButtonKey,
text: doubleActionRightButtonText ?? '',
onPressed: onRightActionButtonPressed,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
],
),
);
}
}
}
@@ -0,0 +1,352 @@
import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
import 'package:cake_wallet/cake_pay/src/widgets/flip_card_widget.dart';
import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
import 'base_bottom_sheet_widget.dart';
class CakePayCardInfoBottomSheet extends BaseBottomSheet {
CakePayCardInfoBottomSheet({
required String titleText,
required MaterialThemeBase currentTheme,
required FooterType footerType,
String? titleIconPath,
String? singleActionButtonText,
VoidCallback? onSingleActionButtonPressed,
Key? singleActionButtonKey,
String? doubleActionLeftButtonText,
String? doubleActionRightButtonText,
VoidCallback? onLeftActionButtonPressed,
VoidCallback? onRightActionButtonPressed,
Key? rightActionButtonKey,
Key? leftActionButtonKey,
required this.onUpdateBalancePressed,
required this.isReloadable,
required this.balance,
this.contentImage,
this.howToUse,
this.applyBoxShadow = false,
Key? key,
}) : _currentTheme = currentTheme,
super(
titleText: titleText,
maxHeight: 900,
titleIconPath: titleIconPath,
currentTheme: currentTheme,
footerType: footerType,
singleActionButtonText: singleActionButtonText,
onSingleActionButtonPressed: onSingleActionButtonPressed,
singleActionButtonKey: singleActionButtonKey,
doubleActionLeftButtonText: doubleActionLeftButtonText,
doubleActionRightButtonText: doubleActionRightButtonText,
onLeftActionButtonPressed: onLeftActionButtonPressed,
onRightActionButtonPressed: onRightActionButtonPressed,
leftActionButtonKey: leftActionButtonKey,
rightActionButtonKey: rightActionButtonKey,
key: key);
final VoidCallback onUpdateBalancePressed;
final MaterialThemeBase _currentTheme;
final String? contentImage;
final String? howToUse;
final bool isReloadable;
final String balance;
final bool applyBoxShadow;
final _cardKey = GlobalKey<FlipCardState>();
@override
Widget contentWidget(BuildContext context) {
final itemTitleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
);
final itemSubTitleTextStyle = Theme.of(context).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurfaceVariant,
decoration: TextDecoration.none,
);
final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (contentImage != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 16),
child: AspectRatio(
aspectRatio: 1.6,
child: FlipCard(
key: _cardKey,
flipOnTouch: true,
front: _buildCardImage(context, contentImage!, applyBoxShadow),
back: _buildBarcodeSide(context,
cardNumber: '6006491979836784204', pin: '4782'),
),
),
),
Positioned(
bottom: 0,
right: 0,
child: GestureDetector(
onTap: () => _cardKey.currentState?.toggleCard(),
child: Container(
height: 43,
width: 43,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 1),
color: Colors.white.withAlpha(75),
shape: BoxShape.circle,
),
child: Transform.scale(
scale: .8,
child: const ImageIcon(
AssetImage('assets/images/transfer.png'),
color: Colors.white,
),
)),
),
),
],
),
),
Container(),
Text(
'Tap card to show details',
style: itemTitleTextStyle.copyWith(fontSize: 12, fontWeight: FontWeight.w700),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
children: [
const SizedBox(height: 34),
CakePayInfoTile(
isReloadable: isReloadable,
itemValue: balance,
itemTitleTextStyle: itemTitleTextStyle,
itemSubTitleTextStyle: itemSubTitleTextStyle,
tileBackgroundColor: tileBackgroundColor),
const SizedBox(height: 8),
_HowToUseTile(howToUse: howToUse ?? '', tileBackgroundColor: tileBackgroundColor),
const SizedBox(height: 40),
],
),
),
],
);
}
}
class _HowToUseTile extends StatelessWidget {
const _HowToUseTile({required this.howToUse, required this.tileBackgroundColor});
final String howToUse;
final Color tileBackgroundColor;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => _showHowToUseCard(context: context, howToUse: howToUse),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).how_to_use_card,
style: Theme.of(context).textTheme.bodyLarge),
Icon(
Icons.chevron_right_rounded,
color: Theme.of(context).textTheme.titleLarge!.color!,
),
],
)),
);
}
}
class CakePayInfoTile extends StatelessWidget {
const CakePayInfoTile({
super.key,
required this.isReloadable,
required this.itemValue,
required this.itemTitleTextStyle,
this.itemSubTitle,
required this.itemSubTitleTextStyle,
required this.tileBackgroundColor,
});
final bool isReloadable;
final String itemValue;
final TextStyle itemTitleTextStyle;
final String? itemSubTitle;
final TextStyle itemSubTitleTextStyle;
final Color tileBackgroundColor;
@override
Widget build(BuildContext context) {
return Semantics(
container: true,
label: isReloadable ? 'Balance' : 'Total Value',
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(isReloadable ? 'Balance' : 'Total Value', style: itemTitleTextStyle),
Text(itemValue,
style: itemTitleTextStyle.copyWith(fontSize: 18, fontWeight: FontWeight.w600)),
],
),
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Text(isReloadable ? 'Top Up Balance' : 'Manually update Balance',
style: itemTitleTextStyle.copyWith(fontSize: 12, fontWeight: FontWeight.w600)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
color: Theme.of(context).dialogBackgroundColor)),
const SizedBox(height: 4),
],
),
),
);
}
}
void _showHowToUseCard({required BuildContext context, String? howToUse}) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return CakePayAlertModal(
title: S.of(context).how_to_use_card,
content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
ClickableLinksText(
text: howToUse ?? '',
textStyle: Theme.of(context).textTheme.bodyMedium!,
linkStyle: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color!,
fontSize: 18,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.w400,
),
),
]),
actionTitle: S.current.got_it,
);
});
}
Widget _buildCardImage(BuildContext ctx, String path, bool addShadow) {
final border = BorderRadius.circular(10);
return Container(
decoration: addShadow
? BoxDecoration(
borderRadius: border,
boxShadow: [
BoxShadow(color: Colors.black.withAlpha(150), blurRadius: 5)
],
)
: null,
child: ClipRRect(
borderRadius: border,
child: Stack(
fit: StackFit.expand,
children: [
Container(color: Theme.of(ctx).cardColor.withAlpha(200)),
ImageUtil.getImageFromPath(
imagePath: path,
fit: BoxFit.cover,
),
],
),
),
);
}
Widget _buildBarcodeSide(BuildContext context, {required String cardNumber, required String pin}) =>
SizedBox.expand(
child: Container(
padding: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 34),
decoration: BoxDecoration(
color: Theme.of(context).cardColor.withAlpha(200),
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(color: Colors.black.withAlpha(150), blurRadius: 5)
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
color: Theme.of(context).textTheme.titleLarge!.color!.withOpacity(.1),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Gift Card Number',
style: TextStyle(
fontFamily: 'Lato',
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(207, 207, 207, 1))),
const SizedBox(height: 4),
Text(cardNumber,
style: TextStyle(
fontFamily: 'Lato',
fontSize: 12,
fontWeight: FontWeight.w900,
color: Color.fromRGBO(146, 146, 146, 1))),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('PIN Number',
style: TextStyle(
fontFamily: 'Lato',
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color.fromRGBO(207, 207, 207, 1))),
const SizedBox(height: 4),
Text(pin,
style: TextStyle(
fontFamily: 'Lato',
fontSize: 12,
fontWeight: FontWeight.w900,
color: Color.fromRGBO(146, 146, 146, 1))),
],
),
],
),
],
),
),
);
@@ -0,0 +1,239 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
class CakePayTransactionSentBottomSheet extends StatelessWidget {
const CakePayTransactionSentBottomSheet({
super.key,
required this.titleText,
required this.currency,
required this.amount,
required this.amountValue,
required this.fiatAmountValue,
required this.output,
required this.fee,
required this.feeValue,
required this.feeFiatAmount,
this.titleIconWidget,
required this.quantity,
required this.onClose,
required this.paymentId,
required this.paymentIdValue,
});
final String titleText;
final Widget? titleIconWidget;
final CryptoCurrency currency;
final String amount;
final String amountValue;
final String fiatAmountValue;
final Output output;
final String fee;
final String feeValue;
final String feeFiatAmount;
final String quantity;
final VoidCallback onClose;
final String paymentId;
final String paymentIdValue;
TextStyle _titleStyle(BuildContext ctx) => Theme.of(ctx).textTheme.bodyLarge!;
TextStyle _valueStyle(BuildContext ctx) => Theme.of(ctx).textTheme.titleMedium!.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
);
TextStyle _subStyle(BuildContext ctx) => Theme.of(ctx).textTheme.labelSmall!.copyWith(
color: Theme.of(ctx).colorScheme.onSurfaceVariant,
);
Widget _buildHeader(BuildContext ctx) => Column(
children: [
const SizedBox(height: 12),
Container(
width: 64,
height: 5,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Theme.of(ctx).colorScheme.onSurface,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (titleIconWidget != null) titleIconWidget!,
const SizedBox(width: 6),
Text(
titleText,
style: Theme.of(ctx).textTheme.titleLarge!.copyWith(
fontSize: 20,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
],
),
const SizedBox(height: 12),
],
);
Widget _buildBody(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
children: [
_StandardTile(
itemTitle: amount,
titleStyle: _titleStyle(context),
itemValue: '$amountValue ${currency.title}',
itemValueStyle: _valueStyle(context),
itemSubTitle: fiatAmountValue,
itemSubTitleStyle: _subStyle(context),
),
const SizedBox(height: 8),
_StandardTile(
itemTitle: fee,
titleStyle: _titleStyle(context),
itemValue: feeValue,
itemValueStyle: _valueStyle(context),
itemSubTitle: feeFiatAmount,
itemSubTitleStyle: _subStyle(context),
),
const SizedBox(height: 8),
_StandardTile(
itemTitle: output.parsedAddress.profileName,
titleStyle: _titleStyle(context),
itemValue: output.fiatAmount,
itemValueStyle: _valueStyle(context),
itemSubTitle: quantity,
itemSubTitleStyle: _subStyle(context),
imagePath: output.parsedAddress.profileImageUrl,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Column(
children: [
Text(paymentId + ': ' + paymentIdValue,
style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center),
const SizedBox(height: 18),
Image.asset('assets/images/envelope.png'),
const SizedBox(height: 18),
Text(
S.of(context).cake_pay_card_email_delivered_message,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
PrimaryButton(
text: S.of(context).close,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimaryContainer,
onPressed: onClose,
),
],
),
),
],
),
);
@override
Widget build(BuildContext context) {
final maxHeight = MediaQuery.of(context).size.height * 0.9;
return ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: Material(
color: Theme.of(context).colorScheme.surface,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildHeader(context),
_buildBody(context),
],
),
),
),
),
);
}
}
class _StandardTile extends StatelessWidget {
const _StandardTile({
required this.itemTitle,
required this.titleStyle,
required this.itemValue,
required this.itemValueStyle,
this.itemSubTitle,
this.itemSubTitleStyle,
this.imagePath,
});
final String itemTitle;
final TextStyle titleStyle;
final String itemValue;
final TextStyle itemValueStyle;
final String? itemSubTitle;
final TextStyle? itemSubTitleStyle;
final String? imagePath;
@override
Widget build(BuildContext context) {
return Semantics(
container: true,
label: itemTitle,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).colorScheme.surfaceContainerLowest.withAlpha(80)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
if (imagePath != null)
Padding(
padding: const EdgeInsets.only(right: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: ImageUtil.getImageFromPath(
imagePath: imagePath!, height: 40, width: 40),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
itemTitle,
style: titleStyle,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
)),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(itemValue, style: itemValueStyle.copyWith(height: 1.0)),
if (itemSubTitle != null) Text(itemSubTitle!, style: itemSubTitleStyle),
],
),
],
),
),
);
}
}
@@ -1,39 +1,29 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'base_bottom_sheet_widget.dart';
class ConfirmSendingBottomSheet extends BaseBottomSheet {
final CryptoCurrency currency;
final MaterialThemeBase currentTheme;
final String? paymentId;
final String? paymentIdValue;
final String? expirationTime;
final String amount;
final String amountValue;
final String fiatAmountValue;
final String fee;
final String feeValue;
final String feeFiatAmount;
final String? explanation;
final List<Output> outputs;
final VoidCallback onSlideComplete;
final WalletType walletType;
final PendingChange? change;
final bool isOpenCryptoPay;
ConfirmSendingBottomSheet({
required String titleText,
required MaterialThemeBase currentTheme,
required FooterType footerType,
String? titleIconPath,
String? slideActionButtonText,
VoidCallback? onSlideActionComplete,
String? accessibleNavigationModeSlideActionButtonText,
required this.currency,
required this.currentTheme,
this.paymentId,
this.paymentIdValue,
this.expirationTime,
@@ -44,14 +34,45 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
required this.feeValue,
required this.feeFiatAmount,
required this.outputs,
required this.onSlideComplete,
required this.walletType,
this.change,
this.explanation,
this.isOpenCryptoPay = false,
this.cakePayBuyCardViewModel,
this.quantity,
Key? key,
}) : showScrollbar = outputs.length > 3,
super(titleText: titleText, titleIconPath: titleIconPath);
_currentTheme = currentTheme,
super(
titleText: titleText,
maxHeight: 900,
titleIconPath: titleIconPath,
currentTheme: currentTheme,
footerType: footerType,
slideActionButtonText: slideActionButtonText ?? 'Swipe to send',
onSlideActionComplete: onSlideActionComplete,
accessibleNavigationModeSlideActionButtonText:
accessibleNavigationModeSlideActionButtonText,
key: key);
final CryptoCurrency currency;
final MaterialThemeBase _currentTheme;
final String? paymentId;
final String? paymentIdValue;
final String? expirationTime;
final String amount;
final String amountValue;
final String fiatAmountValue;
final String fee;
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
final WalletType walletType;
final PendingChange? change;
final bool isOpenCryptoPay;
final CakePayBuyCardViewModel? cakePayBuyCardViewModel;
final String? quantity;
final String? explanation;
final bool showScrollbar;
final ScrollController scrollController = ScrollController();
@@ -69,25 +90,32 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
decoration: TextDecoration.none,
);
final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
final tileBackgroundColor = _currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark.withAlpha(140)
: CustomThemeColors.cardGradientColorPrimaryLight;
Widget content = Padding(
padding: EdgeInsets.fromLTRB(8, 0, showScrollbar ? 16 : 8, 8),
child: Column(
children: [
if (paymentId != null && paymentIdValue != null)
if (paymentId != null && paymentIdValue != null && cakePayBuyCardViewModel != null)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: AddressTile(
itemTitle: paymentId!,
itemTitleTextStyle: itemTitleTextStyle,
walletType: walletType,
isBatchSending: false,
amount: '',
address: paymentIdValue!,
itemSubTitleTextStyle: itemSubTitleTextStyle,
tileBackgroundColor: tileBackgroundColor,
),
child: Observer(
builder: (_) => AddressTile(
itemTitle: paymentId!,
itemTitleTextStyle: itemTitleTextStyle,
amountTextStyle: itemSubTitleTextStyle,
walletType: walletType,
amount: expirationTime != null
? S.current.offer_expires_in +
' ${cakePayBuyCardViewModel!.formattedRemainingTime}'
: null,
address: paymentIdValue!,
itemSubTitleTextStyle: itemSubTitleTextStyle,
tileBackgroundColor: tileBackgroundColor,
applyAddressFormatting: false,
)),
),
if (explanation != null)
Padding(
@@ -128,12 +156,13 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
final bool isBatchSending = outputs.length > 1;
final item = outputs[index];
final contactName = item.parsedAddress.name;
final isCakePayName = contactName == 'Cake Pay';
final batchContactTitle =
'${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}';
final _address = item.isParsedAddress ? item.extractedAddress : item.address;
final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}';
return isBatchSending || contactName.isNotEmpty
? AddressExpansionTile(
return isBatchSending || (contactName.isNotEmpty && !isCakePayName)
? ExpansionAddressTile(
contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
name: isBatchSending ? batchContactTitle : contactName,
address: _address,
@@ -145,12 +174,15 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
tileBackgroundColor: tileBackgroundColor,
)
: AddressTile(
itemTitle: S.of(context).address,
itemTitle: isCakePayName
? item.parsedAddress.profileName
: S.of(context).address,
imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null,
itemTitleTextStyle: itemTitleTextStyle,
isBatchSending: isBatchSending,
walletType: walletType,
amount: _amount,
amount: isCakePayName ? item.fiatAmount : _amount,
address: _address,
itemSubTitle: isCakePayName ? quantity : null,
itemSubTitleTextStyle: itemSubTitleTextStyle,
tileBackgroundColor: tileBackgroundColor,
);
@@ -159,7 +191,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
if (change != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: AddressExpansionTile(
child: ExpansionAddressTile(
contactType: 'Change',
name: S.of(context).send_change_to_you,
address: change!.address,
@@ -195,31 +227,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
return content;
}
}
@override
Widget footerWidget(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(40, 12, 40, 34),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
if (showScrollbar)
BoxShadow(
color: Theme.of(context).colorScheme.outlineVariant,
spreadRadius: 2,
blurRadius: 10,
offset: const Offset(0, 0),
),
],
),
child: StandardSlideButton(
onSlideComplete: onSlideComplete,
buttonText: 'Swipe to send',
currentTheme: currentTheme,
accessibleNavigationModeButtonText: S.of(context).send,
),
);
}
}
class StandardTile extends StatelessWidget {
@@ -304,22 +311,28 @@ class AddressTile extends StatelessWidget {
super.key,
required this.itemTitle,
required this.itemTitleTextStyle,
required this.isBatchSending,
required this.amount,
required this.address,
required this.itemSubTitleTextStyle,
required this.tileBackgroundColor,
required this.walletType,
this.amountTextStyle,
this.applyAddressFormatting = true,
this.imagePath,
this.amount,
this.itemSubTitle,
});
final String itemTitle;
final TextStyle itemTitleTextStyle;
final bool isBatchSending;
final String amount;
final String? amount;
final String address;
final TextStyle itemSubTitleTextStyle;
final TextStyle? amountTextStyle;
final Color tileBackgroundColor;
final WalletType walletType;
final bool applyAddressFormatting;
final String? imagePath;
final String? itemSubTitle;
@override
Widget build(BuildContext context) {
@@ -335,26 +348,65 @@ class AddressTile extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(itemTitle, style: itemTitleTextStyle),
if (isBatchSending) Text(amount, style: itemTitleTextStyle),
Expanded(
child: Row(
children: [
if (imagePath != null)
Padding(
padding: const EdgeInsets.only(right: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: ImageUtil.getImageFromPath(
imagePath: imagePath!, height: 40, width: 40),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(
itemTitle,
style: itemTitleTextStyle,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
)),
),
],
),
),
if (amount != null) Text(amount!, style: amountTextStyle ?? itemTitleTextStyle),
],
),
AddressFormatter.buildSegmentedAddress(
address: address,
walletType: walletType,
evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
address.isEmpty
? Container()
: applyAddressFormatting
? AddressFormatter.buildSegmentedAddress(
address: address,
walletType: walletType,
evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
))
: Text(
address,
style: itemTitleTextStyle,
),
itemSubTitle == null
? Container()
: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(itemSubTitle!, style: itemSubTitleTextStyle),
],
),
),
],
),
);
}
}
class AddressExpansionTile extends StatelessWidget {
const AddressExpansionTile({
class ExpansionAddressTile extends StatelessWidget {
const ExpansionAddressTile({
super.key,
required this.contactType,
required this.name,
@@ -425,7 +477,6 @@ class AddressExpansionTile extends StatelessWidget {
walletType: walletType,
evenTextStyle: Theme.of(context).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
),
@@ -1,74 +1,96 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cake_wallet/src/widgets/simple_checkbox.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'base_bottom_sheet_widget.dart';
class LoadingBottomSheet extends BaseBottomSheet {
LoadingBottomSheet({required String titleText, String? titleIconPath})
: super(titleText: titleText, titleIconPath: titleIconPath);
LoadingBottomSheet(
{required String titleText, String? titleIconPath})
: super(titleText: titleText, titleIconPath: titleIconPath, footerType: FooterType.none, maxHeight: 900);
@override
Widget contentWidget(BuildContext context) {
return SizedBox(
height: 200,
height: 300,
child: Center(child: CircularProgressIndicator()),
);
}
@override
Widget footerWidget(BuildContext context) => const SizedBox(height: 94);
}
class InfoBottomSheet extends BaseBottomSheet {
final MaterialThemeBase currentTheme;
final String? contentImage;
final Color? contentImageColor;
final String? content;
final bool isTwoAction;
final bool showDontAskMeCheckbox;
final bool showDisclaimerText;
final Function(bool)? onCheckboxChanged;
final String? actionButtonText;
final VoidCallback? actionButton;
final Key? actionButtonKey;
final String? leftButtonText;
final String? rightButtonText;
final VoidCallback? actionLeftButton;
final VoidCallback? actionRightButton;
final Key? rightActionButtonKey;
final Key? leftActionButtonKey;
final double height;
final double? contentImageSize;
InfoBottomSheet({
required String titleText,
String? titleIconPath,
required this.currentTheme,
required this.footerType,
this.contentImage,
this.contentImageColor,
this.contentImageSize,
this.content,
this.isTwoAction = false,
this.showDontAskMeCheckbox = false,
this.showDisclaimerText = false,
this.height = 200,
this.onCheckboxChanged,
this.actionButtonText,
this.actionButton,
this.actionButtonKey,
this.leftButtonText,
this.rightButtonText,
this.actionLeftButton,
this.actionRightButton,
this.rightActionButtonKey,
this.content,
this.bottomActionPanel,
this.singleActionButtonText,
this.onSingleActionButtonPressed,
this.singleActionButtonKey,
this.doubleActionLeftButtonText,
this.doubleActionRightButtonText,
this.onLeftActionButtonPressed,
this.onRightActionButtonPressed,
this.leftActionButtonKey,
double maxHeight = 900,
}) : super(titleText: titleText, titleIconPath: titleIconPath, maxHeight: maxHeight);
this.rightActionButtonKey,
this.showDisclaimerText = true,
Key? key,
}) : super(
titleText: titleText,
titleIconPath: titleIconPath,
maxHeight: 900,
currentTheme: currentTheme,
footerType: footerType,
singleActionButtonText: singleActionButtonText,
onSingleActionButtonPressed: onSingleActionButtonPressed,
singleActionButtonKey: singleActionButtonKey,
doubleActionLeftButtonText: doubleActionLeftButtonText,
doubleActionRightButtonText: doubleActionRightButtonText,
onLeftActionButtonPressed: onLeftActionButtonPressed,
onRightActionButtonPressed: onRightActionButtonPressed,
leftActionButtonKey: leftActionButtonKey,
rightActionButtonKey: rightActionButtonKey,
key: key);
final MaterialThemeBase currentTheme;
final FooterType footerType;
final String? contentImage;
final Color? contentImageColor;
final String? content;
final Widget? bottomActionPanel;
final String? singleActionButtonText;
final VoidCallback? onSingleActionButtonPressed;
final Key? singleActionButtonKey;
final String? doubleActionLeftButtonText;
final String? doubleActionRightButtonText;
final VoidCallback? onLeftActionButtonPressed;
final VoidCallback? onRightActionButtonPressed;
final Key? rightActionButtonKey;
final Key? leftActionButtonKey;
final double height;
final double? contentImageSize;
final bool showDisclaimerText;
@override
Widget contentWidget(BuildContext context) {
@@ -79,9 +101,11 @@ class InfoBottomSheet extends BaseBottomSheet {
if (contentImage != null)
Expanded(
flex: 4,
child: SizedBox(
width: contentImageSize,
child: getImage(contentImage!, imageColor: contentImageColor),
child: ImageUtil.getImageFromPath(
imagePath: contentImage!,
svgImageColor: contentImageColor,
fit: BoxFit.contain,
borderRadius: 10,
),
)
else
@@ -110,6 +134,7 @@ class InfoBottomSheet extends BaseBottomSheet {
],
),
),
bottomActionPanel ?? const SizedBox(),
if (showDisclaimerText)
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 10),
@@ -141,123 +166,8 @@ class InfoBottomSheet extends BaseBottomSheet {
),
),
),
if (showDontAskMeCheckbox)
Padding(
padding: const EdgeInsets.only(left: 34),
child: Row(
children: [
SimpleCheckbox(onChanged: onCheckboxChanged),
const SizedBox(width: 8),
Text(
'Don’t ask me next time',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
decoration: TextDecoration.none,
),
),
],
),
),
],
),
);
}
@override
Widget footerWidget(BuildContext context) {
if (isTwoAction) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 34),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
child: Container(
padding: const EdgeInsets.only(right: 8.0, top: 8.0),
child: PrimaryButton(
key: leftActionButtonKey,
onPressed: actionLeftButton,
text: leftButtonText ?? '',
color: Theme.of(context).colorScheme.surfaceContainer,
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
),
Flexible(
child: Container(
padding: const EdgeInsets.only(left: 8.0, top: 8.0),
child: PrimaryButton(
key: rightActionButtonKey,
onPressed: actionRightButton,
text: rightButtonText ?? '',
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
),
);
} else {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 34),
child: LoadingPrimaryButton(
key: actionButtonKey,
onPressed: actionButton ?? () {},
text: actionButtonText ?? '',
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: false,
isDisabled: false,
),
);
}
}
Widget getImage(String imagePath, {Color? imageColor}) {
final bool isSvg = imagePath.endsWith('.svg');
if (isSvg) {
return SvgPicture.asset(
imagePath,
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
);
} else {
return Image.asset(imagePath);
}
}
}
class SimpleCheckbox extends StatefulWidget {
SimpleCheckbox({this.onChanged});
final Function(bool)? onChanged;
@override
State<SimpleCheckbox> createState() => _SimpleCheckboxState();
}
class _SimpleCheckboxState extends State<SimpleCheckbox> {
bool initialValue = false;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 24.0,
width: 24.0,
child: Checkbox(
value: initialValue,
onChanged: (value) => setState(() {
initialValue = value!;
widget.onChanged?.call(value);
}),
checkColor: Theme.of(context).colorScheme.onSurfaceVariant,
activeColor: Colors.transparent,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: WidgetStateBorderSide.resolveWith((states) =>
BorderSide(color: Theme.of(context).colorScheme.onSurfaceVariant, width: 1.0)),
),
);
}
}
@@ -21,7 +21,7 @@ class InfoStepsBottomSheet extends BaseBottomSheet {
required this.steps,
String? titleIconPath,
required this.currentTheme,
}) : super(titleText: titleText, titleIconPath: titleIconPath);
}) : super(titleText: titleText, titleIconPath: titleIconPath, footerType: FooterType.none, maxHeight: 900);
@override
Widget contentWidget(BuildContext context) => SizedBox(
+33 -53
View File
@@ -56,67 +56,47 @@ class _NumberTextFieldState extends State<NumberTextField> {
@override
Widget build(BuildContext context) => TextField(
style: textMediumSemiBold(color: Theme.of(context).colorScheme.onSurfaceVariant),
enableInteractiveSelection: false,
textAlign: TextAlign.center,
textAlignVertical: TextAlignVertical.bottom,
controller: _controller,
focusNode: _focusNode,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0),
decoration: InputDecoration(
style: Theme.of(context).textTheme.titleMedium!,
enableInteractiveSelection: false,
textAlign: TextAlign.center,
textAlignVertical: TextAlignVertical.bottom,
controller: _controller,
focusNode: _focusNode,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0),
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.all(0),
fillColor: Colors.transparent,
counterText: '',
isDense: true,
filled: true,
suffixIconConstraints: BoxConstraints(
maxHeight: widget.arrowsHeight,
maxWidth: widget.arrowsWidth + widget.contentPadding.right,
),
prefixIconConstraints: BoxConstraints(
maxHeight: widget.arrowsHeight,
maxWidth: widget.arrowsWidth + widget.contentPadding.left,
),
suffixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
prefixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
prefixIcon: Material(
type: MaterialType.transparency,
child: InkWell(
child: Container(
width: widget.arrowsWidth,
alignment: Alignment.bottomCenter,
child: Icon(
Icons.arrow_left_outlined,
size: widget.arrowsWidth,
),
),
onTap: _canGoDown ? () => _update(false) : null,
),
),
type: MaterialType.transparency,
child: InkWell(
child: Container(
width: widget.arrowsWidth,
alignment: Alignment.bottomCenter,
child: Icon(Icons.keyboard_arrow_left_outlined ,size: widget.arrowsWidth)),
onTap: _canGoDown ? () => _update(false) : null)),
suffixIcon: Material(
type: MaterialType.transparency,
child: InkWell(
child: Container(
width: widget.arrowsWidth,
alignment: Alignment.bottomCenter,
child: Icon(
Icons.arrow_right_outlined,
size: widget.arrowsWidth,
),
),
onTap: _canGoUp ? () => _update(true) : null,
),
),
),
maxLines: 1,
onChanged: (value) {
final intValue = int.tryParse(value);
widget.onChanged?.call(intValue);
_updateArrows(intValue);
},
inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)],
);
type: MaterialType.transparency,
child: InkWell(
child: Container(
width: widget.arrowsWidth,
alignment: Alignment.bottomCenter,
child: Icon(Icons.keyboard_arrow_right_outlined, size: widget.arrowsWidth)),
onTap: _canGoUp ? () => _update(true) : null))),
maxLines: 1,
onChanged: (value) {
final intValue = int.tryParse(value);
widget.onChanged?.call(intValue);
_updateArrows(intValue);
},
inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)]);
void _update(bool up) {
var intValue = int.tryParse(_controller.text);
+34
View File
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class SimpleCheckbox extends StatefulWidget {
SimpleCheckbox({this.onChanged});
final Function(bool)? onChanged;
@override
State<SimpleCheckbox> createState() => _SimpleCheckboxState();
}
class _SimpleCheckboxState extends State<SimpleCheckbox> {
bool initialValue = false;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 24.0,
width: 24.0,
child: Checkbox(
value: initialValue,
onChanged: (value) => setState(() {
initialValue = value!;
widget.onChanged?.call(value);
}),
checkColor: Theme.of(context).textTheme.titleLarge!.color,
activeColor: Colors.transparent,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: WidgetStateBorderSide.resolveWith((states) => BorderSide(
color: Theme.of(context).textTheme.titleLarge!.color!, width: 1.0)),
),
);
}
}
@@ -1,5 +1,6 @@
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:flutter/material.dart';
class StandardSlideButton extends StatefulWidget {
@@ -29,7 +30,9 @@ class _StandardSlideButtonState extends State<StandardSlideButton> {
Widget build(BuildContext context) {
final bool accessible = MediaQuery.of(context).accessibleNavigation;
final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
final tileBackgroundColor = widget.currentTheme.isDark
? CustomThemeColors.backgroundGradientColorDark
: CustomThemeColors.backgroundGradientColorLight;
return accessible
? PrimaryButton(
@@ -87,7 +90,7 @@ class _StandardSlideButtonState extends State<StandardSlideButton> {
height: widget.height - 8,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.surface,
),
alignment: Alignment.center,
child: Icon(
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
class TabViewWrapper extends StatefulWidget {
const TabViewWrapper({
super.key,
required this.tabs,
required this.views,
this.tabBarPadding = const EdgeInsets.only(right: 24),
this.labelStyle,
this.unselectedLabelStyle,
this.indicatorColor,
}) : assert(tabs.length == views.length, 'Tabs and views must be of equal length.');
final List<Tab> tabs;
final List<Widget> views;
final EdgeInsets tabBarPadding;
final TextStyle? labelStyle;
final TextStyle? unselectedLabelStyle;
final Color? indicatorColor;
@override
State<TabViewWrapper> createState() => _TabViewWrapperState();
}
class _TabViewWrapperState extends State<TabViewWrapper> with SingleTickerProviderStateMixin {
late final TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: widget.tabs.length, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final textStyle = TextStyle(
fontSize: 18,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface);
return Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: TabBar(
controller: _tabController,
isScrollable: true,
splashFactory: NoSplash.splashFactory,
indicatorSize: TabBarIndicatorSize.label,
labelStyle: widget.labelStyle ?? textStyle,
unselectedLabelStyle: widget.unselectedLabelStyle ??
textStyle.copyWith(color: textStyle.color?.withAlpha(150)),
labelColor: widget.labelStyle?.color ?? textStyle.color,
indicatorColor: widget.indicatorColor,
indicatorPadding: EdgeInsets.zero,
labelPadding: widget.tabBarPadding,
tabAlignment: TabAlignment.start,
dividerColor: Colors.transparent,
padding: EdgeInsets.zero,
tabs: widget.tabs,
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: widget.views,
),
),
],
);
}
}