Skip to content

Commit

Permalink
smallest commit to coffeecard_app:
Browse files Browse the repository at this point in the history
main:
Add support for using concrete products when claiming a ticket
-> Upgraded ticket use flow to accommodate the selection of menu items explicitly.
-> The last selected menu item is cached locally for each ticket (cache cleared on logout).

other:
Simplified the Product feature file structure.
Add improvements to network request handling by adopting a TaskEither type for better composability.
Added package Hive for local storage
Rename some files and refactor several imports across the codebase
Fixed various minor bugs, tightened type constraints, and made use of pattern matching & fpdart types.
  • Loading branch information
marfavi committed Jan 22, 2024
1 parent 7ed6d62 commit fec6939
Show file tree
Hide file tree
Showing 94 changed files with 1,001 additions and 794 deletions.
2 changes: 1 addition & 1 deletion lib/core/firebase_analytics_event_logging.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:coffeecard/features/product/domain/entities/product.dart';
import 'package:coffeecard/features/product/product_model.dart';
import 'package:coffeecard/features/purchase/domain/entities/payment.dart';
import 'package:firebase_analytics/firebase_analytics.dart';

Expand Down
10 changes: 9 additions & 1 deletion lib/core/network/network_request_executor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import 'package:logger/logger.dart';
part 'network_request_executor_mapping.dart';

typedef _NetworkRequest<BodyType> = Future<Response<BodyType>> Function();
typedef _ExecutorResult<R> = Future<Either<NetworkFailure, R>>;
typedef _ExecutorResult<R> = Future<Either<Failure, R>>;
typedef _ExecutorTaskEither<R> = TaskEither<Failure, R>;

class NetworkRequestExecutor {
final Logger logger;
Expand Down Expand Up @@ -43,6 +44,13 @@ class NetworkRequestExecutor {
}
}

/// Executes a network request inside a [TaskEither].
///
/// See [execute] for more information.
_ExecutorTaskEither<Body> executeAsTask<Body>(_NetworkRequest<Body> request) {
return TaskEither(() => execute(request));
}

/// Executes the network [request] and returns the result as an [Either].
///
/// If the request fails, a [NetworkFailure] is returned in a [Left].
Expand Down
2 changes: 1 addition & 1 deletion lib/core/widgets/components/barista_perks_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:coffeecard/core/strings.dart';
import 'package:coffeecard/core/widgets/components/helpers/grid.dart';
import 'package:coffeecard/core/widgets/components/section_title.dart';
import 'package:coffeecard/core/widgets/components/user_role_indicator.dart';
import 'package:coffeecard/features/product/domain/entities/purchasable_products.dart';
import 'package:coffeecard/features/product/purchasable_products.dart';
import 'package:coffeecard/features/ticket/presentation/widgets/perk_card.dart';
import 'package:coffeecard/features/user/domain/entities/role.dart';
import 'package:flutter/material.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,47 @@
import 'package:coffeecard/core/strings.dart';
import 'package:coffeecard/core/styles/app_colors.dart';
import 'package:coffeecard/core/styles/app_text_styles.dart';
import 'package:coffeecard/core/widgets/components/card.dart';
import 'package:coffeecard/core/widgets/components/helpers/shimmer_builder.dart';
import 'package:coffeecard/features/ticket/domain/entities/ticket.dart';
import 'package:coffeecard/features/ticket/presentation/widgets/swipe_ticket_confirm.dart';
import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';

