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