diff --git a/lib/constants/routes.dart b/lib/constants/routes.dart index b7a44b63..20c84038 100644 --- a/lib/constants/routes.dart +++ b/lib/constants/routes.dart @@ -26,6 +26,7 @@ class RoutePath { static const walletInfo = 'wallet-info'; static const whatIsAWallet = 'what-is-wallet'; static const securityAndPrivacy = 'security-and-privacy'; + static const transactionStatusTimeout = 'status-timeout'; static const doneMinting = 'animation/done-minting'; static const openNftAnimation = 'animation/open-nft'; diff --git a/lib/features/authentication/presentation/providers/sign_in/sign_in_notifier.g.dart b/lib/features/authentication/presentation/providers/sign_in/sign_in_notifier.g.dart index 12e89666..d8e94152 100644 --- a/lib/features/authentication/presentation/providers/sign_in/sign_in_notifier.g.dart +++ b/lib/features/authentication/presentation/providers/sign_in/sign_in_notifier.g.dart @@ -6,7 +6,7 @@ part of 'sign_in_notifier.dart'; // RiverpodGenerator // ************************************************************************** -String _$signInControllerHash() => r'0b4a50956bed4e7a5c7a9fce17957f439e4db742'; +String _$signInControllerHash() => r'b8141fd549b8312e55b40cd91ab98980108745e6'; /// See also [SignInController]. @ProviderFor(SignInController) diff --git a/lib/features/authentication/presentation/providers/sign_up/sign_up_data_notifier.g.dart b/lib/features/authentication/presentation/providers/sign_up/sign_up_data_notifier.g.dart index 11c8d31e..84b5c9d7 100644 --- a/lib/features/authentication/presentation/providers/sign_up/sign_up_data_notifier.g.dart +++ b/lib/features/authentication/presentation/providers/sign_up/sign_up_data_notifier.g.dart @@ -7,7 +7,7 @@ part of 'sign_up_data_notifier.dart'; // ************************************************************************** String _$signUpDataNotifierHash() => - r'fea3fbb7bf4084a57dade097d2da86b8017a0425'; + r'5957a747f39d9b1d72eaba775115662fcc70bd09'; /// See also [SignUpDataNotifier]. @ProviderFor(SignUpDataNotifier) diff --git a/lib/features/authentication/presentation/providers/sign_up/sign_up_notifier.g.dart b/lib/features/authentication/presentation/providers/sign_up/sign_up_notifier.g.dart index 59a323dc..7881c946 100644 --- a/lib/features/authentication/presentation/providers/sign_up/sign_up_notifier.g.dart +++ b/lib/features/authentication/presentation/providers/sign_up/sign_up_notifier.g.dart @@ -6,7 +6,7 @@ part of 'sign_up_notifier.dart'; // RiverpodGenerator // ************************************************************************** -String _$signUpNotifierHash() => r'9368767aca36c4c003422497d32f74166cca799e'; +String _$signUpNotifierHash() => r'270537a96759c241e28baabf6386defa06bda373'; /// See also [SignUpNotifier]. @ProviderFor(SignUpNotifier) diff --git a/lib/features/comic/presentation/widgets/icons/bookmark_icon.dart b/lib/features/comic/presentation/widgets/icons/bookmark_icon.dart index a063f342..57bd35e9 100644 --- a/lib/features/comic/presentation/widgets/icons/bookmark_icon.dart +++ b/lib/features/comic/presentation/widgets/icons/bookmark_icon.dart @@ -1,5 +1,4 @@ import 'package:d_reader_flutter/features/comic/domain/providers/comic_provider.dart'; -import 'package:d_reader_flutter/features/comic/presentation/providers/comic_providers.dart'; import 'package:d_reader_flutter/features/library/presentation/providers/favorites/favorites_providers.dart'; import 'package:d_reader_flutter/shared/presentations/providers/global/global_providers.dart'; import 'package:d_reader_flutter/shared/theme/app_colors.dart'; @@ -18,6 +17,8 @@ class BookmarkIcon extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final bookmarkNotifier = + ref.watch(bookmarkSelectedProvider(isBookmarked).notifier); return GestureDetector( onTap: ref.watch(bookmarkLoadingProvider) ? null @@ -25,21 +26,23 @@ class BookmarkIcon extends ConsumerWidget { final loadingNotifier = ref.read(bookmarkLoadingProvider.notifier); loadingNotifier.update((state) => true); + bookmarkNotifier.update((state) => !state); await ref.read(comicRepositoryProvider).bookmarkComic(slug); loadingNotifier.update((state) => false); - ref.invalidate(comicSlugProvider); + ref.invalidate(favoriteComicsProvider); }, child: Container( constraints: const BoxConstraints(minHeight: 42), padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: isBookmarked + color: ref.watch(bookmarkSelectedProvider(isBookmarked)) ? ColorPalette.dReaderGreen.withOpacity(.4) : Colors.transparent, border: Border.all( - color: - isBookmarked ? Colors.transparent : ColorPalette.greyscale300, + color: ref.watch(bookmarkSelectedProvider(isBookmarked)) + ? Colors.transparent + : ColorPalette.greyscale300, ), borderRadius: BorderRadius.circular(4), ), @@ -47,9 +50,9 @@ class BookmarkIcon extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ SvgPicture.asset( - 'assets/icons/bookmark_${isBookmarked ? 'saved' : 'unsaved'}.svg', + 'assets/icons/bookmark_${ref.watch(bookmarkSelectedProvider(isBookmarked)) ? 'saved' : 'unsaved'}.svg', colorFilter: ColorFilter.mode( - isBookmarked + ref.watch(bookmarkSelectedProvider(isBookmarked)) ? ColorPalette.dReaderGreen : ColorPalette.greyscale100, BlendMode.srcIn, diff --git a/lib/features/comic_issue/presentation/screens/comic_issue_details.dart b/lib/features/comic_issue/presentation/screens/comic_issue_details.dart index d18bec44..f32d0846 100644 --- a/lib/features/comic_issue/presentation/screens/comic_issue_details.dart +++ b/lib/features/comic_issue/presentation/screens/comic_issue_details.dart @@ -7,6 +7,7 @@ import 'package:d_reader_flutter/features/candy_machine/presentations/providers/ import 'package:d_reader_flutter/features/comic_issue/domain/models/comic_issue.dart'; import 'package:d_reader_flutter/features/comic_issue/presentation/providers/comic_issue_providers.dart'; import 'package:d_reader_flutter/features/comic_issue/presentation/providers/controller/comic_issue_controller.dart'; +import 'package:d_reader_flutter/features/comic_issue/presentation/widgets/tabs/about/mint_info_container.dart'; import 'package:d_reader_flutter/shared/domain/providers/solana/solana_providers.dart'; import 'package:d_reader_flutter/shared/exceptions/exceptions.dart'; import 'package:d_reader_flutter/features/wallet/presentation/providers/wallet_providers.dart'; @@ -460,6 +461,12 @@ class BottomNavigation extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final candyMachineGroup = ref.watch(selectedCandyMachineGroup); + final (isMintActive, isEnded) = candyMachineGroup != null + ? getMintStatuses(candyMachineGroup) + : (null, null); + final shouldDisableMintButton = + isMintActive != null && !isMintActive && isEnded != null && !isEnded; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -472,6 +479,7 @@ class BottomNavigation extends ConsumerWidget { ? Expanded( child: TransactionButton( isLoading: ref.watch(globalNotifierProvider).isLoading, + isDisabled: shouldDisableMintButton, onPressed: ref.watch(isOpeningSessionProvider) ? null : () async { @@ -568,8 +576,8 @@ class BottomNavigation extends ConsumerWidget { } } -class TransactionButton extends StatelessWidget { - final bool isListing, isLoading, isMultiGroup; +class TransactionButton extends ConsumerWidget { + final bool isListing, isLoading, isMultiGroup, isDisabled; final Function()? onPressed; final String text; final int? price; @@ -581,14 +589,16 @@ class TransactionButton extends StatelessWidget { this.price, this.isListing = false, this.isMultiGroup = false, + this.isDisabled = false, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { return CustomTextButton( size: const Size(150, 50), isLoading: isLoading, fontSize: 16, + isDisabled: isDisabled, borderRadius: const BorderRadius.all( Radius.circular( 8, @@ -619,7 +629,9 @@ class TransactionButton extends StatelessWidget { ) : SolanaPrice( price: price != null && price! > 0 - ? Formatter.formatPriceWithSignificant(price!) + ? Formatter.formatPriceByCurrency( + mintPrice: price!, + splToken: ref.watch(activeSplToken)) : null, textColor: Colors.black, ), diff --git a/lib/features/comic_issue/presentation/widgets/tabs/about/mint_info_container.dart b/lib/features/comic_issue/presentation/widgets/tabs/about/mint_info_container.dart index 948dfaa8..b5b67121 100644 --- a/lib/features/comic_issue/presentation/widgets/tabs/about/mint_info_container.dart +++ b/lib/features/comic_issue/presentation/widgets/tabs/about/mint_info_container.dart @@ -161,8 +161,9 @@ class MintInfoContainer extends ConsumerWidget { ), SolanaPrice( price: candyMachineGroup.mintPrice > 0 - ? Formatter.formatPriceWithSignificant( - candyMachineGroup.mintPrice.round(), + ? Formatter.formatPriceByCurrency( + mintPrice: candyMachineGroup.mintPrice, + splToken: ref.watch(activeSplToken), ) : null, ) @@ -228,29 +229,53 @@ class _ComicVaultContainer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final textTheme = Theme.of(context).textTheme; - return Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: ColorPalette.greyscale400, + return Theme( + data: Theme.of(context).copyWith( + dividerColor: Colors.transparent, ), - child: Column( - children: [ - Row( - children: [ - SvgPicture.asset( - 'assets/icons/lock.svg', - ), - const SizedBox( - width: 8, - ), - Text( - 'Comic Vault', - style: textTheme.bodySmall?.copyWith( - color: ColorPalette.greyscale100, - ), + child: ExpansionTile( + tilePadding: const EdgeInsets.symmetric(horizontal: 8), + trailing: const SizedBox(), + backgroundColor: ColorPalette.greyscale400, + collapsedBackgroundColor: ColorPalette.greyscale400, + collapsedShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 8, + ), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 8, + ), + ), + childrenPadding: const EdgeInsets.only( + left: 8, + right: 8, + bottom: 8, + top: 0, + ), + title: Row( + children: [ + SvgPicture.asset( + 'assets/icons/lock.svg', + ), + const SizedBox( + width: 8, + ), + Text( + 'Comic Vault', + style: textTheme.bodySmall?.copyWith( + color: ColorPalette.greyscale100, ), - ], + ), + ], + ), + children: [ + Text( + 'Comic Vault stores portion of the supply of each issue to later use in giveaways & other activities where we reward loyal users', + style: textTheme.bodySmall?.copyWith( + color: ColorPalette.greyscale100, + ), ), ], ), diff --git a/lib/features/library/presentation/widgets/cards/library_card.dart b/lib/features/library/presentation/widgets/cards/library_card.dart index e96d6f50..4b12ce58 100644 --- a/lib/features/library/presentation/widgets/cards/library_card.dart +++ b/lib/features/library/presentation/widgets/cards/library_card.dart @@ -28,27 +28,23 @@ class LibraryCard extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - flex: 9, - child: AspectRatio( - aspectRatio: comicAspectRatio, - child: CachedImageBgPlaceholder( - imageUrl: comic.cover, - opacity: .4, - overrideBorderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), - ), - child: comic.logo.isNotEmpty - ? CachedNetworkImage( - imageUrl: comic.logo, - ) - : null, + AspectRatio( + aspectRatio: comicAspectRatio, + child: CachedImageBgPlaceholder( + imageUrl: comic.cover, + opacity: .4, + overrideBorderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), ), + child: comic.logo.isNotEmpty + ? CachedNetworkImage( + imageUrl: comic.logo, + ) + : null, ), ), Expanded( - flex: 4, child: Padding( padding: const EdgeInsets.all(8), child: Column( diff --git a/lib/features/library/presentation/widgets/cards/owned_issue_card.dart b/lib/features/library/presentation/widgets/cards/owned_issue_card.dart index cb5b0823..1223b5c0 100644 --- a/lib/features/library/presentation/widgets/cards/owned_issue_card.dart +++ b/lib/features/library/presentation/widgets/cards/owned_issue_card.dart @@ -40,21 +40,17 @@ class OwnedIssueCard extends ConsumerWidget { ), child: Row( children: [ - Expanded( - flex: 3, - child: AspectRatio( - aspectRatio: comicIssueAspectRatio, - child: CachedImageBgPlaceholder( - imageUrl: issue.cover, - bgImageFit: BoxFit.cover, - ), + AspectRatio( + aspectRatio: comicIssueAspectRatio, + child: CachedImageBgPlaceholder( + imageUrl: issue.cover, + bgImageFit: BoxFit.cover, ), ), const SizedBox( width: 16, ), Expanded( - flex: 7, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/features/library/presentation/widgets/cards/owned_nft_card.dart b/lib/features/library/presentation/widgets/cards/owned_nft_card.dart index 9011980b..f6689070 100644 --- a/lib/features/library/presentation/widgets/cards/owned_nft_card.dart +++ b/lib/features/library/presentation/widgets/cards/owned_nft_card.dart @@ -1,3 +1,4 @@ +import 'package:d_reader_flutter/constants/constants.dart'; import 'package:d_reader_flutter/constants/routes.dart'; import 'package:d_reader_flutter/features/library/presentation/providers/owned/owned_providers.dart'; import 'package:d_reader_flutter/features/nft/domain/models/nft.dart'; @@ -9,6 +10,7 @@ import 'package:d_reader_flutter/shared/widgets/image_widgets/cached_image_bg_pl import 'package:d_reader_flutter/shared/widgets/unsorted/rarity.dart'; import 'package:d_reader_flutter/shared/widgets/unsorted/royalty.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class OwnedNftCard extends ConsumerWidget { @@ -37,8 +39,8 @@ class OwnedNftCard extends ConsumerWidget { ), child: Row( children: [ - Expanded( - flex: 3, + AspectRatio( + aspectRatio: comicIssueAspectRatio, child: CachedImageBgPlaceholder( imageUrl: nft.image, ), @@ -47,7 +49,6 @@ class OwnedNftCard extends ConsumerWidget { width: 16, ), Expanded( - flex: 7, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -77,7 +78,8 @@ class OwnedNftCard extends ConsumerWidget { ), ], ), - Row( + Wrap( + runSpacing: 8, children: [ nft.isUsed ? const RoyaltyWidget( diff --git a/lib/features/library/presentation/widgets/modals/owned_nfts_bottom_sheet.dart b/lib/features/library/presentation/widgets/modals/owned_nfts_bottom_sheet.dart index 64e4d269..9c5c309e 100644 --- a/lib/features/library/presentation/widgets/modals/owned_nfts_bottom_sheet.dart +++ b/lib/features/library/presentation/widgets/modals/owned_nfts_bottom_sheet.dart @@ -37,7 +37,7 @@ class OwnedNftsBottomSheet extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - 'Choose to open', + 'Choose to unwrap', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, @@ -48,7 +48,7 @@ class OwnedNftsBottomSheet extends StatelessWidget { height: 8, ), Text( - 'In order to read comic issue you need to open it from its package.', + 'By unwrapping the comic, you will be able to read it. This action is irreversible and will make the comic lose the mint condition.', //By unwrapping the comic, you will be able to read it. This action is irreversible and will make the comic lose the mint condition. textAlign: TextAlign.center, style: TextStyle( fontSize: 16, @@ -98,7 +98,7 @@ class OwnedNftsBottomSheet extends StatelessWidget { crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.start, direction: Axis.horizontal, - runSpacing: 4, + runSpacing: 8, children: [ RoyaltyWidget( iconPath: ownedNft.isUsed diff --git a/lib/features/nft/presentation/providers/nft_controller.dart b/lib/features/nft/presentation/providers/nft_controller.dart index 1129716f..e2fc333b 100644 --- a/lib/features/nft/presentation/providers/nft_controller.dart +++ b/lib/features/nft/presentation/providers/nft_controller.dart @@ -4,6 +4,7 @@ import 'package:d_reader_flutter/features/comic_issue/presentation/providers/own import 'package:d_reader_flutter/features/library/presentation/providers/owned/owned_providers.dart'; import 'package:d_reader_flutter/features/nft/domain/models/nft.dart'; import 'package:d_reader_flutter/features/nft/presentation/providers/nft_providers.dart'; +import 'package:d_reader_flutter/shared/domain/models/enums.dart'; import 'package:d_reader_flutter/shared/domain/providers/solana/solana_transaction_notifier.dart'; import 'package:d_reader_flutter/shared/exceptions/exceptions.dart'; import 'package:d_reader_flutter/shared/presentations/providers/global/global_notifier.dart'; @@ -118,11 +119,12 @@ class NftController extends _$NftController { required VideoPlayerController videoPlayerController, required AnimationController animationController, required Future Function(NftModel nft) onSuccess, + required Function() onTimeout, required Function() onFail, }) async { - final bool isMinting = - ref.watch(globalNotifierProvider).isMinting != null && - ref.watch(globalNotifierProvider).isMinting!; + final transactionMessage = + ref.watch(globalNotifierProvider).signatureMessage; + final bool isMinted = ref.watch(lastProcessedNftProvider) != null; if (videoPlayerController.value.isPlaying) { if (isMinted) { @@ -131,8 +133,16 @@ class NftController extends _$NftController { animationController: animationController, onSuccess: onSuccess, ); + } else if (transactionMessage == + TransactionStatusMessage.timeout.getString()) { + onTimeout(); + } else if (transactionMessage == + TransactionStatusMessage.fail.getString()) { + onFail(); } - } else if (!isMinting && !isMinted) { + } else if (transactionMessage == + TransactionStatusMessage.fail.getString() && + !isMinted) { onFail(); } } @@ -146,27 +156,32 @@ class NftController extends _$NftController { animationController.reverse( from: 1, ); + if (ref.watch(lastProcessedNftProvider) == null) { + return; + } final nft = await ref .read(nftProvider(ref.watch(lastProcessedNftProvider)!).future); - if (nft != null) { - ref.invalidate(lastProcessedNftProvider); - ref.invalidate(ownedComicsProvider); - ref.invalidate(ownedIssuesAsyncProvider); - ref.invalidate(nftsProvider); - await onSuccess(nft); + if (nft == null) { + return; } + + ref.invalidate(lastProcessedNftProvider); + ref.invalidate(ownedComicsProvider); + ref.invalidate(ownedIssuesAsyncProvider); + ref.invalidate(nftsProvider); + ref + .read(globalNotifierProvider.notifier) + .update(isLoading: false, newMessage: ''); + await onSuccess(nft); } mintOpenListener({ required VideoPlayerController videoPlayerController, required AnimationController animationController, - required Function(String nftAddress) onSuccess, + required Function(int comicIssueId) onSuccess, required Function() onFail, }) { - final bool isMinting = - ref.watch(globalNotifierProvider).isMinting != null && - ref.watch(globalNotifierProvider).isMinting!; final bool isMinted = ref.watch(lastProcessedNftProvider) != null; if (videoPlayerController.value.isPlaying) { @@ -176,7 +191,8 @@ class NftController extends _$NftController { onSuccess: onSuccess, videoPlayerController: videoPlayerController, ); - } else if (!isMinting && !isMinted) { + } else if (ref.watch(globalNotifierProvider).signatureMessage == + TransactionStatusMessage.fail.getString()) { onFail(); } } @@ -185,35 +201,46 @@ class NftController extends _$NftController { _handleOpenedCase({ required VideoPlayerController videoPlayerController, required AnimationController animationController, - required Function(String nftAddress) onSuccess, + required Function(int comicIssueId) onSuccess, }) { videoPlayerController.pause(); animationController.reverse( from: 1, ); final String? nftAddress = ref.read(lastProcessedNftProvider); - if (nftAddress != null) { - ref.invalidate(lastProcessedNftProvider); - ref.invalidate(nftsProvider); - ref.invalidate(ownedComicsProvider); - ref.invalidate(ownedIssuesAsyncProvider); - ref.invalidate(comicIssuePagesProvider); - ref.invalidate(comicIssueDetailsProvider); - onSuccess(nftAddress); + if (nftAddress == null) { + return; } + ref.invalidate(lastProcessedNftProvider); + ref.invalidate(nftsProvider); + ref.invalidate(ownedComicsProvider); + ref.invalidate(ownedIssuesAsyncProvider); + ref.invalidate(comicIssuePagesProvider); + ref.invalidate(comicIssueDetailsProvider); + ref + .read(globalNotifierProvider.notifier) + .update(isLoading: false, newMessage: ''); + ref.read(nftProvider(nftAddress).future).then( + (value) { + if (value != null) { + onSuccess(value.comicIssueId); + } + }, + ); } handleNftUnwrap({ required String nftAddress, required String ownerAddress, required Function() onSuccess, + required Function(String message) onFail, }) async { final useMintResult = await ref.read(solanaTransactionNotifierProvider.notifier).useMint( nftAddress: nftAddress, ownerAddress: ownerAddress, ); - useMintResult.fold((exception) => null, (result) { + useMintResult.fold((exception) => onFail(exception.message), (result) { if (result == successResult) { onSuccess(); } diff --git a/lib/features/nft/presentation/providers/nft_providers.dart b/lib/features/nft/presentation/providers/nft_providers.dart index bd3686e1..1bb4ca20 100644 --- a/lib/features/nft/presentation/providers/nft_providers.dart +++ b/lib/features/nft/presentation/providers/nft_providers.dart @@ -3,6 +3,7 @@ import 'dart:async' show Timer; import 'package:d_reader_flutter/config/config.dart'; import 'package:d_reader_flutter/features/nft/domain/models/nft.dart'; import 'package:d_reader_flutter/features/nft/domain/providers/nft_provider.dart'; +import 'package:d_reader_flutter/shared/domain/models/enums.dart'; import 'package:d_reader_flutter/shared/domain/providers/environment/environment_notifier.dart'; import 'package:d_reader_flutter/shared/presentations/providers/global/global_notifier.dart'; import 'package:d_reader_flutter/shared/utils/utils.dart'; @@ -46,37 +47,48 @@ final lastProcessedNftProvider = StateProvider( }, ); -final mintingStatusProvider = StateProvider.family( +final transactionChainStatusProvider = StateProvider.family( (ref, signature) { - if (signature.isNotEmpty) { - final client = createSolanaClient( - rpcUrl: ref.read(environmentProvider).solanaCluster == - SolanaCluster.devnet.value - ? Config.rpcUrlDevnet - : Config.rpcUrlMainnet, - ); + if (signature.isEmpty) { + ref.read(globalNotifierProvider.notifier).update( + isLoading: false, + newMessage: '', + ); + return; + } + final client = createSolanaClient( + rpcUrl: ref.read(environmentProvider).solanaCluster == + SolanaCluster.devnet.value + ? Config.rpcUrlDevnet + : Config.rpcUrlMainnet, + ); - client - .waitForSignatureStatus( - signature, - status: Commitment.confirmed, - timeout: const Duration(seconds: 12), - ) - .then((value) { - Future.delayed(const Duration(seconds: 8), () { - ref.read(globalNotifierProvider.notifier).update( - isLoading: false, - isMinting: false, - ); - }); - }).onError((error, stackTrace) { - Sentry.captureException(error, - stackTrace: 'Signature status provider $signature'); + client + .waitForSignatureStatus( + signature, + status: Commitment.confirmed, + timeout: const Duration(seconds: 12), + ) + .then((value) { + ref.read(globalNotifierProvider.notifier).update( + isLoading: false, + newMessage: TransactionStatusMessage.success.getString(), + ); + }).onError((error, stackTrace) { + Sentry.captureException(error, + stackTrace: 'Signature status provider $signature'); + ref.read(globalNotifierProvider.notifier).update( + isLoading: false, + newMessage: TransactionStatusMessage.fail.getString(), + ); + }).timeout( + const Duration(seconds: 12), + onTimeout: () { ref.read(globalNotifierProvider.notifier).update( isLoading: false, - isMinting: null, + newMessage: TransactionStatusMessage.timeout.getString(), ); - }); - } + }, + ); }, ); diff --git a/lib/features/nft/presentation/screens/animations/mint_animation_screen.dart b/lib/features/nft/presentation/screens/animations/mint_animation_screen.dart index 0abb4b64..16d8151b 100644 --- a/lib/features/nft/presentation/screens/animations/mint_animation_screen.dart +++ b/lib/features/nft/presentation/screens/animations/mint_animation_screen.dart @@ -1,20 +1,19 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:d_reader_flutter/config/config.dart'; -import 'package:d_reader_flutter/constants/enums.dart'; import 'package:d_reader_flutter/constants/routes.dart'; +import 'package:d_reader_flutter/features/comic_issue/presentation/providers/comic_issue_providers.dart'; import 'package:d_reader_flutter/features/nft/presentation/providers/nft_controller.dart'; import 'package:d_reader_flutter/features/nft/presentation/utils/extensions.dart'; import 'package:d_reader_flutter/features/nft/presentation/utils/utils.dart'; -import 'package:d_reader_flutter/shared/data/local/local_store.dart'; import 'package:d_reader_flutter/features/nft/domain/models/nft.dart'; import 'package:d_reader_flutter/shared/domain/providers/environment/environment_notifier.dart'; import 'package:d_reader_flutter/shared/presentations/providers/global/global_notifier.dart'; import 'package:d_reader_flutter/shared/theme/app_colors.dart'; -import 'package:d_reader_flutter/shared/utils/dialog_triggers.dart'; import 'package:d_reader_flutter/shared/utils/screen_navigation.dart'; +import 'package:d_reader_flutter/shared/utils/show_snackbar.dart'; import 'package:d_reader_flutter/shared/utils/url_utils.dart'; import 'package:d_reader_flutter/shared/widgets/buttons/custom_text_button.dart'; -import 'package:d_reader_flutter/shared/widgets/checkbox/custom_labeled_checkbox.dart'; +import 'package:d_reader_flutter/shared/widgets/buttons/unwrap_button.dart'; import 'package:d_reader_flutter/shared/widgets/unsorted/rarity.dart'; import 'package:d_reader_flutter/shared/widgets/unsorted/royalty.dart'; import 'package:flutter/material.dart'; @@ -54,25 +53,39 @@ class _MintLoadingAnimationState extends ConsumerState _controller.play(); _controller.addListener(() async { await ref.read(nftControllerProvider.notifier).mintLoadingListener( - videoPlayerController: _controller, - animationController: _animationController, - onSuccess: (NftModel nft) async { - await Future.delayed( - const Duration(milliseconds: 1000), - () { - nextScreenReplace( - context: context, - path: RoutePath.doneMinting, - homeSubRoute: true, - extra: nft, - ); - }, - ); - }, - onFail: () { - _controller.pause(); - context.pop(); - }); + videoPlayerController: _controller, + animationController: _animationController, + onSuccess: (NftModel nft) async { + await Future.delayed( + const Duration(milliseconds: 1000), + () { + nextScreenReplace( + context: context, + path: RoutePath.doneMinting, + homeSubRoute: true, + extra: nft, + ); + }, + ); + }, + onTimeout: () { + _controller.pause(); + nextScreenReplace( + context: context, + path: RoutePath.transactionStatusTimeout, + homeSubRoute: true, + ); + }, + onFail: () { + _controller.pause(); + context.pop(); + showSnackBar( + context: context, + text: 'Failed to mint', + backgroundColor: ColorPalette.dReaderRed, + ); + }, + ); }); } @@ -177,16 +190,22 @@ class _DoneMintingAnimationState extends State _handleUnwrap({required WidgetRef ref}) async { await ref.read(nftControllerProvider.notifier).handleNftUnwrap( - nftAddress: widget.nft.address, - ownerAddress: widget.nft.ownerAddress, - onSuccess: () { - nextScreenReplace( - context: context, - path: RoutePath.openNftAnimation, - homeSubRoute: true, - ); - }, - ); + nftAddress: widget.nft.address, + ownerAddress: widget.nft.ownerAddress, + onSuccess: () { + nextScreenReplace( + context: context, + path: RoutePath.openNftAnimation, + homeSubRoute: true, + ); + }, + onFail: (String message) { + showSnackBar( + context: context, + text: message, + backgroundColor: ColorPalette.dReaderRed, + ); + }); } @override @@ -283,14 +302,19 @@ class _DoneMintingAnimationState extends State return GestureDetector( onTap: () async { final nft = widget.nft; + final comicIssue = await ref.read( + comicIssueDetailsProvider( + nft.comicIssueId.toString()) + .future, + ); final dReaderWebUrl = ref .read(environmentProvider) .solanaCluster == SolanaCluster.devnet.value - ? 'https://dev-devnet.dreader.app/mint/${nft.comicIssueId}' - : 'https://dreader.app/mint/${nft.comicIssueId}'; + ? 'https://dev-devnet.dreader.app/mint/${comicIssue.comicSlug}_${comicIssue.slug}?utm_source=mobile' + : 'https://dreader.app/mint/${comicIssue.comicSlug}_${comicIssue.slug}?utm_source=mobile'; final uri = Uri.encodeFull( - 'https://twitter.com/intent/tweet?text=I just minted a ${nft.rarity.toLowerCase()} copy of the ${nft.name.split('#')[0]}!\n\nMint yours here while the supply lasts.👇\n\n$dReaderWebUrl', + 'https://twitter.com/intent/tweet?text=I just minted a ${nft.rarity} ${comicIssue.comic?.title}: ${comicIssue.title} comic on @dReaderApp! 📚\n\nMint yours here while the supply lasts.👇\n\n$dReaderWebUrl', ); await openUrl(uri); }, @@ -365,70 +389,12 @@ class _DoneMintingAnimationState extends State builder: (context, ref, child) { final bool isLoading = ref.watch(globalNotifierProvider).isLoading; - return CustomTextButton( - backgroundColor: ColorPalette.dReaderYellow100, - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ), - borderRadius: BorderRadius.circular(8), - textColor: Colors.black, - size: const Size(0, 50), + return UnwrapButton( isLoading: isLoading, - onPressed: isLoading - ? null - : () async { - final shouldTriggerDialog = LocalStore - .instance - .get(WalkthroughKeys.unwrap.name) == - null; - if (!shouldTriggerDialog) { - return await _handleUnwrap(ref: ref); - } - - bool isChecked = false; - return triggerWalkthroughDialog( - context: context, - buttonText: 'Unwrap', - onSubmit: () async { - if (isChecked) { - LocalStore.instance.put( - WalkthroughKeys.unwrap.name, - true, - ); - } - context.pop(); - await _handleUnwrap(ref: ref); - }, - title: 'Comic unwraping', - subtitle: - 'By unwrapping the comic, you will be able to read it. This action is irreversible and will make the comic lose the mint condition.', - bottomWidget: StatefulBuilder( - builder: (context, setState) { - return CustomLabeledCheckbox( - isChecked: isChecked, - onChange: () { - setState( - () { - isChecked = !isChecked; - }, - ); - }, - label: Text( - 'Don\'t ask me again', - style: Theme.of(context) - .textTheme - .bodySmall, - ), - ); - }, - ), - assetPath: '', - ); - }, - child: const Text( - 'Unwrap', - ), + nft: widget.nft, + onPressed: () async { + await _handleUnwrap(ref: ref); + }, ); }, ), diff --git a/lib/features/nft/presentation/screens/animations/open_nft_animation_screen.dart b/lib/features/nft/presentation/screens/animations/open_nft_animation_screen.dart index a7db73c1..ade9f73f 100644 --- a/lib/features/nft/presentation/screens/animations/open_nft_animation_screen.dart +++ b/lib/features/nft/presentation/screens/animations/open_nft_animation_screen.dart @@ -44,11 +44,10 @@ class _OpenNftAnimationState extends ConsumerState ref.read(nftControllerProvider.notifier).mintOpenListener( videoPlayerController: _controller, animationController: _animationController, - onSuccess: (String nftAddress) { + onSuccess: (int comicIssueId) { nextScreenReplace( context: context, - path: - '${RoutePath.nftDetails}/$nftAddress', // TODO open eReader + path: '${RoutePath.eReader}/$comicIssueId', homeSubRoute: true, ); }, diff --git a/lib/features/nft/presentation/screens/nft_details.dart b/lib/features/nft/presentation/screens/nft_details.dart index 7f3d0be7..7ce86e63 100644 --- a/lib/features/nft/presentation/screens/nft_details.dart +++ b/lib/features/nft/presentation/screens/nft_details.dart @@ -18,6 +18,7 @@ import 'package:d_reader_flutter/features/nft/presentation/utils/utils.dart'; import 'package:d_reader_flutter/shared/utils/show_snackbar.dart'; import 'package:d_reader_flutter/shared/widgets/buttons/custom_text_button.dart'; import 'package:d_reader_flutter/features/nft/presentation/widgets/nft_card.dart'; +import 'package:d_reader_flutter/shared/widgets/buttons/unwrap_button.dart'; import 'package:d_reader_flutter/shared/widgets/cards/skeleton_card.dart'; import 'package:d_reader_flutter/shared/widgets/unsorted/rarity.dart'; import 'package:d_reader_flutter/shared/widgets/unsorted/royalty.dart'; @@ -176,57 +177,81 @@ class NftDetails extends ConsumerWidget { width: 16, ), Expanded( - child: Button( - borderColor: ColorPalette.dReaderYellow100, - isLoading: - ref.watch(globalNotifierProvider).isLoading, - loadingColor: ColorPalette.dReaderYellow100, - onPressed: () async { - if (nft.isUsed) { - return nextScreenPush( - context: context, - path: - '${RoutePath.eReader}/${nft.comicIssueId}', - ); - } - await ref - .read(nftControllerProvider.notifier) - .openNft( - nft: nft, - onOpen: (String result) { - _handleNftOpen( - context: context, - ref: ref, - openResponse: result, - ); - }, - onException: (exception) { - if (exception - is LowPowerModeException || - exception - is NoWalletFoundException) { - triggerLowPowerOrNoWallet( - context, - exception, - ); - return; - } else if (exception - is AppException) { - showSnackBar( - context: context, - text: exception.message, + child: nft.isUsed + ? Button( + borderColor: + ColorPalette.dReaderYellow100, + isLoading: ref + .watch(globalNotifierProvider) + .isLoading, + loadingColor: + ColorPalette.dReaderYellow100, + onPressed: () async { + return nextScreenPush( + context: context, + path: + '${RoutePath.eReader}/${nft.comicIssueId}', + ); + }, + child: Text( + 'Read', + style: textTheme.titleMedium?.copyWith( + color: ColorPalette.dReaderYellow100, + ), + ), + ) + : UnwrapButton( + nft: nft, + onPressed: () async { + await ref + .read( + nftControllerProvider.notifier) + .openNft( + nft: nft, + onOpen: (String result) { + _handleNftOpen( + context: context, + ref: ref, + openResponse: result, + ); + }, + onException: (exception) { + if (exception + is LowPowerModeException || + exception + is NoWalletFoundException) { + triggerLowPowerOrNoWallet( + context, + exception, + ); + return; + } else if (exception + is AppException) { + showSnackBar( + context: context, + text: exception.message, + ); + } + }, ); - } - }, - ); - }, - child: Text( - nft.isUsed ? 'Read' : 'Unwrap', - style: textTheme.titleMedium?.copyWith( - color: ColorPalette.dReaderYellow100, - ), - ), - ), + }, + borderColor: + ColorPalette.dReaderYellow100, + isLoading: ref + .watch(globalNotifierProvider) + .isLoading, + backgroundColor: Colors.transparent, + loadingColor: + ColorPalette.dReaderYellow100, + textColor: ColorPalette.dReaderYellow100, + size: Size( + MediaQuery.sizeOf(context).width / 2.4, + 40, + ), + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + ), ), ], ), @@ -256,35 +281,42 @@ class NftDetails extends ConsumerWidget { const SizedBox( height: 8, ), - Row( + Wrap( + runSpacing: 8, children: [ - Container( - padding: const EdgeInsets.all(8), - height: 40, - margin: const EdgeInsets.only(right: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - border: Border.all( - color: ColorPalette.dReaderBlue, - ), - ), - child: Row( - children: [ - Text( - '${Formatter.formatPrice(nft.royalties)}%', - style: textTheme.bodySmall?.copyWith( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + height: 40, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all( color: ColorPalette.dReaderBlue, ), ), - const SizedBox( - width: 4, + child: Row( + children: [ + Text( + '${Formatter.formatPrice(nft.royalties)}%', + style: + textTheme.bodySmall?.copyWith( + color: ColorPalette.dReaderBlue, + ), + ), + const SizedBox( + width: 4, + ), + Text( + 'royalty', + style: textTheme.bodyMedium, + ), + ], ), - Text( - 'royalty', - style: textTheme.bodyMedium, - ), - ], - ), + ), + ], ), RoyaltyWidget( isLarge: true, diff --git a/lib/features/settings/presentation/screens/profile/profile.dart b/lib/features/settings/presentation/screens/profile/profile.dart index ab5a11de..dc61e02e 100644 --- a/lib/features/settings/presentation/screens/profile/profile.dart +++ b/lib/features/settings/presentation/screens/profile/profile.dart @@ -418,9 +418,7 @@ class Avatar extends StatelessWidget { height: 96, width: 96, padding: const EdgeInsets.all(16), - child: const CircularProgressIndicator( - color: ColorPalette.dReaderBlue, - ), + child: const CircularProgressIndicator(), ), ) : user.avatar.isNotEmpty diff --git a/lib/features/transaction/presentation/screens/transaction_timeout.dart b/lib/features/transaction/presentation/screens/transaction_timeout.dart new file mode 100644 index 00000000..43398bb2 --- /dev/null +++ b/lib/features/transaction/presentation/screens/transaction_timeout.dart @@ -0,0 +1,42 @@ +import 'package:d_reader_flutter/shared/theme/app_colors.dart'; +import 'package:d_reader_flutter/shared/widgets/buttons/custom_text_button.dart'; +import 'package:d_reader_flutter/shared/widgets/unsorted/carrot_error_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class TransactionTimeoutScreen extends StatelessWidget { + const TransactionTimeoutScreen({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Scaffold( + backgroundColor: ColorPalette.appBackgroundColor, + body: CarrotErrorWidget( + mainErrorText: 'Network is congested', + padding: const EdgeInsets.symmetric(horizontal: 16), + height: MediaQuery.sizeOf(context).height, + adviceText: + 'Your transaction might have failed.\nPlease check if comic is in your wallet and/or retry the purchase.', + additionalChild: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomTextButton( + backgroundColor: Colors.transparent, + borderColor: ColorPalette.greyscale50, + textColor: ColorPalette.greyscale50, + onPressed: () { + context.pop(); + }, + child: Text( + 'Close', + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/wallet/presentation/providers/wallet_notifier.g.dart b/lib/features/wallet/presentation/providers/wallet_notifier.g.dart index 1a0b8b1b..71518bc0 100644 --- a/lib/features/wallet/presentation/providers/wallet_notifier.g.dart +++ b/lib/features/wallet/presentation/providers/wallet_notifier.g.dart @@ -6,7 +6,7 @@ part of 'wallet_notifier.dart'; // RiverpodGenerator // ************************************************************************** -String _$walletControllerHash() => r'56e743ffbc0bc1da3c9f6a67760a43e0baf329f5'; +String _$walletControllerHash() => r'f81cfe2ef4a8331368d9d4fd78d0041639267a85'; /// See also [WalletController]. @ProviderFor(WalletController) diff --git a/lib/routing/router.dart b/lib/routing/router.dart index 388993f5..ad5c3293 100644 --- a/lib/routing/router.dart +++ b/lib/routing/router.dart @@ -3,6 +3,7 @@ import 'package:d_reader_flutter/features/authentication/presentation/screens/si import 'package:d_reader_flutter/features/authentication/presentation/screens/verify_email.dart'; import 'package:d_reader_flutter/features/nft/domain/models/nft.dart'; import 'package:d_reader_flutter/features/settings/presentation/screens/security_and_privacy.dart'; +import 'package:d_reader_flutter/features/transaction/presentation/screens/transaction_timeout.dart'; import 'package:d_reader_flutter/shared/domain/providers/environment/environment_notifier.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -237,7 +238,13 @@ final List homeRoutes = [ builder: (context, state) { return const SecurityAndPrivacyScreen(); }, - ) + ), + GoRoute( + path: RoutePath.transactionStatusTimeout, + builder: (context, state) { + return const TransactionTimeoutScreen(); + }, + ), ], ), ]; diff --git a/lib/shared/domain/models/enums.dart b/lib/shared/domain/models/enums.dart index cddda9e3..5a569bec 100644 --- a/lib/shared/domain/models/enums.dart +++ b/lib/shared/domain/models/enums.dart @@ -36,3 +36,22 @@ enum NftRarity { epic, legendary, } + +enum TransactionStatusMessage { + success, + fail, + unknown, + waiting, + timeout; + + String getString() { + return switch (this) { + TransactionStatusMessage.success => 'Confirmed', + TransactionStatusMessage.fail => 'Fail', + TransactionStatusMessage.unknown => 'Unknown transaction status', + TransactionStatusMessage.waiting => 'Waiting', + TransactionStatusMessage.timeout => + 'Network is congested, your transaction might have failed. Please check your wallet', + }; + } +} diff --git a/lib/shared/domain/providers/environment/environment_notifier.dart b/lib/shared/domain/providers/environment/environment_notifier.dart index 122b5bb1..78d995d3 100644 --- a/lib/shared/domain/providers/environment/environment_notifier.dart +++ b/lib/shared/domain/providers/environment/environment_notifier.dart @@ -135,7 +135,7 @@ class Environment extends _$Environment { authToken: state.authToken, walletAuthTokenMap: state.walletAuthTokenMap, jwtToken: null, - publicKey: state.publicKey, + publicKey: null, refreshToken: null, user: null, wallets: null, diff --git a/lib/shared/domain/providers/environment/environment_notifier.g.dart b/lib/shared/domain/providers/environment/environment_notifier.g.dart index 54abf4e9..1f745af3 100644 --- a/lib/shared/domain/providers/environment/environment_notifier.g.dart +++ b/lib/shared/domain/providers/environment/environment_notifier.g.dart @@ -6,7 +6,7 @@ part of 'environment_notifier.dart'; // RiverpodGenerator // ************************************************************************** -String _$environmentHash() => r'a58848f3cdea9077307adda55cfd32fd031086c2'; +String _$environmentHash() => r'ee44082b181b2aea8900718d361d4590857a5719'; /// See also [Environment]. @ProviderFor(Environment) diff --git a/lib/shared/domain/providers/solana/solana_notifier.dart b/lib/shared/domain/providers/solana/solana_notifier.dart index 0b95856e..73a81b77 100644 --- a/lib/shared/domain/providers/solana/solana_notifier.dart +++ b/lib/shared/domain/providers/solana/solana_notifier.dart @@ -1,11 +1,13 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:d_reader_flutter/config/config.dart'; import 'package:d_reader_flutter/constants/constants.dart'; import 'package:d_reader_flutter/features/authentication/domain/providers/auth_provider.dart'; import 'package:d_reader_flutter/features/candy_machine/presentations/providers/candy_machine_providers.dart'; import 'package:d_reader_flutter/features/user/presentation/providers/user_providers.dart'; +import 'package:d_reader_flutter/features/wallet/domain/models/wallet.dart'; import 'package:d_reader_flutter/features/wallet/presentation/providers/wallet_providers.dart'; import 'package:d_reader_flutter/shared/domain/models/either.dart'; import 'package:d_reader_flutter/shared/domain/providers/environment/environment_notifier.dart'; @@ -213,7 +215,8 @@ class SolanaNotifier extends _$SolanaNotifier { ); final result = await _authorizeAndSignIfNeeded( client: client, - shouldSignMessage: isConnectOnly || wallets.isEmpty, + wallets: wallets, + shouldSignMessage: isConnectOnly, ); if (result != successResult) { @@ -250,6 +253,7 @@ class SolanaNotifier extends _$SolanaNotifier { Future _authorizeAndSignIfNeeded({ required MobileWalletAdapterClient client, + required List wallets, bool shouldSignMessage = true, String? overrideCluster, }) async { @@ -276,7 +280,10 @@ class SolanaNotifier extends _$SolanaNotifier { }, ), ); - if (shouldSignMessage) { + final isExistingWallet = wallets.firstWhereOrNull( + (element) => element.address == publicKey.toBase58()) != + null; + if (shouldSignMessage || !isExistingWallet) { return await _signMessageAndConnectWallet( client: client, signer: publicKey, diff --git a/lib/shared/domain/providers/solana/solana_notifier.g.dart b/lib/shared/domain/providers/solana/solana_notifier.g.dart index 7a1b0740..91d553cc 100644 --- a/lib/shared/domain/providers/solana/solana_notifier.g.dart +++ b/lib/shared/domain/providers/solana/solana_notifier.g.dart @@ -6,7 +6,7 @@ part of 'solana_notifier.dart'; // RiverpodGenerator // ************************************************************************** -String _$solanaNotifierHash() => r'3ba127e233cdfbb7ca13badcfcda3d95122167b9'; +String _$solanaNotifierHash() => r'685acc9d82bc0040024a006a281897bf2ed46147'; /// See also [SolanaNotifier]. @ProviderFor(SolanaNotifier) diff --git a/lib/shared/domain/providers/solana/solana_transaction_notifier.dart b/lib/shared/domain/providers/solana/solana_transaction_notifier.dart index 01069040..a0673785 100644 --- a/lib/shared/domain/providers/solana/solana_transaction_notifier.dart +++ b/lib/shared/domain/providers/solana/solana_transaction_notifier.dart @@ -10,6 +10,7 @@ import 'package:d_reader_flutter/features/nft/presentation/providers/nft_provide import 'package:d_reader_flutter/features/settings/presentation/providers/spl_tokens.dart'; import 'package:d_reader_flutter/features/transaction/domain/providers/transaction_provider.dart'; import 'package:d_reader_flutter/shared/domain/models/either.dart'; +import 'package:d_reader_flutter/shared/domain/models/enums.dart'; import 'package:d_reader_flutter/shared/domain/providers/environment/environment_notifier.dart'; import 'package:d_reader_flutter/shared/domain/providers/solana/solana_notifier.dart'; import 'package:d_reader_flutter/shared/exceptions/exceptions.dart'; @@ -98,11 +99,11 @@ class SolanaTransactionNotifier extends _$SolanaTransactionNotifier { preflightCommitment: Commitment.confirmed, ); } - ref - .read(globalNotifierProvider.notifier) - .update(isLoading: false, isMinting: true); + ref.read(globalNotifierProvider.notifier).update( + isLoading: false, + newMessage: TransactionStatusMessage.waiting.getString()); - ref.read(mintingStatusProvider(sendTransactionResult)); + ref.read(transactionChainStatusProvider(sendTransactionResult)); await session.close(); return const Right(successResult); } catch (exception) { @@ -277,10 +278,10 @@ class SolanaTransactionNotifier extends _$SolanaTransactionNotifier { signedTx.encode(), preflightCommitment: Commitment.confirmed, ); - ref - .read(globalNotifierProvider.notifier) - .update(isLoading: false, isMinting: true); - ref.read(mintingStatusProvider(sendTransactionResult)); + ref.read(globalNotifierProvider.notifier).update( + isLoading: false, + newMessage: TransactionStatusMessage.waiting.getString()); + ref.read(transactionChainStatusProvider(sendTransactionResult)); await session.close(); return successResult; } catch (exception) { diff --git a/lib/shared/presentations/providers/global/global_notifier.dart b/lib/shared/presentations/providers/global/global_notifier.dart index 88f61ca4..4627336e 100644 --- a/lib/shared/presentations/providers/global/global_notifier.dart +++ b/lib/shared/presentations/providers/global/global_notifier.dart @@ -10,18 +10,14 @@ class GlobalNotifier extends _$GlobalNotifier { return const GlobalState(isLoading: false); } - void update({required bool isLoading, bool? isMinting}) { + void update({required bool isLoading, String? newMessage}) { state = state.copyWith( isLoading: isLoading, - isMinting: isMinting, + signatureMessage: newMessage ?? state.signatureMessage, ); } void updateLoading(bool isLoading) { state = state.copyWith(isLoading: isLoading); } - - void updateMinting(bool isMinting) { - state = state.copyWith(isMinting: isMinting); - } } diff --git a/lib/shared/presentations/providers/global/global_providers.dart b/lib/shared/presentations/providers/global/global_providers.dart index 777006e3..73911313 100644 --- a/lib/shared/presentations/providers/global/global_providers.dart +++ b/lib/shared/presentations/providers/global/global_providers.dart @@ -44,3 +44,9 @@ final obscureTextProvider = StateProvider.autoDispose((ref) { final additionalObscureTextProvider = StateProvider.autoDispose((ref) { return true; }); + +final bookmarkSelectedProvider = StateProvider.family.autoDispose( + (ref, arg) { + return arg; + }, +); diff --git a/lib/shared/presentations/providers/global/state/global_state.dart b/lib/shared/presentations/providers/global/state/global_state.dart index 18cf0cad..5d147ae6 100644 --- a/lib/shared/presentations/providers/global/state/global_state.dart +++ b/lib/shared/presentations/providers/global/state/global_state.dart @@ -6,6 +6,6 @@ part 'global_state.freezed.dart'; abstract class GlobalState with _$GlobalState { const factory GlobalState({ required bool isLoading, - bool? isMinting, + @Default('') String signatureMessage, }) = _GlobalState; } diff --git a/lib/shared/presentations/providers/global/state/global_state.freezed.dart b/lib/shared/presentations/providers/global/state/global_state.freezed.dart index 699fbd6c..d2aee1ff 100644 --- a/lib/shared/presentations/providers/global/state/global_state.freezed.dart +++ b/lib/shared/presentations/providers/global/state/global_state.freezed.dart @@ -17,7 +17,7 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$GlobalState { bool get isLoading => throw _privateConstructorUsedError; - bool? get isMinting => throw _privateConstructorUsedError; + String get signatureMessage => throw _privateConstructorUsedError; @JsonKey(ignore: true) $GlobalStateCopyWith get copyWith => @@ -30,7 +30,7 @@ abstract class $GlobalStateCopyWith<$Res> { GlobalState value, $Res Function(GlobalState) then) = _$GlobalStateCopyWithImpl<$Res, GlobalState>; @useResult - $Res call({bool isLoading, bool? isMinting}); + $Res call({bool isLoading, String signatureMessage}); } /// @nodoc @@ -47,17 +47,17 @@ class _$GlobalStateCopyWithImpl<$Res, $Val extends GlobalState> @override $Res call({ Object? isLoading = null, - Object? isMinting = freezed, + Object? signatureMessage = null, }) { return _then(_value.copyWith( isLoading: null == isLoading ? _value.isLoading : isLoading // ignore: cast_nullable_to_non_nullable as bool, - isMinting: freezed == isMinting - ? _value.isMinting - : isMinting // ignore: cast_nullable_to_non_nullable - as bool?, + signatureMessage: null == signatureMessage + ? _value.signatureMessage + : signatureMessage // ignore: cast_nullable_to_non_nullable + as String, ) as $Val); } } @@ -70,7 +70,7 @@ abstract class _$$GlobalStateImplCopyWith<$Res> __$$GlobalStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool isLoading, bool? isMinting}); + $Res call({bool isLoading, String signatureMessage}); } /// @nodoc @@ -85,17 +85,17 @@ class __$$GlobalStateImplCopyWithImpl<$Res> @override $Res call({ Object? isLoading = null, - Object? isMinting = freezed, + Object? signatureMessage = null, }) { return _then(_$GlobalStateImpl( isLoading: null == isLoading ? _value.isLoading : isLoading // ignore: cast_nullable_to_non_nullable as bool, - isMinting: freezed == isMinting - ? _value.isMinting - : isMinting // ignore: cast_nullable_to_non_nullable - as bool?, + signatureMessage: null == signatureMessage + ? _value.signatureMessage + : signatureMessage // ignore: cast_nullable_to_non_nullable + as String, )); } } @@ -103,16 +103,18 @@ class __$$GlobalStateImplCopyWithImpl<$Res> /// @nodoc class _$GlobalStateImpl implements _GlobalState { - const _$GlobalStateImpl({required this.isLoading, this.isMinting}); + const _$GlobalStateImpl( + {required this.isLoading, this.signatureMessage = ''}); @override final bool isLoading; @override - final bool? isMinting; + @JsonKey() + final String signatureMessage; @override String toString() { - return 'GlobalState(isLoading: $isLoading, isMinting: $isMinting)'; + return 'GlobalState(isLoading: $isLoading, signatureMessage: $signatureMessage)'; } @override @@ -122,12 +124,12 @@ class _$GlobalStateImpl implements _GlobalState { other is _$GlobalStateImpl && (identical(other.isLoading, isLoading) || other.isLoading == isLoading) && - (identical(other.isMinting, isMinting) || - other.isMinting == isMinting)); + (identical(other.signatureMessage, signatureMessage) || + other.signatureMessage == signatureMessage)); } @override - int get hashCode => Object.hash(runtimeType, isLoading, isMinting); + int get hashCode => Object.hash(runtimeType, isLoading, signatureMessage); @JsonKey(ignore: true) @override @@ -139,12 +141,12 @@ class _$GlobalStateImpl implements _GlobalState { abstract class _GlobalState implements GlobalState { const factory _GlobalState( {required final bool isLoading, - final bool? isMinting}) = _$GlobalStateImpl; + final String signatureMessage}) = _$GlobalStateImpl; @override bool get isLoading; @override - bool? get isMinting; + String get signatureMessage; @override @JsonKey(ignore: true) _$$GlobalStateImplCopyWith<_$GlobalStateImpl> get copyWith => diff --git a/lib/shared/widgets/buttons/unwrap_button.dart b/lib/shared/widgets/buttons/unwrap_button.dart new file mode 100644 index 00000000..3724cec6 --- /dev/null +++ b/lib/shared/widgets/buttons/unwrap_button.dart @@ -0,0 +1,100 @@ +import 'package:d_reader_flutter/constants/enums.dart'; +import 'package:d_reader_flutter/features/nft/domain/models/nft.dart'; +import 'package:d_reader_flutter/shared/data/local/local_store.dart'; +import 'package:d_reader_flutter/shared/theme/app_colors.dart'; +import 'package:d_reader_flutter/shared/utils/dialog_triggers.dart'; +import 'package:d_reader_flutter/shared/widgets/buttons/custom_text_button.dart'; +import 'package:d_reader_flutter/shared/widgets/checkbox/custom_labeled_checkbox.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class UnwrapButton extends ConsumerWidget { + final bool isLoading; + final NftModel nft; + final Color backgroundColor, borderColor, loadingColor, textColor; + final Size? size; + final EdgeInsets padding; + final Future Function() onPressed; + const UnwrapButton({ + super.key, + required this.isLoading, + required this.nft, + required this.onPressed, + this.backgroundColor = ColorPalette.dReaderYellow100, + this.borderColor = Colors.transparent, + this.loadingColor = ColorPalette.appBackgroundColor, + this.textColor = Colors.black, + this.padding = const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + this.size, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return CustomTextButton( + backgroundColor: backgroundColor, + borderColor: borderColor, + loadingColor: loadingColor, + padding: padding, + borderRadius: BorderRadius.circular(8), + textColor: Colors.black, + size: size ?? const Size(0, 50), + isLoading: isLoading, + onPressed: isLoading + ? null + : () async { + final shouldTriggerDialog = + LocalStore.instance.get(WalkthroughKeys.unwrap.name) == null; + if (!shouldTriggerDialog) { + return await onPressed(); + } + + bool isChecked = false; + return triggerWalkthroughDialog( + context: context, + buttonText: 'Unwrap', + onSubmit: () async { + if (isChecked) { + LocalStore.instance.put( + WalkthroughKeys.unwrap.name, + true, + ); + } + context.pop(); + await onPressed(); + }, + title: 'Comic unwraping', + subtitle: + 'By unwrapping the comic, you will be able to read it. This action is irreversible and will make the comic lose the mint condition.', + bottomWidget: StatefulBuilder( + builder: (context, setState) { + return CustomLabeledCheckbox( + isChecked: isChecked, + onChange: () { + setState( + () { + isChecked = !isChecked; + }, + ); + }, + label: Text( + 'Don\'t ask me again', + style: Theme.of(context).textTheme.bodySmall, + ), + ); + }, + ), + assetPath: '', + ); + }, + child: Text( + 'Unwrap', + style: + Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor), + ), + ); + } +} diff --git a/lib/shared/widgets/checkbox/custom_labeled_checkbox.dart b/lib/shared/widgets/checkbox/custom_labeled_checkbox.dart index 8a5d1262..73789b77 100644 --- a/lib/shared/widgets/checkbox/custom_labeled_checkbox.dart +++ b/lib/shared/widgets/checkbox/custom_labeled_checkbox.dart @@ -20,7 +20,7 @@ class CustomLabeledCheckbox extends StatelessWidget { onChange(); }, child: Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( height: 20, diff --git a/lib/shared/widgets/dialogs/walkthrough_dialog.dart b/lib/shared/widgets/dialogs/walkthrough_dialog.dart index 9bd14b74..75720823 100644 --- a/lib/shared/widgets/dialogs/walkthrough_dialog.dart +++ b/lib/shared/widgets/dialogs/walkthrough_dialog.dart @@ -59,10 +59,11 @@ class WalkthroughDialog extends StatelessWidget { ), ), const SizedBox( - height: 8, + height: 16, ), CustomTextButton( onPressed: onSubmit, + size: const Size(0, 50), borderRadius: BorderRadius.circular(8), child: Text( buttonText, @@ -75,7 +76,7 @@ class WalkthroughDialog extends StatelessWidget { ), if (bottomWidget != null) ...[ const SizedBox( - height: 8, + height: 16, ), bottomWidget!, ], diff --git a/lib/shared/widgets/icons/favorite_icon_count.dart b/lib/shared/widgets/icons/favorite_icon_count.dart index fe4c150e..57170dee 100644 --- a/lib/shared/widgets/icons/favorite_icon_count.dart +++ b/lib/shared/widgets/icons/favorite_icon_count.dart @@ -1,10 +1,10 @@ import 'package:d_reader_flutter/features/comic/presentation/providers/comic_providers.dart'; import 'package:d_reader_flutter/features/comic_issue/domain/providers/comic_issue_provider.dart'; -import 'package:d_reader_flutter/features/comic_issue/presentation/providers/comic_issue_providers.dart'; import 'package:d_reader_flutter/shared/presentations/providers/global/global_providers.dart'; import 'package:d_reader_flutter/shared/theme/app_colors.dart'; import 'package:d_reader_flutter/shared/utils/formatter.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -26,21 +26,24 @@ class FavoriteIconCount extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final TextTheme textTheme = Theme.of(context).textTheme; + final favoriteNotifier = useState(isFavourite); + final countNotifier = useState(favouritesCount); return GestureDetector( onTap: (slug != null || issueId != null) && !ref.watch(privateLoadingProvider) ? () async { final loadingNotifier = ref.read(privateLoadingProvider.notifier); loadingNotifier.update((state) => true); + favoriteNotifier.value = !favoriteNotifier.value; + countNotifier.value = favoriteNotifier.value + ? ++countNotifier.value + : --countNotifier.value; if (issueId != null) { await ref .read(comicIssueRepositoryProvider) .favouritiseIssue(issueId!); - ref.invalidate(comicIssueDetailsProvider); - ref.invalidate(paginatedIssuesProvider); } else if (slug != null) { await ref.read(updateComicFavouriteProvider(slug!).future); - ref.invalidate(comicSlugProvider); } loadingNotifier.update((state) => false); } @@ -50,12 +53,12 @@ class FavoriteIconCount extends HookConsumerWidget { padding: const EdgeInsets.all(8), constraints: const BoxConstraints(minWidth: 64, minHeight: 42), decoration: BoxDecoration( - color: isFavourite + color: favoriteNotifier.value ? ColorPalette.dReaderRed.withOpacity(.4) : ColorPalette.appBackgroundColor, borderRadius: BorderRadius.circular(4), border: Border.all( - color: isFavourite + color: favoriteNotifier.value ? Colors.transparent : ColorPalette.greyscale300, ), @@ -65,7 +68,7 @@ class FavoriteIconCount extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ SvgPicture.asset( - isFavourite + favoriteNotifier.value ? 'assets/icons/heart.svg' : 'assets/icons/heart_light.svg', width: 16, @@ -75,7 +78,7 @@ class FavoriteIconCount extends HookConsumerWidget { width: 4, ), Text( - Formatter.formatCount(favouritesCount), + Formatter.formatCount(countNotifier.value), style: textTheme.bodyMedium?.copyWith( color: ColorPalette.greyscale100, letterSpacing: .2, @@ -87,7 +90,7 @@ class FavoriteIconCount extends HookConsumerWidget { : Row( children: [ SvgPicture.asset( - isFavourite + favoriteNotifier.value ? 'assets/icons/heart.svg' : 'assets/icons/heart_light.svg', width: 16, @@ -97,7 +100,7 @@ class FavoriteIconCount extends HookConsumerWidget { width: 4, ), Text( - Formatter.formatCount(favouritesCount), + Formatter.formatCount(countNotifier.value), style: textTheme.bodySmall?.copyWith( color: ColorPalette.greyscale100, ), diff --git a/lib/shared/widgets/unsorted/carrot_error_widget.dart b/lib/shared/widgets/unsorted/carrot_error_widget.dart index ee2db165..64d89227 100644 --- a/lib/shared/widgets/unsorted/carrot_error_widget.dart +++ b/lib/shared/widgets/unsorted/carrot_error_widget.dart @@ -25,18 +25,23 @@ class CarrotErrorScaffold extends StatelessWidget { class CarrotErrorWidget extends StatelessWidget { final String adviceText, mainErrorText; final double height; + final EdgeInsets? padding; + final Widget? additionalChild; const CarrotErrorWidget({ super.key, this.adviceText = 'Try resetting the app and make sure it\'s running on the latest version', this.mainErrorText = 'Something broke!', this.height = 300, + this.padding, + this.additionalChild, }); @override Widget build(BuildContext context) { return Container( height: height, + padding: padding, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, @@ -75,7 +80,13 @@ class CarrotErrorWidget extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: ColorPalette.greyscale100, ), - ) + ), + if (additionalChild != null) ...[ + const SizedBox( + height: 16, + ), + additionalChild! + ] ], ), );