diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 000000000..7e7e7f67d
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1 @@
+extensions:
diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 945d2b5c1..635b4de60 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -47,6 +47,8 @@
LSRequiresIPhoneOS
+ UIApplicationSupportsIndirectInputEvents
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
@@ -68,7 +70,5 @@
UIViewControllerBasedStatusBarAppearance
- UIApplicationSupportsIndirectInputEvents
-
diff --git a/lib/core/strings.dart b/lib/core/strings.dart
index 5a05ee6aa..5e165ddfe 100644
--- a/lib/core/strings.dart
+++ b/lib/core/strings.dart
@@ -162,14 +162,20 @@ abstract final class Strings {
return "You're buying $amount $title tickets";
}
- static String paymentConfirmationTopSingle(int amount, String title) {
- return "You're buying and swiping $amount $title";
+ static String paymentConfirmationTopSingle(String title) {
+ return "You're buying 1 $title";
+ }
+
+ static String paymentConfirmationTopFreeSingle(String title) {
+ return "You're claiming 1 $title";
}
static String paymentConfirmationBottomPurchase(int price) {
return 'Pay $price,- with...';
}
+ static String oneCup = '1 cup';
+
static String amountTickets(int amount) {
return '$amount tickets';
}
diff --git a/lib/core/styles/text_style_builder.dart b/lib/core/styles/text_style_builder.dart
index 466d468a9..9123691b0 100644
--- a/lib/core/styles/text_style_builder.dart
+++ b/lib/core/styles/text_style_builder.dart
@@ -148,6 +148,8 @@ class TextStyleBuilder {
final isMono = _fontFamily == _AnalogFontFamily.mono;
final letterSpacing = _fontFamily == _AnalogFontFamily.body ? 0.25 : null;
+ const lineHeight = 1.1;
+ final height = isHeading ? lineHeight : null;
return isMono
? TextStyle(
fontFamily: 'RobotoMono',
@@ -162,6 +164,7 @@ class TextStyleBuilder {
color: _color,
decoration: _decoration,
letterSpacing: letterSpacing,
+ height: height,
fontVariations: [
FontVariation('wght', _fontWeight),
if (_fontSize != null) FontVariation('opsz', _fontSize!),
diff --git a/lib/core/widgets/components/barista_perks_section.dart b/lib/core/widgets/components/barista_perks_section.dart
index 8111c828d..06fc3be73 100644
--- a/lib/core/widgets/components/barista_perks_section.dart
+++ b/lib/core/widgets/components/barista_perks_section.dart
@@ -27,12 +27,9 @@ class _BaristaPerksSectionState extends State {
final roleName = widget.userRole.name.capitalize();
return Column(
children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- SectionTitle(Strings.perksTitle(roleName)),
- UserRoleIndicator(widget.userRole),
- ],
+ SectionTitle.withSideWidget(
+ Strings.perksTitle(roleName),
+ sideWidget: UserRoleIndicator(widget.userRole),
),
Grid(
gap: GridGap.normal,
diff --git a/lib/core/widgets/components/helpers/grid.dart b/lib/core/widgets/components/helpers/grid.dart
index 2d4b4862a..d3f6cbf5a 100644
--- a/lib/core/widgets/components/helpers/grid.dart
+++ b/lib/core/widgets/components/helpers/grid.dart
@@ -38,6 +38,8 @@ class Grid extends StatelessWidget {
int _columns(bool isSmall) => isSmall && singleColumnOnSmallDevice ? 1 : 2;
+ int _rows(bool isSmall) => (children.length / _columns(isSmall)).ceil();
+
@override
Widget build(BuildContext context) {
final isSmall = deviceIsSmall(context);
@@ -47,15 +49,12 @@ class Grid extends StatelessWidget {
if (children.isEmpty) {
return const SizedBox.shrink();
}
- return UnconstrainedBox(
- constrainedAxis: Axis.horizontal,
- child: LayoutGrid(
- columnSizes: List.filled(columns, 1.fr),
- rowSizes: List.filled(children.length, auto),
- columnGap: horizontalGap,
- rowGap: verticalGap,
- children: children,
- ),
+ return LayoutGrid(
+ columnSizes: List.filled(columns, 1.fr),
+ rowSizes: List.filled(_rows(isSmall), auto),
+ columnGap: horizontalGap,
+ rowGap: verticalGap,
+ children: children,
);
}
}
diff --git a/lib/core/widgets/components/section_title.dart b/lib/core/widgets/components/section_title.dart
index 61b997f18..188f93b40 100644
--- a/lib/core/widgets/components/section_title.dart
+++ b/lib/core/widgets/components/section_title.dart
@@ -2,22 +2,28 @@ import 'package:coffeecard/core/styles/app_text_styles.dart';
import 'package:flutter/material.dart';
class SectionTitle extends StatelessWidget {
- final String title;
- final double bottomMargin;
+ /// A section title no side widget.
+ const SectionTitle(this.title) : sideWidget = null;
- const SectionTitle(this.title) : bottomMargin = 8;
+ /// A section title with a side widget.
+ const SectionTitle.withSideWidget(this.title, {required this.sideWidget});
- /// Section title for register screens. Has extra bottom margin.
- const SectionTitle.register(this.title) : bottomMargin = 16;
+ final String title;
+ final Widget? sideWidget;
@override
Widget build(BuildContext context) {
- return Container(
- margin: EdgeInsets.only(bottom: bottomMargin),
- child: Text(
- title,
- style: AppTextStyle.sectionTitle,
- ),
+ final textWidget = Text(title, style: AppTextStyle.sectionTitle);
+ final sideWidget = this.sideWidget;
+
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: (sideWidget == null)
+ ? textWidget
+ : Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [textWidget, sideWidget],
+ ),
);
}
}
diff --git a/lib/features/form/presentation/widgets/form.dart b/lib/features/form/presentation/widgets/form.dart
index a4363429e..efa118668 100644
--- a/lib/features/form/presentation/widgets/form.dart
+++ b/lib/features/form/presentation/widgets/form.dart
@@ -7,6 +7,7 @@ import 'package:coffeecard/features/form/presentation/bloc/form_bloc.dart';
import 'package:flutter/material.dart' hide FormState;
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:gap/gap.dart';
part 'form_text_field.dart';
@@ -86,7 +87,8 @@ class FormBase extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- if (title != null) SectionTitle.register(title!),
+ if (title != null) SectionTitle(title!),
+ const Gap(8),
_FormTextField(
inputValidators: inputValidators,
onChanged: (input) {
diff --git a/lib/features/leaderboard/presentation/widgets/statistics_section.dart b/lib/features/leaderboard/presentation/widgets/statistics_section.dart
index 976d3e4d6..632ca0ba9 100644
--- a/lib/features/leaderboard/presentation/widgets/statistics_section.dart
+++ b/lib/features/leaderboard/presentation/widgets/statistics_section.dart
@@ -1,12 +1,11 @@
import 'package:coffeecard/core/strings.dart';
-import 'package:coffeecard/core/styles/app_text_styles.dart';
import 'package:coffeecard/core/widgets/components/helpers/grid.dart';
+import 'package:coffeecard/core/widgets/components/section_title.dart';
import 'package:coffeecard/features/leaderboard/presentation/widgets/statistics_card.dart';
import 'package:coffeecard/features/user/domain/entities/user.dart';
import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:gap/gap.dart';
class StatisticsSection extends StatelessWidget {
const StatisticsSection();
@@ -35,8 +34,7 @@ class _YourStatsGrid extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- Text(Strings.statsYourStats, style: AppTextStyle.sectionTitle),
- const Gap(8),
+ const SectionTitle(Strings.statsYourStats),
Grid(
singleColumnOnSmallDevice: false,
gap: GridGap.tightVertical,
diff --git a/lib/features/occupation/presentation/widgets/occupation_form.dart b/lib/features/occupation/presentation/widgets/occupation_form.dart
index 220a651e6..16cda18fd 100644
--- a/lib/features/occupation/presentation/widgets/occupation_form.dart
+++ b/lib/features/occupation/presentation/widgets/occupation_form.dart
@@ -38,7 +38,6 @@ class OccupationForm extends StatelessWidget {
children: [
const Gap(16),
const SectionTitle(Strings.registerOccupationTitle),
- const Gap(16),
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(24)),
child: Column(
diff --git a/lib/features/product/presentation/functions.dart b/lib/features/product/presentation/functions.dart
index 0405383a8..97047283b 100644
--- a/lib/features/product/presentation/functions.dart
+++ b/lib/features/product/presentation/functions.dart
@@ -18,11 +18,18 @@ Future buyModal({
/// Callback that will be run after the purchase modal is closed, but before
/// the receipt overlay is shown.
+ ///
+ /// [payment] will be null if the purchase was cancelled.
required Future Function(BuildContext, Payment?) callback,
}) async {
- final scrimText = (product.price == 0)
- ? Strings.paymentConfirmationTopSingle(product.amount, product.name)
- : Strings.paymentConfirmationTopTickets(product.amount, product.name);
+ // Find the correct text to show in the scrim.
+ const free = 0;
+ const single = 1;
+ final scrimText = switch ((product.price, product.amount)) {
+ (free, single) => Strings.paymentConfirmationTopFreeSingle(product.name),
+ (_, single) => Strings.paymentConfirmationTopSingle(product.name),
+ _ => Strings.paymentConfirmationTopTickets(product.amount, product.name),
+ };
// Create a task that will open the purchase modal and wait for the result.
final maybePayment = await showModalBottomSheet(
@@ -44,29 +51,33 @@ Future buyModal({
if (!context.mounted) return;
// Show the receipt overlay if the payment was successful.
- return _afterPurchaseModal(context, maybePayment);
-}
-
-Future _afterPurchaseModal(BuildContext context, Payment? payment) async {
- // Don't do anything if the payment is null or not completed.
- if (payment == null || payment.status != PaymentStatus.completed) {
- return;
+ if (maybePayment?.status == PaymentStatus.completed) {
+ return _afterPurchaseModal(context, maybePayment!, product);
}
+}
+Future _afterPurchaseModal(
+ BuildContext context,
+ Payment payment,
+ Product product,
+) async {
final envState = context.read().state;
+ final singleTicketPurchase = product.amount == 1;
- final updateTicketsRequest = context.read().getTickets();
- final updateReceiptsRequest = context.read().fetchReceipts();
+ final ticketsCubit = context.read();
+ final receiptCubit = context.read();
- ReceiptOverlay.show(
- context: context,
- isTestEnvironment: envState is EnvironmentLoaded && envState.env.isTest,
- status: Strings.purchased,
- productName: payment.productName,
- timeUsed: payment.purchaseTime,
- ).ignore();
-
- // TODO: Explain why we need to await here.
- await updateTicketsRequest;
- await updateReceiptsRequest;
+ if (singleTicketPurchase) {
+ await ticketsCubit.useTicket(product.id);
+ } else {
+ ticketsCubit.getTickets();
+ ReceiptOverlay.show(
+ context: context,
+ isTestEnvironment: envState is EnvironmentLoaded && envState.env.isTest,
+ status: Strings.purchased,
+ productName: payment.productName,
+ timeUsed: payment.purchaseTime,
+ ).ignore();
+ }
+ await receiptCubit.fetchReceipts();
}
diff --git a/lib/features/product/presentation/widgets/buy_tickets_card.dart b/lib/features/product/presentation/widgets/buy_tickets_card.dart
index ffd17ec12..75d7af14d 100644
--- a/lib/features/product/presentation/widgets/buy_tickets_card.dart
+++ b/lib/features/product/presentation/widgets/buy_tickets_card.dart
@@ -4,6 +4,7 @@ import 'package:coffeecard/core/widgets/components/card.dart';
import 'package:coffeecard/core/widgets/components/helpers/responsive.dart';
import 'package:coffeecard/features/product/domain/entities/product.dart';
import 'package:coffeecard/features/product/presentation/functions.dart';
+import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
@@ -21,7 +22,11 @@ class _BuyTicketsCardState extends State {
return buyModal(
context: context,
product: product,
- callback: (context, _) async => Navigator.of(context).pop(),
+ callback: (context, maybePayment) async {
+ if (maybePayment?.status == PaymentStatus.completed) {
+ Navigator.of(context).pop();
+ }
+ },
);
}
diff --git a/lib/features/ticket/presentation/pages/tickets_page.dart b/lib/features/ticket/presentation/pages/tickets_page.dart
index 5e541154b..ee1dd7125 100644
--- a/lib/features/ticket/presentation/pages/tickets_page.dart
+++ b/lib/features/ticket/presentation/pages/tickets_page.dart
@@ -2,6 +2,7 @@ import 'package:coffeecard/core/strings.dart';
import 'package:coffeecard/core/widgets/components/barista_perks_section.dart';
import 'package:coffeecard/core/widgets/components/scaffold.dart';
import 'package:coffeecard/core/widgets/upgrade_alert.dart';
+import 'package:coffeecard/features/product/domain/entities/purchasable_products.dart';
import 'package:coffeecard/features/ticket/presentation/widgets/shop_section.dart';
import 'package:coffeecard/features/ticket/presentation/widgets/tickets_section.dart';
import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart';
@@ -22,7 +23,7 @@ class TicketsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = (context.read().state as UserLoaded).user;
- final hasBaristaPerks = user.hasBaristaPerks;
+ final perksAvailable = context.read().perks.isNotEmpty;
return UpgradeAlert(
child: AppScaffold.withTitle(
@@ -37,7 +38,7 @@ class TicketsPage extends StatelessWidget {
padding: const EdgeInsets.all(16.0),
children: [
const TicketSection(),
- if (hasBaristaPerks) BaristaPerksSection(userRole: user.role),
+ if (perksAvailable) BaristaPerksSection(userRole: user.role),
const ShopSection(),
],
),
diff --git a/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart b/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart
index 0edff46ba..62435484c 100644
--- a/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart
+++ b/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart
@@ -119,7 +119,9 @@ class _ModalContentState extends State<_ModalContent>
child: SlideAction(
elevation: 0,
text: Strings.useTicket,
- textStyle: AppTextStyle.buttonText,
+ textStyle: AppTextStyle.buttonText.apply(
+ color: AppColors.white,
+ ),
height: 56,
sliderButtonIcon: const Icon(
Icons.navigate_next,
diff --git a/lib/features/ticket/presentation/widgets/tickets_section.dart b/lib/features/ticket/presentation/widgets/tickets_section.dart
index 021b4f33e..2c004fd64 100644
--- a/lib/features/ticket/presentation/widgets/tickets_section.dart
+++ b/lib/features/ticket/presentation/widgets/tickets_section.dart
@@ -26,13 +26,9 @@ class TicketSection extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- const Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- SectionTitle(Strings.ticketsMyTickets),
- OpeningHoursIndicator(),
- ],
+ const SectionTitle.withSideWidget(
+ Strings.ticketsMyTickets,
+ sideWidget: OpeningHoursIndicator(),
),
BlocConsumer(
listenWhen: (previous, current) =>
diff --git a/test/core/widgets/components/tickets/goldens/buy_tickets_card.png b/test/core/widgets/components/tickets/goldens/buy_tickets_card.png
index f46050610..0b7112ab3 100644
Binary files a/test/core/widgets/components/tickets/goldens/buy_tickets_card.png and b/test/core/widgets/components/tickets/goldens/buy_tickets_card.png differ
diff --git a/test/core/widgets/components/tickets/goldens/coffee_card.png b/test/core/widgets/components/tickets/goldens/coffee_card.png
index 234aa6341..8c380d9bf 100644
Binary files a/test/core/widgets/components/tickets/goldens/coffee_card.png and b/test/core/widgets/components/tickets/goldens/coffee_card.png differ