From b412df53104f8be465332e33eaa2ba6213425b5b Mon Sep 17 00:00:00 2001 From: Mai Thinh Date: Wed, 2 Oct 2024 10:46:04 +0700 Subject: [PATCH] feat: new rsvp flow (#739) # What ? - RSVP flow (mainly focus on pay flow) https://trello.com/c/DVhYT5zQ/698-rsvp-flow-focus-on-payment-flow # TODO: - New UI for select tickets - New UI for tickets assignment after payment # Screenshot https://github.com/user-attachments/assets/2fa12a2c-89b0-4622-8544-bf489960c05d --- assets/icons/ic_wallet_dark_gradient.svg | 65 ++ .../event_application_form_setting_bloc.dart | 7 +- .../buy_tickets_bloc/buy_tickets_bloc.dart | 98 ++- .../buy_tickets_with_crypto_bloc.dart | 36 +- .../redeem_tickets_bloc.dart | 11 +- .../add_new_card_bloc/add_new_card_bloc.dart | 2 +- lib/core/data/event/dtos/event_dtos.dart | 2 +- .../event_ticket_repository_impl.dart | 26 +- .../data/payment/payment_repository_impl.dart | 1 + lib/core/data/user/dtos/user_query.dart | 2 + lib/core/domain/event/entities/event.dart | 2 + .../repository/event_ticket_repository.dart | 3 +- .../view/create_guild_loading_view.dart | 4 +- .../pages/edit_profile/edit_profile_page.dart | 38 +- .../sub_pages/edit_profile_personal_page.dart | 41 +- .../sub_pages/edit_profile_social_page.dart | 33 +- .../widgets/edit_profile_field_item.dart | 25 + .../sub_pages/create_event_base_page.dart | 2 +- .../view/event_invite_loading_view.dart | 4 +- .../event_issue_tickets_loading_view.dart | 4 +- .../event_detail_base_page.dart | 8 +- ...t_event_application_form_loading_view.dart | 4 +- .../guest_event_detail_buy_button.dart | 7 +- .../widgets/claim_loading_widget.dart | 4 +- .../event_buy_tickets_page.dart | 19 + .../event_buy_tickets_processing_page.dart | 474 ++++++++++++ .../handler/buy_tickets_listener.dart | 0 .../buy_tickets_with_crypto_listener.dart | 8 +- ...wait_for_payment_notification_handler.dart | 17 +- .../loaders/payment_processing_view.dart | 54 ++ .../loaders/transaction_confirming_view.dart | 151 ++++ .../wallet_signature_pending_view.dart | 94 +++ .../event_tickets_summary_page.dart | 681 ++++++------------ .../widgets/event_info_summary.dart | 113 +++ .../widgets/event_order_summary.dart | 215 ------ .../widgets/event_tickets_summary.dart | 155 ++-- .../widgets/event_total_price_summary.dart | 117 +++ .../widgets/pay_by_crypto_button.dart | 264 ------- .../pay_button.dart} | 50 +- .../payment_footer/pay_by_crypto_footer.dart | 127 ++++ .../pay_by_stripe_footer.dart} | 34 +- .../select_card_button.dart | 0 .../add_promo_code_input.dart | 0 .../promo_code_input_bottomsheet.dart | 106 +++ .../promo_code/promo_code_summary.dart | 108 +++ .../rsvp_application_form.dart | 20 + .../rsvp_application_questions_form.dart | 75 ++ .../rsvp_profle_fields_form.dart | 153 ++++ .../widgets/select_ticket_item.dart | 4 +- .../circular_countdown_timer.dart | 438 +++++++++++ .../countdown_text_format.dart | 7 + .../custom_timer_painter.dart | 93 +++ .../circular_loading_widget.dart | 8 +- .../button/linear_gradient_button_widget.dart | 3 +- .../dropdown/frosted_glass_drop_down_v2.dart | 63 +- .../widgets/lemon_text_field.dart | 41 +- lib/graphql/backend/schema.graphql | 200 ++++- .../tickets/mutation/redeem_tickets.graphql | 17 + lib/i18n/common/common_en.i18n.json | 3 +- lib/i18n/event/event_en.i18n.json | 18 +- lib/router/app_router.dart | 3 + 61 files changed, 3094 insertions(+), 1268 deletions(-) create mode 100644 assets/icons/ic_wallet_dark_gradient.svg create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/event_buy_tickets_processing_page.dart rename lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/{event_tickets_summary_page => event_buy_tickets_processing_page}/handler/buy_tickets_listener.dart (100%) rename lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/{event_tickets_summary_page => event_buy_tickets_processing_page}/handler/buy_tickets_with_crypto_listener.dart (88%) rename lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/{event_tickets_summary_page => event_buy_tickets_processing_page}/handler/wait_for_payment_notification_handler.dart (84%) create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/payment_processing_view.dart create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/transaction_confirming_view.dart create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/wallet_signature_pending_view.dart create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_info_summary.dart delete mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_summary.dart create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_total_price_summary.dart delete mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/pay_by_crypto_button.dart rename lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/{event_order_slide_to_pay.dart => payment_footer/pay_button.dart} (52%) create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_by_crypto_footer.dart rename lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/{event_order_summary_footer.dart => payment_footer/pay_by_stripe_footer.dart} (78%) rename lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/{ => payment_footer}/select_card_button.dart (100%) rename lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/{ => promo_code}/add_promo_code_input.dart (100%) create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/promo_code_input_bottomsheet.dart create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/promo_code_summary.dart create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_application_form.dart create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_application_questions_form.dart create mode 100644 lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_profle_fields_form.dart create mode 100644 lib/core/presentation/widgets/animation/circular_countdown_timer_widget/circular_countdown_timer.dart create mode 100644 lib/core/presentation/widgets/animation/circular_countdown_timer_widget/countdown_text_format.dart create mode 100644 lib/core/presentation/widgets/animation/circular_countdown_timer_widget/custom_timer_painter.dart rename lib/core/presentation/widgets/{common/circular_loading => animation}/circular_loading_widget.dart (90%) create mode 100644 lib/graphql/backend/tickets/mutation/redeem_tickets.graphql diff --git a/assets/icons/ic_wallet_dark_gradient.svg b/assets/icons/ic_wallet_dark_gradient.svg new file mode 100644 index 000000000..fe11af6f9 --- /dev/null +++ b/assets/icons/ic_wallet_dark_gradient.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/core/application/event/event_application_form_setting_bloc/event_application_form_setting_bloc.dart b/lib/core/application/event/event_application_form_setting_bloc/event_application_form_setting_bloc.dart index b742d0f4c..520423de4 100644 --- a/lib/core/application/event/event_application_form_setting_bloc/event_application_form_setting_bloc.dart +++ b/lib/core/application/event/event_application_form_setting_bloc/event_application_form_setting_bloc.dart @@ -63,7 +63,11 @@ class EventApplicationFormSettingBloc extends Bloc< ) { List newQuestions = [ ...state.questions, - Input$QuestionInput(question: "", required: false), + Input$QuestionInput( + question: "", + required: false, + type: Enum$QuestionType.text, + ), ]; emit( @@ -131,6 +135,7 @@ class EventApplicationFormSettingBloc extends Bloc< $_id: item.id, question: item.question ?? '', required: item.isRequired, + type: Enum$QuestionType.text, ), ) .toList(), diff --git a/lib/core/application/event_tickets/buy_tickets_bloc/buy_tickets_bloc.dart b/lib/core/application/event_tickets/buy_tickets_bloc/buy_tickets_bloc.dart index b34010cc4..7ae330680 100644 --- a/lib/core/application/event_tickets/buy_tickets_bloc/buy_tickets_bloc.dart +++ b/lib/core/application/event_tickets/buy_tickets_bloc/buy_tickets_bloc.dart @@ -34,15 +34,6 @@ class BuyTicketsBloc extends Bloc { emit(BuyTicketsState.loading()); final paymentMethod = event.input.transferParams?.paymentMethod ?? ''; - if (_currentPayment != null) { - add( - BuyTicketsEvent.processPaymentIntent( - paymentMethod: event.input.transferParams?.paymentMethod ?? '', - payment: _currentPayment!, - ), - ); - } - final result = await eventTicketRepository.buyTickets(input: event.input); result.fold( @@ -89,32 +80,72 @@ class BuyTicketsBloc extends Bloc { final payment = event.payment; _currentPayment = payment; try { - // TODO: will remove after confirmation with BE - // @deprecated - // final stripePublicKey = payment.transferMetadata?.tryGet('public_key') ?? ''; - // final stripeClientSecret = payment.transferMetadata?.tryGet('client_secret') ?? ''; - // final paymentMethodId = event.paymentMethod; - - // Stripe.publishableKey = stripePublicKey; - - // // if payment method not defined then have to use stripe payment sheet - // if (paymentMethodId == null || paymentMethodId.isEmpty) { - // await Stripe.instance.initPaymentSheet( - // paymentSheetParameters: SetupPaymentSheetParameters( - // paymentIntentClientSecret: stripeClientSecret, - // style: ThemeMode.dark, - // ), - // ); - - // await Stripe.instance.presentPaymentSheet(); - // } - - add( - BuyTicketsEvent.processUpdatePayment( - payment: payment, - paymentMethod: event.paymentMethod, + final stripePublicKey = payment.transferMetadata?['public_key'] ?? ''; + final stripeClientSecret = + payment.transferMetadata?['client_secret'] ?? ''; + final paymentMethodId = event.paymentMethod; + + Stripe.stripeAccountId = + _currentPayment?.accountExpanded?.accountInfo?.accountId ?? ''; + Stripe.publishableKey = stripePublicKey; + + // update payment with payment method to see if it's 3D secure or requiring action + final updatePaymentResult = await paymentRepository.updatePayment( + input: UpdatePaymentInput( + id: event.payment.id ?? '', + transferParams: UpdatePaymentTransferParams( + paymentMethod: event.paymentMethod, + returnUrl: '${AppConfig.appScheme}://payment', + ), ), ); + + if (updatePaymentResult.isLeft()) { + return emit( + BuyTicketsState.failure( + failureReason: InitPaymentFailure(), + ), + ); + } + final updatedPayment = updatePaymentResult.fold((l) => null, (r) => r); + + final nextActionUrl = + (updatedPayment?.transferMetadata?['next_action_url'] ?? '') + as String; + final actionRequired = nextActionUrl.isNotEmpty; + + // 3D secure card + if (actionRequired) { + final intent = await Stripe.instance.confirmPayment( + paymentIntentClientSecret: stripeClientSecret, + data: const PaymentMethodParams.card( + paymentMethodData: PaymentMethodData(), + ), + ); + + if (intent.status == PaymentIntentsStatus.Succeeded) { + add( + BuyTicketsEvent.processUpdatePayment( + payment: payment, + paymentMethod: paymentMethodId, + ), + ); + } else { + emit( + BuyTicketsState.failure( + failureReason: InitPaymentFailure(), + ), + ); + } + } else { + // If card is not 3D secure, then update payment + add( + BuyTicketsEvent.processUpdatePayment( + payment: payment, + paymentMethod: paymentMethodId, + ), + ); + } } catch (e) { if (e is StripeException) { return emit( @@ -123,6 +154,7 @@ class BuyTicketsBloc extends Bloc { ), ); } + emit( BuyTicketsState.failure( failureReason: InitPaymentFailure(), diff --git a/lib/core/application/event_tickets/buy_tickets_with_crypto_bloc/buy_tickets_with_crypto_bloc.dart b/lib/core/application/event_tickets/buy_tickets_with_crypto_bloc/buy_tickets_with_crypto_bloc.dart index eeef91cdb..a2aa65eec 100644 --- a/lib/core/application/event_tickets/buy_tickets_with_crypto_bloc/buy_tickets_with_crypto_bloc.dart +++ b/lib/core/application/event_tickets/buy_tickets_with_crypto_bloc/buy_tickets_with_crypto_bloc.dart @@ -233,7 +233,9 @@ class BuyTicketsWithCryptoBloc return emit( BuyTicketsWithCryptoState.failure( data: state.data, - failureReason: WalletConnectFailure(), + failureReason: WalletConnectFailure( + message: txHash, + ), ), ); } @@ -326,7 +328,9 @@ class BuyTicketsWithCryptoBloc emit( BuyTicketsWithCryptoState.failure( data: state.data, - failureReason: WalletConnectFailure(), + failureReason: WalletConnectFailure( + message: _txHash, + ), ), ); } @@ -475,17 +479,33 @@ class BuyTicketsWithCryptoStateData with _$BuyTicketsWithCryptoStateData { }) = _BuyTicketsWithCryptoStateData; } -class BuyWithCryptoFailure {} +class BuyWithCryptoFailure { + final String? message; + BuyWithCryptoFailure({ + this.message, + }); +} -class InitCryptoPaymentFailure extends BuyWithCryptoFailure {} +class InitCryptoPaymentFailure extends BuyWithCryptoFailure { + InitCryptoPaymentFailure({ + super.message, + }); +} class WalletConnectFailure extends BuyWithCryptoFailure { - final String? message; WalletConnectFailure({ - this.message, + super.message, }); } -class UpdateCryptoPaymentFailure extends BuyWithCryptoFailure {} +class UpdateCryptoPaymentFailure extends BuyWithCryptoFailure { + UpdateCryptoPaymentFailure({ + super.message, + }); +} -class NotificationCryptoPaymentFailure extends BuyWithCryptoFailure {} +class NotificationCryptoPaymentFailure extends BuyWithCryptoFailure { + NotificationCryptoPaymentFailure({ + super.message, + }); +} diff --git a/lib/core/application/event_tickets/redeem_tickets_bloc/redeem_tickets_bloc.dart b/lib/core/application/event_tickets/redeem_tickets_bloc/redeem_tickets_bloc.dart index 5a154d8f0..cf65f3df0 100644 --- a/lib/core/application/event_tickets/redeem_tickets_bloc/redeem_tickets_bloc.dart +++ b/lib/core/application/event_tickets/redeem_tickets_bloc/redeem_tickets_bloc.dart @@ -1,8 +1,8 @@ import 'package:app/core/data/event/repository/event_ticket_repository_impl.dart'; import 'package:app/core/domain/event/entities/event.dart'; import 'package:app/core/domain/event/entities/redeem_tickets_response.dart'; -import 'package:app/core/domain/event/input/redeem_tickets_input/redeem_tickets_input.dart'; import 'package:app/core/domain/payment/entities/purchasable_item/purchasable_item.dart'; +import 'package:app/graphql/backend/schema.graphql.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -25,10 +25,15 @@ class RedeemTicketsBloc extends Bloc { emit(RedeemTicketsState.loading()); final result = await _eventTicketRepository.redeemTickets( - input: RedeemTicketsInput( + input: Input$RedeemTicketsInput( event: event.id ?? '', items: blocEvent.ticketItems - .map((item) => RedeemItem(count: item.count, ticketType: item.id)) + .map( + (item) => Input$PurchasableItem( + count: item.count, + id: item.id, + ), + ) .toList(), ), ); diff --git a/lib/core/application/payment/add_new_card_bloc/add_new_card_bloc.dart b/lib/core/application/payment/add_new_card_bloc/add_new_card_bloc.dart index 4d091fc4b..1e6154bda 100644 --- a/lib/core/application/payment/add_new_card_bloc/add_new_card_bloc.dart +++ b/lib/core/application/payment/add_new_card_bloc/add_new_card_bloc.dart @@ -58,7 +58,7 @@ class AddNewCardBloc extends Cubit { }) async { try { emit(state.copyWith(status: AddNewCardBlocStatus.loading)); - // For debugging purpose + Stripe.stripeAccountId = null; Stripe.publishableKey = publishableKey; final expirationMonth = int.parse(state.validThrough!.substring(0, 2)); diff --git a/lib/core/data/event/dtos/event_dtos.dart b/lib/core/data/event/dtos/event_dtos.dart index 7587e7c80..4f4ab315b 100644 --- a/lib/core/data/event/dtos/event_dtos.dart +++ b/lib/core/data/event/dtos/event_dtos.dart @@ -51,7 +51,7 @@ class EventDto with _$EventDto { AddressDto? address, @JsonKey(name: 'payment_accounts_new') List? paymentAccountsNew, @JsonKey(name: 'payment_accounts_expanded') - List? paymentAccountsExpanded, + List? paymentAccountsExpanded, @JsonKey(name: 'guest_limit') double? guestLimit, @JsonKey(name: 'guest_limit_per') double? guestLimitPer, bool? virtual, diff --git a/lib/core/data/event/repository/event_ticket_repository_impl.dart b/lib/core/data/event/repository/event_ticket_repository_impl.dart index 01740d70f..a8ddcd6a4 100644 --- a/lib/core/data/event/repository/event_ticket_repository_impl.dart +++ b/lib/core/data/event/repository/event_ticket_repository_impl.dart @@ -20,7 +20,6 @@ import 'package:app/core/domain/event/input/calculate_tickets_pricing_input/calc import 'package:app/core/domain/event/input/get_event_currencies_input/get_event_currencies_input.dart'; import 'package:app/core/domain/event/input/get_event_ticket_types_input/get_event_ticket_types_input.dart'; import 'package:app/core/domain/event/input/get_tickets_input/get_tickets_input.dart'; -import 'package:app/core/domain/event/input/redeem_tickets_input/redeem_tickets_input.dart'; import 'package:app/core/domain/event/repository/event_ticket_repository.dart'; import 'package:app/core/failure.dart'; import 'package:app/core/utils/gql/gql.dart'; @@ -33,6 +32,7 @@ import 'package:app/graphql/backend/event/mutation/delete_event_ticket_type.grap import 'package:app/graphql/backend/event/mutation/email_event_ticket.graphql.dart'; import 'package:app/graphql/backend/event/mutation/update_event_ticket_type.graphql.dart'; import 'package:app/graphql/backend/schema.graphql.dart'; +import 'package:app/graphql/backend/tickets/mutation/redeem_tickets.graphql.dart'; import 'package:app/injection/register_module.dart'; import 'package:dartz/dartz.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; @@ -88,24 +88,26 @@ class EventTicketRepositoryImpl implements EventTicketRepository { @override Future> redeemTickets({ - required RedeemTicketsInput input, + required Input$RedeemTicketsInput input, }) async { - final result = await _client.mutate( - MutationOptions( - document: redeemTicketsMutation, - variables: { - 'input': input.toJson(), - }, - parserFn: (data) => RedeemTicketsResponse.fromDto( - RedeemTicketsResponseDto.fromJson(data['redeemTickets'] ?? []), + final result = await _client.mutate$RedeemTickets( + Options$Mutation$RedeemTickets( + variables: Variables$Mutation$RedeemTickets( + input: input, ), ), ); - if (result.hasException) { + if (result.hasException || result.parsedData?.redeemTickets == null) { return Left(Failure.withGqlException(result.exception)); } - return Right(result.parsedData!); + return Right( + RedeemTicketsResponse.fromDto( + RedeemTicketsResponseDto.fromJson( + result.parsedData!.redeemTickets.toJson(), + ), + ), + ); } @override diff --git a/lib/core/data/payment/payment_repository_impl.dart b/lib/core/data/payment/payment_repository_impl.dart index ff151e5f4..3a5452e81 100644 --- a/lib/core/data/payment/payment_repository_impl.dart +++ b/lib/core/data/payment/payment_repository_impl.dart @@ -54,6 +54,7 @@ class PaymentRepositoryImpl extends PaymentRepository { QueryOptions( document: getStripeCardsQuery, variables: input.toJson(), + fetchPolicy: FetchPolicy.networkOnly, parserFn: (data) => List.from( data['getStripeCards'] ?? [], ) diff --git a/lib/core/data/user/dtos/user_query.dart b/lib/core/data/user/dtos/user_query.dart index 57d33bf1d..4c2a83edc 100644 --- a/lib/core/data/user/dtos/user_query.dart +++ b/lib/core/data/user/dtos/user_query.dart @@ -222,6 +222,7 @@ final getMeQuery = gql(''' $privateFragment $userTermFragment $userFarcasterInfoFragment + $userStripeFragment query() { getMe() { @@ -230,6 +231,7 @@ final getMeQuery = gql(''' ...privateFragment ...userTermFragment ...userFarcasterInfoFragment + ...userStripeFragment } } '''); diff --git a/lib/core/domain/event/entities/event.dart b/lib/core/domain/event/entities/event.dart index a349d9003..47c969539 100644 --- a/lib/core/domain/event/entities/event.dart +++ b/lib/core/domain/event/entities/event.dart @@ -1,4 +1,5 @@ import 'package:app/core/data/event/dtos/event_dtos.dart'; +import 'package:app/core/data/payment/dtos/payment_account_dto/payment_account_dto.dart'; import 'package:app/core/domain/common/entities/common.dart'; import 'package:app/core/domain/event/entities/event_application_profile_field.dart'; import 'package:app/core/domain/event/entities/event_payment_ticket_discount.dart'; @@ -130,6 +131,7 @@ class Event with _$Event { address: dto.address != null ? Address.fromDto(dto.address!) : null, paymentAccountsNew: dto.paymentAccountsNew ?? [], paymentAccountsExpanded: List.from(dto.paymentAccountsExpanded ?? []) + .whereType() .map((item) => PaymentAccount.fromDto(item)) .toList(), guestLimit: dto.guestLimit, diff --git a/lib/core/domain/event/repository/event_ticket_repository.dart b/lib/core/domain/event/repository/event_ticket_repository.dart index cdb84acd9..86e07eef9 100644 --- a/lib/core/domain/event/repository/event_ticket_repository.dart +++ b/lib/core/domain/event/repository/event_ticket_repository.dart @@ -11,7 +11,6 @@ import 'package:app/core/domain/event/input/calculate_tickets_pricing_input/calc import 'package:app/core/domain/event/input/get_event_currencies_input/get_event_currencies_input.dart'; import 'package:app/core/domain/event/input/get_event_ticket_types_input/get_event_ticket_types_input.dart'; import 'package:app/core/domain/event/input/get_tickets_input/get_tickets_input.dart'; -import 'package:app/core/domain/event/input/redeem_tickets_input/redeem_tickets_input.dart'; import 'package:app/core/failure.dart'; import 'package:app/graphql/backend/event/mutation/create_event_ticket_discount.graphql.dart'; import 'package:app/graphql/backend/event/mutation/email_event_ticket.graphql.dart'; @@ -30,7 +29,7 @@ abstract class EventTicketRepository { }); Future> redeemTickets({ - required RedeemTicketsInput input, + required Input$RedeemTicketsInput input, }); Future> assignTickets({ diff --git a/lib/core/presentation/pages/chat/create_guild_channel/sub_pages/create_guild_processing_page/view/create_guild_loading_view.dart b/lib/core/presentation/pages/chat/create_guild_channel/sub_pages/create_guild_processing_page/view/create_guild_loading_view.dart index 974ce0f88..4e2012b76 100644 --- a/lib/core/presentation/pages/chat/create_guild_channel/sub_pages/create_guild_processing_page/view/create_guild_loading_view.dart +++ b/lib/core/presentation/pages/chat/create_guild_channel/sub_pages/create_guild_processing_page/view/create_guild_loading_view.dart @@ -1,4 +1,4 @@ -import 'package:app/core/presentation/widgets/common/circular_loading/circular_loading_widget.dart'; +import 'package:app/core/presentation/widgets/animation/circular_loading_widget.dart'; import 'package:app/gen/fonts.gen.dart'; import 'package:app/i18n/i18n.g.dart'; import 'package:app/theme/spacing.dart'; @@ -19,7 +19,7 @@ class CreateGuildLoadingView extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ const Spacer(), - const CircularLoading(), + const CircularLoadingWidget(), const Spacer(), Column( children: [ diff --git a/lib/core/presentation/pages/edit_profile/edit_profile_page.dart b/lib/core/presentation/pages/edit_profile/edit_profile_page.dart index 601d9f8d5..78300b8f2 100644 --- a/lib/core/presentation/pages/edit_profile/edit_profile_page.dart +++ b/lib/core/presentation/pages/edit_profile/edit_profile_page.dart @@ -162,18 +162,25 @@ class EditProfileView extends StatelessWidget { ); }, child: Container( - padding: EdgeInsets.all(Spacing.xSmall), decoration: BoxDecoration( - color: colorScheme.onPrimary - .withOpacity(0.06), + color: LemonColor.atomicBlack, borderRadius: BorderRadius.circular( LemonRadius.normal, ), ), child: ListTile( - title: Text(t.profile.socialHandle), - subtitle: - Text(t.profile.socialHandleDesc), + title: Text( + t.profile.socialHandle, + style: Typo.medium.copyWith( + color: colorScheme.onPrimary, + ), + ), + subtitle: Text( + t.profile.socialHandleDesc, + style: Typo.small.copyWith( + color: colorScheme.onSecondary, + ), + ), trailing: Assets.icons.icArrowBack.svg( width: 18.w, height: 18.w, @@ -201,18 +208,25 @@ class EditProfileView extends StatelessWidget { ); }, child: Container( - padding: EdgeInsets.all(Spacing.xSmall), decoration: BoxDecoration( - color: colorScheme.onPrimary - .withOpacity(0.06), + color: LemonColor.atomicBlack, borderRadius: BorderRadius.circular( LemonRadius.normal, ), ), child: ListTile( - title: Text(t.profile.personalInfo), - subtitle: - Text(t.profile.personalInfoDesc), + title: Text( + t.profile.personalInfo, + style: Typo.medium.copyWith( + color: colorScheme.onPrimary, + ), + ), + subtitle: Text( + t.profile.personalInfoDesc, + style: Typo.small.copyWith( + color: colorScheme.onSecondary, + ), + ), trailing: Assets.icons.icArrowBack.svg( width: 18.w, height: 18.w, diff --git a/lib/core/presentation/pages/edit_profile/sub_pages/edit_profile_personal_page.dart b/lib/core/presentation/pages/edit_profile/sub_pages/edit_profile_personal_page.dart index d40f32926..cc9368e4e 100644 --- a/lib/core/presentation/pages/edit_profile/sub_pages/edit_profile_personal_page.dart +++ b/lib/core/presentation/pages/edit_profile/sub_pages/edit_profile_personal_page.dart @@ -4,13 +4,11 @@ import 'package:app/core/domain/common/common_enums.dart'; import 'package:app/core/domain/user/entities/user.dart'; import 'package:app/core/presentation/pages/edit_profile/widgets/edit_profile_field_item.dart'; import 'package:app/core/presentation/widgets/common/button/linear_gradient_button_widget.dart'; -import 'package:app/core/presentation/widgets/common/dropdown/frosted_glass_drop_down_v2.dart'; import 'package:app/core/presentation/widgets/common/appbar/lemon_appbar_widget.dart'; import 'package:app/core/utils/snackbar_utils.dart'; import 'package:app/gen/fonts.gen.dart'; import 'package:app/i18n/i18n.g.dart'; import 'package:app/theme/color.dart'; -import 'package:app/theme/sizing.dart'; import 'package:app/theme/spacing.dart'; import 'package:app/theme/typo.dart'; import 'package:auto_route/auto_route.dart'; @@ -55,13 +53,14 @@ class EditProfilePersonalDialogState extends State { }, child: BlocBuilder( builder: (context, state) { + final fieldItemBackgroundColor = LemonColor.chineseBlack; return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Scaffold( appBar: LemonAppBar( - backgroundColor: colorScheme.onPrimaryContainer, + backgroundColor: LemonColor.atomicBlack, ), - backgroundColor: colorScheme.onPrimaryContainer, + backgroundColor: LemonColor.atomicBlack, body: Padding( padding: EdgeInsets.symmetric(horizontal: Spacing.smMedium), child: Column( @@ -75,14 +74,16 @@ class EditProfilePersonalDialogState extends State { Text( t.profile.personalInfo, style: Typo.extraLarge.copyWith( - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w600, + fontFamily: FontFamily.nohemiVariable, + color: colorScheme.onPrimary, ), ), SizedBox(height: Spacing.superExtraSmall), Text( t.profile.personalInfoLongDesc, style: Typo.mediumPlus.copyWith( - color: colorScheme.onPrimary.withOpacity(0.56), + color: colorScheme.onSecondary, ), ), SizedBox(height: Spacing.smMedium), @@ -96,6 +97,7 @@ class EditProfilePersonalDialogState extends State { ); }, value: widget.userProfile?.jobTitle, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.smMedium), EditProfileFieldItem( @@ -108,23 +110,21 @@ class EditProfilePersonalDialogState extends State { ); }, value: widget.userProfile?.companyName, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.smMedium), - FrostedGlassDropDownV2( - label: t.profile.industry, - hintText: t.profile.hint.industry, - listItem: LemonIndustry.values - .map((e) => e.industry) - .toList(), - onValueChange: (value) { + EditProfileFieldItem( + profileFieldKey: ProfileFieldKey.industry, + onChange: (value) { context.read().add( EditProfileEvent.industrySelect( industry: value, ), ); }, - selectedValue: state.industry ?? + value: state.industry ?? widget.userProfile?.industry, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.smMedium), EditProfileFieldItem( @@ -137,6 +137,7 @@ class EditProfilePersonalDialogState extends State { ); }, value: widget.userProfile?.educationTitle, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.smMedium), EditProfileFieldItem( @@ -150,6 +151,7 @@ class EditProfilePersonalDialogState extends State { }, value: state.gender ?? widget.userProfile?.newGender, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.smMedium), Focus( @@ -172,6 +174,7 @@ class EditProfilePersonalDialogState extends State { }, value: widget.userProfile?.dateOfBirth.toString(), + backgroundColor: fieldItemBackgroundColor, ), onFocusChange: (hasFocus) { if (!hasFocus) { @@ -205,6 +208,7 @@ class EditProfilePersonalDialogState extends State { }, value: state.ethnicity ?? widget.userProfile?.ethnicity, + backgroundColor: fieldItemBackgroundColor, ), ], ), @@ -212,20 +216,13 @@ class EditProfilePersonalDialogState extends State { ), Container( margin: EdgeInsets.symmetric(vertical: Spacing.smMedium), - child: LinearGradientButton( + child: LinearGradientButton.primaryButton( onTap: () { context.read().add( EditProfileEvent.submitEditProfile(), ); }, label: t.profile.saveChanges, - textStyle: Typo.medium.copyWith( - fontFamily: FontFamily.nohemiVariable, - fontWeight: FontWeight.w600, - ), - height: Sizing.large, - radius: BorderRadius.circular(LemonRadius.large), - mode: GradientButtonMode.lavenderMode, loadingWhen: state.status == EditProfileStatus.loading, ), ), diff --git a/lib/core/presentation/pages/edit_profile/sub_pages/edit_profile_social_page.dart b/lib/core/presentation/pages/edit_profile/sub_pages/edit_profile_social_page.dart index bee5af07a..b921c6b43 100644 --- a/lib/core/presentation/pages/edit_profile/sub_pages/edit_profile_social_page.dart +++ b/lib/core/presentation/pages/edit_profile/sub_pages/edit_profile_social_page.dart @@ -9,13 +9,12 @@ import 'package:app/core/presentation/widgets/common/appbar/lemon_appbar_widget. import 'package:app/core/utils/snackbar_utils.dart'; import 'package:app/gen/fonts.gen.dart'; import 'package:app/i18n/i18n.g.dart'; -import 'package:app/theme/sizing.dart'; +import 'package:app/theme/color.dart'; import 'package:app/theme/spacing.dart'; import 'package:app/theme/typo.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { final User? userProfile; @@ -25,6 +24,7 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final t = Translations.of(context); + final fieldItemBackgroundColor = LemonColor.chineseBlack; return BlocListener( listener: (context, state) { if (state.status == EditProfileStatus.success) { @@ -36,9 +36,9 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { }, child: Scaffold( appBar: LemonAppBar( - backgroundColor: colorScheme.onPrimaryContainer, + backgroundColor: LemonColor.atomicBlack, ), - backgroundColor: colorScheme.onPrimaryContainer, + backgroundColor: LemonColor.atomicBlack, body: Padding( padding: EdgeInsets.symmetric(horizontal: Spacing.smMedium), child: Column( @@ -50,13 +50,17 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { children: [ Text( t.profile.socialHandle, - style: Typo.extraLarge, + style: Typo.extraLarge.copyWith( + color: colorScheme.onPrimary, + fontFamily: FontFamily.nohemiVariable, + fontWeight: FontWeight.w600, + ), ), SizedBox(height: Spacing.superExtraSmall), Text( t.profile.socialHandleLongDesc, style: Typo.mediumPlus.copyWith( - color: colorScheme.onPrimary.withOpacity(0.56), + color: colorScheme.onSecondary, ), ), SizedBox(height: Spacing.medium), @@ -70,6 +74,7 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { ); }, value: userProfile?.handleTwitter, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.medium), EditProfileFieldItem( @@ -82,6 +87,7 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { ); }, value: userProfile?.handleLinkedin, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.medium), EditProfileFieldItem( @@ -94,6 +100,7 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { ); }, value: userProfile?.handleInstagram, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.medium), EditProfileFieldItem( @@ -106,6 +113,7 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { ); }, value: userProfile?.handleFarcaster, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.medium), EditProfileFieldItem( @@ -118,6 +126,7 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { ); }, value: userProfile?.handleGithub, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.medium), EditProfileFieldItem( @@ -130,6 +139,7 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { ); }, value: userProfile?.handleLens, + backgroundColor: fieldItemBackgroundColor, ), SizedBox(height: Spacing.medium), EditProfileFieldItem( @@ -142,6 +152,7 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { ); }, value: userProfile?.handleMirror, + backgroundColor: fieldItemBackgroundColor, ), ], ), @@ -151,21 +162,13 @@ class EditProfileSocialDialog extends StatelessWidget with LemonBottomSheet { builder: (context, state) { return Container( margin: EdgeInsets.symmetric(vertical: Spacing.smMedium), - width: 1.sw, - child: LinearGradientButton( + child: LinearGradientButton.primaryButton( onTap: () { context.read().add( EditProfileEvent.submitEditProfile(), ); }, label: t.profile.saveChanges, - textStyle: Typo.medium.copyWith( - fontFamily: FontFamily.nohemiVariable, - fontWeight: FontWeight.w600, - ), - height: Sizing.large, - radius: BorderRadius.circular(LemonRadius.large), - mode: GradientButtonMode.lavenderMode, loadingWhen: state.status == EditProfileStatus.loading, ), ); diff --git a/lib/core/presentation/pages/edit_profile/widgets/edit_profile_field_item.dart b/lib/core/presentation/pages/edit_profile/widgets/edit_profile_field_item.dart index 7cbe3d794..c6844ead3 100644 --- a/lib/core/presentation/pages/edit_profile/widgets/edit_profile_field_item.dart +++ b/lib/core/presentation/pages/edit_profile/widgets/edit_profile_field_item.dart @@ -20,6 +20,8 @@ class EditProfileFieldItem extends StatelessWidget { this.controller, this.showRequired, required this.value, + this.backgroundColor, + this.labelStyle, }); // final User userProfile; @@ -29,11 +31,16 @@ class EditProfileFieldItem extends StatelessWidget { final TextEditingController? controller; final bool? showRequired; final String? value; + final Color? backgroundColor; + final TextStyle? labelStyle; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final t = Translations.of(context); + final border = Border.all( + color: colorScheme.outline, + ); switch (profileFieldKey) { // // Dropdown @@ -45,6 +52,7 @@ class EditProfileFieldItem extends StatelessWidget { : ""; return FrostedGlassDropDownV2( label: t.profile.pronoun, + labelStyle: labelStyle, hintText: t.profile.pronoun, listItem: LemonPronoun.values.map((e) => e.pronoun).toList(), onValueChange: (value) { @@ -54,10 +62,13 @@ class EditProfileFieldItem extends StatelessWidget { }, selectedValue: value == '' ? null : newValue, showRequired: showRequired, + backgroundColor: backgroundColor, + border: border, ); case ProfileFieldKey.industry: return FrostedGlassDropDownV2( label: t.profile.industry, + labelStyle: labelStyle, hintText: t.profile.hint.industry, listItem: LemonIndustry.values.map((e) => e.industry).toList(), onValueChange: (value) { @@ -67,10 +78,13 @@ class EditProfileFieldItem extends StatelessWidget { }, selectedValue: value == '' ? null : value, showRequired: showRequired, + backgroundColor: backgroundColor, + border: border, ); case ProfileFieldKey.newGender: return FrostedGlassDropDownV2( label: t.profile.gender, + labelStyle: labelStyle, hintText: t.profile.hint.gender, listItem: LemonGender.values.map((e) => e.newGender).toList(), onValueChange: (value) { @@ -80,10 +94,13 @@ class EditProfileFieldItem extends StatelessWidget { }, selectedValue: value == '' ? null : value, showRequired: showRequired, + backgroundColor: backgroundColor, + border: border, ); case ProfileFieldKey.ethnicity: return FrostedGlassDropDownV2( label: t.profile.ethnicity, + labelStyle: labelStyle, hintText: t.profile.hint.ethnicity, listItem: LemonEthnicity.values.map((e) => e.ethnicity).toList(), onValueChange: (value) { @@ -93,6 +110,8 @@ class EditProfileFieldItem extends StatelessWidget { }, selectedValue: value == '' ? null : value, showRequired: showRequired, + backgroundColor: backgroundColor, + border: border, ); // // DateTimePicker mode @@ -100,10 +119,13 @@ class EditProfileFieldItem extends StatelessWidget { case ProfileFieldKey.dateOfBirth: return LemonTextField( label: t.profile.dob, + labelStyle: labelStyle, onChange: onChange, controller: controller, hintText: t.profile.hint.dob, showRequired: showRequired, + fillColor: backgroundColor, + filled: backgroundColor != null, inputFormatters: [ CustomDateTextFormatter(), LengthLimitingTextInputFormatter(10), @@ -145,10 +167,13 @@ class EditProfileFieldItem extends StatelessWidget { default: return LemonTextField( label: profileFieldKey.label, + labelStyle: labelStyle, hintText: '', initialText: value, onChange: onChange, showRequired: showRequired, + fillColor: backgroundColor, + filled: backgroundColor != null, ); } } diff --git a/lib/core/presentation/pages/event/create_event/sub_pages/create_event_base_page.dart b/lib/core/presentation/pages/event/create_event/sub_pages/create_event_base_page.dart index d7eebc1ca..5e657fff6 100644 --- a/lib/core/presentation/pages/event/create_event/sub_pages/create_event_base_page.dart +++ b/lib/core/presentation/pages/event/create_event/sub_pages/create_event_base_page.dart @@ -122,7 +122,7 @@ class CreateEventBasePage extends StatelessWidget { errorText: state.title.displayError?.getMessage( t.event.eventCreation.title, ), - labelStyle: Typo.mediumPlus.copyWith( + style: Typo.mediumPlus.copyWith( color: colorScheme.onSecondary, fontWeight: FontWeight.w500, ), diff --git a/lib/core/presentation/pages/event/event_control_panel_page/sub_pages/event_invite_setting_page/sub_pages/event_invite_processing_page/view/event_invite_loading_view.dart b/lib/core/presentation/pages/event/event_control_panel_page/sub_pages/event_invite_setting_page/sub_pages/event_invite_processing_page/view/event_invite_loading_view.dart index 440b74dc0..d9bdcb8fd 100644 --- a/lib/core/presentation/pages/event/event_control_panel_page/sub_pages/event_invite_setting_page/sub_pages/event_invite_processing_page/view/event_invite_loading_view.dart +++ b/lib/core/presentation/pages/event/event_control_panel_page/sub_pages/event_invite_setting_page/sub_pages/event_invite_processing_page/view/event_invite_loading_view.dart @@ -1,4 +1,4 @@ -import 'package:app/core/presentation/widgets/common/circular_loading/circular_loading_widget.dart'; +import 'package:app/core/presentation/widgets/animation/circular_loading_widget.dart'; import 'package:app/gen/fonts.gen.dart'; import 'package:app/i18n/i18n.g.dart'; import 'package:app/theme/spacing.dart'; @@ -19,7 +19,7 @@ class EventInviteLoadingView extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ const Spacer(), - const CircularLoading(), + const CircularLoadingWidget(), const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/core/presentation/pages/event/event_control_panel_page/sub_pages/event_issue_tickets_setting_page/sub_pages/event_issue_tickets_processing_page/view/event_issue_tickets_loading_view.dart b/lib/core/presentation/pages/event/event_control_panel_page/sub_pages/event_issue_tickets_setting_page/sub_pages/event_issue_tickets_processing_page/view/event_issue_tickets_loading_view.dart index cd18a6eaa..9be96dc4e 100644 --- a/lib/core/presentation/pages/event/event_control_panel_page/sub_pages/event_issue_tickets_setting_page/sub_pages/event_issue_tickets_processing_page/view/event_issue_tickets_loading_view.dart +++ b/lib/core/presentation/pages/event/event_control_panel_page/sub_pages/event_issue_tickets_setting_page/sub_pages/event_issue_tickets_processing_page/view/event_issue_tickets_loading_view.dart @@ -1,4 +1,4 @@ -import 'package:app/core/presentation/widgets/common/circular_loading/circular_loading_widget.dart'; +import 'package:app/core/presentation/widgets/animation/circular_loading_widget.dart'; import 'package:app/gen/fonts.gen.dart'; import 'package:app/i18n/i18n.g.dart'; import 'package:app/theme/spacing.dart'; @@ -19,7 +19,7 @@ class EventIssueTicketsLoadingView extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ const Spacer(), - const CircularLoading(), + const CircularLoadingWidget(), const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/core/presentation/pages/event/event_detail_page/event_detail_base_page.dart b/lib/core/presentation/pages/event/event_detail_page/event_detail_base_page.dart index b3e7e2a05..f821dd6a6 100644 --- a/lib/core/presentation/pages/event/event_detail_page/event_detail_base_page.dart +++ b/lib/core/presentation/pages/event/event_detail_page/event_detail_base_page.dart @@ -66,11 +66,11 @@ class _EventDetailBasePageView extends StatelessWidget { ), ), fetched: (event) { - final userId = context.read().state.maybeWhen( - orElse: () => '', - authenticated: (session) => session.userId, + final user = context.watch().state.maybeWhen( + orElse: () => null, + authenticated: (user) => user, ); - + final userId = user?.userId ?? ''; final isCohost = EventUtils.isCohost( event: event, userId: userId, diff --git a/lib/core/presentation/pages/event/event_detail_page/guest_event_detail_page/sub_pages/guest_event_application_page/sub_pages/guest_event_application_form_processing_page/view/guest_event_application_form_loading_view.dart b/lib/core/presentation/pages/event/event_detail_page/guest_event_detail_page/sub_pages/guest_event_application_page/sub_pages/guest_event_application_form_processing_page/view/guest_event_application_form_loading_view.dart index 45d96df6b..949c71a80 100644 --- a/lib/core/presentation/pages/event/event_detail_page/guest_event_detail_page/sub_pages/guest_event_application_page/sub_pages/guest_event_application_form_processing_page/view/guest_event_application_form_loading_view.dart +++ b/lib/core/presentation/pages/event/event_detail_page/guest_event_detail_page/sub_pages/guest_event_application_page/sub_pages/guest_event_application_form_processing_page/view/guest_event_application_form_loading_view.dart @@ -1,4 +1,4 @@ -import 'package:app/core/presentation/widgets/common/circular_loading/circular_loading_widget.dart'; +import 'package:app/core/presentation/widgets/animation/circular_loading_widget.dart'; import 'package:app/gen/fonts.gen.dart'; import 'package:app/i18n/i18n.g.dart'; import 'package:app/theme/spacing.dart'; @@ -19,7 +19,7 @@ class GuestEventApplicationFormLoadingView extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ const Spacer(), - const CircularLoading(), + const CircularLoadingWidget(), const Spacer(), Column( children: [ diff --git a/lib/core/presentation/pages/event/event_detail_page/guest_event_detail_page/widgets/guest_event_detail_buy_button.dart b/lib/core/presentation/pages/event/event_detail_page/guest_event_detail_page/widgets/guest_event_detail_buy_button.dart index 1d9b91590..b5fded3d6 100644 --- a/lib/core/presentation/pages/event/event_detail_page/guest_event_detail_page/widgets/guest_event_detail_buy_button.dart +++ b/lib/core/presentation/pages/event/event_detail_page/guest_event_detail_page/widgets/guest_event_detail_buy_button.dart @@ -101,9 +101,10 @@ class _GuestEventDetailBuyButtonView extends StatelessWidget { if (refetch != null) refetch!(); }, applicationFormNotCompleted: (user) async { - await AutoRouter.of(context) - .navigate(GuestEventApplicationRoute(event: event, user: user)); - if (refetch != null) refetch!(); + // Application form is moved to events buy tickets page + await AutoRouter.of(context).navigate( + EventBuyTicketsRoute(event: event), + ); }, allPassed: () async { await AutoRouter.of(context).navigate( diff --git a/lib/core/presentation/pages/event/event_detail_page/host_event_detail_page/sub_pages/claimd_split_relay_payment_page/widgets/claim_loading_widget.dart b/lib/core/presentation/pages/event/event_detail_page/host_event_detail_page/sub_pages/claimd_split_relay_payment_page/widgets/claim_loading_widget.dart index 61daa9af5..795fd922c 100644 --- a/lib/core/presentation/pages/event/event_detail_page/host_event_detail_page/sub_pages/claimd_split_relay_payment_page/widgets/claim_loading_widget.dart +++ b/lib/core/presentation/pages/event/event_detail_page/host_event_detail_page/sub_pages/claimd_split_relay_payment_page/widgets/claim_loading_widget.dart @@ -1,7 +1,7 @@ import 'package:app/core/presentation/widgets/animation/success_circle_animation_widget.dart'; import 'package:app/core/presentation/widgets/common/button/lemon_outline_button_widget.dart'; import 'package:app/core/presentation/widgets/common/button/linear_gradient_button_widget.dart'; -import 'package:app/core/presentation/widgets/common/circular_loading/circular_loading_widget.dart'; +import 'package:app/core/presentation/widgets/animation/circular_loading_widget.dart'; import 'package:app/gen/fonts.gen.dart'; import 'package:app/i18n/i18n.g.dart'; import 'package:app/theme/spacing.dart'; @@ -50,7 +50,7 @@ class _WaitingClaimView extends StatelessWidget { return Column( children: [ const Spacer(), - const CircularLoading(), + const CircularLoadingWidget(), const Spacer(), Padding( padding: EdgeInsets.symmetric(horizontal: Spacing.smMedium), diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/event_buy_tickets_page.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/event_buy_tickets_page.dart index 338d3f3e1..97d4a8318 100644 --- a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/event_buy_tickets_page.dart +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/event_buy_tickets_page.dart @@ -1,5 +1,8 @@ +import 'package:app/core/application/auth/auth_bloc.dart'; +import 'package:app/core/application/event/event_application_form_bloc/event_application_form_bloc.dart'; import 'package:app/core/application/event/event_buy_additional_tickets_bloc/event_buy_additonal_tickets_bloc.dart'; import 'package:app/core/application/event/event_provider_bloc/event_provider_bloc.dart'; +import 'package:app/core/application/event_tickets/calculate_event_tickets_pricing_bloc/calculate_event_tickets_pricing_bloc.dart'; import 'package:app/core/application/event_tickets/get_event_ticket_types_bloc/get_event_ticket_types_bloc.dart'; import 'package:app/core/application/event_tickets/redeem_tickets_bloc/redeem_tickets_bloc.dart'; import 'package:app/core/application/event_tickets/select_event_tickets_bloc/select_event_tickets_bloc.dart'; @@ -24,6 +27,10 @@ class EventBuyTicketsPage extends StatelessWidget implements AutoRouteWrapper { @override Widget wrappedRoute(BuildContext context) { + final user = context.watch().state.maybeWhen( + authenticated: (user) => user, + orElse: () => null, + ); return MultiBlocProvider( providers: [ BlocProvider( @@ -52,6 +59,18 @@ class EventBuyTicketsPage extends StatelessWidget implements AutoRouteWrapper { isBuyMore: isBuyMore, ), ), + BlocProvider( + create: (context) => CalculateEventTicketPricingBloc(), + ), + BlocProvider( + create: (context) => EventApplicationFormBloc() + ..add( + EventApplicationFormBlocEvent.initFieldState( + event: event, + user: user, + ), + ), + ), ], child: this, ); diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/event_buy_tickets_processing_page.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/event_buy_tickets_processing_page.dart new file mode 100644 index 000000000..f8d3b014f --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/event_buy_tickets_processing_page.dart @@ -0,0 +1,474 @@ +import 'package:app/core/application/event/event_application_form_bloc/event_application_form_bloc.dart'; +import 'package:app/core/application/event/event_provider_bloc/event_provider_bloc.dart'; +import 'package:app/core/application/event_tickets/buy_tickets_bloc/buy_tickets_bloc.dart'; +import 'package:app/core/application/event_tickets/buy_tickets_with_crypto_bloc/buy_tickets_with_crypto_bloc.dart'; +import 'package:app/core/application/event_tickets/calculate_event_tickets_pricing_bloc/calculate_event_tickets_pricing_bloc.dart'; +import 'package:app/core/application/event_tickets/select_event_tickets_bloc/select_event_tickets_bloc.dart'; +import 'package:app/core/application/payment/payment_listener/payment_listener.dart'; +import 'package:app/core/application/payment/select_payment_card_cubit/select_payment_card_cubit.dart'; +import 'package:app/core/domain/event/entities/event.dart'; +import 'package:app/core/domain/event/entities/event_tickets_pricing_info.dart'; +import 'package:app/core/domain/event/event_repository.dart'; +import 'package:app/core/domain/event/input/buy_tickets_input/buy_tickets_input.dart'; +import 'package:app/core/domain/payment/entities/payment_card/payment_card.dart'; +import 'package:app/core/domain/payment/entities/purchasable_item/purchasable_item.dart'; +import 'package:app/core/domain/user/user_repository.dart'; +import 'package:app/core/domain/web3/entities/chain.dart'; +import 'package:app/core/domain/web3/web3_repository.dart'; +import 'package:app/core/failure.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/buy_tickets_listener.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/buy_tickets_with_crypto_listener.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/wait_for_payment_notification_handler.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/payment_processing_view.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/transaction_confirming_view.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/wallet_signature_pending_view.dart'; +import 'package:app/core/presentation/widgets/common/button/linear_gradient_button_widget.dart'; +import 'package:app/core/service/wallet/wallet_connect_service.dart'; +import 'package:app/core/utils/auth_utils.dart'; +import 'package:app/core/utils/event_utils.dart'; +import 'package:app/core/utils/payment_utils.dart'; +import 'package:app/core/utils/snackbar_utils.dart'; +import 'package:app/gen/fonts.gen.dart'; +import 'package:app/graphql/backend/schema.graphql.dart'; +import 'package:app/i18n/i18n.g.dart'; +import 'package:app/injection/register_module.dart'; +import 'package:app/router/app_router.gr.dart'; +import 'package:app/theme/sizing.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web3modal_flutter/web3modal_flutter.dart' as web3modal; + +@RoutePage() +class EventBuyTicketsProcessingPage extends StatelessWidget { + const EventBuyTicketsProcessingPage({super.key}); + + @override + Widget build(BuildContext context) { + final selectedNetwork = + context.read().state.selectedNetwork; + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => BuyTicketsBloc(), + ), + BlocProvider( + create: (context) => BuyTicketsWithCryptoBloc( + selectedNetwork: selectedNetwork, + ), + ), + ], + child: const EventBuyTicketsProcessingPageView(), + ); + } +} + +class EventBuyTicketsProcessingPageView extends StatefulWidget { + const EventBuyTicketsProcessingPageView({ + super.key, + }); + + @override + State createState() => + _EventBuyTicketsProcessingPageViewState(); +} + +class _EventBuyTicketsProcessingPageViewState + extends State { + final _waitForNotificationTimer = WaitForPaymentNotificationHandler(); + bool _submittingApplicationFormDone = false; + + EventTicketsPricingInfo? get pricingInfo => + context.read().state.maybeWhen( + orElse: () => null, + success: (pricingInfo, isFree) => pricingInfo, + ); + + bool get isFree => + context.read().state.maybeWhen( + orElse: () => false, + success: (pricingInfo, isFree) => isFree, + failure: (pricingInfo, isFree) => isFree, + ); + + PaymentCard? get selectedCard => + context.read().state.when( + empty: () => null, + cardSelected: (selectedCard) => selectedCard, + ); + + List get selectedTickets => + context.read().state.selectedTickets; + + String? get selectedCurrency => + context.read().state.selectedCurrency; + + String? get selectedNetwork => + context.read().state.selectedNetwork; + + bool get isCryptoCurrency => selectedNetwork?.isNotEmpty == true; + + Event get event => context.read().event; + + String get userWalletAddress => + getIt().w3mService.session?.address ?? ''; + + @override + void initState() { + super.initState(); + _processPayment(context); + } + + @override + void dispose() { + _waitForNotificationTimer.cancel(); + super.dispose(); + } + + Future _processPayment(BuildContext context) async { + try { + await _checkAndSubmitApplicationForm(context); + + if (isCryptoCurrency) { + _processCryptoPayment(context); + } else { + _proceessStripePayment(context); + } + } catch (e) { + SnackBarUtils.showError( + message: e.toString(), + ); + AutoRouter.of(context).pop(); + } + } + + void _proceessStripePayment(BuildContext context) { + context.read().add( + BuyTicketsEvent.buy( + input: BuyTicketsInput( + discount: pricingInfo?.promoCode, + eventId: event.id ?? '', + accountId: pricingInfo?.paymentAccounts?.first.id ?? '', + currency: selectedCurrency ?? '', + items: selectedTickets, + total: pricingInfo?.total ?? '0', + fee: pricingInfo?.paymentAccounts?.firstOrNull?.fee ?? '0', + transferParams: isFree + ? null + : BuyTicketsTransferParamsInput( + paymentMethod: selectedCard?.providerId ?? '', + ), + ), + ), + ); + } + + void _processCryptoPayment(BuildContext context) { + context.read().add( + BuyTicketsWithCryptoEvent.initAndSignPayment( + userWalletAddress: userWalletAddress, + input: BuyTicketsInput( + eventId: event.id ?? '', + accountId: pricingInfo?.paymentAccounts?.first.id ?? '', + currency: selectedCurrency ?? '', + items: selectedTickets, + total: pricingInfo?.total ?? '0', + network: selectedNetwork ?? '', + discount: pricingInfo?.promoCode, + fee: pricingInfo?.paymentAccounts?.first.fee, + ), + ), + ); + } + + Future _checkAndSubmitApplicationForm(BuildContext context) async { + if (event.applicationFormSubmission != null) { + setState(() { + _submittingApplicationFormDone = true; + }); + return; + } + bool isApplicationFormRequired = + (event.applicationQuestions ?? []).isNotEmpty || + (event.applicationProfileFields ?? []).isNotEmpty; + + if (!isApplicationFormRequired) { + setState(() { + _submittingApplicationFormDone = true; + }); + return; + } + final profileFields = + context.read().state.fieldsState; + final answers = context.read().state.answers; + + final updateUserResult = await getIt().updateUser( + input: Input$UserInput.fromJson(profileFields), + ); + if (updateUserResult.isLeft()) { + throw Exception('Failed to update user'); + } + final submitAnswersResult = + await getIt().submitEventApplicationAnswers( + answers: answers, + eventId: event.id ?? '', + ); + if (submitAnswersResult.isLeft()) { + throw Exception('Failed to submit answers'); + } + setState(() { + _submittingApplicationFormDone = true; + }); + } + + void _handleEventRequireApproval(BuildContext context) async { + final event = context.read().event; + AutoRouter.of(context).root.popUntilRouteWithPath('/events'); + AutoRouter.of(context).root.push( + RSVPEventSuccessPopupRoute( + event: event, + primaryMessage: t.event.eventApproval.waitingApproval, + secondaryMessage: t.event.eventApproval.waitingApprovalDescription, + onPressed: (outerContext) async { + AutoRouter.of(outerContext).replace( + EventDetailRoute( + eventId: event.id ?? '', + ), + ); + }, + ), + ); + } + + Future _onNavigateWhenPaymentConfirmed( + BuildContext context, + Event event, + ) { + final selectedTicketTypes = + context.read().state.selectedTickets; + final totalTicketsCount = selectedTicketTypes.fold( + 0, + (previousValue, element) => previousValue + element.count, + ); + final userId = AuthUtils.getUserId(context); + final isAttending = EventUtils.isAttending(event: event, userId: userId); + + return AutoRouter.of(context).replaceAll( + [ + RSVPEventSuccessPopupRoute( + event: event, + primaryMessage: + isAttending ? t.event.eventBuyTickets.ticketsPurchased : null, + secondaryMessage: isAttending + ? t.event.eventBuyTickets.addictionalTicketsPurchasedSuccess( + count: totalTicketsCount, + ) + : null, + buttonBuilder: (newContext) => LinearGradientButton( + onTap: () { + AutoRouter.of(newContext).replaceAll([ + EventUtils.getAssignTicketsRouteForBuyFlow( + event: event, + userId: userId, + ), + ]); + }, + height: Sizing.large, + textStyle: Typo.medium.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onPrimary.withOpacity(0.87), + fontFamily: FontFamily.nohemiVariable, + ), + radius: BorderRadius.circular(LemonRadius.small * 2), + label: t.common.next, + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return MultiBlocListener( + listeners: [ + BuyTicketsListener.create( + onFailure: () { + AutoRouter.of(context).pop(); + }, + onDone: ({ + payment, + eventJoinRequest, + }) { + if (eventJoinRequest != null) { + return _handleEventRequireApproval(context); + } + _waitForNotificationTimer.start( + context, + paymentId: payment?.id ?? '', + onPaymentFailed: () { + context.read().add( + BuyTicketsEvent.receivedPaymentFailedFromNotification( + payment: payment, + ), + ); + }, + onPaymentDone: () { + _onNavigateWhenPaymentConfirmed(context, event); + }, + ); + }, + ), + BuyTicketsWithCryptoListener.create( + onFailure: () { + AutoRouter.of(context).pop(); + }, + onSigned: (data) { + // NOTE: Auto trigger transaction when user done signing signature + final currencyInfo = PaymentUtils.getCurrencyInfo( + pricingInfo, + currency: selectedCurrency ?? '', + ); + + if (currencyInfo == null) return; + final totalCryptoAmount = + (pricingInfo?.cryptoTotal ?? BigInt.zero) + + // add fee if available for EthereumRelay + (pricingInfo?.paymentAccounts?.firstOrNull?.cryptoFee ?? + BigInt.zero); + context.read().add( + BuyTicketsWithCryptoEvent.makeTransaction( + from: userWalletAddress, + amount: totalCryptoAmount, + to: pricingInfo?.paymentAccounts?.firstOrNull?.accountInfo + ?.address ?? + '', + currencyInfo: currencyInfo, + currency: selectedCurrency ?? '', + eventId: event.id ?? '', + ), + ); + }, + onDone: (data) { + if (data.eventJoinRequest != null) { + return _handleEventRequireApproval(context); + } + _waitForNotificationTimer.startWithCrypto( + context, + chainId: selectedNetwork ?? '', + txHash: data.txHash ?? '', + paymentId: data.payment?.id ?? '', + onPaymentFailed: () { + context.read().add( + BuyTicketsWithCryptoEvent + .receivedPaymentFailedFromNotification( + payment: data.payment, + ), + ); + }, + onPaymentDone: () { + _onNavigateWhenPaymentConfirmed(context, event); + }, + ); + }, + ), + ], + child: PaymentListener( + onReceivedPaymentFailed: (eventId, payment) { + final currentPayment = isCryptoCurrency + ? context.read().state.data.payment + : context.read().state.maybeWhen( + orElse: () => null, + done: (payment, _) => payment, + ); + if (currentPayment?.id == payment.id && eventId == event.id) { + _waitForNotificationTimer.cancel(); + if (isCryptoCurrency) { + context.read().add( + BuyTicketsWithCryptoEvent + .receivedPaymentFailedFromNotification( + payment: payment, + ), + ); + } else { + context.read().add( + BuyTicketsEvent.receivedPaymentFailedFromNotification( + payment: payment, + ), + ); + } + } + }, + onReceivedPaymentSuccess: (eventId, payment) { + final eventJoinRequest = isCryptoCurrency + ? context + .read() + .state + .data + .eventJoinRequest + : context.read().state.maybeWhen( + orElse: () => null, + done: (payment, eventJoinRequest) => eventJoinRequest, + ); + if (eventJoinRequest != null) { + return; + } + if (eventId == event.id) { + _waitForNotificationTimer.cancel(); + _onNavigateWhenPaymentConfirmed(context, event); + } + }, + child: Scaffold( + body: Builder( + builder: (context) { + if (!_submittingApplicationFormDone) { + return const PaymentProcessingView(); + } + if (isCryptoCurrency) { + return BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => const SizedBox.shrink(), + loading: (data) { + if (data.signature == null) { + return const WalletSignaturePendingView(); + } + return const PaymentProcessingView(); + }, + signed: (data) => const PaymentProcessingView(), + done: (data) => + FutureBuilder>( + future: getIt() + .getChainById(chainId: selectedNetwork ?? ''), + builder: (context, snapshot) { + if (snapshot.hasError || + snapshot.data == null || + snapshot.connectionState == + ConnectionState.waiting) { + return const PaymentProcessingView(); + } + final chain = snapshot.data?.getOrElse(() => null); + final duration = (chain?.blockTime ?? 0) * + (chain?.safeConfirmations ?? 0); + return TransactionConfirmingView( + duration: + duration != 0 ? duration.toInt() + 10 : 60, + chain: chain, + ); + }, + ), + ); + }, + ); + } + return const PaymentProcessingView(); + }, + ), + ), + ), + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/buy_tickets_listener.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/buy_tickets_listener.dart similarity index 100% rename from lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/buy_tickets_listener.dart rename to lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/buy_tickets_listener.dart diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/buy_tickets_with_crypto_listener.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/buy_tickets_with_crypto_listener.dart similarity index 88% rename from lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/buy_tickets_with_crypto_listener.dart rename to lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/buy_tickets_with_crypto_listener.dart index e4dc0a529..c8693bd9b 100644 --- a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/buy_tickets_with_crypto_listener.dart +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/buy_tickets_with_crypto_listener.dart @@ -7,11 +7,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class BuyTicketsWithCryptoListener { static BlocListener create({ Function(BuyTicketsWithCryptoStateData data)? onDone, + Function(BuyTicketsWithCryptoStateData data)? onSigned, + Function()? onFailure, }) { return BlocListener( listener: (context, state) { state.maybeWhen( orElse: () => null, + signed: (data) { + onSigned?.call(data); + }, failure: (data, failureReason) { if (failureReason is InitCryptoPaymentFailure || failureReason is UpdateCryptoPaymentFailure || @@ -20,7 +25,7 @@ class BuyTicketsWithCryptoListener { context: context, builder: (context) => LemonAlertDialog( onClose: () => Navigator.of(context).pop(), - child: Text(t.common.pleaseTryAgain), + child: Text(failureReason.message ?? t.common.pleaseTryAgain), ), ); } @@ -40,6 +45,7 @@ class BuyTicketsWithCryptoListener { state: BuyTicketsWithCryptoState.idle(data: state.data), ), ); + onFailure?.call(); }, done: (data) { // TODO: will trigger timeout 30s and if payment noti not coming yet => manually diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/wait_for_payment_notification_handler.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/wait_for_payment_notification_handler.dart similarity index 84% rename from lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/wait_for_payment_notification_handler.dart rename to lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/wait_for_payment_notification_handler.dart index c51d5b55e..764a141ef 100644 --- a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/wait_for_payment_notification_handler.dart +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/handler/wait_for_payment_notification_handler.dart @@ -73,22 +73,17 @@ class WaitForPaymentNotificationHandler { Function()? onPaymentDone, Function()? onPaymentFailed, }) async { - timer = Timer(maxDurationToWaitForNotification, () async { - final getChainResult = - await getIt().getChainById(chainId: chainId); - final chain = getChainResult.getOrElse(() => null); + final getChainResult = + await getIt().getChainById(chainId: chainId); + final chain = getChainResult.getOrElse(() => null); + final waitTime = (chain?.blockTime?.toInt() ?? 1) * + (chain?.safeConfirmations?.toInt() ?? 1); + timer = Timer(Duration(seconds: waitTime * 2), () async { final receipt = await Web3Utils.waitForReceipt( rpcUrl: chain?.rpcUrl ?? '', txHash: txHash, deplayDuration: delayIntervalDuration, ); - // This make sure BE already confirmed after transaction receipt is retured - await Future.delayed( - Duration( - seconds: (chain?.blockTime?.toInt() ?? 1) * - (chain?.safeConfirmations?.toInt() ?? 1), - ), - ); final payment = await _checkPayment(paymentId); if (receipt?.status == true && payment?.state == PaymentState.succeeded) { diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/payment_processing_view.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/payment_processing_view.dart new file mode 100644 index 000000000..95282cea9 --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/payment_processing_view.dart @@ -0,0 +1,54 @@ +import 'package:app/core/presentation/widgets/animation/circular_loading_widget.dart'; +import 'package:app/gen/fonts.gen.dart'; +import 'package:app/i18n/i18n.g.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; +import 'package:flutter/material.dart'; + +class PaymentProcessingView extends StatelessWidget { + const PaymentProcessingView({super.key}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final t = Translations.of(context); + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + const Spacer(), + const CircularLoadingWidget(), + const Spacer(), + Padding( + padding: EdgeInsets.symmetric(horizontal: Spacing.smMedium), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + t.event.eventBuyTickets.processingPayment, + style: Typo.extraLarge.copyWith( + color: colorScheme.onPrimary, + fontWeight: FontWeight.bold, + fontFamily: FontFamily.nohemiVariable, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: Spacing.superExtraSmall), + Text( + t.event.eventBuyTickets.processingPaymentDescription, + style: Typo.mediumPlus.copyWith( + color: colorScheme.onSecondary, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/transaction_confirming_view.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/transaction_confirming_view.dart new file mode 100644 index 000000000..a46ef7bb3 --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/transaction_confirming_view.dart @@ -0,0 +1,151 @@ +import 'package:app/core/domain/web3/entities/chain.dart'; +import 'package:app/core/presentation/widgets/animation/circular_countdown_timer_widget/circular_countdown_timer.dart'; +import 'package:app/gen/fonts.gen.dart'; +import 'package:app/i18n/i18n.g.dart'; +import 'package:app/theme/color.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; +import 'package:duration/duration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class TransactionConfirmingView extends StatelessWidget { + final int duration; + final String? txHash; + final Chain? chain; + + const TransactionConfirmingView({ + super.key, + required this.duration, + this.txHash, + required this.chain, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final t = Translations.of(context); + + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + // TODO: BE will support return explorer url to view transaction + // Padding( + // padding: EdgeInsets.only( + // top: Spacing.smMedium, + // right: Spacing.smMedium, + // ), + // child: Row( + // mainAxisSize: MainAxisSize.max, + // mainAxisAlignment: MainAxisAlignment.end, + // children: [ + // LemonOutlineButton( + // onTap: () { + // }, + // backgroundColor: LemonColor.otherMessage, + // radius: BorderRadius.circular(LemonRadius.button), + // label: t.event.eventBuyTickets.viewTransaction, + // textStyle: Typo.small.copyWith( + // color: colorScheme.onSecondary, + // ), + // borderColor: Colors.transparent, + // trailing: ThemeSvgIcon( + // color: colorScheme.onPrimary, + // builder: (colorFilter) => Assets.icons.icExpand.svg( + // colorFilter: colorFilter, + // width: 9.w, + // height: 9.w, + // ), + // ), + // ), + // ], + // ), + // ), + // SizedBox(height: Spacing.xLarge * 2), + const Spacer(), + const Spacer(), + const Spacer(), + Stack( + children: [ + Align( + alignment: Alignment.center, + child: Container( + width: 240.w, + height: 240.w, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(220.w), + border: Border.all( + color: colorScheme.outline, + width: 4.w, + ), + ), + ), + ), + Align( + alignment: Alignment.center, + child: CircularCountDownTimer( + width: 240.w, + height: 240.w, + duration: duration, + isReverse: true, + isReverseAnimation: true, + fillColor: LemonColor.paleViolet, + ringColor: Colors.transparent, + strokeWidth: 25.w, + strokeCap: StrokeCap.round, + textStyle: TextStyle( + fontSize: 56.sp, + color: colorScheme.onPrimary, + fontFamily: FontFamily.orbitron, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const Spacer(), + const Spacer(), + const Spacer(), + Padding( + padding: EdgeInsets.symmetric( + horizontal: Spacing.smMedium, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + t.event.eventBuyTickets.confirmingTransaction, + style: Typo.extraLarge.copyWith( + color: colorScheme.onPrimary, + fontWeight: FontWeight.bold, + fontFamily: FontFamily.nohemiVariable, + ), + ), + SizedBox(height: Spacing.superExtraSmall), + Text( + t.event.eventBuyTickets.confirmingTransactionDescription( + duration: prettyDuration( + Duration( + seconds: duration, + ), + tersity: DurationTersity.second, + upperTersity: DurationTersity.minute, + ), + ), + textAlign: TextAlign.center, + style: Typo.mediumPlus.copyWith( + color: colorScheme.onSecondary, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/wallet_signature_pending_view.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/wallet_signature_pending_view.dart new file mode 100644 index 000000000..c5e0cbba0 --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_buy_tickets_processing_page/views/loaders/wallet_signature_pending_view.dart @@ -0,0 +1,94 @@ +import 'package:app/core/application/event_tickets/calculate_event_tickets_pricing_bloc/calculate_event_tickets_pricing_bloc.dart'; +import 'package:app/core/application/event_tickets/select_event_tickets_bloc/select_event_tickets_bloc.dart'; +import 'package:app/core/presentation/widgets/animation/circular_animation_widget.dart'; +import 'package:app/core/service/wallet/wallet_connect_service.dart'; +import 'package:app/core/utils/payment_utils.dart'; +import 'package:app/core/utils/web3_utils.dart'; +import 'package:app/gen/assets.gen.dart'; +import 'package:app/gen/fonts.gen.dart'; +import 'package:app/i18n/i18n.g.dart'; +import 'package:app/injection/register_module.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web3modal_flutter/web3modal_flutter.dart'; + +class WalletSignaturePendingView extends StatelessWidget { + const WalletSignaturePendingView({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final t = Translations.of(context); + final walletAddress = + getIt().w3mService.session?.address ?? ''; + final selectedCurrency = + context.read().state.selectedCurrency; + + return BlocBuilder( + builder: (context, state) { + final pricingInfo = state.maybeWhen( + orElse: () => null, + success: (pricingInfo, isFree) => pricingInfo, + ); + final currencyInfo = PaymentUtils.getCurrencyInfo( + pricingInfo, + currency: selectedCurrency ?? '', + ); + final amountText = Web3Utils.formatCryptoCurrency( + pricingInfo?.cryptoTotal ?? BigInt.zero, + currency: selectedCurrency ?? '', + decimals: currencyInfo?.decimals ?? 2, + ); + + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + const Spacer(), + const Spacer(), + const Spacer(), + CircularAnimationWidget( + icon: Assets.icons.icWalletDarkGradient.svg(), + ), + const Spacer(), + const Spacer(), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + t.event.eventBuyTickets.signaturePending, + style: Typo.extraLarge.copyWith( + color: colorScheme.onPrimary, + fontWeight: FontWeight.bold, + fontFamily: FontFamily.nohemiVariable, + ), + ), + SizedBox(height: Spacing.superExtraSmall), + Text( + t.event.eventBuyTickets.signaturePendingDescription( + walletAddress: Web3Utils.formatIdentifier(walletAddress), + amount: amountText, + ), + textAlign: TextAlign.center, + style: Typo.mediumPlus.copyWith( + color: colorScheme.onSecondary, + ), + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/event_tickets_summary_page.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/event_tickets_summary_page.dart index fbbc49139..22ab0c13f 100644 --- a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/event_tickets_summary_page.dart +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/event_tickets_summary_page.dart @@ -1,37 +1,28 @@ +import 'package:app/core/application/event/event_application_form_bloc/event_application_form_bloc.dart'; import 'package:app/core/application/event/event_provider_bloc/event_provider_bloc.dart'; -import 'package:app/core/application/event_tickets/buy_tickets_bloc/buy_tickets_bloc.dart'; -import 'package:app/core/application/event_tickets/buy_tickets_with_crypto_bloc/buy_tickets_with_crypto_bloc.dart'; import 'package:app/core/application/event_tickets/calculate_event_tickets_pricing_bloc/calculate_event_tickets_pricing_bloc.dart'; import 'package:app/core/application/event_tickets/get_event_ticket_types_bloc/get_event_ticket_types_bloc.dart'; import 'package:app/core/application/event_tickets/select_event_tickets_bloc/select_event_tickets_bloc.dart'; import 'package:app/core/application/payment/get_payment_cards_bloc/get_payment_cards_bloc.dart'; -import 'package:app/core/application/payment/payment_listener/payment_listener.dart'; import 'package:app/core/application/payment/select_payment_card_cubit/select_payment_card_cubit.dart'; -import 'package:app/core/domain/event/entities/event.dart'; import 'package:app/core/domain/event/entities/event_ticket_types.dart'; -import 'package:app/core/domain/event/input/buy_tickets_input/buy_tickets_input.dart'; +import 'package:app/core/domain/event/entities/event_tickets_pricing_info.dart'; import 'package:app/core/domain/event/input/calculate_tickets_pricing_input/calculate_tickets_pricing_input.dart'; +import 'package:app/core/domain/payment/entities/purchasable_item/purchasable_item.dart'; import 'package:app/core/domain/payment/input/get_stripe_cards_input/get_stripe_cards_input.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/buy_tickets_listener.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/buy_tickets_with_crypto_listener.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/handler/wait_for_payment_notification_handler.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/add_promo_code_input.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_summary.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_summary_footer.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_info_summary.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_total_price_summary.dart'; import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_tickets_summary.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/pay_by_crypto_button.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_by_crypto_footer.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_by_stripe_footer.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/promo_code_summary.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_application_form.dart'; import 'package:app/core/presentation/widgets/common/appbar/lemon_appbar_widget.dart'; -import 'package:app/core/presentation/widgets/common/button/linear_gradient_button_widget.dart'; import 'package:app/core/presentation/widgets/common/list/empty_list_widget.dart'; -import 'package:app/core/presentation/widgets/common/slide_to_act/slide_to_act.dart'; import 'package:app/core/presentation/widgets/loading_widget.dart'; -import 'package:app/core/utils/auth_utils.dart'; -import 'package:app/core/utils/date_format_utils.dart'; -import 'package:app/core/utils/event_utils.dart'; -import 'package:app/gen/fonts.gen.dart'; +import 'package:app/core/utils/string_utils.dart'; import 'package:app/i18n/i18n.g.dart'; -import 'package:app/router/app_router.gr.dart'; -import 'package:app/theme/sizing.dart'; +import 'package:app/theme/color.dart'; import 'package:app/theme/spacing.dart'; import 'package:app/theme/typo.dart'; import 'package:auto_route/auto_route.dart'; @@ -53,8 +44,8 @@ class EventTicketsSummaryPage extends StatelessWidget { return MultiBlocProvider( providers: [ - BlocProvider( - create: (context) => CalculateEventTicketPricingBloc() + BlocProvider.value( + value: context.read() ..add( CalculateEventTicketPricingEvent.calculate( input: CalculateTicketsPricingInput( @@ -66,92 +57,17 @@ class EventTicketsSummaryPage extends StatelessWidget { ), ), ), - BlocProvider( - create: (context) => BuyTicketsBloc(), - ), - BlocProvider( - create: (context) => BuyTicketsWithCryptoBloc( - selectedNetwork: selectedNetwork, - ), - ), ], - child: EventTicketsSummaryPageView(), + child: const EventTicketsSummaryPageView(), ); } } class EventTicketsSummaryPageView extends StatelessWidget { - EventTicketsSummaryPageView({ + const EventTicketsSummaryPageView({ super.key, }); - final _slideActionKey = GlobalKey(); - final _waitForNotificationTimer = WaitForPaymentNotificationHandler(); - - void _handleEventRequireApproval(BuildContext context) async { - final event = context.read().event; - AutoRouter.of(context).root.popUntilRouteWithPath('/events'); - AutoRouter.of(context).root.push( - RSVPEventSuccessPopupRoute( - event: event, - primaryMessage: t.event.eventApproval.waitingApproval, - secondaryMessage: t.event.eventApproval.waitingApprovalDescription, - onPressed: (outerContext) async { - AutoRouter.of(outerContext).replace( - EventDetailRoute( - eventId: event.id ?? '', - ), - ); - }, - ), - ); - } - - Future _onNavigateWhenPaymentConfirmed( - BuildContext context, - Event event, - ) { - final selectedTicketTypes = - context.read().state.selectedTickets; - final totalTicketsCount = selectedTicketTypes.fold( - 0, - (previousValue, element) => previousValue + element.count, - ); - final userId = AuthUtils.getUserId(context); - final isAttending = EventUtils.isAttending(event: event, userId: userId); - - return AutoRouter.of(context).replaceAll( - [ - RSVPEventSuccessPopupRoute( - event: event, - primaryMessage: - isAttending ? t.event.eventBuyTickets.ticketsPurchased : null, - secondaryMessage: isAttending - ? t.event.eventBuyTickets.addictionalTicketsPurchasedSuccess( - count: totalTicketsCount, - ) - : null, - buttonBuilder: (newContext) => LinearGradientButton( - onTap: () => AutoRouter.of(newContext).replace( - EventUtils.getAssignTicketsRouteForBuyFlow( - event: event, - userId: userId, - ), - ), - height: Sizing.large, - textStyle: Typo.medium.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onPrimary.withOpacity(0.87), - fontFamily: FontFamily.nohemiVariable, - ), - radius: BorderRadius.circular(LemonRadius.small * 2), - label: t.common.next, - ), - ), - ], - ); - } - @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; @@ -167,6 +83,14 @@ class EventTicketsSummaryPageView extends StatelessWidget { success: (response, _) => response.ticketTypes ?? [], ); final isCryptoCurrency = selectedNetwork?.isNotEmpty == true; + final isApplicationFormRequired = + event.applicationProfileFields?.isNotEmpty == true || + event.applicationQuestions?.isNotEmpty == true; + final isApplicationFormValid = !isApplicationFormRequired + ? true + : event.applicationFormSubmission != null + ? true + : context.watch().state.isValid; return MultiBlocListener( listeners: [ @@ -205,370 +129,241 @@ class EventTicketsSummaryPageView extends StatelessWidget { ); }, ), - BuyTicketsListener.create( - onFailure: () => _slideActionKey.currentState?.reset(), - onDone: ({ - payment, - eventJoinRequest, - }) { - if (eventJoinRequest != null) { - return _handleEventRequireApproval(context); - } - _waitForNotificationTimer.start( - context, - paymentId: payment?.id ?? '', - onPaymentFailed: () { - context.read().add( - BuyTicketsEvent.receivedPaymentFailedFromNotification( - payment: payment, - ), - ); - }, - onPaymentDone: () { - _onNavigateWhenPaymentConfirmed(context, event); - }, - ); - }, - ), - BuyTicketsWithCryptoListener.create( - onDone: (data) { - if (data.eventJoinRequest != null) { - return _handleEventRequireApproval(context); - } - _waitForNotificationTimer.startWithCrypto( - context, - chainId: selectedNetwork ?? '', - txHash: data.txHash ?? '', - paymentId: data.payment?.id ?? '', - onPaymentFailed: () { - context.read().add( - BuyTicketsWithCryptoEvent - .receivedPaymentFailedFromNotification( - payment: data.payment, - ), - ); - }, - onPaymentDone: () { - _onNavigateWhenPaymentConfirmed(context, event); - }, - ); - }, - ), ], - child: PaymentListener( - onReceivedPaymentFailed: (eventId, payment) { - final currentPayment = isCryptoCurrency - ? context.read().state.data.payment - : context.read().state.maybeWhen( - orElse: () => null, - done: (payment, _) => payment, - ); - if (currentPayment?.id == payment.id && eventId == event.id) { - _waitForNotificationTimer.cancel(); - if (isCryptoCurrency) { - context.read().add( - BuyTicketsWithCryptoEvent - .receivedPaymentFailedFromNotification( - payment: payment, - ), - ); - } else { - context.read().add( - BuyTicketsEvent.receivedPaymentFailedFromNotification( - payment: payment, - ), - ); - } - } - }, - onReceivedPaymentSuccess: (eventId, payment) { - final eventJoinRequest = isCryptoCurrency - ? context - .read() - .state - .data - .eventJoinRequest - : context.read().state.maybeWhen( - orElse: () => null, - done: (payment, eventJoinRequest) => eventJoinRequest, - ); - if (eventJoinRequest != null) { - return; - } - if (eventId == event.id) { - _waitForNotificationTimer.cancel(); - _onNavigateWhenPaymentConfirmed(context, event); - } - }, - child: WillPopScope( - // prevent accidentally swipe back - onWillPop: () async => true, - child: Stack( - children: [ - Scaffold( - backgroundColor: colorScheme.background, - appBar: const LemonAppBar(), - body: SafeArea( - child: Stack( - children: [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.symmetric( - horizontal: Spacing.smMedium, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - t.event.eventBuyTickets.orderSummary, - style: Typo.extraLarge.copyWith( - color: colorScheme.onPrimary, - fontFamily: FontFamily.nohemiVariable, - fontWeight: FontWeight.w800, - ), - ), - Text( - "${event.title} • ${DateFormatUtils.dateWithTimezone( - dateTime: event.start ?? DateTime.now(), - timezone: event.timezone ?? '', - pattern: DateFormatUtils.dateOnlyFormat, - )}", - style: Typo.mediumPlus.copyWith( - color: colorScheme.onSecondary, - ), - ), - ], - ), + child: WillPopScope( + // prevent accidentally swipe back + onWillPop: () async => true, + child: Stack( + children: [ + Scaffold( + backgroundColor: colorScheme.background, + appBar: LemonAppBar( + title: t.event.eventBuyTickets.registration, + ), + body: SafeArea( + child: Stack( + children: [ + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric( + horizontal: Spacing.xSmall, ), - SizedBox(height: Spacing.large), - BlocBuilder( - builder: (context, state) { - return state.when( - idle: () => const SizedBox.shrink(), - loading: () => - Loading.defaultLoading(context), - failure: (pricingInfo, isFree) { - if (pricingInfo != null) { - return EventTicketsSummary( - ticketTypes: ticketTypes, - selectedTickets: selectedTickets, - selectedCurrency: selectedCurrency, - selectedNetwork: selectedNetwork, - pricingInfo: pricingInfo, - ); - } - return EmptyList( - emptyText: t.common.somethingWrong, - ); - }, - success: (pricingInfo, isFree) => - EventTicketsSummary( - ticketTypes: ticketTypes, - selectedTickets: selectedTickets, - selectedCurrency: selectedCurrency, - selectedNetwork: selectedNetwork, - pricingInfo: pricingInfo, - ), - ); - }, + child: EventInfoSummary( + event: event, ), - SizedBox(height: Spacing.smMedium), - BlocBuilder( - builder: (context, state) => AddPromoCodeInput( - pricingInfo: state.maybeWhen( - orElse: () => null, - failure: ((pricingInfo, isFree) => - pricingInfo), - success: (pricingInfo, isFree) => pricingInfo, - ), - onPressApply: (promoCode) { - context - .read() - .add( - CalculateEventTicketPricingEvent - .calculate( - input: CalculateTicketsPricingInput( - discount: promoCode, - eventId: event.id ?? '', - items: selectedTickets, - currency: selectedCurrency, - network: selectedNetwork, - ), - ), - ); + ), + SizedBox(height: Spacing.medium), + BlocBuilder( + builder: (context, state) { + return state.when( + idle: () => const SizedBox.shrink(), + loading: () => Loading.defaultLoading(context), + failure: (pricingInfo, isFree) { + if (pricingInfo != null) { + return _TicketsAndTotalPricingSummary( + ticketTypes: ticketTypes, + selectedTickets: selectedTickets, + selectedCurrency: selectedCurrency, + selectedNetwork: selectedNetwork, + pricingInfo: pricingInfo, + ); + } + return EmptyList( + emptyText: t.common.somethingWrong, + ); }, + success: (pricingInfo, isFree) => + _TicketsAndTotalPricingSummary( + ticketTypes: ticketTypes, + selectedTickets: selectedTickets, + selectedCurrency: selectedCurrency, + selectedNetwork: selectedNetwork, + pricingInfo: pricingInfo, + ), + ); + }, + ), + SizedBox(height: Spacing.medium), + if (event.applicationFormSubmission == null) ...[ + Padding( + padding: EdgeInsets.symmetric( + horizontal: Spacing.xSmall, ), + child: const RSVPApplicationForm(), ), - SizedBox(height: Spacing.smMedium), - BlocBuilder( - builder: (context, state) { - return state.when( - idle: () => const SizedBox.shrink(), - loading: () => - Loading.defaultLoading(context), - failure: (pricingInfo, isFree) { - if (pricingInfo != null) { - return EventOrderSummary( - selectedCurrency: selectedCurrency, - selectedNetwork: selectedNetwork, - pricingInfo: pricingInfo, - ); - } - return EmptyList( - emptyText: t.common.somethingWrong, - ); - }, - success: (pricingInfo, isFree) => - EventOrderSummary( - selectedCurrency: selectedCurrency, - selectedNetwork: selectedNetwork, - pricingInfo: pricingInfo, - ), - ); - }, - ), - SizedBox(height: 150.w + Spacing.medium), + SizedBox(height: 150.w), ], - ), + ], ), - Align( - alignment: Alignment.bottomCenter, - child: BlocBuilder( - builder: (context, state) { - final pricingInfo = state.maybeWhen( - orElse: () => null, - failure: ((pricingInfo, isFree) => pricingInfo), - success: (pricingInfo, isFree) => pricingInfo, - ); - final isFree = state.maybeWhen( - orElse: () => false, - failure: (pricingInfo, isFree) => isFree, - success: (pricingInfo, isFree) => isFree, - ); - - if (pricingInfo == null) { - return const SizedBox.shrink(); - } - if (isCryptoCurrency) { - return PayByCryptoButton( - selectedTickets: selectedTickets, - selectedCurrency: selectedCurrency, - selectedNetwork: selectedNetwork, - pricingInfo: pricingInfo, - isFree: isFree, - ); - } + ), + Align( + alignment: Alignment.bottomCenter, + child: BlocBuilder( + builder: (context, state) { + final pricingInfo = state.maybeWhen( + orElse: () => null, + failure: ((pricingInfo, isFree) => pricingInfo), + success: (pricingInfo, isFree) => pricingInfo, + ); + final isFree = state.maybeWhen( + orElse: () => false, + failure: (pricingInfo, isFree) => isFree, + success: (pricingInfo, isFree) => isFree, + ); - return EventOrderSummaryFooter( - isFree: isFree, + if (pricingInfo == null) { + return const SizedBox.shrink(); + } + if (isCryptoCurrency) { + return PayByCryptoFooter( + selectedTickets: selectedTickets, selectedCurrency: selectedCurrency, - onSlideToPay: () { - if (pricingInfo.paymentAccounts == null || - pricingInfo.paymentAccounts?.isEmpty == - true) { - return const SizedBox.shrink(); - } - final selectedCard = context - .read() - .state - .when( - empty: () => null, - cardSelected: (selectedCard) => - selectedCard, - ); - context.read().add( - BuyTicketsEvent.buy( - input: BuyTicketsInput( - discount: pricingInfo.promoCode, - eventId: event.id ?? '', - accountId: pricingInfo - .paymentAccounts?.first.id ?? - '', - currency: selectedCurrency, - items: selectedTickets, - total: pricingInfo.total ?? '0', - fee: pricingInfo.paymentAccounts - ?.firstOrNull?.fee ?? - '0', - transferParams: isFree - ? null - : BuyTicketsTransferParamsInput( - paymentMethod: selectedCard - ?.providerId ?? - '', - ), - ), - ), - ); - }, + selectedNetwork: selectedNetwork, pricingInfo: pricingInfo, - slideActionKey: _slideActionKey, - onCardAdded: (newCard) { - context.read().add( - GetPaymentCardsEvent.manuallyAddMoreCard( - paymentCard: newCard, - ), - ); - context - .read() - .selectPaymentCard( - paymentCard: newCard, - ); - }, - onSelectCard: (selectedCard) { - context - .read() - .selectPaymentCard( - paymentCard: selectedCard, - ); - }, + isFree: isFree, + disabled: !isApplicationFormValid, ); - }, - ), + } + + return PayByStripeFooter( + disabled: !isApplicationFormValid, + isFree: isFree, + selectedCurrency: selectedCurrency, + pricingInfo: pricingInfo, + onCardAdded: (newCard) { + context.read().add( + GetPaymentCardsEvent.manuallyAddMoreCard( + paymentCard: newCard, + ), + ); + context + .read() + .selectPaymentCard( + paymentCard: newCard, + ); + }, + onSelectCard: (selectedCard) { + context + .read() + .selectPaymentCard( + paymentCard: selectedCard, + ); + }, + ); + }, ), - ], - ), - ), - ), - BlocBuilder( - builder: (context, state) => state.maybeWhen( - idle: () => const SizedBox.shrink(), - failure: (failureReason) => const SizedBox.shrink(), - orElse: () => Positioned.fill( - child: Container( - color: colorScheme.background.withOpacity(0.5), - child: Loading.defaultLoading(context), ), - ), + ], ), ), - BlocBuilder( - builder: (context, state) => state.maybeWhen( - orElse: () => const SizedBox.shrink(), - loading: (_) => Positioned.fill( - child: Container( - color: colorScheme.background.withOpacity(0.5), - child: Loading.defaultLoading(context), - ), + ), + ], + ), + ), + ); + } +} + +class _TicketsAndTotalPricingSummary extends StatelessWidget { + const _TicketsAndTotalPricingSummary({ + required this.ticketTypes, + required this.selectedTickets, + required this.selectedCurrency, + required this.selectedNetwork, + required this.pricingInfo, + }); + + final List ticketTypes; + final List selectedTickets; + final String selectedCurrency; + final String? selectedNetwork; + final EventTicketsPricingInfo pricingInfo; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final t = Translations.of(context); + final event = context.read().event; + return Padding( + padding: EdgeInsets.symmetric(horizontal: Spacing.xSmall), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + StringUtils.capitalize(t.event.tickets(n: 2)), + style: Typo.medium.copyWith( + color: colorScheme.onPrimary, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: Spacing.xSmall), + Container( + decoration: BoxDecoration( + color: LemonColor.atomicBlack, + border: Border.all( + color: colorScheme.outline, + width: 1.w, + ), + borderRadius: BorderRadius.circular(LemonRadius.medium), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.all(Spacing.small), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + EventTicketsSummary( + ticketTypes: ticketTypes, + selectedTickets: selectedTickets, + selectedCurrency: selectedCurrency, + selectedNetwork: selectedNetwork, + pricingInfo: pricingInfo, + ), + SizedBox(height: Spacing.xSmall), + PromoCodeSummary( + pricingInfo: pricingInfo, + onPressApply: ({promoCode}) { + context.read().add( + CalculateEventTicketPricingEvent.calculate( + input: CalculateTicketsPricingInput( + discount: promoCode, + eventId: event.id ?? '', + items: selectedTickets, + currency: selectedCurrency, + network: selectedNetwork, + ), + ), + ); + }, + ), + ], ), - done: (_) => Positioned.fill( - child: Container( - color: colorScheme.background.withOpacity(0.5), - child: Loading.defaultLoading(context), - ), + ), + Divider( + thickness: 1.w, + color: colorScheme.outline, + ), + Padding( + padding: EdgeInsets.all(Spacing.small), + child: EventTotalPriceSummary( + selectedCurrency: selectedCurrency, + selectedNetwork: selectedNetwork, + pricingInfo: pricingInfo, ), ), - ), - ], + ], + ), ), - ), + ], ), ); } diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_info_summary.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_info_summary.dart new file mode 100644 index 000000000..f0a0179d1 --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_info_summary.dart @@ -0,0 +1,113 @@ +import 'package:app/core/domain/event/entities/event.dart'; +import 'package:app/core/presentation/widgets/image_placeholder_widget.dart'; +import 'package:app/core/presentation/widgets/lemon_network_image/lemon_network_image.dart'; +import 'package:app/core/presentation/widgets/theme_svg_icon_widget.dart'; +import 'package:app/core/utils/date_format_utils.dart'; +import 'package:app/core/utils/event_utils.dart'; +import 'package:app/gen/assets.gen.dart'; +import 'package:app/theme/color.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class EventInfoSummary extends StatelessWidget { + final Event event; + + const EventInfoSummary({ + super.key, + required this.event, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + decoration: BoxDecoration( + color: LemonColor.atomicBlack, + border: Border.all( + color: colorScheme.outline, + width: 1.w, + ), + borderRadius: BorderRadius.circular( + LemonRadius.normal, + ), + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(Spacing.small), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + LemonNetworkImage( + imageUrl: EventUtils.getEventThumbnailUrl(event: event), + width: 18.w, + height: 18.w, + placeholder: ImagePlaceholder.eventCard(), + borderRadius: BorderRadius.circular(3.r), + ), + SizedBox(width: Spacing.xSmall), + Text( + event.title ?? '', + style: Typo.medium.copyWith( + color: colorScheme.onPrimary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + Divider( + thickness: 1.w, + color: colorScheme.outline, + ), + Padding( + padding: EdgeInsets.all(Spacing.small), + child: Column( + children: [ + Row( + children: [ + ThemeSvgIcon( + color: colorScheme.onSecondary, + builder: (filter) => Assets.icons.icCalendar.svg( + colorFilter: filter, + ), + ), + SizedBox(width: Spacing.xSmall), + Text( + DateFormatUtils.fullDateWithTime(event.start), + style: Typo.medium.copyWith( + color: colorScheme.onSecondary, + ), + ), + ], + ), + if (event.address != null) ...[ + SizedBox(height: Spacing.xSmall), + Row( + children: [ + ThemeSvgIcon( + color: colorScheme.onSecondary, + builder: (filter) => Assets.icons.icLocationPin.svg( + colorFilter: filter, + ), + ), + SizedBox(width: Spacing.xSmall), + Text( + event.address?.title ?? '', + style: Typo.medium.copyWith( + color: colorScheme.onSecondary, + ), + ), + ], + ), + ], + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_summary.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_summary.dart deleted file mode 100644 index 83963ccb1..000000000 --- a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_summary.dart +++ /dev/null @@ -1,215 +0,0 @@ -import 'package:app/core/domain/event/entities/event_tickets_pricing_info.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/ticket_wave_custom_paint.dart'; -import 'package:app/core/presentation/widgets/common/dotted_line/dotted_line.dart'; -import 'package:app/core/utils/number_utils.dart'; -import 'package:app/core/utils/payment_utils.dart'; -import 'package:app/core/utils/web3_utils.dart'; -import 'package:app/i18n/i18n.g.dart'; -import 'package:app/theme/color.dart'; -import 'package:app/theme/spacing.dart'; -import 'package:app/theme/typo.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:collection/collection.dart'; - -class EventOrderSummary extends StatelessWidget { - const EventOrderSummary({ - super.key, - required this.pricingInfo, - required this.selectedCurrency, - required this.selectedNetwork, - }); - - final EventTicketsPricingInfo pricingInfo; - final String? selectedNetwork; - final String selectedCurrency; - - BigInt get _totalCryptoAmount { - return (pricingInfo.cryptoTotal ?? BigInt.zero) + - // add fee if available for EthereumRelay - (pricingInfo.paymentAccounts?.firstOrNull?.cryptoFee ?? BigInt.zero); - } - - double get _totalFiatAmount { - return (pricingInfo.fiatTotal ?? 0) + - (pricingInfo.paymentAccounts?.firstOrNull?.fiatFee ?? 0); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - final colorScheme = Theme.of(context).colorScheme; - final isCryptoCurrency = selectedNetwork?.isNotEmpty == true; - final currencyInfo = - PaymentUtils.getCurrencyInfo(pricingInfo, currency: selectedCurrency); - - return Padding( - padding: EdgeInsets.symmetric(horizontal: Spacing.smMedium), - child: Column( - children: [ - LayoutBuilder( - builder: (context, constraint) => CustomPaint( - size: Size(constraint.maxWidth, 11.w), - painter: TicketWaveCustomPaint(), - ), - ), - Container( - decoration: BoxDecoration( - color: colorScheme.onPrimary.withOpacity(0.06), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(LemonRadius.normal), - bottomRight: Radius.circular(LemonRadius.normal), - ), - ), - padding: EdgeInsets.only( - top: Spacing.medium, - left: Spacing.medium, - right: Spacing.medium, - bottom: Spacing.xSmall, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SummaryRow( - label: t.event.eventOrder.itemTotal, - value: isCryptoCurrency - ? Web3Utils.formatCryptoCurrency( - pricingInfo.cryptoSubTotal ?? BigInt.zero, - currency: selectedCurrency, - decimals: currencyInfo?.decimals ?? 0, - ) - : NumberUtils.formatCurrency( - amount: pricingInfo.fiatSubTotal ?? 0, - currency: selectedCurrency, - ), - textColor: colorScheme.onPrimary.withOpacity(0.87), - ), - if (pricingInfo.paymentAccounts?.first.fee?.isNotEmpty == - true) ...[ - SizedBox(height: Spacing.xSmall), - SummaryRow( - label: t.event.eventOrder.fee, - value: Web3Utils.formatCryptoCurrency( - pricingInfo.paymentAccounts?.firstOrNull?.cryptoFee ?? - BigInt.zero, - currency: selectedCurrency, - decimals: currencyInfo?.decimals ?? 0, - ), - textColor: colorScheme.onPrimary.withOpacity(0.87), - ), - ], - if (pricingInfo.discount != null && - pricingInfo.promoCode?.isNotEmpty == true) ...[ - SizedBox(height: Spacing.xSmall), - SummaryRow( - label: - '${t.event.eventOrder.promo} [${pricingInfo.promoCode}]', - value: - '-${isCryptoCurrency ? Web3Utils.formatCryptoCurrency( - pricingInfo.cryptoDiscount ?? BigInt.zero, - currency: selectedCurrency, - decimals: currencyInfo?.decimals ?? 0, - ) : NumberUtils.formatCurrency( - amount: pricingInfo.fiatDiscount ?? 0, - currency: selectedCurrency, - )}', - textColor: LemonColor.promoApplied, - ), - ], - ], - ), - ), - SizedBox( - height: 3.w, - ), - Container( - padding: EdgeInsets.symmetric( - vertical: Spacing.medium, - horizontal: Spacing.medium, - ), - decoration: BoxDecoration( - color: colorScheme.onPrimary.withOpacity(0.06), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(LemonRadius.normal), - topRight: Radius.circular(LemonRadius.normal), - ), - ), - child: SummaryRow( - label: t.event.eventOrder.grandTotal, - value: isCryptoCurrency - ? Web3Utils.formatCryptoCurrency( - _totalCryptoAmount, - currency: selectedCurrency, - decimals: currencyInfo?.decimals ?? 0, - ) - : NumberUtils.formatCurrency( - amount: _totalFiatAmount, - currency: selectedCurrency, - attemptedDecimals: currencyInfo?.decimals ?? 2, - ), - textStyle: Typo.mediumPlus.copyWith( - color: colorScheme.onPrimary, - fontWeight: FontWeight.w600, - ), - ), - ), - Transform.flip( - flipY: true, - child: LayoutBuilder( - builder: (context, constraint) => CustomPaint( - size: Size(constraint.maxWidth, 11.w), - painter: TicketWaveCustomPaint(), - ), - ), - ), - ], - ), - ); - } -} - -class SummaryRow extends StatelessWidget { - const SummaryRow({ - super.key, - required this.label, - required this.value, - this.textColor, - this.textStyle, - }); - - final String label; - final String value; - final Color? textColor; - final TextStyle? textStyle; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final customTextStyle = textStyle ?? - Typo.mediumPlus.copyWith( - color: textColor ?? colorScheme.onSecondary, - ); - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: customTextStyle, - ), - Expanded( - child: Padding( - padding: EdgeInsets.only(bottom: 3.w), - child: DottedLine( - dashColor: colorScheme.onPrimary.withOpacity(0.06), - ), - ), - ), - Text( - value, - style: customTextStyle, - ), - ], - ); - } -} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_tickets_summary.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_tickets_summary.dart index 41bcde879..412fa0af4 100644 --- a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_tickets_summary.dart +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_tickets_summary.dart @@ -8,8 +8,10 @@ import 'package:app/core/utils/number_utils.dart'; import 'package:app/core/utils/payment_utils.dart'; import 'package:app/core/utils/web3_utils.dart'; import 'package:app/gen/assets.gen.dart'; + import 'package:app/theme/sizing.dart'; import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -33,23 +35,32 @@ class EventTicketsSummary extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - mainAxisSize: MainAxisSize.min, - children: selectedTickets.map((selectedTicket) { - final selectedTicketType = - ticketTypes.firstWhere((item) => item.id == selectedTicket.id); - final currencyInfo = PaymentUtils.getCurrencyInfo( - pricingInfo, - currency: selectedCurrency, - ); + children: [ + ListView.separated( + shrinkWrap: true, + itemCount: selectedTickets.length, + separatorBuilder: (context, index) => + SizedBox(height: Spacing.xSmall), + itemBuilder: (context, index) { + final selectedTicket = selectedTickets[index]; + final selectedTicketType = + ticketTypes.firstWhere((item) => item.id == selectedTicket.id); + final currencyInfo = PaymentUtils.getCurrencyInfo( + pricingInfo, + currency: selectedCurrency, + ); - return TicketSummaryItem( - ticketType: selectedTicketType, - count: selectedTicket.count, - currency: selectedCurrency, - network: selectedNetwork, - currencyInfo: currencyInfo, - ); - }).toList(), + return TicketSummaryItem( + ticketType: selectedTicketType, + count: selectedTicket.count, + currency: selectedCurrency, + network: selectedNetwork, + currencyInfo: currencyInfo, + ); + }, + ), + // ], + ], ); } } @@ -75,65 +86,71 @@ class TicketSummaryItem extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; final isCrypto = network?.isNotEmpty == true; - return Container( - margin: EdgeInsets.only(bottom: Spacing.xSmall), - padding: EdgeInsets.symmetric(horizontal: Spacing.smMedium), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text('$count x '), - Text(ticketType.title ?? ''), - SizedBox(width: Spacing.extraSmall), - InkWell( - onTap: () => context.router.pop(), - child: Container( - width: 21.w, - height: 21.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(LemonRadius.extraSmall), - color: colorScheme.outline, - ), - child: Center( - child: ThemeSvgIcon( - color: colorScheme.onSurfaceVariant, - builder: (filter) => Assets.icons.icEdit.svg( - colorFilter: filter, - width: Sizing.small / 2, - height: Sizing.small / 2, - ), + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '$count x ${ticketType.title ?? ''}', + style: Typo.medium.copyWith( + color: colorScheme.onSecondary, + ), + ), + SizedBox(width: Spacing.extraSmall), + InkWell( + onTap: () => context.router.pop(), + child: Container( + width: 21.w, + height: 21.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(LemonRadius.extraSmall), + color: colorScheme.outline, + ), + child: Center( + child: ThemeSvgIcon( + color: colorScheme.onSurfaceVariant, + builder: (filter) => Assets.icons.icEdit.svg( + colorFilter: filter, + width: Sizing.small / 2, + height: Sizing.small / 2, ), ), ), ), - const Spacer(), - if (!isCrypto) - Text( - NumberUtils.formatCurrency( - amount: (EventTicketUtils.getTicketPriceByCurrencyAndNetwork( - ticketType: ticketType, - currency: currency, - )?.fiatCost ?? - 0) * - count, - currency: currency, - ), + ), + const Spacer(), + if (!isCrypto) + Text( + NumberUtils.formatCurrency( + amount: (EventTicketUtils.getTicketPriceByCurrencyAndNetwork( + ticketType: ticketType, + currency: currency, + )?.fiatCost ?? + 0) * + count, + currency: currency, ), - if (isCrypto) - Text( - Web3Utils.formatCryptoCurrency( - (EventTicketUtils.getTicketPriceByCurrencyAndNetwork( - ticketType: ticketType, - currency: currency, - network: network, - )?.cryptoCost ?? - BigInt.zero) * - BigInt.from(count), - currency: currency, - decimals: currencyInfo?.decimals ?? 0, - ), + style: Typo.medium.copyWith( + color: colorScheme.onSecondary, + ), + ), + if (isCrypto) + Text( + Web3Utils.formatCryptoCurrency( + (EventTicketUtils.getTicketPriceByCurrencyAndNetwork( + ticketType: ticketType, + currency: currency, + network: network, + )?.cryptoCost ?? + BigInt.zero) * + BigInt.from(count), + currency: currency, + decimals: currencyInfo?.decimals ?? 0, ), - ], - ), + style: Typo.medium.copyWith( + color: colorScheme.onSecondary, + ), + ), + ], ); } } diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_total_price_summary.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_total_price_summary.dart new file mode 100644 index 000000000..bb18af535 --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_total_price_summary.dart @@ -0,0 +1,117 @@ +import 'package:app/core/domain/event/entities/event_tickets_pricing_info.dart'; +import 'package:app/core/utils/number_utils.dart'; +import 'package:app/core/utils/payment_utils.dart'; +import 'package:app/core/utils/web3_utils.dart'; +import 'package:app/i18n/i18n.g.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; +import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; + +class EventTotalPriceSummary extends StatelessWidget { + const EventTotalPriceSummary({ + super.key, + required this.pricingInfo, + required this.selectedCurrency, + required this.selectedNetwork, + }); + + final EventTicketsPricingInfo pricingInfo; + final String? selectedNetwork; + final String selectedCurrency; + + BigInt get _totalCryptoAmount { + return (pricingInfo.cryptoTotal ?? BigInt.zero) + + // add fee if available for EthereumRelay + (pricingInfo.paymentAccounts?.firstOrNull?.cryptoFee ?? BigInt.zero); + } + + double get _totalFiatAmount { + return (pricingInfo.fiatTotal ?? 0) + + (pricingInfo.paymentAccounts?.firstOrNull?.fiatFee ?? 0); + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + final colorScheme = Theme.of(context).colorScheme; + final isCryptoCurrency = selectedNetwork?.isNotEmpty == true; + final currencyInfo = + PaymentUtils.getCurrencyInfo(pricingInfo, currency: selectedCurrency); + + return Column( + children: [ + if (pricingInfo.paymentAccounts?.first.fee?.isNotEmpty == true) ...[ + SummaryRow( + label: t.event.eventOrder.fee, + value: Web3Utils.formatCryptoCurrency( + pricingInfo.paymentAccounts?.firstOrNull?.cryptoFee ?? + BigInt.zero, + currency: selectedCurrency, + decimals: currencyInfo?.decimals ?? 0, + ), + textColor: colorScheme.onSecondary, + ), + ], + SizedBox(height: Spacing.xSmall), + SummaryRow( + label: t.event.eventOrder.grandTotal, + value: isCryptoCurrency + ? Web3Utils.formatCryptoCurrency( + _totalCryptoAmount, + currency: selectedCurrency, + decimals: currencyInfo?.decimals ?? 0, + ) + : NumberUtils.formatCurrency( + amount: _totalFiatAmount, + currency: selectedCurrency, + attemptedDecimals: currencyInfo?.decimals ?? 2, + ), + textStyle: Typo.medium.copyWith( + color: colorScheme.onPrimary, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } +} + +class SummaryRow extends StatelessWidget { + const SummaryRow({ + super.key, + required this.label, + required this.value, + this.textColor, + this.textStyle, + }); + + final String label; + final String value; + final Color? textColor; + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final customTextStyle = textStyle ?? + Typo.medium.copyWith( + color: textColor ?? colorScheme.onSecondary, + ); + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: customTextStyle, + ), + const Spacer(), + Text( + value, + style: customTextStyle, + ), + ], + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/pay_by_crypto_button.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/pay_by_crypto_button.dart deleted file mode 100644 index 01e88c1ae..000000000 --- a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/pay_by_crypto_button.dart +++ /dev/null @@ -1,264 +0,0 @@ -import 'package:app/core/application/event/event_provider_bloc/event_provider_bloc.dart'; -import 'package:app/core/application/event_tickets/buy_tickets_with_crypto_bloc/buy_tickets_with_crypto_bloc.dart'; -import 'package:app/core/application/wallet/wallet_bloc/wallet_bloc.dart'; -import 'package:app/core/domain/event/entities/event.dart'; -import 'package:app/core/domain/event/entities/event_tickets_pricing_info.dart'; -import 'package:app/core/domain/event/input/buy_tickets_input/buy_tickets_input.dart'; -import 'package:app/core/domain/payment/entities/purchasable_item/purchasable_item.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_slide_to_pay.dart'; -import 'package:app/core/presentation/widgets/common/button/linear_gradient_button_widget.dart'; -import 'package:app/core/presentation/widgets/common/slide_to_act/slide_to_act.dart'; -import 'package:app/core/presentation/widgets/loading_widget.dart'; -import 'package:app/core/presentation/widgets/web3/connect_wallet_button.dart'; -import 'package:app/core/presentation/widgets/web3/wallet_connect_active_session.dart'; -import 'package:app/core/service/wallet/wallet_connect_service.dart'; -import 'package:app/core/utils/payment_utils.dart'; -import 'package:app/gen/fonts.gen.dart'; -import 'package:app/i18n/i18n.g.dart'; -import 'package:app/injection/register_module.dart'; -import 'package:app/theme/sizing.dart'; -import 'package:app/theme/spacing.dart'; -import 'package:app/theme/typo.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:web3modal_flutter/web3modal_flutter.dart' as web3modal; - -class PayByCryptoButton extends StatelessWidget { - final EventTicketsPricingInfo pricingInfo; - final String selectedCurrency; - final String? selectedNetwork; - final List selectedTickets; - final bool isFree; - - const PayByCryptoButton({ - super.key, - required this.pricingInfo, - required this.selectedCurrency, - required this.selectedTickets, - this.selectedNetwork, - this.isFree = false, - }); - - @override - Widget build(BuildContext context) { - return PayByCryptoButtonView( - pricingInfo: pricingInfo, - selectedCurrency: selectedCurrency, - selectedNetwork: selectedNetwork, - selectedTickets: selectedTickets, - isFree: isFree, - ); - } -} - -class PayByCryptoButtonView extends StatefulWidget { - final EventTicketsPricingInfo pricingInfo; - final String selectedCurrency; - final String? selectedNetwork; - final List selectedTickets; - final bool isFree; - - const PayByCryptoButtonView({ - super.key, - required this.pricingInfo, - required this.selectedCurrency, - required this.selectedTickets, - this.selectedNetwork, - this.isFree = false, - }); - - @override - State createState() => _PayByCryptoButtonViewState(); -} - -class _PayByCryptoButtonViewState extends State { - final _slideToActionKey = GlobalKey(); - - BigInt get _totalCryptoAmount { - return (widget.pricingInfo.cryptoTotal ?? BigInt.zero) + - // add fee if available for EthereumRelay - (widget.pricingInfo.paymentAccounts?.firstOrNull?.cryptoFee ?? - BigInt.zero); - } - - void clickInitPaymentAndSigned({ - required Event event, - required String userWalletAddress, - }) { - context.read().add( - BuyTicketsWithCryptoEvent.initAndSignPayment( - userWalletAddress: userWalletAddress, - input: BuyTicketsInput( - eventId: event.id ?? '', - accountId: widget.pricingInfo.paymentAccounts?.first.id ?? '', - currency: widget.selectedCurrency, - items: widget.selectedTickets, - total: widget.pricingInfo.total ?? '0', - network: widget.selectedNetwork, - discount: widget.pricingInfo.promoCode, - fee: widget.pricingInfo.paymentAccounts?.first.fee, - ), - ), - ); - } - - void clickMakeTransaction({ - required Event event, - required String userWalletAddress, - }) { - final currencyInfo = PaymentUtils.getCurrencyInfo( - widget.pricingInfo, - currency: widget.selectedCurrency, - ); - - if (currencyInfo == null) return; - - context.read().add( - BuyTicketsWithCryptoEvent.makeTransaction( - from: userWalletAddress, - amount: _totalCryptoAmount, - to: widget.pricingInfo.paymentAccounts?.firstOrNull?.accountInfo - ?.address ?? - '', - currencyInfo: currencyInfo, - currency: widget.selectedCurrency, - eventId: event.id ?? '', - ), - ); - } - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final t = Translations.of(context); - return SafeArea( - child: Container( - padding: EdgeInsets.symmetric( - vertical: Spacing.smMedium, - horizontal: Spacing.smMedium, - ), - decoration: BoxDecoration( - color: colorScheme.background, - border: Border( - top: BorderSide( - width: 2.w, - color: colorScheme.onPrimary.withOpacity(0.06), - ), - ), - ), - child: BlocBuilder( - builder: (context, walletState) { - final event = context.read().event; - - if (widget.isFree) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - EventOrderSlideToPay( - onSlideToPay: () => clickInitPaymentAndSigned( - event: event, - userWalletAddress: '', - ), - slideActionKey: _slideToActionKey, - selectedCurrency: widget.selectedCurrency, - selectedNetwork: widget.selectedNetwork, - pricingInfo: widget.pricingInfo, - ), - ], - ); - } - - if (walletState.activeSession == null) { - return const ConnectWalletButton(); - } - - final w3mService = getIt().w3mService; - final userWalletAddress = w3mService.session?.address ?? ''; - - return BlocBuilder( - builder: (context, state) { - String buttonTitle = ''; - Function()? onPress; - - if (state is BuyTicketsWithCryptoStateFailure) { - _slideToActionKey.currentState?.reset(); - } - - if (state is BuyTicketsWithCryptoStateLoading) { - buttonTitle = t.common.processing; - } - - if (state is BuyTicketsWithCryptoStateIdle) { - buttonTitle = - buttonTitle = t.event.eventCryptoPayment.signPayment; - onPress = () => clickInitPaymentAndSigned( - event: event, - userWalletAddress: userWalletAddress, - ); - } - - if (state is BuyTicketsWithCryptoStateSigned) { - buttonTitle = buttonTitle = t.event.eventCryptoPayment.pay; - onPress = () => clickMakeTransaction( - event: event, - userWalletAddress: userWalletAddress, - ); - } - - if (state is BuyTicketsWithCryptoStateDone) { - return Text( - t.event.eventCryptoPayment.waitForConfirmation, - style: Typo.medium.copyWith( - fontFamily: FontFamily.nohemiVariable, - fontWeight: FontWeight.w600, - ), - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - WalletConnectActiveSessionWidget( - title: t.event.eventPayment.payUsing, - ), - SizedBox(height: Spacing.smMedium), - if (state is BuyTicketsWithCryptoStateLoading) - Loading.defaultLoading(context), - if (state is BuyTicketsWithCryptoStateSigned) - EventOrderSlideToPay( - onSlideToPay: () => onPress?.call(), - slideActionKey: _slideToActionKey, - selectedCurrency: widget.selectedCurrency, - selectedNetwork: widget.selectedNetwork, - pricingInfo: widget.pricingInfo, - ), - if (state is! BuyTicketsWithCryptoStateSigned && - state is! BuyTicketsWithCryptoStateLoading) - LinearGradientButton( - onTap: () { - onPress?.call(); - }, - height: Sizing.large, - radius: BorderRadius.circular(LemonRadius.small * 2), - textStyle: Typo.medium.copyWith( - fontFamily: FontFamily.nohemiVariable, - fontWeight: FontWeight.w600, - color: colorScheme.onPrimary.withOpacity(0.87), - ), - mode: GradientButtonMode.lavenderMode, - label: buttonTitle, - // loadingWhen: state is BuyTicketsWithCryptoStateLoading, - ), - ], - ); - }, - ); - }, - ), - ), - ); - } -} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_slide_to_pay.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_button.dart similarity index 52% rename from lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_slide_to_pay.dart rename to lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_button.dart index ca72e7c43..55be9cac5 100644 --- a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_slide_to_pay.dart +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_button.dart @@ -1,34 +1,27 @@ import 'package:app/core/domain/event/entities/event_tickets_pricing_info.dart'; -import 'package:app/core/presentation/widgets/common/slide_to_act/slide_to_act.dart'; -import 'package:app/core/presentation/widgets/theme_svg_icon_widget.dart'; +import 'package:app/core/presentation/widgets/common/button/linear_gradient_button_widget.dart'; import 'package:app/core/utils/number_utils.dart'; import 'package:app/core/utils/payment_utils.dart'; import 'package:app/core/utils/web3_utils.dart'; -import 'package:app/gen/assets.gen.dart'; -import 'package:app/gen/fonts.gen.dart'; import 'package:app/i18n/i18n.g.dart'; -import 'package:app/theme/color.dart'; -import 'package:app/theme/sizing.dart'; -import 'package:app/theme/typo.dart'; +import 'package:app/router/app_router.gr.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:collection/collection.dart'; -class EventOrderSlideToPay extends StatelessWidget { - const EventOrderSlideToPay({ +class PayButton extends StatelessWidget { + const PayButton({ super.key, - required this.onSlideToPay, - required this.slideActionKey, required this.selectedCurrency, required this.selectedNetwork, + required this.disabled, this.pricingInfo, }); - final Function() onSlideToPay; final EventTicketsPricingInfo? pricingInfo; final String selectedCurrency; final String? selectedNetwork; - final GlobalKey slideActionKey; + final bool disabled; BigInt get _totalCryptoAmount { return (pricingInfo?.cryptoTotal ?? BigInt.zero) + @@ -43,7 +36,6 @@ class EventOrderSlideToPay extends StatelessWidget { @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; final t = Translations.of(context); final currencyInfo = PaymentUtils.getCurrencyInfo(pricingInfo, currency: selectedCurrency); @@ -60,28 +52,14 @@ class EventOrderSlideToPay extends StatelessWidget { attemptedDecimals: currencyInfo?.decimals ?? 2, ); - return SizedBox( - height: 60.w, - child: SlideAction( - key: slideActionKey, - onSubmit: () async { - onSlideToPay(); + return Opacity( + opacity: disabled ? 0.5 : 1, + child: LinearGradientButton.primaryButton( + label: t.event.eventBuyTickets.payAmount(amount: amountText), + onTap: () { + if (disabled) return; + AutoRouter.of(context).push(const EventBuyTicketsProcessingRoute()); }, - text: '${t.event.eventPayment.slideToPay} $amountText', - textStyle: Typo.medium.copyWith( - color: LemonColor.paleViolet, - fontWeight: FontWeight.w600, - fontFamily: FontFamily.nohemiVariable, - ), - outerColor: colorScheme.background, - sliderButtonIcon: ThemeSvgIcon( - color: colorScheme.primary.withOpacity(0.18), - builder: (filter) => Assets.icons.icRoundDoubleArrow.svg( - colorFilter: filter, - width: Sizing.small, - height: Sizing.small, - ), - ), ), ); } diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_by_crypto_footer.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_by_crypto_footer.dart new file mode 100644 index 000000000..49689486c --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_by_crypto_footer.dart @@ -0,0 +1,127 @@ +import 'package:app/core/application/wallet/wallet_bloc/wallet_bloc.dart'; +import 'package:app/core/domain/event/entities/event_tickets_pricing_info.dart'; +import 'package:app/core/domain/payment/entities/purchasable_item/purchasable_item.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_button.dart'; +import 'package:app/core/presentation/widgets/web3/connect_wallet_button.dart'; +import 'package:app/core/presentation/widgets/web3/wallet_connect_active_session.dart'; +import 'package:app/i18n/i18n.g.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class PayByCryptoFooter extends StatelessWidget { + final EventTicketsPricingInfo pricingInfo; + final String selectedCurrency; + final String? selectedNetwork; + final List selectedTickets; + final bool isFree; + final bool disabled; + + const PayByCryptoFooter({ + super.key, + required this.pricingInfo, + required this.selectedCurrency, + required this.selectedTickets, + this.selectedNetwork, + this.isFree = false, + this.disabled = false, + }); + + @override + Widget build(BuildContext context) { + return PayByCryptoFooterView( + pricingInfo: pricingInfo, + selectedCurrency: selectedCurrency, + selectedNetwork: selectedNetwork, + selectedTickets: selectedTickets, + isFree: isFree, + disabled: disabled, + ); + } +} + +class PayByCryptoFooterView extends StatefulWidget { + final EventTicketsPricingInfo pricingInfo; + final String selectedCurrency; + final String? selectedNetwork; + final List selectedTickets; + final bool isFree; + final bool disabled; + + const PayByCryptoFooterView({ + super.key, + required this.pricingInfo, + required this.selectedCurrency, + required this.selectedTickets, + this.selectedNetwork, + this.isFree = false, + this.disabled = false, + }); + + @override + State createState() => _PayByCryptoFooterViewState(); +} + +class _PayByCryptoFooterViewState extends State { + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final t = Translations.of(context); + return SafeArea( + child: Container( + padding: EdgeInsets.symmetric( + vertical: Spacing.smMedium, + horizontal: Spacing.smMedium, + ), + decoration: BoxDecoration( + color: colorScheme.background, + border: Border( + top: BorderSide( + width: 2.w, + color: colorScheme.onPrimary.withOpacity(0.06), + ), + ), + ), + child: BlocBuilder( + builder: (context, walletState) { + if (widget.isFree) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PayButton( + disabled: widget.disabled, + selectedCurrency: widget.selectedCurrency, + selectedNetwork: widget.selectedNetwork, + pricingInfo: widget.pricingInfo, + ), + ], + ); + } + + if (walletState.activeSession == null) { + return const ConnectWalletButton(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + WalletConnectActiveSessionWidget( + title: t.event.eventPayment.payUsing, + ), + SizedBox(height: Spacing.smMedium), + PayButton( + disabled: widget.disabled, + selectedCurrency: widget.selectedCurrency, + selectedNetwork: widget.selectedNetwork, + pricingInfo: widget.pricingInfo, + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_summary_footer.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_by_stripe_footer.dart similarity index 78% rename from lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_summary_footer.dart rename to lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_by_stripe_footer.dart index aaeb4209a..28a352835 100644 --- a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_summary_footer.dart +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_by_stripe_footer.dart @@ -2,9 +2,8 @@ import 'package:app/core/application/payment/select_payment_card_cubit/select_pa import 'package:app/core/domain/event/entities/event_tickets_pricing_info.dart'; import 'package:app/core/domain/payment/entities/payment_card/payment_card.dart'; import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_card_tile.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/event_order_slide_to_pay.dart'; -import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/select_card_button.dart'; -import 'package:app/core/presentation/widgets/common/slide_to_act/slide_to_act.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/pay_button.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/select_card_button.dart'; import 'package:app/router/app_router.gr.dart'; import 'package:app/theme/spacing.dart'; import 'package:auto_route/auto_route.dart'; @@ -12,27 +11,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -class EventOrderSummaryFooter extends StatelessWidget { - const EventOrderSummaryFooter({ +class PayByStripeFooter extends StatelessWidget { + const PayByStripeFooter({ super.key, this.pricingInfo, - required this.onSlideToPay, - required this.slideActionKey, required this.selectedCurrency, this.selectedNetwork, this.onSelectCard, this.onCardAdded, this.isFree = false, + this.disabled = false, }); - - final Function() onSlideToPay; final EventTicketsPricingInfo? pricingInfo; - final GlobalKey slideActionKey; final Function(PaymentCard paymentCard)? onSelectCard; final Function(PaymentCard paymentCard)? onCardAdded; final String selectedCurrency; final String? selectedNetwork; final bool isFree; + final bool disabled; String get stripePublishableKey { return pricingInfo?.paymentAccounts?.isNotEmpty == true @@ -64,10 +60,9 @@ class EventOrderSummaryFooter extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, children: [ - EventOrderSlideToPay( - onSlideToPay: onSlideToPay, + PayButton( + disabled: disabled, pricingInfo: pricingInfo, - slideActionKey: slideActionKey, selectedCurrency: selectedCurrency, selectedNetwork: selectedNetwork, ), @@ -99,13 +94,9 @@ class EventOrderSummaryFooter extends StatelessWidget { publishableKey: stripePublishableKey, onCardAdded: onCardAdded, onSelectCard: onSelectCard, - buyButton: EventOrderSlideToPay( - onSlideToPay: () async { - await AutoRouter.of(context).pop(); - onSlideToPay(); - }, + buyButton: PayButton( + disabled: disabled, pricingInfo: pricingInfo, - slideActionKey: GlobalKey(), selectedCurrency: selectedCurrency, selectedNetwork: selectedNetwork, ), @@ -115,10 +106,9 @@ class EventOrderSummaryFooter extends StatelessWidget { paymentCard: selectedPaymentCard, ), SizedBox(height: Spacing.smMedium), - EventOrderSlideToPay( - onSlideToPay: onSlideToPay, + PayButton( + disabled: disabled, pricingInfo: pricingInfo, - slideActionKey: slideActionKey, selectedCurrency: selectedCurrency, selectedNetwork: selectedNetwork, ), diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/select_card_button.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/select_card_button.dart similarity index 100% rename from lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/select_card_button.dart rename to lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/payment_footer/select_card_button.dart diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/add_promo_code_input.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/add_promo_code_input.dart similarity index 100% rename from lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/add_promo_code_input.dart rename to lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/add_promo_code_input.dart diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/promo_code_input_bottomsheet.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/promo_code_input_bottomsheet.dart new file mode 100644 index 000000000..e3779fd2a --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/promo_code_input_bottomsheet.dart @@ -0,0 +1,106 @@ +import 'package:app/core/presentation/widgets/bottomsheet_grabber/bottomsheet_grabber.dart'; +import 'package:app/core/presentation/widgets/common/button/linear_gradient_button_widget.dart'; +import 'package:app/core/presentation/widgets/lemon_text_field.dart'; +import 'package:app/core/utils/text_formatter/upper_case_text_formatter.dart'; +import 'package:app/i18n/i18n.g.dart'; +import 'package:app/theme/color.dart'; +import 'package:app/theme/sizing.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; +import 'package:flutter/material.dart'; + +class PromoCodeInputBottomsheet extends StatefulWidget { + final Function({ + String? promoCode, + })? onPressApply; + + const PromoCodeInputBottomsheet({ + super.key, + this.onPressApply, + }); + + @override + State createState() => + _PromoCodeInputBottomsheetState(); +} + +class _PromoCodeInputBottomsheetState extends State { + final promoTextController = TextEditingController(); + bool isValid = false; + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + final colorScheme = Theme.of(context).colorScheme; + return SafeArea( + child: Container( + padding: EdgeInsets.only( + left: Spacing.small, + right: Spacing.small, + bottom: MediaQuery.of(context).viewInsets.bottom != 0 + ? MediaQuery.of(context).viewInsets.bottom + Spacing.smMedium + : 0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const BottomSheetGrabber(), + SizedBox(height: Spacing.medium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + t.event.eventBuyTickets.discount, + style: Typo.large.copyWith( + fontWeight: FontWeight.w600, + color: colorScheme.onPrimary, + ), + ), + SizedBox(height: Spacing.superExtraSmall), + Text( + t.event.eventBuyTickets.discountDescription, + style: Typo.medium.copyWith( + color: colorScheme.onSecondary, + ), + ), + SizedBox(height: Spacing.medium), + SizedBox( + height: Sizing.xLarge, + child: LemonTextField( + filled: true, + fillColor: LemonColor.chineseBlack, + inputFormatters: [ + UpperCaseTextFormatter(), + ], + controller: promoTextController, + hintText: t.event.eventBuyTickets.enterPromoCode, + onChange: (v) { + setState(() { + isValid = v.isNotEmpty; + }); + }, + ), + ), + SizedBox(height: Spacing.medium), + Opacity( + opacity: isValid ? 1 : 0.5, + child: LinearGradientButton.primaryButton( + label: t.common.actions.apply, + onTap: () { + if (promoTextController.text.isEmpty) { + return; + } + widget.onPressApply + ?.call(promoCode: promoTextController.text); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/promo_code_summary.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/promo_code_summary.dart new file mode 100644 index 000000000..a823b7a7a --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/promo_code_summary.dart @@ -0,0 +1,108 @@ +import 'package:app/core/domain/event/entities/event_tickets_pricing_info.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/promo_code/promo_code_input_bottomsheet.dart'; +import 'package:app/core/presentation/widgets/theme_svg_icon_widget.dart'; +import 'package:app/gen/assets.gen.dart'; +import 'package:app/i18n/i18n.g.dart'; +import 'package:app/theme/color.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class PromoCodeSummary extends StatelessWidget { + const PromoCodeSummary({ + super.key, + required this.pricingInfo, + this.onPressRemove, + this.onPressApply, + }); + + final EventTicketsPricingInfo pricingInfo; + final Function()? onPressRemove; + final Function({ + String? promoCode, + })? onPressApply; + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + final colorScheme = Theme.of(context).colorScheme; + if (pricingInfo.promoCode == null || + pricingInfo.promoCode?.isEmpty == true) { + return InkWell( + onTap: () { + showCupertinoModalBottomSheet( + useRootNavigator: true, + context: context, + bounce: true, + backgroundColor: LemonColor.atomicBlack, + barrierColor: Colors.black.withOpacity(0.5), + builder: (context) => PromoCodeInputBottomsheet( + onPressApply: ({ + promoCode, + }) { + onPressApply?.call(promoCode: promoCode); + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ); + }, + child: Text( + t.event.eventBuyTickets.addDiscountCode, + style: Typo.medium.copyWith( + color: LemonColor.paleViolet, + ), + ), + ); + } + + return Row( + children: [ + Text( + t.event.eventBuyTickets.discountCode, + style: Typo.medium.copyWith( + color: colorScheme.onSecondary, + ), + ), + const Spacer(), + InkWell( + onTap: () { + onPressApply?.call(promoCode: null); + }, + child: Row( + children: [ + Text( + pricingInfo.promoCode ?? '', + style: Typo.medium.copyWith( + color: LemonColor.malachiteGreen, + ), + ), + SizedBox(width: Spacing.extraSmall), + Container( + width: 18.w, + height: 18.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18.r), + border: Border.all( + color: colorScheme.outline, + width: 0.5.w, + ), + ), + child: Center( + child: ThemeSvgIcon( + builder: (filter) => Assets.icons.icClose.svg( + colorFilter: filter, + width: 12.w, + height: 12.w, + ), + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_application_form.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_application_form.dart new file mode 100644 index 000000000..199ce4334 --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_application_form.dart @@ -0,0 +1,20 @@ +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_application_questions_form.dart'; +import 'package:app/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_profle_fields_form.dart'; +import 'package:flutter/material.dart'; + +class RSVPApplicationForm extends StatelessWidget { + const RSVPApplicationForm({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Column( + mainAxisSize: MainAxisSize.min, + children: [ + RSVPProfileFieldsForm(), + RSVPApplicationQuestionsForm(), + ], + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_application_questions_form.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_application_questions_form.dart new file mode 100644 index 000000000..b9425f7e2 --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_application_questions_form.dart @@ -0,0 +1,75 @@ +import 'package:app/core/application/auth/auth_bloc.dart'; +import 'package:app/core/application/event/event_application_form_bloc/event_application_form_bloc.dart'; +import 'package:app/core/application/event/event_provider_bloc/event_provider_bloc.dart'; +import 'package:app/core/domain/event/entities/event_application_question.dart'; +import 'package:app/core/domain/user/entities/user.dart'; +import 'package:app/core/presentation/widgets/lemon_text_field.dart'; +import 'package:app/theme/color.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:collection/collection.dart'; + +class RSVPApplicationQuestionsForm extends StatelessWidget { + const RSVPApplicationQuestionsForm({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final event = context.read().event; + User? user = context.watch().state.maybeWhen( + authenticated: (user) => user, + orElse: () => null, + ); + List applicationQuestions = + (event.applicationQuestions ?? []).toList(); + if (user == null) { + return const SizedBox(); + } + return BlocBuilder( + builder: (context, state) { + final colorScheme = Theme.of(context).colorScheme; + return Column( + children: applicationQuestions.map((applicationQuestion) { + return Column( + children: [ + LemonTextField( + filled: true, + fillColor: LemonColor.atomicBlack, + label: applicationQuestion.question, + labelStyle: Typo.medium.copyWith( + color: colorScheme.onPrimary, + fontWeight: FontWeight.w500, + ), + hintText: '', + initialText: state.answers + .firstWhereOrNull( + (answer) => + answer.question == applicationQuestion.id, + ) + ?.answer ?? + '', + onChange: (value) { + context.read().add( + EventApplicationFormBlocEvent.updateAnswer( + event: event, + questionId: applicationQuestion.id ?? '', + answer: value, + ), + ); + }, + showRequired: applicationQuestion.isRequired, + ), + SizedBox( + height: Spacing.smMedium, + ), + ], + ); + }).toList(), + ); + }, + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_profle_fields_form.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_profle_fields_form.dart new file mode 100644 index 000000000..8c8ce291c --- /dev/null +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/event_tickets_summary_page/widgets/rsvp_application_form/rsvp_profle_fields_form.dart @@ -0,0 +1,153 @@ +import 'package:app/core/application/auth/auth_bloc.dart'; +import 'package:app/core/application/event/event_application_form_bloc/event_application_form_bloc.dart'; +import 'package:app/core/application/event/event_provider_bloc/event_provider_bloc.dart'; +import 'package:app/core/domain/common/common_enums.dart'; +import 'package:app/core/domain/event/entities/event_application_profile_field.dart'; +import 'package:app/core/domain/user/entities/user.dart'; +import 'package:app/core/presentation/pages/edit_profile/widgets/edit_profile_field_item.dart'; +import 'package:app/core/presentation/widgets/loading_widget.dart'; +import 'package:app/theme/color.dart'; +import 'package:app/theme/spacing.dart'; +import 'package:app/theme/typo.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:matrix/matrix.dart' as matrix; +import 'package:app/core/utils/date_utils.dart' as date_utils; + +class RSVPProfileFieldsForm extends StatefulWidget { + const RSVPProfileFieldsForm({ + super.key, + }); + + @override + State createState() => _RSVPProfileFieldsFormState(); +} + +class _RSVPProfileFieldsFormState extends State { + final birthDayCtrl = TextEditingController(); + + @override + void initState() { + super.initState(); + final fieldState = + context.read().state.fieldsState; + if (fieldState.tryGet(ProfileFieldKey.dateOfBirth.fieldKey) != null && + fieldState.tryGet(ProfileFieldKey.dateOfBirth.fieldKey) != '') { + String dateOfBirthValue = + fieldState.tryGet(ProfileFieldKey.dateOfBirth.fieldKey).toString(); + birthDayCtrl.text = + date_utils.DateUtils.formatDateTimeToDDMMYYYY(dateOfBirthValue); + } + } + + @override + Widget build(BuildContext context) { + final event = context.read().event; + User? user = context.watch().state.maybeWhen( + authenticated: (user) => user, + orElse: () => null, + ); + List applicationProfileFields = + (event.applicationProfileFields ?? []).toList(); + if (user == null) { + return const SizedBox(); + } + final colorScheme = Theme.of(context).colorScheme; + return BlocBuilder( + builder: (context, state) { + if (state.isInitialized == true) { + return Loading.defaultLoading(context); + } + return Column( + children: applicationProfileFields.map((applicationProfileField) { + final profileFieldKey = ProfileFieldKey.values.firstWhere( + (element) => element.fieldKey == applicationProfileField.field, + orElse: () => ProfileFieldKey.unknown, + ); + String? value = + state.fieldsState.tryGet(profileFieldKey.fieldKey).toString(); + if (profileFieldKey == ProfileFieldKey.unknown) { + return const SizedBox(); + } + return Column( + children: [ + // Special handle for date picker + if (profileFieldKey == ProfileFieldKey.dateOfBirth) + Focus( + child: EditProfileFieldItem( + controller: birthDayCtrl, + profileFieldKey: profileFieldKey, + onChange: (value) { + birthDayCtrl.text = value; + }, + onDateSelect: (selectedDate) { + birthDayCtrl.text = + date_utils.DateUtils.toLocalDateString( + selectedDate, + ); + context.read().add( + EventApplicationFormBlocEvent.updateField( + key: applicationProfileField.field ?? '', + value: selectedDate.toUtc().toIso8601String(), + event: event, + ), + ); + }, + value: value, + showRequired: applicationProfileField.required, + backgroundColor: LemonColor.atomicBlack, + labelStyle: Typo.medium.copyWith( + color: colorScheme.onPrimary, + ), + ), + onFocusChange: (hasFocus) { + if (!hasFocus) { + // On blur + birthDayCtrl.text = + date_utils.DateUtils.toLocalDateString( + date_utils.DateUtils.parseDateString( + birthDayCtrl.text, + ), + ); + context.read().add( + EventApplicationFormBlocEvent.updateField( + key: applicationProfileField.field ?? '', + value: date_utils.DateUtils.parseDateString( + (birthDayCtrl.text), + ).toIso8601String(), + event: event, + ), + ); + } + }, + ) + else + EditProfileFieldItem( + profileFieldKey: profileFieldKey, + onChange: (value) { + context.read().add( + EventApplicationFormBlocEvent.updateField( + key: applicationProfileField.field ?? '', + value: value, + event: event, + ), + ); + }, + value: value, + showRequired: applicationProfileField.required, + backgroundColor: LemonColor.atomicBlack, + labelStyle: Typo.medium.copyWith( + color: colorScheme.onPrimary, + ), + ), + SizedBox( + height: Spacing.smMedium, + ), + ], + ); + }).toList(), + ); + }, + ); + } +} diff --git a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/select_tickets_page/widgets/select_ticket_item.dart b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/select_tickets_page/widgets/select_ticket_item.dart index 0ffbc39cc..3431ee897 100644 --- a/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/select_tickets_page/widgets/select_ticket_item.dart +++ b/lib/core/presentation/pages/event_tickets/event_buy_tickets_page/sub_pages/select_tickets_page/widgets/select_ticket_item.dart @@ -50,9 +50,7 @@ class SelectTicketItem extends StatelessWidget { required String currency, String? network, }) { - if (newCount < (ticketType.limit ?? 0)) { - onCountChange(newCount, currency, network); - } + onCountChange(newCount, currency, network); } void minus({ diff --git a/lib/core/presentation/widgets/animation/circular_countdown_timer_widget/circular_countdown_timer.dart b/lib/core/presentation/widgets/animation/circular_countdown_timer_widget/circular_countdown_timer.dart new file mode 100644 index 000000000..f1fad4ecc --- /dev/null +++ b/lib/core/presentation/widgets/animation/circular_countdown_timer_widget/circular_countdown_timer.dart @@ -0,0 +1,438 @@ +library circular_countdown_timer; + +import 'package:flutter/material.dart'; + +import 'package:app/core/presentation/widgets/animation/circular_countdown_timer_widget/countdown_text_format.dart'; +import 'package:app/core/presentation/widgets/animation/circular_countdown_timer_widget/custom_timer_painter.dart'; + +export 'countdown_text_format.dart'; + +/// Create a Circular Countdown Timer. +class CircularCountDownTimer extends StatefulWidget { + /// Filling Color for Countdown Widget. + final Color fillColor; + + /// Filling Gradient for Countdown Widget. + final Gradient? fillGradient; + + /// Ring Color for Countdown Widget. + final Color ringColor; + + /// Ring Gradient for Countdown Widget. + final Gradient? ringGradient; + + /// Background Color for Countdown Widget. + final Color? backgroundColor; + + /// Background Gradient for Countdown Widget. + final Gradient? backgroundGradient; + + /// This Callback will execute when the Countdown Ends. + final VoidCallback? onComplete; + + /// This Callback will execute when the Countdown Starts. + final VoidCallback? onStart; + + /// This Callback will execute when the Countdown Changes. + final ValueChanged? onChange; + + /// Countdown duration in Seconds. + final int duration; + + /// Countdown initial elapsed Duration in Seconds. + final int initialDuration; + + /// Width of the Countdown Widget. + final double width; + + /// Height of the Countdown Widget. + final double height; + + /// Border Thickness of the Countdown Ring. + final double strokeWidth; + + /// Begin and end contours with a flat edge and no extension. + final StrokeCap strokeCap; + + /// Text Style for Countdown Text. + final TextStyle? textStyle; + + /// Text Align for Countdown Text. + final TextAlign textAlign; + + /// Format for the Countdown Text. + final String? textFormat; + + /// Handles Countdown Timer (true for Reverse Countdown (max to 0), false for Forward Countdown (0 to max)). + final bool isReverse; + + /// Handles Animation Direction (true for Reverse Animation, false for Forward Animation). + final bool isReverseAnimation; + + /// Handles visibility of the Countdown Text. + final bool isTimerTextShown; + + /// Controls (i.e Start, Pause, Resume, Restart) the Countdown Timer. + final CountDownController? controller; + + /// Handles the timer start. + final bool autoStart; + + /* + * Function to format the text. + * Allows you to format the current duration to any String. + * It also provides the default function in case you want to format specific moments + as in reverse when reaching '0' show 'GO', and for the rest of the instances follow + the default behavior. + */ + final Function( + Function(Duration duration) defaultFormatterFunction, + Duration duration, + )? timeFormatterFunction; + + const CircularCountDownTimer({ + required this.width, + required this.height, + required this.duration, + required this.fillColor, + required this.ringColor, + this.timeFormatterFunction, + this.backgroundColor, + this.fillGradient, + this.ringGradient, + this.backgroundGradient, + this.initialDuration = 0, + this.isReverse = false, + this.isReverseAnimation = false, + this.onComplete, + this.onStart, + this.onChange, + this.strokeWidth = 5.0, + this.strokeCap = StrokeCap.butt, + this.textStyle, + this.textAlign = TextAlign.left, + super.key, + this.isTimerTextShown = true, + this.autoStart = true, + this.textFormat, + this.controller, + }) : assert(initialDuration <= duration); + + @override + CircularCountDownTimerState createState() => CircularCountDownTimerState(); +} + +class CircularCountDownTimerState extends State + with TickerProviderStateMixin { + AnimationController? _controller; + Animation? _countDownAnimation; + CountDownController? countDownController; + + String get time { + String timeStamp = ""; + if (widget.isReverse && + !widget.autoStart && + !countDownController!.isStarted.value) { + if (widget.timeFormatterFunction != null) { + timeStamp = Function.apply( + widget.timeFormatterFunction!, + [_getTime, Duration(seconds: widget.duration)], + ).toString(); + } else { + timeStamp = _getTime(Duration(seconds: widget.duration)); + } + } else { + Duration? duration = _controller!.duration! * _controller!.value; + if (widget.timeFormatterFunction != null) { + timeStamp = + Function.apply(widget.timeFormatterFunction!, [_getTime, duration]) + .toString(); + } else { + timeStamp = _getTime(duration); + } + } + if (widget.onChange != null) widget.onChange!(timeStamp); + + return timeStamp; + } + + void _setAnimation() { + if (widget.autoStart) { + if (widget.isReverse) { + _controller!.reverse(from: 1); + } else { + _controller!.forward(); + } + } + } + + void _setAnimationDirection() { + // if ((widget.isReverse && !widget.isReverseAnimation) || + // (widget.isReverse && widget.isReverseAnimation)) { + // _countDownAnimation = + // Tween(begin: 1, end: 0).animate(_controller!); + // } else if (!widget.isReverse && widget.isReverseAnimation) { + // _countDownAnimation = + // Tween(begin: 0, end: 1).animate(_controller!); + // } + if ((!widget.isReverse && widget.isReverseAnimation) || + (widget.isReverse && !widget.isReverseAnimation)) { + _countDownAnimation = + Tween(begin: 1, end: 0).animate(_controller!); + } + } + + void _setController() { + countDownController?._state = this; + countDownController?._isReverse = widget.isReverse; + countDownController?._initialDuration = widget.initialDuration; + countDownController?._duration = widget.duration; + countDownController?.isStarted.value = widget.autoStart; + + if (widget.initialDuration > 0 && widget.autoStart) { + if (widget.isReverse) { + _controller?.value = 1 - (widget.initialDuration / widget.duration); + } else { + _controller?.value = (widget.initialDuration / widget.duration); + } + + countDownController?.start(); + } + } + + String _getTime(Duration duration) { + // For HH:mm:ss format + if (widget.textFormat == CountdownTextFormat.HH_MM_SS) { + return '${duration.inHours.toString().padLeft(2, '0')}:${(duration.inMinutes % 60).toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}'; + } + // For mm:ss format + else if (widget.textFormat == CountdownTextFormat.MM_SS) { + return '${(duration.inMinutes % 60).toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}'; + } + // For ss format + else if (widget.textFormat == CountdownTextFormat.SS) { + return (duration.inSeconds).toString().padLeft(2, '0'); + } + // For s format + else if (widget.textFormat == CountdownTextFormat.S) { + return '${(duration.inSeconds)}'; + } else { + // Default format + return _defaultFormat(duration); + } + } + + _defaultFormat(Duration duration) { + if (duration.inHours != 0) { + return '${duration.inHours.toString().padLeft(2, '0')}:${(duration.inMinutes % 60).toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}'; + } else if (duration.inMinutes != 0) { + return '${(duration.inMinutes % 60).toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}'; + } else { + return '${duration.inSeconds % 60}'; + } + } + + void _onStart() { + if (widget.onStart != null) widget.onStart!(); + } + + void _onComplete() { + if (widget.onComplete != null) widget.onComplete!(); + } + + @override + void initState() { + countDownController = widget.controller ?? CountDownController(); + super.initState(); + _controller = AnimationController( + vsync: this, + duration: Duration(seconds: widget.duration), + ); + + _controller!.addStatusListener((status) { + switch (status) { + case AnimationStatus.forward: + _onStart(); + break; + + case AnimationStatus.reverse: + _onStart(); + break; + + case AnimationStatus.dismissed: + _onComplete(); + break; + case AnimationStatus.completed: + + /// [AnimationController]'s value is manually set to [1.0] that's why [AnimationStatus.completed] is invoked here this animation is [isReverse] + /// Only call the [_onComplete] block when the animation is not reversed. + if (!widget.isReverse) _onComplete(); + break; + default: + // Do nothing + } + }); + + _setAnimation(); + _setAnimationDirection(); + _setController(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: widget.width, + height: widget.height, + child: AnimatedBuilder( + animation: _controller!, + builder: (context, child) { + return Align( + child: AspectRatio( + aspectRatio: 1.0, + child: Stack( + children: [ + Positioned.fill( + child: CustomPaint( + painter: CustomTimerPainter( + animation: _countDownAnimation ?? _controller, + fillColor: widget.fillColor, + fillGradient: widget.fillGradient, + ringColor: widget.ringColor, + ringGradient: widget.ringGradient, + strokeWidth: widget.strokeWidth, + strokeCap: widget.strokeCap, + isReverse: widget.isReverse, + isReverseAnimation: widget.isReverseAnimation, + backgroundColor: widget.backgroundColor, + backgroundGradient: widget.backgroundGradient, + ), + ), + ), + widget.isTimerTextShown + ? Align( + alignment: FractionalOffset.center, + child: Text( + time, + style: widget.textStyle ?? + const TextStyle( + fontSize: 16.0, + color: Colors.black, + ), + textAlign: widget.textAlign, + ), + ) + : Container(), + ], + ), + ), + ); + }, + ), + ); + } + + @override + void dispose() { + _controller!.stop(); + _controller!.dispose(); + super.dispose(); + } +} + +/// Controls (i.e Start, Pause, Resume, Restart) the Countdown Timer. +class CountDownController { + CircularCountDownTimerState? _state; + bool? _isReverse; + ValueNotifier isStarted = ValueNotifier(false), + isPaused = ValueNotifier(false), + isResumed = ValueNotifier(false), + isRestarted = ValueNotifier(false); + int? _initialDuration, _duration; + + /// This Method Starts the Countdown Timer + void start() { + if (_isReverse != null && _state != null && _state?._controller != null) { + if (_isReverse!) { + _state?._controller?.reverse( + from: + _initialDuration == 0 ? 1 : 1 - (_initialDuration! / _duration!), + ); + } else { + _state?._controller?.forward( + from: _initialDuration == 0 ? 0 : (_initialDuration! / _duration!), + ); + } + isStarted.value = true; + isPaused.value = false; + isResumed.value = false; + isRestarted.value = false; + } + } + + /// This Method Pauses the Countdown Timer + void pause() { + if (_state != null && _state?._controller != null) { + _state?._controller?.stop(canceled: false); + isPaused.value = true; + isRestarted.value = false; + isResumed.value = false; + } + } + + /// This Method Resumes the Countdown Timer + void resume() { + if (_isReverse != null && _state != null && _state?._controller != null) { + if (_isReverse!) { + _state?._controller?.reverse(from: _state!._controller!.value); + } else { + _state?._controller?.forward(from: _state!._controller!.value); + } + isResumed.value = true; + isRestarted.value = false; + isPaused.value = false; + } + } + + /// This Method Restarts the Countdown Timer, + /// Here optional int parameter **duration** is the updated duration for countdown timer + + void restart({int? duration}) { + if (_isReverse != null && _state != null && _state?._controller != null) { + _state?._controller!.duration = Duration( + seconds: duration ?? _state!._controller!.duration!.inSeconds, + ); + if (_isReverse!) { + _state?._controller?.reverse(from: 1); + } else { + _state?._controller?.forward(from: 0); + } + isStarted.value = true; + isRestarted.value = true; + isPaused.value = false; + isResumed.value = false; + } + } + + /// This Method resets the Countdown Timer + void reset() { + if (_state != null && _state?._controller != null) { + _state?._controller?.reset(); + isStarted.value = _state?.widget.autoStart ?? false; + isRestarted.value = false; + isPaused.value = false; + isResumed.value = false; + } + } + + /// This Method returns the **Current Time** of Countdown Timer i.e + /// Time Used in terms of **Forward Countdown** and Time Left in terms of **Reverse Countdown** + + String? getTime() { + if (_state != null && _state?._controller != null) { + return _state?._getTime( + _state!._controller!.duration! * _state!._controller!.value, + ); + } + return ""; + } +} diff --git a/lib/core/presentation/widgets/animation/circular_countdown_timer_widget/countdown_text_format.dart b/lib/core/presentation/widgets/animation/circular_countdown_timer_widget/countdown_text_format.dart new file mode 100644 index 000000000..b10e88e90 --- /dev/null +++ b/lib/core/presentation/widgets/animation/circular_countdown_timer_widget/countdown_text_format.dart @@ -0,0 +1,7 @@ +// ignore_for_file: constant_identifier_names +class CountdownTextFormat { + static const String HH_MM_SS = "HH:mm:ss"; + static const String MM_SS = "mm:ss"; + static const String SS = "ss"; + static const String S = "s"; +} diff --git a/lib/core/presentation/widgets/animation/circular_countdown_timer_widget/custom_timer_painter.dart b/lib/core/presentation/widgets/animation/circular_countdown_timer_widget/custom_timer_painter.dart new file mode 100644 index 000000000..1634f65d1 --- /dev/null +++ b/lib/core/presentation/widgets/animation/circular_countdown_timer_widget/custom_timer_painter.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class CustomTimerPainter extends CustomPainter { + CustomTimerPainter({ + this.animation, + this.fillColor, + this.fillGradient, + this.ringColor, + this.ringGradient, + this.strokeWidth, + this.strokeCap, + this.backgroundColor, + this.isReverse, + this.isReverseAnimation, + this.backgroundGradient, + }) : super(repaint: animation); + + final Animation? animation; + final Color? fillColor, ringColor, backgroundColor; + final double? strokeWidth; + final StrokeCap? strokeCap; + final bool? isReverse, isReverseAnimation; + final Gradient? fillGradient, ringGradient, backgroundGradient; + + @override + void paint(Canvas canvas, Size size) { + Paint paint = Paint() + ..color = ringColor! + ..strokeWidth = strokeWidth! + ..strokeCap = strokeCap! + ..style = PaintingStyle.stroke; + + if (ringGradient != null) { + final rect = Rect.fromCircle( + center: size.center(Offset.zero), + radius: size.width / 2, + ); + paint.shader = ringGradient!.createShader(rect); + } else { + paint.shader = null; + } + + canvas.drawCircle(size.center(Offset.zero), size.width / 2, paint); + double progress = (animation!.value) * 2 * math.pi; + double startAngle = math.pi * 1.5; + + if ((!isReverse! && isReverseAnimation!) || + (isReverse! && isReverseAnimation!)) { + progress = progress * -1; + startAngle = -math.pi / 2; + } + + if (fillGradient != null) { + final rect = Rect.fromCircle( + center: size.center(Offset.zero), + radius: size.width / 2, + ); + paint.shader = fillGradient!.createShader(rect); + } else { + paint.shader = null; + paint.color = fillColor!; + } + + canvas.drawArc(Offset.zero & size, startAngle, progress, false, paint); + + if (backgroundColor != null || backgroundGradient != null) { + final backgroundPaint = Paint(); + + if (backgroundGradient != null) { + final rect = Rect.fromCircle( + center: size.center(Offset.zero), + radius: size.width / 2.2, + ); + backgroundPaint.shader = backgroundGradient!.createShader(rect); + } else { + backgroundPaint.color = backgroundColor!; + } + canvas.drawCircle( + size.center(Offset.zero), + size.width / 2.2, + backgroundPaint, + ); + } + } + + @override + bool shouldRepaint(CustomTimerPainter oldDelegate) { + return animation!.value != oldDelegate.animation!.value || + ringColor != oldDelegate.ringColor || + fillColor != oldDelegate.fillColor; + } +} diff --git a/lib/core/presentation/widgets/common/circular_loading/circular_loading_widget.dart b/lib/core/presentation/widgets/animation/circular_loading_widget.dart similarity index 90% rename from lib/core/presentation/widgets/common/circular_loading/circular_loading_widget.dart rename to lib/core/presentation/widgets/animation/circular_loading_widget.dart index 5a6709b35..aaf75d820 100644 --- a/lib/core/presentation/widgets/common/circular_loading/circular_loading_widget.dart +++ b/lib/core/presentation/widgets/animation/circular_loading_widget.dart @@ -4,14 +4,14 @@ import 'package:app/theme/sizing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -class CircularLoading extends StatefulWidget { - const CircularLoading({super.key}); +class CircularLoadingWidget extends StatefulWidget { + const CircularLoadingWidget({super.key}); @override - State createState() => CircularLoadingState(); + State createState() => CircularLoadingWidgetState(); } -class CircularLoadingState extends State +class CircularLoadingWidgetState extends State with TickerProviderStateMixin { late AnimationController _animationContrl; late Animation _animation; diff --git a/lib/core/presentation/widgets/common/button/linear_gradient_button_widget.dart b/lib/core/presentation/widgets/common/button/linear_gradient_button_widget.dart index b25bf8ed1..805b53d14 100644 --- a/lib/core/presentation/widgets/common/button/linear_gradient_button_widget.dart +++ b/lib/core/presentation/widgets/common/button/linear_gradient_button_widget.dart @@ -142,6 +142,7 @@ class LinearGradientButton extends StatelessWidget { Widget? trailing, TextStyle? textStyle, double? height, + BorderRadius? radius, }) => LinearGradientButton( onTap: onTap, @@ -155,7 +156,7 @@ class LinearGradientButton extends StatelessWidget { ), mode: GradientButtonMode.lavenderMode, height: height ?? Sizing.large, - radius: BorderRadius.circular(LemonRadius.small * 2), + radius: radius ?? BorderRadius.circular(LemonRadius.small * 2), trailing: trailing, ); diff --git a/lib/core/presentation/widgets/common/dropdown/frosted_glass_drop_down_v2.dart b/lib/core/presentation/widgets/common/dropdown/frosted_glass_drop_down_v2.dart index 21b3ccbfa..4a1099292 100644 --- a/lib/core/presentation/widgets/common/dropdown/frosted_glass_drop_down_v2.dart +++ b/lib/core/presentation/widgets/common/dropdown/frosted_glass_drop_down_v2.dart @@ -15,14 +15,20 @@ class FrostedGlassDropDownV2 extends StatelessWidget { this.label, this.hintText, this.showRequired, + this.backgroundColor, + this.labelStyle, + this.border, }); final String? label; + final TextStyle? labelStyle; final String? hintText; final List listItem; final String? selectedValue; final ValueChanged onValueChange; final bool? showRequired; + final Color? backgroundColor; + final Border? border; @override Widget build(BuildContext context) { @@ -35,37 +41,38 @@ class FrostedGlassDropDownV2 extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - label!, - style: Typo.small.copyWith( - color: colorScheme.onPrimary.withOpacity(0.36), - ), - ), - if (showRequired == true) ...[ - SizedBox( - width: Spacing.superExtraSmall, - ), - Text( - "*", - style: Typo.mediumPlus.copyWith( - color: LemonColor.coralReef, - fontWeight: FontWeight.w500, + Expanded( + child: Text.rich( + TextSpan( + text: label ?? '', + style: labelStyle ?? + Typo.small.copyWith( + color: colorScheme.onSecondary, + ), + children: [ + if (showRequired == true) + TextSpan( + text: " *", + style: Typo.mediumPlus.copyWith( + color: LemonColor.coralReef, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), - ], + ), ], ), SizedBox(height: Spacing.superExtraSmall), ], Container( width: 1.sw, - padding: EdgeInsets.symmetric( - horizontal: Spacing.smMedium, - vertical: Spacing.xSmall, - ), + height: 54.w, decoration: BoxDecoration( + border: border, borderRadius: BorderRadius.circular(12.r), - color: colorScheme.onPrimary.withOpacity(0.06), + color: backgroundColor ?? colorScheme.onPrimary.withOpacity(0.06), ), child: DropdownButtonHideUnderline( child: DropdownButton2( @@ -87,21 +94,15 @@ class FrostedGlassDropDownV2 extends StatelessWidget { hint: Text( hintText ?? Translations.of(context).common.selectItem, style: Typo.medium.copyWith( - color: colorScheme.onPrimary.withOpacity(0.36), + color: colorScheme.onSurfaceVariant, ), overflow: TextOverflow.ellipsis, ), - value: selectedValue, - onChanged: onValueChange, buttonStyleData: ButtonStyleData( - height: 40.h, - width: 1.sw, - overlayColor: MaterialStatePropertyAll( - colorScheme.onPrimary.withOpacity( - 0.06, - ), - ), + padding: EdgeInsets.only(right: Spacing.xSmall), ), + value: selectedValue, + onChanged: onValueChange, dropdownStyleData: DropdownStyleData( offset: Offset(-18.w, 0), decoration: BoxDecoration( diff --git a/lib/core/presentation/widgets/lemon_text_field.dart b/lib/core/presentation/widgets/lemon_text_field.dart index a837a90e1..9c4013d6f 100644 --- a/lib/core/presentation/widgets/lemon_text_field.dart +++ b/lib/core/presentation/widgets/lemon_text_field.dart @@ -33,6 +33,7 @@ class LemonTextField extends StatelessWidget { this.onFieldSubmitted, this.enableSuggestions, this.autocorrect, + this.style, this.labelStyle, this.placeholderStyle, }); @@ -63,14 +64,15 @@ class LemonTextField extends StatelessWidget { final Function(String newValue)? onFieldSubmitted; final bool? enableSuggestions; final bool? autocorrect; + final TextStyle? style; final TextStyle? labelStyle; final TextStyle? placeholderStyle; + @override Widget build(BuildContext context) { final theme = Theme.of(context); final border = OutlineInputBorder( - borderSide: - BorderSide(color: borderColor ?? theme.colorScheme.outlineVariant), + borderSide: BorderSide(color: borderColor ?? theme.colorScheme.outline), borderRadius: BorderRadius.circular(radius), ); final errorBorder = OutlineInputBorder( @@ -86,23 +88,24 @@ class LemonTextField extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (showRequired == true) ...[ - Text( - "*", - style: Typo.mediumPlus.copyWith( - color: LemonColor.coralReef, - fontWeight: FontWeight.w500, - ), - ), - SizedBox( - width: Spacing.superExtraSmall, - ), - ], Expanded( - child: Text( - label ?? '', - style: Typo.small.copyWith( - color: theme.colorScheme.onPrimary.withOpacity(0.36), + child: Text.rich( + TextSpan( + text: label ?? '', + style: labelStyle ?? + Typo.small.copyWith( + color: theme.colorScheme.onSecondary, + ), + children: [ + if (showRequired == true) + TextSpan( + text: " *", + style: Typo.mediumPlus.copyWith( + color: LemonColor.coralReef, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), ), @@ -117,7 +120,7 @@ class LemonTextField extends StatelessWidget { autofocus: autofocus, onChanged: onChange, focusNode: focusNode, - style: labelStyle ?? + style: style ?? theme.textTheme.bodyMedium! .copyWith(color: theme.colorScheme.onPrimary), minLines: minLines, diff --git a/lib/graphql/backend/schema.graphql b/lib/graphql/backend/schema.graphql index 4a539acfb..5668052bb 100644 --- a/lib/graphql/backend/schema.graphql +++ b/lib/graphql/backend/schema.graphql @@ -49,6 +49,8 @@ type Query { getPastEvents(skip: Int! = 0, limit: Int! = 25, site: MongoID, user: MongoID, sort: JSON): [Event!]! getUpcomingEvents(skip: Int! = 0, limit: Int! = 25, site: MongoID, user: MongoID, host: Boolean, sort: JSON): [Event!]! getEventTags(all: Boolean): [String!]! + generateEventInvitationUrl(event: MongoID!): GenerateEventInvitationUrlResponse! + getEventInvitationUrl(shortid: String!, tk: String): EventInvitationUrl getEventPaymentSummary(event: MongoID!): [EventPaymentSummary!]! getEventQuestions(input: GetEventQuestionsInput!): [EventQuestion!]! getEventRewardUses(input: GetEventRewardUsesInput!): [EventRewardUse!]! @@ -95,6 +97,7 @@ type Query { getStoreDeliveryOptions(store: MongoID!, address: AddressInput!): [DeliveryOption!]! getStoreSalesTax(store: MongoID!, address: AddressInput!): SalesTax! getStripeCards(skip: Int! = 0, limit: Int! = 25): [StripeCard!]! + getStripeConnectedAccountCapability: StripeAccountCapability tgGetMyChannels(input: ScanChannelsInput!): ScanChannelsResult! joinChannel(event_ids: MongoID!): Boolean! exportEventTickets(_id: MongoID!, ticket_type_ids: [MongoID!], search_text: String, pagination: PaginationInput): ExportedTickets! @@ -110,6 +113,7 @@ type Query { """Get tickets of the specified ticket types""" ticket_types: [MongoID!] ): [Ticket!]! + getTicket(shortid: String!): Ticket getUserContacts(skip: Int! = 0, limit: Int! = 25, input: GetUserContactsInput): GetUserContactsResponse! getUserDiscovery(longitude: Float!, latitude: Float!, search_range: Float, event: MongoID, offerings: [MongoID!]): UserDiscovery! getUserDiscoverySwipes(skip: Int! = 0, limit: Int! = 25, incoming: Boolean, state: UserDiscoverySwipeState, other_wallets: Boolean): [UserDiscoverySwipe!]! @@ -125,6 +129,8 @@ type Query { getUsers(skip: Int! = 0, limit: Int! = 25, _id: [MongoID!], tag_recommended: Boolean, wallets: [String!], search: String): [User!]! getUsersSpotlight: [User!]! getUserWalletRequest(wallet: String!): UserWalletRequest! + getUserFromUserMigration(username: String, email: String): User + getEventInvitation(event: MongoID!): EventInvitation } type BadgeList { @@ -204,6 +210,7 @@ type User { username: String settings: JSON wallets: [String!] + wallets_new: JSON wallet_custodial: String attended: Float followers: Float @@ -435,6 +442,7 @@ type Event { declined: [MongoID!] payment_fee: Float! application_required: Boolean + rsvp_wallet_platforms: [ApplicationBlokchainPlatform!] application_form_url: String application_profile_fields: [ApplicationProfileField!] accepted_store_promotion: MongoID @@ -470,6 +478,7 @@ type Event { hide_session_guests: Boolean hide_speakers: Boolean hide_stories_action: Boolean + hide_attending: Boolean highlight: Boolean latitude: Float layout_sections: [LayoutSection!] @@ -563,6 +572,16 @@ enum EventState { cancelled } +type ApplicationBlokchainPlatform { + platform: BlockchainPlatform! + required: Boolean +} + +enum BlockchainPlatform { + ethereum + solana +} + type ApplicationProfileField { field: String! required: Boolean @@ -1061,9 +1080,30 @@ type TicketBase { type EventApplicationQuestion { _id: MongoID! - question: String! + question: String required: Boolean position: Int + type: QuestionType + options: [String!] + select_type: SelectType + questions: [String!] +} + +"""The type of the question in the event application question""" +enum QuestionType { + text + options + checkbox + website + company +} + +""" +Select type for the question of type "options" +""" +enum SelectType { + single + multi } type Badge { @@ -1102,7 +1142,8 @@ input GetCommentsArgs { type EventApplicationAnswer { _id: MongoID! - user: MongoID! + user: MongoID + email: String question: MongoID! answer: String question_expanded: EventApplicationQuestion! @@ -1168,7 +1209,8 @@ type EventCheckin { _id: MongoID! active: Boolean! event: MongoID! - user: MongoID! + user: MongoID + email: String user_expanded: User } @@ -1298,6 +1340,7 @@ type EmailTracking { enum EmailTemplateType { invitation join_request_approved + join_request_approved_with_tickets join_request_declined join_requested post_rsvp @@ -1366,10 +1409,13 @@ type EventJoinRequest { _id: MongoID! created_at: DateTimeISO! event: MongoID! - user: MongoID! + user: MongoID + email: String state: EventJoinRequestState! decided_at: DateTimeISO decided_by: MongoID + requested_tickets: [RequestedTicket!] + payment_id: MongoID user_expanded: UserWithEmail event_expanded: Event decided_by_expanded: User @@ -1381,6 +1427,11 @@ enum EventJoinRequestState { declined } +type RequestedTicket { + ticket_type: MongoID! + count: Float! +} + type UserWithEmail { _id: MongoID active: Boolean! @@ -1448,6 +1499,7 @@ type UserWithEmail { username: String settings: JSON wallets: [String!] + wallets_new: JSON wallet_custodial: String attended: Float followers: Float @@ -1549,6 +1601,16 @@ type Guest { email: String } +type GenerateEventInvitationUrlResponse { + shortid: String! + tk: String +} + +type EventInvitationUrl { + event: MongoID! + user: MongoID! +} + type EventPaymentSummary { currency: String! decimals: Float! @@ -1716,8 +1778,11 @@ type Chain { safe_confirmations: Float! logo_url: String tokens: [Token!] + access_registry_contract: String + payment_config_registry_contract: String escrow_manager_contract: String relay_payment_contract: String + stake_payment_contract: String eas_event_contract: String eas_graphql_url: String } @@ -1755,7 +1820,9 @@ enum NewPaymentState { created initialized failed + await_capture succeeded + refunded } type BuyerInfo { @@ -1788,8 +1855,10 @@ input FilterPaymentStateInput { } type PaymentRefundSignature { - fullRefund: Boolean! signature: String! + + """The args that will be supplied to the contract refund function""" + args: [String!]! } type Post { @@ -2404,6 +2473,44 @@ type StripeCard { last4: String! } +type StripeAccountCapability { + id: String! + capabilities: [Capability!]! +} + +type Capability { + type: StripeCapabilityType! + detail: CapabilityDetail! +} + +enum StripeCapabilityType { + card + apple_pay + google_pay +} + +type CapabilityDetail { + available: Boolean! + display_preference: DisplayPreference! +} + +type DisplayPreference { + overridable: Boolean! + preference: StripeAccountCapabilityDisplayPreferencePreference! + value: StripeAccountCapabilityDisplayPreferenceValue! +} + +enum StripeAccountCapabilityDisplayPreferencePreference { + on + off + none +} + +enum StripeAccountCapabilityDisplayPreferenceValue { + on + off +} + type ScanChannelsResult { offset_date: Float! offset_id: Float! @@ -2513,10 +2620,12 @@ type RelayPaymentInfo { input CalculateTicketsPricingInput { buyer_info: BuyerInfoInput - currency: String! - network: String + connect_wallets: [ConnectWalletInput!] event: MongoID! items: [PurchasableItem!]! + inviter: MongoID + currency: String! + network: String discount: String } @@ -2525,6 +2634,12 @@ input BuyerInfoInput { name: String } +input ConnectWalletInput { + platform: BlockchainPlatform! + token: String! + signature: String! +} + input PurchasableItem { id: MongoID! count: Int! @@ -2540,6 +2655,8 @@ type Ticket { assigned_to: MongoID invited_by: MongoID assigned_to_expanded: User + event_expanded: Event + type_expanded: EventTicketType } type GetUserContactsResponse { @@ -2684,6 +2801,23 @@ type UserWalletRequest { token: String! } +type EventInvitation { + _id: MongoID! + event: MongoID! + inviters: [MongoID!]! + user: MongoID + email: String + phone: String + created_at: DateTimeISO! + status: InvitationResponse! +} + +enum InvitationResponse { + PENDING + DECLINED + ACCEPTED +} + type Mutation { createBadgeList(input: CreateBadgeListInput!): BadgeList! updateBadgeList(input: UpdateBadgeListInput!, _id: MongoID!): BadgeList! @@ -2698,7 +2832,7 @@ type Mutation { mailEventTicket(event: MongoID!, payment: MongoID, emails: [String!]!): Boolean! submitEventApplicationQuestions(event: MongoID!, questions: [QuestionInput!]!): [EventApplicationQuestion!]! deleteEventApplicationQuestions(questions: [MongoID!]!, event: MongoID!): Boolean! - submitEventApplicationAnswers(answers: [EventApplicationAnswerInput!]!, event: MongoID!): Boolean! + submitEventApplicationAnswers(answers: [EventApplicationAnswerInput!]!, event: MongoID!, email: String): Boolean! createEventFromEventbrite(input: CreateEventFromEventbriteInput!, id: String!): Event! createEventbriteWebhookForEvent(eventbrite_event: String!, _id: MongoID!): Boolean! createEventBroadcast(input: CreateEventBroadcastInput!, event: MongoID!): Boolean! @@ -2818,6 +2952,7 @@ type Mutation { deleteStripeCard(_id: MongoID!): StripeCard! disconnectStripeAccount: Boolean! generateStripeAccountLink(refresh_url: String!, return_url: String!): GenerateStripeAccountLinkResponse! + updateStripeConnectedAccountCapability(input: UpdateStripeConnectedAccountCapabilityInput!): StripeAccountCapability! tgSendCode(input: SendCodeInput!): String! tgVerify(input: VerifyCodeInput!): Boolean! tgUnlinkAccount: Boolean! @@ -2879,6 +3014,10 @@ input QuestionInput { question: String required: Boolean position: Int + type: QuestionType! + options: [String!] + select_type: SelectType + questions: [String!] } input EventApplicationAnswerInput { @@ -2915,7 +3054,8 @@ input UpdateEventBroadcastInput { input UpdateEventCheckinInput { active: Boolean! event: MongoID! - user: MongoID! + user: MongoID + shortid: String } input ManageEventCohostRequestsInput { @@ -2967,6 +3107,12 @@ input SubmitEventFeedbackInput { input CreateEventJoinRequestInput { event: MongoID! + requested_tickets: [RequestedTicketInput!] +} + +input RequestedTicketInput { + ticket_type: MongoID! + count: Float! } input DecideUserJoinRequestsInput { @@ -2980,6 +3126,7 @@ input EventInput { start: DateTimeISO end: DateTimeISO application_required: Boolean + rsvp_wallet_platforms: [ApplicationBlokchainPlatformInput!] application_form_url: String application_profile_fields: [ApplicationProfileFieldInput!] accepted_store_promotion: MongoID @@ -3010,6 +3157,7 @@ input EventInput { hide_session_guests: Boolean hide_speakers: Boolean hide_stories_action: Boolean + hide_attending: Boolean latitude: Float layout_sections: [LayoutSectionInput!] longitude: Float @@ -3026,6 +3174,7 @@ input EventInput { photos: [String!] private: Boolean published: Boolean + registration_disabled: Boolean approval_required: Boolean rewards: [EventRewardInput!] sessions: [EventSessionInput!] @@ -3050,6 +3199,11 @@ input EventInput { space: MongoID } +input ApplicationBlokchainPlatformInput { + platform: BlockchainPlatform! + required: Boolean +} + input ApplicationProfileFieldInput { field: String! required: Boolean @@ -3780,6 +3934,16 @@ type GenerateStripeAccountLinkResponse { url: String! } +input UpdateStripeConnectedAccountCapabilityInput { + id: String! + capabilities: [CapabilityInput!]! +} + +input CapabilityInput { + type: StripeCapabilityType! + preference: StripeAccountCapabilityDisplayPreferencePreference! +} + input SendCodeInput { phone_number: String! } @@ -3809,17 +3973,15 @@ input TicketAssignee { type RedeemTicketsResponse { tickets: [Ticket!] + join_request: EventJoinRequest } input RedeemTicketsInput { buyer_info: BuyerInfoInput + connect_wallets: [ConnectWalletInput!] event: MongoID! - items: [RedeemItem!]! -} - -input RedeemItem { - ticket_type: MongoID! - count: Int! + items: [PurchasableItem!]! + inviter: MongoID } input TicketAssignment { @@ -3829,14 +3991,17 @@ input TicketAssignment { type BuyTicketsResponse { payment: NewPayment + join_request: EventJoinRequest } input BuyTicketsInput { buyer_info: BuyerInfoInput - currency: String! - network: String + connect_wallets: [ConnectWalletInput!] event: MongoID! items: [PurchasableItem!]! + inviter: MongoID + currency: String! + network: String discount: String total: String! fee: String @@ -3932,6 +4097,7 @@ input UserInput { terms_accepted_adult: Boolean terms_accepted_conditions: Boolean notification_filters: [JSON!] + email: String } input UserDiscoverySettingsInput { diff --git a/lib/graphql/backend/tickets/mutation/redeem_tickets.graphql b/lib/graphql/backend/tickets/mutation/redeem_tickets.graphql new file mode 100644 index 000000000..d61c4b20f --- /dev/null +++ b/lib/graphql/backend/tickets/mutation/redeem_tickets.graphql @@ -0,0 +1,17 @@ +mutation RedeemTickets($input: RedeemTicketsInput!) { + redeemTickets(input: $input) { + join_request { + _id + } + tickets { + _id + event + type + accepted + assigned_email + assigned_to + invited_by + cancelled_by + } + } +} diff --git a/lib/i18n/common/common_en.i18n.json b/lib/i18n/common/common_en.i18n.json index 4ef8c7ec4..a175e70fe 100644 --- a/lib/i18n/common/common_en.i18n.json +++ b/lib/i18n/common/common_en.i18n.json @@ -76,7 +76,8 @@ "create": "Create", "joinNow": "Join now", "viewAll": "View all", - "setLimit": "Set limit" + "setLimit": "Set limit", + "apply": "Apply" }, "followed": "Followed", "anonymous": "anonymous", diff --git a/lib/i18n/event/event_en.i18n.json b/lib/i18n/event/event_en.i18n.json index 57f1a1d33..9d00284c9 100644 --- a/lib/i18n/event/event_en.i18n.json +++ b/lib/i18n/event/event_en.i18n.json @@ -42,8 +42,8 @@ "eventBuyTickets": { "selectTickets": "Select tickets", "selectCategory": "Select category", - "orderSummary": "Order summary", - "enterPromoCode": "Enter promo code", + "registration": "Registration", + "enterPromoCode": "Enter discount code", "selectCurrency": "Please select currency first", "paymentMethods": { "card": "Card", @@ -63,7 +63,19 @@ "ticketLocked": "Ticket locked!", "ticketLockedDescription": "You need to be on the whitelist to purchase it", "ticketsPurchased": "Tickets purchased!", - "addictionalTicketsPurchasedSuccess": "You have $count additional tickets to assign to your friends. You can do this later inside the event." + "addictionalTicketsPurchasedSuccess": "You have $count additional tickets to assign to your friends. You can do this later inside the event.", + "addDiscountCode": "Add discount code", + "discountCode": "Discount code", + "discount": "Discount", + "discountDescription": "Apply a discount code to discount your purchase", + "payAmount": "Pay $amount", + "signaturePending": "Signature pending", + "signaturePendingDescription": "Check your wallet $walletAddress to sign the transaction for $amount", + "confirmingTransaction": "Confirming transaction", + "confirmingTransactionDescription": "Your transaction is being verified. This could sometimes take up to $duration...", + "viewTransaction": "View transaction", + "processingPayment": "Processing payment", + "processingPaymentDescription": "Your payment is being processed. This could sometimes take up to 60 seconds" }, "eventOrder": { "itemTotal": "Item total", diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart index 482c4d687..6df19fee0 100644 --- a/lib/router/app_router.dart +++ b/lib/router/app_router.dart @@ -404,6 +404,9 @@ final eventBuyTicketsRoutes = AutoRoute( AutoRoute( page: EventTicketsSummaryRoute.page, ), + AutoRoute( + page: EventBuyTicketsProcessingRoute.page, + ), AutoRoute( page: EventTicketsPaymentMethodRoute.page, ),