mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Merge branch 'cake-tech:dev' into dev
This commit is contained in:
@@ -118,7 +118,7 @@ jobs:
|
||||
pushd scripts/monero_c/release
|
||||
mkdir -p $(git describe --tags)
|
||||
pushd $(git describe --tags)
|
||||
wget https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip
|
||||
wget https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC2/release-bundle.zip
|
||||
unzip release-bundle.zip
|
||||
rm release-bundle.zip
|
||||
popd
|
||||
|
||||
@@ -225,3 +225,5 @@ scripts/zcash_lib
|
||||
scripts/android/yttrium
|
||||
scripts/bitbox_flutter
|
||||
scripts/reown_flutter
|
||||
|
||||
*.salive
|
||||
@@ -60,7 +60,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId appProperties['id']
|
||||
minSdkVersion 24
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 36
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
@@ -128,6 +128,14 @@
|
||||
<data android:scheme="nano-gpt" />
|
||||
</intent-filter>
|
||||
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||
android:resource="@xml/device_filter" />
|
||||
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
|
||||
</activity>
|
||||
<meta-data
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
<!-- BitBox02 Vendor ID: 0x03eb, Product ID: 0x2403 -->
|
||||
<usb-device vendor-id="1003" product-id="9219"/>
|
||||
|
||||
<!-- Trezor Devices -->
|
||||
<usb-device vendor-id="21324" product-id="1"/>
|
||||
<usb-device vendor-id="4617" product-id="21440"/>
|
||||
<usb-device vendor-id="4617" product-id="21441"/>
|
||||
</resources>
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.5 2C17.8807 2 19 3.11929 19 4.5V5C19.2761 5 19.5 5.22386 19.5 5.5V8C19.5 8.24171 19.3286 8.44371 19.1006 8.49023L19 8.5V19.5C19 20.8807 17.8807 22 16.5 22H7.5C6.11929 22 5 20.8807 5 19.5V4.5C5 3.11929 6.11929 2 7.5 2H16.5ZM14.5 19C13.9477 19 13.5 19.4477 13.5 20C13.5 20.5523 13.9477 21 14.5 21H16.5C17.0523 21 17.5 20.5523 17.5 20C17.5 19.4477 17.0523 19 16.5 19H14.5ZM7.5 3.5C6.94772 3.5 6.5 3.94772 6.5 4.5V16.5C6.5 17.0523 6.94772 17.5 7.5 17.5H16.5C17.0523 17.5 17.5 17.0523 17.5 16.5V4.5C17.5 3.94772 17.0523 3.5 16.5 3.5H7.5Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 665 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 32 32">
|
||||
<path fill="#000" fill-rule="evenodd" d="M7 6a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4v12.191a9 9 0 0 1-1.204 4.498l-3.353 5.81A3 3 0 0 1 17.845 30h-3.69a3 3 0 0 1-2.598-1.5l-3.353-5.811A9 9 0 0 1 7 18.19zm4-2a2 2 0 0 0-2 2v12.191a7 7 0 0 0 .937 3.498l3.352 5.81a1 1 0 0 0 .866.501h3.69a1 1 0 0 0 .866-.5l3.352-5.81a7 7 0 0 0 .937-3.5V6a2 2 0 0 0-2-2z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 447 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -29,7 +29,7 @@ enum DeviceConnectionType {
|
||||
break;
|
||||
case HardwareWalletType.trezor:
|
||||
isSupported = [
|
||||
// WalletType.monero,
|
||||
WalletType.monero,
|
||||
WalletType.bitcoin,
|
||||
WalletType.litecoin,
|
||||
WalletType.ethereum,
|
||||
|
||||
@@ -4,7 +4,8 @@ import 'dart:typed_data';
|
||||
import 'package:cw_core/hardware/hardware_account_data.dart';
|
||||
|
||||
abstract class HardwareWalletService {
|
||||
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5});
|
||||
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
Future<Uint8List> signTransaction({required String transaction}) => throw UnimplementedError();
|
||||
|
||||
|
||||
+23
-3
@@ -132,6 +132,26 @@ class Node {
|
||||
};
|
||||
}
|
||||
|
||||
Node.fromUri(Uri uri, WalletType type)
|
||||
: id = 0,
|
||||
uriRaw =
|
||||
"${uri.host}${uri.hasPort ? ':${uri.port}' : (uri.queryParameters['port']?.isNotEmpty == true ? ':${uri.queryParameters['port']}' : '')}",
|
||||
path = uri.path,
|
||||
login = uri.userInfo.contains(':')
|
||||
? uri.userInfo.split(':')[0]
|
||||
: uri.queryParameters['username'],
|
||||
password = uri.userInfo.contains(':')
|
||||
? uri.userInfo.split(':')[1]
|
||||
: uri.queryParameters['password'],
|
||||
useSSL = uri.queryParameters['protocol'] == 'https',
|
||||
trusted = uri.queryParameters['trusted'] == 'true',
|
||||
isPow = false,
|
||||
isEnabledForAutoSwitching = false,
|
||||
isOfficial = false,
|
||||
isBuiltin = false,
|
||||
isDefault = false,
|
||||
typeRaw = serializeToInt(type);
|
||||
|
||||
Future<int> delete() async {
|
||||
return await db!.delete(tableName, where: '${selfIdColumn} = ?', whereArgs: [id]);
|
||||
}
|
||||
@@ -378,14 +398,14 @@ class Node {
|
||||
final body = {'jsonrpc': '2.0', 'id': '0', 'method': methodName};
|
||||
|
||||
try {
|
||||
final client = ProxyWrapper().getHttpIOClient();
|
||||
|
||||
final jsonBody = json.encode(body);
|
||||
|
||||
final response = await client.post(
|
||||
rpcUri,
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: rpcUri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonBody,
|
||||
allowMitmMoneroBypassSSLCheck: true
|
||||
);
|
||||
// Check if we received a 401 Unauthorized response
|
||||
if (response.statusCode == 401) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import "package:yaml/yaml.dart";
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
@@ -78,6 +77,7 @@ Future<List<Node>> loadDefaultNodes(WalletType type) async {
|
||||
map["isBuiltin"] = 1;
|
||||
map["isOfficial"] = ((map["isOfficial"] as bool? ?? false) ? 1 : 0);
|
||||
map["isDefault"] = ((map["isDefault"] as bool? ?? false) ? 1 : 0);
|
||||
|
||||
final node = Node.fromMap(Map<String, Object>.from(map));
|
||||
node.type = type;
|
||||
nodes.add(node);
|
||||
@@ -94,7 +94,11 @@ Future<List<Node>> loadDefaultNanoPowNodes() async {
|
||||
|
||||
for (final raw in loadedPowNodes) {
|
||||
if (raw is Map) {
|
||||
final node = Node.fromMap(Map<String, Object>.from(raw));
|
||||
final map = Map.from(raw);
|
||||
map["isBuiltin"] = 1;
|
||||
map["isOfficial"] = ((map["isOfficial"] as bool? ?? false) ? 1 : 0);
|
||||
map["isDefault"] = ((map["isDefault"] as bool? ?? false) ? 1 : 0);
|
||||
final node = Node.fromMap(Map<String, Object>.from(map));
|
||||
node.isPow = true;
|
||||
node.type = WalletType.nano;
|
||||
nodes.add(node);
|
||||
|
||||
@@ -44,7 +44,7 @@ enum HardwareWalletType {
|
||||
coldcard,
|
||||
seedsigner,
|
||||
keystone,
|
||||
trezor,
|
||||
trezor;
|
||||
}
|
||||
|
||||
enum WalletInfoAddressType {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -118,3 +119,10 @@ void _logLedgerCommand(Uint8List command, [bool isResponse = true]) {
|
||||
"> ${_ledgerMoneroCommands[command[1]]} ${toHexString(command.sublist(2))}");
|
||||
}
|
||||
}
|
||||
|
||||
class MoneroLedgerService extends HardwareWalletService {
|
||||
final LedgerConnection connection;
|
||||
|
||||
MoneroLedgerService(this.connection);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import 'package:cw_monero/monero_unspent.dart';
|
||||
import 'package:cw_monero/monero_wallet_addresses.dart';
|
||||
import 'package:cw_monero/monero_wallet_service.dart';
|
||||
import 'package:cw_monero/pending_monero_transaction.dart';
|
||||
import 'package:cw_monero/trezor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
|
||||
@@ -385,8 +386,42 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
||||
currentWallet!.hasUnknownKeyImages();
|
||||
}
|
||||
|
||||
MoneroTrezorService? trezorService;
|
||||
Future<void> syncTrezor() async {
|
||||
if (trezorService == null) throw Exception("Trezor not connected");
|
||||
|
||||
final ptr = Pointer<Void>.fromAddress(currentWallet!.ffiAddress());
|
||||
final tdis = monero.Wallet_exportTrezorTdis(ptr);
|
||||
|
||||
final response = await Trezor(trezorService!).keyImageSync(tdis);
|
||||
|
||||
final success = monero.Wallet_importTrezorEncryptedKeyImagesJson(ptr, response);
|
||||
|
||||
if (!success) throw Exception(monero.Wallet_errorString(ptr));
|
||||
}
|
||||
|
||||
Future<String> signTrezorTransaction(String json) async {
|
||||
if (trezorService == null) throw Exception("Trezor not connected");
|
||||
return Trezor(trezorService!).signTransaction(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
if (hardwareWalletType == HardwareWalletType.trezor) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
try {
|
||||
return await _createTransaction(credentials);
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
}
|
||||
await save();
|
||||
await Future.delayed(Duration(seconds: i));
|
||||
}
|
||||
}
|
||||
return await _createTransaction(credentials);
|
||||
}
|
||||
|
||||
Future<PendingTransaction> _createTransaction(Object credentials) async {
|
||||
final _credentials = credentials as MoneroTransactionCreationCredentials;
|
||||
final inputs = <String>[];
|
||||
final outputs = _credentials.outputs;
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_core/get_height_by_date.dart';
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:cw_core/monero_wallet_utils.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
@@ -20,8 +20,8 @@ import 'package:cw_monero/api/wallet_manager.dart';
|
||||
import 'package:cw_monero/bip39_seed.dart';
|
||||
import 'package:cw_monero/ledger.dart';
|
||||
import 'package:cw_monero/monero_wallet.dart';
|
||||
import 'package:cw_monero/trezor.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
|
||||
import 'package:monero/monero.dart' as monero;
|
||||
import 'package:polyseed/polyseed.dart';
|
||||
|
||||
@@ -46,11 +46,11 @@ class MoneroNewWalletCredentials extends WalletCredentials {
|
||||
class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
|
||||
MoneroRestoreWalletFromHardwareCredentials(
|
||||
{required String name,
|
||||
required this.ledgerConnection,
|
||||
required this.hardwareWalletService,
|
||||
int height = 0,
|
||||
String? password})
|
||||
: super(name: name, password: password, height: height);
|
||||
LedgerConnection ledgerConnection;
|
||||
HardwareWalletService hardwareWalletService;
|
||||
}
|
||||
|
||||
class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
@@ -193,7 +193,7 @@ class MoneroWalletService extends WalletService<
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
password: password);
|
||||
|
||||
if (wallet.isHardwareWallet) {
|
||||
if (wallet.hardwareWalletType == HardwareWalletType.ledger) {
|
||||
wallet.setLedgerConnection(gLedger!);
|
||||
gLedger = null;
|
||||
}
|
||||
@@ -301,15 +301,29 @@ class MoneroWalletService extends WalletService<
|
||||
try {
|
||||
final path = await pathForWallet(name: credentials.name, type: getType());
|
||||
final password = credentials.password;
|
||||
final height = credentials.height;
|
||||
|
||||
enableLedgerExchange(credentials.ledgerConnection);
|
||||
if (credentials.hardwareWalletService case MoneroLedgerService service) {
|
||||
enableLedgerExchange(service.connection);
|
||||
|
||||
await monero_wallet_manager.restoreWalletFromHardwareWallet(
|
||||
await monero_wallet_manager.restoreWalletFromHardwareWallet(
|
||||
path: path,
|
||||
password: password!,
|
||||
restoreHeight: height!,
|
||||
deviceName: 'Ledger');
|
||||
restoreHeight: credentials.height!,
|
||||
deviceName: 'Ledger',
|
||||
);
|
||||
} else if (credentials.hardwareWalletService case MoneroTrezorService service) {
|
||||
final watchCredentials = await Trezor(service).getWatchCredentials();
|
||||
|
||||
monero_wallet_manager.restoreWalletFromKeys(
|
||||
path: path,
|
||||
password: credentials.password!,
|
||||
language: "English",
|
||||
restoreHeight: credentials.height!,
|
||||
address: watchCredentials.address,
|
||||
viewKey: watchCredentials.watchKey,
|
||||
spendKey: "",
|
||||
);
|
||||
}
|
||||
|
||||
final wallet = MoneroWallet(
|
||||
walletInfo: credentials.walletInfo!,
|
||||
@@ -567,7 +581,7 @@ class MoneroWalletService extends WalletService<
|
||||
if (walletInfo == null) {
|
||||
return false;
|
||||
}
|
||||
return walletInfo.isHardwareWallet;
|
||||
return walletInfo.hardwareWalletType == HardwareWalletType.ledger;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,4 +599,4 @@ Future<void> closeWalletAwaitIfShould(int wmaddr, int waddr) async {
|
||||
monero.WalletManager_errorString(Pointer.fromAddress(wmaddr));
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_monero/api/account_list.dart';
|
||||
import 'package:cw_monero/api/structs/pending_transaction.dart';
|
||||
import 'package:cw_monero/api/transaction_history.dart'
|
||||
@@ -10,6 +13,7 @@ import 'package:cw_core/amount_converter.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_monero/api/wallet.dart';
|
||||
import 'package:cw_monero/monero_wallet.dart';
|
||||
import 'package:monero/monero.dart' as monero;
|
||||
|
||||
class DoubleSpendException implements Exception {
|
||||
DoubleSpendException();
|
||||
@@ -43,22 +47,39 @@ class PendingMoneroTransaction with PendingTransaction {
|
||||
CryptoCurrency.xmr, pendingTransactionDescription.fee);
|
||||
|
||||
@override
|
||||
bool shouldCommitUR() => isViewOnly;
|
||||
bool shouldCommitUR() => isViewOnly && wallet.hardwareWalletType != HardwareWalletType.trezor;
|
||||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
try {
|
||||
await monero_transaction_history.commitTransactionFromPointerAddress(
|
||||
address: pendingTransactionDescription.pointerAddress,
|
||||
useUR: false);
|
||||
} catch (e) {
|
||||
final message = e.toString();
|
||||
if (wallet.hardwareWalletType == HardwareWalletType.trezor) {
|
||||
final ptr = Pointer<Void>.fromAddress(pendingTransactionDescription.pointerAddress);
|
||||
final ret = await monero.PendingTransaction_commitTrezor(ptr, 0);
|
||||
|
||||
if (message.contains('Reason: double spend')) {
|
||||
throw DoubleSpendException();
|
||||
final json = await wallet.signTrezorTransaction(ret);
|
||||
|
||||
final wptr = Pointer<Void>.fromAddress(currentWallet!.ffiAddress());
|
||||
|
||||
final suc = await monero.Wallet_submitTransactionHex(wptr, json);
|
||||
|
||||
if (!suc) {
|
||||
final err = monero.UnsignedTransaction_errorString(ptr);
|
||||
printV(err);
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await monero_transaction_history.commitTransactionFromPointerAddress(
|
||||
address: pendingTransactionDescription.pointerAddress,
|
||||
useUR: false);
|
||||
} catch (e) {
|
||||
final message = e.toString();
|
||||
|
||||
rethrow;
|
||||
if (message.contains('Reason: double spend')) {
|
||||
throw DoubleSpendException();
|
||||
}
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
storeSync(force: true);
|
||||
unawaited(() async {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:trezor_flutter/trezor_flutter.dart';
|
||||
|
||||
class MoneroTrezorService extends HardwareWalletService {
|
||||
final TrezorClient client;
|
||||
|
||||
MoneroTrezorService(this.client);
|
||||
}
|
||||
|
||||
class MoneroTrezorWatchCredentials {
|
||||
final String watchKey;
|
||||
final String address;
|
||||
|
||||
const MoneroTrezorWatchCredentials(this.watchKey, this.address);
|
||||
}
|
||||
|
||||
class Trezor {
|
||||
final MoneroTrezorService service;
|
||||
|
||||
Trezor(this.service);
|
||||
|
||||
Future<MoneroTrezorWatchCredentials> getWatchCredentials() async {
|
||||
final credentials = await TrezorMonero(service.client).getWatchCredentials();
|
||||
|
||||
return MoneroTrezorWatchCredentials(credentials.$1, credentials.$2);
|
||||
}
|
||||
|
||||
Future<String> keyImageSync(String tdis) async {
|
||||
final tdisMap = jsonDecode(tdis) as Map<String, dynamic>;
|
||||
final tdisList = tdisMap["tdis"] as List<dynamic>;
|
||||
|
||||
final txIds = <MoneroKeyImageTxData>[];
|
||||
for (final tdi in tdisList) {
|
||||
txIds.add(MoneroKeyImageTxData(
|
||||
outKey: tdi["out_key"],
|
||||
txPubKey: tdi["tx_pub_key"],
|
||||
internalOutputIndex: tdi["internal_output_index"],
|
||||
subAddrMajor: tdi["sub_addr_major"],
|
||||
subAddrMinor: tdi["sub_addr_minor"],
|
||||
additionalTxPubKeys: (tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? []
|
||||
));
|
||||
}
|
||||
final keyImages = await TrezorMonero(service.client).syncKeyImages(txIds);
|
||||
|
||||
return jsonEncode(keyImages.toMap());
|
||||
}
|
||||
|
||||
Future<String> signTransaction(String json) async {
|
||||
return TrezorMonero(service.client).signTransaction(jsonDecode(json));
|
||||
}
|
||||
}
|
||||
+45
-11
@@ -296,6 +296,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
fast_base58:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fast_base58
|
||||
sha256: "611f65633b734f27a850b51371b3eba993a5165650e12e8e7b02959f3768ba06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -342,10 +350,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_web_bluetooth
|
||||
sha256: "1363831def5eed1e1064d1eca04e8ccb35446e8f758579c3c519e156b77926da"
|
||||
sha256: ad26a1b3fef95b86ea5f63793b9a0cdc1a33490f35d754e4e711046cae3ebbf8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -494,18 +502,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ledger_flutter_plus
|
||||
sha256: "1c03f3c4a9754b5f0170a9eb9552ec54fa86e985f8ee71a255ee2c5629b53d31"
|
||||
sha256: "59c5a6062505c721c2ca909fa085c52cffe93de995174b8418b54a6acf52eb70"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
version: "1.5.6"
|
||||
ledger_usb_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ledger_usb_plus
|
||||
sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87"
|
||||
sha256: "911df2d1d55cdae612ff2c9db140cf71a9b28a67ba16adfdc4983cd9f02c531e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.8"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -582,9 +590,9 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "impls/monero.dart"
|
||||
ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2
|
||||
resolved-ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2
|
||||
url: "https://github.com/mrcyjanek/monero_c"
|
||||
ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d"
|
||||
resolved-ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d"
|
||||
url: "http://github.com/mrcyjanek/monero_c.git"
|
||||
source: git
|
||||
version: "0.0.0"
|
||||
mutex:
|
||||
@@ -724,6 +732,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -962,6 +978,24 @@ packages:
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
trezor_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: trezor-flutter
|
||||
ref: "4bf54339a1caefd4c53a4a8873a9686ec13a9107"
|
||||
resolved-ref: "4bf54339a1caefd4c53a4a8873a9686ec13a9107"
|
||||
url: "https://github.com/cake-tech/trezor-flutter.git"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
trezor_usb_transport:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: trezor_usb_transport
|
||||
ref: "4bf54339a1caefd4c53a4a8873a9686ec13a9107"
|
||||
resolved-ref: "4bf54339a1caefd4c53a4a8873a9686ec13a9107"
|
||||
url: "https://github.com/cake-tech/trezor-flutter.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -982,10 +1016,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_ble
|
||||
sha256: "1fad089150a29db82b3b7d60327e18c5ad6b3a5bb509defc1c690b0a76b9c098"
|
||||
sha256: "8325ca9f5cce2fba2b4efef74a4fcfe7144f303b409d6fc6ed16197b00a1a241"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.0"
|
||||
version: "1.2.0"
|
||||
universal_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -25,11 +25,16 @@ dependencies:
|
||||
path: ../cw_core
|
||||
monero:
|
||||
git:
|
||||
url: https://github.com/mrcyjanek/monero_c
|
||||
ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2
|
||||
url: https://github.com/mrcyjanek/monero_c.git
|
||||
ref: 3bfb3856a838f2bf6b729501837bb0295dedf25d
|
||||
path: impls/monero.dart
|
||||
mutex: ^3.1.0
|
||||
ledger_flutter_plus: ^1.4.1
|
||||
trezor_flutter:
|
||||
git:
|
||||
url: https://github.com/cake-tech/trezor-flutter.git
|
||||
ref: 4bf54339a1caefd4c53a4a8873a9686ec13a9107
|
||||
path: trezor-flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -502,9 +502,9 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "impls/monero.dart"
|
||||
ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2
|
||||
resolved-ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2
|
||||
url: "https://github.com/mrcyjanek/monero_c"
|
||||
ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d"
|
||||
resolved-ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d"
|
||||
url: "http://github.com/mrcyjanek/monero_c.git"
|
||||
source: git
|
||||
version: "0.0.0"
|
||||
mutex:
|
||||
|
||||
@@ -23,8 +23,8 @@ dependencies:
|
||||
path: ../cw_core
|
||||
monero:
|
||||
git:
|
||||
url: https://github.com/mrcyjanek/monero_c
|
||||
ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 # monero_c hash
|
||||
url: https://github.com/mrcyjanek/monero_c.git
|
||||
ref: 3bfb3856a838f2bf6b729501837bb0295dedf25d
|
||||
path: impls/monero.dart
|
||||
mutex: ^3.1.0
|
||||
|
||||
|
||||
@@ -507,9 +507,9 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "impls/monero.dart"
|
||||
ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2
|
||||
resolved-ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2
|
||||
url: "https://github.com/mrcyjanek/monero_c"
|
||||
ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d"
|
||||
resolved-ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d"
|
||||
url: "http://github.com/mrcyjanek/monero_c.git"
|
||||
source: git
|
||||
version: "0.0.0"
|
||||
nested:
|
||||
|
||||
@@ -24,8 +24,8 @@ dependencies:
|
||||
fluttertoast: ^8.2.12
|
||||
monero:
|
||||
git:
|
||||
url: https://github.com/mrcyjanek/monero_c
|
||||
ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 # monero_c hash
|
||||
url: https://github.com/mrcyjanek/monero_c.git
|
||||
ref: 3bfb3856a838f2bf6b729501837bb0295dedf25d
|
||||
path: impls/monero.dart
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -231,12 +231,9 @@ class CommonTestFlows {
|
||||
|
||||
await _newWalletTypePageRobot.findParticularWalletTypeInScrollableList(type);
|
||||
|
||||
// Tapping the wallet type navigates directly (no separate Next button).
|
||||
_tester.printToConsole('Tapping wallet type: ${type.name}');
|
||||
await _newWalletTypePageRobot.selectWalletType(type);
|
||||
await _tester.pumpAndSettle(Duration(milliseconds: 500));
|
||||
|
||||
_tester.printToConsole('Pressing next button');
|
||||
await _newWalletTypePageRobot.onNextButtonPressed();
|
||||
|
||||
await _tester.pumpAndSettle(Duration(milliseconds: 2000));
|
||||
|
||||
|
||||
@@ -77,11 +77,9 @@ class NewWalletTypePageRobot {
|
||||
tester.printToConsole('Wallet type ${type.name} is now visible and tappable');
|
||||
}
|
||||
|
||||
// Tapping a wallet type now navigates directly (the page no longer has a
|
||||
// separate "Next" button — selection happens on tap).
|
||||
Future<void> selectWalletType(WalletType type) async {
|
||||
await commonTestCases.tapItemByKey('new_wallet_type_${type.name}_button_key');
|
||||
}
|
||||
|
||||
Future<void> onNextButtonPressed() async {
|
||||
await commonTestCases.tapItemByKey('new_wallet_type_next_button_key');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +295,7 @@ SPEC CHECKSUMS:
|
||||
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
|
||||
warp_api: 997d8fb3e777669bc6260d0c12e56cd40b9db1d1
|
||||
YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966
|
||||
|
||||
PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a
|
||||
|
||||
@@ -336,7 +336,7 @@ class DFXBuyProvider extends BuyProvider {
|
||||
required String cryptoCurrencyAddress,
|
||||
String? countryCode}) async {
|
||||
if (wallet.isHardwareWallet) {
|
||||
if (!hardwareWalletVM!.isConnected) {
|
||||
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: wallet.walletInfo.type,
|
||||
|
||||
@@ -173,7 +173,7 @@ class RobinhoodBuyProvider extends BuyProvider {
|
||||
required String cryptoCurrencyAddress,
|
||||
String? countryCode}) async {
|
||||
if (wallet.isHardwareWallet) {
|
||||
if (!hardwareWalletVM!.isConnected) {
|
||||
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: wallet.walletInfo.type,
|
||||
|
||||
+6
-16
@@ -240,7 +240,7 @@ import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/bitbox_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/trezor_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/link_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
|
||||
@@ -414,7 +414,7 @@ Future<void> setup({
|
||||
switch(type) {
|
||||
case HardwareWalletType.bitbox: return getIt<BitboxViewModel>();
|
||||
case HardwareWalletType.ledger: return getIt<LedgerViewModel>();
|
||||
case HardwareWalletType.trezor: return getIt<TrezorViewModel>();
|
||||
case HardwareWalletType.trezor: return getIt<TrezorConnectViewModel>();
|
||||
case HardwareWalletType.cupcake:
|
||||
case HardwareWalletType.coldcard:
|
||||
case HardwareWalletType.seedsigner:
|
||||
@@ -429,7 +429,7 @@ Future<void> setup({
|
||||
|
||||
getIt.registerLazySingleton(() => TrezorConnect("cakewallet://trezor_connect",
|
||||
appName: "Cake Wallet"));
|
||||
getIt.registerLazySingleton(() => TrezorViewModel(getIt<TrezorConnect>()));
|
||||
getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt<TrezorConnect>()));
|
||||
|
||||
|
||||
getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
|
||||
@@ -1118,9 +1118,9 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));
|
||||
|
||||
getIt.registerLazySingleton(() {
|
||||
getIt.registerFactoryParam<NodeListViewModel, bool, void>((isPow, _) {
|
||||
final appStore = getIt.get<AppStore>();
|
||||
return NodeListViewModel(appStore);
|
||||
return NodeListViewModel(appStore, isPow);
|
||||
});
|
||||
|
||||
getIt.registerFactoryParam<NewAddressesPage, bool, void>(
|
||||
@@ -1131,11 +1131,6 @@ Future<void> setup({
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerLazySingleton(() {
|
||||
final appStore = getIt.get<AppStore>();
|
||||
return PowNodeListViewModel(appStore);
|
||||
});
|
||||
|
||||
getIt.registerFactory(() => ConnectionSyncViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
|
||||
|
||||
getIt.registerFactory(() => ConnectionSyncPage(getIt.get<ConnectionSyncViewModel>()));
|
||||
@@ -1178,8 +1173,6 @@ Future<void> setup({
|
||||
final Node? editingNode = args['editingNode'] as Node?;
|
||||
return NodeCreateOrEditViewModel(
|
||||
isPow,
|
||||
getIt.get<NodeListViewModel>(),
|
||||
getIt.get<PowNodeListViewModel>(),
|
||||
type,
|
||||
getIt.get<SettingsStore>(),
|
||||
editingNode: editingNode
|
||||
@@ -1700,10 +1693,7 @@ Future<void> setup({
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<ManageNodesPage, bool, void>((bool isPow, _) {
|
||||
if (isPow) {
|
||||
return ManageNodesPage(isPow, powNodeListViewModel: getIt.get<PowNodeListViewModel>(), dashboardViewModel: getIt.get<DashboardViewModel>());
|
||||
}
|
||||
return ManageNodesPage(isPow, nodeListViewModel: getIt.get<NodeListViewModel>(), dashboardViewModel: getIt.get<DashboardViewModel>());
|
||||
return ManageNodesPage(isPow, nodeListViewModel: getIt.get<NodeListViewModel>(param1: isPow), dashboardViewModel: getIt.get<DashboardViewModel>());
|
||||
});
|
||||
|
||||
getIt.registerFactory(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:bitbox_flutter/usb/bitbox_device.dart' as bitbox;
|
||||
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
|
||||
import 'package:trezor_flutter/trezor_flutter.dart' as trezor;
|
||||
|
||||
abstract class HardwareWalletDevice {
|
||||
String get name;
|
||||
@@ -41,6 +42,21 @@ class BitboxHardwareWalletDevice extends HardwareWalletDevice {
|
||||
HardwareWalletConnectionType get connectionType => HardwareWalletConnectionType.usb;
|
||||
}
|
||||
|
||||
class TrezorHardwareWalletDevice extends HardwareWalletDevice {
|
||||
final trezor.TrezorDevice device;
|
||||
|
||||
TrezorHardwareWalletDevice(this.device);
|
||||
|
||||
@override
|
||||
String get name => device.name;
|
||||
|
||||
@override
|
||||
HardwareWalletDeviceType get type => device.deviceInfo.toGeneric();
|
||||
|
||||
@override
|
||||
HardwareWalletConnectionType get connectionType => device.connectionType.toGeneric();
|
||||
}
|
||||
|
||||
enum HardwareWalletDeviceType {
|
||||
ledgerBlue,
|
||||
ledgerNanoS,
|
||||
@@ -50,7 +66,12 @@ enum HardwareWalletDeviceType {
|
||||
ledgerStax,
|
||||
ledgerFlex,
|
||||
BitBox02,
|
||||
BitBox02Nova;
|
||||
BitBox02Nova,
|
||||
trezorModelOne,
|
||||
trezorModelT,
|
||||
trezorSafe3,
|
||||
trezorSafe5,
|
||||
trezorSafe7;
|
||||
}
|
||||
|
||||
enum HardwareWalletConnectionType {
|
||||
@@ -60,7 +81,7 @@ enum HardwareWalletConnectionType {
|
||||
qr;
|
||||
}
|
||||
|
||||
extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType {
|
||||
extension LedgerToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType {
|
||||
HardwareWalletDeviceType toGeneric() {
|
||||
switch (this) {
|
||||
case ledger.LedgerDeviceType.blue:
|
||||
@@ -81,7 +102,24 @@ extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType {
|
||||
}
|
||||
}
|
||||
|
||||
extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType {
|
||||
extension TrezorToGenericHardwareWalletDeviceType on trezor.TrezorDeviceType {
|
||||
HardwareWalletDeviceType toGeneric() {
|
||||
switch (this) {
|
||||
case trezor.TrezorDeviceType.modelOne:
|
||||
return HardwareWalletDeviceType.trezorModelOne;
|
||||
case trezor.TrezorDeviceType.modelT:
|
||||
return HardwareWalletDeviceType.trezorModelT;
|
||||
case trezor.TrezorDeviceType.safe3:
|
||||
return HardwareWalletDeviceType.trezorSafe3;
|
||||
case trezor.TrezorDeviceType.safe5:
|
||||
return HardwareWalletDeviceType.trezorSafe5;
|
||||
case trezor.TrezorDeviceType.safe7:
|
||||
return HardwareWalletDeviceType.trezorSafe7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LedgerToGenericHardwareWalletConnectionType on ledger.ConnectionType {
|
||||
HardwareWalletConnectionType toGeneric() {
|
||||
switch (this) {
|
||||
case ledger.ConnectionType.usb:
|
||||
@@ -91,3 +129,14 @@ extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TrezorToGenericHardwareWalletConnectionType on trezor.ConnectionType {
|
||||
HardwareWalletConnectionType toGeneric() {
|
||||
switch (this) {
|
||||
case trezor.ConnectionType.usb:
|
||||
return HardwareWalletConnectionType.usb;
|
||||
case trezor.ConnectionType.ble:
|
||||
return HardwareWalletConnectionType.ble;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ class ListItemRegularRow extends ListItem {
|
||||
this.truncateTrailingText = false,
|
||||
this.foregroundColor,
|
||||
this.trailingIconSize,
|
||||
this.copyableText
|
||||
this.copyableText,
|
||||
this.leadingIconErrorWidget,
|
||||
this.leadingIconSize,
|
||||
});
|
||||
|
||||
final String? subtitle;
|
||||
@@ -31,4 +33,6 @@ class ListItemRegularRow extends ListItem {
|
||||
final bool truncateTrailingText;
|
||||
final Color? foregroundColor;
|
||||
final double? trailingIconSize;
|
||||
final Widget? leadingIconErrorWidget;
|
||||
final double? leadingIconSize;
|
||||
}
|
||||
|
||||
@@ -216,6 +216,14 @@ class AddressResolver {
|
||||
return emailRegex.hasMatch(address);
|
||||
}
|
||||
|
||||
String _stripHtmlTags(String value) {
|
||||
return value
|
||||
.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n')
|
||||
.replaceAll(RegExp(r'</p\s*>', caseSensitive: false), '\n')
|
||||
.replaceAll(RegExp(r'<[^>]*>'), '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
Future<ParsedAddress> resolve(BuildContext context, String text, CryptoCurrency currency) async {
|
||||
final ticker = currency.title;
|
||||
try {
|
||||
@@ -348,7 +356,8 @@ class AddressResolver {
|
||||
await MastodonAPI.lookupUserByUserName(userName: userName, apiHost: hostName);
|
||||
|
||||
if (mastodonUser != null) {
|
||||
String? addressFromBio = extractAddressByType(raw: mastodonUser.note, type: currency, requireSurroundingWhitespaces: true);
|
||||
final mastodonNote = _stripHtmlTags(mastodonUser.note);
|
||||
String? addressFromBio = extractAddressByType(raw: mastodonNote, type: currency, requireSurroundingWhitespaces: true);
|
||||
|
||||
if (addressFromBio != null && addressFromBio.isNotEmpty) {
|
||||
return ParsedAddress.fetchMastodonAddress(
|
||||
@@ -361,7 +370,7 @@ class AddressResolver {
|
||||
await MastodonAPI.getPinnedPosts(userId: mastodonUser.id, apiHost: hostName);
|
||||
|
||||
if (pinnedPosts.isNotEmpty) {
|
||||
final userPinnedPostsText = pinnedPosts.map((item) => item.content).join('\n');
|
||||
final userPinnedPostsText = pinnedPosts.map((item) => _stripHtmlTags(item.content)).join('\n');
|
||||
String? addressFromPinnedPost =
|
||||
extractAddressByType(raw: userPinnedPostsText, type: currency,
|
||||
requireSurroundingWhitespaces: true);
|
||||
|
||||
+33
-22
@@ -236,13 +236,14 @@ class CWMonero extends Monero {
|
||||
required String name,
|
||||
required String password,
|
||||
required int height,
|
||||
required ledger.LedgerConnection ledgerConnection,
|
||||
required HardwareWalletService hardwareWalletService,
|
||||
}) =>
|
||||
MoneroRestoreWalletFromHardwareCredentials(
|
||||
name: name,
|
||||
password: password,
|
||||
height: height,
|
||||
ledgerConnection: ledgerConnection);
|
||||
name: name,
|
||||
password: password,
|
||||
height: height,
|
||||
hardwareWalletService: hardwareWalletService,
|
||||
);
|
||||
|
||||
@override
|
||||
WalletCredentials createMoneroRestoreWalletFromSeedCredentials(
|
||||
@@ -416,9 +417,7 @@ class CWMonero extends Monero {
|
||||
}
|
||||
|
||||
@override
|
||||
void monerocCheck() {
|
||||
checkIfMoneroCIsFine();
|
||||
}
|
||||
void monerocCheck() => checkIfMoneroCIsFine();
|
||||
|
||||
@override
|
||||
Future<void> setLedgerConnection(Object wallet, ledger.LedgerConnection connection) async {
|
||||
@@ -427,29 +426,41 @@ class CWMonero extends Monero {
|
||||
}
|
||||
|
||||
@override
|
||||
void resetLedgerConnection() {
|
||||
disableLedgerExchange();
|
||||
}
|
||||
void resetLedgerConnection() => disableLedgerExchange();
|
||||
|
||||
@override
|
||||
void setGlobalLedgerConnection(ledger.LedgerConnection connection) {
|
||||
gLedger = connection;
|
||||
}
|
||||
void setGlobalLedgerConnection(ledger.LedgerConnection connection) => gLedger = connection;
|
||||
|
||||
@override
|
||||
String? getLastLedgerCommand() => latestLedgerCommand;
|
||||
|
||||
bool isViewOnly() {
|
||||
return isViewOnlyBySpendKey(null);
|
||||
@override
|
||||
void setHardwareWalletService(Object wallet, HardwareWalletService service) {
|
||||
final moneroWallet = wallet as MoneroWallet;
|
||||
moneroWallet.trezorService = service as MoneroTrezorService;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, List<int>> debugCallLength() {
|
||||
return monero_wallet_api.debugCallLength();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> getWalletCacheDebug() {
|
||||
return monero_wallet_api.getWalletCacheDebug();
|
||||
HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) =>
|
||||
MoneroLedgerService(connection);
|
||||
|
||||
@override
|
||||
HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorClient client) =>
|
||||
MoneroTrezorService(client);
|
||||
|
||||
@override
|
||||
Future<void> syncTrezor(Object wallet) async {
|
||||
final moneroWallet = wallet as MoneroWallet;
|
||||
|
||||
return moneroWallet.syncTrezor();
|
||||
}
|
||||
|
||||
bool isViewOnly() => isViewOnlyBySpendKey(null);
|
||||
|
||||
@override
|
||||
Map<String, List<int>> debugCallLength() => monero_wallet_api.debugCallLength();
|
||||
|
||||
@override
|
||||
Map<String, dynamic> getWalletCacheDebug() => monero_wallet_api.getWalletCacheDebug();
|
||||
}
|
||||
|
||||
@@ -726,12 +726,13 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
}
|
||||
|
||||
if (widget.sendViewModel.wallet.isHardwareWallet) {
|
||||
if (!widget.sendViewModel.hardwareWalletViewModel!.isConnected) {
|
||||
if (!widget.sendViewModel.hardwareWalletViewModel!
|
||||
.isConnected(widget.sendViewModel.walletType)) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: widget.sendViewModel.walletType,
|
||||
hardwareWalletType: widget.sendViewModel.wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (BuildContext context, _) {
|
||||
onConnectDevice: (_, __) {
|
||||
widget.sendViewModel.hardwareWalletViewModel!
|
||||
.initWallet(widget.sendViewModel.wallet);
|
||||
Navigator.of(context).pop();
|
||||
@@ -748,8 +749,12 @@ class _NewSendPageState extends State<NewSendPage> {
|
||||
amount += item.formattedCryptoAmount;
|
||||
}
|
||||
if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
|
||||
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
|
||||
arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet));
|
||||
if (widget.sendViewModel.wallet.hardwareWalletType == HardwareWalletType.trezor) {
|
||||
await monero!.syncTrezor(widget.sendViewModel.wallet);
|
||||
} else {
|
||||
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
|
||||
arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet));
|
||||
}
|
||||
await Future.delayed(Duration(seconds: 1)); // wait for monero to refresh the state
|
||||
}
|
||||
if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) {
|
||||
|
||||
@@ -17,10 +17,10 @@ import 'package:cake_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/lnurl.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
import '../../../pages/receive_page.dart';
|
||||
@@ -198,7 +198,12 @@ class CoinActionRow extends StatelessWidget {
|
||||
Navigator.of(context)
|
||||
.pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
|
||||
return;
|
||||
} else {
|
||||
} else if(["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
|
||||
Navigator.of(context).pushNamed(Routes.newNode,
|
||||
arguments: {"editingNode": Node.fromUri(Uri.parse(code), walletType)});
|
||||
return;
|
||||
|
||||
}else {
|
||||
final uri = Uri.tryParse(code);
|
||||
if (uri == null) return;
|
||||
req = PaymentRequest.fromUri(uri);
|
||||
|
||||
@@ -86,11 +86,11 @@ class WalletInfoBar extends StatelessWidget {
|
||||
case null:
|
||||
return null;
|
||||
case HardwareWalletType.bitbox:
|
||||
return "assets/images/hardware_wallet/device_bitbox.svg";
|
||||
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
|
||||
case HardwareWalletType.ledger:
|
||||
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
case HardwareWalletType.trezor:
|
||||
return "assets/images/hardware_wallet/device_trezor_safe_5.svg";
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg";
|
||||
case HardwareWalletType.cupcake:
|
||||
return "assets/images/cupcake.svg";
|
||||
case HardwareWalletType.coldcard:
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class DigitInputController {
|
||||
String _text = "";
|
||||
|
||||
String get text => _text;
|
||||
|
||||
set text(String newText) {
|
||||
_text = newText;
|
||||
for (final listener in _listeners) {
|
||||
listener.call();
|
||||
}
|
||||
}
|
||||
|
||||
final List<void Function()> _listeners = [];
|
||||
|
||||
void addListener(void Function() listener) => _listeners.add(listener);
|
||||
}
|
||||
|
||||
class DigitInputPill extends StatelessWidget {
|
||||
const DigitInputPill({super.key, this.digit, required this.highlighted});
|
||||
|
||||
final String? digit;
|
||||
final bool highlighted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 51,
|
||||
height: 84,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border:
|
||||
highlighted ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2) : null,
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(digit ?? " ", style: TextStyle(fontSize: 40)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DigitInput extends StatefulWidget {
|
||||
const DigitInput({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.desiredLength,
|
||||
this.breakAt = 3,
|
||||
});
|
||||
|
||||
final DigitInputController controller;
|
||||
final int desiredLength;
|
||||
final int breakAt;
|
||||
|
||||
@override
|
||||
State<DigitInput> createState() => _DigitInputState();
|
||||
}
|
||||
|
||||
class _DigitInputState extends State<DigitInput> implements TextInputClient {
|
||||
TextInputConnection? _textInputConnection;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_openVirtualKeyboard();
|
||||
}
|
||||
|
||||
void _openVirtualKeyboard() {
|
||||
if (_textInputConnection == null || !_textInputConnection!.attached) {
|
||||
_textInputConnection = TextInput.attach(
|
||||
this,
|
||||
const TextInputConfiguration(
|
||||
inputType: TextInputType.numberWithOptions(
|
||||
signed: false,
|
||||
decimal: false,
|
||||
),
|
||||
inputAction: TextInputAction.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_textInputConnection!.setEditingState(
|
||||
TextEditingValue(
|
||||
text: widget.controller.text,
|
||||
selection: TextSelection.collapsed(offset: widget.controller.text.length),
|
||||
),
|
||||
);
|
||||
_textInputConnection!.show();
|
||||
}
|
||||
|
||||
void _closeVirtualKeyboard() => _textInputConnection?.close();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_closeVirtualKeyboard();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _openVirtualKeyboard,
|
||||
child: SizedBox(
|
||||
height: 84,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => DigitInputPill(
|
||||
digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
|
||||
highlighted: widget.controller.text.length == index,
|
||||
),
|
||||
separatorBuilder: (context, index) =>
|
||||
SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
|
||||
itemCount: widget.desiredLength,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
final RegExp numberRegExp = RegExp(r'^\d{0,6}$');
|
||||
|
||||
if (numberRegExp.hasMatch(value.text)) {
|
||||
setState(() => widget.controller.text = value.text);
|
||||
} else {
|
||||
// flutter keeps the text editing state regardless of whether you actually used it or not.
|
||||
// so, if validation fails, we gotta set it back to what it was explicitly
|
||||
_textInputConnection?.setEditingState(
|
||||
TextEditingValue(
|
||||
text: widget.controller.text,
|
||||
selection: TextSelection.collapsed(offset: widget.controller.text.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performAction(TextInputAction action) {}
|
||||
|
||||
@override
|
||||
TextEditingValue? get currentTextEditingValue => TextEditingValue(text: widget.controller.text);
|
||||
|
||||
@override
|
||||
void connectionClosed() => _textInputConnection = null;
|
||||
|
||||
@override
|
||||
AutofillScope? get currentAutofillScope => null;
|
||||
|
||||
@override
|
||||
void performPrivateCommand(String action, Map<String, dynamic> data) {}
|
||||
|
||||
@override
|
||||
void showAutocorrectionPromptRect(int start, int end) {}
|
||||
|
||||
@override
|
||||
void updateFloatingCursor(RawFloatingCursorPoint point) {}
|
||||
|
||||
@override
|
||||
void insertTextPlaceholder(Size size) {}
|
||||
|
||||
@override
|
||||
void removeTextPlaceholder() {}
|
||||
|
||||
@override
|
||||
void showToolbar() {}
|
||||
|
||||
@override
|
||||
void performSelector(String selectorName) {}
|
||||
|
||||
@override
|
||||
void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {}
|
||||
|
||||
@override
|
||||
void insertContent(KeyboardInsertedContent content) {}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HardwareWalletProceedOnDeviceMessage extends StatelessWidget {
|
||||
const HardwareWalletProceedOnDeviceMessage({super.key, required this.hardwareWalletType});
|
||||
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(spacing: 8, children: [
|
||||
if (hardwareWalletIcon != null)
|
||||
CakeImageWidget(
|
||||
imageUrl: hardwareWalletIcon!,
|
||||
width: 36,
|
||||
height: 36,
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
S.of(context).proceed_on_device,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? get hardwareWalletIcon {
|
||||
switch (hardwareWalletType) {
|
||||
case HardwareWalletType.bitbox:
|
||||
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
|
||||
case HardwareWalletType.ledger:
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
case HardwareWalletType.trezor:
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
|
||||
case HardwareWalletType.cupcake:
|
||||
return "assets/images/cupcake.svg";
|
||||
case HardwareWalletType.coldcard:
|
||||
case HardwareWalletType.seedsigner:
|
||||
case HardwareWalletType.keystone:
|
||||
return "assets/images/hardware_wallet/device_qr.svg";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/digit_input.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class HardwareWalletProceedOnDeviceSheet extends StatefulWidget {
|
||||
const HardwareWalletProceedOnDeviceSheet({
|
||||
super.key,
|
||||
required this.hardwareWalletType,
|
||||
required this.trezorConnectVM,
|
||||
required this.onRetry,
|
||||
});
|
||||
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
final TrezorConnectViewModelBase trezorConnectVM;
|
||||
final void Function() onRetry;
|
||||
|
||||
@override
|
||||
State<HardwareWalletProceedOnDeviceSheet> createState() =>
|
||||
_HardwareWalletProceedOnDeviceSheetState();
|
||||
}
|
||||
|
||||
class _HardwareWalletProceedOnDeviceSheetState extends State<HardwareWalletProceedOnDeviceSheet> {
|
||||
TrezorParingState _paringState = TrezorParingState.initial;
|
||||
|
||||
final DigitInputController _controller = DigitInputController();
|
||||
|
||||
static const pinOpenDuration = Duration(milliseconds: 300);
|
||||
|
||||
// you should probably check if there can be other pin lengths
|
||||
static const pinLength = 6;
|
||||
|
||||
ReactionDisposer? paringStateReaction;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(() => setState(() {}));
|
||||
|
||||
_paringState = widget.trezorConnectVM.paringState;
|
||||
paringStateReaction = reaction((_) => widget.trezorConnectVM.paringState, (paringState) {
|
||||
if (paringState is SuccessTrezorParingState) {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
setState(() => _paringState = paringState);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
paringStateReaction?.reaction.dispose();
|
||||
}
|
||||
|
||||
bool get _isAwaitingPin => _paringState == TrezorParingState.enterPin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "",
|
||||
leadingWidget: AnimatedSwitcher(
|
||||
layoutBuilder: (currentChild, previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: <Widget>[
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
duration: pinOpenDuration,
|
||||
child: Text(
|
||||
key: ValueKey(pageTitle),
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
pageTitle,
|
||||
),
|
||||
),
|
||||
trailingIcon: Icon(
|
||||
Icons.close,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTrailingPressed: () => showPopUp(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).are_you_sure_exit,
|
||||
alertContent: S.of(context).hww_exit_desc,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
rightButtonText: S.of(context).yes_exit,
|
||||
rightAlertButtonStyle: AlertButtonStyle.error(context),
|
||||
actionLeftButton: Navigator.of(context).pop,
|
||||
actionRightButton: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: DirectionalAnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 400),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 24, right: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(),
|
||||
AnimatedOpacity(
|
||||
curve: Curves.easeOutQuad,
|
||||
opacity: hasFullPin ? 1 : 0,
|
||||
duration: pinOpenDuration,
|
||||
child: ModernButton(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
iconColor: Theme.of(context).colorScheme.onPrimary,
|
||||
size: 36,
|
||||
icon: Icon(Icons.arrow_forward),
|
||||
onPressed: () =>
|
||||
widget.trezorConnectVM.setParingPin(_controller.text.trim()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get content {
|
||||
if (_paringState is InitialTrezorParingState || _paringState is EnterPinTrezorParingState) {
|
||||
return _enterPinCode();
|
||||
}
|
||||
|
||||
if (_paringState is VerifyingPinTrezorParingState) return _verifyingCode();
|
||||
|
||||
if (_paringState is FailTrezorParingState)
|
||||
return _errorBox((_paringState as FailTrezorParingState).message);
|
||||
|
||||
return SizedBox.shrink(key: ValueKey(3));
|
||||
}
|
||||
|
||||
void retry() {
|
||||
_controller.text = "";
|
||||
widget.onRetry();
|
||||
}
|
||||
|
||||
String get pageTitle {
|
||||
if (_paringState is InitialTrezorParingState) return S.of(context).device_confirmation;
|
||||
if (_isAwaitingPin) return S.of(context).pairing_code;
|
||||
return "";
|
||||
}
|
||||
|
||||
bool get hasFullPin => _controller.text.length == pinLength && _isAwaitingPin;
|
||||
|
||||
String? get hardwareWalletIcon {
|
||||
switch (widget.hardwareWalletType) {
|
||||
case HardwareWalletType.bitbox:
|
||||
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
|
||||
case HardwareWalletType.ledger:
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
case HardwareWalletType.trezor:
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
|
||||
case HardwareWalletType.cupcake:
|
||||
return "assets/images/cupcake.svg";
|
||||
case HardwareWalletType.coldcard:
|
||||
case HardwareWalletType.seedsigner:
|
||||
case HardwareWalletType.keystone:
|
||||
return "assets/images/hardware_wallet/device_qr.svg";
|
||||
}
|
||||
}
|
||||
|
||||
Widget _verifyingCode() => Container(
|
||||
key: ValueKey(1),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CupertinoActivityIndicator(radius: 36),
|
||||
const SizedBox(),
|
||||
Text(
|
||||
"${S.of(context).verifying_code}...",
|
||||
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
|
||||
),
|
||||
Text(
|
||||
S.of(context).this_can_take_few_seconds,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _errorBox(String errorText) => Container(
|
||||
key: ValueKey(2),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
Text(
|
||||
S.of(context).pairing_error,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
errorText,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Spacer(),
|
||||
NewPrimaryButton(
|
||||
onPressed: retry,
|
||||
text: S.of(context).try_again,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _enterPinCode() => Column(
|
||||
key: ValueKey(0),
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: pinOpenDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
width: _isAwaitingPin ? 40 : 100,
|
||||
height: _isAwaitingPin ? 40 : 100,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: hardwareWalletIcon,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: pinOpenDuration,
|
||||
switchInCurve: Curves.easeInCubic,
|
||||
switchOutCurve: Curves.easeOutCubic,
|
||||
child: _isAwaitingPin
|
||||
? Column(
|
||||
spacing: 24,
|
||||
key: ValueKey(1),
|
||||
children: [
|
||||
Text(
|
||||
textAlign: TextAlign.center,
|
||||
"${S.of(context).pairing_code_desc_1}\n${S.of(context).pairing_code_desc_2}",
|
||||
),
|
||||
DigitInput(
|
||||
controller: _controller,
|
||||
desiredLength: pinLength,
|
||||
)
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
key: ValueKey(0),
|
||||
spacing: 12,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).proceed_on_device,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
S.of(context).proceed_on_device_description,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HardwareWalletTrezorParingSheet extends StatefulWidget {
|
||||
const HardwareWalletTrezorParingSheet({super.key});
|
||||
|
||||
@override
|
||||
State<HardwareWalletTrezorParingSheet> createState() => _HardwareWalletTrezorParingSheetState();
|
||||
}
|
||||
|
||||
class _HardwareWalletTrezorParingSheetState extends State<HardwareWalletTrezorParingSheet> {
|
||||
final TextEditingController _paringCodeController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(18)),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "Paring code",
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
onTrailingPressed: () {},
|
||||
leadingIcon: Icon(Icons.close),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 12,
|
||||
children: [
|
||||
SizedBox(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 75,
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(16),
|
||||
),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
textAlign: TextAlign.left,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
controller: _paringCodeController,
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(signed: false, decimal: false),
|
||||
decoration: InputDecoration(
|
||||
hint: Text(
|
||||
"000 000",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(),
|
||||
NewPrimaryButton(
|
||||
text: S.of(context).continue_text,
|
||||
onPressed: () => Navigator.of(context).pop(_paringCodeController.text),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,12 @@ import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class ModalHeader extends StatelessWidget {
|
||||
const ModalHeader(
|
||||
{super.key, required this.iconPath, required this.message, required this.title});
|
||||
{super.key, required this.iconPath, required this.message, required this.title, this.bottomWidget});
|
||||
|
||||
final String iconPath;
|
||||
final String title;
|
||||
final String message;
|
||||
final Widget? bottomWidget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -41,7 +42,11 @@ class ModalHeader extends StatelessWidget {
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
))
|
||||
)),
|
||||
if(bottomWidget != null) ...[
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 18), child: Container(height:1, color: Theme.of(context).colorScheme.surfaceContainerHigh),),
|
||||
bottomWidget!
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/confirm_swiper.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_message.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:vector_graphics/vector_graphics.dart';
|
||||
|
||||
class SendConfirmBottomWidget extends StatelessWidget {
|
||||
const SendConfirmBottomWidget({super.key, required this.sendViewModel});
|
||||
@@ -61,7 +59,7 @@ class SendConfirmBottomWidget extends StatelessWidget {
|
||||
text: "${S.of(context).signing_transaction}...",
|
||||
);
|
||||
case IsAwaitingDeviceResponseState:
|
||||
return HardwareWalletConfirmationMessage(
|
||||
return HardwareWalletProceedOnDeviceMessage(
|
||||
hardwareWalletType: sendViewModel.wallet.hardwareWalletType!);
|
||||
case TransactionCommitting:
|
||||
return LoadingBottomWidget(
|
||||
@@ -173,58 +171,3 @@ class TransactionErrorActions extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HardwareWalletConfirmationMessage extends StatelessWidget {
|
||||
const HardwareWalletConfirmationMessage({super.key, required this.hardwareWalletType});
|
||||
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(spacing: 8, children: [
|
||||
if (hardwareWalletIcon != null)
|
||||
CakeImageWidget(imageUrl:
|
||||
hardwareWalletIcon!,
|
||||
width: 36,
|
||||
height: 36,
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
S.of(context).proceed_on_device,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
)
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? get hardwareWalletIcon {
|
||||
switch (hardwareWalletType) {
|
||||
case HardwareWalletType.bitbox:
|
||||
return "assets/images/hardware_wallet/device_bitbox.svg";
|
||||
case HardwareWalletType.ledger:
|
||||
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
|
||||
case HardwareWalletType.trezor:
|
||||
return "assets/images/hardware_wallet/device_trezor_safe_5.svg";
|
||||
case HardwareWalletType.cupcake:
|
||||
return "assets/images/cupcake.svg";
|
||||
case HardwareWalletType.coldcard:
|
||||
case HardwareWalletType.seedsigner:
|
||||
case HardwareWalletType.keystone:
|
||||
return "assets/images/hardware_wallet/device_qr.svg";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class _SwapConfirmSheetState extends State<SwapConfirmSheet> {
|
||||
final sendVM = widget.exchangeTradeViewModel.sendViewModel;
|
||||
|
||||
if (sendVM.wallet.isHardwareWallet) {
|
||||
if (!sendVM.hardwareWalletViewModel!.isConnected) {
|
||||
if (!sendVM.hardwareWalletViewModel!.isConnected(sendVM.walletType)) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: sendVM.walletType,
|
||||
|
||||
+1
-1
@@ -371,7 +371,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||
(_) => getIt.get<NewWalletTypePage>(
|
||||
param1: NewWalletTypeArguments(
|
||||
onTypeSelected: (BuildContext context, WalletType type) {
|
||||
if (hardwareWalletType == HardwareWalletType.trezor) {
|
||||
if (hardwareWalletType == HardwareWalletType.trezor && type != WalletType.monero) {
|
||||
Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount,
|
||||
arguments: [type, hardwareWalletType]);
|
||||
return;
|
||||
|
||||
@@ -3,10 +3,13 @@ import 'dart:io';
|
||||
|
||||
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/main.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
@@ -91,6 +94,8 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
var bleDevices = <HardwareWalletDevice>[];
|
||||
var usbDevices = <HardwareWalletDevice>[];
|
||||
|
||||
List<HardwareWalletDevice> get allDevices => [...bleDevices, ...usbDevices];
|
||||
|
||||
late Timer? _usbRefreshTimer = null;
|
||||
late Timer? _bleRefreshTimer = null;
|
||||
late Timer? _bleStateTimer = null;
|
||||
@@ -158,24 +163,38 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
|
||||
Future<void> _connectToDevice(HardwareWalletDevice device) async {
|
||||
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
|
||||
if (isConnected) widget.onConnectDevice(context, widget.hardwareWalletVM);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isConnected) widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM);
|
||||
});
|
||||
}
|
||||
|
||||
String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) {
|
||||
switch (deviceType) {
|
||||
case HardwareWalletDeviceType.ledgerNanoX:
|
||||
return 'assets/images/hardware_wallet/ledger_nano_x.png';
|
||||
return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg';
|
||||
case HardwareWalletDeviceType.ledgerNanoGen5:
|
||||
return 'assets/images/hardware_wallet/device_ledger_nano_gen_5.svg';
|
||||
return 'assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg';
|
||||
case HardwareWalletDeviceType.ledgerStax:
|
||||
return 'assets/images/hardware_wallet/ledger_stax.png';
|
||||
return 'assets/new-ui/hardware_wallets/ledger_stax.svg';
|
||||
case HardwareWalletDeviceType.ledgerFlex:
|
||||
return 'assets/images/hardware_wallet/ledger_flex.png';
|
||||
return 'assets/new-ui/hardware_wallets/ledger_flex.svg';
|
||||
case HardwareWalletDeviceType.BitBox02:
|
||||
return 'assets/images/hardware_wallet/device_bitbox.svg';
|
||||
case HardwareWalletDeviceType.BitBox02Nova:
|
||||
return 'assets/new-ui/hardware_wallets/device_bitbox.svg';
|
||||
case HardwareWalletDeviceType.trezorModelOne:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_model_one.svg';
|
||||
case HardwareWalletDeviceType.trezorModelT:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_model_t.svg';
|
||||
case HardwareWalletDeviceType.trezorSafe3:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_safe_3.svg';
|
||||
case HardwareWalletDeviceType.trezorSafe5:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_safe_5.svg';
|
||||
case HardwareWalletDeviceType.trezorSafe7:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_safe_7.svg';
|
||||
|
||||
default:
|
||||
return 'assets/images/hardware_wallet/ledger_nano_x.png';
|
||||
return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,25 +216,15 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/hardware_wallet.svg",
|
||||
),
|
||||
Offstage(
|
||||
offstage: !longWait,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
S.of(context).if_you_dont_see_your_device,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => Offstage(
|
||||
@@ -231,56 +240,69 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (bleDevices.length > 0) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
S.of(context).bluetooth,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
...bleDevices
|
||||
.map(
|
||||
(device) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: DeviceTile(
|
||||
onPressed: () => _connectToDevice(device),
|
||||
title: device.name,
|
||||
leading: _getDeviceTileLeading(device.type),
|
||||
connectionType: device.connectionType,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18)),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final item = allDevices[index];
|
||||
return GestureDetector(
|
||||
onTap: () => _connectToDevice(item),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: _getDeviceTileLeading(item.type),
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn),
|
||||
),
|
||||
Text(item.name)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (item.connectionType ==
|
||||
HardwareWalletConnectionType.ble)
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/bluetooth.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn),
|
||||
),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
],
|
||||
if (usbDevices.length > 0) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
S.of(context).usb,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
...usbDevices
|
||||
.map(
|
||||
(device) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: DeviceTile(
|
||||
onPressed: () => _connectToDevice(device),
|
||||
title: device.name,
|
||||
leading: _getDeviceTileLeading(device.type),
|
||||
connectionType: device.connectionType,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh),
|
||||
itemCount: allDevices.length),
|
||||
),
|
||||
if (widget.allowChangeWallet) ...[
|
||||
PrimaryButton(
|
||||
text: S.of(context).wallets,
|
||||
@@ -293,10 +315,10 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
),
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
NewPrimaryButton(
|
||||
text: S.of(context).how_to_connect,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
textColor: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () => _onHowToConnect(context),
|
||||
)
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
@@ -43,14 +44,12 @@ class DeviceTile extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
if (leading != null && !leading!.endsWith(".svg"))
|
||||
Image.asset(
|
||||
leading!,
|
||||
height: 30,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
if (leading != null && leading!.endsWith(".svg"))
|
||||
SvgPicture.asset(leading!, height: 30),
|
||||
CakeImageWidget(
|
||||
height: 30,
|
||||
imageUrl: leading,
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.onSurface, BlendMode.srcIn),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 16),
|
||||
|
||||
@@ -34,11 +34,11 @@ class CryptoBalanceWidget extends StatelessWidget {
|
||||
case null:
|
||||
return null;
|
||||
case HardwareWalletType.bitbox:
|
||||
return "assets/images/hardware_wallet/device_bitbox.svg";
|
||||
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
|
||||
case HardwareWalletType.ledger:
|
||||
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
case HardwareWalletType.trezor:
|
||||
return "assets/images/hardware_wallet/device_trezor_safe_5.svg";
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg";
|
||||
case HardwareWalletType.cupcake:
|
||||
return "assets/images/cupcake.svg";
|
||||
case HardwareWalletType.coldcard:
|
||||
|
||||
@@ -213,7 +213,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
final sendVM = widget.exchangeTradeViewModel.sendViewModel;
|
||||
|
||||
if (sendVM.wallet.isHardwareWallet) {
|
||||
if (!sendVM.hardwareWalletViewModel!.isConnected) {
|
||||
if (!sendVM.hardwareWalletViewModel!.isConnected(sendVM.walletType)) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: sendVM.walletType,
|
||||
|
||||
@@ -160,7 +160,7 @@ class DEuroSavingsPage extends BasePage {
|
||||
|
||||
Future<void> _requireHardwareWallet(BuildContext context) async {
|
||||
if (_dEuroViewModel.wallet.isHardwareWallet) {
|
||||
if (!_dEuroViewModel.hardwareWalletViewModel!.isConnected) {
|
||||
if (!_dEuroViewModel.hardwareWalletViewModel!.isConnected(_dEuroViewModel.wallet.type)) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: _dEuroViewModel.wallet.type,
|
||||
|
||||
@@ -6,14 +6,8 @@ import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_utils.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
|
||||
import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_alert.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/hardware/device_connection_type.dart';
|
||||
@@ -21,10 +15,38 @@ import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NewWalletTypePageHeader extends StatelessWidget {
|
||||
const NewWalletTypePageHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const SizedBox(
|
||||
width: 138,
|
||||
height: 75,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/hardware_wallet.svg",
|
||||
width: 75,
|
||||
height: 75,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 63,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/cake_coin.svg",
|
||||
width: 75,
|
||||
height: 75,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class NewWalletTypePage extends BasePage {
|
||||
NewWalletTypePage({
|
||||
required this.newWalletTypeArguments,
|
||||
});
|
||||
NewWalletTypePage({required this.newWalletTypeArguments});
|
||||
|
||||
final NewWalletTypeArguments newWalletTypeArguments;
|
||||
|
||||
@@ -38,23 +60,21 @@ class NewWalletTypePage extends BasePage {
|
||||
|
||||
@override
|
||||
Function(BuildContext)? get pushToNextWidget => (context) {
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus) {
|
||||
currentFocus.focusedChild?.unfocus();
|
||||
}
|
||||
};
|
||||
final currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus) currentFocus.focusedChild?.unfocus();
|
||||
};
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => WalletTypeForm(
|
||||
walletImage: currentTheme.isDark ? walletTypeImage : walletTypeLightImage,
|
||||
isCreate: newWalletTypeArguments.isCreate,
|
||||
onTypeSelected: newWalletTypeArguments.onTypeSelected,
|
||||
hardwareWalletType: newWalletTypeArguments.hardwareWalletType,
|
||||
);
|
||||
walletImage: currentTheme.isDark ? walletTypeImage : walletTypeLightImage,
|
||||
isCreate: newWalletTypeArguments.isCreate,
|
||||
onTypeSelected: newWalletTypeArguments.onTypeSelected,
|
||||
hardwareWalletType: newWalletTypeArguments.hardwareWalletType,
|
||||
);
|
||||
}
|
||||
|
||||
class WalletTypeForm extends StatefulWidget {
|
||||
WalletTypeForm({
|
||||
const WalletTypeForm({
|
||||
required this.walletImage,
|
||||
required this.isCreate,
|
||||
this.onTypeSelected,
|
||||
@@ -77,9 +97,6 @@ class WalletTypeFormState extends State<WalletTypeForm> {
|
||||
|
||||
static const aspectRatioImage = 1.22;
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
WalletType? selected;
|
||||
List<WalletType> types;
|
||||
List<WalletType> filteredTypes = [];
|
||||
|
||||
@@ -87,21 +104,12 @@ class WalletTypeFormState extends State<WalletTypeForm> {
|
||||
void initState() {
|
||||
types = filteredTypes = availableWalletTypes
|
||||
.where((element) =>
|
||||
!widget.isHardwareWallet ||
|
||||
DeviceConnectionType.supportedConnectionTypes(
|
||||
element, widget.hardwareWalletType!, Platform.isIOS)
|
||||
.isNotEmpty)
|
||||
!widget.isHardwareWallet ||
|
||||
DeviceConnectionType.supportedConnectionTypes(
|
||||
element, widget.hardwareWalletType!, Platform.isIOS)
|
||||
.isNotEmpty)
|
||||
.toList();
|
||||
super.initState();
|
||||
|
||||
searchController.addListener(() {
|
||||
setState(() {
|
||||
filteredTypes = List.from(types.where((type) => walletTypeToDisplayName(type)
|
||||
.toLowerCase()
|
||||
.contains(searchController.text.toLowerCase())));
|
||||
return;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -110,96 +118,102 @@ class WalletTypeFormState extends State<WalletTypeForm> {
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 48),
|
||||
child: Text(
|
||||
S.of(context).choose_wallet_currency,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const NewWalletTypePageHeader(),
|
||||
Text(
|
||||
S.of(context).choose_wallet_currency,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 24, 24, 12),
|
||||
child: SearchBarWidget(searchController: searchController),
|
||||
),
|
||||
Expanded(
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
scrollableKey: ValueKey('new_wallet_type_scrollable_key'),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
...filteredTypes.map(
|
||||
(type) => Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
child: SelectButton(
|
||||
key: ValueKey('new_wallet_type_${type.name}_button_key'),
|
||||
image: CakeImageWidget(
|
||||
imageUrl: getCryptoCurrencyIconForWalletListItem(type),
|
||||
height: 24,
|
||||
width: 24,
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: ListView.separated(
|
||||
key: const ValueKey('new_wallet_type_scrollable_key'),
|
||||
itemCount: filteredTypes.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final item = filteredTypes[index];
|
||||
final curr = getCryptoCurrencyForWalletListItem(item);
|
||||
|
||||
return GestureDetector(
|
||||
key: ValueKey('new_wallet_type_${item.name}_button_key'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
onTypeSelected(item);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
height: 24,
|
||||
width: 24,
|
||||
imageUrl: curr.iconPath,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(curr.fullName ?? curr.name),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
curr.title,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
text: walletTypeToDisplayName(type),
|
||||
showTrailingIcon: false,
|
||||
height: 54,
|
||||
isSelected: selected == type,
|
||||
onTap: () => setState(() => selected = type),
|
||||
deviceConnectionTypes: widget.isHardwareWallet
|
||||
? DeviceConnectionType.supportedConnectionTypes(
|
||||
type, widget.hardwareWalletType!, Platform.isIOS)
|
||||
: [],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
bottomSection: PrimaryButton(
|
||||
key: ValueKey('new_wallet_type_next_button_key'),
|
||||
onPressed: () => onTypeSelected(),
|
||||
text: S.of(context).seed_language_next,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: selected == null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onTypeSelected() async {
|
||||
if (selected == null) throw Exception('Wallet Type is not selected yet.');
|
||||
|
||||
if (selected == WalletType.haven && widget.isCreate) {
|
||||
return await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => PopUpCancellableAlertDialog(
|
||||
contentText: S.of(context).pause_wallet_creation,
|
||||
actionButtonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onTypeSelected(WalletType selected) async {
|
||||
// If it's a restore flow, trigger the external callback
|
||||
// If it's not a BIP39 Wallet or if there are no other wallets, route to the newWallet page
|
||||
// Any other scenario, route to pre-existing seed page
|
||||
final walletList = await WalletInfo.getAll();
|
||||
if (!widget.isCreate) {
|
||||
widget.onTypeSelected!(context, selected!);
|
||||
} else if (!isBIP39Wallet(selected!) || walletList.isEmpty) {
|
||||
widget.onTypeSelected!(context, selected);
|
||||
} else if (!isBIP39Wallet(selected) || walletList.isEmpty) {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.newWallet,
|
||||
arguments: NewWalletArguments(type: selected!),
|
||||
arguments: NewWalletArguments(type: selected),
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected!);
|
||||
Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/screens/nodes/node_share_page.dart';
|
||||
import 'package:cake_wallet/src/screens/nodes/widgets/node_form.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/primary_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class NodeCreateOrEditPage extends StatefulWidget {
|
||||
@@ -76,6 +76,14 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
reaction((_)=>widget.nodeCreateOrEditViewModel.state, (state) async {
|
||||
if(state is ExecutedSuccessfullyState) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
if (mounted) Navigator.of(context).pop(widget.nodeCreateOrEditViewModel.editingNode);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -90,86 +98,62 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
|
||||
title: widget.editingNode != null ? S.current.edit_node : S.current.node_new,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
trailingIcon: CakeImageWidget(imageUrl:
|
||||
"assets/new-ui/scan.svg",width:24,height:24,
|
||||
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
onTrailingPressed: () => widget.nodeCreateOrEditViewModel.scanQRCodeForNewNode(context),
|
||||
trailingIcon: Icon(Icons.check),
|
||||
onTrailingPressed: () async {
|
||||
if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await widget.nodeCreateOrEditViewModel
|
||||
.save(saveAsCurrent: widget.isSelected ?? false);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: KeyboardHideOverlay(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 18, right: 18),
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: const EdgeInsets.only(bottom: 24.0, top: 8),
|
||||
content: NodeForm(
|
||||
child: Column(
|
||||
spacing: 18,
|
||||
children: [NodeForm(
|
||||
key: _nodeFormKey,
|
||||
nodeViewModel: widget.nodeCreateOrEditViewModel,
|
||||
),
|
||||
bottomSectionPadding: const EdgeInsets.only(bottom: 24),
|
||||
bottomSection: Observer(
|
||||
builder: (_) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: LoadingPrimaryButton(
|
||||
onPressed: () async {
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).remove_node,
|
||||
alertContent: S.of(context).remove_node_message,
|
||||
rightButtonText: S.of(context).remove,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
actionRightButton: () => Navigator.pop(context, true),
|
||||
actionLeftButton: () => Navigator.pop(context, false),
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
NewListSections(sections: {"":[
|
||||
if(widget.editingNode != null)
|
||||
|
||||
if (confirmed) {
|
||||
await widget.nodeCreateOrEditViewModel.delete(editingNode: widget.editingNode!);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
text: S.of(context).delete,
|
||||
isDisabled: widget.editingNode == null ||
|
||||
!widget.nodeCreateOrEditViewModel.isReady ||
|
||||
widget.editingNode!.isBuiltin ||
|
||||
(widget.isSelected ?? false),
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: PrimaryButton(
|
||||
onPressed: () async {
|
||||
if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
ListItemRegularRow(keyValue: "share", label: S.of(context).share_this_node, foregroundColor: Theme.of(context).colorScheme.primary, showArrow: false, onTap: (){
|
||||
|
||||
await widget.nodeCreateOrEditViewModel.save(
|
||||
saveAsCurrent: widget.isSelected ?? false);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
text: S.of(context).save,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: (!widget.nodeCreateOrEditViewModel.isReady) ||
|
||||
(widget.nodeCreateOrEditViewModel.connectionState is IsExecutingState),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Navigator.of(context).push(CupertinoPageRoute(builder: (context)=>NodeSharePage(uri: widget.editingNode!.uri, currency: walletTypeToCryptoCurrency(widget.type!))));
|
||||
|
||||
|
||||
}),
|
||||
if(!(widget.editingNode == null ||
|
||||
!widget.nodeCreateOrEditViewModel.isReady ||
|
||||
widget.editingNode!.isBuiltin ||
|
||||
(widget.isSelected ?? false)))
|
||||
ListItemRegularRow(keyValue: "delete", label: S.of(context).delete_this_node, foregroundColor: Theme.of(context).colorScheme.errorContainer, showArrow: false, onTap: ()async{
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).remove_node,
|
||||
alertContent: S.of(context).remove_node_message,
|
||||
rightButtonText: S.of(context).remove,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
actionRightButton: () => Navigator.pop(context, true),
|
||||
actionLeftButton: () => Navigator.pop(context, false),
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
|
||||
if (confirmed) {
|
||||
await widget.nodeCreateOrEditViewModel.delete(editingNode: widget.editingNode!);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
})
|
||||
]})
|
||||
|
||||
]
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NodeSharePage extends StatelessWidget {
|
||||
const NodeSharePage({super.key, required this.uri, required this.currency});
|
||||
|
||||
final Uri uri;
|
||||
final CryptoCurrency currency;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final qrSize = MediaQuery.of(context).size.width * 0.786;
|
||||
final padding = 16.0;
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).share,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 40,
|
||||
children: [
|
||||
Stack(alignment: Alignment.center, children: [
|
||||
SizedBox(
|
||||
width: qrSize,
|
||||
height: qrSize,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: QrImage(
|
||||
size: qrSize - padding * 2,
|
||||
embeddedImagePath: "",
|
||||
data: uri.toString())),
|
||||
),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: Stack(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/settings_row_icons/nodes.svg",
|
||||
width: 54,
|
||||
height: 54,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
]),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 36.0),
|
||||
child: Text(S.of(context).node_share_desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,115 +1,55 @@
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/nodes/widgets/node_indicator.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NodeListRow extends StandardListRow {
|
||||
class NodeListRow extends StatelessWidget {
|
||||
NodeListRow(
|
||||
{required String title,
|
||||
String? subtitle,
|
||||
required this.node,
|
||||
required void Function(BuildContext context) onTap,
|
||||
required bool isSelected,
|
||||
required this.isPow})
|
||||
: super(title: title, onTap: onTap, isSelected: isSelected, subtitle: subtitle);
|
||||
{required this.node,
|
||||
required this.onTap,
|
||||
required this.onEditComplete,
|
||||
required this.isSelected,
|
||||
required this.isPow, this.speed});
|
||||
|
||||
final Node node;
|
||||
final bool isPow;
|
||||
final VoidCallback onTap;
|
||||
final Future<void> Function(Object?) onEditComplete;
|
||||
final bool isSelected;
|
||||
final NodeSpeed? speed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final leading = buildLeading(context);
|
||||
final trailing = buildTrailing(context);
|
||||
return Container(
|
||||
padding: EdgeInsets.only(left: 18, right: 18, top: 2, bottom: 2),
|
||||
margin: EdgeInsets.only(top: 2, bottom: 2),
|
||||
child: FilledButton(
|
||||
onPressed: () => onTap?.call(context),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
leading,
|
||||
buildCenter(context, hasLeftOffset: true),
|
||||
trailing,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: node.requestNode(),
|
||||
builder: (context, snapshot) {
|
||||
switch (snapshot.connectionState) {
|
||||
case ConnectionState.done:
|
||||
return NodeIndicator(isLive: snapshot.data ?? false);
|
||||
default:
|
||||
return NodeIndicator(isLive: false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTrailing(BuildContext context) {
|
||||
final hasLabel = node.label != null && node.label!.isNotEmpty;
|
||||
final uriText = "${node.uri.host}:${node.uri.port}";
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
isPow ? Routes.newPowNode : Routes.newNode,
|
||||
arguments: {'editingNode': node, 'isSelected': isSelected},
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NodeHeaderListRow extends StandardListRow {
|
||||
NodeHeaderListRow({required String title, required void Function(BuildContext context) onTap})
|
||||
: super(title: title, onTap: onTap, isSelected: false);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final leading = buildLeading(context);
|
||||
final trailing = buildTrailing(context);
|
||||
return Container(
|
||||
height: 56,
|
||||
padding: EdgeInsets.only(left: 12, right: 12, top: 2, bottom: 2),
|
||||
child: TextButton(
|
||||
onPressed: () => onTap?.call(context),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
),
|
||||
),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 12),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
if (leading != null) leading,
|
||||
buildCenter(context, hasLeftOffset: leading != null),
|
||||
leading,
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment:CrossAxisAlignment.start,spacing:4,children: [
|
||||
Row(
|
||||
spacing: 4,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(hasLabel ? node.label! : uriText, style: TextStyle(fontSize: 12),),
|
||||
if(badgePath != null)
|
||||
CakeImageWidget(imageUrl: badgePath, colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary,BlendMode.srcIn),)
|
||||
],
|
||||
),
|
||||
if(hasLabel) Text(uriText, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),),
|
||||
],),
|
||||
),
|
||||
trailing,
|
||||
],
|
||||
),
|
||||
@@ -117,12 +57,44 @@ class NodeHeaderListRow extends StandardListRow {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) => isSelected
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
)
|
||||
: SizedBox(width: 24, height: 24);
|
||||
|
||||
Widget buildTrailing(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 20,
|
||||
child: Icon(Icons.add,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant, size: 24.0),
|
||||
return Row(
|
||||
children: [
|
||||
if (speed != null) CakeImageWidget(imageUrl: speed!.iconPath, width: 24, height: 24),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
final res = await Navigator.of(context).pushNamed(isPow ? Routes.newPowNode : Routes.newNode,
|
||||
arguments: {'editingNode': node, 'isSelected': isSelected});
|
||||
await onEditComplete(res);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/3dots_vertical.svg",
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String? get badgePath {
|
||||
if(node.isOfficial) return "assets/new-ui/official_node.svg";
|
||||
if(node.isBuiltin) return "assets/new-ui/default_node.svg";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@ class SendPage extends BasePage {
|
||||
}
|
||||
|
||||
if (sendViewModel.wallet.isHardwareWallet) {
|
||||
if (!sendViewModel.hardwareWalletViewModel!.isConnected) {
|
||||
if (!sendViewModel.hardwareWalletViewModel!.isConnected(sendViewModel.walletType)) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: sendViewModel.walletType,
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modal_header.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modal_page_wrapper.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class ManageNodesPage extends BasePage {
|
||||
ManageNodesPage(this.isPow, {this.nodeListViewModel, this.powNodeListViewModel, this.dashboardViewModel})
|
||||
: assert((isPow && powNodeListViewModel != null) || (!isPow && nodeListViewModel != null)),
|
||||
assert(powNodeListViewModel == null || nodeListViewModel == null);
|
||||
class ManageNodesPage extends StatefulWidget {
|
||||
ManageNodesPage(this.isPow, {required this.nodeListViewModel, this.dashboardViewModel});
|
||||
|
||||
final DashboardViewModel? dashboardViewModel;
|
||||
final NodeListViewModel? nodeListViewModel;
|
||||
final PowNodeListViewModel? powNodeListViewModel;
|
||||
final NodeListViewModel nodeListViewModel;
|
||||
final bool isPow;
|
||||
|
||||
@override
|
||||
bool get hideAppBar => true;
|
||||
State<ManageNodesPage> createState() => _ManageNodesPageState();
|
||||
}
|
||||
|
||||
class _ManageNodesPageState extends State<ManageNodesPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(Duration(milliseconds: 300))
|
||||
.then((_) => widget.nodeListViewModel.speedTestNodes());
|
||||
}
|
||||
|
||||
@override
|
||||
String get title => S.current.manage_nodes;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
Widget build(BuildContext context) {
|
||||
return ModalPageWrapper(
|
||||
horizontalPadding: 0,
|
||||
topBar: ModalTopBar(
|
||||
title: S.of(context).manage_nodes,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
@@ -44,87 +44,96 @@ class ManageNodesPage extends BasePage {
|
||||
trailingWidget: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (dashboardViewModel?.hasRescan ?? true)
|
||||
ModernButton(
|
||||
Observer(
|
||||
builder: (_) => ModernButton(
|
||||
size: 36,
|
||||
icon: Icon(Icons.history),
|
||||
onPressed: () => Navigator.of(context).pushNamed(Routes.rescan)),
|
||||
icon: widget.nodeListViewModel.isTestingNodeSpeed
|
||||
? CupertinoActivityIndicator()
|
||||
: Icon(Icons.refresh),
|
||||
onPressed: () => widget.nodeListViewModel.speedTestNodes()),
|
||||
),
|
||||
ModernButton(
|
||||
size: 36,
|
||||
icon: Icon(Icons.add),
|
||||
onPressed: () => Navigator.of(context).pushNamed(isPow ? Routes.newPowNode : Routes.newNode))
|
||||
onPressed: ()async {
|
||||
final res = await Navigator.of(context)
|
||||
.pushNamed(widget.isPow ? Routes.newPowNode : Routes.newNode);
|
||||
if(res != null && res is Node) {
|
||||
widget.nodeListViewModel.nodes.add(res);
|
||||
}
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
header: ModalHeader(
|
||||
iconPath: "assets/new-ui/settings_row_icons/nodes.svg",
|
||||
message: S.of(context).nodes_desc,
|
||||
title: S.of(context).nodes,
|
||||
bottomWidget: Observer(
|
||||
builder: (_) => NodeListRow(
|
||||
node: widget.nodeListViewModel.currentNode,
|
||||
speed: widget.nodeListViewModel.nodeSpeedFor(widget.nodeListViewModel.currentNode),
|
||||
onEditComplete: (res)async{
|
||||
if(res != null && res is Node) {
|
||||
widget.nodeListViewModel.nodes.removeWhere((item)=>item.id == res.id);
|
||||
widget.nodeListViewModel.nodes.add(res);
|
||||
}
|
||||
|
||||
},
|
||||
onTap: () {},
|
||||
isSelected: true,
|
||||
isPow: widget.isPow)),
|
||||
),
|
||||
content: Column(
|
||||
children: [
|
||||
if (FeatureFlag.isAutomaticNodeSwitchingEnabled)
|
||||
Observer(
|
||||
builder: (_) => SettingsSwitcherCell(
|
||||
key: ValueKey('manage_nodes_page_enable_auto_node_switching_button_key'),
|
||||
title: S.current.enable_auto_node_switching,
|
||||
value: isPow
|
||||
? powNodeListViewModel!.enableAutomaticNodeSwitching
|
||||
: nodeListViewModel!.enableAutomaticNodeSwitching,
|
||||
onValueChange: (BuildContext context, bool value) {
|
||||
if (isPow) {
|
||||
powNodeListViewModel!.setEnableAutomaticNodeSwitching(value);
|
||||
} else {
|
||||
nodeListViewModel!.setEnableAutomaticNodeSwitching(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
if (FeatureFlag.isAutomaticNodeSwitchingEnabled) SizedBox(height: 8),
|
||||
Observer(
|
||||
builder: (BuildContext context) {
|
||||
int itemsCount =
|
||||
nodeListViewModel?.nodes.length ?? powNodeListViewModel!.nodes.length;
|
||||
return SectionStandardList(
|
||||
scrollController: ModalScrollController.of(context),
|
||||
sectionCount: 1,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
dividerPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
itemCounter: (int sectionIndex) => itemsCount,
|
||||
itemBuilder: (_, index) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
final node =
|
||||
nodeListViewModel?.nodes[index] ?? powNodeListViewModel!.nodes[index];
|
||||
late bool isSelected;
|
||||
if (isPow) {
|
||||
isSelected = node.id == powNodeListViewModel!.currentNode.id;
|
||||
} else {
|
||||
isSelected = node.id == nodeListViewModel!.currentNode.id;
|
||||
}
|
||||
final nodeListRow = NodeListRow(
|
||||
title: node.uriRaw,
|
||||
subtitle: node.label ?? '',
|
||||
node: node,
|
||||
isSelected: isSelected,
|
||||
isPow: isPow,
|
||||
onTap: (_) async {
|
||||
if (isSelected) {
|
||||
return;
|
||||
}
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18)),
|
||||
child: Observer(
|
||||
builder: (BuildContext context) {
|
||||
int itemsCount = widget.nodeListViewModel.nonCurrentNodes.length;
|
||||
return ListView.separated(
|
||||
controller: ModalScrollController.of(context),
|
||||
shrinkWrap: true,
|
||||
physics: BouncingScrollPhysics(),
|
||||
itemCount: itemsCount,
|
||||
separatorBuilder: (context, index) => Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
final node = widget.nodeListViewModel.nonCurrentNodes[index];
|
||||
final nodeListRow = NodeListRow(
|
||||
node: node,
|
||||
isSelected: false,
|
||||
isPow: widget.isPow,
|
||||
speed: widget.nodeListViewModel.nodeSpeedFor(node),
|
||||
onEditComplete: (res) async {
|
||||
if(res != null && res is Node) {
|
||||
widget.nodeListViewModel.nodes.removeWhere((item)=>item.id == res.id);
|
||||
widget.nodeListViewModel.nodes.add(res);
|
||||
}
|
||||
|
||||
},
|
||||
onTap: () async {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.of(context).change_current_node_title,
|
||||
alertContent: nodeListViewModel?.getAlertContent(node.uriRaw) ??
|
||||
powNodeListViewModel!.getAlertContent(node.uriRaw),
|
||||
alertContent:
|
||||
widget.nodeListViewModel.getAlertContent(node.uriRaw),
|
||||
leftButtonText: S.of(context).cancel,
|
||||
rightButtonText: S.of(context).change,
|
||||
actionLeftButton: () => Navigator.of(context).pop(),
|
||||
actionRightButton: () async {
|
||||
if (isPow) {
|
||||
await powNodeListViewModel!.setAsCurrent(node);
|
||||
} else {
|
||||
await nodeListViewModel!.setAsCurrent(node);
|
||||
}
|
||||
await widget.nodeListViewModel.setAsCurrent(node);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
@@ -132,32 +141,16 @@ class ManageNodesPage extends BasePage {
|
||||
);
|
||||
},
|
||||
);
|
||||
return nodeListRow;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
return nodeListRow;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Future<void> _presentReconnectAlert(BuildContext context) async {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.of(context).reconnection,
|
||||
alertContent: S.of(context).reconnect_alert_text,
|
||||
rightButtonText: S.of(context).ok,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
actionRightButton: () async {
|
||||
Navigator.of(context).pop();
|
||||
await dashboardViewModel!.reconnect();
|
||||
},
|
||||
actionLeftButton: () => Navigator.of(context).pop());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class BottomSheetServiceImpl implements BottomSheetService {
|
||||
int closeAfter = 0,
|
||||
bool isModalDismissible = false,
|
||||
}) async {
|
||||
// Create the bottom sheet queue item
|
||||
final completer = Completer<dynamic>();
|
||||
final queueItem = BottomSheetQueueItemModel(
|
||||
widget: widget,
|
||||
@@ -38,15 +37,12 @@ class BottomSheetServiceImpl implements BottomSheetService {
|
||||
isModalDismissible: isModalDismissible,
|
||||
);
|
||||
|
||||
// If the current sheet it null, set it to the queue item
|
||||
if (currentSheet.value == null) {
|
||||
currentSheet.value = queueItem;
|
||||
} else {
|
||||
// Otherwise, add it to the queue
|
||||
queue.add(queueItem);
|
||||
}
|
||||
|
||||
// Return the future
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:eth_sig_util/eth_sig_util.dart';
|
||||
import 'package:eth_sig_util/util/utils.dart';
|
||||
@@ -106,7 +110,6 @@ class EvmChainServiceImpl {
|
||||
|
||||
if (isApproved) {
|
||||
try {
|
||||
// Load the private key
|
||||
final keys = wcKeyService.getKeysForChain(appStore.wallet!);
|
||||
final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
|
||||
|
||||
@@ -155,7 +158,6 @@ class EvmChainServiceImpl {
|
||||
|
||||
if (isApproved) {
|
||||
try {
|
||||
// Load the private key
|
||||
final keys = wcKeyService.getKeysForChain(appStore.wallet!);
|
||||
final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
|
||||
|
||||
@@ -297,7 +299,6 @@ class EvmChainServiceImpl {
|
||||
|
||||
if (transaction is Transaction) {
|
||||
try {
|
||||
// Load the private key
|
||||
final keys = wcKeyService.getKeysForChain(appStore.wallet!);
|
||||
final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
|
||||
|
||||
@@ -309,7 +310,6 @@ class EvmChainServiceImpl {
|
||||
chainId: int.parse(chainId),
|
||||
);
|
||||
|
||||
// Sign the transaction
|
||||
final signedTx = bytesToHex(signature, include0x: true);
|
||||
response = response.copyWith(result: signedTx);
|
||||
} on RPCError catch (e) {
|
||||
@@ -349,7 +349,6 @@ class EvmChainServiceImpl {
|
||||
);
|
||||
if (transaction is Transaction) {
|
||||
try {
|
||||
// Load the private key
|
||||
final keys = wcKeyService.getKeysForChain(appStore.wallet!);
|
||||
final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
|
||||
final chainId = getChainId().split(':').last;
|
||||
@@ -440,30 +439,26 @@ class EvmChainServiceImpl {
|
||||
|
||||
transaction = await _applyWCBufferedFees(transaction);
|
||||
|
||||
final gweiGasPrice =
|
||||
(transaction.gasPrice?.getInWei ?? transaction.maxFeePerGas?.getInWei ?? BigInt.zero) /
|
||||
BigInt.from(1000000000);
|
||||
final nativeCurrency = evm?.getChainInfoByChainId(reference.chainId ?? 1)?.currency;
|
||||
final nativeSymbol = nativeCurrency?.title ?? 'ETH';
|
||||
|
||||
final amount = (transaction.value?.getInWei ?? BigInt.zero) / BigInt.from(1e18);
|
||||
|
||||
final txMessageText = '${S.current.value}: ${amount.toStringAsFixed(9)} ETH\n'
|
||||
final txMessageText = '${S.current.value}: ${amount.toStringAsFixed(9)} $nativeSymbol\n'
|
||||
'${S.current.from}: ${transaction.from?.hex}\n'
|
||||
'${S.current.to}: ${transaction.to?.hex}';
|
||||
|
||||
final feeRows = _buildFeeExtraModels(transaction, nativeCurrency, nativeSymbol);
|
||||
|
||||
if (await MethodsUtils.requestApproval(
|
||||
txMessageText,
|
||||
title: title,
|
||||
method: method,
|
||||
chainId: chainId,
|
||||
address: address,
|
||||
address: address ?? transaction.from?.hex ?? '',
|
||||
transportType: transportType,
|
||||
verifyContext: verifyContext,
|
||||
extraModels: [
|
||||
WCConnectionModel(
|
||||
title: S.current.gas_price,
|
||||
elements: ['${gweiGasPrice.toStringAsFixed(2)} GWEI'],
|
||||
),
|
||||
],
|
||||
extraModels: feeRows,
|
||||
)) {
|
||||
return transaction;
|
||||
}
|
||||
@@ -471,6 +466,66 @@ class EvmChainServiceImpl {
|
||||
return JsonRpcError(code: 5002, message: S.current.user_rejected_method);
|
||||
}
|
||||
|
||||
List<WCConnectionModel> _buildFeeExtraModels(
|
||||
Transaction transaction,
|
||||
CryptoCurrency? nativeCurrency,
|
||||
String nativeSymbol,
|
||||
) {
|
||||
final gasLimit = transaction.maxGas;
|
||||
if (gasLimit == null || gasLimit <= 0) return const [];
|
||||
|
||||
final gasLimitBig = BigInt.from(gasLimit);
|
||||
final isEip1559 = transaction.isEIP1559;
|
||||
final perGasWei =
|
||||
isEip1559 ? transaction.maxFeePerGas?.getInWei : transaction.gasPrice?.getInWei;
|
||||
if (perGasWei == null || perGasWei <= BigInt.zero) return const [];
|
||||
|
||||
final feeWei = gasLimitBig * perGasWei;
|
||||
final feeNative = feeWei.toDouble() / 1e18;
|
||||
|
||||
final label = isEip1559 ? S.current.wc_max_network_fee : S.current.wc_network_fee;
|
||||
|
||||
return [
|
||||
WCConnectionModel(
|
||||
title: label,
|
||||
elements: [_formatFeeLine(feeNative, nativeSymbol, nativeCurrency)],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
String _formatFeeLine(
|
||||
double feeNative,
|
||||
String nativeSymbol,
|
||||
CryptoCurrency? nativeCurrency,
|
||||
) {
|
||||
final cryptoPart = '${_formatNativeAmount(feeNative)} $nativeSymbol';
|
||||
|
||||
if (nativeCurrency == null) return cryptoPart;
|
||||
|
||||
try {
|
||||
final fiatStore = getIt.get<FiatConversionStore>();
|
||||
final price = fiatStore.prices[nativeCurrency];
|
||||
if (price == null || price <= 0) return cryptoPart;
|
||||
|
||||
final fiatSymbol = appStore.settingsStore.fiatCurrency.title;
|
||||
final fiatValue = calculateFiatAmount(
|
||||
price: price,
|
||||
cryptoAmount: feeNative.toString(),
|
||||
);
|
||||
if (fiatValue.isEmpty || fiatValue == '0.00') return cryptoPart;
|
||||
|
||||
return '$cryptoPart (~ $fiatValue $fiatSymbol)';
|
||||
} catch (_) {
|
||||
return cryptoPart;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatNativeAmount(double value) {
|
||||
if (value == 0) return '0';
|
||||
if (value >= 0.0001) return value.toStringAsFixed(6);
|
||||
return value.toStringAsExponential(4);
|
||||
}
|
||||
|
||||
Future<Transaction> _ensureWCTransactionHasGasLimit(Transaction transaction) async {
|
||||
final hasGasLimit = transaction.maxGas != null && transaction.maxGas! > 0;
|
||||
if (hasGasLimit) return transaction;
|
||||
@@ -637,7 +692,6 @@ $messageDetails''';
|
||||
if (value == null) continue;
|
||||
|
||||
if (types.containsKey(fieldType)) {
|
||||
// Handle nested types
|
||||
final nestedFields = types[fieldType] as List<dynamic>;
|
||||
if (fieldType == 'Person') {
|
||||
// Special formatting for Person type
|
||||
|
||||
@@ -80,7 +80,6 @@ class SolanaChainService {
|
||||
error: JsonRpcError(code: error.code, message: error.message),
|
||||
);
|
||||
}
|
||||
//
|
||||
} catch (e) {
|
||||
debugPrint('solanaSignMessage error $e');
|
||||
final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
|
||||
|
||||
@@ -6,12 +6,7 @@ import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
abstract class WalletConnectKeyService {
|
||||
/// Returns a list of all the keys.
|
||||
List<ChainKeyModel> getKeys(WalletBase wallet);
|
||||
|
||||
/// Returns a list of all the keys for a given chain id.
|
||||
/// If the chain is not found, returns an empty list.
|
||||
/// - [chain]: The chain to get the keys for.
|
||||
List<ChainKeyModel> getKeysForChain(WalletBase wallet);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,9 @@ import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/chai
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/utils/eth_utils.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/utils/method_utils.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_request_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_request_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_session_auth_request_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_request_sheet.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_signing_request_sheet.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
|
||||
import 'bottom_sheet_service.dart';
|
||||
@@ -56,8 +55,6 @@ abstract class WalletKitServiceBase with Store {
|
||||
bool isInitialized;
|
||||
|
||||
/// The list of requests from the dapp
|
||||
/// Potential types include, but aren't limited to:
|
||||
/// [SessionProposalEvent], [SessionAuthRequest]
|
||||
@observable
|
||||
ObservableList<PairingInfo> pairings;
|
||||
|
||||
@@ -69,7 +66,6 @@ abstract class WalletKitServiceBase with Store {
|
||||
|
||||
@action
|
||||
void create() {
|
||||
// Create the walletkit client
|
||||
_walletKit = ReownWalletKit(
|
||||
core: ReownCore(
|
||||
projectId: secrets.walletConnectProjectId,
|
||||
@@ -85,7 +81,6 @@ abstract class WalletKitServiceBase with Store {
|
||||
|
||||
_walletKit.core.addLogListener(_logListener);
|
||||
|
||||
// Setup our listeners
|
||||
log('Created instance of walletKit');
|
||||
|
||||
_walletKit.core.pairing.onPairingInvalid.subscribe(_onPairingInvalid);
|
||||
@@ -288,16 +283,12 @@ abstract class WalletKitServiceBase with Store {
|
||||
if (args != null) {
|
||||
final proposer = args.params.proposer;
|
||||
final result = (await _bottomSheetHandler.queueBottomSheet(
|
||||
widget: WCRequestWidget(
|
||||
widget: WCConnectionRequestSheet(
|
||||
proposalData: args.params,
|
||||
requester: proposer,
|
||||
verifyContext: args.verifyContext,
|
||||
child: WCConnectionRequestWidget(
|
||||
proposalData: args.params,
|
||||
verifyContext: args.verifyContext,
|
||||
requester: proposer,
|
||||
walletKeyService: walletKeyService,
|
||||
walletKit: walletKit,
|
||||
appStore: appStore,
|
||||
),
|
||||
walletKeyService: walletKeyService,
|
||||
appStore: appStore,
|
||||
),
|
||||
)) ??
|
||||
WCBottomSheetResult.reject;
|
||||
@@ -400,10 +391,8 @@ abstract class WalletKitServiceBase with Store {
|
||||
debugPrint('_onPairingCreate $args');
|
||||
|
||||
if (args != null && args.topic != null && args.topic!.isNotEmpty) {
|
||||
// Save the pairing topic when pairing is created
|
||||
savePairingTopicToLocalStorage(args.topic!);
|
||||
|
||||
// Refresh pairings to show the new pairing in the list
|
||||
_refreshPairings();
|
||||
}
|
||||
}
|
||||
@@ -440,16 +429,28 @@ abstract class WalletKitServiceBase with Store {
|
||||
formattedMessages.add({iss: message});
|
||||
}
|
||||
|
||||
final requesterMetadata = args.requester.metadata;
|
||||
final requesterIcon = requesterMetadata.icons.isNotEmpty
|
||||
? requesterMetadata.icons.first
|
||||
: null;
|
||||
final chainKeysForAuth = walletKeyService.getKeysForChain(appStore.wallet!);
|
||||
final addressForAuth =
|
||||
chainKeysForAuth.isNotEmpty ? chainKeysForAuth.first.publicKey : '';
|
||||
final combinedMessageBody =
|
||||
formattedMessages.map((m) => m.values.first as String).join('\n\n');
|
||||
|
||||
final WCBottomSheetResult result = (await _bottomSheetHandler.queueBottomSheet(
|
||||
widget: WCSessionAuthRequestWidget(
|
||||
child: WCConnectionRequestWidget(
|
||||
sessionAuthPayload: newAuthPayload,
|
||||
verifyContext: args.verifyContext,
|
||||
requester: args.requester,
|
||||
walletKeyService: walletKeyService,
|
||||
walletKit: _walletKit,
|
||||
appStore: appStore,
|
||||
),
|
||||
widget: WCSigningRequestSheet(
|
||||
title: S.current.wc_signing_request_title,
|
||||
swipeLabel: S.current.wc_swipe_to_sign,
|
||||
dappName: requesterMetadata.name,
|
||||
dappIconUrl: requesterIcon,
|
||||
dappSubtitle: requesterMetadata.url,
|
||||
message: combinedMessageBody,
|
||||
walletName: appStore.wallet?.name ?? '',
|
||||
address: addressForAuth,
|
||||
verifyContext: args.verifyContext,
|
||||
signAllCount: formattedMessages.length,
|
||||
),
|
||||
) as WCBottomSheetResult?) ??
|
||||
WCBottomSheetResult.reject;
|
||||
@@ -458,10 +459,8 @@ abstract class WalletKitServiceBase with Store {
|
||||
final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
|
||||
final privateKey = '0x${chainKeys.first.privateKey}';
|
||||
final credentials = EthPrivateKey.fromHex(privateKey);
|
||||
//
|
||||
final messageToSign = formattedMessages.length;
|
||||
final count = (result == WCBottomSheetResult.one) ? 1 : messageToSign;
|
||||
//
|
||||
final List<Cacao> cacaos = [];
|
||||
for (var i = 0; i < count; i++) {
|
||||
final iss = formattedMessages[i].keys.first;
|
||||
@@ -572,19 +571,6 @@ abstract class WalletKitServiceBase with Store {
|
||||
_refreshPairings();
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateSession({
|
||||
required String topic,
|
||||
required Map<String, Namespace> namespaces,
|
||||
}) async {
|
||||
await walletKit.updateSession(topic: topic, namespaces: namespaces);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> extendSession({required String topic}) async {
|
||||
await walletKit.extendSession(topic: topic);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> pairWithUri(Uri uri) async {
|
||||
try {
|
||||
@@ -633,7 +619,7 @@ abstract class WalletKitServiceBase with Store {
|
||||
|
||||
@action
|
||||
List<SessionData> getSessionsForPairingInfo(PairingInfo pairing) {
|
||||
return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
|
||||
return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
|
||||
}
|
||||
|
||||
String getKeyForStoringTopicsForWallet() {
|
||||
@@ -664,18 +650,14 @@ abstract class WalletKitServiceBase with Store {
|
||||
}
|
||||
|
||||
List<String> getPairingTopicsForWallet(String key) {
|
||||
// Get the JSON-encoded string from shared preferences
|
||||
final jsonString = sharedPreferences.getString(key);
|
||||
|
||||
// If the string is null, return an empty list
|
||||
if (jsonString == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Decode the JSON string to a list of strings
|
||||
final List<dynamic> jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
|
||||
// Cast each item to a string
|
||||
return jsonList.map((item) => item as String).toList();
|
||||
}
|
||||
|
||||
@@ -691,12 +673,10 @@ abstract class WalletKitServiceBase with Store {
|
||||
}
|
||||
|
||||
Future<void> savePairingTopicToLocalStorage(String pairingTopic) async {
|
||||
// Get key specific to the current wallet
|
||||
final key = getKeyForStoringTopicsForWallet();
|
||||
|
||||
if (key.isEmpty) return;
|
||||
|
||||
// Get all pairing topics attached to this key
|
||||
final pairingTopicsForWallet = getPairingTopicsForWallet(key);
|
||||
|
||||
bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic);
|
||||
@@ -704,13 +684,10 @@ abstract class WalletKitServiceBase with Store {
|
||||
'Is Pairing Topic Saved: $isPairingTopicAlreadySaved, Key: $key, Topic: $pairingTopic');
|
||||
|
||||
if (!isPairingTopicAlreadySaved) {
|
||||
// Update the list with the most recent pairing topic
|
||||
pairingTopicsForWallet.add(pairingTopic);
|
||||
|
||||
// Convert the list of updated pairing topics to a JSON-encoded string
|
||||
final jsonString = jsonEncode(pairingTopicsForWallet);
|
||||
|
||||
// Save the encoded string to shared preferences
|
||||
await sharedPreferences.setString(key, jsonString);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_request_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_message_card.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_signing_request_sheet.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/themes/core/custom_theme_colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
@@ -12,6 +13,14 @@ import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
class MethodsUtils {
|
||||
static final walletKit = getIt.get<WalletKitService>().walletKit;
|
||||
static final bottomSheetService = getIt.get<BottomSheetService>();
|
||||
|
||||
static const _transactionMethods = {
|
||||
'eth_sendTransaction',
|
||||
'eth_signTransaction',
|
||||
'solana_signTransaction',
|
||||
'solana_signAllTransactions',
|
||||
'solana_signAndSendTransaction',
|
||||
};
|
||||
|
||||
static Future<bool> requestApproval(
|
||||
String text, {
|
||||
@@ -23,23 +32,53 @@ class MethodsUtils {
|
||||
List<WCConnectionModel> extraModels = const [],
|
||||
VerifyContext? verifyContext,
|
||||
}) async {
|
||||
final appStore = getIt.get<AppStore>();
|
||||
final pending = walletKit.pendingRequests.getAll();
|
||||
final session = pending.isNotEmpty
|
||||
? walletKit.sessions.get(pending.last.topic)
|
||||
: null;
|
||||
final dAppMetadata = session?.peer.metadata;
|
||||
|
||||
final isTransaction = method != null && _transactionMethods.contains(method);
|
||||
final resolvedTitle = title ??
|
||||
(isTransaction
|
||||
? S.current.wc_approve_request_title
|
||||
: S.current.wc_signing_request_title);
|
||||
final swipeLabel =
|
||||
isTransaction ? S.current.wc_swipe_to_approve : S.current.wc_swipe_to_sign;
|
||||
|
||||
final extraRows = <WCMessageRow>[];
|
||||
if (method != null && method.isNotEmpty) {
|
||||
extraRows.add(WCMessageRow(label: S.current.method, value: method));
|
||||
}
|
||||
if (chainId != null && chainId.isNotEmpty) {
|
||||
extraRows.add(WCMessageRow(label: S.current.chain_id, value: chainId));
|
||||
}
|
||||
if (transportType.isNotEmpty) {
|
||||
extraRows.add(WCMessageRow(
|
||||
label: S.current.transport_type,
|
||||
value: transportType.toUpperCase(),
|
||||
));
|
||||
}
|
||||
for (final model in extraModels) {
|
||||
if (model.title == null) continue;
|
||||
final value = model.elements?.join(', ') ?? model.text ?? '';
|
||||
extraRows.add(WCMessageRow(label: model.title!, value: value));
|
||||
}
|
||||
|
||||
final WCBottomSheetResult result = (await bottomSheetService.queueBottomSheet(
|
||||
widget: WCRequestWidget(
|
||||
widget: WCSigningRequestSheet(
|
||||
title: resolvedTitle,
|
||||
swipeLabel: swipeLabel,
|
||||
dappName: dAppMetadata?.name ?? '',
|
||||
dappIconUrl:
|
||||
(dAppMetadata?.icons.isNotEmpty ?? false) ? dAppMetadata!.icons.first : null,
|
||||
dappSubtitle: method ?? dAppMetadata?.url ?? '',
|
||||
message: text,
|
||||
walletName: appStore.wallet?.name ?? '',
|
||||
address: address ?? '',
|
||||
verifyContext: verifyContext,
|
||||
child: WCConnectionWidget(
|
||||
title: title ?? S.current.approve_request,
|
||||
info: [
|
||||
WCConnectionModel(
|
||||
title: '${S.current.method}: $method\n'
|
||||
'${S.current.transport_type}: ${transportType.toUpperCase()}\n'
|
||||
'${S.current.chain_id}: $chainId\n'
|
||||
'${S.current.address}: $address\n\n'
|
||||
'${S.current.message}:',
|
||||
elements: [text],
|
||||
),
|
||||
...extraModels,
|
||||
],
|
||||
),
|
||||
extraRows: extraRows,
|
||||
),
|
||||
) as WCBottomSheetResult?) ??
|
||||
WCBottomSheetResult.reject;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
|
||||
class ConnectionWidgetBuilder {
|
||||
static List<WCConnectionWidget> buildFromRequiredNamespaces(
|
||||
Map<String, Namespace> generatedNamespaces,
|
||||
) {
|
||||
final List<WCConnectionWidget> views = [];
|
||||
for (final key in generatedNamespaces.keys) {
|
||||
final namespaces = generatedNamespaces[key]!;
|
||||
final chains = NamespaceUtils.getChainsFromAccounts(namespaces.accounts);
|
||||
|
||||
final List<WCConnectionModel> models = [];
|
||||
|
||||
// If the chains property is present, add the chain data to the models
|
||||
models.add(WCConnectionModel(title: S.current.chains, elements: chains));
|
||||
models.add(WCConnectionModel(title: S.current.methods, elements: namespaces.methods));
|
||||
|
||||
if (namespaces.events.isNotEmpty) {
|
||||
models.add(WCConnectionModel(title: S.current.events, elements: namespaces.events));
|
||||
}
|
||||
|
||||
views.add(WCConnectionWidget(title: key, info: models));
|
||||
}
|
||||
|
||||
return views;
|
||||
}
|
||||
|
||||
static List<WCConnectionWidget> buildFromNamespaces(
|
||||
String topic,
|
||||
Map<String, Namespace> namespaces,
|
||||
BuildContext context,
|
||||
) {
|
||||
final List<WCConnectionWidget> views = [];
|
||||
for (final key in namespaces.keys) {
|
||||
final ns = namespaces[key]!;
|
||||
final List<WCConnectionModel> models = [];
|
||||
|
||||
// If the chains property is present, add the chain data to the models
|
||||
models.add(WCConnectionModel(title: S.current.accounts, elements: ns.accounts));
|
||||
models.add(WCConnectionModel(title: S.current.methods, elements: ns.methods));
|
||||
|
||||
if (ns.events.isNotEmpty) {
|
||||
models.add(WCConnectionModel(title: S.current.events, elements: ns.events));
|
||||
}
|
||||
|
||||
views.add(WCConnectionWidget(title: key, info: models));
|
||||
}
|
||||
|
||||
return views;
|
||||
}
|
||||
|
||||
static Map<String, Namespace> updateNamespaces(
|
||||
Map<String, Namespace> currentNamespaces,
|
||||
String namespace,
|
||||
List<String> newChains,
|
||||
) {
|
||||
final updatedNamespaces = Map<String, Namespace>.from(currentNamespaces);
|
||||
|
||||
final accounts = currentNamespaces[namespace]!.accounts;
|
||||
final address = NamespaceUtils.getAccount(accounts.first);
|
||||
final newAccounts = newChains.map((c) => '$c:$address').toList();
|
||||
|
||||
final newNamespaces = currentNamespaces[namespace]!.copyWith(
|
||||
chains: NamespaceUtils.getChainsFromAccounts(accounts)..addAll(newChains),
|
||||
accounts: List<String>.from(accounts)..addAll(newAccounts),
|
||||
);
|
||||
|
||||
updatedNamespaces[namespace] = newNamespaces;
|
||||
|
||||
return updatedNamespaces;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
|
||||
class WCPermission {
|
||||
const WCPermission({required this.iconUrl, required this.label});
|
||||
|
||||
final String iconUrl;
|
||||
final String label;
|
||||
}
|
||||
|
||||
class WCPermissionsMapper {
|
||||
static const _transactionMethods = {
|
||||
'eth_sendTransaction',
|
||||
'eth_signTransaction',
|
||||
'solana_signTransaction',
|
||||
'solana_signAllTransactions',
|
||||
'solana_signAndSendTransaction',
|
||||
};
|
||||
|
||||
static const _messageSigningMethods = {
|
||||
'personal_sign',
|
||||
'eth_sign',
|
||||
'eth_signTypedData',
|
||||
'eth_signTypedData_v1',
|
||||
'eth_signTypedData_v3',
|
||||
'eth_signTypedData_v4',
|
||||
'solana_signMessage',
|
||||
};
|
||||
|
||||
static const _chainAdminMethods = {
|
||||
'wallet_switchEthereumChain',
|
||||
'wallet_addEthereumChain',
|
||||
};
|
||||
|
||||
static List<WCPermission> fromGeneratedNamespaces(Map<String, Namespace> generatedNamespaces) {
|
||||
final methods = <String>{};
|
||||
for (final ns in generatedNamespaces.values) {
|
||||
methods.addAll(ns.methods);
|
||||
}
|
||||
|
||||
final permissions = <WCPermission>[
|
||||
WCPermission(iconUrl: "assets/new-ui/global_view.svg", label: S.current.wc_permission_view_balance),
|
||||
];
|
||||
|
||||
final wantsTransactionApproval = methods.any(_transactionMethods.contains);
|
||||
if (wantsTransactionApproval) {
|
||||
permissions.add(WCPermission(
|
||||
iconUrl: "assets/new-ui/green_check.svg",
|
||||
label: S.current.wc_permission_request_approval,
|
||||
));
|
||||
}
|
||||
|
||||
final wantsMessageSigning = methods.any(_messageSigningMethods.contains);
|
||||
if (wantsMessageSigning) {
|
||||
permissions.add(WCPermission(
|
||||
iconUrl: "assets/new-ui/pencil.svg",
|
||||
label: S.current.wc_permission_sign_messages,
|
||||
));
|
||||
}
|
||||
|
||||
final wantsChainAdmin = methods.any(_chainAdminMethods.contains);
|
||||
if (wantsChainAdmin) {
|
||||
permissions.add(WCPermission(
|
||||
iconUrl: "assets/new-ui/exchange_providers.svg",
|
||||
label: S.current.wc_permission_switch_chains,
|
||||
));
|
||||
}
|
||||
|
||||
final known = {..._transactionMethods, ..._messageSigningMethods, ..._chainAdminMethods};
|
||||
final unknown = methods.where((m) => !known.contains(m)).toList()..sort();
|
||||
for (final method in unknown) {
|
||||
permissions.add(WCPermission(
|
||||
iconUrl: "assets/new-ui/help.svg",
|
||||
label: S.current.wc_permission_other(method),
|
||||
));
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +1,86 @@
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/enter_wallet_connect_uri_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_hero_card.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/utils/permission_handler.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/utils/permission_handler.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'widgets/wc_pairing_item_widget.dart';
|
||||
import 'wc_pairing_detail_page.dart';
|
||||
|
||||
class WalletConnectConnectionsView extends StatelessWidget {
|
||||
final WalletKitService walletKitService;
|
||||
|
||||
WalletConnectConnectionsView({required this.walletKitService, Uri? launchUri, Key? key})
|
||||
: super(key: key) {
|
||||
WalletConnectConnectionsView({
|
||||
required this.walletKitService,
|
||||
Uri? launchUri,
|
||||
Key? key,
|
||||
}) : super(key: key) {
|
||||
_triggerPairingFromDeeplink(launchUri);
|
||||
}
|
||||
|
||||
final WalletKitService walletKitService;
|
||||
|
||||
void _triggerPairingFromDeeplink(Uri? launchUri) async {
|
||||
if (launchUri == null) return;
|
||||
|
||||
if(launchUri.scheme == "wc") {
|
||||
if (launchUri.scheme == 'wc') {
|
||||
await walletKitService.pairWithUri(launchUri);
|
||||
return;
|
||||
}
|
||||
|
||||
final actualLinkList = launchUri.query.split("uri=");
|
||||
|
||||
final actualLinkList = launchUri.query.split('uri=');
|
||||
if (actualLinkList.length <= 1) return;
|
||||
|
||||
final query = actualLinkList[1];
|
||||
|
||||
final decoded = Uri.decodeComponent(query).trim();
|
||||
|
||||
final decoded = Uri.decodeComponent(actualLinkList[1]).trim();
|
||||
final sanitized = decoded.startsWith('@') ? decoded.substring(1) : decoded;
|
||||
|
||||
final uriData = Uri.tryParse(sanitized);
|
||||
|
||||
if (uriData == null || (uriData.scheme.isEmpty)) return;
|
||||
if (uriData == null || uriData.scheme.isEmpty) return;
|
||||
|
||||
await walletKitService.pairWithUri(uriData);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WCPairingsWidget(walletKitService: walletKitService);
|
||||
}
|
||||
}
|
||||
|
||||
class WCPairingsWidget extends BasePage {
|
||||
WCPairingsWidget({required this.walletKitService, Key? key})
|
||||
: walletKit = walletKitService.walletKit;
|
||||
|
||||
final ReownWalletKit walletKit;
|
||||
final WalletKitService walletKitService;
|
||||
|
||||
@override
|
||||
String get title => S.current.walletConnect;
|
||||
|
||||
Future<void> _onScanQrCode(BuildContext context, ReownWalletKit web3Wallet) async {
|
||||
final String? uri;
|
||||
|
||||
if (DeviceInfo.instance.isMobile) {
|
||||
bool isCameraPermissionGranted =
|
||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||
if (!isCameraPermissionGranted) return;
|
||||
uri = await presentQRScanner(context);
|
||||
} else {
|
||||
uri = await _showEnterWalletConnectURIPopUp(context);
|
||||
}
|
||||
Future<void> _onScanQrCode(BuildContext context) async {
|
||||
final isCameraPermissionGranted =
|
||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||
if (!isCameraPermissionGranted) return;
|
||||
|
||||
final uri = await presentQRScanner(context);
|
||||
await _handleWalletConnectURI(uri, context);
|
||||
}
|
||||
|
||||
Future<String?> _showEnterWalletConnectURIPopUp(BuildContext context) async {
|
||||
final walletConnectURI = await showPopUp<String>(
|
||||
Future<void> _onPasteLink(BuildContext context) async {
|
||||
final uri = await showPopUp<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return EnterWalletConnectURIWrapperWidget();
|
||||
},
|
||||
builder: (BuildContext context) => EnterWalletConnectURIWrapperWidget(),
|
||||
);
|
||||
return walletConnectURI;
|
||||
await _handleWalletConnectURI(uri, context);
|
||||
}
|
||||
|
||||
Future<void> _handleWalletConnectURI(
|
||||
String? walletConnectURI,
|
||||
BuildContext context,
|
||||
) async {
|
||||
Future<void> _handleWalletConnectURI(String? walletConnectURI, BuildContext context) async {
|
||||
if (walletConnectURI == null) return _invalidUriToast(context, S.current.nullURIError);
|
||||
|
||||
log('_onFoundUri: $walletConnectURI');
|
||||
// Accept either a raw WC URI or a full URL containing `uri=` parameter
|
||||
String input = walletConnectURI.trim();
|
||||
|
||||
if (input.contains('uri=')) {
|
||||
final parts = input.split('uri=');
|
||||
if (parts.length > 1) {
|
||||
input = Uri.decodeComponent(parts.last);
|
||||
}
|
||||
if (parts.length > 1) input = Uri.decodeComponent(parts.last);
|
||||
}
|
||||
if (input.startsWith('@')) input = input.substring(1);
|
||||
|
||||
// Some scanners may prefix with '@', strip it
|
||||
if (input.startsWith('@')) {
|
||||
input = input.substring(1);
|
||||
}
|
||||
final Uri? uriData = Uri.tryParse(input);
|
||||
final bool hasValidScheme = uriData != null && uriData.scheme.isNotEmpty;
|
||||
if (!hasValidScheme) {
|
||||
final uriData = Uri.tryParse(input);
|
||||
if (uriData == null || uriData.scheme.isEmpty) {
|
||||
return _invalidUriToast(context, S.current.invalid_input);
|
||||
}
|
||||
|
||||
await walletKitService.pairWithUri(uriData);
|
||||
}
|
||||
|
||||
@@ -135,88 +99,132 @@ class WCPairingsWidget extends BasePage {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 24),
|
||||
Text(
|
||||
S.current.connectWalletPrompt,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
PrimaryButton(
|
||||
text: S.current.newConnection,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: () => _onScanQrCode(context, walletKit),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final uri = await _showEnterWalletConnectURIPopUp(context);
|
||||
await _handleWalletConnectURI(uri, context);
|
||||
},
|
||||
child: Text(
|
||||
'Click to paste WalletConnect Link',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Visibility(
|
||||
visible: walletKitService.pairings.isEmpty,
|
||||
child: Center(
|
||||
child: Text(
|
||||
S.current.activeConnectionsPrompt,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
replacement: ListView.builder(
|
||||
itemCount: walletKitService.pairings.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final pairing = walletKitService.pairings[index];
|
||||
return WCPairingItemWidget(
|
||||
key: ValueKey(pairing.topic),
|
||||
pairing: pairing,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WalletConnectPairingDetailsPage(
|
||||
pairing: pairing,
|
||||
walletKitService: walletKitService,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 48),
|
||||
],
|
||||
);
|
||||
},
|
||||
void _openPairingDetails(BuildContext context, PairingInfo pairing) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => WalletConnectPairingDetailsPage(
|
||||
pairing: pairing,
|
||||
walletKitService: walletKitService,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
final isMobile = DeviceInfo.instance.isMobile;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colors.surface,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ModernButton(
|
||||
size: 40,
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
icon: Icon(Icons.arrow_back_ios_new, size: 16),
|
||||
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const WCHeroCard(),
|
||||
const SizedBox(height: 24),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (walletKitService.pairings.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Center(
|
||||
child: Text(
|
||||
S.of(context).activeConnectionsPrompt,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final items = <ListItemRegularRow>[];
|
||||
for (final pairing in walletKitService.pairings) {
|
||||
final metadata = pairing.peerMetadata;
|
||||
if (metadata == null) continue;
|
||||
items.add(
|
||||
ListItemRegularRow(
|
||||
keyValue: pairing.topic,
|
||||
label: metadata.name,
|
||||
subtitle: metadata.url,
|
||||
iconPath: metadata.icons.isNotEmpty
|
||||
? metadata.icons.first
|
||||
: 'assets/new-ui/walletconnect_icon.svg',
|
||||
onTap: () => _openPairingDetails(context, pairing),
|
||||
leadingIconSize: 36,
|
||||
leadingIconErrorWidget: CakeImageWidget(
|
||||
imageUrl: 'assets/new-ui/walletconnect_icon.svg',
|
||||
width: 36,
|
||||
height: 36,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (items.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Center(
|
||||
child: Text(
|
||||
S.of(context).activeConnectionsPrompt,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return NewListSections(sections: {'': items});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
NewPrimaryButton(
|
||||
onPressed: () => _onPasteLink(context),
|
||||
text: S.of(context).wc_paste_link,
|
||||
color: colors.surfaceContainerHigh,
|
||||
textColor: colors.primary,
|
||||
),
|
||||
if (isMobile) ...[
|
||||
const SizedBox(height: 12),
|
||||
NewPrimaryButton(
|
||||
onPressed: () => _onScanQrCode(context),
|
||||
text: S.of(context).wc_scan_qr,
|
||||
color: colors.primary,
|
||||
textColor: colors.onPrimary,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/utils/wc_permissions_mapper.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_dapp_card.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_permissions_card.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_wallet_card.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
|
||||
import 'utils/namespace_model_builder.dart';
|
||||
|
||||
class WalletConnectPairingDetailsPage extends StatefulWidget {
|
||||
final PairingInfo pairing;
|
||||
final WalletKitService walletKitService;
|
||||
@@ -25,180 +28,100 @@ class WalletConnectPairingDetailsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class WalletConnectPairingDetailsPageState extends State<WalletConnectPairingDetailsPage> {
|
||||
List<Widget> sessionWidgets = [];
|
||||
late String expiryDate;
|
||||
List<SessionData> sessions = const [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initDateTime();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
initSessions();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
sessions = widget.walletKitService.getSessionsForPairingInfo(widget.pairing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void initDateTime() {
|
||||
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(widget.pairing.expiry * 1000);
|
||||
int year = dateTime.year;
|
||||
int month = dateTime.month;
|
||||
int day = dateTime.day;
|
||||
|
||||
expiryDate = '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
void initSessions() {
|
||||
List<SessionData> sessions = widget.walletKitService.getSessionsForPairingInfo(widget.pairing);
|
||||
|
||||
for (final SessionData session in sessions) {
|
||||
List<Widget> namespaceWidget = ConnectionWidgetBuilder.buildFromNamespaces(
|
||||
session.topic,
|
||||
session.namespaces,
|
||||
context,
|
||||
);
|
||||
// Loop through and add the namespace widgets, but put 20 pixels between each one
|
||||
for (int i = 0; i < namespaceWidget.length; i++) {
|
||||
sessionWidgets.add(namespaceWidget[i]);
|
||||
if (i != namespaceWidget.length - 1) {
|
||||
sessionWidgets.add(const SizedBox(height: 20.0));
|
||||
}
|
||||
}
|
||||
|
||||
sessionWidgets.add(const SizedBox.square(dimension: 10.0));
|
||||
sessionWidgets.add(
|
||||
PrimaryButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
await widget.walletKitService.extendSession(
|
||||
topic: session.topic,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('${e.toString()}');
|
||||
}
|
||||
},
|
||||
text: S.current.extend_session,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
sessionWidgets.add(const SizedBox.square(dimension: 10.0));
|
||||
sessionWidgets.add(
|
||||
PrimaryButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
await widget.walletKitService.updateSession(
|
||||
topic: session.topic,
|
||||
namespaces: session.namespaces,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('${e.toString()}');
|
||||
}
|
||||
},
|
||||
text: S.current.update_session,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
sessionWidgets.add(const SizedBox.square(dimension: 10.0));
|
||||
sessionWidgets.add(
|
||||
PrimaryButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
await widget.walletKitService.disconnectSession(
|
||||
topic: session.topic,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('${e.toString()}');
|
||||
}
|
||||
},
|
||||
text: S.current.disconnect_session,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
}
|
||||
final dateTime = DateTime.fromMillisecondsSinceEpoch(widget.pairing.expiry * 1000);
|
||||
expiryDate = '${dateTime.year}-'
|
||||
'${dateTime.month.toString().padLeft(2, '0')}-'
|
||||
'${dateTime.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WCCDetailsWidget(
|
||||
widget.pairing,
|
||||
expiryDate,
|
||||
sessionWidgets,
|
||||
widget.walletKitService,
|
||||
pairing: widget.pairing,
|
||||
expiryDate: expiryDate,
|
||||
sessions: sessions,
|
||||
walletKitService: widget.walletKitService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WCCDetailsWidget extends BasePage {
|
||||
WCCDetailsWidget(
|
||||
this.pairing,
|
||||
this.expiryDate,
|
||||
this.sessionWidgets,
|
||||
this.walletKitService,
|
||||
);
|
||||
WCCDetailsWidget({
|
||||
required this.pairing,
|
||||
required this.expiryDate,
|
||||
required this.sessions,
|
||||
required this.walletKitService,
|
||||
});
|
||||
|
||||
final PairingInfo pairing;
|
||||
final String expiryDate;
|
||||
final List<Widget> sessionWidgets;
|
||||
final List<SessionData> sessions;
|
||||
final WalletKitService walletKitService;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: CircleAvatar(
|
||||
backgroundImage: (pairing.peerMetadata!.icons.isNotEmpty
|
||||
? NetworkImage(pairing.peerMetadata!.icons[0])
|
||||
: AssetImage(
|
||||
CakeTor.instance!.enabled
|
||||
? 'assets/images/tor_logo.svg'
|
||||
: 'assets/images/app_logo.png')) as ImageProvider<Object>,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
Text(
|
||||
pairing.peerMetadata!.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
pairing.peerMetadata!.url,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${S.current.expiresOn}: $expiryDate',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: sessionWidgets,
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
PrimaryButton(
|
||||
onPressed: () =>
|
||||
_onDeleteButtonPressed(context, pairing.peerMetadata!.name, walletKitService),
|
||||
text: S.current.delete,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final metadata = pairing.peerMetadata;
|
||||
if (metadata == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final iconUrl = metadata.icons.isNotEmpty ? metadata.icons.first : null;
|
||||
final walletName = getIt.get<AppStore>().wallet?.name ?? '';
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
WCDappCard(
|
||||
name: metadata.name,
|
||||
iconUrl: iconUrl,
|
||||
subtitle: metadata.url,
|
||||
action: WCDappCardAction.connected,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'${S.of(context).expiresOn}: $expiryDate',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
for (final session in sessions) ...[
|
||||
_SessionSection(
|
||||
session: session,
|
||||
walletName: walletName,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
NewPrimaryButton(
|
||||
onPressed: () => _onDeleteButtonPressed(context, metadata.name, walletKitService),
|
||||
text: S.current.delete,
|
||||
color: colors.error,
|
||||
textColor: colors.onError,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -240,3 +163,37 @@ class WCCDetailsWidget extends BasePage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SessionSection extends StatelessWidget {
|
||||
const _SessionSection({
|
||||
required this.session,
|
||||
required this.walletName,
|
||||
});
|
||||
|
||||
final SessionData session;
|
||||
final String walletName;
|
||||
|
||||
String _firstAddress() {
|
||||
for (final namespace in session.namespaces.values) {
|
||||
if (namespace.accounts.isNotEmpty) {
|
||||
return NamespaceUtils.getAccount(namespace.accounts.first);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final permissions = WCPermissionsMapper.fromGeneratedNamespaces(session.namespaces);
|
||||
final address = _firstAddress();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
WCWalletCard(walletName: walletName, address: address),
|
||||
const SizedBox(height: 24),
|
||||
WCPermissionsCard(permissions: permissions),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,15 +75,15 @@ class EnterWalletConnectURIWidget extends BaseAlertDialog {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
suffixIcon: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
width: 36,
|
||||
height: 36,
|
||||
padding: EdgeInsets.only(top: 0),
|
||||
child: Semantics(
|
||||
child: Semantics(
|
||||
label: S.of(context).paste,
|
||||
child: InkWell(
|
||||
onTap: () => _pasteWalletConnectURI(),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WCConnectionItemWidget extends StatelessWidget {
|
||||
const WCConnectionItemWidget({required this.model, Key? key}) : super(key: key);
|
||||
|
||||
final WCConnectionModel model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsetsDirectional.only(top: 8),
|
||||
child: Visibility(
|
||||
visible: model.elements != null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
model.title ?? '',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (model.elements != null)
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
direction: Axis.horizontal,
|
||||
children: model.elements!
|
||||
.map((e) => _ModelElementWidget(model: model, modelElement: e))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
replacement: _NoModelElementWidget(model: model),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoModelElementWidget extends StatelessWidget {
|
||||
const _NoModelElementWidget({required this.model});
|
||||
|
||||
final WCConnectionModel model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
model.text!,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ModelElementWidget extends StatelessWidget {
|
||||
const _ModelElementWidget({
|
||||
required this.model,
|
||||
required this.modelElement,
|
||||
});
|
||||
|
||||
final WCConnectionModel model;
|
||||
final String modelElement;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: model.elementActions != null ? model.elementActions![modelElement] : null,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
modelElement,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 50,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/confirm_swiper.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/utils/wc_permissions_mapper.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_dapp_card.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_permissions_card.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_scam_banner.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_sheet_header.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_wallet_card.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
|
||||
class WCConnectionRequestSheet extends StatelessWidget {
|
||||
const WCConnectionRequestSheet({
|
||||
super.key,
|
||||
required this.proposalData,
|
||||
required this.requester,
|
||||
required this.walletKeyService,
|
||||
required this.appStore,
|
||||
this.verifyContext,
|
||||
});
|
||||
|
||||
final ProposalData proposalData;
|
||||
final ConnectionMetadata requester;
|
||||
final WalletConnectKeyService walletKeyService;
|
||||
final AppStore appStore;
|
||||
final VerifyContext? verifyContext;
|
||||
|
||||
String _resolveAddress() {
|
||||
final wallet = appStore.wallet;
|
||||
if (wallet == null) return '';
|
||||
final keys = walletKeyService.getKeysForChain(wallet);
|
||||
if (keys.isEmpty) return '';
|
||||
return keys.first.publicKey;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final permissions =
|
||||
WCPermissionsMapper.fromGeneratedNamespaces(proposalData.generatedNamespaces ?? const {});
|
||||
|
||||
final metadata = requester.metadata;
|
||||
final iconUrl = metadata.icons.isNotEmpty ? metadata.icons.first : null;
|
||||
final walletName = appStore.wallet?.name ?? '';
|
||||
final address = _resolveAddress();
|
||||
final isScam = verifyContext?.validation.scam ?? false;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
WCSheetHeader(title: S.of(context).wc_connect_request_title),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
WCDappCard(
|
||||
name: metadata.name,
|
||||
iconUrl: iconUrl,
|
||||
subtitle: metadata.url,
|
||||
action: WCDappCardAction.connect,
|
||||
verifyContext: verifyContext,
|
||||
),
|
||||
if (isScam) ...[
|
||||
const SizedBox(height: 16),
|
||||
const WCScamBanner(),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
WCWalletCard(walletName: walletName, address: address),
|
||||
const SizedBox(height: 24),
|
||||
WCPermissionsCard(permissions: permissions),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: ConfirmSwiper(
|
||||
swiperText: S.of(context).wc_swipe_to_approve,
|
||||
onConfirmed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop(WCBottomSheetResult.one);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/utils/namespace_model_builder.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_widget.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
|
||||
class WCConnectionRequestWidget extends StatelessWidget {
|
||||
WCConnectionRequestWidget({
|
||||
this.sessionAuthPayload,
|
||||
this.proposalData,
|
||||
this.requester,
|
||||
this.verifyContext,
|
||||
required this.walletKeyService,
|
||||
required this.walletKit,
|
||||
required this.appStore,
|
||||
});
|
||||
|
||||
final SessionAuthPayload? sessionAuthPayload;
|
||||
final ProposalData? proposalData;
|
||||
final ConnectionMetadata? requester;
|
||||
final VerifyContext? verifyContext;
|
||||
final WalletConnectKeyService walletKeyService;
|
||||
final AppStore appStore;
|
||||
final ReownWalletKit walletKit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (requester == null) {
|
||||
return Text(S.current.error.toUpperCase());
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${requester!.metadata.name} ${S.current.wouoldLikeToConnect}',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
(sessionAuthPayload != null)
|
||||
? _buildSessionAuthRequestView()
|
||||
: _buildSessionProposalView(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSessionAuthRequestView() {
|
||||
final cacaoPayload = CacaoRequestPayload.fromSessionAuthPayload(
|
||||
sessionAuthPayload!,
|
||||
);
|
||||
|
||||
final List<WCConnectionModel> messagesModels = [];
|
||||
for (var chain in sessionAuthPayload!.chains) {
|
||||
final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
|
||||
final iss = 'did:pkh:$chain:${chainKeys.first.publicKey}';
|
||||
|
||||
final message = walletKit.formatAuthMessage(
|
||||
iss: iss,
|
||||
cacaoPayload: cacaoPayload,
|
||||
);
|
||||
|
||||
messagesModels.add(
|
||||
WCConnectionModel(
|
||||
title: '${S.current.message} ${messagesModels.length + 1}',
|
||||
elements: [message],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return WCConnectionWidget(
|
||||
title: '${messagesModels.length} ${S.current.messages}',
|
||||
info: messagesModels,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSessionProposalView(BuildContext context) {
|
||||
// Create the connection models using the required and optional namespaces provided by the proposal data
|
||||
|
||||
final views = ConnectionWidgetBuilder.buildFromRequiredNamespaces(
|
||||
proposalData!.generatedNamespaces ?? {},
|
||||
);
|
||||
|
||||
return Column(children: views);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'wc_connection_item_widget.dart';
|
||||
|
||||
class WCConnectionWidget extends StatelessWidget {
|
||||
const WCConnectionWidget({required this.title, required this.info, super.key});
|
||||
|
||||
final String title;
|
||||
final List<WCConnectionModel> info;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...info.map((e) => WCConnectionItemWidget(model: e)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/themes/core/custom_theme_colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
|
||||
enum WCDappCardAction { connect, sign, connected }
|
||||
|
||||
class WCDappCard extends StatelessWidget {
|
||||
const WCDappCard({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.iconUrl,
|
||||
required this.subtitle,
|
||||
required this.action,
|
||||
this.verifyContext,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String? iconUrl;
|
||||
final String subtitle;
|
||||
final WCDappCardAction action;
|
||||
final VerifyContext? verifyContext;
|
||||
|
||||
String _actionLine(BuildContext context) {
|
||||
switch (action) {
|
||||
case WCDappCardAction.connect:
|
||||
return S.of(context).wc_would_like_to_connect_to(name);
|
||||
case WCDappCardAction.sign:
|
||||
return S.of(context).wc_would_like_to_sign(name);
|
||||
case WCDappCardAction.connected:
|
||||
return S.of(context).wc_connected_to(name);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: CakeImageWidget(
|
||||
borderRadius: 16,
|
||||
imageUrl: iconUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: CakeImageWidget(
|
||||
imageUrl: 'assets/new-ui/walletconnect_icon.svg',
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
_actionLine(context),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
if (subtitle.trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_shouldShowBadge()) ...[
|
||||
const SizedBox(height: 10),
|
||||
_VerifyBadge(verifyContext: verifyContext!),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _shouldShowBadge() {
|
||||
if (verifyContext == null) return false;
|
||||
if (verifyContext!.validation.scam) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class _VerifyBadge extends StatelessWidget {
|
||||
const _VerifyBadge({required this.verifyContext});
|
||||
|
||||
final VerifyContext verifyContext;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String label;
|
||||
|
||||
if (verifyContext.validation.valid) {
|
||||
icon = Icons.check_circle;
|
||||
color = CustomThemeColors.syncGreen;
|
||||
label = S.of(context).wc_verified;
|
||||
} else {
|
||||
icon = Icons.warning_amber_rounded;
|
||||
color = CustomThemeColors.syncYellow;
|
||||
label = S.of(context).wc_not_verified;
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: color),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WCHeroCard extends StatelessWidget {
|
||||
const WCHeroCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: 'assets/new-ui/walletconnect_icon.svg',
|
||||
width: 36,
|
||||
height: 36,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
S.of(context).walletConnect,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.08,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
S.of(context).wc_pairing_list_header_subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
letterSpacing: -0.06,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WCMessageRow {
|
||||
const WCMessageRow({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
}
|
||||
|
||||
class WCMessageCard extends StatelessWidget {
|
||||
const WCMessageCard({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.title,
|
||||
this.extraRows = const [],
|
||||
});
|
||||
|
||||
final String? title;
|
||||
final String message;
|
||||
final List<WCMessageRow> extraRows;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final resolvedTitle = title ?? S.of(context).wc_message_to_sign;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
resolvedTitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
if (message.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
for (final row in extraRows) ...[
|
||||
const SizedBox(height: 12),
|
||||
Divider(height: 1, color: colors.outlineVariant),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
row.label,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: colors.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
row.value,
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
|
||||
class WCPairingItemWidget extends StatelessWidget {
|
||||
const WCPairingItemWidget({required this.pairing, required this.onTap, super.key});
|
||||
|
||||
final PairingInfo pairing;
|
||||
final void Function() onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
PairingMetadata? metadata = pairing.peerMetadata;
|
||||
|
||||
if (metadata == null) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(pairing.expiry * 1000);
|
||||
int year = dateTime.year;
|
||||
int month = dateTime.month;
|
||||
int day = dateTime.day;
|
||||
|
||||
String expiryDate =
|
||||
'$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: CakeImageWidget(
|
||||
borderRadius: 8,
|
||||
width: 60,
|
||||
height: 60,
|
||||
imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0] : null,
|
||||
errorWidget: CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/images/walletconnect_logo.png'),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
metadata.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
metadata.url,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${S.current.expiresOn}: $expiryDate',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 44,
|
||||
height: 40,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/utils/wc_permissions_mapper.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WCPermissionsCard extends StatelessWidget {
|
||||
const WCPermissionsCard({super.key, required this.permissions});
|
||||
|
||||
final List<WCPermission> permissions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
if (permissions.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < permissions.length; i++) ...[
|
||||
if (i > 0) Divider(height: 1, color: colors.outlineVariant, indent: 44),
|
||||
_PermissionRow(permission: permissions[i]),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PermissionRow extends StatelessWidget {
|
||||
const _PermissionRow({required this.permission});
|
||||
|
||||
final WCPermission permission;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
child: CakeImageWidget(
|
||||
imageUrl: permission.iconUrl,
|
||||
width: 28,
|
||||
height: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
permission.label,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_verify_context_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
|
||||
class WCRequestWidget extends StatelessWidget {
|
||||
WCRequestWidget({
|
||||
required this.child,
|
||||
this.verifyContext,
|
||||
this.onAccept,
|
||||
this.onReject,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final VerifyContext? verifyContext;
|
||||
final VoidCallback? onAccept;
|
||||
final VoidCallback? onReject;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
WCVerifyContextWidget(
|
||||
verifyContext: verifyContext,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(child: child),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: onReject ??
|
||||
() {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop(WCBottomSheetResult.reject);
|
||||
}
|
||||
},
|
||||
text: S.current.reject,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
textColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: onAccept ??
|
||||
() {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop(WCBottomSheetResult.one);
|
||||
}
|
||||
},
|
||||
text: S.current.approve,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WCScamBanner extends StatelessWidget {
|
||||
const WCScamBanner({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.errorContainer,
|
||||
border: Border.all(color: colors.onError, width: 2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
size: 36,
|
||||
color: colors.error,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).wc_scam_warning_title,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleSmall!.copyWith(
|
||||
color: colors.error,
|
||||
letterSpacing: -0.06,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
S.of(context).wc_scam_warning_message,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: colors.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WCSessionAuthRequestWidget extends StatelessWidget {
|
||||
const WCSessionAuthRequestWidget({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(child: child),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
PrimaryButton(
|
||||
onPressed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop(WCBottomSheetResult.reject);
|
||||
}
|
||||
},
|
||||
text: S.current.cancel,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
textColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
PrimaryButton(
|
||||
onPressed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop(WCBottomSheetResult.one);
|
||||
}
|
||||
},
|
||||
text: S.current.sign_one,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
PrimaryButton(
|
||||
onPressed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop(WCBottomSheetResult.all);
|
||||
}
|
||||
},
|
||||
text: S.current.sign_all,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WCSheetHeader extends StatelessWidget {
|
||||
const WCSheetHeader({super.key, required this.title});
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: InkResponse(
|
||||
radius: 36,
|
||||
onTap: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop(WCBottomSheetResult.reject);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 20,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineMedium!.copyWith(letterSpacing: -0.09),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/confirm_swiper.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_dapp_card.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_message_card.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_scam_banner.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_sheet_header.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_wallet_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
|
||||
class WCSigningRequestSheet extends StatelessWidget {
|
||||
const WCSigningRequestSheet({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.swipeLabel,
|
||||
required this.dappName,
|
||||
required this.message,
|
||||
required this.walletName,
|
||||
required this.address,
|
||||
this.dappIconUrl,
|
||||
this.dappSubtitle,
|
||||
this.verifyContext,
|
||||
this.messageTitle,
|
||||
this.extraRows = const [],
|
||||
this.signAllCount,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String swipeLabel;
|
||||
final String dappName;
|
||||
final String? dappIconUrl;
|
||||
final String? dappSubtitle;
|
||||
final String message;
|
||||
final String? messageTitle;
|
||||
final List<WCMessageRow> extraRows;
|
||||
final String walletName;
|
||||
final String address;
|
||||
final VerifyContext? verifyContext;
|
||||
final int? signAllCount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final showSignAll = (signAllCount ?? 0) > 1;
|
||||
final isScam = verifyContext?.validation.scam ?? false;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
WCSheetHeader(title: title),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
WCDappCard(
|
||||
name: dappName,
|
||||
iconUrl: dappIconUrl,
|
||||
subtitle: dappSubtitle ?? '',
|
||||
action: WCDappCardAction.sign,
|
||||
verifyContext: verifyContext,
|
||||
),
|
||||
if (isScam) ...[
|
||||
const SizedBox(height: 16),
|
||||
const WCScamBanner(),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
WCWalletCard(walletName: walletName, address: address),
|
||||
const SizedBox(height: 24),
|
||||
WCMessageCard(
|
||||
title: messageTitle,
|
||||
message: message,
|
||||
extraRows: extraRows,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: ConfirmSwiper(
|
||||
swiperText: swipeLabel,
|
||||
onConfirmed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop(WCBottomSheetResult.one);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
if (showSignAll) ...[
|
||||
const SizedBox(height: 24),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop(WCBottomSheetResult.all);
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colors.primary,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
),
|
||||
child: Text(S.of(context).wc_sign_all_count(signAllCount!.toString())),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/themes/core/custom_theme_colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reown_walletkit/reown_walletkit.dart';
|
||||
|
||||
class WCVerifyContextWidget extends StatelessWidget {
|
||||
const WCVerifyContextWidget({
|
||||
super.key,
|
||||
required this.verifyContext,
|
||||
});
|
||||
|
||||
final VerifyContext? verifyContext;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (verifyContext == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (verifyContext!.validation.scam) {
|
||||
return VerifyBanner(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
origin: verifyContext!.origin,
|
||||
title: S.current.security_risk,
|
||||
text: S.current.security_risk_description,
|
||||
);
|
||||
}
|
||||
if (verifyContext!.validation.invalid) {
|
||||
return VerifyBanner(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
origin: verifyContext!.origin,
|
||||
title: S.current.domain_mismatch,
|
||||
text: S.current.domain_mismatch_description,
|
||||
);
|
||||
}
|
||||
if (verifyContext!.validation.valid) {
|
||||
return VerifyHeader(
|
||||
iconColor: Theme.of(context).colorScheme.onPrimary,
|
||||
title: verifyContext!.origin,
|
||||
);
|
||||
}
|
||||
return VerifyBanner(
|
||||
color: CustomThemeColors.syncYellow,
|
||||
origin: verifyContext!.origin,
|
||||
title: S.current.cannot_verify,
|
||||
text: S.current.cannot_verify_description,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyHeader extends StatelessWidget {
|
||||
const VerifyHeader({
|
||||
super.key,
|
||||
required this.iconColor,
|
||||
required this.title,
|
||||
});
|
||||
final Color iconColor;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.shield_outlined,
|
||||
color: iconColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: iconColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyBanner extends StatelessWidget {
|
||||
const VerifyBanner({
|
||||
super.key,
|
||||
required this.origin,
|
||||
required this.title,
|
||||
required this.text,
|
||||
required this.color,
|
||||
});
|
||||
final String origin, title, text;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
origin,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox.square(dimension: 8.0),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.2),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
VerifyHeader(
|
||||
iconColor: color,
|
||||
title: title,
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/utils/clipboard_util.dart';
|
||||
import 'package:cake_wallet/utils/show_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class WCWalletCard extends StatelessWidget {
|
||||
const WCWalletCard({
|
||||
super.key,
|
||||
required this.walletName,
|
||||
required this.address,
|
||||
});
|
||||
|
||||
final String walletName;
|
||||
final String address;
|
||||
|
||||
static String _truncate(String value) {
|
||||
if (value.length <= 14) return value;
|
||||
return '${value.substring(0, 6)}...${value.substring(value.length - 7)}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
WalletDetailEntry(label: S.of(context).wallet, value: walletName),
|
||||
Divider(height: 1, color: colors.outlineVariant),
|
||||
WalletDetailEntry(
|
||||
label: S.of(context).address,
|
||||
value: _truncate(address),
|
||||
onTap: address.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
await ClipboardUtil.setSensitiveDataToClipboard(
|
||||
ClipboardData(text: address),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
showBar<void>(context, S.of(context).copied_to_clipboard);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WalletDetailEntry extends StatelessWidget {
|
||||
const WalletDetailEntry({required this.label, required this.value, this.onTap});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:vector_graphics/vector_graphics.dart';
|
||||
|
||||
@@ -15,7 +13,9 @@ class CakeImageWidget extends StatelessWidget {
|
||||
this.errorWidget,
|
||||
this.color,
|
||||
this.colorFilter,
|
||||
this.borderRadius = 24.0, this.alignment, this.allowDrawingOutsideViewBox,
|
||||
this.borderRadius = 24.0,
|
||||
this.alignment,
|
||||
this.allowDrawingOutsideViewBox,
|
||||
});
|
||||
|
||||
final String? imageUrl;
|
||||
@@ -38,8 +38,8 @@ class CakeImageWidget extends StatelessWidget {
|
||||
|
||||
final isSvg = imageUrl!.toLowerCase().endsWith('.svg');
|
||||
final isAsset = imageUrl!.startsWith('assets/');
|
||||
final effectiveColorFilter = colorFilter ??
|
||||
(color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null);
|
||||
final effectiveColorFilter =
|
||||
colorFilter ?? (color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null);
|
||||
|
||||
Widget imageWidget;
|
||||
if (isAsset) {
|
||||
@@ -49,8 +49,7 @@ class CakeImageWidget extends StatelessWidget {
|
||||
width: width,
|
||||
alignment: alignment ?? Alignment.center,
|
||||
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false,
|
||||
colorFilter:
|
||||
effectiveColorFilter,
|
||||
colorFilter: effectiveColorFilter,
|
||||
fit: fit ?? BoxFit.contain, errorBuilder: (context, e, trace) {
|
||||
return SvgPicture.asset(
|
||||
imageUrl!,
|
||||
@@ -59,8 +58,7 @@ class CakeImageWidget extends StatelessWidget {
|
||||
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false,
|
||||
width: width,
|
||||
errorBuilder: (_, __, ___) => SizedBox(height: height, width: width),
|
||||
colorFilter:
|
||||
effectiveColorFilter,
|
||||
colorFilter: effectiveColorFilter,
|
||||
fit: fit ?? BoxFit.contain,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,9 @@ class ListItemRegularRowWidget extends StatelessWidget {
|
||||
this.trailingIconSize,
|
||||
this.bottomWidget,
|
||||
this.trailingWidget,
|
||||
this.copyableText
|
||||
this.copyableText,
|
||||
this.leadingIconErrorWidget,
|
||||
this.leadingIconSize,
|
||||
});
|
||||
|
||||
final String keyValue;
|
||||
@@ -42,6 +44,8 @@ class ListItemRegularRowWidget extends StatelessWidget {
|
||||
final Color? foregroundColor;
|
||||
final double? trailingIconSize;
|
||||
final String? copyableText;
|
||||
final Widget? leadingIconErrorWidget;
|
||||
final double? leadingIconSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -78,8 +82,9 @@ class ListItemRegularRowWidget extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: CakeImageWidget(
|
||||
imageUrl: iconPath!,
|
||||
width: 24,
|
||||
height: 24,
|
||||
width: leadingIconSize ?? 24,
|
||||
height: leadingIconSize ?? 24,
|
||||
errorWidget: leadingIconErrorWidget,
|
||||
)),
|
||||
Flexible(
|
||||
child: Column(
|
||||
|
||||
@@ -107,6 +107,8 @@ class NewListSections extends StatelessWidget {
|
||||
trailingIconSize: item.trailingIconSize,
|
||||
trailingWidget: item.trailingWidget,
|
||||
bottomWidget: item.bottomWidget,
|
||||
leadingIconErrorWidget: item.leadingIconErrorWidget,
|
||||
leadingIconSize: item.leadingIconSize,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
|
||||
bool _isConnecting = false;
|
||||
|
||||
@override
|
||||
bool get isConnected => false;
|
||||
bool isConnected(WalletType type) => false;
|
||||
|
||||
@override
|
||||
HardwareWalletService getHardwareWalletService(WalletType type) {
|
||||
|
||||
@@ -6,10 +6,11 @@ import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
abstract class HardwareWalletViewModel {
|
||||
HardwareWalletType get hardwareWalletType;
|
||||
bool get isConnected;
|
||||
bool get isBleEnabled;
|
||||
bool get hasBluetooth;
|
||||
|
||||
bool isConnected(WalletType type);
|
||||
|
||||
Future<void> updateBleState();
|
||||
|
||||
Stream<HardwareWalletDevice> scanForBleDevices();
|
||||
|
||||
@@ -114,7 +114,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
if (_isConnecting) return false;
|
||||
_isConnecting = true;
|
||||
_connectingWalletType = type;
|
||||
if (isConnected) {
|
||||
if (isConnected(type)) {
|
||||
try {
|
||||
await _connection!.disconnect().catchError((_) {});
|
||||
} catch (_) {}
|
||||
@@ -126,7 +126,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
|
||||
if (_connectionChangeSubscription == null) {
|
||||
_connectionChangeSubscription = ledger
|
||||
.deviceStateChanges
|
||||
.deviceStateChanges(device.device.id)
|
||||
.listen(_connectionChangeListener);
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isConnected => _connection != null && !(_connection!.isDisconnected);
|
||||
bool isConnected(WalletType type) => _connection != null && !(_connection!.isDisconnected);
|
||||
|
||||
sdk.LedgerConnection get connection => _connection!;
|
||||
|
||||
@@ -196,6 +196,8 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
@override
|
||||
HardwareWalletService getHardwareWalletService(WalletType type) {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return monero!.getLedgerHardwareWalletService(connection);
|
||||
case WalletType.bitcoin:
|
||||
return bitcoin!.getLedgerHardwareWalletService(connection, true);
|
||||
case WalletType.litecoin:
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/main.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
||||
import 'package:cake_wallet/wallet_type_utils.dart';
|
||||
import 'package:cw_core/hardware/device_connection_type.dart';
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:trezor_connect/trezor_connect.dart' as connect_sdk;
|
||||
import 'package:trezor_flutter/trezor_flutter.dart' as sdk;
|
||||
|
||||
part 'trezor_connect_view_model.g.dart';
|
||||
|
||||
class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel;
|
||||
|
||||
abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store {
|
||||
final connect_sdk.TrezorConnect trezorConnect;
|
||||
|
||||
late final sdk.TrezorInterface trezorBLE;
|
||||
late final sdk.TrezorInterface trezorUSB;
|
||||
|
||||
sdk.ThpState _state = sdk.ThpState();
|
||||
|
||||
TrezorConnectViewModelBase(this.trezorConnect) {
|
||||
if (_doesSupportHardwareWallets) {
|
||||
reaction((_) => isBleEnabled, (_) {
|
||||
if (isBleEnabled) _initBLE();
|
||||
});
|
||||
updateBleState();
|
||||
|
||||
if (!Platform.isIOS) {
|
||||
trezorUSB = sdk.TrezorInterface.usb();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool get _doesSupportHardwareWallets {
|
||||
if (isMoneroOnly) {
|
||||
return DeviceConnectionType.supportedConnectionTypes(
|
||||
WalletType.monero, HardwareWalletType.trezor, Platform.isIOS)
|
||||
.isNotEmpty;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _bleIsInitialized = false;
|
||||
|
||||
Future<void> _initBLE() async {
|
||||
if (isBleEnabled && !_bleIsInitialized) {
|
||||
trezorBLE = sdk.TrezorInterface.ble(
|
||||
onPermissionRequest: (_) async {
|
||||
if (Platform.isMacOS) return true;
|
||||
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Permission.bluetoothScan,
|
||||
Permission.bluetoothConnect,
|
||||
Permission.bluetoothAdvertise,
|
||||
].request();
|
||||
|
||||
return statuses.values.where((status) => status.isDenied).isEmpty;
|
||||
},
|
||||
bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)),
|
||||
);
|
||||
_bleIsInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isBleEnabled = false;
|
||||
|
||||
@override
|
||||
bool get hasBluetooth => true;
|
||||
|
||||
@override
|
||||
Future<void> updateBleState() async {
|
||||
final bleState = await sdk.UniversalBle.getBluetoothAvailabilityState();
|
||||
|
||||
final newState = bleState == sdk.AvailabilityState.poweredOn;
|
||||
|
||||
if (newState != isBleEnabled) isBleEnabled = newState;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<HardwareWalletDevice> scanForBleDevices() =>
|
||||
trezorBLE.scan().map((d) => TrezorHardwareWalletDevice(d));
|
||||
|
||||
@override
|
||||
Future<List<HardwareWalletDevice>> getAllUsbDevices() => trezorUSB.devices
|
||||
.then((devices) => devices.map((d) => TrezorHardwareWalletDevice(d)).toList());
|
||||
|
||||
@override
|
||||
Future<void> stopScanning() async {
|
||||
if (_bleIsInitialized) await trezorBLE.stopScanning();
|
||||
if (!Platform.isIOS) await trezorUSB.stopScanning();
|
||||
}
|
||||
|
||||
bool _isConnecting = false;
|
||||
sdk.TrezorClient? _client;
|
||||
|
||||
Completer<String?>? _pinCompleter;
|
||||
|
||||
@observable
|
||||
TrezorParingState paringState = TrezorParingState.initial;
|
||||
|
||||
void setParingPin(String pin) => _pinCompleter?.complete(pin);
|
||||
|
||||
@override
|
||||
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type,
|
||||
[bool isRetry = false]) async {
|
||||
if (!(device is TrezorHardwareWalletDevice)) return false;
|
||||
if (_isConnecting) return false;
|
||||
_isConnecting = true;
|
||||
paringState = TrezorParingState.initial;
|
||||
|
||||
try {
|
||||
final trezorInterface =
|
||||
device.connectionType == HardwareWalletConnectionType.ble ? trezorBLE : trezorUSB;
|
||||
final connection = await trezorInterface.connect(device.device);
|
||||
|
||||
if (!isRetry) {
|
||||
showModalBottomSheet(
|
||||
context: navigatorKey.currentContext!,
|
||||
isScrollControlled: true,
|
||||
isDismissible: false,
|
||||
enableDrag: false,
|
||||
useSafeArea: true,
|
||||
builder: (context) =>
|
||||
HardwareWalletProceedOnDeviceSheet(
|
||||
hardwareWalletType: hardwareWalletType,
|
||||
trezorConnectVM: this,
|
||||
onRetry: () => connectDevice(device, type, true)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> onPinCode() async {
|
||||
_pinCompleter = Completer<String?>();
|
||||
paringState = TrezorParingState.enterPin;
|
||||
|
||||
final res = await _pinCompleter!.future;
|
||||
paringState = TrezorParingState.verifyingPin;
|
||||
if (res == null) throw Exception();
|
||||
return res;
|
||||
}
|
||||
|
||||
final deviceInfo = await _deviceName;
|
||||
|
||||
_client = sdk.TrezorClient.getClientForConnection(
|
||||
connection,
|
||||
_state,
|
||||
"Cake Wallet",
|
||||
deviceInfo,
|
||||
onPinCode,
|
||||
);
|
||||
|
||||
await _client!.createChannel();
|
||||
|
||||
paringState = TrezorParingState.success;
|
||||
return true;
|
||||
} catch (e) {
|
||||
await _client?.connection.disconnect();
|
||||
_client = null;
|
||||
_state = sdk.ThpState();
|
||||
// rethrow;
|
||||
paringState = TrezorParingState.fail(e.toString());
|
||||
printV(e);
|
||||
return false;
|
||||
} finally {
|
||||
_isConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> get _deviceName async {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
if (Platform.isAndroid) return (await deviceInfo.androidInfo).model;
|
||||
if (Platform.isIOS) return (await deviceInfo.iosInfo).name;
|
||||
return "Computer";
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected(WalletType type) => type == WalletType.monero
|
||||
? _client != null && _client?.connection.isDisconnected == false
|
||||
: true;
|
||||
|
||||
@override
|
||||
HardwareWalletService getHardwareWalletService(WalletType type) {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return monero!.getTrezorHardwareWalletService(_client!);
|
||||
case WalletType.bitcoin:
|
||||
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true);
|
||||
case WalletType.litecoin:
|
||||
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false);
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
return evm!.getTrezorHardwareWalletService(trezorConnect);
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> initWallet(WalletBase wallet) async {
|
||||
switch (wallet.type) {
|
||||
case WalletType.monero:
|
||||
return monero!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${wallet.type} for trezor');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
abstract class TrezorParingState {
|
||||
static TrezorParingState initial = InitialTrezorParingState();
|
||||
static TrezorParingState enterPin = EnterPinTrezorParingState();
|
||||
static TrezorParingState verifyingPin = VerifyingPinTrezorParingState();
|
||||
static TrezorParingState success = SuccessTrezorParingState();
|
||||
static TrezorParingState fail(String message) => FailTrezorParingState(message);
|
||||
}
|
||||
|
||||
class InitialTrezorParingState extends TrezorParingState {}
|
||||
|
||||
class EnterPinTrezorParingState extends TrezorParingState {}
|
||||
|
||||
class VerifyingPinTrezorParingState extends TrezorParingState {}
|
||||
|
||||
class SuccessTrezorParingState extends TrezorParingState {}
|
||||
|
||||
class FailTrezorParingState extends TrezorParingState {
|
||||
final String message;
|
||||
|
||||
FailTrezorParingState(this.message);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:trezor_connect/trezor_connect.dart' as sdk;
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'trezor_view_model.g.dart';
|
||||
|
||||
class TrezorViewModel = TrezorViewModelBase with _$TrezorViewModel;
|
||||
|
||||
abstract class TrezorViewModelBase extends HardwareWalletViewModel with Store {
|
||||
final sdk.TrezorConnect trezorConnect;
|
||||
|
||||
TrezorViewModelBase(this.trezorConnect);
|
||||
|
||||
@override
|
||||
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isBleEnabled = false;
|
||||
|
||||
@override
|
||||
bool get hasBluetooth => false;
|
||||
|
||||
@override
|
||||
Future<void> updateBleState() async {}
|
||||
|
||||
@override
|
||||
Stream<HardwareWalletDevice> scanForBleDevices() => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<List<HardwareWalletDevice>> getAllUsbDevices() async => [];
|
||||
|
||||
@override
|
||||
Future<void> stopScanning() async {}
|
||||
|
||||
@override
|
||||
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async => true;
|
||||
|
||||
@override
|
||||
bool get isConnected => true;
|
||||
|
||||
@override
|
||||
HardwareWalletService getHardwareWalletService(WalletType type) {
|
||||
switch (type) {
|
||||
case WalletType.bitcoin:
|
||||
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true);
|
||||
case WalletType.litecoin:
|
||||
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false);
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
return evm!.getTrezorHardwareWalletService(trezorConnect);
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> initWallet(WalletBase wallet) async {
|
||||
switch (wallet.type) {
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
return bitcoin!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${wallet.type} for trezor');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,7 @@ import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
|
||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/utils/permission_handler.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -20,7 +17,7 @@ class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase
|
||||
|
||||
abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
NodeCreateOrEditViewModelBase(
|
||||
this.isPow, this.nodeListViewModel,this.powNodeListViewModel, this.walletType, this._settingsStore, {this.editingNode})
|
||||
this.isPow, this.walletType, this._settingsStore, {this.editingNode})
|
||||
: state = InitialExecutionState(),
|
||||
connectionState = InitialExecutionState(),
|
||||
label = editingNode?.label ?? '',
|
||||
@@ -116,8 +113,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
@observable
|
||||
bool isPow;
|
||||
|
||||
final NodeListViewModel? nodeListViewModel;
|
||||
final PowNodeListViewModel? powNodeListViewModel;
|
||||
|
||||
@computed
|
||||
bool get isReady =>
|
||||
@@ -257,12 +252,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
@action
|
||||
Future<void> delete({required Node editingNode}) async {
|
||||
await editingNode.delete();
|
||||
if(nodeListViewModel != null) {
|
||||
nodeListViewModel!.bindNodes();
|
||||
}
|
||||
if(powNodeListViewModel != null) {
|
||||
powNodeListViewModel!.bindNodes();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -287,12 +276,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||
}
|
||||
|
||||
state = ExecutedSuccessfullyState();
|
||||
if(nodeListViewModel != null) {
|
||||
nodeListViewModel!.bindNodes();
|
||||
}
|
||||
if(powNodeListViewModel != null) {
|
||||
powNodeListViewModel!.bindNodes();
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
state = FailureState(e.toString());
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cw_core/node_list.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cake_wallet/entities/node_list.dart';
|
||||
import 'package:cake_wallet/entities/default_settings_migration.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
@@ -14,11 +16,34 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
|
||||
part 'node_list_view_model.g.dart';
|
||||
|
||||
|
||||
class NodeSpeed {
|
||||
final String iconPath;
|
||||
|
||||
// fraction of best node ping needed to be classified as this speed.
|
||||
final double treshold;
|
||||
|
||||
const NodeSpeed._(this.iconPath, this.treshold);
|
||||
|
||||
static const disconnected = NodeSpeed._(
|
||||
"assets/new-ui/node_speed_badges/disconnected.svg", 0);
|
||||
static const slow = NodeSpeed._(
|
||||
"assets/new-ui/node_speed_badges/slow.svg", 0);
|
||||
static const medium = NodeSpeed._(
|
||||
"assets/new-ui/node_speed_badges/medium.svg", 0.5);
|
||||
static const fast = NodeSpeed._(
|
||||
"assets/new-ui/node_speed_badges/fast.svg", 0.8);
|
||||
|
||||
static const all = [fast, medium, slow, disconnected];
|
||||
}
|
||||
|
||||
|
||||
class NodeListViewModel = NodeListViewModelBase with _$NodeListViewModel;
|
||||
|
||||
abstract class NodeListViewModelBase with Store {
|
||||
NodeListViewModelBase(this._appStore)
|
||||
NodeListViewModelBase(this._appStore, this.isPow)
|
||||
: nodes = ObservableList<Node>(),
|
||||
_nodeSpeeds = ObservableMap<String, int>(),
|
||||
settingsStore = _appStore.settingsStore {
|
||||
bindNodes();
|
||||
|
||||
@@ -37,6 +62,7 @@ abstract class NodeListViewModelBase with Store {
|
||||
bindNodes();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@computed
|
||||
Node get currentNode {
|
||||
@@ -52,7 +78,7 @@ abstract class NodeListViewModelBase with Store {
|
||||
return settingsStore.getCurrentNode(walletType, chainId: chainId);
|
||||
}
|
||||
|
||||
final node = settingsStore.nodes[walletType];
|
||||
final node = isPow ? settingsStore.powNodes[walletType] : settingsStore.nodes[walletType];
|
||||
if (node == null) {
|
||||
throw Exception('No node for wallet type: $walletType');
|
||||
}
|
||||
@@ -73,6 +99,59 @@ abstract class NodeListViewModelBase with Store {
|
||||
final ObservableList<Node> nodes;
|
||||
final SettingsStore settingsStore;
|
||||
final AppStore _appStore;
|
||||
final bool isPow;
|
||||
final ObservableMap<String, int> _nodeSpeeds;
|
||||
|
||||
|
||||
@computed
|
||||
List<Node> get nonCurrentNodes => nodes.where((item)=>item != currentNode).toList();
|
||||
|
||||
@observable
|
||||
bool isTestingNodeSpeed = false;
|
||||
|
||||
Future<void> speedTestNodes() async {
|
||||
try {
|
||||
isTestingNodeSpeed = true;
|
||||
_nodeSpeeds.clear();
|
||||
final nodes = this.nodes.toList();
|
||||
await Future.wait(nodes.map((node) async {
|
||||
final sw = Stopwatch()..start();
|
||||
final res = await node
|
||||
.requestNode()
|
||||
.timeout(const Duration(seconds: 10), onTimeout: () => false);
|
||||
sw.stop();
|
||||
if (res) {
|
||||
_nodeSpeeds[node.uriRaw] = sw.elapsedMilliseconds;
|
||||
}
|
||||
}));
|
||||
} finally {
|
||||
isTestingNodeSpeed = false;
|
||||
}
|
||||
}
|
||||
|
||||
NodeSpeed? nodeSpeedFor(Node node) {
|
||||
final bestSpeed = _bestNodeSpeed;
|
||||
final currentSpeed = _nodeSpeeds[node.uriRaw];
|
||||
if (bestSpeed == null) {
|
||||
return null;
|
||||
}
|
||||
if (currentSpeed == null) {
|
||||
return NodeSpeed.disconnected;
|
||||
}
|
||||
for (final speed in NodeSpeed.all) {
|
||||
if (currentSpeed < (bestSpeed / speed.treshold)) return speed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int? get _bestNodeSpeed {
|
||||
try {
|
||||
printV(_nodeSpeeds);
|
||||
return _nodeSpeeds.values.reduce(min);
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> reset() async {
|
||||
await resetToDefault();
|
||||
@@ -125,7 +204,12 @@ abstract class NodeListViewModelBase with Store {
|
||||
}
|
||||
|
||||
// For non-EVM wallets, use the wallet type directly
|
||||
settingsStore.nodes[walletType] = node;
|
||||
if(isPow) {
|
||||
settingsStore.powNodes[walletType] = node;
|
||||
} else {
|
||||
settingsStore.nodes[walletType] = node;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -150,7 +234,7 @@ abstract class NodeListViewModelBase with Store {
|
||||
}
|
||||
|
||||
// For non-EVM wallets, use the wallet type directly
|
||||
nodes.addAll(await Node.getAllForWalletType(walletType));
|
||||
nodes.addAll(isPow ? await Node.getAllForWalletTypePow(walletType) : await Node.getAllForWalletType(walletType));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/utils/mobx.dart';
|
||||
import 'package:cw_core/node_list.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cake_wallet/entities/node_list.dart';
|
||||
import 'package:cake_wallet/entities/default_settings_migration.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
part 'pow_node_list_view_model.g.dart';
|
||||
|
||||
class PowNodeListViewModel = PowNodeListViewModelBase with _$PowNodeListViewModel;
|
||||
|
||||
abstract class PowNodeListViewModelBase with Store {
|
||||
PowNodeListViewModelBase(this._appStore)
|
||||
: nodes = ObservableList<Node>(),
|
||||
settingsStore = _appStore.settingsStore {
|
||||
bindNodes();
|
||||
|
||||
reaction((_) => _appStore.wallet, (WalletBase? _wallet) {
|
||||
bindNodes();
|
||||
});
|
||||
}
|
||||
|
||||
@computed
|
||||
Node get currentNode {
|
||||
final node = settingsStore.powNodes[_appStore.wallet!.type];
|
||||
|
||||
if (node == null) {
|
||||
throw Exception('No node for wallet type: ${_appStore.wallet!.type}');
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
String getAlertContent(String uri) =>
|
||||
S.current.change_current_node(uri) +
|
||||
'${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance!.enabled ? '' : S.current.orbot_running_alert) : ''}';
|
||||
|
||||
@computed
|
||||
bool get enableAutomaticNodeSwitching => settingsStore.enableAutomaticNodeSwitching;
|
||||
|
||||
@action
|
||||
void setEnableAutomaticNodeSwitching(bool value) =>
|
||||
settingsStore.enableAutomaticNodeSwitching = value;
|
||||
|
||||
final ObservableList<Node> nodes;
|
||||
final SettingsStore settingsStore;
|
||||
final AppStore _appStore;
|
||||
|
||||
Future<void> reset() async {
|
||||
await resetPowToDefault();
|
||||
|
||||
Node node;
|
||||
|
||||
switch (_appStore.wallet!.type) {
|
||||
case WalletType.nano:
|
||||
node = (await getNanoDefaultPowNode())!;
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${_appStore.wallet!.type} for pow node list');
|
||||
}
|
||||
|
||||
await setAsCurrent(node);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> delete(Node node) async => node.delete();
|
||||
|
||||
Future<void> setAsCurrent(Node node) async =>
|
||||
settingsStore.powNodes[_appStore.wallet!.type] = node;
|
||||
|
||||
@action
|
||||
Future<void> bindNodes() async {
|
||||
nodes.clear();
|
||||
nodes.addAll(await Node.getAllPow());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,7 +619,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
if (!(state is IsExecutingState)) state = IsExecutingState();
|
||||
|
||||
if (wallet.isHardwareWallet) {
|
||||
state = IsAwaitingDeviceResponseState();
|
||||
if (walletType == WalletType.monero) {
|
||||
_ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (monero!.getLastLedgerCommand() == "INS_CLSAG") {
|
||||
@@ -627,6 +626,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
state = IsDeviceSigningResponseState();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
state = IsAwaitingDeviceResponseState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,8 +971,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
throw Exception("Pending transaction doesn't exist. It should not be happened.");
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
state = TransactionCommitting();
|
||||
state = wallet.isHardwareWallet && walletType == WalletType.monero
|
||||
? IsAwaitingDeviceResponseState()
|
||||
: TransactionCommitting();
|
||||
|
||||
if (ocpRequest != null) {
|
||||
await _handleOcpRequest();
|
||||
|
||||
@@ -39,8 +39,8 @@ abstract class SupportViewModelBase with Store {
|
||||
LinkListItem(
|
||||
title: 'Discord',
|
||||
icon: 'assets/images/discord.png',
|
||||
linkTitle: 'discord.gg/pwmWa6aFpX',
|
||||
link: 'https://discord.gg/pwmWa6aFpX'),
|
||||
linkTitle: 'discord.gg/2vqYJdXG2H',
|
||||
link: 'https://discord.gg/2vqYJdXG2H'),
|
||||
LinkListItem(
|
||||
title: 'Telegram',
|
||||
icon: 'assets/images/Telegram.png',
|
||||
|
||||
@@ -89,7 +89,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
|
||||
|
||||
credentials = monero!.createMoneroRestoreWalletFromHardwareCredentials(
|
||||
name: name,
|
||||
ledgerConnection: (hardwareWalletVM as LedgerViewModel).connection,
|
||||
hardwareWalletService: hardwareWalletVM.getHardwareWalletService(type),
|
||||
password: password,
|
||||
height: _options['height'] as int? ?? 0,
|
||||
);
|
||||
|
||||
+8
-2
@@ -125,7 +125,7 @@ dependencies:
|
||||
ledger_flutter_plus:
|
||||
git:
|
||||
url: https://github.com/vespr-wallet/ledger-flutter-plus
|
||||
ref: 60817d4b20144f9da9029f5034790272795b9d38
|
||||
ref: 0ccfbe26298e8974dfd4e891f34719c8348562f9
|
||||
trezor_connect:
|
||||
git:
|
||||
url: https://github.com/cake-tech/trezor_connect
|
||||
@@ -203,7 +203,7 @@ dependency_overrides:
|
||||
ledger_flutter_plus:
|
||||
git:
|
||||
url: https://github.com/cake-tech/ledger-flutter-plus
|
||||
ref: 5237e5b3d5f07696eefd0aaee9c32478ed399d20
|
||||
ref: 7bcb00380783f8611f68f5cd63fec96261fea2c2
|
||||
ledger_usb_plus:
|
||||
git:
|
||||
url: https://github.com/konstantinullrich/ledger-usb-plus
|
||||
@@ -227,6 +227,10 @@ dependency_overrides:
|
||||
decimal: ^2.3.3
|
||||
flutter_rust_bridge: 2.11.1
|
||||
flutter_secure_storage_windows: 3.1.2
|
||||
universal_ble:
|
||||
git:
|
||||
url: https://github.com/cake-tech/universal_ble.git
|
||||
ref: f7044558ad111a156987614971e08c583c80175c
|
||||
|
||||
flutter_icons:
|
||||
image_path: "assets/images/app_logo.png"
|
||||
@@ -290,6 +294,8 @@ flutter:
|
||||
- assets/new-ui/card_icons/outline_icons/
|
||||
- assets/new-ui/card_icons/symbol_icons/
|
||||
- assets/new-ui/crypto_full_icons/
|
||||
- assets/new-ui/hardware_wallets/
|
||||
- assets/new-ui/node_speed_badges/
|
||||
|
||||
fonts:
|
||||
- family: Lato
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user