mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
solana nft improvements and other fixes (#3581)
* feat: improvements covering solana nfts(send, ownership badge, persistent imports etc), closing emptied token accounts and other fixes from patch * feat: more improvements for nfts including security hardening * fix: display error in full
This commit is contained in:
@@ -87,7 +87,11 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||
|
||||
final dashboardPageView = RefreshIndicator(
|
||||
displacement: responsiveLayoutUtil.screenHeight * 0.1,
|
||||
onRefresh: () async => await widget.dashboardViewModel.refreshDashboard(),
|
||||
onRefresh: () async {
|
||||
unawaited(widget.balancePage.nftViewModel.getNFTAssetByWallet());
|
||||
|
||||
await widget.dashboardViewModel.refreshDashboard();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: Container(
|
||||
|
||||
@@ -2,10 +2,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/entities/solana_nft_asset_model.dart';
|
||||
import 'package:cake_wallet/entities/wallet_nft_response.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import "package:cake_wallet/src/screens/dashboard/pages/nft_send_page.dart";
|
||||
import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/gradient_background.dart';
|
||||
import "package:cake_wallet/src/widgets/primary_button.dart";
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
|
||||
class NFTDetailsPage extends BasePage {
|
||||
@@ -56,30 +59,56 @@ class NFTDetailsPage extends BasePage {
|
||||
// );
|
||||
}
|
||||
|
||||
bool get _canSend =>
|
||||
arguments.isSolanaNFT &&
|
||||
(arguments.solanaNFTAssetModel?.mint?.isNotEmpty ?? false) &&
|
||||
arguments.solanaNFTAssetModel?.isOwned != false;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: arguments.isSolanaNFT
|
||||
? SolanaNFTDetailsWidget(
|
||||
solanaNftAsset: arguments.solanaNFTAssetModel,
|
||||
)
|
||||
: EVMChainNFTDetailsWidget(
|
||||
nftAsset: arguments.nftAsset,
|
||||
Widget body(BuildContext context) => SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
child: arguments.isSolanaNFT
|
||||
? SolanaNFTDetailsWidget(
|
||||
solanaNftAsset: arguments.solanaNFTAssetModel,
|
||||
)
|
||||
: EVMChainNFTDetailsWidget(
|
||||
nftAsset: arguments.nftAsset,
|
||||
),
|
||||
),
|
||||
if (_canSend)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24),
|
||||
child: PrimaryButton(
|
||||
text: S.of(context).send,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: () async {
|
||||
final sent = await Navigator.of(context).pushNamed(
|
||||
Routes.nftSendPage,
|
||||
arguments: NFTSendPageArguments(asset: arguments.solanaNFTAssetModel!),
|
||||
);
|
||||
|
||||
if (sent == true && context.mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _NFTImageWidget extends StatelessWidget {
|
||||
@@ -162,6 +191,15 @@ class SolanaNFTDetailsWidget extends StatelessWidget {
|
||||
return Center(child: Text(S.current.no_extra_detail));
|
||||
}
|
||||
|
||||
final moreInfoEntries = <MapEntry<String, String>>[
|
||||
MapEntry(S.current.mint_address, solanaNftAsset?.mint ?? ""),
|
||||
MapEntry(S.current.contractName, solanaNftAsset?.contract?.name ?? ""),
|
||||
MapEntry(S.current.contractSymbol, solanaNftAsset?.contract?.symbol ?? ""),
|
||||
MapEntry(S.current.collection_name, solanaNftAsset?.collection?.name ?? ""),
|
||||
MapEntry(S.current.collection_description, solanaNftAsset?.collection?.description ?? ""),
|
||||
MapEntry(S.current.collection_address, solanaNftAsset?.collection?.collectionAddress ?? ""),
|
||||
].where((entry) => entry.value.isNotEmpty).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -171,49 +209,75 @@ class SolanaNFTDetailsWidget extends StatelessWidget {
|
||||
infoType: S.current.name,
|
||||
infoValue: solanaNftAsset?.name ?? '---',
|
||||
),
|
||||
if (solanaNftAsset?.description != null) ...[
|
||||
if (solanaNftAsset?.isOwned == false) ...[
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.watching,
|
||||
infoValue: S.current.nft_not_held_by_wallet,
|
||||
),
|
||||
],
|
||||
if (solanaNftAsset?.description?.isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.description,
|
||||
infoValue: solanaNftAsset!.description ?? '---',
|
||||
infoValue: solanaNftAsset!.description!,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.mint_address,
|
||||
infoValue: solanaNftAsset?.mint ?? '---',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.contractName,
|
||||
infoValue: solanaNftAsset?.contract?.name ?? '---',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.contractSymbol,
|
||||
infoValue: solanaNftAsset?.contract?.symbol ?? '---',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.collection_name,
|
||||
infoValue: solanaNftAsset?.collection?.name ?? '---',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.collection_description,
|
||||
infoValue: solanaNftAsset?.collection?.description ?? '---',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.collection_address,
|
||||
infoValue: solanaNftAsset?.collection?.collectionAddress ?? '---',
|
||||
),
|
||||
if (moreInfoEntries.isNotEmpty) _NFTMoreInfoSection(entries: moreInfoEntries),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NFTMoreInfoSection extends StatefulWidget {
|
||||
const _NFTMoreInfoSection({required this.entries});
|
||||
|
||||
final List<MapEntry<String, String>> entries;
|
||||
|
||||
@override
|
||||
State<_NFTMoreInfoSection> createState() => _NFTMoreInfoSectionState();
|
||||
}
|
||||
|
||||
class _NFTMoreInfoSectionState extends State<_NFTMoreInfoSection> {
|
||||
bool isExpanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () => setState(() => isExpanded = !isExpanded),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
isExpanded ? S.current.hide_details : S.current.show_details,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isExpanded)
|
||||
for (final entry in widget.entries) ...[
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(infoType: entry.key, infoValue: entry.value),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _NFTSingleInfoTile extends StatelessWidget {
|
||||
final String infoType;
|
||||
final String infoValue;
|
||||
|
||||
@@ -89,7 +89,7 @@ class _ImportNFTPage extends BasePage {
|
||||
},
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
iconColor: Theme.of(context).colorScheme.primary,
|
||||
placeholder: '0x...',
|
||||
placeholder: nftViewModel.isSolanaWallet ? S.current.mint_address : "0x...",
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/solana_nft_tile_widget
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import "package:mobx/mobx.dart";
|
||||
|
||||
class NFTListingPage extends StatefulWidget {
|
||||
final NFTViewModel nftViewModel;
|
||||
@@ -18,23 +19,34 @@ class NFTListingPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NFTListingPageState extends State<NFTListingPage> {
|
||||
late final ReactionDisposer _walletDisposer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_walletDisposer = reaction(
|
||||
(_) => widget.nftViewModel.appStore.wallet,
|
||||
(_) => widget.nftViewModel.onWalletChanged(),
|
||||
);
|
||||
|
||||
fetchNFTsForWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_walletDisposer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> fetchNFTsForWallet() async {
|
||||
await widget.nftViewModel.getNFTAssetByWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return Column(
|
||||
Widget build(BuildContext context) => SliverToBoxAdapter(
|
||||
child: Observer(
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
@@ -50,6 +62,17 @@ class _NFTListingPageState extends State<NFTListingPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.nftViewModel.unresolvedNFTCount > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12),
|
||||
child: Text(
|
||||
S.current.nft_some_not_loaded,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.nftViewModel.isLoading)
|
||||
Center(
|
||||
child: CircularProgressIndicator(
|
||||
@@ -62,11 +85,9 @@ class _NFTListingPageState extends State<NFTListingPage> {
|
||||
else
|
||||
NFTListWidget(nftViewModel: widget.nftViewModel)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class NFTListWidget extends StatelessWidget {
|
||||
@@ -103,7 +124,7 @@ class NFTListWidget extends StatelessWidget {
|
||||
itemCount: nftViewModel.solanaNftAssetModels.length,
|
||||
itemBuilder: (context, index) {
|
||||
final nftAsset = nftViewModel.solanaNftAssetModels[index];
|
||||
return SolanaNFTTileWidget(nftAsset: nftAsset);
|
||||
return SolanaNFTTileWidget(nftAsset: nftAsset, nftViewModel: nftViewModel);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
import "package:cake_wallet/core/address_validator.dart";
|
||||
import "package:cake_wallet/core/auth_service.dart";
|
||||
import "package:cake_wallet/core/execution_state.dart";
|
||||
import "package:cake_wallet/entities/solana_nft_asset_model.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/src/screens/base_page.dart";
|
||||
import "package:cake_wallet/src/widgets/address_text_field.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/gradient_background.dart";
|
||||
import "package:cake_wallet/src/widgets/primary_button.dart";
|
||||
import "package:cake_wallet/utils/show_pop_up.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/nft_send_view_model.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/pending_transaction.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class NFTSendPage extends StatefulWidget {
|
||||
const NFTSendPage({
|
||||
required this.nftSendViewModel,
|
||||
required this.authService,
|
||||
required this.arguments,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final NFTSendViewModel nftSendViewModel;
|
||||
final AuthService authService;
|
||||
final NFTSendPageArguments arguments;
|
||||
|
||||
@override
|
||||
State<NFTSendPage> createState() => _NFTSendPageState();
|
||||
}
|
||||
|
||||
class _NFTSendPageState extends State<NFTSendPage> {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
late TextEditingController addressController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
addressController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
addressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _NFTSendPage(
|
||||
nftSendViewModel: widget.nftSendViewModel,
|
||||
authService: widget.authService,
|
||||
asset: widget.arguments.asset,
|
||||
formKey: formKey,
|
||||
addressController: addressController,
|
||||
);
|
||||
}
|
||||
|
||||
class _NFTSendPage extends BasePage {
|
||||
_NFTSendPage({
|
||||
required this.nftSendViewModel,
|
||||
required this.authService,
|
||||
required this.asset,
|
||||
required this.formKey,
|
||||
required this.addressController,
|
||||
});
|
||||
|
||||
final NFTSendViewModel nftSendViewModel;
|
||||
final AuthService authService;
|
||||
final SolanaNFTAssetModel asset;
|
||||
final GlobalKey<FormState> formKey;
|
||||
final TextEditingController addressController;
|
||||
|
||||
@override
|
||||
String get title => S.current.send;
|
||||
|
||||
@override
|
||||
bool get gradientBackground => true;
|
||||
|
||||
@override
|
||||
Widget Function(BuildContext, Widget) get rootWrapper =>
|
||||
(context, scaffold) => GradientBackground(scaffold: scaffold);
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
height: MediaQuery.sizeOf(context).height / 3.5,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: colorScheme.outline),
|
||||
color: colorScheme.surface,
|
||||
),
|
||||
child: CakeImageWidget(imageUrl: asset.imageOriginalUrl),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
(asset.name?.isNotEmpty ?? false) ? asset.name! : (asset.symbol ?? "---"),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: colorScheme.surfaceContainer,
|
||||
),
|
||||
child: AddressTextField(
|
||||
controller: addressController,
|
||||
placeholder: S.of(context).recipient_address,
|
||||
options: const [
|
||||
AddressTextFieldOption.paste,
|
||||
AddressTextFieldOption.qrCode,
|
||||
],
|
||||
buttonColor: colorScheme.surfaceContainerHighest,
|
||||
validator: AddressValidator(type: CryptoCurrency.sol).call,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Observer(
|
||||
builder: (_) => LoadingPrimaryButton(
|
||||
text: S.of(context).send,
|
||||
color: colorScheme.primary,
|
||||
textColor: colorScheme.onPrimary,
|
||||
isLoading: nftSendViewModel.state is IsExecutingState,
|
||||
onPressed: () => _onSendPressed(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSendPressed(BuildContext context) async {
|
||||
if (nftSendViewModel.state is IsExecutingState) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(formKey.currentState?.validate() ?? false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nftSendViewModel.markSendStarted();
|
||||
|
||||
final destinationAddress = addressController.text.trim();
|
||||
|
||||
await authService.authenticateAction(
|
||||
context,
|
||||
conditionToDetermineIfToUse2FA: nftSendViewModel.shouldRequireTOTP2FAFor(destinationAddress),
|
||||
onAuthSuccess: (isAuthenticatedSuccessfully) async {
|
||||
if (!isAuthenticatedSuccessfully || !context.mounted) {
|
||||
nftSendViewModel.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
await _createAndConfirm(context, destinationAddress);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createAndConfirm(BuildContext context, String destinationAddress) async {
|
||||
await nftSendViewModel.createTransaction(asset, destinationAddress);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final state = nftSendViewModel.state;
|
||||
|
||||
if (state is FailureState) {
|
||||
await _showError(context, state.error);
|
||||
return;
|
||||
}
|
||||
|
||||
final pendingTransaction = nftSendViewModel.pendingTransaction;
|
||||
|
||||
if (pendingTransaction == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final confirmed = await _confirm(context, destinationAddress, pendingTransaction);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
nftSendViewModel.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
await _commit(context);
|
||||
}
|
||||
|
||||
Future<bool> _confirm(
|
||||
BuildContext context,
|
||||
String destinationAddress,
|
||||
PendingTransaction pendingTransaction,
|
||||
) async {
|
||||
final additionalCost = pendingTransaction.additionalCost;
|
||||
|
||||
var confirmed = false;
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).confirm_sending,
|
||||
alertContent: "",
|
||||
alertContentTextWidget: _NFTSendConfirmation(
|
||||
name: asset.name,
|
||||
mint: asset.mint,
|
||||
destinationAddress: destinationAddress,
|
||||
fee: pendingTransaction.feeFormatted,
|
||||
accountCreationFee: additionalCost?.toStringWithSymbol(fractionalDigits: 8),
|
||||
),
|
||||
leftButtonText: S.of(context).cancel,
|
||||
rightButtonText: S.of(context).send,
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(),
|
||||
actionRightButton: () {
|
||||
confirmed = true;
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
Future<void> _commit(BuildContext context) async {
|
||||
await nftSendViewModel.commitTransaction();
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final state = nftSendViewModel.state;
|
||||
if (state is FailureState) {
|
||||
await _showError(context, state.error);
|
||||
return;
|
||||
}
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertWithOneAction(
|
||||
alertTitle: "",
|
||||
alertContent: S.of(context).transaction_sent,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
|
||||
Future<void> _showError(BuildContext context, String error) => showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class NFTSendPageArguments {
|
||||
NFTSendPageArguments({required this.asset});
|
||||
|
||||
final SolanaNFTAssetModel asset;
|
||||
}
|
||||
|
||||
class _NFTSendConfirmation extends StatelessWidget {
|
||||
const _NFTSendConfirmation({
|
||||
required this.name,
|
||||
required this.mint,
|
||||
required this.destinationAddress,
|
||||
required this.fee,
|
||||
required this.accountCreationFee,
|
||||
});
|
||||
|
||||
final String? name;
|
||||
final String? mint;
|
||||
final String destinationAddress;
|
||||
final String fee;
|
||||
final String? accountCreationFee;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final creationFee = accountCreationFee;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (name?.isNotEmpty ?? false) _ConfirmationRow(label: S.of(context).name, value: name!),
|
||||
if (mint?.isNotEmpty ?? false)
|
||||
_ConfirmationRow(label: S.of(context).mint_address, value: mint!),
|
||||
_ConfirmationRow(label: S.of(context).address, value: destinationAddress),
|
||||
_ConfirmationRow(label: S.of(context).send_estimated_fee, value: fee),
|
||||
if (creationFee != null)
|
||||
Text(
|
||||
S.of(context).recipient_account_creation_fee(creationFee),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConfirmationRow extends StatelessWidget {
|
||||
const _ConfirmationRow({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +1,127 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/entities/solana_nft_asset_model.dart';
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import "package:cake_wallet/view_model/dashboard/nft_view_model.dart";
|
||||
|
||||
class SolanaNFTTileWidget extends StatelessWidget {
|
||||
const SolanaNFTTileWidget({super.key, required this.nftAsset});
|
||||
const SolanaNFTTileWidget({
|
||||
required this.nftAsset,
|
||||
required this.nftViewModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SolanaNFTAssetModel nftAsset;
|
||||
final NFTViewModel nftViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.nftDetailsPage,
|
||||
arguments: NFTDetailsPageArguments(
|
||||
isSolanaNFT: true,
|
||||
solanaNFTAssetModel: nftAsset,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 0.0,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
margin: const EdgeInsets.all(8),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 0.0,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: nftAsset.imageOriginalUrl,
|
||||
),
|
||||
Widget build(BuildContext context) => InkWell(
|
||||
onTap: () async {
|
||||
final walletName = nftViewModel.currentWalletName;
|
||||
|
||||
final sent = await Navigator.of(context).pushNamed(
|
||||
Routes.nftDetailsPage,
|
||||
arguments: NFTDetailsPageArguments(
|
||||
isSolanaNFT: true,
|
||||
solanaNFTAssetModel: nftAsset,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Symbol: ${nftAsset.symbol ?? '---'}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
);
|
||||
|
||||
if (sent != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final mint = nftAsset.mint;
|
||||
if (walletName != null && mint != null && mint.isNotEmpty) {
|
||||
await nftViewModel.onNFTSent(walletName, mint);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 0.0,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
margin: const EdgeInsets.all(8),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 0.0,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
(nftAsset.name?.isNotEmpty ?? false)
|
||||
? nftAsset.name!
|
||||
: (nftAsset.symbol ?? '---'),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: nftAsset.imageOriginalUrl,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Symbol: ${nftAsset.symbol ?? "---"}",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
(nftAsset.name?.isNotEmpty ?? false)
|
||||
? nftAsset.name!
|
||||
: (nftAsset.symbol ?? "---"),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
if (nftAsset.isOwned == false) ...[
|
||||
const SizedBox(height: 8),
|
||||
const _WatchingBadge(),
|
||||
],
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class _WatchingBadge extends StatelessWidget {
|
||||
const _WatchingBadge();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: Text(
|
||||
S.current.watching,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user