chore: refactor a big widget's build method and some more general house keeping
What changed, and why it matters
This commit is mostly a cleanup and visual refactor of the gift-card purchase screens. It splits a large widget into smaller pieces, replaces a generic gift-card icon with a reusable credit-card icon, and switches money amounts from floating-point (double) to a more precise Decimal type. There is no obvious security fix here; the changes are housekeeping and UI consistency.
No security action required. Treat as routine refactor; verify Decimal parsing handles all API responses correctly during normal QA.
Security signals we found
Monetary values changed from double to Decimal to avoid floating-point rounding issues
Added mounted check before Navigator operations after async purchase
Added await for async addOrderId call
No input validation, crypto, or authentication changes
Evidence from the diff
The diff refactors CakePay gift-card UI code: extracts widgets in cakepay_card_detail_view.dart, adds a CreditCardIcon widget used across several pages, and changes CakePayCard monetary fields from double to Decimal. It also adds a toMap() serializer, awaits addOrderId, and guards navigation with if (mounted). No vulnerability is patched and no security-sensitive logic is introduced.
Changed components
lib/pages/cakepay/cakepay_card_detail_view.dartlib/pages/cakepay/cakepay_vendors_view.dartlib/pages/more_view/gift_cards_view.dartlib/pages/wallet_view/wallet_view.dartlib/pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dartlib/services/cakepay/src/models/card.dartlib/widgets/icon_widgets/credit_card_icon.dartInspect captured patch +588 / −406
diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart
index 7fbd0eb..371d61e 100644
--- a/lib/pages/cakepay/cakepay_card_detail_view.dart
+++ b/lib/pages/cakepay/cakepay_card_detail_view.dart
@@ -1,3 +1,4 @@
+import 'package:decimal/decimal.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -15,6 +16,7 @@ import '../../widgets/desktop/desktop_dialog.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
+import '../../widgets/icon_widgets/credit_card_icon.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
import '../../widgets/stack_text_field.dart';
@@ -34,7 +36,7 @@ class CakePayCardDetailView extends StatefulWidget {
class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
late CakePayCard _card;
bool _purchasing = false;
- double? _selectedDenomination;
+ Decimal? _selectedDenomination;
int _quantity = 1;
bool _termsAccepted = false;
final _customAmountController = TextEditingController();
@@ -75,8 +77,8 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
if (_emailController.text.trim().isEmpty) return false;
final price = _priceString;
if (price.isEmpty) return false;
- final parsed = double.tryParse(price);
- if (parsed == null || parsed <= 0) return false;
+ final parsed = Decimal.tryParse(price);
+ if (parsed == null || parsed <= Decimal.zero) return false;
if (_card.isRangeDenomination) {
if (_card.minValue != null && parsed < _card.minValue!) return false;
if (_card.maxValue != null && parsed > _card.maxValue!) return false;
@@ -202,19 +204,21 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
// Track order ID locally so the orders list view can fetch it
// via getOrder() without requiring Knox user auth.
- CakePayService.instance.addOrderId(order.orderId);
+ await CakePayService.instance.addOrderId(order.orderId);
- if (Util.isDesktop) {
- Navigator.of(context, rootNavigator: true).pop();
- await showDialog<void>(
- context: context,
- builder: (_) => CakePayOrderView(orderId: order.orderId),
- );
- } else {
- await Navigator.of(context).pushReplacementNamed(
- CakePayOrderView.routeName,
- arguments: order.orderId,
- );
+ if (mounted) {
+ if (Util.isDesktop) {
+ Navigator.of(context, rootNavigator: true).pop();
+ await showDialog<void>(
+ context: context,
+ builder: (_) => CakePayOrderView(orderId: order.orderId),
+ );
+ } else {
+ await Navigator.of(context).pushReplacementNamed(
+ CakePayOrderView.routeName,
+ arguments: order.orderId,
+ );
+ }
}
} else {
await showDialog<dynamic>(
@@ -251,90 +255,366 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
final isDesktop = Util.isDesktop;
final card = _card;
- final denominationSelector = card.isFixedDenomination
- ? Wrap(
- spacing: 8,
- runSpacing: 8,
- children: card.denominations.map((d) {
- final selected = d == _selectedDenomination;
- return ChoiceChip(
- label: Text(
- "${d.toStringAsFixed(0)} ${card.currencyCode ?? ''}",
- style:
- (isDesktop
- ? STextStyles.desktopTextExtraExtraSmall(context)
- : STextStyles.itemSubtitle12(context))
- .copyWith(
- color: selected
- ? Theme.of(
- context,
- ).extension<StackColors>()!.textDark
- : null,
- ),
+ return ConditionalParent(
+ condition: isDesktop,
+ builder: (child) => DesktopDialog(
+ maxWidth: 580,
+ maxHeight: 700,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "Gift Card",
+ style: STextStyles.desktopH3(context),
+ ),
),
- selected: selected,
- onSelected: (val) {
- if (val) setState(() => _selectedDenomination = d);
- },
- );
- }).toList(),
- )
- : card.isRangeDenomination
- ? Column(
- crossAxisAlignment: CrossAxisAlignment.start,
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 32,
+ vertical: 8,
+ ),
+ child: child,
+ ),
+ ),
+ ],
+ ),
+ ),
+ child: ConditionalParent(
+ condition: !isDesktop,
+ builder: (child) => Background(
+ child: Scaffold(
+ backgroundColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.background,
+ appBar: AppBar(
+ leading: AppBarBackButton(
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ title: Text("Gift Card", style: STextStyles.navBarTitle(context)),
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
+ child: child,
+ ),
+ ),
+ ),
+ ),
+ child: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
+ if (card.cardImageUrl != null)
+ _CardImage(imageUrl: card.cardImageUrl!, isDesktop: isDesktop),
+ SizedBox(height: isDesktop ? 16 : 12),
+ Text(
+ card.name,
+ style: isDesktop
+ ? STextStyles.desktopH2(context)
+ : STextStyles.pageTitleH1(context),
+ ),
+ if (card.description != null && card.description!.isNotEmpty) ...[
+ SizedBox(height: isDesktop ? 16 : 12),
+ _PlainInfoBlock(text: card.description!, isDesktop: isDesktop),
+ ],
+ if (card.howToUse != null && card.howToUse!.isNotEmpty) ...[
+ SizedBox(height: isDesktop ? 16 : 12),
+ _TitledInfoBlock(
+ title: "How to use",
+ body: card.howToUse!,
+ isDesktop: isDesktop,
+ ),
+ ],
+ if (card.termsAndConditions != null &&
+ card.termsAndConditions!.isNotEmpty) ...[
+ SizedBox(height: isDesktop ? 16 : 12),
+ _TitledInfoBlock(
+ title: "Terms & conditions",
+ body: card.termsAndConditions!,
+ isDesktop: isDesktop,
+ ),
+ ],
+ if (card.expiryAndValidity != null &&
+ card.expiryAndValidity!.isNotEmpty) ...[
+ SizedBox(height: isDesktop ? 16 : 12),
+ _TitledInfoBlock(
+ title: "Expiry & validity",
+ body: card.expiryAndValidity!,
+ isDesktop: isDesktop,
+ ),
+ ],
+ SizedBox(height: isDesktop ? 24 : 16),
+ _DenominationSelector(
+ card: card,
+ isDesktop: isDesktop,
+ selectedDenomination: _selectedDenomination,
+ customAmountController: _customAmountController,
+ customAmountFocusNode: _customAmountFocusNode,
+ onDenominationSelected: (Decimal d) =>
+ setState(() => _selectedDenomination = d),
+ onCustomAmountChanged: () => setState(() {}),
+ ),
+ SizedBox(height: isDesktop ? 16 : 12),
+ _QuantityRow(
+ isDesktop: isDesktop,
+ quantity: _quantity,
+ onDecrement: _quantity > 1
+ ? () => setState(() => _quantity--)
+ : null,
+ onIncrement: () => setState(() => _quantity++),
+ ),
+ SizedBox(height: isDesktop ? 16 : 12),
+ _TermsCheckbox(
+ isDesktop: isDesktop,
+ accepted: _termsAccepted,
+ onToggle: () =>
+ setState(() => _termsAccepted = !_termsAccepted),
+ onOpenTerms: _openTerms,
+ ),
+ SizedBox(height: isDesktop ? 16 : 12),
Text(
- "Enter amount (${card.minValue?.toStringAsFixed(0) ?? '?'} - "
- "${card.maxValue?.toStringAsFixed(0) ?? '?'} "
- "${card.currencyCode ?? ''})",
+ "Email for receipt and delivery",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.itemSubtitle12(context),
),
const SizedBox(height: 8),
- ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
- child: TextField(
- controller: _customAmountController,
- focusNode: _customAmountFocusNode,
- keyboardType: const TextInputType.numberWithOptions(
- decimal: true,
- ),
- onChanged: (_) => setState(() {}),
- style: isDesktop
- ? STextStyles.desktopTextExtraSmall(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveText,
- height: 1.8,
- )
- : STextStyles.field(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveText,
- ),
- decoration:
- standardInputDecoration(
- "Amount",
- _customAmountFocusNode,
- context,
- desktopMed: isDesktop,
- ).copyWith(
- filled: true,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 12,
- ),
- ),
- ),
+ _EmailField(
+ isDesktop: isDesktop,
+ controller: _emailController,
+ focusNode: _emailFocusNode,
+ onChanged: () => setState(() {}),
),
+ SizedBox(height: isDesktop ? 24 : 16),
+ PrimaryButton(
+ label: _purchasing ? "Processing..." : "Purchase",
+ enabled: _canPurchase,
+ onPressed: _canPurchase ? _purchase : null,
+ ),
+ if (!isDesktop) const SizedBox(height: 16),
],
- )
- : const SizedBox.shrink();
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _CardImage extends StatelessWidget {
+ const _CardImage({required this.imageUrl, required this.isDesktop});
+
+ final String imageUrl;
+ final bool isDesktop;
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: Image.network(
+ imageUrl,
+ width: isDesktop ? 200 : 150,
+ fit: BoxFit.contain,
+ errorBuilder: (BuildContext _, Object __, StackTrace? ___) =>
+ CreditCardIcon(
+ width: isDesktop ? 80 : 60,
+ height: isDesktop ? 80 : 60,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _PlainInfoBlock extends StatelessWidget {
+ const _PlainInfoBlock({required this.text, required this.isDesktop});
+
+ final String text;
+ final bool isDesktop;
+
+ @override
+ Widget build(BuildContext context) {
+ return RoundedWhiteContainer(
+ child: Text(
+ text,
+ style: isDesktop
+ ? STextStyles.desktopTextExtraExtraSmall(context)
+ : STextStyles.itemSubtitle12(context),
+ ),
+ );
+ }
+}
+
+class _TitledInfoBlock extends StatelessWidget {
+ const _TitledInfoBlock({
+ required this.title,
+ required this.body,
+ required this.isDesktop,
+ });
+
+ final String title;
+ final String body;
+ final bool isDesktop;
+
+ @override
+ Widget build(BuildContext context) {
+ return RoundedWhiteContainer(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: isDesktop
+ ? STextStyles.desktopTextSmall(context)
+ : STextStyles.titleBold12(context),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ body,
+ style: isDesktop
+ ? STextStyles.desktopTextExtraExtraSmall(context)
+ : STextStyles.itemSubtitle12(context),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _DenominationSelector extends StatelessWidget {
+ const _DenominationSelector({
+ required this.card,
+ required this.isDesktop,
+ required this.selectedDenomination,
+ required this.customAmountController,
+ required this.customAmountFocusNode,
+ required this.onDenominationSelected,
+ required this.onCustomAmountChanged,
+ });
+
+ final CakePayCard card;
+ final bool isDesktop;
+ final Decimal? selectedDenomination;
+ final TextEditingController customAmountController;
+ final FocusNode customAmountFocusNode;
+ final ValueChanged<Decimal> onDenominationSelected;
+ final VoidCallback onCustomAmountChanged;
+
+ @override
+ Widget build(BuildContext context) {
+ if (card.isFixedDenomination) {
+ return Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: card.denominations.map((d) {
+ final bool selected = d == selectedDenomination;
+ return ChoiceChip(
+ label: Text(
+ "${d.toStringAsFixed(0)} ${card.currencyCode ?? ''}",
+ style:
+ (isDesktop
+ ? STextStyles.desktopTextExtraExtraSmall(context)
+ : STextStyles.itemSubtitle12(context))
+ .copyWith(
+ color: selected
+ ? Theme.of(
+ context,
+ ).extension<StackColors>()!.textDark
+ : null,
+ ),
+ ),
+ selected: selected,
+ onSelected: (bool val) {
+ if (val) onDenominationSelected(d);
+ },
+ );
+ }).toList(),
+ );
+ }
+
+ if (card.isRangeDenomination) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Enter amount (${card.minValue?.toStringAsFixed(0) ?? '?'} - "
+ "${card.maxValue?.toStringAsFixed(0) ?? '?'} "
+ "${card.currencyCode ?? ''})",
+ style: isDesktop
+ ? STextStyles.desktopTextExtraExtraSmall(context)
+ : STextStyles.itemSubtitle12(context),
+ ),
+ const SizedBox(height: 8),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ controller: customAmountController,
+ focusNode: customAmountFocusNode,
+ keyboardType: const TextInputType.numberWithOptions(
+ decimal: true,
+ ),
+ onChanged: (_) => onCustomAmountChanged(),
+ style: isDesktop
+ ? STextStyles.desktopTextExtraSmall(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveText,
+ height: 1.8,
+ )
+ : STextStyles.field(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveText,
+ ),
+ decoration:
+ standardInputDecoration(
+ "Amount",
+ customAmountFocusNode,
+ context,
+ desktopMed: isDesktop,
+ ).copyWith(
+ filled: true,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ return const SizedBox.shrink();
+ }
+}
- final quantityRow = Row(
+class _QuantityRow extends StatelessWidget {
+ const _QuantityRow({
+ required this.isDesktop,
+ required this.quantity,
+ required this.onDecrement,
+ required this.onIncrement,
+ });
+
+ final bool isDesktop;
+ final int quantity;
+ final VoidCallback? onDecrement;
+ final VoidCallback onIncrement;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
children: [
Text(
"Quantity",
@@ -345,23 +625,40 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
const Spacer(),
IconButton(
icon: const Icon(Icons.remove_circle_outline, size: 20),
- onPressed: _quantity > 1 ? () => setState(() => _quantity--) : null,
+ onPressed: onDecrement,
),
Text(
- "$_quantity",
+ "$quantity",
style: isDesktop
? STextStyles.desktopTextSmall(context)
: STextStyles.titleBold12(context),
),
IconButton(
icon: const Icon(Icons.add_circle_outline, size: 20),
- onPressed: () => setState(() => _quantity++),
+ onPressed: onIncrement,
),
],
);
+ }
+}
+
+class _TermsCheckbox extends StatelessWidget {
+ const _TermsCheckbox({
+ required this.isDesktop,
+ required this.accepted,
+ required this.onToggle,
+ required this.onOpenTerms,
+ });
+
+ final bool isDesktop;
+ final bool accepted;
+ final VoidCallback onToggle;
+ final VoidCallback onOpenTerms;
- final termsCheckbox = GestureDetector(
- onTap: () => setState(() => _termsAccepted = !_termsAccepted),
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onToggle,
child: Container(
color: Colors.transparent,
child: Row(
@@ -373,7 +670,7 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
child: IgnorePointer(
child: Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
- value: _termsAccepted,
+ value: accepted,
onChanged: (_) {},
),
),
@@ -392,7 +689,7 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
style: STextStyles.richLink(
context,
).copyWith(fontSize: isDesktop ? null : 14),
- recognizer: TapGestureRecognizer()..onTap = _openTerms,
+ recognizer: TapGestureRecognizer()..onTap = onOpenTerms,
),
const TextSpan(
text:
@@ -410,230 +707,58 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
),
),
);
+ }
+}
- final content = SingleChildScrollView(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- if (card.cardImageUrl != null)
- Center(
- child: ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child: Image.network(
- card.cardImageUrl!,
- width: isDesktop ? 200 : 150,
- fit: BoxFit.contain,
- errorBuilder: (_, __, ___) =>
- Icon(Icons.card_giftcard, size: isDesktop ? 80 : 60),
- ),
- ),
- ),
- SizedBox(height: isDesktop ? 16 : 12),
- Text(
- card.name,
- style: isDesktop
- ? STextStyles.desktopH2(context)
- : STextStyles.pageTitleH1(context),
- ),
- if (card.description != null && card.description!.isNotEmpty) ...[
- SizedBox(height: isDesktop ? 16 : 12),
- RoundedWhiteContainer(
- child: Text(
- card.description!,
- style: isDesktop
- ? STextStyles.desktopTextExtraExtraSmall(context)
- : STextStyles.itemSubtitle12(context),
- ),
- ),
- ],
- if (card.howToUse != null && card.howToUse!.isNotEmpty) ...[
- SizedBox(height: isDesktop ? 16 : 12),
- RoundedWhiteContainer(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "How to use",
- style: isDesktop
- ? STextStyles.desktopTextSmall(context)
- : STextStyles.titleBold12(context),
- ),
- const SizedBox(height: 8),
- Text(
- card.howToUse!,
- style: isDesktop
- ? STextStyles.desktopTextExtraExtraSmall(context)
- : STextStyles.itemSubtitle12(context),
- ),
- ],
- ),
- ),
- ],
- if (card.termsAndConditions != null &&
- card.termsAndConditions!.isNotEmpty) ...[
- SizedBox(height: isDesktop ? 16 : 12),
- RoundedWhiteContainer(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "Terms & conditions",
- style: isDesktop
- ? STextStyles.desktopTextSmall(context)
- : STextStyles.titleBold12(context),
- ),
- const SizedBox(height: 8),
- Text(
- card.termsAndConditions!,
- style: isDesktop
- ? STextStyles.desktopTextExtraExtraSmall(context)
- : STextStyles.itemSubtitle12(context),
- ),
- ],
- ),
- ),
- ],
- if (card.expiryAndValidity != null &&
- card.expiryAndValidity!.isNotEmpty) ...[
- SizedBox(height: isDesktop ? 16 : 12),
- RoundedWhiteContainer(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "Expiry & validity",
- style: isDesktop
- ? STextStyles.desktopTextSmall(context)
- : STextStyles.titleBold12(context),
- ),
- const SizedBox(height: 8),
- Text(
- card.expiryAndValidity!,
- style: isDesktop
- ? STextStyles.desktopTextExtraExtraSmall(context)
- : STextStyles.itemSubtitle12(context),
- ),
- ],
- ),
- ),
- ],
- SizedBox(height: isDesktop ? 24 : 16),
- denominationSelector,
- SizedBox(height: isDesktop ? 16 : 12),
- quantityRow,
- SizedBox(height: isDesktop ? 16 : 12),
- termsCheckbox,
- SizedBox(height: isDesktop ? 16 : 12),
- Text(
- "Email for receipt and delivery",
- style: isDesktop
- ? STextStyles.desktopTextExtraExtraSmall(context)
- : STextStyles.itemSubtitle12(context),
- ),
- const SizedBox(height: 8),
- ClipRRect(
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius,
- ),
- child: TextField(
- controller: _emailController,
- focusNode: _emailFocusNode,
- autocorrect: false,
- enableSuggestions: false,
- keyboardType: TextInputType.emailAddress,
- onChanged: (_) => setState(() {}),
- style: isDesktop
- ? STextStyles.desktopTextExtraSmall(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveText,
- height: 1.8,
- )
- : STextStyles.field(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.textFieldActiveText,
- ),
- decoration:
- standardInputDecoration(
- "Email",
- _emailFocusNode,
- context,
- desktopMed: isDesktop,
- ).copyWith(
- filled: true,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 12,
- ),
- ),
- ),
- ),
- SizedBox(height: isDesktop ? 24 : 16),
- PrimaryButton(
- label: _purchasing ? "Processing..." : "Purchase",
- enabled: _canPurchase,
- onPressed: _canPurchase ? _purchase : null,
- ),
- ],
- ),
- );
+class _EmailField extends StatelessWidget {
+ const _EmailField({
+ required this.isDesktop,
+ required this.controller,
+ required this.focusNode,
+ required this.onChanged,
+ });
- return _scaffold(isDesktop: isDesktop, child: content);
- }
+ final bool isDesktop;
+ final TextEditingController controller;
+ final FocusNode focusNode;
+ final VoidCallback onChanged;
- Widget _scaffold({required bool isDesktop, required Widget child}) {
- return ConditionalParent(
- condition: isDesktop,
- builder: (child) => DesktopDialog(
- maxWidth: 580,
- maxHeight: 700,
- child: Column(
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Padding(
- padding: const EdgeInsets.only(left: 32),
- child: Text(
- "Gift Card",
- style: STextStyles.desktopH3(context),
- ),
- ),
- const DesktopDialogCloseButton(),
- ],
- ),
- Expanded(
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 32,
- vertical: 8,
- ),
- child: child,
+ @override
+ Widget build(BuildContext context) {
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
+ child: TextField(
+ controller: controller,
+ focusNode: focusNode,
+ autocorrect: false,
+ enableSuggestions: false,
+ keyboardType: TextInputType.emailAddress,
+ onChanged: (_) => onChanged(),
+ style: isDesktop
+ ? STextStyles.desktopTextExtraSmall(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveText,
+ height: 1.8,
+ )
+ : STextStyles.field(context).copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldActiveText,
),
- ),
- ],
- ),
- ),
- child: ConditionalParent(
- condition: !isDesktop,
- builder: (child) => Background(
- child: Scaffold(
- backgroundColor: Theme.of(
+ decoration:
+ standardInputDecoration(
+ "Email",
+ focusNode,
context,
- ).extension<StackColors>()!.background,
- appBar: AppBar(
- leading: AppBarBackButton(
- onPressed: () => Navigator.of(context).pop(),
+ desktopMed: isDesktop,
+ ).copyWith(
+ filled: true,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
),
- title: Text("Gift Card", style: STextStyles.navBarTitle(context)),
),
- body: SafeArea(
- child: Padding(padding: const EdgeInsets.all(16), child: child),
- ),
- ),
- ),
- child: child,
),
);
}
diff --git a/lib/pages/cakepay/cakepay_vendors_view.dart b/lib/pages/cakepay/cakepay_vendors_view.dart
index 2c7b3f5..5c16cdd 100644
--- a/lib/pages/cakepay/cakepay_vendors_view.dart
+++ b/lib/pages/cakepay/cakepay_vendors_view.dart
@@ -15,6 +15,7 @@ import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
+import '../../widgets/icon_widgets/credit_card_icon.dart';
import '../../widgets/loading_indicator.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_text_field.dart';
@@ -96,15 +97,19 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
});
}
- void _onCardTapped(CakePayCard card) {
+ Future<void> _onCardTapped(CakePayCard card) async {
if (Util.isDesktop) {
+ // this pop makes going back annoying as the whole list needs to be
+ // searched again with API calls etc. Leaving in for now as this is how I
+ // found it and removing here could introduce worse issues somewhere else.
Navigator.of(context, rootNavigator: true).pop();
- showDialog<void>(
+
+ await showDialog<void>(
context: context,
builder: (_) => CakePayCardDetailView(card: card),
);
} else {
- Navigator.of(
+ await Navigator.of(
context,
).pushNamed(CakePayCardDetailView.routeName, arguments: card);
}
@@ -165,7 +170,10 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
),
),
body: SafeArea(
- child: Padding(padding: const EdgeInsets.all(16), child: child),
+ child: Padding(
+ padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
+ child: child,
+ ),
),
),
),
@@ -205,6 +213,9 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
shrinkWrap: isDesktop,
primary: isDesktop ? false : null,
itemCount: cards.length,
+ padding: isDesktop
+ ? null
+ : const EdgeInsets.only(bottom: 16),
separatorBuilder: (_, __) =>
SizedBox(height: isDesktop ? 16 : 12),
itemBuilder: (_, index) => _CardTile(
@@ -256,9 +267,16 @@ class _SearchField extends StatelessWidget {
focusNode,
context,
).copyWith(
- prefixIcon: const Padding(
- padding: EdgeInsets.symmetric(horizontal: 10, vertical: 12),
- child: Icon(Icons.search, size: 20),
+ prefixIcon: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 16,
+ ),
+ child: SvgPicture.asset(
+ Assets.svg.search,
+ width: 16,
+ height: 16,
+ ),
),
),
onSubmitted: onSubmitted,
@@ -411,10 +429,15 @@ class _CardTile extends StatelessWidget {
width: isDesktop ? 60 : 48,
height: isDesktop ? 40 : 32,
fit: BoxFit.cover,
- errorBuilder: (_, __, ___) =>
- Icon(Icons.card_giftcard, size: isDesktop ? 40 : 32),
+ errorBuilder: (_, __, ___) => CreditCardIcon(
+ width: isDesktop ? 40 : 32,
+ height: isDesktop ? 40 : 32,
+ ),
)
- : Icon(Icons.card_giftcard, size: isDesktop ? 40 : 32),
+ : CreditCardIcon(
+ width: isDesktop ? 40 : 32,
+ height: isDesktop ? 40 : 32,
+ ),
),
const SizedBox(width: 12),
Expanded(
@@ -445,7 +468,12 @@ class _CardTile extends StatelessWidget {
],
),
),
- Icon(Icons.chevron_right, color: colors.textSubtitle1),
+ SvgPicture.asset(
+ Assets.svg.chevronRight,
+ width: 20,
+ height: 20,
+ colorFilter: ColorFilter.mode(colors.textSubtitle1, .srcIn),
+ ),
],
),
),
diff --git a/lib/pages/more_view/gift_cards_view.dart b/lib/pages/more_view/gift_cards_view.dart
index 9fcf82b..48ff0f3 100644
--- a/lib/pages/more_view/gift_cards_view.dart
+++ b/lib/pages/more_view/gift_cards_view.dart
@@ -1,17 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:flutter_svg/svg.dart';
import '../../app_config.dart';
import '../../services/event_bus/events/global/tor_connection_status_changed_event.dart';
import '../../services/tor_service.dart';
import '../../themes/stack_colors.dart';
-import '../../utilities/assets.dart';
import '../../utilities/text_styles.dart';
import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
+import '../../widgets/icon_widgets/credit_card_icon.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/tor_subscription.dart';
import '../cakepay/cakepay_orders_view.dart';
@@ -51,11 +50,7 @@ class _GiftCardsViewState extends ConsumerState<GiftCardsView> {
context,
).extension<StackColors>()!.background,
appBar: AppBar(
- leading: AppBarBackButton(
- onPressed: () {
- Navigator.of(context).pop();
- },
- ),
+ leading: const AppBarBackButton(),
title: Text("Gift cards", style: STextStyles.navBarTitle(context)),
),
body: SafeArea(
@@ -69,11 +64,7 @@ class _GiftCardsViewState extends ConsumerState<GiftCardsView> {
children: [
Row(
children: [
- SvgPicture.asset(
- Assets.svg.creditCard,
- width: 32,
- height: 32,
- ),
+ const CreditCardIcon(width: 32, height: 32),
const SizedBox(width: 12),
Expanded(
child: Column(
@@ -116,24 +107,26 @@ class _GiftCardsViewState extends ConsumerState<GiftCardsView> {
Row(
children: [
Expanded(
- child: PrimaryButton(
- label: "Browse",
+ child: SecondaryButton(
+ label: "My Orders",
enabled: !_torEnabled,
onPressed: () {
Navigator.of(
context,
- ).pushNamed(CakePayVendorsView.routeName);
+ ).pushNamed(CakePayOrdersView.routeName);
},
),
),
+
const SizedBox(width: 16),
Expanded(
- child: SecondaryButton(
- label: "My Orders",
+ child: PrimaryButton(
+ label: "Browse",
+ enabled: !_torEnabled,
onPressed: () {
Navigator.of(
context,
- ).pushNamed(CakePayOrdersView.routeName);
+ ).pushNamed(CakePayVendorsView.routeName);
},
),
),
diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart
index 12affd4..83a4d6e 100644
--- a/lib/pages/wallet_view/wallet_view.dart
+++ b/lib/pages/wallet_view/wallet_view.dart
@@ -71,6 +71,7 @@ import '../../widgets/custom_buttons/blue_text_button.dart';
import '../../widgets/custom_loading_overlay.dart';
import '../../widgets/desktop/secondary_button.dart';
import '../../widgets/frost_scaffold.dart';
+import '../../widgets/icon_widgets/credit_card_icon.dart';
import '../../widgets/loading_indicator.dart';
import '../../widgets/small_tor_icon.dart';
import '../../widgets/stack_dialog.dart';
@@ -96,6 +97,8 @@ import '../exchange_view/wallet_initiated_exchange_view.dart';
import '../finalize_view/finalize_view.dart';
import '../masternodes/masternodes_home_view.dart';
import '../monkey/monkey_view.dart';
+import '../more_view/gift_cards_view.dart';
+import '../more_view/services_view.dart';
import '../namecoin_names/namecoin_names_home_view.dart';
import '../notification_views/notifications_view.dart';
import '../ordinals/ordinals_view.dart';
@@ -109,8 +112,6 @@ import '../settings_views/wallet_settings_view/wallet_network_settings_view/wall
import '../settings_views/wallet_settings_view/wallet_settings_view.dart';
import '../signing/signing_view.dart';
import '../spark_names/spark_names_home_view.dart';
-import '../more_view/gift_cards_view.dart';
-import '../more_view/services_view.dart';
import '../token_view/my_tokens_view.dart';
import 'sub_widgets/transactions_list.dart';
import 'sub_widgets/wallet_summary.dart';
@@ -1364,8 +1365,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
),
WalletNavigationBarItemData(
label: "Gift cards",
- icon: SvgPicture.asset(
- Assets.svg.creditCard,
+ icon: CreditCardIcon(
height: 20,
width: 20,
color: Theme.of(
diff --git a/lib/pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart b/lib/pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart
index 7693f43..964028a 100644
--- a/lib/pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart
+++ b/lib/pages_desktop_specific/services/sub_widgets/desktop_gift_cards_view.dart
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:flutter_svg/svg.dart';
import '../../../app_config.dart';
import '../../../pages/cakepay/cakepay_orders_view.dart';
@@ -8,10 +7,10 @@ import '../../../pages/cakepay/cakepay_vendors_view.dart';
import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart';
import '../../../services/tor_service.dart';
import '../../../themes/stack_colors.dart';
-import '../../../utilities/assets.dart';
import '../../../utilities/text_styles.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/desktop/secondary_button.dart';
+import '../../../widgets/icon_widgets/credit_card_icon.dart';
import '../../../widgets/rounded_white_container.dart';
import '../../../widgets/tor_subscription.dart';
@@ -53,17 +52,9 @@ class _DesktopGiftCardsViewState extends ConsumerState<DesktopGiftCardsView> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: SvgPicture.asset(
- Assets.svg.creditCard,
- width: 48,
- height: 48,
- colorFilter: ColorFilter.mode(
- Theme.of(context).extension<StackColors>()!.textDark,
- BlendMode.srcIn,
- ),
- ),
+ const Padding(
+ padding: EdgeInsets.all(8.0),
+ child: CreditCardIcon(width: 48, height: 48),
),
Padding(
padding: const EdgeInsets.all(10),
diff --git a/lib/services/cakepay/src/models/card.dart b/lib/services/cakepay/src/models/card.dart
index 2fed2f4..83d2eb3 100644
--- a/lib/services/cakepay/src/models/card.dart
+++ b/lib/services/cakepay/src/models/card.dart
@@ -1,3 +1,5 @@
+import "package:decimal/decimal.dart";
+
class CakePayCard {
final int id;
final String name;
@@ -9,11 +11,11 @@ class CakePayCard {
final String? cardImageUrl;
final String? country;
final String? currencyCode;
- final List<double> denominations;
- final double? minValue;
- final double? maxValue;
- final double? minValueUsd;
- final double? maxValueUsd;
+ final List<Decimal> denominations;
+ final Decimal? minValue;
+ final Decimal? maxValue;
+ final Decimal? minValueUsd;
+ final Decimal? maxValueUsd;
final bool available;
final String? lastUpdated;
@@ -38,72 +40,84 @@ class CakePayCard {
});
factory CakePayCard.fromJson(Map<String, dynamic> json) {
- final rawDenoms = json['denominations'] ?? json['denominations_list'];
- final denominations = <double>[];
+ final dynamic rawDenoms =
+ json["denominations"] ?? json["denominations_list"];
+ final List<Decimal> denominations = <Decimal>[];
if (rawDenoms is List) {
- for (final d in rawDenoms) {
- if (d is num) {
- denominations.add(d.toDouble());
- } else if (d is String) {
- final parsed = double.tryParse(d);
- if (parsed != null) denominations.add(parsed);
- } else if (d is Map) {
- final v = d['value'];
- if (v is num) {
- denominations.add(v.toDouble());
- } else if (v is String) {
- final parsed = double.tryParse(v);
- if (parsed != null) denominations.add(parsed);
- }
- }
+ for (final dynamic d in rawDenoms) {
+ final Decimal? parsed = _toDecimal(d is Map ? d["value"] : d);
+ if (parsed != null) denominations.add(parsed);
}
}
return CakePayCard(
- id: json['id'] as int? ?? 0,
- name: (json['name'] ?? '') as String,
- type: json['type'] as String?,
- description: json['description'] as String?,
- termsAndConditions: json['terms_and_conditions'] as String?,
- howToUse: json['how_to_use'] as String?,
- expiryAndValidity: json['expiry_and_validity'] as String?,
- cardImageUrl: json['card_image_url'] as String?,
- country: json['country'] is Map
- ? (json['country'] as Map<String, dynamic>)['name'] as String?
- : json['country'] as String?,
- currencyCode: json['currency_code'] as String?,
+ id: json["id"] as int? ?? 0,
+ name: (json["name"] ?? "") as String,
+ type: json["type"] as String?,
+ description: json["description"] as String?,
+ termsAndConditions: json["terms_and_conditions"] as String?,
+ howToUse: json["how_to_use"] as String?,
+ expiryAndValidity: json["expiry_and_validity"] as String?,
+ cardImageUrl: json["card_image_url"] as String?,
+ country: json["country"] is Map
+ ? (json["country"] as Map<String, dynamic>)["name"] as String?
+ : json["country"] as String?,
+ currencyCode: json["currency_code"] as String?,
denominations: denominations,
- minValue: _toDouble(json['min_value']),
- maxValue: _toDouble(json['max_value']),
- minValueUsd: _toDouble(json['min_value_usd']),
- maxValueUsd: _toDouble(json['max_value_usd']),
- available: json['available'] as bool? ?? true,
- lastUpdated: json['last_updated'] as String?,
+ minValue: _toDecimal(json["min_value"]),
+ maxValue: _toDecimal(json["max_value"]),
+ minValueUsd: _toDecimal(json["min_value_usd"]),
+ maxValueUsd: _toDecimal(json["max_value_usd"]),
+ available: json["available"] as bool? ?? true,
+ lastUpdated: json["last_updated"] as String?,
);
}
+ Map<String, dynamic> toMap() {
+ return <String, dynamic>{
+ "id": id,
+ "name": name,
+ "type": type,
+ "description": description,
+ "terms_and_conditions": termsAndConditions,
+ "how_to_use": howToUse,
+ "expiry_and_validity": expiryAndValidity,
+ "card_image_url": cardImageUrl,
+ "country": country,
+ "currency_code": currencyCode,
+ "denominations": denominations.map((Decimal d) => d.toString()).toList(),
+ "min_value": minValue?.toString(),
+ "max_value": maxValue?.toString(),
+ "min_value_usd": minValueUsd?.toString(),
+ "max_value_usd": maxValueUsd?.toString(),
+ "available": available,
+ "last_updated": lastUpdated,
+ };
+ }
+
bool get isFixedDenomination => denominations.isNotEmpty;
bool get isRangeDenomination =>
denominations.isEmpty && minValue != null && maxValue != null;
String get denominationRange {
if (isFixedDenomination) {
- return denominations.map((d) => d.toStringAsFixed(0)).join(', ');
+ return denominations.map((Decimal d) => d.toStringAsFixed(0)).join(", ");
}
if (isRangeDenomination) {
- return '${minValue!.toStringAsFixed(0)} - ${maxValue!.toStringAsFixed(0)}';
+ return "${minValue!.toStringAsFixed(0)} - ${maxValue!.toStringAsFixed(0)}";
}
- return '';
+ return "";
}
@override
- String toString() => 'CakePayCard($id, $name)';
+ String toString() => toMap().toString();
}
-double? _toDouble(dynamic v) {
+Decimal? _toDecimal(dynamic v) {
if (v == null) return null;
- if (v is double) return v;
- if (v is int) return v.toDouble();
- if (v is String) return double.tryParse(v);
+ if (v is Decimal) return v;
+ if (v is int) return Decimal.fromInt(v);
+ if (v is double) return Decimal.parse(v.toString());
+ if (v is String) return Decimal.tryParse(v);
return null;
}
diff --git a/lib/widgets/icon_widgets/credit_card_icon.dart b/lib/widgets/icon_widgets/credit_card_icon.dart
new file mode 100644
index 0000000..369792e
--- /dev/null
+++ b/lib/widgets/icon_widgets/credit_card_icon.dart
@@ -0,0 +1,31 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+
+import '../../themes/stack_colors.dart';
+import '../../utilities/assets.dart';
+
+class CreditCardIcon extends StatelessWidget {
+ const CreditCardIcon({
+ super.key,
+ this.width = 32,
+ this.height = 32,
+ this.color,
+ });
+
+ final double width;
+ final double height;
+ final Color? color;
+
+ @override
+ Widget build(BuildContext context) {
+ return SvgPicture.asset(
+ Assets.svg.creditCard,
+ width: width,
+ height: height,
+ colorFilter: ColorFilter.mode(
+ color ?? Theme.of(context).extension<StackColors>()!.textDark3,
+ BlendMode.srcIn,
+ ),
+ );
+ }
+}
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.