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

* refactor: cleanup cake_pay folder

* refactor: add My Cards tab on the main screen

* ui: add user card item

* refactor: cakePay purchase flow

* feat: add user card info

* refactor: auth flow

* refactor: theme data

* revert

* Update qr_widget.dart

* fix: update with new themes

* minor fixes

* feat: put redemption flow under a feature flag

* feat: implement simulated purchasing flow in debug mode

* feat: support LTC and MWEB payments

* Update savings_page.dart

* fix: UI fixes

* add Cake Pay transaction sent bottom sheet

* localization

* Update cake_pay_buy_card_page.dart

* addressing review comments

* fix marge conflicts
This commit is contained in:
Serhii
2025-07-11 23:56:35 +03:00
committed by GitHub
parent 203aff83f2
commit ef84db5874
96 changed files with 3907 additions and 2638 deletions
@@ -0,0 +1,73 @@
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:flutter/material.dart';
class CakePayAlertModal extends StatelessWidget {
const CakePayAlertModal({
super.key,
required this.title,
required this.content,
required this.actionTitle,
this.showCloseButton = true,
this.dismissible = false,
});
final String title;
final Widget content;
final String actionTitle;
final bool showCloseButton;
final bool dismissible;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final maxHeight = MediaQuery.of(context).size.height * 0.8;
return AlertBackground(
dismissible: dismissible,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight),
child: Material(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(30)),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (title.isNotEmpty) ...[
Text(title,
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface, fontWeight: FontWeight.bold),
textAlign: TextAlign.center),
const SizedBox(height: 12),
],
Flexible(child: SingleChildScrollView(child: content)),
const SizedBox(height: 24),
PrimaryButton(
onPressed: () => Navigator.pop(context),
text: actionTitle,
color: theme.colorScheme.surfaceContainer,
textColor: theme.colorScheme.primary),
if (showCloseButton) ...[
const SizedBox(height: 16),
InkWell(
onTap: () => Navigator.pop(context),
child: CircleAvatar(
backgroundColor: theme.colorScheme.surfaceContainer,
child: Icon(Icons.close, color: theme.colorScheme.onSurface),
),
),
],
],
),
),
),
),
),
);
}
}
@@ -0,0 +1,126 @@
import 'package:cake_wallet/entities/country.dart';
import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
import 'package:cake_wallet/utils/debounce.dart';
import 'package:flutter/material.dart';
class CakePaySearchBar extends StatefulWidget {
const CakePaySearchBar(
{required this.initialQuery,
required this.onSearch,
required this.onFilter,
this.onCountryPick,
this.controller,
this.selectedCountry});
final String initialQuery;
final ValueChanged<String> onSearch;
final VoidCallback onFilter;
final VoidCallback? onCountryPick;
final Country? selectedCountry;
final TextEditingController? controller;
@override
State<CakePaySearchBar> createState() => _CakePaySearchBarState();
}
class _CakePaySearchBarState extends State<CakePaySearchBar> {
late final TextEditingController _searchController =
widget.controller ?? TextEditingController(text: widget.initialQuery);
final _searchFocusNode = FocusNode();
final _debounce = Debounce(const Duration(milliseconds: 500));
@override
void initState() {
super.initState();
_searchController
.addListener(() => _debounce.run(() => widget.onSearch(_searchController.text)));
}
@override
void dispose() {
_searchController.dispose();
_searchFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 32,
child: Row(
children: [
Expanded(child: SearchBarWidget(searchController: _searchController)),
const SizedBox(width: 5),
_CakePayFilterButton(onFilter: widget.onFilter),
if (widget.selectedCountry != null)
_CountryPickerWidget(
onTap: widget.onCountryPick,
selectedCountry: widget.selectedCountry!,
),
],
),
);
}
}
class _CakePayFilterButton extends StatelessWidget {
const _CakePayFilterButton({required this.onFilter});
final VoidCallback onFilter;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onFilter,
child: Container(
width: 32,
padding: const EdgeInsets.symmetric(vertical: 7),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(10)),
child: Image.asset('assets/images/filter_icon.png',
color: Theme.of(context).colorScheme.onSurface)),
);
}
}
class _CountryPickerWidget extends StatelessWidget {
const _CountryPickerWidget({required this.selectedCountry, this.onTap});
final Country selectedCountry;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 5),
child: GestureDetector(
onTap: onTap,
child: Container(
height: 32,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(10)),
child: Row(
children: [
Image.asset(selectedCountry.iconPath,
width: 24,
height: 24,
errorBuilder: (_, __, ___) => const SizedBox(width: 24, height: 24)),
const SizedBox(width: 6),
Text(
selectedCountry.countryCode,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,47 @@
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,
),
),
],
)
],
),
);
}
}
+82
View File
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'image_placeholder.dart';
class CardItem extends StatelessWidget {
CardItem({
required this.title,
required this.subTitle,
this.isAmount = false,
this.onTap,
this.logoUrl,
});
final VoidCallback? onTap;
final String title;
final String subTitle;
final String? logoUrl;
final bool isAmount;
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: InkWell(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(10),
),
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(fontSize: 18, fontWeight: FontWeight.w700)),
Text(subTitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(fontSize: 10, fontWeight: FontWeight.w700)),
],
),
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,122 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/number_text_fild_widget.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class DenominationsAmountWidget extends StatelessWidget {
const DenominationsAmountWidget(
{required this.fiatCurrency,
required this.denominations,
required this.amountFieldFocus,
required this.amountController,
required this.quantityFieldFocus,
required this.quantityController,
required this.cakePayBuyCardViewModel,
required this.onAmountChanged,
required this.onQuantityChanged});
final String fiatCurrency;
final List<String> denominations;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final FocusNode quantityFieldFocus;
final TextEditingController quantityController;
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
final Function(String) onAmountChanged;
final Function(int?) onQuantityChanged;
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.1,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 8,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownFilterList(
items: denominations,
itemPrefix: fiatCurrency,
selectedItem: denominations.first,
onItemSelected: (value) {
amountController.text = value;
onAmountChanged(value);
}),
const SizedBox(height: 4),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
child: Text(S.of(context).value,
maxLines: 2,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
],
),
),
Spacer(),
Expanded(
flex: 5,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NumberTextField(
controller: quantityController,
focusNode: quantityFieldFocus,
min: 1,
max: 99,
onChanged: (value) => onQuantityChanged(value)),
const SizedBox(height: 4),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: Text(S.of(context).quantity,
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
],
),
),
Spacer(),
Expanded(
flex: 8,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Observer(
builder: (_) => Text('$fiatCurrency ${cakePayBuyCardViewModel.totalAmount}',
maxLines: 1, style: Theme.of(context).textTheme.titleMedium!)),
const SizedBox(height: 4),
Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
child: Text(S.of(context).total,
maxLines: 1,
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
],
)),
],
),
);
}
}
@@ -0,0 +1,103 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/typography.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class EnterAmountWidget extends StatelessWidget {
const EnterAmountWidget(
{required this.minValue,
required this.maxValue,
required this.fiatCurrency,
required this.amountFieldFocus,
required this.amountController,
required this.onAmountChanged});
final String minValue;
final String maxValue;
final String fiatCurrency;
final FocusNode amountFieldFocus;
final TextEditingController amountController;
final Function(String) onAmountChanged;
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Spacer(flex: 1),
Text(
S.of(context).enter_amount,
style: Theme.of(context).textTheme.titleLarge!,
),
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant))),
child: BaseTextFormField(
isDense: true,
contentPadding: EdgeInsets.zero,
controller: amountController,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
hintText: '0.00',
maxLines: 1,
prefixIcon: Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
'$fiatCurrency: ',
style: Theme.of(context).textTheme.titleMedium!,
),
),
prefixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
suffixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
suffixIcon: Padding(
padding: const EdgeInsets.only(top: 2, right: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('QTY ', style: Theme.of(context).textTheme.titleMedium!),
Text(
'1',
style: Theme.of(context).textTheme
.titleMedium!
.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)
),
],
),
),
textStyle: Theme.of(context).textTheme.titleMedium!,
placeholderTextStyle: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')),
FilteringTextInputFormatter.allow(
RegExp(r'^\d+(\.|\,)?\d{0,2}'),
),
],
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).min_amount(minValue) + ' $fiatCurrency',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
)),
Text(S.of(context).max_amount(maxValue) + ' $fiatCurrency',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
)),
],
),
Spacer(flex: 1),
],
),
);
}
}
@@ -0,0 +1,62 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
class FlipCard extends StatefulWidget {
const FlipCard({
super.key,
required this.front,
required this.back,
this.duration = const Duration(milliseconds: 400),
this.flipOnTouch = true,
});
final Widget front;
final Widget back;
final Duration duration;
final bool flipOnTouch;
@override
FlipCardState createState() => FlipCardState();
}
class FlipCardState extends State<FlipCard> with SingleTickerProviderStateMixin {
late final AnimationController _ctrl =
AnimationController(vsync: this, duration: widget.duration);
bool _isFront = true;
void toggleCard() {
if (_isFront) {
_ctrl.forward();
} else {
_ctrl.reverse();
}
_isFront = !_isFront;
}
@override
Widget build(BuildContext context) {
final content = AnimatedBuilder(
animation: _ctrl,
builder: (_, __) {
final angle = _ctrl.value * math.pi;
final isFront = angle < math.pi / 2;
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(angle),
child: isFront ? widget.front
: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi),
child: widget.back,
),
);
},
);
return widget.flipOnTouch
? GestureDetector(onTap: toggleCard, child: content)
: content;
}
}
@@ -0,0 +1,28 @@
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,
),
),
);
}
}
@@ -0,0 +1,65 @@
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));
}
}
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
class RoundedOverlayCards extends StatelessWidget {
const RoundedOverlayCards({
this.topCardChild = const SizedBox(),
this.bottomCardChild = const SizedBox(),
});
final Widget topCardChild;
final Widget bottomCardChild;
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
return ClipRRect(
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
child: Container(
height: screenHeight * 0.50,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
color: Theme.of(context).colorScheme.surfaceContainer
),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
color: Theme.of(context).colorScheme.surfaceContainerLow,
),
height: screenHeight * 0.38,
width: double.infinity,
child: topCardChild)),
bottomCardChild,
],
),
),
);
}
}
@@ -0,0 +1,36 @@
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: TextStyle(
color: Theme.of(context).textTheme.titleLarge!.color,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
Icon(
Icons.chevron_right_rounded,
color: Theme.of(context).textTheme.titleLarge!.color,
),
],
),
);
}
}
@@ -0,0 +1,203 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class ThreeCheckboxAlert extends BaseAlertDialog {
ThreeCheckboxAlert({
required this.alertTitle,
required this.leftButtonText,
required this.rightButtonText,
required this.actionLeftButton,
required this.actionRightButton,
this.alertBarrierDismissible = true,
Key? key,
});
final String alertTitle;
final String leftButtonText;
final String rightButtonText;
final VoidCallback actionLeftButton;
final Function(bool, bool, bool) actionRightButton;
final bool alertBarrierDismissible;
bool checkbox1 = false;
void toggleCheckbox1() => checkbox1 = !checkbox1;
bool checkbox2 = false;
void toggleCheckbox2() => checkbox2 = !checkbox2;
bool checkbox3 = false;
void toggleCheckbox3() => checkbox3 = !checkbox3;
bool showValidationMessage = true;
@override
String get titleText => alertTitle;
@override
bool get isDividerExists => true;
@override
String get leftActionButtonText => leftButtonText;
@override
String get rightActionButtonText => rightButtonText;
@override
VoidCallback get actionLeft => actionLeftButton;
@override
VoidCallback get actionRight => () {
actionRightButton(checkbox1, checkbox2, checkbox3);
};
@override
bool get barrierDismissible => alertBarrierDismissible;
@override
Widget content(BuildContext context) {
return ThreeCheckboxAlertContent(
checkbox1: checkbox1,
toggleCheckbox1: toggleCheckbox1,
checkbox2: checkbox2,
toggleCheckbox2: toggleCheckbox2,
checkbox3: checkbox3,
toggleCheckbox3: toggleCheckbox3,
);
}
}
class ThreeCheckboxAlertContent extends StatefulWidget {
ThreeCheckboxAlertContent({
required this.checkbox1,
required this.toggleCheckbox1,
required this.checkbox2,
required this.toggleCheckbox2,
required this.checkbox3,
required this.toggleCheckbox3,
Key? key,
}) : super(key: key);
bool checkbox1;
void Function() toggleCheckbox1;
bool checkbox2;
void Function() toggleCheckbox2;
bool checkbox3;
void Function() toggleCheckbox3;
@override
_ThreeCheckboxAlertContentState createState() => _ThreeCheckboxAlertContentState(
checkbox1: checkbox1,
toggleCheckbox1: toggleCheckbox1,
checkbox2: checkbox2,
toggleCheckbox2: toggleCheckbox2,
checkbox3: checkbox3,
toggleCheckbox3: toggleCheckbox3,
);
static _ThreeCheckboxAlertContentState? of(BuildContext context) {
return context.findAncestorStateOfType<_ThreeCheckboxAlertContentState>();
}
}
class _ThreeCheckboxAlertContentState extends State<ThreeCheckboxAlertContent> {
_ThreeCheckboxAlertContentState({
required this.checkbox1,
required this.toggleCheckbox1,
required this.checkbox2,
required this.toggleCheckbox2,
required this.checkbox3,
required this.toggleCheckbox3,
});
bool checkbox1;
void Function() toggleCheckbox1;
bool checkbox2;
void Function() toggleCheckbox2;
bool checkbox3;
void Function() toggleCheckbox3;
bool showValidationMessage = true;
bool get areAllCheckboxesChecked => checkbox1 && checkbox2 && checkbox3;
@override
Widget build(BuildContext context) {
return Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
StandardCheckbox(
value: checkbox1,
caption: S.of(context).cakepay_confirm_no_vpn,
onChanged: (bool? value) {
setState(() {
checkbox1 = value ?? false;
toggleCheckbox1();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
StandardCheckbox(
value: checkbox2,
caption: S.of(context).cakepay_confirm_voided_refund,
onChanged: (bool? value) {
setState(() {
checkbox2 = value ?? false;
toggleCheckbox2();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
StandardCheckbox(
value: checkbox3,
caption: S.of(context).cakepay_confirm_terms_agreed,
onChanged: (bool? value) {
setState(() {
checkbox3 = value ?? false;
toggleCheckbox3();
showValidationMessage = !areAllCheckboxesChecked;
});
},
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => launchUrl(
Uri.parse("https://cakepay.com/cakepay-web-terms.txt"),
mode: LaunchMode.externalApplication,
),
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
S.of(context).settings_terms_and_conditions,
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
color: Theme.of(context).primaryColor,
decoration: TextDecoration.none,
height: 1,
),
softWrap: true,
),
),
),
if (showValidationMessage)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Please confirm all checkboxes',
style: TextStyle(
color: Colors.red,
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w400,
decoration: TextDecoration.none,
),
),
),
],
),
);
}
}
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'image_placeholder.dart';
class UserCardItem extends StatelessWidget {
UserCardItem({
required this.title,
required this.subTitle,
this.onTap,
this.logoUrl
});
final VoidCallback? onTap;
final String title;
final String subTitle;
final String? logoUrl;
@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: InkWell(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
if (logoUrl != null)
Padding(
padding: const EdgeInsets.all(2.0),
child: AspectRatio(
aspectRatio: 1.65,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(13)),
child: Image.network(
logoUrl!,
fit: BoxFit.cover,
loadingBuilder:
(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) => CakePayCardImagePlaceholder(),
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Row(
children: [
Expanded(
child: Text(title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 16, fontWeight: FontWeight.w700)),
),
SizedBox(width: 8),
Text(subTitle,
maxLines: 1,
overflow: TextOverflow.clip,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 16, fontWeight: FontWeight.w700)),
],
),
),
],
),
),
),
);
}
}