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