From 7615bd77fea6a8d8bad0cc04f03e40d15bb88029 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 May 2026 14:22:18 -0600 Subject: [PATCH 1/5] chore(ui): WIP clean up --- lib/pages/cakepay/cakepay_order_view.dart | 115 ++++---------- lib/pages/cakepay/cakepay_orders_view.dart | 17 +-- .../sub_widgets/exchange_provider_option.dart | 5 +- .../global_settings_view/hidden_settings.dart | 142 ------------------ lib/pages/shopinbit/shopinbit_offer_view.dart | 18 +-- .../shopinbit/shopinbit_payment_view.dart | 13 +- .../shopinbit/shopinbit_ticket_detail.dart | 10 +- .../shopinbit/shopinbit_tickets_view.dart | 10 +- lib/services/cakepay/cakepay_service.dart | 5 - 9 files changed, 50 insertions(+), 285 deletions(-) diff --git a/lib/pages/cakepay/cakepay_order_view.dart b/lib/pages/cakepay/cakepay_order_view.dart index 71c9fe89a..aa11d295a 100644 --- a/lib/pages/cakepay/cakepay_order_view.dart +++ b/lib/pages/cakepay/cakepay_order_view.dart @@ -23,6 +23,7 @@ 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/desktop/primary_button.dart'; +import '../../widgets/loading_indicator.dart'; import '../../widgets/qr.dart'; import '../../widgets/rounded_white_container.dart'; import 'cakepay_send_from_view.dart'; @@ -215,12 +216,7 @@ class _CakePayOrderViewState extends ConsumerState { setState(() { _loading = false; if (!resp.hasError && resp.value != null) { - var order = resp.value!; - final override = CakePayService.devStatusOverrides[order.orderId]; - if (override != null) { - order = order.copyWith(status: override); - } - _order = order; + _order = resp.value!; if (_isTerminal(_order!.status)) { _pollTimer?.cancel(); _countdownTimer?.cancel(); @@ -324,60 +320,6 @@ class _CakePayOrderViewState extends ConsumerState { ]; } - String _statusLabel(CakePayOrderStatus status) { - switch (status) { - case CakePayOrderStatus.new_: - return "New"; - case CakePayOrderStatus.expiredButStillPending: - return "Expired (pending)"; - case CakePayOrderStatus.expired: - return "Expired"; - case CakePayOrderStatus.failed: - return "Failed"; - case CakePayOrderStatus.paid: - return "Paid"; - case CakePayOrderStatus.paidPartial: - return "Partially paid"; - case CakePayOrderStatus.pendingPurchase: - return "Pending purchase"; - case CakePayOrderStatus.purchaseProcessing: - return "Processing"; - case CakePayOrderStatus.purchased: - return "Purchased"; - case CakePayOrderStatus.pendingEmail: - return "Pending email"; - case CakePayOrderStatus.complete: - return "Complete"; - case CakePayOrderStatus.pendingRefund: - return "Pending refund"; - case CakePayOrderStatus.refunded: - return "Refunded"; - } - } - - Color _statusColor(BuildContext context, CakePayOrderStatus status) { - final colors = Theme.of(context).extension()!; - switch (status) { - case CakePayOrderStatus.complete: - case CakePayOrderStatus.purchased: - return colors.accentColorGreen; - case CakePayOrderStatus.new_: - case CakePayOrderStatus.paid: - case CakePayOrderStatus.paidPartial: - return colors.accentColorBlue; - case CakePayOrderStatus.pendingPurchase: - case CakePayOrderStatus.purchaseProcessing: - case CakePayOrderStatus.pendingEmail: - case CakePayOrderStatus.expiredButStillPending: - return colors.accentColorYellow; - case CakePayOrderStatus.expired: - case CakePayOrderStatus.failed: - case CakePayOrderStatus.pendingRefund: - case CakePayOrderStatus.refunded: - return colors.textSubtitle1; - } - } - @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; @@ -385,13 +327,7 @@ class _CakePayOrderViewState extends ConsumerState { if (_loading) { return _scaffold( isDesktop: isDesktop, - child: const Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), + child: const LoadingIndicator(width: 24, height: 24), ); } @@ -412,24 +348,33 @@ class _CakePayOrderViewState extends ConsumerState { final order = _order!; final paymentOptions = order.paymentOptions; - final statusBadge = Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: _statusColor(context, order.status).withValues(alpha: 0.2), - ), - child: Text( - _statusLabel(order.status), - style: - (isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12(context)) - .copyWith(color: _statusColor(context, order.status)), - ), - ); - final details = [ - Row(mainAxisAlignment: MainAxisAlignment.end, children: [statusBadge]), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: order.status + .color(Theme.of(context).extension()!) + .withValues(alpha: 0.2), + ), + child: Text( + order.status.label, + style: + (isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle12(context)) + .copyWith( + color: order.status.color( + Theme.of(context).extension()!, + ), + ), + ), + ), + ], + ), SizedBox(height: isDesktop ? 8 : 6), RoundedWhiteContainer( child: GestureDetector( @@ -727,7 +672,7 @@ class _CakePayOrderViewState extends ConsumerState { const SizedBox(width: 8), Expanded( child: Text( - _statusLabel(status), + status.label, style: (isDesktop ? STextStyles.desktopTextExtraExtraSmall(context) diff --git a/lib/pages/cakepay/cakepay_orders_view.dart b/lib/pages/cakepay/cakepay_orders_view.dart index e1fd13513..3f52d5ada 100644 --- a/lib/pages/cakepay/cakepay_orders_view.dart +++ b/lib/pages/cakepay/cakepay_orders_view.dart @@ -10,6 +10,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/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'cakepay_order_view.dart'; @@ -44,12 +45,7 @@ class _CakePayOrdersViewState extends State { for (final id in orderIds) { final resp = await CakePayService.instance.client.getOrder(id); if (!resp.hasError && resp.value != null) { - var order = resp.value!; - final override = CakePayService.devStatusOverrides[order.orderId]; - if (override != null) { - order = order.copyWith(status: override); - } - results.add(order); + results.add(resp.value!); } } @@ -193,14 +189,7 @@ class _CakePayOrdersViewState extends State { final content = Stack( children: [ list, - if (_syncing) - const Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), + if (_syncing) const LoadingIndicator(width: 24, height: 24), ], ); diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart index 1a3a88db9..700088664 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart @@ -36,6 +36,7 @@ import '../../../widgets/dialogs/basic_dialog.dart'; import '../../../widgets/exchange/trocador/trocador_kyc_info_button.dart'; import '../../../widgets/exchange/trocador/trocador_rating_type_enum.dart'; import '../../../widgets/icon_widgets/exchange_icon.dart'; +import '../../../widgets/loading_indicator.dart'; class ExchangeOption extends ConsumerStatefulWidget { const ExchangeOption({ @@ -388,9 +389,7 @@ class _ProviderOptionState extends ConsumerState { if (loadingProgress == null) { return child; } else { - return const Center( - child: CircularProgressIndicator(), - ); + return const LoadingIndicator(); } }, errorBuilder: (context, error, stackTrace) { diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index ca102c4c5..46ab31b91 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -17,8 +17,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import '../../../db/isar/main_db.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../providers/providers.dart'; -import '../../../services/cakepay/cakepay_service.dart'; -import '../../../services/cakepay/src/models/order.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/constants.dart'; @@ -369,25 +367,6 @@ class HiddenSettings extends StatelessWidget { ); }, ), - const SizedBox(height: 12), - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (_) => const _CakePayDevStatusDialog(), - ); - }, - child: RoundedWhiteContainer( - child: Text( - "CakePay status overrides", - style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.accentColorDark, - ), - ), - ), - ), // const SizedBox( // height: 12, // ), @@ -428,124 +407,3 @@ class HiddenSettings extends StatelessWidget { ); } } - -class _CakePayDevStatusDialog extends StatefulWidget { - const _CakePayDevStatusDialog(); - - @override - State<_CakePayDevStatusDialog> createState() => - _CakePayDevStatusDialogState(); -} - -class _CakePayDevStatusDialogState extends State<_CakePayDevStatusDialog> { - late final List _orderIds; - - @override - void initState() { - super.initState(); - _orderIds = CakePayService.instance.getOrderIds(); - } - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).extension()!; - - return AlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "CakePay Status Overrides", - style: STextStyles.pageTitleH2(context), - ), - if (CakePayService.devStatusOverrides.isNotEmpty) - TextButton( - onPressed: () { - setState(() { - CakePayService.devStatusOverrides.clear(); - }); - }, - child: Text("Clear all", style: STextStyles.link2(context)), - ), - ], - ), - content: SizedBox( - width: 400, - child: _orderIds.isEmpty - ? Text( - "No tracked CakePay orders.\n" - "Create an order first, then come back here to override " - "its status.", - style: STextStyles.itemSubtitle(context), - ) - : ListView.separated( - shrinkWrap: true, - itemCount: _orderIds.length, - separatorBuilder: (_, __) => const Divider(height: 16), - itemBuilder: (context, index) { - final id = _orderIds[index]; - final current = CakePayService.devStatusOverrides[id]; - - return Row( - children: [ - Expanded( - child: Text( - id.length > 12 ? "${id.substring(0, 12)}..." : id, - style: STextStyles.itemSubtitle12(context), - ), - ), - const SizedBox(width: 8), - DropdownButton( - value: current, - hint: Text( - "API default", - style: STextStyles.itemSubtitle12( - context, - ).copyWith(color: colors.textSubtitle2), - ), - underline: const SizedBox(), - isDense: true, - items: [ - DropdownMenuItem( - value: null, - child: Text( - "API default", - style: STextStyles.itemSubtitle12( - context, - ).copyWith(color: colors.textSubtitle2), - ), - ), - ...CakePayOrderStatus.values.map( - (s) => DropdownMenuItem( - value: s, - child: Text( - s.value, - style: STextStyles.itemSubtitle12(context), - ), - ), - ), - ], - onChanged: (value) { - setState(() { - if (value == null) { - CakePayService.devStatusOverrides.remove(id); - } else { - CakePayService.devStatusOverrides[id] = value; - } - }); - }, - ), - ], - ); - }, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close", style: STextStyles.button(context)), - ), - ], - ); - } -} diff --git a/lib/pages/shopinbit/shopinbit_offer_view.dart b/lib/pages/shopinbit/shopinbit_offer_view.dart index ace2f3d37..f746b03c8 100644 --- a/lib/pages/shopinbit/shopinbit_offer_view.dart +++ b/lib/pages/shopinbit/shopinbit_offer_view.dart @@ -11,6 +11,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/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_shipping_view.dart'; @@ -154,14 +155,6 @@ class _ShopInBitOfferViewState extends State { ], ); - const loadingOverlay = Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ); - if (isDesktop) { return DesktopDialog( maxWidth: 580, @@ -187,7 +180,12 @@ class _ShopInBitOfferViewState extends State { horizontal: 32, vertical: 16, ), - child: Stack(children: [content, if (_loading) loadingOverlay]), + child: Stack( + children: [ + content, + if (_loading) const LoadingIndicator(width: 24, height: 24), + ], + ), ), ), ], @@ -220,7 +218,7 @@ class _ShopInBitOfferViewState extends State { ), ), ), - if (_loading) loadingOverlay, + if (_loading) const LoadingIndicator(width: 24, height: 24), ], ); }, diff --git a/lib/pages/shopinbit/shopinbit_payment_view.dart b/lib/pages/shopinbit/shopinbit_payment_view.dart index 0467d3fb7..98136696d 100644 --- a/lib/pages/shopinbit/shopinbit_payment_view.dart +++ b/lib/pages/shopinbit/shopinbit_payment_view.dart @@ -29,6 +29,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/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_send_from_view.dart'; @@ -471,14 +472,6 @@ class _ShopInBitPaymentViewState extends ConsumerState { Widget build(BuildContext context) { final isDesktop = Util.isDesktop; - const loadingOverlay = Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ); - // Build coin rows from _methods/_addresses final coinRows = []; for (int i = 0; i < _methods.length; i++) { @@ -737,7 +730,7 @@ class _ShopInBitPaymentViewState extends ConsumerState { child: Stack( children: [ SingleChildScrollView(child: content), - if (_loading) loadingOverlay, + if (_loading) const LoadingIndicator(width: 24, height: 24), ], ), ), @@ -779,7 +772,7 @@ class _ShopInBitPaymentViewState extends ConsumerState { ), ), ), - if (_loading) loadingOverlay, + if (_loading) const LoadingIndicator(width: 24, height: 24), ], ); }, diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index 85ceb97cf..1a7781656 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -15,6 +15,7 @@ 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/desktop/primary_button.dart'; +import '../../widgets/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_offer_view.dart'; @@ -497,14 +498,7 @@ class _ShopInBitTicketDetailState extends State { return _chatBubble(message, isDesktop); }, ), - if (_loading) - const Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), + if (_loading) const LoadingIndicator(width: 24, height: 24), ], ), ); diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart index ce62d3be3..d600a00fc 100644 --- a/lib/pages/shopinbit/shopinbit_tickets_view.dart +++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart @@ -15,6 +15,7 @@ import '../../widgets/background.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/loading_indicator.dart'; import '../../widgets/rounded_white_container.dart'; import 'shopinbit_car_fee_view.dart'; import 'shopinbit_car_research_payment_view.dart'; @@ -446,14 +447,7 @@ class _ShopInBitTicketsViewState extends State { final content = Stack( children: [ list, - if (_syncing) - const Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), + if (_syncing) const LoadingIndicator(width: 24, height: 24), ], ); diff --git a/lib/services/cakepay/cakepay_service.dart b/lib/services/cakepay/cakepay_service.dart index 1016bc4b7..86c493045 100644 --- a/lib/services/cakepay/cakepay_service.dart +++ b/lib/services/cakepay/cakepay_service.dart @@ -1,16 +1,11 @@ import '../../db/hive/db.dart'; import '../../external_api_keys.dart'; import 'src/client.dart'; -import 'src/models/order.dart'; class CakePayService { static final instance = CakePayService._(); CakePayService._(); - /// Dev-only: override order statuses for local UI testing. - /// Keys are order IDs, values are the status to pretend the API returned. - static final Map devStatusOverrides = {}; - CakePayClient? _client; CakePayClient get client { From 2c790e2a859d52f32d6afa6d25f191826e828c18 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 May 2026 14:55:41 -0600 Subject: [PATCH 2/5] feat(db): create a shared drift database with initial table for cake pay order IDs --- lib/db/drift/shared_database.dart | 51 +++++ lib/db/drift/shared_database.g.dart | 303 ++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 lib/db/drift/shared_database.dart create mode 100644 lib/db/drift/shared_database.g.dart diff --git a/lib/db/drift/shared_database.dart b/lib/db/drift/shared_database.dart new file mode 100644 index 000000000..2e00d3c90 --- /dev/null +++ b/lib/db/drift/shared_database.dart @@ -0,0 +1,51 @@ +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart'; +import 'package:path/path.dart' as path; + +import '../../utilities/stack_file_system.dart'; + +part 'shared_database.g.dart'; + +abstract final class SharedDrift { + static bool _didInit = false; + + static SharedDatabase? _db; + + static SharedDatabase get() { + if (!_didInit) { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + _didInit = true; + } + + return _db ??= SharedDatabase._(); + } +} + +class CakepayOrders extends Table { + TextColumn get orderId => text()(); + + @override + Set get primaryKey => {orderId}; +} + +@DriftDatabase(tables: [CakepayOrders]) +final class SharedDatabase extends _$SharedDatabase { + SharedDatabase._([QueryExecutor? executor]) + : super(executor ?? _openConnection()); + + @override + int get schemaVersion => 1; + + static QueryExecutor _openConnection() { + return driftDatabase( + name: "shared", + native: DriftNativeOptions( + shareAcrossIsolates: true, + databasePath: () async { + final dir = await StackFileSystem.applicationDriftDirectory(); + return path.join(dir.path, "shared", "shared.db"); + }, + ), + ); + } +} diff --git a/lib/db/drift/shared_database.g.dart b/lib/db/drift/shared_database.g.dart new file mode 100644 index 000000000..2e0c5de8b --- /dev/null +++ b/lib/db/drift/shared_database.g.dart @@ -0,0 +1,303 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'shared_database.dart'; + +// ignore_for_file: type=lint +class $CakepayOrdersTable extends CakepayOrders + with TableInfo<$CakepayOrdersTable, CakepayOrder> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CakepayOrdersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _orderIdMeta = const VerificationMeta( + 'orderId', + ); + @override + late final GeneratedColumn orderId = GeneratedColumn( + 'order_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [orderId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'cakepay_orders'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('order_id')) { + context.handle( + _orderIdMeta, + orderId.isAcceptableOrUnknown(data['order_id']!, _orderIdMeta), + ); + } else if (isInserting) { + context.missing(_orderIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {orderId}; + @override + CakepayOrder map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CakepayOrder( + orderId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}order_id'], + )!, + ); + } + + @override + $CakepayOrdersTable createAlias(String alias) { + return $CakepayOrdersTable(attachedDatabase, alias); + } +} + +class CakepayOrder extends DataClass implements Insertable { + final String orderId; + const CakepayOrder({required this.orderId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['order_id'] = Variable(orderId); + return map; + } + + CakepayOrdersCompanion toCompanion(bool nullToAbsent) { + return CakepayOrdersCompanion(orderId: Value(orderId)); + } + + factory CakepayOrder.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CakepayOrder(orderId: serializer.fromJson(json['orderId'])); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return {'orderId': serializer.toJson(orderId)}; + } + + CakepayOrder copyWith({String? orderId}) => + CakepayOrder(orderId: orderId ?? this.orderId); + CakepayOrder copyWithCompanion(CakepayOrdersCompanion data) { + return CakepayOrder( + orderId: data.orderId.present ? data.orderId.value : this.orderId, + ); + } + + @override + String toString() { + return (StringBuffer('CakepayOrder(') + ..write('orderId: $orderId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => orderId.hashCode; + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CakepayOrder && other.orderId == this.orderId); +} + +class CakepayOrdersCompanion extends UpdateCompanion { + final Value orderId; + final Value rowid; + const CakepayOrdersCompanion({ + this.orderId = const Value.absent(), + this.rowid = const Value.absent(), + }); + CakepayOrdersCompanion.insert({ + required String orderId, + this.rowid = const Value.absent(), + }) : orderId = Value(orderId); + static Insertable custom({ + Expression? orderId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (orderId != null) 'order_id': orderId, + if (rowid != null) 'rowid': rowid, + }); + } + + CakepayOrdersCompanion copyWith({Value? orderId, Value? rowid}) { + return CakepayOrdersCompanion( + orderId: orderId ?? this.orderId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (orderId.present) { + map['order_id'] = Variable(orderId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CakepayOrdersCompanion(') + ..write('orderId: $orderId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$SharedDatabase extends GeneratedDatabase { + _$SharedDatabase(QueryExecutor e) : super(e); + $SharedDatabaseManager get managers => $SharedDatabaseManager(this); + late final $CakepayOrdersTable cakepayOrders = $CakepayOrdersTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [cakepayOrders]; +} + +typedef $$CakepayOrdersTableCreateCompanionBuilder = + CakepayOrdersCompanion Function({ + required String orderId, + Value rowid, + }); +typedef $$CakepayOrdersTableUpdateCompanionBuilder = + CakepayOrdersCompanion Function({Value orderId, Value rowid}); + +class $$CakepayOrdersTableFilterComposer + extends Composer<_$SharedDatabase, $CakepayOrdersTable> { + $$CakepayOrdersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get orderId => $composableBuilder( + column: $table.orderId, + builder: (column) => ColumnFilters(column), + ); +} + +class $$CakepayOrdersTableOrderingComposer + extends Composer<_$SharedDatabase, $CakepayOrdersTable> { + $$CakepayOrdersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get orderId => $composableBuilder( + column: $table.orderId, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$CakepayOrdersTableAnnotationComposer + extends Composer<_$SharedDatabase, $CakepayOrdersTable> { + $$CakepayOrdersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get orderId => + $composableBuilder(column: $table.orderId, builder: (column) => column); +} + +class $$CakepayOrdersTableTableManager + extends + RootTableManager< + _$SharedDatabase, + $CakepayOrdersTable, + CakepayOrder, + $$CakepayOrdersTableFilterComposer, + $$CakepayOrdersTableOrderingComposer, + $$CakepayOrdersTableAnnotationComposer, + $$CakepayOrdersTableCreateCompanionBuilder, + $$CakepayOrdersTableUpdateCompanionBuilder, + ( + CakepayOrder, + BaseReferences<_$SharedDatabase, $CakepayOrdersTable, CakepayOrder>, + ), + CakepayOrder, + PrefetchHooks Function() + > { + $$CakepayOrdersTableTableManager( + _$SharedDatabase db, + $CakepayOrdersTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$CakepayOrdersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$CakepayOrdersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$CakepayOrdersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value orderId = const Value.absent(), + Value rowid = const Value.absent(), + }) => CakepayOrdersCompanion(orderId: orderId, rowid: rowid), + createCompanionCallback: + ({ + required String orderId, + Value rowid = const Value.absent(), + }) => + CakepayOrdersCompanion.insert(orderId: orderId, rowid: rowid), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$CakepayOrdersTableProcessedTableManager = + ProcessedTableManager< + _$SharedDatabase, + $CakepayOrdersTable, + CakepayOrder, + $$CakepayOrdersTableFilterComposer, + $$CakepayOrdersTableOrderingComposer, + $$CakepayOrdersTableAnnotationComposer, + $$CakepayOrdersTableCreateCompanionBuilder, + $$CakepayOrdersTableUpdateCompanionBuilder, + ( + CakepayOrder, + BaseReferences<_$SharedDatabase, $CakepayOrdersTable, CakepayOrder>, + ), + CakepayOrder, + PrefetchHooks Function() + >; + +class $SharedDatabaseManager { + final _$SharedDatabase _db; + $SharedDatabaseManager(this._db); + $$CakepayOrdersTableTableManager get cakepayOrders => + $$CakepayOrdersTableTableManager(_db, _db.cakepayOrders); +} From e37737e8140467d3593b58d4290c40d6449354a8 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 May 2026 14:57:16 -0600 Subject: [PATCH 3/5] use drift/sqlite to store order ids instead of piggybacking in the preferences store --- lib/pages/cakepay/cakepay_orders_view.dart | 2 +- lib/services/cakepay/cakepay_service.dart | 52 ++++++++++------------ 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/lib/pages/cakepay/cakepay_orders_view.dart b/lib/pages/cakepay/cakepay_orders_view.dart index 3f52d5ada..48b966507 100644 --- a/lib/pages/cakepay/cakepay_orders_view.dart +++ b/lib/pages/cakepay/cakepay_orders_view.dart @@ -39,7 +39,7 @@ class _CakePayOrdersViewState extends State { Future _syncFromApi() async { setState(() => _syncing = true); try { - final orderIds = CakePayService.instance.getOrderIds(); + final orderIds = await CakePayService.instance.getOrderIds(); final results = []; for (final id in orderIds) { diff --git a/lib/services/cakepay/cakepay_service.dart b/lib/services/cakepay/cakepay_service.dart index 86c493045..48db6a917 100644 --- a/lib/services/cakepay/cakepay_service.dart +++ b/lib/services/cakepay/cakepay_service.dart @@ -1,4 +1,6 @@ -import '../../db/hive/db.dart'; +import 'package:drift/drift.dart'; + +import '../../db/drift/shared_database.dart'; import '../../external_api_keys.dart'; import 'src/client.dart'; @@ -12,35 +14,29 @@ class CakePayService { return _client ??= CakePayClient(apiToken: kCakePayApiToken); } - // Mirrors ShopInBit's local ticket storage pattern but uses lightweight - // Hive prefs instead of a full Isar collection, since CakePay orders can - // be fetched individually via getOrder() with the seller key. - - static const _kCakePayOrderIds = "cakePayOrderIds"; - - /// Persist a newly-created order ID so the orders list view can find it - /// later without requiring Knox user auth. - void addOrderId(String orderId) { - final ids = getOrderIds(); - if (!ids.contains(orderId)) { - ids.insert(0, orderId); - DB.instance.put( - boxName: DB.boxNamePrefs, - key: _kCakePayOrderIds, - value: ids, - ); - } + Future addOrderId(String orderId) async { + final db = SharedDrift.get(); + + await db.transaction(() async { + await db + .into(db.cakepayOrders) + .insert( + CakepayOrdersCompanion.insert(orderId: orderId), + mode: .insertOrIgnore, + ); + }); } /// Return locally-tracked order IDs (most recent first). - List getOrderIds() { - final raw = DB.instance.get( - boxName: DB.boxNamePrefs, - key: _kCakePayOrderIds, - ); - if (raw is List) { - return raw.cast().toList(); - } - return []; + Future> getOrderIds() async { + final db = SharedDrift.get(); + + final rows = + await (db.select(db.cakepayOrders)..orderBy([ + (t) => OrderingTerm(expression: t.rowId, mode: OrderingMode.desc), + ])) + .get(); + + return rows.map((row) => row.orderId).toList(); } } From cdcc72d6d8995fc15ea2603ac8297a55544f5906 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 14 May 2026 14:24:51 -0600 Subject: [PATCH 4/5] chore: refactor a big widget's build method and some more general house keeping --- .../cakepay/cakepay_card_detail_view.dart | 749 ++++++++++-------- lib/pages/cakepay/cakepay_vendors_view.dart | 50 +- lib/pages/more_view/gift_cards_view.dart | 29 +- lib/pages/wallet_view/wallet_view.dart | 8 +- .../sub_widgets/desktop_gift_cards_view.dart | 17 +- lib/services/cakepay/src/models/card.dart | 110 +-- .../icon_widgets/credit_card_icon.dart | 31 + 7 files changed, 588 insertions(+), 406 deletions(-) create mode 100644 lib/widgets/icon_widgets/credit_card_icon.dart diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart index 7fbd0ebff..371d61e1c 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 { 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 { 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 { // 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( - 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( + context: context, + builder: (_) => CakePayOrderView(orderId: order.orderId), + ); + } else { + await Navigator.of(context).pushReplacementNamed( + CakePayOrderView.routeName, + arguments: order.orderId, + ); + } } } else { await showDialog( @@ -251,90 +255,366 @@ class _CakePayCardDetailViewState extends State { 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()!.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()!.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()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context).copyWith( - color: Theme.of( - context, - ).extension()!.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 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()!.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()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context).copyWith( + color: Theme.of( + context, + ).extension()!.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 { 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 { child: IgnorePointer( child: Checkbox( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _termsAccepted, + value: accepted, onChanged: (_) {}, ), ), @@ -392,7 +689,7 @@ class _CakePayCardDetailViewState extends State { 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 { ), ), ); + } +} - 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()!.textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context).copyWith( - color: Theme.of( - context, - ).extension()!.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()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context).copyWith( + color: Theme.of( + context, + ).extension()!.textFieldActiveText, ), - ), - ], - ), - ), - child: ConditionalParent( - condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: Theme.of( + decoration: + standardInputDecoration( + "Email", + focusNode, context, - ).extension()!.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 2c7b3f5cb..5c16cdd7d 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 { }); } - void _onCardTapped(CakePayCard card) { + Future _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( + + await showDialog( 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 { ), ), 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 { 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 9fcf82bbc..48ff0f364 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 { context, ).extension()!.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 { 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 { 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 12affd42b..83a4d6e8f 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 { ), 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 7693f4357..964028acb 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 { 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()!.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 2fed2f47e..83d2eb3bc 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 denominations; - final double? minValue; - final double? maxValue; - final double? minValueUsd; - final double? maxValueUsd; + final List 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 json) { - final rawDenoms = json['denominations'] ?? json['denominations_list']; - final denominations = []; + final dynamic rawDenoms = + json["denominations"] ?? json["denominations_list"]; + final List denominations = []; 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)['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)["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 toMap() { + return { + "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 000000000..369792e56 --- /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()!.textDark3, + BlendMode.srcIn, + ), + ); + } +} From 211971b5d7c6cac19173071026388dbaf756e8ab Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 14 May 2026 14:27:01 -0600 Subject: [PATCH 5/5] fix: context.mounted check (and a bunch of auto format) --- .../sub_widgets/crypto_selection_view.dart | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart index 089b492af..7f7705b43 100644 --- a/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart +++ b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart @@ -98,8 +98,9 @@ class _CryptoSelectionViewState extends ConsumerState { builder: (child) { return Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -109,7 +110,7 @@ class _CryptoSelectionViewState extends ConsumerState { const Duration(milliseconds: 50), ); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -145,45 +146,45 @@ class _CryptoSelectionViewState extends ConsumerState { focusNode: _searchFocusNode, onChanged: filter, style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: - _searchController.text.isNotEmpty + decoration: + standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - filter(""); - }, - ), - ], + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + filter(""); + }, + ), + ], + ), ), - ), - ) + ) : null, - ), + ), ), ), const SizedBox(height: 10), @@ -226,14 +227,12 @@ class _CryptoSelectionViewState extends ConsumerState { const SizedBox(height: 2), Text( _coins[index].ticker.toUpperCase(), - style: STextStyles.smallMed12( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) .extension()! .textSubtitle1, - ), + ), ), ], ),