Merge branch 'dev' into cw-1440-new-buy-sell-flow

# Conflicts:
#	res/values/strings_it.arb
#	res/values/strings_ja.arb
#	res/values/strings_ko.arb
#	res/values/strings_my.arb
#	res/values/strings_nl.arb
#	res/values/strings_pl.arb
#	res/values/strings_pt.arb
#	res/values/strings_pt_BR.arb
#	res/values/strings_ru.arb
#	res/values/strings_th.arb
#	res/values/strings_tl.arb
#	res/values/strings_tr.arb
#	res/values/strings_uk.arb
#	res/values/strings_ur.arb
#	res/values/strings_vi.arb
#	res/values/strings_yo.arb
#	res/values/strings_zh.arb
This commit is contained in:
Robert Malikowski
2026-09-11 12:53:54 +02:00
153 changed files with 4277 additions and 899 deletions
+3 -3
View File
@@ -558,9 +558,9 @@ class CWBitcoin extends Bitcoin {
@override
HardwareWalletService getTrezorHardwareWalletService(
trezor.TrezorConnect connect, bool isBitcoin) {
if (isBitcoin) return BitcoinTrezorService(connect);
return LitecoinTrezorService(connect);
trezor.TrezorConnect? connect, trezor.TrezorClient? client, bool isBitcoin) {
if (isBitcoin) return BitcoinTrezorService(client!);
return LitecoinTrezorService(connect!);
}
@override
@@ -0,0 +1,6 @@
class CakePayUnauthorizedException implements Exception {
const CakePayUnauthorizedException();
@override
String toString() => 'Cake Pay session is no longer valid, please log in again.';
}
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/cake_pay/src/cake_pay_exceptions.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
@@ -497,19 +498,37 @@ class CakePayBuyCardPage extends BasePage {
}
Future<void> purchaseCard(BuildContext context) async {
bool isLogged = await cakePayBuyCardViewModel.isUserLogged;
if (!isLogged) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
} else {
try {
await cakePayBuyCardViewModel.createOrder();
} catch (_) {
try {
final isLogged = await cakePayBuyCardViewModel.isUserLogged;
if (!isLogged) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
await cakePayBuyCardViewModel.logout();
if (context.mounted) {
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
}
return;
}
await cakePayBuyCardViewModel.createOrder();
} on CakePayUnauthorizedException {
cakePayBuyCardViewModel.isSimulatingFlow = false;
if (context.mounted) {
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
}
} catch (error) {
cakePayBuyCardViewModel.isSimulatingFlow = false;
_sendViewModel.state = FailureState(
_sendViewModel.translateErrorMessage(
error,
cakePayBuyCardViewModel.walletType,
_sendViewModel.wallet.currency,
),
);
} finally {
cakePayBuyCardViewModel.isPurchasing = false;
}
cakePayBuyCardViewModel.isPurchasing = false;
}
BuildContext? dialogContext;
@@ -477,11 +477,18 @@ class _ShopTabState extends State<_ShopTab> {
if (i >= vendors.length) return const _Loading();
final vendor = vendors[i];
return CardItem(
logoUrl: vendor.card?.cardImageUrl,
title: vendor.name,
subTitle: vendor.card?.description ?? '',
onTap: () => Navigator.pushNamed(context, Routes.cakePayBuyCardPage,
arguments: [vendor]));
logoUrl: vendor.card?.cardImageUrl,
title: vendor.name,
subTitle: vendor.card?.description ?? '',
onTap: () async {
await Navigator.pushNamed(
context,
Routes.cakePayBuyCardPage,
arguments: [vendor],
);
await viewModel.checkAuth();
},
);
},
),
],
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:cake_wallet/cake_pay/src/cake_pay_exceptions.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_user_credentials.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
@@ -129,6 +130,10 @@ class CakePayApi {
body: body,
);
if (response.statusCode == 401) {
throw const CakePayUnauthorizedException();
}
if (response.statusCode == 201) {
final data = json.decode(response.body) as Map<String, dynamic>;
return CakePayOrder.fromMap(data);
@@ -168,6 +173,10 @@ class CakePayApi {
headers: headers,
);
if (response.statusCode == 401) {
throw const CakePayUnauthorizedException();
}
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
@@ -203,6 +212,10 @@ class CakePayApi {
printV('Response: ${response.statusCode}');
if (response.statusCode == 401) {
throw const CakePayUnauthorizedException();
}
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
+42 -19
View File
@@ -1,4 +1,5 @@
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/cake_pay/src/cake_pay_exceptions.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
@@ -95,29 +96,51 @@ class CakePayService {
required bool confirmsTermsAgreed,
}) async {
final userEmail = (await secureStorage.read(key: cakePayEmailStorageKey))!;
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
return await cakePayApi.createOrder(
apiKey: cakePayApiKey,
cardId: cardId,
price: price,
quantity: quantity,
token: token,
userEmail: userEmail,
confirmsNoVpn: confirmsNoVpn,
confirmsVoidedRefund: confirmsVoidedRefund,
confirmsTermsAgreed: confirmsTermsAgreed,
return _withUserToken(
(token) => cakePayApi.createOrder(
apiKey: cakePayApiKey,
cardId: cardId,
price: price,
quantity: quantity,
token: token,
userEmail: userEmail,
confirmsNoVpn: confirmsNoVpn,
confirmsVoidedRefund: confirmsVoidedRefund,
confirmsTermsAgreed: confirmsTermsAgreed,
),
);
}
Future<CakePayOrder> findOrderById({required String orderId}) async {
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
return await cakePayApi.getOrderById(orderId: orderId, token: token);
}
Future<CakePayOrder> findOrderById({required String orderId}) => _withUserToken(
(token) => cakePayApi.getOrderById(
orderId: orderId,
token: token,
),
);
///Simulate Purchase Gift Card
Future<String> simulatePayment({required String orderId}) async {
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
return await cakePayApi.simulatePayment(
CSRFToken: CSRFToken, authorization: authorization, token: token, orderId: orderId);
Future<String> simulatePayment({required String orderId}) => _withUserToken(
(token) => cakePayApi.simulatePayment(
CSRFToken: CSRFToken,
authorization: authorization,
token: token,
orderId: orderId,
),
);
/// Wrapper for requests with user token
Future<T> _withUserToken<T>(Future<T> Function(String token) request) async {
try {
final token = await secureStorage.read(key: cakePayUserTokenKey) ?? '';
if (token.isEmpty) {
throw const CakePayUnauthorizedException();
}
return await request(token);
} on CakePayUnauthorizedException {
await logout();
rethrow;
}
}
}
+4
View File
@@ -227,6 +227,7 @@ import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import "package:cake_wallet/view_model/dashboard/nft_send_view_model.dart";
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
@@ -1708,6 +1709,9 @@ Future<void> setup({
getIt.registerFactory(() => NFTViewModel(appStore, getIt.get<BottomSheetService>()));
getIt.registerFactory(() =>
NFTSendViewModel(appStore, getIt.get<ContactListViewModel>(param1: CryptoCurrency.sol)));
getIt.registerFactory(() => SignViewModel(getIt.get<AppStore>().wallet!));
getIt.registerFactory(() => SeedVerificationPage(getIt.get<WalletSeedViewModel>()));
@@ -641,6 +641,13 @@ Future<void> defaultSettingsMigration(
case 70:
await _addTbbTokenToExistingSolanaWallets();
break;
case 71:
_changeExchangeProviderAvailability(
sharedPreferences,
providerName: "Swaps.XYZ",
enabled: false,
);
break;
default:
break;
}
+15 -3
View File
@@ -1,7 +1,15 @@
import 'package:cake_wallet/generated/locales.dart';
import 'package:devicelocale/devicelocale.dart';
import "package:flutter/widgets.dart";
import 'package:intl/intl.dart';
// Flutter's Material delegates match on the language subtag, so "pt_BR" must be split into two
Locale localeFromLanguageCode(String code) {
final parts = code.split("_");
return parts.length == 2 ? Locale(parts[0], parts[1]) : Locale(code);
}
class LanguageService {
static const Map<String, String> supportedLocales = {
'en': 'English',
@@ -85,12 +93,16 @@ class LanguageService {
static Future<String> localeDetection() async {
try {
var locale = await Devicelocale.currentLocale ?? '';
locale = Intl.shortLocale(locale);
final locale = Intl.canonicalizedLocale(await Devicelocale.currentLocale ?? "");
if (list.keys.contains(locale)) {
return locale;
}
final language = Intl.shortLocale(locale);
if (list.keys.contains(language)) {
return language;
}
return LanguageService.defaultLocale;
} catch (_) {
return LanguageService.defaultLocale;
+116
View File
@@ -0,0 +1,116 @@
import "package:cake_wallet/entities/solana_nft_asset_model.dart";
import "package:cake_wallet/entities/wallet_nft_response.dart";
import "package:cw_core/imported_nft.dart";
class SavedNFTs {
Future<Map<String, SolanaNFTAssetModel>> solanaNFTs(String walletName) async {
final rows = await ImportedNFT.getAllForWallet(walletName, ImportedNFT.solanaChain);
return {
for (final row in rows)
row.identifier: SolanaNFTAssetModel(
address: row.identifier,
mint: row.identifier,
name: row.name,
symbol: row.symbol,
description: row.description,
imageOriginalUrl: row.imageUrl,
isOwned: row.isOwned,
),
};
}
Future<void> addSolana(String walletName, SolanaNFTAssetModel asset) async {
final mint = asset.mint;
if (mint == null || mint.isEmpty) {
return;
}
await _rowForSolana(walletName, mint, asset).save();
}
Future<void> refreshSolana(String walletName, List<SolanaNFTAssetModel> assets) async {
for (final asset in assets) {
final mint = asset.mint;
if (mint == null) {
continue;
}
await ImportedNFT.updateMetadata(_rowForSolana(walletName, mint, asset));
}
}
Future<List<NFTAssetModel>> evmNFTs(String walletName, String chain) async {
final rows = await ImportedNFT.getAllForWallet(walletName, chain);
return rows
.map(
(row) => NFTAssetModel(
tokenAddress: _tokenAddressOf(row.identifier),
tokenId: _tokenIdOf(row.identifier),
name: row.name,
symbol: row.symbol,
normalizedMetadata: NormalizedMetadata(
name: row.name,
description: row.description,
image: row.imageUrl,
),
),
)
.toList();
}
Future<void> addEvm(String walletName, String chain, NFTAssetModel asset) async {
final identifier = evmIdentifier(asset);
if (identifier == null) {
return;
}
await ImportedNFT(
walletName: walletName,
chain: chain,
identifier: identifier,
name: asset.name,
symbol: asset.symbol,
description: asset.normalizedMetadata?.description,
imageUrl: asset.normalizedMetadata?.imageUrl,
).save();
}
Future<void> removeSolana(String walletName, String mint) =>
ImportedNFT.deleteOne(walletName, ImportedNFT.solanaChain, mint);
static String? evmIdentifier(NFTAssetModel asset) {
final address = asset.tokenAddress;
final tokenId = asset.tokenId;
if (address == null || address.isEmpty) {
return null;
}
return "$address:${tokenId ?? ""}";
}
ImportedNFT _rowForSolana(String walletName, String mint, SolanaNFTAssetModel asset) =>
ImportedNFT(
walletName: walletName,
chain: ImportedNFT.solanaChain,
identifier: mint,
name: asset.name,
symbol: asset.symbol,
description: asset.description,
imageUrl: asset.imageOriginalUrl,
isOwned: asset.isOwned,
);
String _tokenAddressOf(String identifier) => identifier.split(":").first;
String _tokenIdOf(String identifier) {
final parts = identifier.split(":");
return parts.length > 1 ? parts.sublist(1).join(":") : "";
}
}
+46 -41
View File
@@ -1,3 +1,6 @@
import "package:cw_core/utils/ipfs_url.dart";
import "package:cw_core/utils/nft_text.dart";
class SolanaNFTAssetModel {
String? address;
String? mint;
@@ -12,46 +15,50 @@ class SolanaNFTAssetModel {
Metaplex? metaplex;
Collection? collection;
Contract? contract;
bool? isOwned;
SolanaNFTAssetModel({
this.address,
this.mint,
this.standard,
this.name,
this.symbol,
this.description,
this.imageOriginalUrl,
String? name,
String? symbol,
String? description,
String? imageOriginalUrl,
this.externalUrl,
this.metadataOriginalUrl,
this.totalSupply,
this.metaplex,
this.collection,
this.contract,
});
this.isOwned,
}) : name = sanitizeNFTText(name),
symbol = sanitizeNFTText(symbol),
description = sanitizeNFTText(description),
imageOriginalUrl = tryNormalizeIpfsUrl(imageOriginalUrl);
factory SolanaNFTAssetModel.fromJson(Map<String, dynamic> json) {
return SolanaNFTAssetModel(
address: json['address'] as String?,
mint: json['mint'] as String?,
standard: json['standard'] as String?,
name: json['name'] as String?,
symbol: json['symbol'] as String?,
description: json['description'] as String?,
imageOriginalUrl: json['imageOriginalUrl'] as String?,
externalUrl: json['externalUrl'] as String?,
metadataOriginalUrl: json['metadataOriginalUrl'] as String?,
totalSupply: json['totalSupply'] as String?,
metaplex: json['metaplex'] != null
? Metaplex.fromJson(json['metaplex'] as Map<String, dynamic>)
: null,
collection: json['collection'] != null
? Collection.fromJson(json['collection'] as Map<String, dynamic>)
: null,
contract: json['contract'] != null
? Contract.fromJson(json['contract'] as Map<String, dynamic>)
: null,
);
}
factory SolanaNFTAssetModel.fromJson(Map<String, dynamic> json) => SolanaNFTAssetModel(
address: json["address"] as String?,
mint: json["mint"] as String?,
standard: json["standard"] as String?,
name: json["name"] as String?,
symbol: json["symbol"] as String?,
description: json["description"] as String?,
imageOriginalUrl: json["imageOriginalUrl"] as String?,
externalUrl: json["externalUrl"] as String?,
metadataOriginalUrl: json["metadataOriginalUrl"] as String?,
totalSupply: json["totalSupply"] as String?,
metaplex: json["metaplex"] != null
? Metaplex.fromJson(json["metaplex"] as Map<String, dynamic>)
: null,
collection: json["collection"] != null
? Collection.fromJson(json["collection"] as Map<String, dynamic>)
: null,
contract: json["contract"] != null
? Contract.fromJson(json["contract"] as Map<String, dynamic>)
: null,
isOwned: json["isOwned"] as bool?,
);
}
class Metaplex {
@@ -100,17 +107,15 @@ class Collection {
this.metaplexMint,
this.sellerFeeBasisPoints});
factory Collection.fromJson(Map<String, dynamic> json) {
return Collection(
collectionAddress: json['collectionAddress'] as String?,
name: json['name'] as String?,
description: json['description'] as String?,
imageOriginalUrl: json['imageOriginalUrl'] as String?,
externalUrl: json['externalUrl'] as String?,
metaplexMint: json['metaplexMint'] as String?,
sellerFeeBasisPoints: json['sellerFeeBasisPoints'] as int?,
);
}
factory Collection.fromJson(Map<String, dynamic> json) => Collection(
collectionAddress: json["collectionAddress"] as String?,
name: sanitizeNFTText(json["name"] as String?),
description: sanitizeNFTText(json["description"] as String?),
imageOriginalUrl: tryNormalizeIpfsUrl(json["imageOriginalUrl"] as String?),
externalUrl: json["externalUrl"] as String?,
metaplexMint: json["metaplexMint"] as String?,
sellerFeeBasisPoints: json["sellerFeeBasisPoints"] as int?,
);
}
class Contract {
@@ -123,8 +128,8 @@ class Contract {
factory Contract.fromJson(Map<String, dynamic> json) {
return Contract(
type: json['type'] as String?,
name: json['name'] as String?,
symbol: json['symbol'] as String?,
name: sanitizeNFTText(json["name"] as String?),
symbol: sanitizeNFTText(json["symbol"] as String?),
);
}
}
+9 -28
View File
@@ -1,3 +1,6 @@
import "package:cw_core/utils/ipfs_url.dart";
import "package:cw_core/utils/nft_text.dart";
class WalletNFTsResponseModel {
final int? page;
final int? pageSize;
@@ -40,8 +43,8 @@ class NFTAssetModel {
tokenAddress: json['token_address'] as String?,
tokenId: json['token_id'] as String?,
contractType: json['contract_type'] as String?,
name: json['name'] as String?,
symbol: json['symbol'] as String?,
name: sanitizeNFTText(json["name"] as String?),
symbol: sanitizeNFTText(json["symbol"] as String?),
normalizedMetadata: json['normalized_metadata'] != null
? new NormalizedMetadata.fromJson(json['normalized_metadata'] as Map<String, dynamic>)
: null,
@@ -61,33 +64,11 @@ class NormalizedMetadata {
factory NormalizedMetadata.fromJson(Map<String, dynamic> json) {
return NormalizedMetadata(
name: json['name'] as String?,
description: json['description'] as String?,
image: json['image'] as String?,
name: sanitizeNFTText(json["name"] as String?),
description: sanitizeNFTText(json["description"] as String?),
image: json["image"] as String?,
);
}
String? get imageUrl {
if (image == null) return image;
if (image!.contains('ipfs.io')) return image;
if (!image!.contains('ipfs')) return image;
// IPFS public gateway provided by Cloudflare is https://cloudflare-ipfs.com/ipfs/
//
// Here is an example of an ipfs image link:
//
// [ipfs://bafkreia2i2ctfexpovgzfff66wqhbmwwpvqjvozan7ioifzcnq76jharwu]
//https://ipfs.io/ipfs/QmTRcRXo6cXByjHYHTVxGpag6vpocrG3rxjPC9PxKAArR9/1620.png
const String ipfsPublicGateway = 'https://cloudflare-ipfs.com/ipfs/';
final ipfsPath = image?.split('//')[1];
final imageLink = '$ipfsPublicGateway$ipfsPath';
return imageLink;
}
String? get imageUrl => tryNormalizeIpfsUrl(image);
}
+4 -2
View File
@@ -65,6 +65,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_daemon/flutter_daemon.dart';
import "package:flutter_localizations/flutter_localizations.dart";
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:hive/hive.dart';
import 'package:quick_actions/quick_actions.dart';
@@ -308,7 +309,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 70,
initialMigrationVersion: 71,
);
}
@@ -329,6 +330,7 @@ Future<void> initialSetup({
required int initialMigrationVersion,
}) async {
LanguageService.loadLocaleList();
await GlobalMaterialLocalizations.delegate.load(const Locale("en"));
await defaultSettingsMigration(
secureStorage: secureStorage,
version: initialMigrationVersion,
@@ -426,7 +428,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
themeMode: themeMode,
localizationsDelegates: localizationDelegates,
supportedLocales: S.delegate.supportedLocales,
locale: Locale(appStore.settingsStore.languageCode),
locale: localeFromLanguageCode(appStore.settingsStore.languageCode),
onGenerateRoute: (settings) => Router.createRoute(settings),
initialRoute: initialRoute,
scrollBehavior: AppScrollBehavior(),
+17
View File
@@ -294,6 +294,23 @@ class CWMonero extends Monero {
};
}
@override
Future<int> getNodeHeight(Object wallet) => (wallet as MoneroWallet).getNodeHeight();
@override
String getLegacySeed(Object wallet, String langName) =>
(wallet as MoneroWallet).seedLegacy(langName);
@override
bool isBackgroundSyncRunning(Object wallet) => (wallet as MoneroWallet).isBackgroundSyncRunning;
@override
Future<void> startBackgroundSync(Object wallet) => (wallet as MoneroWallet).startBackgroundSync();
@override
Future<void> stopBackgroundSync(Object wallet, String password) =>
(wallet as MoneroWallet).stopBackgroundSync(password);
@override
int? getRestoreHeight(Object wallet) {
final moneroWallet = wallet as MoneroWallet;
+4
View File
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import "package:cake_wallet/new-ui/page_open_listener.dart";
import 'package:cake_wallet/new-ui/pages/home_page.dart';
import 'package:cake_wallet/new-ui/widgets/changelog_modal.dart';
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
@@ -112,6 +113,9 @@ class _NewDashboardState extends State<NewDashboard> {
setState(() {
_selectedPage = index;
});
if (widget.dashboardPageWidgets[_selectedPage] case PageOpenListener page) {
page.onPageOpen();
}
},
)
],
+3
View File
@@ -0,0 +1,3 @@
abstract interface class PageOpenListener {
void onPageOpen();
}
+9 -6
View File
@@ -1,3 +1,5 @@
import "dart:async";
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -18,9 +20,6 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/transaction_direction.dart";
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -53,9 +52,9 @@ class _NewHomePageState extends State<NewHomePage> {
});
});
reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
if (val && !widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed) {
if(!context.mounted) {
if (!context.mounted) {
return;
}
widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed = true;
@@ -102,7 +101,11 @@ class _NewHomePageState extends State<NewHomePage> {
sliver: CupertinoSliverRefreshControl(
refreshTriggerPullDistance: 160,
refreshIndicatorExtent: 90,
onRefresh: () => widget.dashboardViewModel.refreshDashboard(),
onRefresh: () {
unawaited(widget.nftViewModel.getNFTAssetByWallet());
return widget.dashboardViewModel.refreshDashboard();
},
),
),
SliverToBoxAdapter(
@@ -1,15 +1,20 @@
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
import "package:cake_wallet/new-ui/widgets/money/use_base_unit_provider.dart";
import "package:cake_wallet/store/settings_store.dart";
import "package:flutter/material.dart";
import "package:flutter_bloc/flutter_bloc.dart";
class MoneySettingsProvider extends StatelessWidget {
const MoneySettingsProvider({required this.settingsStore, this.child, super.key});
const MoneySettingsProvider({required this.settingsStore, required this.child, super.key});
final Widget? child;
final Widget child;
final SettingsStore settingsStore;
@override
Widget build(BuildContext context) =>
BlocProvider(create: (_) => MoneySettingsCubit(settingsStore), child: child);
Widget build(BuildContext context) => BlocProvider(
create: (_) => MoneySettingsCubit(settingsStore),
child: BaseUnitScope(
child: child,
),
);
}
@@ -0,0 +1,63 @@
import "dart:async";
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
import "package:cw_core/currency.dart";
import "package:flutter/widgets.dart";
import "package:flutter_bloc/flutter_bloc.dart";
/// Read-only view of the money display settings for the current subtree.
///
/// ```dart
/// final useBaseUnit = BaseUnit.useBaseUnitOf(context, CryptoCurrency.btc);
/// ```
class BaseUnit extends InheritedModel<Currency> {
const BaseUnit({required this.state, required super.child});
final MoneySettingsState state;
static bool useBaseUnitOf(BuildContext context, Currency currency) =>
InheritedModel.inheritFrom<BaseUnit>(context, aspect: currency)!.state.useBaseUnit(currency);
static String getSymbolOf(BuildContext context, Currency currency) =>
InheritedModel.inheritFrom<BaseUnit>(context, aspect: currency)!.state.getSymbol(currency);
@override
bool updateShouldNotify(BaseUnit old) =>
state.bitcoinAmountDisplayMode != old.state.bitcoinAmountDisplayMode;
@override
bool updateShouldNotifyDependent(BaseUnit old, Set<Currency> aspects) => aspects.any((c) =>
state.useBaseUnit(c) != old.state.useBaseUnit(c) ||
state.getSymbol(c) != old.state.getSymbol(c));
}
class BaseUnitScope extends StatefulWidget {
const BaseUnitScope({required this.child, super.key});
final Widget child;
@override
State<BaseUnitScope> createState() => _BaseUnitScopeState();
}
class _BaseUnitScopeState extends State<BaseUnitScope> {
late MoneySettingsState _state;
late final StreamSubscription<MoneySettingsState> _sub;
@override
void initState() {
super.initState();
final cubit = context.read<MoneySettingsCubit>();
_state = cubit.state;
_sub = cubit.stream.listen((s) => setState(() => _state = s));
}
@override
void dispose() {
_sub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) => BaseUnit(state: _state, child: widget.child);
}
+6 -2
View File
@@ -8,14 +8,18 @@ void startOnCurrentNodeChangeReaction(AppStore appStore) {
_onCurrentNodeChangeReaction?.reaction.dispose();
appStore.settingsStore.nodes.observe((change) async {
try {
await appStore.wallet!.connectToNode(node: change.newValue!);
if(change.newValue?.type == appStore.wallet!.type) {
await appStore.wallet!.connectToNode(node: change.newValue!);
}
} catch (e) {
printV(e.toString());
}
});
appStore.settingsStore.powNodes.observe((change) async {
try {
await appStore.wallet!.connectToPowNode(node: change.newValue!);
if(change.newValue?.type == appStore.wallet!.type) {
await appStore.wallet!.connectToPowNode(node: change.newValue!);
}
} catch (e) {
printV(e.toString());
}
+28 -11
View File
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import "package:cake_wallet/core/auth_service.dart";
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
import 'package:cake_wallet/new-ui/pages/about_page.dart';
@@ -46,6 +47,7 @@ import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart';
import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_details_page.dart';
import "package:cake_wallet/src/screens/dashboard/pages/nft_send_page.dart";
import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
import 'package:cake_wallet/src/screens/dev/exchange_provider_logs_page.dart';
@@ -146,6 +148,7 @@ import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
import "package:cake_wallet/view_model/buy/buy_sell_view_model.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import "package:cake_wallet/view_model/dashboard/nft_send_view_model.dart";
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
@@ -364,9 +367,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
ConnectDevicePageParams(
walletType: availableWalletTypes.first,
hardwareWalletType: hardwareWalletType,
onConnectDevice: (BuildContext context, _) => Navigator.of(context).pushNamed(
Routes.chooseHardwareWalletAccount,
arguments: [availableWalletTypes.first, hardwareWalletType]),
onConnectDevice: (context, _) => Navigator.of(context).pushNamed(
Routes.chooseHardwareWalletAccount,
arguments: [availableWalletTypes.first, hardwareWalletType],
),
isReconnect: false,
),
getIt.get<HardwareWalletViewModel>(param1: hardwareWalletType),
@@ -376,19 +380,23 @@ Route<dynamic> createRoute(RouteSettings settings) {
return handleRouteWithPlatformAwareness(
(_) => getIt.get<NewWalletTypePage>(
param1: NewWalletTypeArguments(
onTypeSelected: (BuildContext context, WalletType type) {
if (hardwareWalletType == HardwareWalletType.trezor && type != WalletType.monero) {
Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount,
arguments: [type, hardwareWalletType]);
onTypeSelected: (context, type) {
if (hardwareWalletType == HardwareWalletType.trezor &&
!trezorUseNative.contains(type)) {
Navigator.of(context).pushNamed(
Routes.chooseHardwareWalletAccount,
arguments: [type, hardwareWalletType],
);
return;
}
final arguments = ConnectDevicePageParams(
walletType: type,
hardwareWalletType: hardwareWalletType,
onConnectDevice: (BuildContext context, _) => Navigator.of(context).pushNamed(
Routes.chooseHardwareWalletAccount,
arguments: [type, hardwareWalletType]),
onConnectDevice: (context, _) => Navigator.of(context).pushNamed(
Routes.chooseHardwareWalletAccount,
arguments: [type, hardwareWalletType],
),
isReconnect: false,
);
@@ -959,7 +967,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
));
case Routes.nftDetailsPage:
return MaterialPageRoute<void>(
return MaterialPageRoute<bool>(
builder: (_) => NFTDetailsPage(
arguments: settings.arguments as NFTDetailsPageArguments,
dashboardViewModel: getIt.get<DashboardViewModel>(),
@@ -973,6 +981,15 @@ Route<dynamic> createRoute(RouteSettings settings) {
),
);
case Routes.nftSendPage:
return MaterialPageRoute<bool>(
builder: (_) => NFTSendPage(
nftSendViewModel: getIt.get<NFTSendViewModel>(),
authService: getIt.get<AuthService>(),
arguments: settings.arguments as NFTSendPageArguments,
),
);
case Routes.signPage:
final vm = getIt.get<SignViewModel>();
return MaterialPageRoute<void>(
+1
View File
@@ -115,6 +115,7 @@ class Routes {
static const walletConnectConnectionsListing = '/wallet-connect-connections-listing';
static const nftDetailsPage = '/nft_details_page';
static const importNFTPage = '/import_nft_page';
static const nftSendPage = "/nft_send_page";
static const backgroundSync = '/background_sync';
static const startTor = '/start_tor';
static const receiveAddresses = "/receive_addresses";
+34
View File
@@ -384,6 +384,40 @@ class CWSolana extends Solana {
}
}
@override
Future<PendingTransaction> sendNFT(
WalletBase wallet, {
required String mintAddress,
required String destinationAddress,
String? name,
}) =>
(wallet as SolanaWallet).sendNFT(
mintAddress: mintAddress,
destinationAddress: destinationAddress,
name: name,
);
@override
Future<SolanaNFTMetadata?> getNFTOnChainMetadata(WalletBase wallet, String mintAddress) async {
final metadata = await (wallet as SolanaWallet).client.getNFTOnChainMetadata(mintAddress);
if (metadata == null) {
return null;
}
return SolanaNFTMetadata(
mint: metadata.mint,
name: metadata.name,
symbol: metadata.symbol,
metadataUri: metadata.metadataUri,
imageUrl: metadata.imageUrl,
);
}
@override
Future<Set<String>> getHeldTokenMints(WalletBase wallet) =>
(wallet as SolanaWallet).heldTokenMints();
@override
Future<void> discoverAndAddWalletTokens(WalletBase wallet) async {
if (wallet is! SolanaWallet) return;
@@ -1,9 +1,12 @@
import "dart:async";
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/entities/wallet_list_order_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import "package:cake_wallet/new-ui/page_open_listener.dart";
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart';
@@ -26,7 +29,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class ContactListPage extends BasePage {
class ContactListPage extends BasePage implements PageOpenListener {
ContactListPage(this.contactListViewModel, this.authService, {this.showAddButton = false});
final ContactListViewModel contactListViewModel;
@@ -89,6 +92,11 @@ class ContactListPage extends BasePage {
);
}
@override
void onPageOpen() {
unawaited(contactListViewModel.loadWalletContacts());
}
@override
Widget body(BuildContext context) => ContactPageBody(contactListViewModel: contactListViewModel);
}
@@ -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,380 @@
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.isStartingSend || nftSendViewModel.state is IsExecutingState) {
return;
}
if (!(formKey.currentState?.validate() ?? false)) {
return;
}
nftSendViewModel.markSendStarted();
final destinationAddress = addressController.text.trim();
try {
await authService.authenticateAction(
context,
conditionToDetermineIfToUse2FA:
nftSendViewModel.shouldRequireTOTP2FAFor(destinationAddress),
onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (!isAuthenticatedSuccessfully || !context.mounted) {
nftSendViewModel.reset();
return;
}
await _createAndConfirm(context, destinationAddress);
},
);
} finally {
nftSendViewModel.markSendSettled();
}
}
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,
),
),
);
}
@@ -343,13 +343,14 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
),
const SizedBox(height: 24),
LoadingPrimaryButton(
onPressed: () {
onPressed: () async {
if (widget.privacySettingsViewModel.addCustomNode) {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return;
}
widget.nodeViewModel.save();
await widget.nodeViewModel.save();
widget.nodeViewModel.setAsCurrent(widget.nodeViewModel.editingNode!);
}
if (testnetValue == true &&
widget.privacySettingsViewModel.type == WalletType.bitcoin) {
@@ -358,7 +359,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
widget.nodeViewModel.address = publicBitcoinTestnetElectrumAddress;
widget.nodeViewModel.port = publicBitcoinTestnetElectrumPort;
widget.nodeViewModel.save();
await widget.nodeViewModel.save();
}
if (_passphraseFormKey.currentState != null &&
!_passphraseFormKey.currentState!.validate()) {
@@ -1,10 +1,12 @@
import 'package:cake_wallet/src/screens/order_details/cake_pay_detail_list_card_item.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/cake_pay_order_list_card.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/standard_list_status_row.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/order_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
@@ -14,6 +16,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart';
import 'package:mobx/mobx.dart';
class OrderDetailsPage extends BasePage {
OrderDetailsPage(this.orderDetailsViewModel);
@@ -43,11 +46,44 @@ class OrderDetailsPageBodyState extends State<OrderDetailsPageBody> {
final OrderDetailsViewModel orderDetailsViewModel;
final MaterialThemeBase currentTheme;
ReactionDisposer? _sessionExpiredReaction;
@override
void initState() {
super.initState();
_sessionExpiredReaction = reaction<bool>(
(_) => orderDetailsViewModel.isCakePaySessionExpired,
(isExpired) {
if (!isExpired) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
showPopUp<void>(
context: context,
builder: (dialogContext) => AlertWithOneAction(
alertTitle: S.of(dialogContext).error,
alertContent: S.of(dialogContext).cake_pay_session_expired,
buttonText: S.of(dialogContext).ok,
buttonAction: () => Navigator.of(dialogContext).pop(),
),
);
});
},
fireImmediately: true,
);
}
@override
void dispose() {
super.dispose();
_sessionExpiredReaction?.call();
orderDetailsViewModel.timer?.cancel();
super.dispose();
}
@override
@@ -32,30 +32,35 @@ class ListItemSelectorWidget extends StatelessWidget {
onTap: onTap,
isFirstInSection: isFirstInSection,
isLastInSection: isLastInSection,
builder: (context, textStyle, labelStyle) {
return Row(
builder: (context, textStyle, labelStyle) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(label, style: textStyle)),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
spacing: 8,
children: [
Text(
options[selectedIndex],
style: labelStyle,
),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
colorFilter:
ColorFilter.mode(theme.colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
],
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Flexible(
child: Text(
options[selectedIndex],
style: labelStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
colorFilter:
ColorFilter.mode(theme.colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
],
),
),
),
],
);
});
));
}
}
+14 -3
View File
@@ -1378,8 +1378,8 @@ abstract class SettingsStoreBase with Store {
pinLength = defaultPinLength;
}
final savedLanguageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ??
await LanguageService.localeDetection();
final savedLanguageCode =
_offeredLanguageCode(sharedPreferences) ?? await LanguageService.localeDetection();
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final bitcoinElectrumServerId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
@@ -1938,7 +1938,12 @@ abstract class SettingsStoreBase with Store {
}
pinCodeLength = pinLength;
languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode;
final restoredLanguageCode = _offeredLanguageCode(sharedPreferences);
if (restoredLanguageCode == null) {
await sharedPreferences.setString(PreferencesKey.currentLanguageCode, languageCode);
} else {
languageCode = restoredLanguageCode;
}
shouldShowYatPopup =
sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup;
shouldShowDEuroDisclaimer =
@@ -2315,6 +2320,12 @@ abstract class SettingsStoreBase with Store {
await _sharedPreferences.setString(key, serializedData);
}
static String? _offeredLanguageCode(SharedPreferences sharedPreferences) {
final code = sharedPreferences.getString(PreferencesKey.currentLanguageCode);
return LanguageService.list.containsKey(code) ? code : null;
}
static Future<String?> _getDeviceName() async {
String? deviceName = '';
final deviceInfoPlugin = DeviceInfoPlugin();
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:cake_wallet/cake_pay/src/cake_pay_exceptions.dart';
import 'package:cake_wallet/order/order_provider_description.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/order/order_source_description.dart';
@@ -221,6 +222,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
orders.add(orderRecord);
updateRemainingTime();
_startExpirationTimer();
} on CakePayUnauthorizedException {
rethrow;
} catch (e) {
sendViewModel.state = FailureState(
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
@@ -275,8 +278,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
}
}
Future<void> logout() async => await _cakePayService.logout();
void _startExpirationTimer() {
_timer?.cancel();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
@@ -78,7 +78,6 @@ abstract class CakePayCardsListViewModelBase with Store {
};
String searchString;
String? username;
int page;
late Country _initialSelectedCountry;
@@ -87,6 +86,9 @@ abstract class CakePayCardsListViewModelBase with Store {
late bool _initialDisplayDenominationsCards;
late bool _initialDisplayCustomValueCards;
@observable
String? username;
@observable
List<CakePayCard> userCards;
@@ -189,12 +191,9 @@ abstract class CakePayCardsListViewModelBase with Store {
@action
Future<void> checkAuth() async {
authFuture = ObservableFuture(cakePayService.isLogged());
final logged = await authFuture!;
if (logged) {
username = await cakePayService.getUserEmail();
}
final logged = await cakePayService.isLogged();
username = logged ? await cakePayService.getUserEmail() : null;
authFuture = ObservableFuture.value(logged);
}
@action
@@ -14,6 +14,7 @@ import 'package:cake_wallet/utils/mobx.dart';
import 'package:cake_wallet/utils/token_utilities.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import "package:cw_core/utils/print_verbose.dart";
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
@@ -33,79 +34,103 @@ abstract class ContactListViewModelBase with Store {
}
Future<void> _init() async {
final walletInfos = await WalletInfo.getAll();
for (final info in walletInfos) {
final addressInfos = await info.getAddressInfos();
final addresses = await info.getAddresses();
if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) &&
addressInfos.isNotEmpty) {
for (var key in addressInfos.keys) {
final value = addressInfos[key];
final address = value?.first;
if (address != null) {
final name = _createName(info.name, address.label, key: key);
walletContacts.add(WalletContact(
address.address,
await loadWalletContacts();
_subscription = contactSource.bindToListWithTransform(
contacts,
(contact) => ContactRecord(contactSource, contact),
initialFire: true,
);
setOrderType(settingsStore.contactListOrder);
}
Future<void> _lastWalletContactsLoad = Future.value();
@action
Future<void> loadWalletContacts() async {
final waitFor = _lastWalletContactsLoad;
final done = Completer<void>();
_lastWalletContactsLoad = done.future;
await waitFor;
try {
final loadedContacts = <WalletContact>[];
final walletInfos = await WalletInfo.getAll();
for (final info in walletInfos) {
final addressInfos = await info.getAddressInfos();
final addresses = await info.getAddresses();
if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) &&
addressInfos.isNotEmpty) {
for (var key in addressInfos.keys) {
final value = addressInfos[key];
final address = value?.first;
if (address != null) {
final name = _createName(info.name, address.label, key: key);
loadedContacts.add(WalletContact(
address.address,
name,
getCryptoCurrencyForWalletListItem(
info.type,
),
walletType: info.type,
));
}
}
} else if (addresses.isNotEmpty == true && addresses.length > 1) {
if ([WalletType.monero, WalletType.wownero, WalletType.haven, WalletType.decred]
.contains(info.type)) {
final address = info.address;
final name = _createName(info.name, "", key: 0);
loadedContacts.add(WalletContact(
address,
name,
getCryptoCurrencyForWalletListItem(
info.type,
),
walletType: info.type,
));
} else {
addresses.forEach((address, label) {
if (label.isEmpty) {
return;
}
final name = _createName(info.name, label, key: null);
loadedContacts.add(WalletContact(
address,
name,
getCryptoCurrencyForWalletListItem(
info.type,
isTestnet: info.network == null
? false
: info.network!.toLowerCase().contains("testnet"),
),
walletType: info.type,
));
});
}
}
} else if (addresses.isNotEmpty == true && addresses.length > 1) {
if ([WalletType.monero, WalletType.wownero, WalletType.haven, WalletType.decred]
.contains(info.type)) {
final address = info.address;
final name = _createName(info.name, "", key: 0);
walletContacts.add(WalletContact(
address,
name,
} else {
loadedContacts.add(WalletContact(
info.address,
_createName(info.name, "",
key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)
? 0
: null),
getCryptoCurrencyForWalletListItem(
info.type,
),
walletType: info.type,
));
} else {
addresses.forEach((address, label) {
if (label.isEmpty) {
return;
}
final name = _createName(info.name, label, key: null);
walletContacts.add(WalletContact(
address,
name,
getCryptoCurrencyForWalletListItem(
info.type,
isTestnet:
info.network == null ? false : info.network!.toLowerCase().contains("testnet"),
),
walletType: info.type,
));
});
}
} else {
walletContacts.add(WalletContact(
info.address,
_createName(info.name, "",
key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)
? 0
: null),
getCryptoCurrencyForWalletListItem(
info.type,
),
walletType: info.type,
));
}
walletContacts = loadedContacts;
} catch (e) {
printV("loadWalletContacts failed: $e");
} finally {
done.complete();
}
_subscription = contactSource.bindToListWithTransform(
contacts, (Contact contact) => ContactRecord(contactSource, contact),
initialFire: true);
setOrderType(settingsStore.contactListOrder);
walletContacts = walletContacts.toList(); // rebuild
}
String _createName(String walletName, String label, {int? key = null}) {
@@ -614,6 +614,7 @@ abstract class DashboardViewModelBase with Store {
@computed
bool get shouldShowMwebAd {
return false;
if (wallet.type != WalletType.litecoin) return false;
if (mwebEnabled) return false;
@@ -0,0 +1,146 @@
import "dart:async";
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/solana/solana.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cake_wallet/view_model/contact_list/contact_list_view_model.dart";
import "package:cw_core/exceptions.dart";
import "package:cw_core/pending_transaction.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import "package:mobx/mobx.dart";
part "nft_send_view_model.g.dart";
class NFTSendViewModel = NFTSendViewModelBase with _$NFTSendViewModel;
abstract class NFTSendViewModelBase with Store {
NFTSendViewModelBase(this._appStore, this._contactListViewModel);
final AppStore _appStore;
final ContactListViewModel _contactListViewModel;
@observable
ExecutionState state = InitialExecutionState();
@observable
PendingTransaction? pendingTransaction;
bool shouldRequireTOTP2FAFor(String destinationAddress) {
final settingsStore = _appStore.settingsStore;
final isContact = _contactListViewModel.contactsToShow
.any((contact) => contact.address == destinationAddress);
if (isContact) {
return settingsStore.shouldRequireTOTP2FAForSendsToContact;
}
final isInternalWallet = _contactListViewModel.walletContactsToShow
.any((contact) => contact.address == destinationAddress);
if (isInternalWallet) {
return settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
}
return settingsStore.shouldRequireTOTP2FAForSendsToNonContact;
}
@action
Future<void> createTransaction(SolanaNFTAssetModel asset, String destinationAddress) async {
state = IsExecutingState();
try {
pendingTransaction = await solana!.sendNFT(
_appStore.wallet!,
mintAddress: asset.mint!,
destinationAddress: destinationAddress,
name: asset.name,
);
state = ExecutedSuccessfullyState();
} catch (e) {
pendingTransaction = null;
state = FailureState(_errorMessage(e));
}
}
@action
Future<void> commitTransaction() async {
state = IsExecutingState();
try {
final wallet = _appStore.wallet!;
final transaction = pendingTransaction!;
await transaction.commit();
final signature = transaction.id;
pendingTransaction = null;
state = ExecutedSuccessfullyState();
if (signature.isNotEmpty) {
unawaited(_pollForTransaction(wallet, signature));
}
} catch (e) {
state = FailureState(_errorMessage(e));
}
}
/// Re-entrancy latch for the send button, held only while the tap handler is
/// inside [AuthService.authenticateAction].
///
/// Deliberately not [state]: the auth sheet is pushed closable, and closing it
/// pops the route without ever invoking onAuthenticationFinished, so a flag
/// that waits on the auth result would never be cleared. The caller clears
/// this in a finally instead. Once auth succeeds, createTransaction takes over
/// the guard by setting IsExecutingState synchronously.
bool _isStartingSend = false;
bool get isStartingSend => _isStartingSend;
void markSendStarted() => _isStartingSend = true;
void markSendSettled() => _isStartingSend = false;
@action
void reset() {
_isStartingSend = false;
pendingTransaction = null;
state = InitialExecutionState();
}
Future<void> _pollForTransaction(WalletBase wallet, String signature) async {
try {
await solana!.pollForTransaction(wallet, signature);
} catch (e) {
printV("Failed to poll for NFT transfer: ${e.toString()}");
}
}
String _errorMessage(Object error) {
if (error is NotAnNFTException) {
return S.current.nft_send_not_an_nft;
}
if (error is NoAssociatedTokenAccountException) {
return S.current.solana_no_associated_token_account_exception;
}
if (error is SignSPLTokenTransactionRentException) {
return S.current.solana_sign_spl_token_transaction_rent_exception;
}
if (error is CreateAssociatedTokenAccountException) {
return "${S.current.solana_create_associated_token_account_exception} "
"${S.current.added_message_for_ata_error}";
}
printV("NFT send failed: ${error.toString()}");
return error.toString();
}
}
+382 -112
View File
@@ -1,13 +1,18 @@
import "dart:async";
import 'dart:convert';
import 'dart:developer';
import "package:cake_wallet/entities/saved_nfts.dart";
import 'package:cake_wallet/entities/solana_nft_asset_model.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import "package:cake_wallet/solana/solana.dart";
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/utils/debounce.dart';
import "package:cw_core/utils/ipfs_url.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:mobx/mobx.dart';
@@ -41,94 +46,96 @@ abstract class NFTViewModelBase with Store {
final BottomSheetService bottomSheetService;
final Debounce _fetchDebounce = Debounce(const Duration(milliseconds: 500));
Future<void> _lastFetch = Future.value();
String? _loadedWalletAddress;
Completer<void>? _queuedFetch;
final SavedNFTs _savedNFTs = SavedNFTs();
int _currentRefreshId = 0;
static const _requestTimeout = Duration(seconds: 20);
static const _runDeadline = Duration(minutes: 2);
Map<String, String> get _moralisHeaders => {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
};
@observable
bool isLoading;
@observable
bool isImportNFTLoading;
@observable
int unresolvedNFTCount = 0;
ObservableList<NFTAssetModel> nftAssetByWalletModels;
ObservableList<SolanaNFTAssetModel> solanaNftAssetModels;
bool get isSolanaWallet => appStore.wallet?.type == WalletType.solana;
String? get currentWalletName => appStore.wallet?.name;
@action
Future<void> getNFTAssetByWallet() async {
final wallet = appStore.wallet!;
void onWalletChanged() {
nftAssetByWalletModels.clear();
solanaNftAssetModels.clear();
unresolvedNFTCount = 0;
_nftErrorPopupShown = false;
_fetchDebounce.run(getNFTAssetByWallet);
}
if (!isNFTACtivatedChain(wallet.type, chainId: wallet.chainId)) return;
@action
Future<void> getNFTAssetByWallet() {
final queued = _queuedFetch;
final walletAddress = wallet.walletInfo.address;
log('Fetching wallet NFTs for $walletAddress');
if (queued != null) {
return queued.future;
}
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: wallet.chainId);
// the [chain] refers to the chain network that the nft is on
// the [format] refers to the number format type of the responses
// the [normalizedMetadata] field is a boolean that determines if
// the response would include a json string of the NFT Metadata that can be decoded
// and used within the wallet
// the [excludeSpam] field is a boolean that determines if spam nfts be excluded from the response.
final done = Completer<void>();
_queuedFetch = done;
Uri uri;
if (wallet.type == WalletType.solana) {
uri = Uri.https(
'solana-gateway.moralis.io',
'/account/$chainName/$walletAddress/nft',
);
} else {
uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/$walletAddress/nft',
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"exclude_spam": "true",
"normalizeMetadata": "true",
},
);
final waitFor = _lastFetch;
_lastFetch = done.future;
unawaited(_fetchAfterPreviousRun(waitFor, done));
return done.future;
}
Future<void> _fetchAfterPreviousRun(Future<void> waitFor, Completer<void> done) async {
await waitFor;
if (identical(_queuedFetch, done)) {
_queuedFetch = null;
}
try {
if (isLoading) return;
final wallet = appStore.wallet!;
if (!isNFTACtivatedChain(wallet.type, chainId: wallet.chainId)) {
return;
}
final walletAddress = wallet.walletInfo.address;
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: wallet.chainId);
isLoading = true;
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final refreshId = ++_currentRefreshId;
final decodedResponse = jsonDecode(response.body);
if (isSolanaWallet) {
await _getSolanaNFTAssets(wallet, walletAddress, chainName, refreshId)
.timeout(_runDeadline);
} else {
await _getEvmNFTAssets(wallet.name, walletAddress, chainName, refreshId);
}
_nftErrorPopupShown = false;
if (wallet.type == WalletType.solana) {
final results = await Future.wait(
(decodedResponse as List<dynamic>).map(
(x) {
final data = x as Map<String, dynamic>;
final mint = data['mint'] as String? ?? '';
return getSolanaNFTDetails(mint, chainName);
},
).toList(),
);
solanaNftAssetModels.clear();
solanaNftAssetModels.addAll(results);
} else {
final result =
WalletNFTsResponseModel.fromJson(decodedResponse as Map<String, dynamic>).result ?? [];
nftAssetByWalletModels.clear();
nftAssetByWalletModels.addAll(result);
}
} catch (e) {
log(e.toString());
printV("Fetching wallet NFTs failed: ${e.toString()}");
if (!_nftErrorPopupShown) {
_nftErrorPopupShown = true;
bottomSheetService.queueBottomSheet(
@@ -140,79 +147,342 @@ abstract class NFTViewModelBase with Store {
}
} finally {
isLoading = false;
done.complete();
}
}
Future<SolanaNFTAssetModel> getSolanaNFTDetails(String address, String chainName) async {
final uri = Uri.https(
'solana-gateway.moralis.io',
'/nft/$chainName/$address/metadata',
Future<void> _getEvmNFTAssets(
String walletName,
String walletAddress,
String chainName,
int refreshId,
) async {
final saved = await _savedNFTs.evmNFTs(walletName, chainName);
var result = const <NFTAssetModel>[];
Object? indexerError;
try {
final response = await ProxyWrapper()
.get(
clearnetUri: Uri.https(
"deep-index.moralis.io",
"/api/v2.2/$walletAddress/nft",
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"exclude_spam": "true",
"normalizeMetadata": "true",
},
),
headers: _moralisHeaders,
)
.timeout(_requestTimeout);
result = WalletNFTsResponseModel.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
).result ??
[];
} catch (e) {
indexerError = e;
}
if (appStore.wallet?.walletInfo.address != walletAddress || refreshId != _currentRefreshId) {
return;
}
final listed = result.map(SavedNFTs.evmIdentifier).toSet();
nftAssetByWalletModels.clear();
nftAssetByWalletModels.addAll(result);
nftAssetByWalletModels
.addAll(saved.where((asset) => !listed.contains(SavedNFTs.evmIdentifier(asset))));
if (indexerError != null) {
throw indexerError;
}
}
Future<void> _getSolanaNFTAssets(
WalletBase wallet,
String walletAddress,
String chainName,
int refreshId,
) async {
final importedAssets = await _savedNFTs.solanaNFTs(wallet.name);
var indexerMints = const <String>[];
Object? indexerError;
try {
indexerMints = await _fetchMintsFromIndexer(walletAddress, chainName);
} catch (e) {
indexerError = e;
}
final heldMints = await _fetchMintsHeldOnChain(wallet);
final failedMints = <String>{};
final assets = await _loadAssetsForMints(
wallet: wallet,
chainName: chainName,
mints: <String>{...indexerMints, ...importedAssets.keys},
heldMints: heldMints,
importedAssets: importedAssets,
failedMints: failedMints,
);
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
if (appStore.wallet?.walletInfo.address != walletAddress || refreshId != _currentRefreshId) {
return;
}
assets.addAll(
_assetsToKeepFromPreviousRefresh(walletAddress, assets, failedMints, indexerError != null));
solanaNftAssetModels.clear();
solanaNftAssetModels.addAll(assets);
_loadedWalletAddress = walletAddress;
final shownMints = assets.map((asset) => asset.mint).toSet();
unresolvedNFTCount = failedMints.where((mint) => !shownMints.contains(mint)).length;
if (heldMints != null) {
await _savedNFTs.refreshSolana(wallet.name, assets);
}
if (indexerError != null) {
throw indexerError;
}
}
Future<List<String>> _fetchMintsFromIndexer(String walletAddress, String chainName) async {
final response = await ProxyWrapper()
.get(
clearnetUri: Uri.https(
"solana-gateway.moralis.io",
"/account/$chainName/$walletAddress/nft",
),
headers: _moralisHeaders,
)
.timeout(_requestTimeout);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception("Failed to fetch wallet NFTs (${response.statusCode})");
}
return (jsonDecode(response.body) as List<dynamic>)
.map((entry) => (entry as Map<String, dynamic>)["mint"] as String? ?? "")
.where((mint) => mint.isNotEmpty)
.toList();
}
Future<Set<String>?> _fetchMintsHeldOnChain(WalletBase wallet) async {
try {
return await solana!.getHeldTokenMints(wallet);
} catch (e) {
printV("Could not fetch held token mints: ${e.toString()}");
return null;
}
}
Future<List<SolanaNFTAssetModel>> _loadAssetsForMints({
required WalletBase wallet,
required String chainName,
required Set<String> mints,
required Set<String>? heldMints,
required Map<String, SolanaNFTAssetModel> importedAssets,
required Set<String> failedMints,
}) async {
const batchSize = 5;
final ordered = mints.toList();
final assets = <SolanaNFTAssetModel>[];
for (var i = 0; i < ordered.length; i += batchSize) {
final described = await Future.wait(
ordered.skip(i).take(batchSize).map((mint) async {
try {
final asset = await getSolanaNFTDetails(wallet, mint, chainName);
asset.isOwned = heldMints?.contains(mint) ?? importedAssets[mint]?.isOwned;
return asset;
} catch (e) {
printV("Failed to load NFT $mint: ${e.toString()}");
failedMints.add(mint);
final cached = importedAssets[mint];
if (cached != null && heldMints != null) {
cached.isOwned = heldMints.contains(mint);
}
return cached;
}
}),
);
assets.addAll(described.whereType<SolanaNFTAssetModel>());
}
return assets;
}
List<SolanaNFTAssetModel> _assetsToKeepFromPreviousRefresh(
String walletAddress,
List<SolanaNFTAssetModel> described,
Set<String> failedMints,
bool indexerFailed,
) {
if (_loadedWalletAddress != walletAddress) {
return const [];
}
final describedMints = described.map((asset) => asset.mint).toSet();
return solanaNftAssetModels.where((asset) {
final mint = asset.mint;
if (mint == null || describedMints.contains(mint)) {
return false;
}
return indexerFailed || failedMints.contains(mint);
}).toList();
}
Future<SolanaNFTAssetModel> getSolanaNFTDetails(
WalletBase wallet,
String address,
String chainName,
) async {
SolanaNFTAssetModel? asset;
try {
asset = await _fetchMoralisNFT(address, chainName);
asset.address = address;
asset.mint = address;
} catch (e) {
printV("Moralis NFT metadata failed for $address: ${e.toString()}");
}
if (asset != null && (asset.imageOriginalUrl?.isNotEmpty ?? false)) {
return asset;
}
// fallback for when moralis is unavailable, we still get the core details for that nft onchain
final onChainData = await solana!.getNFTOnChainMetadata(wallet, address);
if (asset != null) {
if (onChainData != null) {
asset.imageOriginalUrl = tryNormalizeIpfsUrl(onChainData.imageUrl);
}
return asset;
}
if (onChainData == null) {
throw Exception("Could not load NFT metadata for $address");
}
return SolanaNFTAssetModel(
address: address,
mint: address,
standard: "metaplex",
name: onChainData.name,
symbol: onChainData.symbol,
imageOriginalUrl: onChainData.imageUrl,
metadataOriginalUrl: onChainData.metadataUri,
);
}
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
Future<SolanaNFTAssetModel> _fetchMoralisNFT(String address, String chainName) async {
final response = await ProxyWrapper()
.get(
clearnetUri: Uri.https(
"solana-gateway.moralis.io",
"/nft/$chainName/$address/metadata",
),
headers: _moralisHeaders,
)
.timeout(_requestTimeout);
return SolanaNFTAssetModel.fromJson(decodedResponse);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception("Failed to fetch NFT metadata (${response.statusCode})");
}
return SolanaNFTAssetModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}
@action
Future<void> onNFTSent(String walletName, String mint) async {
await _savedNFTs.removeSolana(walletName, mint);
if (appStore.wallet?.name == walletName) {
solanaNftAssetModels.removeWhere((asset) => asset.mint == mint);
}
}
Future<void> _importEvmNFT(
String walletName,
String tokenAddress,
String? tokenId,
String chainName,
) async {
final response = await ProxyWrapper()
.get(
clearnetUri: Uri.https(
"deep-index.moralis.io",
"/api/v2.2/nft/$tokenAddress/$tokenId",
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"normalizeMetadata": "true",
},
),
headers: _moralisHeaders,
)
.timeout(_requestTimeout);
final asset = NFTAssetModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
nftAssetByWalletModels.add(asset);
await _savedNFTs.addEvm(walletName, chainName, asset);
}
@action
Future<void> importNFT(String tokenAddress, String? tokenId) async {
final walletType = appStore.wallet!.type;
final wallet = appStore.wallet!;
int? chainId;
if (isEVMCompatibleChain(walletType)) {
chainId = evm!.getSelectedChainId(appStore.wallet!);
if (isEVMCompatibleChain(wallet.type)) {
chainId = evm!.getSelectedChainId(wallet);
}
final chainName = getChainNameBasedOnWalletType(walletType, chainId: chainId);
// the [chain] refers to the chain network that the nft is on
// the [format] refers to the number format type of the responses
// the [normalizedMetadata] field is a boolean that determines if
// the response would include a json string of the NFT Metadata that can be decoded
// and used within the wallet
final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: chainId);
try {
isImportNFTLoading = true;
if (appStore.wallet!.type == WalletType.solana) {
final result = await getSolanaNFTDetails(tokenAddress, chainName);
if (isSolanaWallet) {
final result = await getSolanaNFTDetails(wallet, tokenAddress, chainName);
try {
result.isOwned = (await solana!.getHeldTokenMints(wallet)).contains(result.mint);
} catch (e) {
printV("Could not check NFT ownership: ${e.toString()}");
}
solanaNftAssetModels.removeWhere((asset) => asset.mint == result.mint);
solanaNftAssetModels.add(result);
await _savedNFTs.addSolana(wallet.name, result);
} else {
final uri = Uri.https(
'deep-index.moralis.io',
'/api/v2.2/nft/$tokenAddress/$tokenId',
{
"chain": chainName,
"format": "decimal",
"media_items": "false",
"normalizeMetadata": "true",
},
);
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
final nftAsset = NFTAssetModel.fromJson(decodedResponse);
nftAssetByWalletModels.add(nftAsset);
await _importEvmNFT(wallet.name, tokenAddress, tokenId, chainName);
}
} catch (e) {
printV("Importing NFT $tokenAddress failed: ${e.toString()}");
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: e.toString(),
message: S.current.nft_import_failed,
),
);
} finally {
+15 -19
View File
@@ -3,7 +3,6 @@ import 'dart:async';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cw_monero/monero_wallet.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
@@ -56,26 +55,24 @@ abstract class DevMoneroBackgroundSyncBase with Store {
bool isBackgroundSyncing = false;
Future<void> _setValues() async {
final w = (wallet as MoneroWallet);
localBlockHeight = (await monero!.getCurrentHeight()).toString();
nodeBlockHeight = (await w.getNodeHeight()).toString();
final keys = w.keys;
primaryAddress = keys.primaryAddress;
publicViewKey = keys.publicViewKey;
privateViewKey = keys.privateViewKey;
publicSpendKey = keys.publicSpendKey;
privateSpendKey = keys.privateSpendKey;
passphrase = keys.passphrase;
seed = w.seed;
seedLegacy = w.seedLegacy("English");
nodeBlockHeight = (await monero!.getNodeHeight(wallet)).toString();
final keys = monero!.getKeys(wallet);
primaryAddress = keys["primaryAddress"];
publicViewKey = keys["publicViewKey"];
privateViewKey = keys["privateViewKey"];
publicSpendKey = keys["publicSpendKey"];
privateSpendKey = keys["privateSpendKey"];
passphrase = keys["passphrase"];
seed = wallet.seed;
seedLegacy = monero!.getLegacySeed(wallet, "English");
tick = refreshTimer?.tick ?? -1;
isBackgroundSyncing = w.isBackgroundSyncRunning;
isBackgroundSyncing = monero!.isBackgroundSyncRunning(wallet);
}
@action
Future<void> manualRescan() async {
final w = (wallet as MoneroWallet);
await wallet.rescan(height: await w.getNodeHeight() - 10000);
await wallet.rescan(height: await monero!.getNodeHeight(wallet) - 10000);
}
@action
@@ -93,14 +90,13 @@ abstract class DevMoneroBackgroundSyncBase with Store {
@action
void startBackgroundSync() {
final w = (wallet as MoneroWallet);
w.startBackgroundSync();
unawaited(monero!.startBackgroundSync(wallet));
}
@action
Future<void> stopBackgroundSync() async {
final w = (wallet as MoneroWallet);
final keyService = getIt.get<KeyService>();
await w.stopBackgroundSync(await keyService.getWalletPassword(walletName: wallet.name));
final password = await keyService.getWalletPassword(walletName: wallet.name);
await monero!.stopBackgroundSync(wallet, password);
}
}
@@ -29,6 +29,8 @@ import "package:trezor_flutter/trezor_flutter.dart" as sdk;
part "trezor_connect_view_model.g.dart";
const trezorUseNative = [WalletType.bitcoin, WalletType.monero];
class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel;
abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store {
@@ -242,7 +244,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
}
@override
bool isConnected(WalletType type) => type == WalletType.monero
bool isConnected(WalletType type) => trezorUseNative.contains(type)
? _client != null && _client?.connection.isDisconnected == false
: true;
@@ -252,9 +254,9 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
case WalletType.monero:
return monero!.getTrezorHardwareWalletService(_client!);
case WalletType.bitcoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true);
return bitcoin!.getTrezorHardwareWalletService(null, _client!, true);
case WalletType.litecoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false);
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, null, false);
case WalletType.ethereum:
case WalletType.polygon:
return evm!.getTrezorHardwareWalletService(trezorConnect);
+10 -1
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/cake_pay/src/cake_pay_exceptions.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/core/utilities.dart';
@@ -64,6 +65,9 @@ abstract class OrderDetailsViewModelBase with Store {
@observable
ObservableList<StandartListItem> items;
@observable
bool isCakePaySessionExpired = false;
final CakePayService cakePayService;
final Box<Order> orders;
OrderProvider? _provider;
@@ -74,7 +78,9 @@ abstract class OrderDetailsViewModelBase with Store {
@action
Future<void> _updateOrder() async {
try {
if (_provider == null) return;
if (_provider == null || isCakePaySessionExpired) {
return;
}
final updatedOrderObj = await _provider!.findOrderById(order.id);
final updatedOrder = updatedOrderObj.$1;
@@ -95,6 +101,9 @@ abstract class OrderDetailsViewModelBase with Store {
}
_updateItems();
} on CakePayUnauthorizedException {
timer?.cancel();
isCakePaySessionExpired = true;
} catch (e) {
printV(e.toString());
}
+5 -3
View File
@@ -907,9 +907,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
final isSendAll = outputs.any((output) => output.sendAll);
if (!isSendAll) {
final estimateTxAmountDouble = outputs.fold<double>(
0, (acc, output) => acc + (double.tryParse(output.cryptoAmount) ?? 0));
if (estimateTxAmountDouble <= 0) throw Exception('Amount must be greater than 0');
final estimateTxAmount = outputs.fold<BigInt>(
BigInt.zero, (acc, output) => acc + output.cryptoAmountMoney.amount);
if (estimateTxAmount <= BigInt.zero) {
throw Exception('Amount must be greater than 0');
}
}
pendingTransaction = await wallet.createTransaction(_credentials(provider));
@@ -56,7 +56,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
availableAccounts.addAll(accounts);
_nextIndex += limit;
} catch (e) {
} on Exception catch (e) {
printV(e);
error =
hardwareWalletVM.interpretErrorCode(e.toString()) ?? S.current.ledger_connection_error;
+1 -4
View File
@@ -11,13 +11,10 @@ import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_monero/monero_wallet.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/decred/decred.dart';
import 'package:polyseed/polyseed.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
part 'wallet_keys_view_model.g.dart';
@@ -118,7 +115,7 @@ abstract class WalletKeysViewModelBase with Store {
final langName = PolyseedLang.getByPhrase(_wallet.seed!).nameEnglish;
if (_wallet.type == WalletType.monero) {
return (_wallet as MoneroWalletBase).seedLegacy(langName);
return monero!.getLegacySeed(_wallet, langName);
} else if (_wallet.type == WalletType.wownero) {
return wownero!.getLegacySeed(_wallet, langName);
}