Merge branch 'cake-tech:dev' into dev

This commit is contained in:
BSN ∞/21M
2026-06-02 11:04:26 +02:00
committed by GitHub
158 changed files with 4463 additions and 2188 deletions
+1 -1
View File
@@ -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
+2
View File
@@ -225,3 +225,5 @@ scripts/zcash_lib
scripts/android/yttrium
scripts/bitbox_flutter
scripts/reown_flutter
*.salive
+1 -1
View File
@@ -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
View File
@@ -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) {
+6 -2
View File
@@ -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);
+1 -1
View File
@@ -44,7 +44,7 @@ enum HardwareWalletType {
coldcard,
seedsigner,
keystone,
trezor,
trezor;
}
enum WalletInfoAddressType {
+8
View File
@@ -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);
}
+35
View File
@@ -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;
+26 -12
View File
@@ -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));
}));
}
}
}
+31 -10
View File
@@ -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 {
+53
View File
@@ -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
View File
@@ -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:
+7 -2
View File
@@ -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:
+3 -3
View File
@@ -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:
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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:
+2 -2
View File
@@ -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');
}
}
+1
View File
@@ -295,6 +295,7 @@ SPEC CHECKSUMS:
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
warp_api: 997d8fb3e777669bc6260d0c12e56cd40b9db1d1
YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966
PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a
+1 -1
View File
@@ -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
View File
@@ -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;
}
+11 -2
View File
@@ -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
View File
@@ -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();
}
+9 -4
View File
@@ -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:
+182
View File
@@ -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,
)
],
),
)
],
),
),
),
);
}
}
+7 -2
View File
@@ -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
View File
@@ -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;
}
}
+1 -1
View File
@@ -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,
+95 -102
View File
@@ -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,
),
),
),
],
),
),
);
}
}
+7 -9
View File
@@ -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());
}
}
+6 -2
View File
@@ -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();
+2 -2
View File
@@ -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
View File
@@ -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