class CoffeeCard extends StatelessWidget {
final String title;
final int amountOwned;
final int productId;
part 'tickets_card_placeholder.dart';

/// A card representing a group of tickets owned by the user.
///
/// See also [NoTicketsPlaceholder], which is used when the user has no tickets.
class TicketsCard extends StatelessWidget {
/// Creates a [TicketsCard] with the given [ticket].
const TicketsCard(this.ticket);

/// A shimmering placeholder for a [TicketsCard].
const TicketsCard.loadingPlaceholder() : ticket = const Ticket.empty();

final Ticket ticket;

const CoffeeCard({
required this.title,
required this.amountOwned,
required this.productId,
});
bool get isLoadingPlaceholder => ticket == const Ticket.empty();

@override
Widget build(BuildContext context) {
return CardBase(
color: AppColors.ticket,
top: CardTitle(
title: Text(title, style: AppTextStyle.ownedTicket),
),
bottom: CardBottomRow(
left: _TicketDots(amountOwned: amountOwned),
right: _TicketAmountText(amountOwned: amountOwned),
),
gap: 36,
onTap: (context) {
final _ = showSwipeTicketConfirm(
context: context,
productName: title,
amountOwned: amountOwned,
productId: productId,
return ShimmerBuilder(
showShimmer: isLoadingPlaceholder,
builder: (context, colorIfShimmer) {
return CardBase(
color: isLoadingPlaceholder ? colorIfShimmer : AppColors.ticket,
top: CardTitle(
title: Text(ticket.product.name, style: AppTextStyle.ownedTicket),
),
bottom: CardBottomRow(
left: _TicketDots(amountOwned: ticket.amountLeft),
right: _TicketAmountText(amountOwned: ticket.amountLeft),
),
gap: 36,
onTap: (context) {
final _ = showSwipeTicketConfirm(context: context, ticket: ticket);
},
);
},
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import 'package:coffeecard/core/strings.dart';
import 'package:coffeecard/core/styles/app_colors.dart';
import 'package:coffeecard/core/styles/app_text_styles.dart';
import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';
part of 'tickets_card.dart';

class CoffeeCardPlaceholder extends StatelessWidget {
const CoffeeCardPlaceholder();
/// A widget that shows instead of a [TicketsCard] when the user has no tickets.
class NoTicketsPlaceholder extends StatelessWidget {
const NoTicketsPlaceholder();

@override
Widget build(BuildContext context) {
Expand Down
2 changes: 1 addition & 1 deletion lib/core/widgets/pages/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:coffeecard/core/widgets/routers/app_flow.dart';
import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart';
import 'package:coffeecard/features/leaderboard/presentation/pages/leaderboard_page.dart';
import 'package:coffeecard/features/opening_hours/presentation/cubit/opening_hours_cubit.dart';
import 'package:coffeecard/features/product/domain/entities/purchasable_products.dart';
import 'package:coffeecard/features/product/purchasable_products.dart';
import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart';
import 'package:coffeecard/features/receipt/presentation/pages/receipts_page.dart';
import 'package:coffeecard/features/settings/presentation/pages/settings_page.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart';
import 'package:hive_flutter/hive_flutter.dart';

class ClearAuthenticatedUser {
final AuthenticationLocalDataSource dataSource;

ClearAuthenticatedUser({required this.dataSource});

Future<void> call() async {
// Clear last used menu item
final box = await Hive.openBox<int>('lastUsedMenuItemByProductId');
await box.clear();

await dataSource.clearAuthenticatedUser();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class EnvironmentRemoteDataSource {
final CoffeecardApiV2 apiV2;
final NetworkRequestExecutor executor;

Future<Either<NetworkFailure, Environment>> getEnvironmentType() {
Future<Either<Failure, Environment>> getEnvironmentType() {
return executor
.execute(apiV2.apiV2AppconfigGet)
.map(Environment.fromAppConfig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class LeaderboardRemoteDataSource {
required this.executor,
});

Future<Either<NetworkFailure, List<LeaderboardUser>>> getLeaderboard(
Future<Either<Failure, List<LeaderboardUser>>> getLeaderboard(
LeaderboardFilter category,
int top,
) {
Expand All @@ -34,7 +34,7 @@ class LeaderboardRemoteDataSource {
.mapAll(LeaderboardUserModel.fromDTO);
}

Future<Either<NetworkFailure, LeaderboardUser>> getLeaderboardUser(
Future<Either<Failure, LeaderboardUser>> getLeaderboardUser(
LeaderboardFilter category,
) {
return executor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class AccountRemoteDataSource {
);
}

Future<Either<NetworkFailure, Unit>> resendVerificationEmail(
Future<Either<Failure, Unit>> resendVerificationEmail(
String email,
) {
return executor.executeAndDiscard(
Expand All @@ -64,17 +64,17 @@ class AccountRemoteDataSource {
);
}

Future<Either<NetworkFailure, User>> getUser() {
Future<Either<Failure, User>> getUser() {
return executor.execute(apiV2.apiV2AccountGet).map(UserModel.fromResponse);
}

Future<Either<NetworkFailure, Unit>> requestPasscodeReset(String email) {
Future<Either<Failure, Unit>> requestPasscodeReset(String email) {
return executor.executeAndDiscard(
() => apiV1.apiV1AccountForgotpasswordPost(body: EmailDto(email: email)),
);
}

Future<Either<NetworkFailure, bool>> emailExists(String email) {
Future<Either<Failure, bool>> emailExists(String email) {
final body = EmailExistsRequest(email: email);
return executor
.execute(() => apiV2.apiV2AccountEmailExistsPost(body: body))
Expand Down
2 changes: 1 addition & 1 deletion lib/features/login/domain/usecases/resend_email.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ResendEmail {

ResendEmail({required this.remoteDataSource});

Future<Either<NetworkFailure, void>> call(String email) async {
Future<Either<Failure, void>> call(String email) async {
return remoteDataSource.resendVerificationEmail(email);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class OccupationRemoteDataSource {
required this.executor,
});

Future<Either<NetworkFailure, List<OccupationModel>>> getOccupations() {
Future<Either<Failure, List<OccupationModel>>> getOccupations() {
return executor
.execute(api.apiV1ProgrammesGet)
.mapAll(OccupationModel.fromDTOV1);
Expand Down

This file was deleted.

24 changes: 0 additions & 24 deletions lib/features/product/data/models/product_model.dart

This file was deleted.

22 changes: 0 additions & 22 deletions lib/features/product/domain/entities/product.dart

This file was deleted.

This file was deleted.

21 changes: 0 additions & 21 deletions lib/features/product/domain/usecases/get_all_products.dart

This file was deleted.

15 changes: 15 additions & 0 deletions lib/features/product/menu_item_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart';
import 'package:equatable/equatable.dart';

class MenuItem extends Equatable {
const MenuItem({required this.id, required this.name});

factory MenuItem.fromResponse(MenuItemResponse response) =>
MenuItem(id: response.id, name: response.name);

final int id;
final String name;

@override
List<Object?> get props => [id, name];
}
20 changes: 11 additions & 9 deletions lib/features/product/presentation/cubit/product_cubit.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import 'package:coffeecard/core/errors/failures.dart';
import 'package:coffeecard/features/product/domain/entities/purchasable_products.dart';
import 'package:coffeecard/features/product/domain/usecases/get_all_products.dart';
import 'package:coffeecard/features/product/product_model.dart';
import 'package:coffeecard/features/product/product_repository.dart';
import 'package:coffeecard/features/product/purchasable_products.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

part 'product_state.dart';

class ProductCubit extends Cubit<ProductState> {
final GetAllProducts getAllProducts;
final ProductRepository productRepository;

ProductCubit({required this.getAllProducts}) : super(const ProductsLoading());
ProductCubit({required this.productRepository})
: super(const ProductsLoading());

Future<void> getProducts() async {
emit(const ProductsLoading());
final result = await getAllProducts();
emit(result.fold(ProductsError.fromFailure, ProductsLoaded.new));
}
Future<void> getProducts() => productRepository
.getProducts()
.match(ProductsError.new, ProductsLoaded.new)
.map(emit)
.run();
}
10 changes: 7 additions & 3 deletions lib/features/product/presentation/cubit/product_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ class ProductsLoading extends ProductState {
class ProductsLoaded extends ProductState {
final PurchasableProducts products;

const ProductsLoaded(this.products);
ProductsLoaded(Iterable<Product> products)
: products = (
clipCards: products.where((p) => p.amount > 1),
singleDrinks: products.where((p) => p.amount == 1),
perks: products.where((p) => p.isPerk),
);

@override
List<Object?> get props => [products];
Expand All @@ -23,8 +28,7 @@ class ProductsLoaded extends ProductState {
class ProductsError extends ProductState {
final String error;

const ProductsError(this.error);
ProductsError.fromFailure(Failure failure) : error = failure.reason;
ProductsError(Failure failure) : error = failure.reason;

@override
List<Object?> get props => [error];
Expand Down
Loading

0 comments on commit fec6939

Please sign in to comment.