diff --git a/lib/model/shared_postcard.dart b/lib/model/shared_postcard.dart index 57c0b0f3a..dfcfb63a0 100644 --- a/lib/model/shared_postcard.dart +++ b/lib/model/shared_postcard.dart @@ -4,7 +4,12 @@ // Use of this source code is governed by the BSD-2-Clause Plus Patent License // that can be found in the LICENSE file. // +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/util/asset_token_ext.dart'; +import 'package:autonomy_flutter/util/constants.dart'; +import 'package:collection/collection.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:nft_collection/database/dao/asset_token_dao.dart'; @JsonSerializable() class SharedPostcard { @@ -24,35 +29,55 @@ class SharedPostcard { owner == other.owner; // fromJson method - factory SharedPostcard.fromJson(Map json) { - return SharedPostcard( - json["tokenID"] as String, - json["owner"] as String, - json["sharedAt"] == null - ? null - : DateTime.parse(json["sharedAt"] as String), - ); - } + factory SharedPostcard.fromJson(Map json) => SharedPostcard( + json['tokenID'] as String, + json['owner'] as String, + json['sharedAt'] == null + ? null + : DateTime.parse(json['sharedAt'] as String), + ); + + Map toJson() => { + 'tokenID': tokenID, + 'owner': owner, + 'sharedAt': sharedAt?.toIso8601String(), + }; - Map toJson() { - return { - "tokenID": tokenID, - "owner": owner, - "sharedAt": sharedAt?.toIso8601String(), - }; + bool get isExpired { + if (sharedAt == null) { + return false; + } + return sharedAt! + .add(POSTCARD_SHARE_LINK_VALID_DURATION) + .isBefore(DateTime.now()); } @override - int get hashCode { - return tokenID.hashCode ^ owner.hashCode; + int get hashCode => tokenID.hashCode ^ owner.hashCode; +} + +extension ListSharedPostcard on List { + Future> get expiredPostcards async { + final expiredPostcards = []; + await Future.wait(map((postcard) async { + if (postcard.isExpired) { + final token = await injector() + .findAssetTokenByIdAndOwner(postcard.tokenID, postcard.owner); + if (token != null && + token.getArtists.lastOrNull?.id == postcard.owner) { + expiredPostcards.add(postcard); + } + } + })); + return expiredPostcards; } } extension Unique on List { List unique([Id Function(E element)? id, bool inplace = true]) { final ids = {}; - var list = inplace ? this : List.from(this); - list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id)); + var list = inplace ? this : List.from(this) + ..retainWhere((x) => ids.add(id != null ? id(x) : x as Id)); return list; } } diff --git a/lib/screen/interactive_postcard/claim_empty_postcard/claim_empty_postcard_screen.dart b/lib/screen/interactive_postcard/claim_empty_postcard/claim_empty_postcard_screen.dart index 35f2d08f0..dc30f6135 100644 --- a/lib/screen/interactive_postcard/claim_empty_postcard/claim_empty_postcard_screen.dart +++ b/lib/screen/interactive_postcard/claim_empty_postcard/claim_empty_postcard_screen.dart @@ -56,7 +56,7 @@ class _ClaimEmptyPostCardScreenState extends State { listener: (context, state) { if (state.isClaimed == true) { unawaited(injector() - .selectPromptsThenStamp(context, state.assetToken!)); + .selectPromptsThenStamp(context, state.assetToken!, null)); } if (state.error != null) { _handleError(state.error!); diff --git a/lib/screen/interactive_postcard/claim_empty_postcard/pay_to_mint_postcard_screen.dart b/lib/screen/interactive_postcard/claim_empty_postcard/pay_to_mint_postcard_screen.dart index cc136dde4..5302c83d6 100644 --- a/lib/screen/interactive_postcard/claim_empty_postcard/pay_to_mint_postcard_screen.dart +++ b/lib/screen/interactive_postcard/claim_empty_postcard/pay_to_mint_postcard_screen.dart @@ -79,12 +79,14 @@ class _PayToMintPostcardScreenState extends State { enabled: state.isClaiming != true, isProcessing: state.isClaiming == true, onTap: () { - unawaited(injector() - .selectPromptsThenStamp( - context, - state.assetToken!.copyWith( - owner: widget.claimRequest.address, - tokenId: widget.claimRequest.tokenId))); + unawaited( + injector().selectPromptsThenStamp( + context, + state.assetToken!.copyWith( + owner: widget.claimRequest.address, + tokenId: widget.claimRequest.tokenId), + null, + )); }, color: POSTCARD_GREEN_BUTTON_COLOR, ), diff --git a/lib/screen/interactive_postcard/design_stamp.dart b/lib/screen/interactive_postcard/design_stamp.dart index ee5d87f9b..f711c1cf9 100644 --- a/lib/screen/interactive_postcard/design_stamp.dart +++ b/lib/screen/interactive_postcard/design_stamp.dart @@ -270,9 +270,9 @@ class _DesignStampPageState extends State { await Navigator.of(context).pushNamed( HandSignaturePage.handSignaturePage, arguments: HandSignaturePayload( - bytes!, - widget.payload.asset, - )); + bytes!, + widget.payload.asset, + widget.payload.shareCode)); setState(() { _line = true; @@ -438,6 +438,7 @@ class StampPainter extends CustomPainter { class DesignStampPayload { final AssetToken asset; final bool allowPop; + final String? shareCode; - DesignStampPayload(this.asset, this.allowPop); + DesignStampPayload(this.asset, this.allowPop, this.shareCode); } diff --git a/lib/screen/interactive_postcard/hand_signature_page.dart b/lib/screen/interactive_postcard/hand_signature_page.dart index 1555e86c2..e6914aca1 100644 --- a/lib/screen/interactive_postcard/hand_signature_page.dart +++ b/lib/screen/interactive_postcard/hand_signature_page.dart @@ -274,7 +274,8 @@ class _HandSignaturePageState extends State { imagePath: imageDataFile.path, metadataPath: metadataFile.path, asset: asset, - location: geoLocation.position))); + location: geoLocation.position, + shareCode: widget.payload.shareCode))); }, color: AppColor.momaGreen, ), @@ -295,9 +296,11 @@ class _HandSignaturePageState extends State { class HandSignaturePayload { final Uint8List image; final AssetToken asset; + final String? shareCode; HandSignaturePayload( this.image, this.asset, + this.shareCode, ); } diff --git a/lib/screen/interactive_postcard/postcard_detail_page.dart b/lib/screen/interactive_postcard/postcard_detail_page.dart index 62cf7a316..77b62910c 100644 --- a/lib/screen/interactive_postcard/postcard_detail_page.dart +++ b/lib/screen/interactive_postcard/postcard_detail_page.dart @@ -142,6 +142,71 @@ class ClaimedPostcardDetailPageState extends State ); } + Future _showSharingExpired(BuildContext context) async { + await UIHelper.showPostcardDrawerAction(context, options: [ + OptionItem( + builder: (context, _) => Row( + children: [ + const SizedBox(width: 15), + SizedBox( + width: 30, + child: SvgPicture.asset( + 'assets/images/restart.svg', + width: 24, + height: 24, + ), + ), + const SizedBox(width: 18), + Expanded( + child: Text( + 'you_need_resend'.tr(), + style: Theme.of(context).textTheme.moMASans700Black18, + ), + ), + ], + ), + ), + OptionItem( + builder: (context, _) => Row( + children: [ + const SizedBox(width: 15), + SvgPicture.asset( + 'assets/images/arrow_right.svg', + width: 24, + height: 24, + ), + const SizedBox(width: 18), + Expanded( + child: Text( + 'no_one_received'.tr(), + style: Theme.of(context).textTheme.moMASans700AuGrey18, + ), + ), + ], + ), + ), + OptionItem( + builder: (context, _) => Row( + children: [ + const SizedBox(width: 15), + SvgPicture.asset( + 'assets/images/cross.svg', + width: 24, + height: 24, + ), + const SizedBox(width: 18), + Expanded( + child: Text( + 'resend_new_link'.tr(), + style: Theme.of(context).textTheme.moMASans700AuGrey18, + ), + ), + ], + ), + ) + ]); + } + Future _removeShareConfig(AssetToken assetToken) async { await _configurationService.removeSharedPostcardWhere( (p) => p.owner == assetToken.owner && p.tokenID == assetToken.id); @@ -384,6 +449,15 @@ class ClaimedPostcardDetailPageState extends State if (assetToken.didSendNext) { unawaited(_removeShareConfig(assetToken)); } + + if (assetToken.isShareExpired && + (assetToken.isLastOwner && !isViewOnly)) { + if (!mounted) { + return; + } + unawaited(_showSharingExpired(context)); + unawaited(_removeShareConfig(assetToken)); + } } if (!mounted) { return; @@ -644,7 +718,7 @@ class ClaimedPostcardDetailPageState extends State ); } if (!(asset.isStamping || asset.isStamped || asset.isProcessingStamp)) { - return PostcardButton( + return PostcardAsyncButton( text: 'stamp_postcard'.tr(), onTap: () async { if (asset.numberOwners > 1) { @@ -652,20 +726,20 @@ class ClaimedPostcardDetailPageState extends State text: 'continue'.tr(), fontSize: 18, onTap: () async { - unawaited(injector().popAndPushNamed( + await injector().popAndPushNamed( AppRouter.designStamp, - arguments: DesignStampPayload(asset, false))); + arguments: DesignStampPayload(asset, false, null)); }, color: AppColor.momaGreen, ); final page = _postcardPreview(context, asset); - unawaited(Navigator.of(context).pushNamed( + await Navigator.of(context).pushNamed( AppRouter.postcardExplain, arguments: PostcardExplainPayload(asset, button, pages: [page]), - )); + ); } else { - unawaited(injector() - .selectPromptsThenStamp(context, asset)); + await injector() + .selectPromptsThenStamp(context, asset, null); } }, color: MoMAColors.moMA8, diff --git a/lib/screen/interactive_postcard/postcard_explain.dart b/lib/screen/interactive_postcard/postcard_explain.dart index 66d49b64f..21c4d1930 100644 --- a/lib/screen/interactive_postcard/postcard_explain.dart +++ b/lib/screen/interactive_postcard/postcard_explain.dart @@ -438,6 +438,7 @@ class _PostcardExplainState extends State { postcardAspectRatio, child: PostcardViewWidget( assetToken: asset, + withPreviewStamp: true, ), ), ), diff --git a/lib/screen/interactive_postcard/prompt_page.dart b/lib/screen/interactive_postcard/prompt_page.dart index 09ce3fa1a..87d36dd6c 100644 --- a/lib/screen/interactive_postcard/prompt_page.dart +++ b/lib/screen/interactive_postcard/prompt_page.dart @@ -96,8 +96,8 @@ class _PromptPageState extends State { Prompt.getUserPrompt(_controller.text.trim())); await Navigator.of(context).pushNamed( AppRouter.designStamp, - arguments: - DesignStampPayload(assetWithPrompt, true)); + arguments: DesignStampPayload(assetWithPrompt, true, + widget.payload.shareCode)); }, )), Flexible( diff --git a/lib/screen/interactive_postcard/stamp_preview.dart b/lib/screen/interactive_postcard/stamp_preview.dart index 0b8d57542..0093c7971 100644 --- a/lib/screen/interactive_postcard/stamp_preview.dart +++ b/lib/screen/interactive_postcard/stamp_preview.dart @@ -14,6 +14,7 @@ import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; import 'package:autonomy_flutter/service/postcard_service.dart'; import 'package:autonomy_flutter/util/asset_token_ext.dart'; +import 'package:autonomy_flutter/util/dio_exception_ext.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; @@ -22,6 +23,7 @@ import 'package:autonomy_flutter/view/postcard_button.dart'; import 'package:autonomy_flutter/view/responsive.dart'; import 'package:autonomy_theme/autonomy_theme.dart'; import 'package:autonomy_theme/extensions/theme_extension/moma_sans.dart'; +import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -65,23 +67,53 @@ class _StampPreviewState extends State with AfterLayoutMixin { setState(() { confirming = true; }); - await _postcardService - .finalizeStamp(widget.payload.asset, widget.payload.imagePath, - widget.payload.metadataPath, widget.payload.location) - .then((final bool isStampSuccess) async { - _setTimer(); - if (mounted) { - setState(() { - confirming = false; - }); - if (!isStampSuccess) { - await UIHelper.showPostcardStampFailed(context); - } + try { + await _postcardService + .finalizeStamp( + asset: widget.payload.asset, + imagePath: widget.payload.imagePath, + metadataPath: widget.payload.metadataPath, + location: widget.payload.location, + shareCode: widget.payload.shareCode) + .then((final bool isStampSuccess) async { + _setTimer(); if (mounted) { - _onClose(context); + setState(() { + confirming = false; + }); + if (!isStampSuccess) { + await UIHelper.showPostcardStampFailed(context); + } + if (mounted) { + _onClose(context); + } + } + }); + } catch (e) { + if (e is DioException) { + if (!mounted) { + return; + } + if (e.isAlreadyClaimedPostcard) { + await UIHelper.showAlreadyClaimedPostcard( + context, + e, + ); + } else if (e.isFailToClaimPostcard) { + await UIHelper.showReceivePostcardFailed( + context, + e, + ); } + if (!mounted) { + return; + } + unawaited(Navigator.of(context).pushNamedAndRemoveUntil( + AppRouter.homePage, + (route) => false, + )); } - }); + } } void _onClose(BuildContext context) { @@ -200,6 +232,7 @@ class StampPreviewPayload { final String imagePath; final String metadataPath; final Location location; + final String? shareCode; // constructor StampPreviewPayload({ @@ -207,6 +240,7 @@ class StampPreviewPayload { required this.imagePath, required this.metadataPath, required this.location, + required this.shareCode, }); } diff --git a/lib/screen/send_receive_postcard/receive_postcard_page.dart b/lib/screen/send_receive_postcard/receive_postcard_page.dart index 8f14e28d2..3bda534b2 100644 --- a/lib/screen/send_receive_postcard/receive_postcard_page.dart +++ b/lib/screen/send_receive_postcard/receive_postcard_page.dart @@ -11,10 +11,8 @@ import 'package:autonomy_flutter/service/navigation_service.dart'; import 'package:autonomy_flutter/service/postcard_service.dart'; import 'package:autonomy_flutter/util/asset_token_ext.dart'; import 'package:autonomy_flutter/util/constants.dart'; -import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/util/wallet_utils.dart'; import 'package:autonomy_flutter/view/postcard_button.dart'; -import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -42,6 +40,8 @@ class ReceivePostCardPage extends StatefulWidget { class _ReceivePostCardPageState extends State { final metricClient = injector.get(); final tokenService = injector.get(); + final accountService = injector(); + final postcardService = injector(); late bool _isProcessing; late bool _isConfirming; late AssetToken assetToken; @@ -103,13 +103,9 @@ class _ReceivePostCardPageState extends State { } } - Future _receivePostcard( - BuildContext context, AssetToken asset) async { - final geoLocation = internetUserGeoLocation; - final accountService = injector(); - final addresses = await accountService.getAddress(asset.blockchain); + Future _getAddress(String blockchain) async { + final addresses = await accountService.getAddress(blockchain); String? address; - AssetToken? pendingToken; if (addresses.isEmpty) { final defaultPersona = await accountService.getOrCreateDefaultPersona(); final walletAddress = @@ -133,42 +129,28 @@ class _ReceivePostCardPageState extends State { }, ); } + return address; + } + + Future _receivePostcard( + BuildContext context, AssetToken asset) async { + AssetToken? pendingToken; + final address = await _getAddress(asset.blockchain); if (address != null) { - try { - pendingToken = - await injector.get().claimSharedPostcardToAddress( - address: address, - assetToken: asset, - location: geoLocation.position, - shareCode: widget.shareCode, - ); - if (!mounted) { - return null; - } - unawaited(Navigator.of(context).pushNamedAndRemoveUntil( - AppRouter.homePage, - (route) => false, - )); - unawaited(Navigator.of(context).pushNamed(AppRouter.designStamp, - arguments: DesignStampPayload(pendingToken, false))); - } catch (e) { - if (e is DioException) { - if (!mounted) { - return null; - } - await UIHelper.showAlreadyClaimedPostcard( - context, - e, - ); - if (!mounted) { - return null; - } - unawaited(Navigator.of(context).pushNamedAndRemoveUntil( - AppRouter.homePage, - (route) => false, - )); - } + pendingToken = postcardService.getPendingTokenAfterClaimShare( + address: address, + assetToken: asset, + ); + if (!mounted) { + return null; } + unawaited(Navigator.of(context).pushNamedAndRemoveUntil( + AppRouter.homePage, + (route) => false, + )); + unawaited(Navigator.of(context).pushNamed(AppRouter.designStamp, + arguments: + DesignStampPayload(pendingToken, false, widget.shareCode))); } setState(() { _isProcessing = false; diff --git a/lib/screen/settings/crypto/wallet_detail/wallet_detail_bloc.dart b/lib/screen/settings/crypto/wallet_detail/wallet_detail_bloc.dart index feb609618..a1cce0e9f 100644 --- a/lib/screen/settings/crypto/wallet_detail/wallet_detail_bloc.dart +++ b/lib/screen/settings/crypto/wallet_detail/wallet_detail_bloc.dart @@ -34,15 +34,18 @@ class WalletDetailBloc extends AuBloc { final balance = await _ethereumService.getBalance(event.address); newState.balance = '${EthAmountFormatter(balance.getInWei).format()} ETH'; - final balanceInUSD = ''' - ${FiatFormatter(balance.getInWei.toDouble() / pow(10, 18) / double.parse(exchangeRate.eth)).format()} USD'''; + final usdBalance = balance.getInWei.toDouble() / + pow(10, 18) * + double.parse(exchangeRate.eth); + final balanceInUSD = '${FiatFormatter(usdBalance).format()} USD'; newState.balanceInUSD = balanceInUSD; break; case CryptoType.XTZ: final balance = await _tezosService.getBalance(event.address); newState.balance = '${XtzAmountFormatter(balance).format()} XTZ'; - final balanceInUSD = ''' - ${FiatFormatter(balance / pow(10, 6) / double.parse(exchangeRate.xtz)).format()} USD'''; + final usdBalance = + balance / pow(10, 6) / double.parse(exchangeRate.xtz); + final balanceInUSD = '${FiatFormatter(usdBalance).format()} USD'; newState.balanceInUSD = balanceInUSD; break; diff --git a/lib/service/navigation_service.dart b/lib/service/navigation_service.dart index 2b5cf8bfa..e0a3f0abd 100644 --- a/lib/service/navigation_service.dart +++ b/lib/service/navigation_service.dart @@ -29,9 +29,7 @@ import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_theme/autonomy_theme.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:nft_collection/models/asset_token.dart'; - -// ignore: implementation_imports +import 'package:nft_collection/models/asset_token.dart'; // ignore: implementation_imports import 'package:overlay_support/src/overlay_state_finder.dart'; class NavigationService { @@ -85,12 +83,12 @@ class NavigationService { } Future selectPromptsThenStamp( - BuildContext context, AssetToken asset) async { + BuildContext context, AssetToken asset, String? shareCode) async { final prompt = asset.postcardMetadata.prompt; await popAndPushNamed( prompt == null ? AppRouter.promptPage : AppRouter.designStamp, - arguments: DesignStampPayload(asset, true)); + arguments: DesignStampPayload(asset, true, shareCode)); } Future? navigateUntil( diff --git a/lib/service/postcard_service.dart b/lib/service/postcard_service.dart index 3d5e1a1e9..5ab6f3345 100644 --- a/lib/service/postcard_service.dart +++ b/lib/service/postcard_service.dart @@ -100,15 +100,18 @@ abstract class PostcardService { {required String address, required RequestPostcardResponse requestPostcardResponse}); - Future claimSharedPostcardToAddress({ - required String address, + AssetToken getPendingTokenAfterClaimShare({ required AssetToken assetToken, - required String shareCode, - required Location location, + required String address, }); - Future finalizeStamp(AssetToken asset, String imagePath, - String metadataPath, Location location); + Future finalizeStamp({ + required AssetToken asset, + required String imagePath, + required String metadataPath, + required Location location, + required String? shareCode, + }); Future> getPrompts(String tokenId); } @@ -574,16 +577,8 @@ class PostcardServiceImpl extends PostcardService { } @override - Future claimSharedPostcardToAddress( - {required String address, - required AssetToken assetToken, - required String shareCode, - required Location location}) async { - await receivePostcard( - shareCode: shareCode, - location: location, - address: address, - ); + AssetToken getPendingTokenAfterClaimShare( + {required AssetToken assetToken, required String address}) { var postcardMetadata = assetToken.postcardMetadata; log.info( 'claimSharedPostcardToAddress metadata ${postcardMetadata.toJson()}'); @@ -597,18 +592,16 @@ class PostcardServiceImpl extends PostcardService { balance: 1, owners: newOwners, ); - - await _tokensService.setCustomTokens([pendingToken]); - unawaited(_tokensService.reindexAddresses([address])); - NftCollectionBloc.eventController.add( - GetTokensByOwnerEvent(pageKey: PageKey.init()), - ); return pendingToken; } @override - Future finalizeStamp(AssetToken asset, String imagePath, - String metadataPath, Location location) async { + Future finalizeStamp( + {required AssetToken asset, + required String imagePath, + required String metadataPath, + required Location location, + required String? shareCode}) async { File imageFile = File(imagePath); File metadataFile = File(metadataPath); @@ -636,6 +629,13 @@ class PostcardServiceImpl extends PostcardService { await _configurationService.setProcessingStampPostcard([ processingStampPostcard, ]); + if (shareCode != null) { + await receivePostcard( + shareCode: shareCode, + location: location, + address: address, + ); + } final isStampSuccess = await stampPostcard( tokenId, walletIndex.first, diff --git a/lib/util/asset_token_ext.dart b/lib/util/asset_token_ext.dart index 67bdb09bb..7894b3002 100644 --- a/lib/util/asset_token_ext.dart +++ b/lib/util/asset_token_ext.dart @@ -633,9 +633,8 @@ extension PostcardExtension on AssetToken { final stampingPostcard = injector().getStampingPostcard(); return stampingPostcard.firstWhereOrNull((final element) { - final bool = element.indexId == id && - element.address == owner && - isLastOwner; + final bool = + element.indexId == id && element.address == owner && isLastOwner; return bool; }); } @@ -662,8 +661,8 @@ extension PostcardExtension on AssetToken { bool get isSending { final sharedPostcards = injector().getSharedPostcard(); - return sharedPostcards - .any((element) => element.owner == owner && element.tokenID == id); + return sharedPostcards.any((element) => + !element.isExpired && element.owner == owner && element.tokenID == id); } bool get isLastOwner { @@ -762,4 +761,11 @@ extension PostcardExtension on AssetToken { } return artistOwner != artists.last; } + + bool get isShareExpired { + final sharedPostcards = + injector().getSharedPostcard(); + return sharedPostcards.any((element) => + element.owner == owner && element.tokenID == id && element.isExpired); + } } diff --git a/lib/util/constants.dart b/lib/util/constants.dart index 0fbd44715..76cc2a7eb 100644 --- a/lib/util/constants.dart +++ b/lib/util/constants.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; // ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars const INDEXER_TOKENS_MAXIMUM = 50; const INDEXER_UNKNOWN_SOURCE = 'unknown'; @@ -127,11 +128,11 @@ const Color POSTCARD_GREEN_BUTTON_COLOR = Color.fromRGBO(79, 174, 79, 1); const POSTCARD_ABOUT_THE_PROJECT = 'https://www.moma.org/calendar/exhibitions/5618?preview=true'; -final moMAGeoLocation = - GeoLocation(position: Location(lat: 40.761, lon: -73.980), address: 'MoMA'); +final moMAGeoLocation = GeoLocation( + position: const Location(lat: 40.761, lon: -73.980), address: 'MoMA'); final internetUserGeoLocation = - GeoLocation(position: Location(lat: null, lon: null), address: 'Web'); + GeoLocation(position: const Location(lat: null, lon: null), address: 'Web'); const int MAX_STAMP_IN_POSTCARD = 15; @@ -157,6 +158,8 @@ double get postcardAspectRatio => Platform.isAndroid const double STAMP_ASPECT_RATIO = 345.0 / 378; +const POSTCARD_SHARE_LINK_VALID_DURATION = Duration(hours: 24); + const USDC_CONTRACT_ADDRESS_GOERLI = '0x07865c6E87B9F70255377e024ace6630C1Eaa37F'; const USDC_CONTRACT_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; @@ -392,7 +395,9 @@ enum AnnouncementID { enum StatusCode { notFound(404), - success(200); + success(200), + forbidden(403), + badRequest(400); const StatusCode(this.value); diff --git a/lib/util/dio_exception_ext.dart b/lib/util/dio_exception_ext.dart index 4569a4f54..c3f1034b4 100644 --- a/lib/util/dio_exception_ext.dart +++ b/lib/util/dio_exception_ext.dart @@ -1,3 +1,4 @@ +import 'package:autonomy_flutter/util/constants.dart'; import 'package:dio/dio.dart'; extension PostcardExepctionExt on DioException { @@ -21,12 +22,22 @@ extension PostcardExepctionExt on DioException { bool get isPostcardNotInMiami => statusCode == PostcardExceptionType.notInMiami.statusCode && dataMessage == PostcardExceptionType.notInMiami.errorMessage; + + bool get isAlreadyClaimedPostcard => + dataMessage == PostcardExceptionType.alreadyClaimed.errorMessage && + statusCode == PostcardExceptionType.alreadyClaimed.statusCode; + + bool get isFailToClaimPostcard => + dataMessage == PostcardExceptionType.failToClaimPostcard.errorMessage && + statusCode == PostcardExceptionType.failToClaimPostcard.statusCode; } enum PostcardExceptionType { alreadyStamped, tooManyRequest, - notInMiami; + notInMiami, + alreadyClaimed, + failToClaimPostcard; String get errorMessage { switch (this) { @@ -34,6 +45,10 @@ enum PostcardExceptionType { return 'blockchain tx request is already existed'; case PostcardExceptionType.notInMiami: return 'only allowed in Miami'; + case PostcardExceptionType.alreadyClaimed: + return 'fail to claim a postcard'; + case PostcardExceptionType.failToClaimPostcard: + return 'fail to claim a postcard'; default: return ''; } @@ -45,6 +60,10 @@ enum PostcardExceptionType { return 429; case PostcardExceptionType.notInMiami: return 403; + case PostcardExceptionType.alreadyClaimed: + return StatusCode.forbidden.value; + case PostcardExceptionType.failToClaimPostcard: + return StatusCode.badRequest.value; default: return 0; } diff --git a/lib/util/ui_helper.dart b/lib/util/ui_helper.dart index e10b9e02c..968f6e773 100644 --- a/lib/util/ui_helper.dart +++ b/lib/util/ui_helper.dart @@ -1390,7 +1390,7 @@ class UIHelper { static showReceivePostcardFailed( BuildContext context, DioException error) async => showErrorDialog(context, 'accept_postcard_failed'.tr(), - error.response?.data['message'], 'close'.tr()); + 'postcard_has_been_claimed'.tr(), 'close'.tr()); static showAlreadyClaimedPostcard( BuildContext context, DioException error) async => diff --git a/lib/view/artwork_common_widget.dart b/lib/view/artwork_common_widget.dart index 6bd56c1b9..39bcdd82e 100644 --- a/lib/view/artwork_common_widget.dart +++ b/lib/view/artwork_common_widget.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/environment.dart'; +import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/model/ff_account.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; @@ -43,13 +44,13 @@ import 'package:path/path.dart' as p; import 'package:url_launcher/url_launcher.dart'; import 'package:uuid/uuid.dart'; -import '../common/injector.dart'; - String getEditionSubTitle(AssetToken token) { - if (token.editionName != null && token.editionName != "") { + if (token.editionName != null && token.editionName != '') { return token.editionName!; } - if (token.edition == 0) return ""; + if (token.edition == 0) { + return ''; + } return token.maxEdition != null && token.maxEdition! >= 1 ? tr('edition_of', args: [token.edition.toString(), token.maxEdition.toString()]) @@ -60,14 +61,13 @@ class MintTokenWidget extends StatelessWidget { final String? thumbnail; final String? tokenId; - const MintTokenWidget({Key? key, this.thumbnail, this.tokenId}) - : super(key: key); + const MintTokenWidget({super.key, this.thumbnail, this.tokenId}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Semantics( - label: "gallery_artwork_${tokenId}_minting", + label: 'gallery_artwork_${tokenId}_minting', child: Container( color: theme.auLightGrey, padding: const EdgeInsets.all(10), @@ -77,7 +77,7 @@ class MintTokenWidget extends StatelessWidget { Align( alignment: AlignmentDirectional.bottomStart, child: Text( - "minting_token".tr(), + 'minting_token'.tr(), style: theme.textTheme.ppMori700QuickSilver8, ), ), @@ -92,14 +92,13 @@ class PendingTokenWidget extends StatelessWidget { final String? thumbnail; final String? tokenId; - const PendingTokenWidget({Key? key, this.thumbnail, this.tokenId}) - : super(key: key); + const PendingTokenWidget({super.key, this.thumbnail, this.tokenId}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Semantics( - label: "gallery_artwork_${tokenId}_pending", + label: 'gallery_artwork_${tokenId}_pending', child: Container( color: theme.auLightGrey, padding: const EdgeInsets.all(10), @@ -127,7 +126,7 @@ class PendingTokenWidget extends StatelessWidget { Align( alignment: AlignmentDirectional.bottomStart, child: Text( - "pending_token".tr(), + 'pending_token'.tr(), style: theme.textTheme.ppMori700QuickSilver8, ), ), @@ -161,6 +160,7 @@ Widget tokenGalleryThumbnailWidget( final cacheManager = injector(); Future cachingState = _cachingStates[thumbnailUrl] ?? + // ignore: discarded_futures cacheManager.store.retrieveCacheData(thumbnailUrl).then((cachedObject) { final cached = cachedObject != null; if (cached) { @@ -170,13 +170,13 @@ Widget tokenGalleryThumbnailWidget( }); return Semantics( - label: "gallery_artwork_${token.id}", + label: 'gallery_artwork_${token.id}', child: Hero( tag: useHero - ? "gallery_thumbnail_${token.id}_${token.owner}" + ? 'gallery_thumbnail_${token.id}_${token.owner}' : const Uuid().v4(), key: const Key('Artwork_Thumbnail'), - child: ext == ".svg" + child: ext == '.svg' ? SvgImage( url: thumbnailUrl, loadingWidgetBuilder: (_) => const GalleryThumbnailPlaceholder(), @@ -195,14 +195,12 @@ Widget tokenGalleryThumbnailWidget( cacheManager: cacheManager, placeholder: (context, index) => FutureBuilder( future: cachingState, - builder: (context, snapshot) { - return GalleryThumbnailPlaceholder( - loading: !(snapshot.data ?? true), - ); - }), + builder: (context, snapshot) => GalleryThumbnailPlaceholder( + loading: !(snapshot.data ?? true), + )), errorWidget: (context, url, error) => CachedNetworkImage( imageUrl: - token.getGalleryThumbnailUrl(usingThumbnailID: false) ?? "", + token.getGalleryThumbnailUrl(usingThumbnailID: false) ?? '', fadeInDuration: Duration.zero, fit: BoxFit.cover, memCacheHeight: cachedImageSize, @@ -212,11 +210,9 @@ Widget tokenGalleryThumbnailWidget( cacheManager: cacheManager, placeholder: (context, index) => FutureBuilder( future: cachingState, - builder: (context, snapshot) { - return GalleryThumbnailPlaceholder( - loading: !(snapshot.data ?? true), - ); - }), + builder: (context, snapshot) => GalleryThumbnailPlaceholder( + loading: !(snapshot.data ?? true), + )), errorWidget: (context, url, error) => const GalleryThumbnailErrorWidget(), ), @@ -228,8 +224,7 @@ Widget tokenGalleryThumbnailWidget( class GalleryUnSupportThumbnailWidget extends StatelessWidget { final String type; - const GalleryUnSupportThumbnailWidget({Key? key, this.type = '.svg'}) - : super(key: key); + const GalleryUnSupportThumbnailWidget({super.key, this.type = '.svg'}); @override Widget build(BuildContext context) { @@ -262,13 +257,13 @@ class GalleryUnSupportThumbnailWidget extends StatelessWidget { } class GalleryThumbnailErrorWidget extends StatelessWidget { - const GalleryThumbnailErrorWidget({Key? key}) : super(key: key); + const GalleryThumbnailErrorWidget({super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.all(10), color: theme.auLightGrey, child: Stack( children: [ @@ -281,7 +276,7 @@ class GalleryThumbnailErrorWidget extends StatelessWidget { Align( alignment: AlignmentDirectional.bottomStart, child: Text( - "IPFS_error".tr(), + 'IPFS_error'.tr(), style: theme.textTheme.ppMori700QuickSilver8, ), ), @@ -294,8 +289,7 @@ class GalleryThumbnailErrorWidget extends StatelessWidget { class GalleryNoThumbnailWidget extends StatelessWidget { final CompactedAssetToken assetToken; - const GalleryNoThumbnailWidget({Key? key, required this.assetToken}) - : super(key: key); + const GalleryNoThumbnailWidget({required this.assetToken, super.key}); String getAssetDefault() { switch (assetToken.getMimeType) { @@ -330,7 +324,7 @@ class GalleryNoThumbnailWidget extends StatelessWidget { Align( alignment: AlignmentDirectional.bottomStart, child: Text( - "no_thumbnail".tr(), + 'no_thumbnail'.tr(), style: theme.textTheme.ppMori700QuickSilver8, ), ), @@ -344,15 +338,15 @@ class GalleryThumbnailPlaceholder extends StatelessWidget { final bool loading; const GalleryThumbnailPlaceholder({ - Key? key, + super.key, this.loading = true, - }) : super(key: key); + }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Semantics( - label: loading ? "loading" : "", + label: loading ? 'loading' : '', child: AspectRatio( aspectRatio: 1, child: Container( @@ -376,7 +370,7 @@ class GalleryThumbnailPlaceholder extends StatelessWidget { child: Align( alignment: AlignmentDirectional.bottomStart, child: Text( - "loading".tr(), + 'loading'.tr(), style: theme.textTheme.ppMori700QuickSilver8, ), ), @@ -400,7 +394,7 @@ Widget placeholder(BuildContext context) { mainAxisAlignment: MainAxisAlignment.center, children: [ GifView.asset( - "assets/images/loading_white.gif", + 'assets/images/loading_white.gif', width: 52, height: 52, frameRate: 12, @@ -409,7 +403,7 @@ Widget placeholder(BuildContext context) { height: 12, ), Text( - "loading...".tr(), + 'loading...'.tr(), style: ResponsiveLayout.isMobile ? theme.textTheme.ppMori400White12 : theme.textTheme.ppMori400White14, @@ -433,23 +427,22 @@ INFTRenderingWidget buildRenderingWidget( Widget? loadingWidget, }) { String mimeType = assetToken.getMimeType; - final renderingWidget = typesOfNFTRenderingWidget(mimeType); - - renderingWidget.setRenderWidgetBuilder(RenderingWidgetBuilder( - previewURL: attempt == null - ? assetToken.getPreviewUrl() - : "${assetToken.getPreviewUrl()}?t=$attempt", - thumbnailURL: assetToken.getGalleryThumbnailUrl(usingThumbnailID: false), - loadingWidget: loadingWidget ?? previewPlaceholder(context), - errorWidget: BrokenTokenWidget(token: assetToken), - cacheManager: injector(), - onLoaded: onLoaded, - onDispose: onDispose, - overriddenHtml: overriddenHtml, - skipViewport: assetToken.scrollable ?? false, - isMute: isMute, - focusNode: focusNode, - )); + final renderingWidget = typesOfNFTRenderingWidget(mimeType) + ..setRenderWidgetBuilder(RenderingWidgetBuilder( + previewURL: attempt == null + ? assetToken.getPreviewUrl() + : '${assetToken.getPreviewUrl()}?t=$attempt', + thumbnailURL: assetToken.getGalleryThumbnailUrl(usingThumbnailID: false), + loadingWidget: loadingWidget ?? previewPlaceholder(context), + errorWidget: BrokenTokenWidget(token: assetToken), + cacheManager: injector(), + onLoaded: onLoaded, + onDispose: onDispose, + overriddenHtml: overriddenHtml, + skipViewport: assetToken.scrollable ?? false, + isMute: isMute, + focusNode: focusNode, + )); return renderingWidget; } @@ -465,8 +458,7 @@ class RetryCubit extends Cubit { class PreviewUnSupportedTokenWidget extends StatelessWidget { final AssetToken token; - const PreviewUnSupportedTokenWidget({Key? key, required this.token}) - : super(key: key); + const PreviewUnSupportedTokenWidget({required this.token, super.key}); @override Widget build(BuildContext context) { @@ -498,7 +490,7 @@ class PreviewUnSupportedTokenWidget extends StatelessWidget { GestureDetector( onTap: () {}, child: Text( - "hide_from_collection".tr(), + 'hide_from_collection'.tr(), style: theme.textTheme.ppMori400Black12 .copyWith(color: AppColor.auSuperTeal), ), @@ -515,12 +507,10 @@ class PreviewUnSupportedTokenWidget extends StatelessWidget { class BrokenTokenWidget extends StatefulWidget { final AssetToken token; - const BrokenTokenWidget({Key? key, required this.token}) : super(key: key); + const BrokenTokenWidget({required this.token, super.key}); @override - State createState() { - return _BrokenTokenWidgetState(); - } + State createState() => _BrokenTokenWidgetState(); } class _BrokenTokenWidgetState extends State @@ -529,10 +519,10 @@ class _BrokenTokenWidgetState extends State @override void afterFirstLayout(BuildContext context) { - metricClient.addEvent( + unawaited(metricClient.addEvent( MixpanelEvent.displayUnableLoadIPFS, data: {'id': widget.token.id}, - ); + )); } @override @@ -564,14 +554,14 @@ class _BrokenTokenWidgetState extends State const Spacer(), GestureDetector( onTap: () { - metricClient.addEvent( + unawaited(metricClient.addEvent( MixpanelEvent.clickLoadIPFSAgain, data: {'id': widget.token.id}, - ); + )); context.read().refresh(); }, child: Text( - "reload".tr(), + 'reload'.tr(), style: theme.textTheme.ppMori400Black12 .copyWith(color: AppColor.auSuperTeal), ), @@ -586,12 +576,11 @@ class _BrokenTokenWidgetState extends State } class CurrentlyCastingArtwork extends StatefulWidget { - const CurrentlyCastingArtwork({Key? key}) : super(key: key); + const CurrentlyCastingArtwork({super.key}); @override - State createState() { - return _CurrentlyCastingArtworkState(); - } + State createState() => + _CurrentlyCastingArtworkState(); } class _CurrentlyCastingArtworkState extends State { @@ -630,14 +619,12 @@ class _CurrentlyCastingArtworkState extends State { } } -Widget previewPlaceholder(BuildContext context) { - return const PreviewPlaceholder(); -} +Widget previewPlaceholder(BuildContext context) => const PreviewPlaceholder(); class PreviewPlaceholder extends StatefulWidget { const PreviewPlaceholder({ - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _PreviewPlaceholderState(); @@ -650,9 +637,9 @@ class _PreviewPlaceholderState extends State @override void dispose() { super.dispose(); - metricClient.addEvent( + unawaited(metricClient.addEvent( MixpanelEvent.showLoadingArtwork, - ); + )); } @override @@ -673,7 +660,7 @@ class _PreviewPlaceholderState extends State mainAxisAlignment: MainAxisAlignment.center, children: [ GifView.asset( - "assets/images/loading_white.gif", + 'assets/images/loading_white.gif', width: 52, height: 52, frameRate: 12, @@ -682,7 +669,7 @@ class _PreviewPlaceholderState extends State height: 13, ), Text( - "loading...".tr(), + 'loading...'.tr(), style: ResponsiveLayout.isMobile ? theme.textTheme.ppMori400White12 : theme.textTheme.ppMori400White14, @@ -697,40 +684,45 @@ class _PreviewPlaceholderState extends State Widget debugInfoWidget(BuildContext context, AssetToken? token) { final theme = Theme.of(context); - if (token == null) return const SizedBox(); + if (token == null) { + return const SizedBox(); + } return FutureBuilder( + // ignore: discarded_futures future: isAppCenterBuild().then((value) { - if (value == false) return Future.value(false); + if (!value) { + return Future.value(false); + } return injector().showTokenDebugInfo(); }), builder: (context, snapshot) { - if (snapshot.data == false) return const SizedBox(); - - TextButton buildInfo(String text, String value) { - return TextButton( - onPressed: () async { - Vibrate.feedback(FeedbackType.light); - final uri = Uri.tryParse(value); - if (uri != null && await canLaunchUrl(uri)) { - launchUrl(uri, mode: LaunchMode.inAppWebView); - } else { - Clipboard.setData(ClipboardData(text: value)); - } - }, - child: Text( - '$text: $value', - style: theme.textTheme.ppMori400White12, - ), - ); + if (snapshot.data == false) { + return const SizedBox(); } + TextButton buildInfo(String text, String value) => TextButton( + onPressed: () async { + Vibrate.feedback(FeedbackType.light); + final uri = Uri.tryParse(value); + if (uri != null && await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.inAppWebView); + } else { + await Clipboard.setData(ClipboardData(text: value)); + } + }, + child: Text( + '$text: $value', + style: theme.textTheme.ppMori400White12, + ), + ); + return Column( children: [ addDivider(), Text( - "debug_info".tr(), + 'debug_info'.tr(), style: theme.textTheme.ppMori400White12, ), buildInfo('IndexerID', token.id), @@ -747,7 +739,7 @@ Widget artworkDetailsRightSection(BuildContext context, AssetToken assetToken) { final artworkID = ((assetToken.swapped ?? false) && assetToken.originTokenInfoId != null) ? assetToken.originTokenInfoId - : assetToken.id.split("-").last; + : assetToken.id.split('-').last; if (assetToken.isPostcard) { return PostcardRightsView( editionID: artworkID, @@ -764,13 +756,13 @@ class ListItemExpandedWidget extends StatefulWidget { final Widget unexpandWidget; const ListItemExpandedWidget({ - Key? key, required this.children, - this.divider, required this.unexpandedCount, required this.expandWidget, required this.unexpandWidget, - }) : super(key: key); + super.key, + this.divider, + }); @override State createState() => _ListItemExpandedWidgetState(); @@ -821,9 +813,8 @@ class _ListItemExpandedWidgetState extends State { } @override - Widget build(BuildContext context) { - return _isExpanded ? expanedWidget(context) : unexpanedWidget(context); - } + Widget build(BuildContext context) => + _isExpanded ? expanedWidget(context) : unexpanedWidget(context); } class SectionExpandedWidget extends StatefulWidget { @@ -837,7 +828,7 @@ class SectionExpandedWidget extends StatefulWidget { final EdgeInsets padding; const SectionExpandedWidget( - {Key? key, + {super.key, this.header, this.headerStyle, this.headerPadding, @@ -845,8 +836,7 @@ class SectionExpandedWidget extends StatefulWidget { this.iconOnExpanded, this.iconOnUnExpaneded, this.withDivicer = true, - this.padding = const EdgeInsets.all(0)}) - : super(key: key); + this.padding = const EdgeInsets.all(0)}); @override State createState() => _SectionExpandedWidgetState(); @@ -894,17 +884,18 @@ class _SectionExpandedWidgetState extends State { theme.textTheme.ppMori400White16, ), const Spacer(), - _isExpanded - ? (widget.iconOnExpanded ?? - RotatedBox( - quarterTurns: -1, - child: defaultIcon, - )) - : widget.iconOnUnExpaneded ?? - RotatedBox( - quarterTurns: 1, - child: defaultIcon, - ) + if (_isExpanded) + widget.iconOnExpanded ?? + RotatedBox( + quarterTurns: -1, + child: defaultIcon, + ) + else + widget.iconOnUnExpaneded ?? + RotatedBox( + quarterTurns: 1, + child: defaultIcon, + ) ], ), ), @@ -916,7 +907,7 @@ class _SectionExpandedWidgetState extends State { visible: _isExpanded, child: Column( children: [ - const SizedBox(height: 23.0), + const SizedBox(height: 23), widget.child ?? const SizedBox(), ], ), @@ -942,8 +933,9 @@ Widget postcardDetailsMetadataSection( color: theme.colorScheme.primary, ); const unexpandedCount = 1; + final otherCount = artists.length - unexpandedCount; return SectionExpandedWidget( - header: "metadata".tr(), + header: 'metadata'.tr(), headerStyle: theme.textTheme.moMASans700Black16.copyWith(fontSize: 18), headerPadding: padding, withDivicer: false, @@ -961,7 +953,7 @@ Widget postcardDetailsMetadataSection( Padding( padding: padding, child: MetaDataItem( - title: "title".tr(), + title: 'title'.tr(), titleStyle: titleStyle, value: assetToken.title ?? '', valueStyle: theme.textTheme.moMASans400Black12, @@ -969,63 +961,62 @@ Widget postcardDetailsMetadataSection( ), if (artists.isNotEmpty) ...[ Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), Padding( padding: padding, child: CustomMetaDataItem( - title: "artists".tr(), + title: 'artists'.tr(), titleStyle: titleStyle, content: ListItemExpandedWidget( expandWidget: Text( - "_others".tr(namedArgs: { - "number": "${artists.length - unexpandedCount}", + (otherCount == 1 ? '_other' : '_others').tr(namedArgs: { + 'number': '$otherCount', }), style: linkStyle, ), unexpandWidget: Text( - "show_less".tr(), + 'show_less'.tr(), style: linkStyle, ), unexpandedCount: unexpandedCount, divider: TextSpan( - text: ", ", + text: ', ', style: textStyle, ), children: [ - ...artists - .mapIndexed((index, artistName) => Text( - artistName, - style: textStyle, - )) - .toList(), + ...artists.mapIndexed((index, artistName) => Text( + artistName, + style: textStyle, + )), ], ), ), ), ], - (assetToken.fungible == false) - ? Column( - children: [ - Divider( - height: 32.0, - color: theme.auLightGrey, - ), - _getEditionNameRow(context, assetToken), - ], - ) - : const SizedBox(), + if (!assetToken.fungible) + Column( + children: [ + Divider( + height: 32, + color: theme.auLightGrey, + ), + _getEditionNameRow(context, assetToken), + ], + ) + else + const SizedBox(), Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), Padding( padding: padding, child: MetaDataItem( - title: "token".tr(), + title: 'token'.tr(), titleStyle: titleStyle, - value: polishSource(assetToken.source ?? ""), + value: polishSource(assetToken.source ?? ''), tapLink: assetToken.isAirdrop ? null : assetToken.assetURL, forceSafariVC: true, valueStyle: theme.textTheme.moMASans400Black12, @@ -1034,13 +1025,13 @@ Widget postcardDetailsMetadataSection( ), ), Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), Padding( padding: padding, child: MetaDataItem( - title: "contract".tr(), + title: 'contract'.tr(), titleStyle: titleStyle, value: assetToken.blockchain.capitalize(), tapLink: assetToken.getBlockchainUrl(), @@ -1051,26 +1042,26 @@ Widget postcardDetailsMetadataSection( ), ), Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), Padding( padding: padding, child: MetaDataItem( - title: "medium".tr(), + title: 'medium'.tr(), titleStyle: titleStyle, value: assetToken.medium?.capitalize() ?? '', valueStyle: theme.textTheme.moMASans400Black12, ), ), Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), Padding( padding: padding, child: MetaDataItem( - title: "date_minted".tr(), + title: 'date_minted'.tr(), titleStyle: titleStyle, value: assetToken.mintedAt != null ? postcardTimeString(assetToken.mintedAt!) @@ -1078,23 +1069,24 @@ Widget postcardDetailsMetadataSection( valueStyle: theme.textTheme.moMASans400Black12, ), ), - assetToken.assetData != null && assetToken.assetData!.isNotEmpty - ? Column( - children: [ - const Divider(height: 32.0), - Padding( - padding: padding, - child: MetaDataItem( - title: "artwork_data".tr(), - titleStyle: titleStyle, - value: assetToken.assetData!, - valueStyle: theme.textTheme.moMASans400Black12, - ), - ) - ], + if (assetToken.assetData != null && assetToken.assetData!.isNotEmpty) + Column( + children: [ + const Divider(height: 32), + Padding( + padding: padding, + child: MetaDataItem( + title: 'artwork_data'.tr(), + titleStyle: titleStyle, + value: assetToken.assetData!, + valueStyle: theme.textTheme.moMASans400Black12, + ), ) - : const SizedBox(), - const SizedBox(height: 16.0), + ], + ) + else + const SizedBox(), + const SizedBox(height: 16), ], ), ); @@ -1105,141 +1097,145 @@ Widget artworkDetailsMetadataSection( final theme = Theme.of(context); final artworkID = ((assetToken.swapped ?? false) && assetToken.originTokenInfoId != null) - ? assetToken.originTokenInfoId ?? "" - : assetToken.id.split("-").last; + ? assetToken.originTokenInfoId ?? '' + : assetToken.id.split('-').last; return SectionExpandedWidget( - header: "metadata".tr(), + header: 'metadata'.tr(), padding: const EdgeInsets.only(bottom: 23), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MetaDataItem( - title: "title".tr(), + title: 'title'.tr(), value: assetToken.title ?? '', ), if (artistName != null) ...[ Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), MetaDataItem( - title: "artist".tr(), + title: 'artist'.tr(), value: artistName, - onTap: () { + onTap: () async { final metricClient = injector.get(); - metricClient.addEvent(MixpanelEvent.clickArtist, data: { + unawaited(metricClient.addEvent(MixpanelEvent.clickArtist, data: { 'id': assetToken.id, 'artistID': assetToken.artistID, - }); + })); final uri = Uri.parse( - assetToken.artistURL?.split(" & ").firstOrNull ?? ""); - launchUrl(uri, mode: LaunchMode.externalApplication); + assetToken.artistURL?.split(' & ').firstOrNull ?? ''); + await launchUrl(uri, mode: LaunchMode.externalApplication); }, forceSafariVC: true, ), ], - (assetToken.fungible == false) - ? Column( - children: [ - Divider( - height: 32.0, - color: theme.auLightGrey, - ), - _getEditionNameRow(context, assetToken), - ], - ) - : const SizedBox(), + if (!assetToken.fungible) + Column( + children: [ + Divider( + height: 32, + color: theme.auLightGrey, + ), + _getEditionNameRow(context, assetToken), + ], + ) + else + const SizedBox(), Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), MetaDataItem( - title: "token".tr(), - value: polishSource(assetToken.source ?? ""), + title: 'token'.tr(), + value: polishSource(assetToken.source ?? ''), tapLink: assetToken.isAirdrop ? null : assetToken.assetURL, forceSafariVC: true, ), Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), - assetToken.source == "feralfile" && artworkID.isNotEmpty - ? FutureBuilder( - future: injector() - .getExhibitionFromTokenID(artworkID), - builder: (context, snapshot) { - if (snapshot.data != null) { - return Column( - children: [ - MetaDataItem( - title: "exhibition".tr(), - value: snapshot.data!.title, - tapLink: feralFileExhibitionUrl(snapshot.data!.slug), - forceSafariVC: true, - ), - Divider( - height: 32.0, - color: theme.auLightGrey, - ), - ], - ); - } else { - return const SizedBox(); - } - }, - ) - : const SizedBox(), + if (assetToken.source == 'feralfile' && artworkID.isNotEmpty) + FutureBuilder( + future: injector() + // ignore: discarded_futures + .getExhibitionFromTokenID(artworkID), + builder: (context, snapshot) { + if (snapshot.data != null) { + return Column( + children: [ + MetaDataItem( + title: 'exhibition'.tr(), + value: snapshot.data!.title, + tapLink: feralFileExhibitionUrl(snapshot.data!.slug), + forceSafariVC: true, + ), + Divider( + height: 32, + color: theme.auLightGrey, + ), + ], + ); + } else { + return const SizedBox(); + } + }, + ) + else + const SizedBox(), MetaDataItem( - title: "contract".tr(), + title: 'contract'.tr(), value: assetToken.blockchain.capitalize(), tapLink: assetToken.getBlockchainUrl(), forceSafariVC: true, ), Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), MetaDataItem( - title: "medium".tr(), + title: 'medium'.tr(), value: assetToken.medium?.capitalize() ?? '', ), Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), MetaDataItem( - title: "date_minted".tr(), + title: 'date_minted'.tr(), value: assetToken.mintedAt != null ? localTimeString(assetToken.mintedAt!) : '', ), - assetToken.assetData != null && assetToken.assetData!.isNotEmpty - ? Column( - children: [ - const Divider(height: 32.0), - MetaDataItem( - title: "artwork_data".tr(), - value: assetToken.assetData!, - ) - ], + if (assetToken.assetData != null && assetToken.assetData!.isNotEmpty) + Column( + children: [ + const Divider(height: 32), + MetaDataItem( + title: 'artwork_data'.tr(), + value: assetToken.assetData!, ) - : const SizedBox(), - const Divider(height: 32.0), + ], + ) + else + const SizedBox(), + const Divider(height: 32), ], ), ); } Widget _getEditionNameRow(BuildContext context, AssetToken assetToken) { - if (assetToken.editionName != null && assetToken.editionName != "") { + if (assetToken.editionName != null && assetToken.editionName != '') { return MetaDataItem( - title: "edition".tr(), + title: 'edition'.tr(), value: assetToken.editionName!, ); } return MetaDataItem( - title: "edition".tr(), + title: 'edition'.tr(), value: assetToken.edition.toString(), ); } @@ -1258,8 +1254,9 @@ Widget postcardOwnership( color: theme.colorScheme.primary, ); const unexpandedCount = 1; + final otherCount = owners.length - unexpandedCount; return SectionExpandedWidget( - header: "token_ownership".tr(), + header: 'token_ownership'.tr(), headerStyle: theme.textTheme.moMASans700Black16.copyWith(fontSize: 18), headerPadding: padding, withDivicer: false, @@ -1277,21 +1274,21 @@ Widget postcardOwnership( Padding( padding: padding, child: Text( - "how_many_shares_you_own".tr(), + 'how_many_shares_you_own'.tr(), style: titleStyle, ), ), - const SizedBox(height: 32.0), + const SizedBox(height: 32), Divider( - height: 40.0, + height: 40, color: theme.auLightGrey, ), Padding( padding: padding, child: MetaDataItem( - title: "shares".tr(), + title: 'shares'.tr(), titleStyle: titleStyle, - value: "${assetToken.maxEdition}", + value: '${assetToken.maxEdition}', tapLink: assetToken.tokenURL, forceSafariVC: true, valueStyle: theme.textTheme.moMASans400Black12, @@ -1302,48 +1299,45 @@ Widget postcardOwnership( height: 20, ), Divider( - height: 40.0, + height: 40, color: theme.auLightGrey, ), Padding( padding: padding, child: CustomMetaDataItem( - title: "owners".tr(), + title: 'owners'.tr(), titleStyle: titleStyle, content: ListItemExpandedWidget( expandWidget: Text( - "_others".tr(namedArgs: { - "number": "${owners.length - unexpandedCount}" - }), + (otherCount == 1 ? '_other' : '_others') + .tr(namedArgs: {'number': '$otherCount'}), style: linkStyle, ), unexpandWidget: Text( - "show_less".tr(), + 'show_less'.tr(), style: linkStyle, ), unexpandedCount: unexpandedCount, children: [ - ...owners.keys - .mapIndexed((index, owner) => Row( - children: [ - Expanded( - child: Text( - owner, - style: theme.textTheme.moMASans400Black12, - ), - ), - Text( - "${owners[owner]}", - style: theme.textTheme.moMASans400Black12, - ), - ], - )) - .toList(), + ...owners.keys.mapIndexed((index, owner) => Row( + children: [ + Expanded( + child: Text( + owner, + style: theme.textTheme.moMASans400Black12, + ), + ), + Text( + '${owners[owner]}', + style: theme.textTheme.moMASans400Black12, + ), + ], + )), ], ), ), ), - const SizedBox(height: 16.0), + const SizedBox(height: 16), ], ), ); @@ -1373,35 +1367,35 @@ Widget tokenOwnership( } return SectionExpandedWidget( - header: "token_ownership".tr(), + header: 'token_ownership'.tr(), padding: const EdgeInsets.only(bottom: 23), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 32.0), + const SizedBox(height: 32), MetaDataItem( - title: "editions".tr(), - value: "${assetToken.maxEdition}", + title: 'editions'.tr(), + value: '${assetToken.maxEdition}', tapLink: assetToken.tokenURL, forceSafariVC: true, ), Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), MetaDataItem( - title: "token_holder".tr(), + title: 'token_holder'.tr(), value: alias.isNotEmpty ? alias : ownerAddress.maskOnly(5), tapLink: addressURL(ownerAddress, CryptoType.fromAddress(ownerAddress)), forceSafariVC: true, ), Divider( - height: 32.0, + height: 32, color: theme.auLightGrey, ), MetaDataItem( - title: "token_held".tr(), + title: 'token_held'.tr(), value: ownedTokens.toString(), tapLink: tapLink, forceSafariVC: true, @@ -1418,12 +1412,12 @@ class CustomMetaDataItem extends StatelessWidget { final bool? forceSafariVC; const CustomMetaDataItem({ - Key? key, required this.title, - this.titleStyle, required this.content, + super.key, + this.titleStyle, this.forceSafariVC, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -1457,16 +1451,16 @@ class MetaDataItem extends StatelessWidget { final TextStyle? linkStyle; const MetaDataItem({ - Key? key, required this.title, - this.titleStyle, required this.value, + super.key, + this.titleStyle, this.onTap, this.tapLink, this.forceSafariVC, this.linkStyle, this.valueStyle, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -1474,7 +1468,7 @@ class MetaDataItem extends StatelessWidget { if (onValueTap == null && tapLink != null) { final uri = Uri.parse(tapLink!); - onValueTap = () => launchUrl(uri, + onValueTap = () async => launchUrl(uri, mode: forceSafariVC == true ? LaunchMode.externalApplication : LaunchMode.platformDefault); @@ -1521,14 +1515,14 @@ class ProvenanceItem extends StatelessWidget { final bool? forceSafariVC; const ProvenanceItem({ - Key? key, required this.title, required this.value, + super.key, this.onTap, this.tapLink, this.forceSafariVC, this.onNameTap, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -1536,7 +1530,7 @@ class ProvenanceItem extends StatelessWidget { if (onValueTap == null && tapLink != null) { final uri = Uri.parse(tapLink!); - onValueTap = () => launchUrl(uri, + onValueTap = () async => launchUrl(uri, mode: forceSafariVC == true ? LaunchMode.externalApplication : LaunchMode.platformDefault); @@ -1599,7 +1593,7 @@ class ProvenanceItem extends StatelessWidget { class HeaderData extends StatelessWidget { final String text; - const HeaderData({Key? key, required this.text}) : super(key: key); + const HeaderData({required this.text, super.key}); @override Widget build(BuildContext context) { @@ -1634,57 +1628,56 @@ class HeaderData extends StatelessWidget { } Widget artworkDetailsProvenanceSectionNotEmpty( - BuildContext context, - List provenances, - HashSet youAddresses, - Map identityMap) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SectionExpandedWidget( - header: "provenance".tr(), - padding: const EdgeInsets.only(bottom: 23), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...provenances.map((el) { - final identity = identityMap[el.owner]; - final identityTitle = el.owner.toIdentityOrMask(identityMap); - final youTitle = - youAddresses.contains(el.owner) ? "_you".tr() : ""; - return Column( - children: [ - ProvenanceItem( - title: (identityTitle ?? '') + youTitle, - value: localTimeString(el.timestamp), - // subTitle: el.blockchain.toUpperCase(), - tapLink: el.txURL, - onNameTap: () => identity != null - ? UIHelper.showIdentityDetailDialog(context, - name: identity, address: el.owner) - : null, - forceSafariVC: true, - ), - const Divider(height: 32.0), - ], - ); - }).toList() - ], + BuildContext context, + List provenances, + HashSet youAddresses, + Map identityMap) => + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionExpandedWidget( + header: 'provenance'.tr(), + padding: const EdgeInsets.only(bottom: 23), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...provenances.map((el) { + final identity = identityMap[el.owner]; + final identityTitle = el.owner.toIdentityOrMask(identityMap); + final youTitle = + youAddresses.contains(el.owner) ? '_you'.tr() : ''; + return Column( + children: [ + ProvenanceItem( + title: (identityTitle ?? '') + youTitle, + value: localTimeString(el.timestamp), + // subTitle: el.blockchain.toUpperCase(), + tapLink: el.txURL, + onNameTap: () => identity != null + ? UIHelper.showIdentityDetailDialog(context, + name: identity, address: el.owner) + : null, + forceSafariVC: true, + ), + const Divider(height: 32), + ], + ); + }) + ], + ), ), - ), - ], - ); -} + ], + ); class PostcardRightsView extends StatefulWidget { final TextStyle? linkStyle; final String? editionID; const PostcardRightsView({ - Key? key, + super.key, this.linkStyle, this.editionID, - }) : super(key: key); + }); @override State createState() => _PostcardRightsViewState(); @@ -1712,7 +1705,7 @@ class _PostcardRightsViewState extends State { builder: (context, snapshot) { if (snapshot.hasData && snapshot.data?.statusCode == 200) { return SectionExpandedWidget( - header: "rights".tr(), + header: 'rights'.tr(), headerStyle: theme.textTheme.moMASans700Black16.copyWith(fontSize: 18), headerPadding: const EdgeInsets.only(left: 15, right: 15), @@ -1729,25 +1722,27 @@ class _PostcardRightsViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Markdown( - key: const Key("rightsSection"), - data: snapshot.data!.data!.replaceAll(".**", "**"), + key: const Key('rightsSection'), + data: snapshot.data!.data!.replaceAll('.**', '**'), softLineBreak: true, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(0), styleSheet: markDownPostcardRightStyle(context), onTapLink: (text, href, title) async { - if (href == null) return; + if (href == null) { + return; + } if (href.isAutonomyDocumentLink) { - injector() + await injector() .openAutonomyDocument(href, text); } else { - launchUrl(Uri.parse(href), + await launchUrl(Uri.parse(href), mode: LaunchMode.externalApplication); } }, ), - const SizedBox(height: 23.0), + const SizedBox(height: 23), ], ), ); @@ -1755,6 +1750,7 @@ class _PostcardRightsViewState extends State { return const SizedBox(); } }, + // ignore: discarded_futures future: dio.get( POSTCARD_RIGHTS_DOCS, )); @@ -1775,10 +1771,10 @@ Widget _rowItem( }) { if (onValueTap == null && tapLink != null) { final uri = Uri.parse(tapLink); - onValueTap = () => launchUrl(uri, + onValueTap = () => unawaited(launchUrl(uri, mode: forceSafariVC == true ? LaunchMode.externalApplication - : LaunchMode.platformDefault); + : LaunchMode.platformDefault)); } final theme = Theme.of(context); @@ -1834,7 +1830,7 @@ Widget _rowItem( ), ), if (onValueTap != null) ...[ - const SizedBox(width: 8.0), + const SizedBox(width: 8), SvgPicture.asset( 'assets/images/iconForward.svg', colorFilter: ColorFilter.mode( @@ -1854,9 +1850,9 @@ class FeralfileArtworkDetailsMetadataSection extends StatelessWidget { final FFSeries series; const FeralfileArtworkDetailsMetadataSection({ - Key? key, required this.series, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -1869,59 +1865,59 @@ class FeralfileArtworkDetailsMetadataSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "metadata".tr(), + 'metadata'.tr(), style: theme.textTheme.displayMedium, ), - const SizedBox(height: 23.0), - _rowItem(context, "title".tr(), series.title), + const SizedBox(height: 23), + _rowItem(context, 'title'.tr(), series.title), const Divider( - height: 32.0, + height: 32, color: AppColor.secondarySpanishGrey, ), if (artist != null) ...[ _rowItem( context, - "artist".tr(), + 'artist'.tr(), artist.getDisplayName(), - tapLink: "${Environment.feralFileAPIURL}/profiles/${artist.id}", + tapLink: '${Environment.feralFileAPIURL}/profiles/${artist.id}', ), const Divider( - height: 32.0, + height: 32, color: AppColor.secondarySpanishGrey, ) ], _rowItem( context, - "token".tr(), - "Feral File", + 'token'.tr(), + 'Feral File', // tapLink: "${Environment.feralFileAPIURL}/artworks/${artwork?.id}" ), const Divider( - height: 32.0, + height: 32, color: AppColor.secondarySpanishGrey, ), _rowItem( context, - "contract".tr(), + 'contract'.tr(), contract?.blockchainType.capitalize() ?? '', tapLink: contract?.getBlockChainUrl(), ), const Divider( - height: 32.0, + height: 32, color: AppColor.secondarySpanishGrey, ), _rowItem( context, - "medium".tr(), + 'medium'.tr(), series.medium.capitalize(), ), const Divider( - height: 32.0, + height: 32, color: AppColor.secondarySpanishGrey, ), _rowItem( context, - "date_minted".tr(), + 'date_minted'.tr(), mintDate != null ? df.format(mintDate).toUpperCase() : null, maxLines: 1, ), @@ -1939,14 +1935,14 @@ class ArtworkDetailsHeader extends StatelessWidget { final bool isReverse; const ArtworkDetailsHeader({ - Key? key, required this.title, required this.subTitle, + super.key, this.hideArtist = false, this.onTitleTap, this.onSubTitleTap, this.isReverse = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/pubspec.lock b/pubspec.lock index 8291fe1af..77ecbf03f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1742,8 +1742,8 @@ packages: dependency: "direct main" description: path: "." - ref: e346141fd9ad4f0667b4c5a403fd8112f76ae5f3 - resolved-ref: e346141fd9ad4f0667b4c5a403fd8112f76ae5f3 + ref: "93f3bcc0fb188ed9368be175878a59da8498a2e3" + resolved-ref: "93f3bcc0fb188ed9368be175878a59da8498a2e3" url: "https://github.com/autonomy-system/nft-collection" source: git version: "0.0.1" @@ -1751,8 +1751,8 @@ packages: dependency: "direct main" description: path: "." - ref: "778f9e0a141f2faba82c36f7c5812fac0ea9f765" - resolved-ref: "778f9e0a141f2faba82c36f7c5812fac0ea9f765" + ref: "6866f4da846324aa4a083b142cf9a835cb3104ce" + resolved-ref: "6866f4da846324aa4a083b142cf9a835cb3104ce" url: "https://github.com/autonomy-system/nft-rendering.git" source: git version: "1.0.9" diff --git a/pubspec.yaml b/pubspec.yaml index 2fa3758b6..73935846a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,7 +79,7 @@ dependencies: nft_rendering: git: url: https://github.com/autonomy-system/nft-rendering.git - ref: 778f9e0a141f2faba82c36f7c5812fac0ea9f765 + ref: 6866f4da846324aa4a083b142cf9a835cb3104ce onesignal_flutter: ^3.3.0 open_settings: ^2.0.2 overlay_support: ^2.0.0 @@ -123,7 +123,7 @@ dependencies: nft_collection: git: url: https://github.com/autonomy-system/nft-collection - ref: e346141fd9ad4f0667b4c5a403fd8112f76ae5f3 + ref: 93f3bcc0fb188ed9368be175878a59da8498a2e3 autonomy_tv_proto: git: url: https://github.com/autonomy-system/autonomy-tv-proto-communication