Merge remote-tracking branch 'origin/paynyms' into majestic_bank

This commit is contained in:
julian 2023-02-01 16:55:52 -06:00
commit 773c5fad9c
79 changed files with 1469 additions and 553 deletions
assets
crypto_plugins
lib
db
exceptions/main_db
models/isar/models/address
pages
pages_desktop_specific
providers/blockchain/dogecoin
route_generator.dart
services
utilities
widgets
macos/Flutter
pubspec.lockpubspec.yaml
test/widget_tests
windows/flutter

BIN
assets/images/unclaimed.png Normal file

Binary file not shown.

After

(image error) Size: 756 KiB

View file

@ -1,11 +1,18 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_908_22491)">
<g opacity="0.4">
<path d="M22.2 6C23.3297 5.37187 24 4.59422 24 3.75C24 1.67906 19.9688 0 15 0C9.98906 0 6 1.67906 6 3.75C6 4.59422 6.67031 5.37187 7.8 6C7.80937 6.00469 7.81758 6.00937 7.82578 6.01406C7.83398 6.01875 7.84219 6.02344 7.85156 6.02813C8.23125 6.00938 8.61094 6 9 6C11.6344 6 14.0906 6.44062 15.9422 7.21406C16.1203 7.28906 16.2984 7.36875 16.4672 7.44844C18.8062 7.28906 20.8359 6.75469 22.2 6Z" fill="#232323"/>
<path d="M19.9435 12.9151C19.7958 12.9551 19.6477 12.9951 19.5 13.0359V13.5C20.7602 13.5 21.9296 13.8885 22.8951 14.5522C23.5995 14.0172 24 13.4028 24 12.75V11.0906C23.4141 11.5734 22.7063 11.9672 21.9422 12.2859C21.3382 12.5376 20.6447 12.7253 19.9435 12.9151Z" fill="#232323"/>
<path d="M18.3703 8.74688C19.0031 9.37969 19.5 10.2234 19.5 11.25V11.4984C20.4328 11.2734 21.2625 10.9781 21.9469 10.6359C21.9739 10.6209 22.0009 10.6021 22.0279 10.5833C22.0852 10.5432 22.1426 10.5032 22.2 10.5C23.3297 9.87187 24 9.09375 24 8.25V6.59063C23.4141 7.07344 22.7063 7.46719 21.9422 7.78594C20.9109 8.2125 19.6969 8.54063 18.3703 8.74688Z" fill="#232323"/>
<path d="M22.2 6C23.3297 5.37187 24 4.59422 24 3.75C24 1.67906 19.9688 0 15 0C9.98906 0 6 1.67906 6 3.75C6 4.59422 6.67031 5.37187 7.8 6C7.80937 6.00469 7.81758 6.00937 7.82578 6.01406C7.83398 6.01875 7.84219 6.02344 7.85156 6.02813C8.23125 6.00938 8.61094 6 9 6C11.6344 6 14.0906 6.44062 15.9422 7.21406C16.1203 7.28906 16.2984 7.36875 16.4672 7.44844C18.8062 7.28906 20.8359 6.75469 22.2 6Z" fill="#D12B41"/>
<path d="M19.9435 12.9151C19.7958 12.9551 19.6477 12.9951 19.5 13.0359V13.5C20.7602 13.5 21.9296 13.8885 22.8951 14.5522C23.5995 14.0172 24 13.4028 24 12.75V11.0906C23.4141 11.5734 22.7063 11.9672 21.9422 12.2859C21.3382 12.5376 20.6447 12.7253 19.9435 12.9151Z" fill="#D12B41"/>
<path d="M18.3703 8.74688C19.0031 9.37969 19.5 10.2234 19.5 11.25V11.4984C20.4328 11.2734 21.2625 10.9781 21.9469 10.6359C21.9739 10.6209 22.0009 10.6021 22.0279 10.5833C22.0852 10.5432 22.1426 10.5032 22.2 10.5C23.3297 9.87187 24 9.09375 24 8.25V6.59063C23.4141 7.07344 22.7063 7.46719 21.9422 7.78594C20.9109 8.2125 19.6969 8.54063 18.3703 8.74688Z" fill="#D12B41"/>
</g>
<path d="M16.2 13.5C17.3297 12.8719 18 12.0938 18 11.25C18 9.17813 13.9688 7.5 9 7.5C4.02938 7.5 0 9.17813 0 11.25C0 12.0938 0.669375 12.8719 1.79953 13.5C1.85443 13.5031 1.91057 13.5415 1.96782 13.5807C1.9966 13.6004 2.02567 13.6203 2.055 13.6359C3.70594 14.4703 6.20625 15 9 15C11.9438 15 14.5594 14.4094 16.2 13.5Z" fill="#232323"/>
<path d="M14.8788 15.6729C13.1948 16.2046 11.1571 16.5 9 16.5C6.36562 16.5 3.91125 16.0594 2.05922 15.2859C1.29469 14.9672 0.583594 14.5734 0 14.0906V15.75C0 16.5938 0.669375 17.3719 1.79953 18C3.44109 18.9094 6.05625 19.5 9 19.5C10.6471 19.5 12.1916 19.3159 13.5211 18.9937C13.6261 17.7367 14.1186 16.5898 14.8788 15.6729Z" fill="#232323"/>
<path d="M13.5862 20.5191C13.7529 21.4936 14.1547 22.3879 14.731 23.1415C13.1742 23.6778 11.1771 24 9 24C4.02938 24 0 22.3219 0 20.25V18.5906C0.583594 19.0734 1.29469 19.4672 2.05922 19.7859C3.91125 20.5594 6.36562 21 9 21C10.6307 21 12.1932 20.8312 13.5862 20.5191Z" fill="#232323"/>
<path d="M24 19.5C24 21.9844 21.9844 24 19.5 24C17.0156 24 15 21.9844 15 19.5C15 17.0156 17.0156 15 19.5 15C21.9844 15 24 17.0156 24 19.5ZM19 17.4719V18.9719H17.5C17.225 18.9719 17 19.225 17 19.4719C17 19.775 17.225 19.9719 17.5 19.9719H19V21.4719C19 21.775 19.225 21.9719 19.5 21.9719C19.775 21.9719 20 21.775 20 21.4719V19.9719H21.5C21.775 19.9719 22 19.775 22 19.4719C22 19.225 21.775 18.9719 21.5 18.9719H20V17.4719C20 17.225 19.775 16.9719 19.5 16.9719C19.225 16.9719 19 17.225 19 17.4719Z" fill="#232323"/>
<path d="M16.2 13.5C17.3297 12.8719 18 12.0938 18 11.25C18 9.17813 13.9688 7.5 9 7.5C4.02938 7.5 0 9.17813 0 11.25C0 12.0938 0.669375 12.8719 1.79953 13.5C1.85443 13.5031 1.91057 13.5415 1.96782 13.5807C1.9966 13.6004 2.02567 13.6203 2.055 13.6359C3.70594 14.4703 6.20625 15 9 15C11.9438 15 14.5594 14.4094 16.2 13.5Z" fill="#D12B41"/>
<path d="M14.8788 15.6729C13.1948 16.2046 11.1571 16.5 9 16.5C6.36562 16.5 3.91125 16.0594 2.05922 15.2859C1.29469 14.9672 0.583594 14.5734 0 14.0906V15.75C0 16.5938 0.669375 17.3719 1.79953 18C3.44109 18.9094 6.05625 19.5 9 19.5C10.6471 19.5 12.1916 19.3159 13.5211 18.9937C13.6261 17.7367 14.1186 16.5898 14.8788 15.6729Z" fill="#D12B41"/>
<path d="M13.5862 20.5191C13.7529 21.4936 14.1547 22.3879 14.731 23.1415C13.1742 23.6778 11.1771 24 9 24C4.02938 24 0 22.3219 0 20.25V18.5906C0.583594 19.0734 1.29469 19.4672 2.05922 19.7859C3.91125 20.5594 6.36562 21 9 21C10.6307 21 12.1932 20.8312 13.5862 20.5191Z" fill="#D12B41"/>
<path d="M24 19.5C24 21.9844 21.9844 24 19.5 24C17.0156 24 15 21.9844 15 19.5C15 17.0156 17.0156 15 19.5 15C21.9844 15 24 17.0156 24 19.5ZM19 17.4719V18.9719H17.5C17.225 18.9719 17 19.225 17 19.4719C17 19.775 17.225 19.9719 17.5 19.9719H19V21.4719C19 21.775 19.225 21.9719 19.5 21.9719C19.775 21.9719 20 21.775 20 21.4719V19.9719H21.5C21.775 19.9719 22 19.775 22 19.4719C22 19.225 21.775 18.9719 21.5 18.9719H20V17.4719C20 17.225 19.775 16.9719 19.5 16.9719C19.225 16.9719 19 17.225 19 17.4719Z" fill="#D12B41"/>
</g>
<defs>
<clipPath id="clip0_908_22491">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

(image error) Size: 2.6 KiB

After

(image error) Size: 2.7 KiB

View file

@ -1,4 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6695 1.06575C15.1855 0.607104 15.9756 0.65358 16.4343 1.16956L20.4343 5.66956C20.8552 6.14317 20.8552 6.85686 20.4343 7.33047L16.4343 11.8305C15.9756 12.3464 15.1855 12.3929 14.6695 11.9343C14.1536 11.4756 14.1071 10.6855 14.5657 10.1696L16.7164 7.75001H6C4.48122 7.75001 3.25 8.98123 3.25 10.5C3.25 11.1904 2.69036 11.75 2 11.75C1.30964 11.75 0.75 11.1904 0.75 10.5C0.75 7.60052 3.10051 5.25001 6 5.25001H16.7164L14.5657 2.83047C14.1071 2.31449 14.1536 1.5244 14.6695 1.06575Z" fill="#232323"/>
<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M9.33045 23.4342C8.81448 23.8929 8.02439 23.8464 7.56574 23.3304L3.56574 18.8304C3.14475 18.3568 3.14475 17.6431 3.56574 17.1695L7.56574 12.6695C8.02439 12.1536 8.81448 12.1071 9.33046 12.5657C9.84643 13.0244 9.89291 13.8145 9.43426 14.3304L7.28355 16.75L18 16.75C19.5188 16.75 20.75 15.5188 20.75 14C20.75 13.3096 21.3096 12.75 22 12.75C22.6904 12.75 23.25 13.3096 23.25 14C23.25 16.8995 20.8995 19.25 18 19.25L7.28355 19.25L9.43426 21.6695C9.89291 22.1855 9.84643 22.9756 9.33045 23.4342Z" fill="#232323"/>
<path d="M19.5 6.5H6C3.79086 6.5 2 8.29086 2 10.5M19.5 6.5L15.5 2M19.5 6.5L15.5 11" stroke="#D12B41" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path opacity="0.4" d="M4.5 18L18 18C20.2091 18 22 16.2091 22 14M4.5 18L8.5 22.5M4.5 18L8.5 13.5" stroke="#D12B41" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

(image error) Size: 1.2 KiB

After

(image error) Size: 455 B

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4173 5.41699V5.91699H10.9173H14.1257C15.1181 5.91699 15.9173 6.71825 15.9173 7.70866V15.5003C15.9173 16.2377 15.3213 16.8337 14.584 16.8337H5.41732C4.67997 16.8337 4.08398 16.2377 4.08398 15.5003V7.70866C4.08398 6.71866 4.88565 5.91699 5.87565 5.91699H9.08398H9.58398V5.41699V3.58366C9.58398 3.35506 9.77205 3.16699 10.0007 3.16699C10.2292 3.16699 10.4173 3.35506 10.4173 3.58366V5.41699ZM8.16732 15.0837H8.58398H8.66732H9.08398H10.9173H11.334H11.4173H11.834H13.6673H14.1673V14.5837V13.667V13.167H13.6673H11.834H11.4173H11.334H10.9173H9.08398H8.66732H8.58398H8.16732H6.33398H5.83398V13.667V14.5837V15.0837H6.33398H8.16732ZM1.33398 10.0003C1.33398 9.89019 1.37745 9.7852 1.45595 9.70663C1.53524 9.62741 1.64114 9.58366 1.75065 9.58366H2.16732V14.0837H1.75065C1.64023 14.0837 1.53452 14.0398 1.45609 13.9614C1.37774 13.8831 1.33398 13.7775 1.33398 13.667V10.0003ZM5.60482 10.0003C5.60482 10.9095 6.34144 11.6462 7.25065 11.6462C8.15987 11.6462 8.89648 10.9095 8.89648 10.0003C8.89648 9.09111 8.15987 8.35449 7.25065 8.35449C6.34144 8.35449 5.60482 9.09111 5.60482 10.0003ZM11.1048 10.0003C11.1048 10.9101 11.8409 11.6462 12.7507 11.6462C13.6599 11.6462 14.3965 10.9095 14.3965 10.0003C14.3965 9.09111 13.6599 8.35449 12.7507 8.35449C11.8416 8.35449 11.1048 9.09037 11.1048 10.0003ZM18.2507 9.58366C18.3599 9.58366 18.4652 9.62719 18.5445 9.70648C18.6238 9.78578 18.6673 9.89112 18.6673 10.0003V13.667C18.6673 13.7774 18.6235 13.8831 18.5451 13.9616C18.4667 14.0399 18.3612 14.0837 18.2507 14.0837H17.834V9.58366H18.2507Z" fill="#8E9192" stroke="#8E9192"/>
</svg>

After

(image error) Size: 1.6 KiB

3
assets/svg/whirlpool.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.16811 18.29C4.45149 18.089 3.04304 17.5064 1.84171 16.5006C1.52855 16.2383 1.08929 15.7904 0.868214 15.5078C0.795124 15.4144 0.815885 15.4205 1.16357 15.5943C2.17257 16.0986 3.56529 16.3941 4.67986 16.3404C5.4666 16.3025 6.00361 16.1827 6.16709 16.0087C6.30087 15.8663 6.24663 15.772 5.85013 15.458C4.68271 14.5333 3.91789 13.4633 3.48109 12.1436C2.95876 10.5655 3.03006 8.85291 3.68121 7.33745C3.85982 6.92178 4.22353 6.25056 4.46108 5.89825C5.0569 5.01461 6.02359 4.09702 7.04772 3.443C7.53459 3.13207 8.67134 2.57608 9.28978 2.34638C10.1175 2.03895 11.0493 1.81907 11.9322 1.72278C12.5152 1.6592 13.8654 1.6973 14.3766 1.79177C15.7252 2.04096 16.8058 2.50254 17.8199 3.26249C18.1679 3.52329 18.8273 4.15363 19.0478 4.43636L19.1673 4.58953L18.7556 4.39158C17.6907 3.87949 16.4125 3.60951 15.2969 3.66106C13.9736 3.72218 13.4271 3.9979 13.8908 4.37051C14.4654 4.83219 14.718 5.05501 14.9678 5.32051C16.047 6.46745 16.6696 7.8829 16.8049 9.49648C16.8912 10.527 16.6998 11.6803 16.2805 12.6564C15.6851 14.0423 14.7972 15.1697 13.573 16.0943C13.0338 16.5014 12.6196 16.756 11.9526 17.09C10.795 17.6696 9.68552 18.033 8.49122 18.2239C8.12694 18.2821 6.49683 18.3285 6.16811 18.29ZM10.567 12.7788C11.6493 12.5494 12.5288 11.6694 12.7584 10.586C12.824 10.2766 12.8238 9.72298 12.7584 9.41267C12.6408 8.85789 12.4049 8.43337 11.9758 8.00424C11.3957 7.42417 10.7838 7.17097 9.95841 7.16954C9.16291 7.16743 8.43295 7.4925 7.88801 8.0894C7.39072 8.63413 7.14979 9.25773 7.14979 10.0002C7.14979 10.8011 7.40895 11.4213 7.98385 11.9962C8.40667 12.419 8.83591 12.6598 9.37601 12.7773C9.67249 12.8418 10.2667 12.8425 10.5673 12.7794L10.567 12.7788Z" fill="#8E9192"/>
</svg>

After

(image error) Size: 1.7 KiB

@ -1 +1 @@
Subproject commit 0309140a95a51388df0effcc39ff0a25b2752b29
Subproject commit 9a150d8cd2c3625424b0059e6b7306f3659fdbe0

View file

@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/exceptions/main_db/main_db_exception.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:tuple/tuple.dart';
@ -40,64 +41,101 @@ class MainDB {
String walletId) =>
isar.addresses.where().walletIdEqualTo(walletId);
Future<void> putAddress(Address address) => isar.writeTxn(() async {
await isar.addresses.put(address);
Future<int> putAddress(Address address) async {
try {
return await isar.writeTxn(() async {
return await isar.addresses.put(address);
});
} catch (e) {
throw MainDBException("failed putAddress: $address", e);
}
}
Future<void> putAddresses(List<Address> addresses) => isar.writeTxn(() async {
await isar.addresses.putAll(addresses);
Future<List<int>> putAddresses(List<Address> addresses) async {
try {
return await isar.writeTxn(() async {
return await isar.addresses.putAll(addresses);
});
} catch (e) {
throw MainDBException("failed putAddresses: $addresses", e);
}
}
Future<void> updateOrPutAddresses(List<Address> addresses) async {
await isar.writeTxn(() async {
for (final address in addresses) {
final storedAddress = await isar.addresses
.getByValueWalletId(address.value, address.walletId);
Future<List<int>> updateOrPutAddresses(List<Address> addresses) async {
try {
List<int> ids = [];
await isar.writeTxn(() async {
for (final address in addresses) {
final storedAddress = await isar.addresses
.getByValueWalletId(address.value, address.walletId);
if (storedAddress == null) {
await isar.addresses.put(address);
} else {
address.id = storedAddress.id;
await storedAddress.transactions.load();
final txns = storedAddress.transactions.toList();
await isar.addresses.delete(storedAddress.id);
await isar.addresses.put(address);
address.transactions.addAll(txns);
await address.transactions.save();
int id;
if (storedAddress == null) {
id = await isar.addresses.put(address);
} else {
address.id = storedAddress.id;
await storedAddress.transactions.load();
final txns = storedAddress.transactions.toList();
await isar.addresses.delete(storedAddress.id);
id = await isar.addresses.put(address);
address.transactions.addAll(txns);
await address.transactions.save();
}
ids.add(id);
}
}
});
});
return ids;
} catch (e) {
throw MainDBException("failed updateOrPutAddresses: $addresses", e);
}
}
Future<Address?> getAddress(String walletId, String address) async {
return isar.addresses.getByValueWalletId(address, walletId);
}
Future<void> updateAddress(Address oldAddress, Address newAddress) =>
isar.writeTxn(() async {
Future<int> updateAddress(Address oldAddress, Address newAddress) async {
try {
return await isar.writeTxn(() async {
newAddress.id = oldAddress.id;
await oldAddress.transactions.load();
final txns = oldAddress.transactions.toList();
await isar.addresses.delete(oldAddress.id);
await isar.addresses.put(newAddress);
final id = await isar.addresses.put(newAddress);
newAddress.transactions.addAll(txns);
await newAddress.transactions.save();
return id;
});
} catch (e) {
throw MainDBException(
"failed updateAddress: from=$oldAddress to=$newAddress", e);
}
}
// transactions
QueryBuilder<Transaction, Transaction, QAfterWhereClause> getTransactions(
String walletId) =>
isar.transactions.where().walletIdEqualTo(walletId);
Future<void> putTransaction(Transaction transaction) =>
isar.writeTxn(() async {
await isar.transactions.put(transaction);
Future<int> putTransaction(Transaction transaction) async {
try {
return await isar.writeTxn(() async {
return await isar.transactions.put(transaction);
});
} catch (e) {
throw MainDBException("failed putTransaction: $transaction", e);
}
}
Future<void> putTransactions(List<Transaction> transactions) =>
isar.writeTxn(() async {
await isar.transactions.putAll(transactions);
Future<List<int>> putTransactions(List<Transaction> transactions) async {
try {
return await isar.writeTxn(() async {
return await isar.transactions.putAll(transactions);
});
} catch (e) {
throw MainDBException("failed putTransactions: $transactions", e);
}
}
Future<Transaction?> getTransaction(String walletId, String txid) async {
return isar.transactions.getByTxidWalletId(txid, walletId);
@ -212,55 +250,60 @@ class MainDB {
}
Future<void> addNewTransactionData(
List<Tuple4<Transaction, List<Output>, List<Input>, Address?>>
transactionsData,
String walletId) async {
await isar.writeTxn(() async {
for (final data in transactionsData) {
final tx = data.item1;
List<Tuple4<Transaction, List<Output>, List<Input>, Address?>>
transactionsData,
String walletId,
) async {
try {
await isar.writeTxn(() async {
for (final data in transactionsData) {
final tx = data.item1;
final potentiallyUnconfirmedTx = await getTransactions(walletId)
.filter()
.txidEqualTo(tx.txid)
.findFirst();
if (potentiallyUnconfirmedTx != null) {
// update use id to replace tx
tx.id = potentiallyUnconfirmedTx.id;
await isar.transactions.delete(potentiallyUnconfirmedTx.id);
}
// save transaction
await isar.transactions.put(tx);
// link and save outputs
if (data.item2.isNotEmpty) {
await isar.outputs.putAll(data.item2);
tx.outputs.addAll(data.item2);
await tx.outputs.save();
}
// link and save inputs
if (data.item3.isNotEmpty) {
await isar.inputs.putAll(data.item3);
tx.inputs.addAll(data.item3);
await tx.inputs.save();
}
if (data.item4 != null) {
final address = await getAddresses(walletId)
final potentiallyUnconfirmedTx = await getTransactions(walletId)
.filter()
.valueEqualTo(data.item4!.value)
.txidEqualTo(tx.txid)
.findFirst();
if (potentiallyUnconfirmedTx != null) {
// update use id to replace tx
tx.id = potentiallyUnconfirmedTx.id;
await isar.transactions.delete(potentiallyUnconfirmedTx.id);
}
// save transaction
await isar.transactions.put(tx);
// check if address exists in db and add if it does not
if (address == null) {
await isar.addresses.put(data.item4!);
// link and save outputs
if (data.item2.isNotEmpty) {
await isar.outputs.putAll(data.item2);
tx.outputs.addAll(data.item2);
await tx.outputs.save();
}
// link and save address
tx.address.value = address ?? data.item4!;
await tx.address.save();
// link and save inputs
if (data.item3.isNotEmpty) {
await isar.inputs.putAll(data.item3);
tx.inputs.addAll(data.item3);
await tx.inputs.save();
}
if (data.item4 != null) {
final address = await getAddresses(walletId)
.filter()
.valueEqualTo(data.item4!.value)
.findFirst();
// check if address exists in db and add if it does not
if (address == null) {
await isar.addresses.put(data.item4!);
}
// link and save address
tx.address.value = address ?? data.item4!;
await tx.address.save();
}
}
}
});
});
} catch (e) {
throw MainDBException("failed addNewTransactionData", e);
}
}
}

View file

@ -0,0 +1,12 @@
import 'package:stackwallet/exceptions/sw_exception.dart';
class MainDBException extends SWException {
MainDBException(super.message, this.originalError);
final Object originalError;
@override
String toString() {
return "$message: originalError=$originalError";
}
}

View file

@ -69,6 +69,7 @@ class Address extends CryptoCurrencyAddress {
"}";
}
// do not modify
enum AddressType {
p2pkh,
p2sh,
@ -79,6 +80,7 @@ enum AddressType {
nonWallet,
}
// do not modify
enum AddressSubType {
receiving,
change,

View file

@ -584,7 +584,7 @@ class _AddAddressBookEntryViewState
"Address ${i + 1}",
style: STextStyles.smallMed12(context),
),
BlueTextButton(
CustomTextButton(
onTap: () {
_removeForm(forms[i].id);
},
@ -601,7 +601,7 @@ class _AddAddressBookEntryViewState
const SizedBox(
height: 16,
),
BlueTextButton(
CustomTextButton(
onTap: () {
_addForm();
scrollController.animateTo(

View file

@ -311,7 +311,7 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> {
"Addresses",
style: STextStyles.itemSubtitle(context),
),
BlueTextButton(
CustomTextButton(
text: "Add new",
onTap: () {
Navigator.of(context).pushNamed(

View file

@ -108,9 +108,36 @@ class _BuyFormState extends ConsumerState<BuyForm> {
static Decimal maxCrypto = Decimal.parse((10000.00000000).toString());
static String boundedCryptoTicker = '';
void fiatFieldOnChanged(String value) async {}
String _amountOutOfRangeErrorString = "";
void validateAmount() {
if (_buyAmountController.text.isEmpty) {
setState(() {
_amountOutOfRangeErrorString = "";
});
return;
}
void cryptoFieldOnChanged(String value) async {}
final value = Decimal.tryParse(_buyAmountController.text);
if (value == null) {
setState(() {
_amountOutOfRangeErrorString = "Invalid amount";
});
} else if (value > maxFiat && buyWithFiat) {
setState(() {
_amountOutOfRangeErrorString =
"Maximum amount: ${maxFiat.toStringAsFixed(2)}";
});
} else if (value < minFiat && buyWithFiat) {
setState(() {
_amountOutOfRangeErrorString =
"Minimum amount: ${minFiat.toStringAsFixed(2)}";
});
} else {
setState(() {
_amountOutOfRangeErrorString = "";
});
}
}
void selectCrypto() async {
if (ref.read(simplexProvider).supportedCryptos.isEmpty) {
@ -253,6 +280,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
minFiat = fiat.minAmount != minFiat ? fiat.minAmount : minFiat;
maxFiat = fiat.maxAmount != maxFiat ? fiat.maxAmount : maxFiat;
});
validateAmount();
},
);
}
@ -359,11 +387,11 @@ class _BuyFormState extends ConsumerState<BuyForm> {
}
}
String? _fetchIconUrlFromTicker(String? ticker) {
if (ticker == null) return null;
return null;
}
// String? _fetchIconUrlFromTicker(String? ticker) {
// if (ticker == null) return null;
//
// return null;
// }
bool isStackCoin(String? ticker) {
if (ticker == null) return false;
@ -377,13 +405,15 @@ class _BuyFormState extends ConsumerState<BuyForm> {
}
Widget? getIconForTicker(String ticker) {
String? iconAsset = /*isStackCoin(ticker)
String iconAsset = /*isStackCoin(ticker)
?*/
Assets.svg.iconFor(coin: coinFromTickerCaseInsensitive(ticker));
// : Assets.svg.buyIconFor(ticker);
return (iconAsset != null)
? SvgPicture.asset(iconAsset, height: 20, width: 20)
: null;
// return (iconAsset != null)
// ? SvgPicture.asset(iconAsset, height: 20, width: 20)
// : null;
return SvgPicture.asset(iconAsset, height: 20, width: 20);
}
Future<void> previewQuote(SimplexQuote quote) async {
@ -501,25 +531,25 @@ class _BuyFormState extends ConsumerState<BuyForm> {
}
} else {
// Error; probably amount out of bounds
String errorMessage = "${quoteResponse.exception?.errorMessage}";
if (errorMessage.contains('must be between')) {
errorMessage = errorMessage.substring(
(errorMessage.indexOf('getQuote exception: ') ?? 19) + 20,
errorMessage.indexOf(", value: null"));
_BuyFormState.boundedCryptoTicker = errorMessage.substring(
errorMessage.indexOf('The ') + 4,
errorMessage.indexOf(' amount must be between'));
_BuyFormState.minCrypto = Decimal.parse(errorMessage.substring(
errorMessage.indexOf('must be between ') + 16,
errorMessage.indexOf(' and ')));
_BuyFormState.maxCrypto = Decimal.parse(errorMessage.substring(
errorMessage.indexOf("$minCrypto and ") + "$minCrypto and ".length,
errorMessage.length));
if (Decimal.parse(_buyAmountController.text) >
_BuyFormState.maxCrypto) {
_buyAmountController.text = _BuyFormState.maxCrypto.toString();
}
}
// String errorMessage = "${quoteResponse.exception?.errorMessage}";
// if (errorMessage.contains('must be between')) {
// errorMessage = errorMessage.substring(
// errorMessage.indexOf('getQuote exception: ') + 20,
// errorMessage.indexOf(", value: null"));
// _BuyFormState.boundedCryptoTicker = errorMessage.substring(
// errorMessage.indexOf('The ') + 4,
// errorMessage.indexOf(' amount must be between'));
// _BuyFormState.minCrypto = Decimal.parse(errorMessage.substring(
// errorMessage.indexOf('must be between ') + 16,
// errorMessage.indexOf(' and ')));
// _BuyFormState.maxCrypto = Decimal.parse(errorMessage.substring(
// errorMessage.indexOf("$minCrypto and ") + "$minCrypto and ".length,
// errorMessage.length));
// if (Decimal.parse(_buyAmountController.text) >
// _BuyFormState.maxCrypto) {
// _buyAmountController.text = _BuyFormState.maxCrypto.toString();
// }
// }
await showDialog<dynamic>(
context: context,
barrierDismissible: true,
@ -541,7 +571,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
height: 24,
),
Text(
errorMessage,
quoteResponse.exception!.errorMessage,
style: STextStyles.smallMed14(context),
),
const SizedBox(
@ -603,10 +633,11 @@ class _BuyFormState extends ConsumerState<BuyForm> {
level: LogLevel.Warning,
);
return BuyResponse(
exception: BuyException(
response.toString(),
BuyExceptionType.generic,
),
exception: response.exception ??
BuyException(
response.toString(),
BuyExceptionType.generic,
),
);
}
}
@ -852,7 +883,11 @@ class _BuyFormState extends ConsumerState<BuyForm> {
.textFieldDefaultBG,
child: Padding(
padding: const EdgeInsets.only(
left: 12.0, top: 12.0, right: 12.0, bottom: 12.0),
left: 12.0,
top: 12.0,
right: 12.0,
bottom: 12.0,
),
child: Row(
children: <Widget>[
Container(
@ -878,7 +913,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
width: 8,
),
Text(
"${selectedFiat?.ticker ?? 'ERR'}",
selectedFiat?.ticker ?? 'ERR',
style: STextStyles.largeMedium14(context),
),
const SizedBox(
@ -886,7 +921,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
),
Expanded(
child: Text(
"${selectedFiat?.name ?? 'Error'}",
selectedFiat?.name ?? 'Error',
style: STextStyles.largeMedium14(context),
),
),
@ -918,12 +953,13 @@ class _BuyFormState extends ConsumerState<BuyForm> {
Theme.of(context).extension<StackColors>()!.textDark3,
),
),
BlueTextButton(
CustomTextButton(
text: buyWithFiat ? "Use crypto amount" : "Use fiat amount",
onTap: () {
setState(() {
buyWithFiat = !buyWithFiat;
});
validateAmount();
},
)
],
@ -938,10 +974,11 @@ class _BuyFormState extends ConsumerState<BuyForm> {
color: Theme.of(context).extension<StackColors>()!.textDark,
),
key: const Key("buyAmountInputFieldTextFieldKey"),
controller: _buyAmountController
..text = _BuyFormState.buyWithFiat
? _BuyFormState.minFiat.toStringAsFixed(2) ?? '50.00'
: _BuyFormState.minCrypto.toStringAsFixed(8),
controller: _buyAmountController,
// note: setting the text value here will set it every time this widget rebuilds
// ..text = _BuyFormState.buyWithFiat
// ? _BuyFormState.minFiat.toStringAsFixed(2) ?? '50.00'
// : _BuyFormState.minCrypto.toStringAsFixed(8),
focusNode: _buyAmountFocusNode,
keyboardType: Util.isDesktop
? null
@ -950,7 +987,10 @@ class _BuyFormState extends ConsumerState<BuyForm> {
decimal: true,
),
textAlign: TextAlign.left,
inputFormatters: [NumericalRangeFormatter()],
// inputFormatters: [NumericalRangeFormatter()],
onChanged: (_) {
validateAmount();
},
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
// top: 22,
@ -985,8 +1025,8 @@ class _BuyFormState extends ConsumerState<BuyForm> {
),
child: Text(
format.simpleCurrencySymbol(
selectedFiat?.ticker ??
"ERR".toUpperCase()),
selectedFiat?.ticker.toUpperCase() ??
"ERR"),
textAlign: TextAlign.center,
style: STextStyles.smallMed12(context).copyWith(
color: Theme.of(context)
@ -1023,18 +1063,20 @@ class _BuyFormState extends ConsumerState<BuyForm> {
key: const Key(
"buyViewClearAmountFieldButtonKey"),
onTap: () {
if (_BuyFormState.buyWithFiat) {
_buyAmountController.text = _BuyFormState
.minFiat
.toStringAsFixed(2);
} else {
if (selectedCrypto?.ticker ==
_BuyFormState.boundedCryptoTicker) {
_buyAmountController.text = _BuyFormState
.minCrypto
.toStringAsFixed(8);
}
}
// if (_BuyFormState.buyWithFiat) {
// _buyAmountController.text = _BuyFormState
// .minFiat
// .toStringAsFixed(2);
// } else {
// if (selectedCrypto?.ticker ==
// _BuyFormState.boundedCryptoTicker) {
// _buyAmountController.text = _BuyFormState
// .minCrypto
// .toStringAsFixed(8);
// }
// }
_buyAmountController.text = "";
validateAmount();
},
child: const XIcon(),
)
@ -1051,7 +1093,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
_buyAmountController.text =
amountString.toString();
setState(() {});
validateAmount();
}
},
child: _buyAmountController.text.isEmpty
@ -1064,6 +1106,14 @@ class _BuyFormState extends ConsumerState<BuyForm> {
),
),
),
SizedBox(
height: isDesktop ? 10 : 4,
),
if (_amountOutOfRangeErrorString.isNotEmpty)
Text(
_amountOutOfRangeErrorString,
style: STextStyles.errorSmall(context),
),
SizedBox(
height: isDesktop ? 20 : 12,
),
@ -1079,8 +1129,8 @@ class _BuyFormState extends ConsumerState<BuyForm> {
),
),
if (isStackCoin(selectedCrypto?.ticker))
BlueTextButton(
text: "Choose from stack",
CustomTextButton(
text: "Choose from Stack",
onTap: () {
try {
final coin = coinFromTickerCaseInsensitive(
@ -1103,7 +1153,11 @@ class _BuyFormState extends ConsumerState<BuyForm> {
_receiveAddressController.text =
await manager.currentReceivingAddress;
setState(() {});
setState(() {
_addressToggleFlag =
_receiveAddressController.text.isNotEmpty;
});
validateAmount();
}
});
} catch (e, s) {
@ -1171,7 +1225,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
_receiveAddressController.text = "";
_address = "";
setState(() {
_addressToggleFlag = true;
_addressToggleFlag = false;
});
},
child: const XIcon(),
@ -1342,27 +1396,15 @@ class _BuyFormState extends ConsumerState<BuyForm> {
SizedBox(
height: isDesktop ? 20 : 12,
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
if (_receiveAddressController.text.isNotEmpty &&
_buyAmountController.text.isNotEmpty) {
previewQuote(quote);
}
},
child: PrimaryButton(
buttonHeight: isDesktop ? ButtonHeight.l : null,
enabled: _receiveAddressController.text.isNotEmpty &&
_buyAmountController.text.isNotEmpty,
onPressed: () {
if (_receiveAddressController.text.isNotEmpty &&
_buyAmountController.text.isNotEmpty) {
previewQuote(quote);
}
},
label: "Preview quote",
)),
PrimaryButton(
buttonHeight: isDesktop ? ButtonHeight.l : null,
enabled: _addressToggleFlag &&
_amountOutOfRangeErrorString.isEmpty &&
_buyAmountController.text.isNotEmpty,
onPressed: () {
previewQuote(quote);
},
label: "Preview quote",
),
],
),
@ -1371,51 +1413,53 @@ class _BuyFormState extends ConsumerState<BuyForm> {
}
}
// See https://stackoverflow.com/a/68072967
class NumericalRangeFormatter extends TextInputFormatter {
NumericalRangeFormatter();
// might need this again in the future
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
TextSelection newSelection = newValue.selection;
String newVal = _BuyFormState.buyWithFiat
? Decimal.parse(newValue.text).toStringAsFixed(2)
: Decimal.parse(newValue.text).toStringAsFixed(8);
if (newValue.text == '') {
return newValue;
} else {
if (_BuyFormState.buyWithFiat) {
if (Decimal.parse(newValue.text) < _BuyFormState.minFiat) {
newVal = _BuyFormState.minFiat.toStringAsFixed(2);
// _BuyFormState._buyAmountController.selection =
// TextSelection.collapsed(
// offset: _BuyFormState.buyWithFiat
// ? _BuyFormState._buyAmountController.text.length - 2
// : _BuyFormState._buyAmountController.text.length - 8);
} else if (Decimal.parse(newValue.text) > _BuyFormState.maxFiat) {
newVal = _BuyFormState.maxFiat.toStringAsFixed(2);
}
} else if (!_BuyFormState.buyWithFiat &&
_BuyFormState.selectedCrypto?.ticker ==
_BuyFormState.boundedCryptoTicker) {
if (Decimal.parse(newValue.text) < _BuyFormState.minCrypto) {
newVal = _BuyFormState.minCrypto.toStringAsFixed(8);
} else if (Decimal.parse(newValue.text) > _BuyFormState.maxCrypto) {
newVal = _BuyFormState.maxCrypto.toStringAsFixed(8);
}
}
}
final regexString = _BuyFormState.buyWithFiat
? r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$'
: r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$';
// return RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
return RegExp(regexString).hasMatch(newVal)
? TextEditingValue(text: newVal, selection: newSelection)
: oldValue;
}
}
// // See https://stackoverflow.com/a/68072967
// class NumericalRangeFormatter extends TextInputFormatter {
// NumericalRangeFormatter();
//
// @override
// TextEditingValue formatEditUpdate(
// TextEditingValue oldValue,
// TextEditingValue newValue,
// ) {
// TextSelection newSelection = newValue.selection;
// String newVal = _BuyFormState.buyWithFiat
// ? Decimal.parse(newValue.text).toStringAsFixed(2)
// : Decimal.parse(newValue.text).toStringAsFixed(8);
// if (newValue.text == '') {
// return newValue;
// } else {
// if (_BuyFormState.buyWithFiat) {
// if (Decimal.parse(newValue.text) < _BuyFormState.minFiat) {
// newVal = _BuyFormState.minFiat.toStringAsFixed(2);
// // _BuyFormState._buyAmountController.selection =
// // TextSelection.collapsed(
// // offset: _BuyFormState.buyWithFiat
// // ? _BuyFormState._buyAmountController.text.length - 2
// // : _BuyFormState._buyAmountController.text.length - 8);
// } else if (Decimal.parse(newValue.text) > _BuyFormState.maxFiat) {
// newVal = _BuyFormState.maxFiat.toStringAsFixed(2);
// }
// } else if (!_BuyFormState.buyWithFiat &&
// _BuyFormState.selectedCrypto?.ticker ==
// _BuyFormState.boundedCryptoTicker) {
// if (Decimal.parse(newValue.text) < _BuyFormState.minCrypto) {
// newVal = _BuyFormState.minCrypto.toStringAsFixed(8);
// } else if (Decimal.parse(newValue.text) > _BuyFormState.maxCrypto) {
// newVal = _BuyFormState.maxCrypto.toStringAsFixed(8);
// }
// }
// }
//
// final regexString = _BuyFormState.buyWithFiat
// ? r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$'
// : r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$';
//
// // return RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
// return RegExp(regexString).hasMatch(newVal)
// ? TextEditingValue(text: newVal, selection: newSelection)
// : oldValue;
// }
// }

View file

@ -189,8 +189,8 @@ class _Step2ViewState extends ConsumerState<Step2View> {
style: STextStyles.smallMed12(context),
),
if (isStackCoin(model.receiveTicker))
BlueTextButton(
text: "Choose from stack",
CustomTextButton(
text: "Choose from Stack",
onTap: () {
try {
final coin =
@ -448,8 +448,8 @@ class _Step2ViewState extends ConsumerState<Step2View> {
style: STextStyles.smallMed12(context),
),
if (isStackCoin(model.sendTicker))
BlueTextButton(
text: "Choose from stack",
CustomTextButton(
text: "Choose from Stack",
onTap: () {
try {
final coin =

View file

@ -516,7 +516,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
const SizedBox(
height: 10,
),
BlueTextButton(
CustomTextButton(
text: "View transaction",
onTap: () {
final Coin coin =

View file

@ -50,7 +50,7 @@ class _HomeViewState extends ConsumerState<HomeView> {
Future<bool> _onWillPop() async {
// go to home view when tapping back on the main exchange view
if (ref.read(homeViewPageIndexStateProvider.state).state == 1) {
if (ref.read(homeViewPageIndexStateProvider.state).state != 0) {
ref.read(homeViewPageIndexStateProvider.state).state = 0;
return false;
}

View file

@ -176,14 +176,49 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
children: [
PayNymBot(
paymentCodeString: widget.accountLite.code,
size: 32,
size: 36,
),
const SizedBox(
width: 12,
),
Text(
widget.accountLite.nymName,
style: STextStyles.w600_12(context),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.accountLite.nymName,
style: STextStyles.w600_14(context),
),
FutureBuilder(
future:
wallet.hasConnected(widget.accountLite.code),
builder: (context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.data == true) {
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const SizedBox(
height: 2,
),
Text(
"Connected",
style: STextStyles.w500_12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
),
)
],
);
} else {
return Container();
}
},
),
],
),
],
),
@ -195,33 +230,33 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
if (snapshot.data!) {
return PrimaryButton(
label: "Send",
buttonHeight: ButtonHeight.l,
buttonHeight: ButtonHeight.xl,
icon: SvgPicture.asset(
Assets.svg.circleArrowUpRight,
width: 10,
height: 10,
width: 14,
height: 14,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 4,
width: 86,
iconSpacing: 8,
width: 100,
onPressed: _onSend,
);
} else {
return PrimaryButton(
label: "Connect",
buttonHeight: ButtonHeight.l,
buttonHeight: ButtonHeight.xl,
icon: SvgPicture.asset(
Assets.svg.circlePlusFilled,
width: 10,
height: 10,
width: 13,
height: 13,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 4,
width: 86,
iconSpacing: 8,
width: 128,
onPressed: _onConnectPressed,
);
}
@ -256,6 +291,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
color: Theme.of(context)
.extension<StackColors>()!
.warningForeground,
fontSize: 12,
),
),
),
@ -286,7 +322,9 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
children: [
Text(
"PayNym address",
style: STextStyles.infoSmall(context),
style: STextStyles.infoSmall(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 6,
@ -297,6 +335,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontSize: 12,
),
),
const SizedBox(
@ -311,7 +350,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
),
QrImage(
padding: const EdgeInsets.all(0),
size: 86,
size: 100,
data: widget.accountLite.code,
foregroundColor:
Theme.of(context).extension<StackColors>()!.textDark,
@ -340,16 +379,16 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
Expanded(
child: SecondaryButton(
label: "Copy",
buttonHeight: ButtonHeight.l,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: SvgPicture.asset(
Assets.svg.copy,
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
iconSpacing: 4,
onPressed: () async {
await Clipboard.setData(
ClipboardData(

View file

@ -56,7 +56,7 @@ class PaynymQrPopup extends StatelessWidget {
children: [
PayNymBot(
paymentCodeString: paynymAccount.codes.first.code,
size: isDesktop ? 56 : 32,
size: isDesktop ? 56 : 36,
),
const SizedBox(
width: 12,
@ -65,7 +65,7 @@ class PaynymQrPopup extends StatelessWidget {
paynymAccount.nymName,
style: isDesktop
? STextStyles.w500_24(context)
: STextStyles.w600_12(context),
: STextStyles.w600_14(context),
),
],
),
@ -87,7 +87,7 @@ class PaynymQrPopup extends StatelessWidget {
children: [
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 107),
constraints: const BoxConstraints(minHeight: 130),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -100,7 +100,9 @@ class PaynymQrPopup extends StatelessWidget {
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.infoSmall(context),
: STextStyles.infoSmall(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 6,
@ -113,14 +115,15 @@ class PaynymQrPopup extends StatelessWidget {
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontSize: 12,
),
),
const SizedBox(
height: 6,
),
BlueTextButton(
CustomTextButton(
text: "Copy",
textSize: isDesktop ? 18 : 10,
textSize: isDesktop ? 18 : 14,
onTap: () async {
await Clipboard.setData(
ClipboardData(
@ -146,7 +149,7 @@ class PaynymQrPopup extends StatelessWidget {
),
QrImage(
padding: const EdgeInsets.all(0),
size: 107,
size: 130,
data: paynymAccount.codes.first.code,
foregroundColor:
Theme.of(context).extension<StackColors>()!.textDark,

View file

@ -117,7 +117,7 @@ class _PaynymClaimViewState extends ConsumerState<PaynymClaimView> {
),
Image(
image: AssetImage(
Assets.png.stack,
Assets.png.unclaimedPaynym,
),
width: MediaQuery.of(context).size.width / 2,
),

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:share_plus/share_plus.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart';
import 'package:stackwallet/pages/paynym/dialogs/paynym_qr_popup.dart';
@ -302,7 +303,9 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
.code,
12,
5),
style: STextStyles.label(context),
style: STextStyles.label(context).copyWith(
fontSize: 14,
),
),
const SizedBox(
height: 11,
@ -312,11 +315,11 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
Expanded(
child: SecondaryButton(
label: "Copy",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: CopyIcon(
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
@ -349,17 +352,34 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
Expanded(
child: SecondaryButton(
label: "Share",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: ShareIcon(
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
onPressed: () {
// copy to clipboard
onPressed: () async {
Rect? sharePositionOrigin;
if (await Util.isIPad) {
final box =
context.findRenderObject() as RenderBox?;
if (box != null) {
sharePositionOrigin =
box.localToGlobal(Offset.zero) & box.size;
}
}
await Share.share(
ref
.read(myPaynymAccountStateProvider.state)
.state!
.codes
.first
.code,
sharePositionOrigin: sharePositionOrigin);
},
),
),
@ -369,11 +389,11 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
Expanded(
child: SecondaryButton(
label: "Address",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: QrCodeIcon(
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
@ -536,7 +556,7 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
child: child,
),
child: SizedBox(
height: isDesktop ? 56 : 40,
height: isDesktop ? 56 : 48,
width: isDesktop ? 490 : null,
child: Toggle(
onColor: Theme.of(context).extension<StackColors>()!.popupBG,

View file

@ -171,14 +171,47 @@ class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
children: [
PayNymBot(
paymentCodeString: widget.accountLite.code,
size: 32,
size: 36,
),
const SizedBox(
width: 12,
),
Text(
widget.accountLite.nymName,
style: STextStyles.desktopTextSmall(context),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.accountLite.nymName,
style: STextStyles.desktopTextSmall(context),
),
FutureBuilder(
future: wallet.hasConnected(widget.accountLite.code),
builder: (context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.data == true) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 2,
),
Text(
"Connected",
style: STextStyles.desktopTextSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
),
)
],
);
} else {
return Container();
}
},
),
],
),
],
),
@ -324,7 +357,7 @@ class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
const SizedBox(
height: 8,
),
BlueTextButton(
CustomTextButton(
text: "Copy",
onTap: () async {
await Clipboard.setData(

View file

@ -37,7 +37,7 @@ class _PaynymCardState extends State<PaynymCard> {
child: Row(
children: [
PayNymBot(
size: 32,
size: 36,
paymentCodeString: widget.paymentCodeString,
),
const SizedBox(
@ -56,7 +56,7 @@ class _PaynymCardState extends State<PaynymCard> {
.extension<StackColors>()!
.textFieldActiveText,
)
: STextStyles.w500_12(context),
: STextStyles.w500_14(context),
),
const SizedBox(
height: 2,
@ -65,7 +65,7 @@ class _PaynymCardState extends State<PaynymCard> {
Format.shorten(widget.paymentCodeString, 12, 5),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.w500_12(context).copyWith(
: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,

View file

@ -77,7 +77,7 @@ class _PaynymCardButtonState extends ConsumerState<PaynymCardButton> {
child: Row(
children: [
PayNymBot(
size: 32,
size: 36,
paymentCodeString: widget.accountLite.code,
),
const SizedBox(
@ -96,7 +96,7 @@ class _PaynymCardButtonState extends ConsumerState<PaynymCardButton> {
.extension<StackColors>()!
.textFieldActiveText,
)
: STextStyles.w500_12(context),
: STextStyles.w500_14(context),
),
const SizedBox(
height: 2,
@ -105,7 +105,7 @@ class _PaynymCardButtonState extends ConsumerState<PaynymCardButton> {
Format.shorten(widget.accountLite.code, 12, 5),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.w500_12(context).copyWith(
: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,

View file

@ -7,10 +7,12 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
import 'package:stackwallet/pages/receive_view/receiving_addresses_view.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
@ -19,6 +21,7 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
class ReceiveView extends ConsumerStatefulWidget {
const ReceiveView({
@ -128,6 +131,84 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
"Receive ${coin.ticker}",
style: STextStyles.navBarTitle(context),
),
actions: [
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("walletNetworkSettingsAddNewNodeViewButton"),
size: 36,
shadows: const [],
color: Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.verticalEllipsis,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () {
showDialog<dynamic>(
barrierColor: Colors.transparent,
barrierDismissible: true,
context: context,
builder: (_) {
return Stack(
children: [
Positioned(
top: 9,
right: 10,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
// boxShadow: [CFColors.standardBoxShadow],
boxShadow: const [],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed(
ReceivingAddressesView.routeName,
arguments: Tuple2(walletId, false),
);
},
child: RoundedWhiteContainer(
child: Material(
color: Colors.transparent,
child: Text(
"Address list",
style: STextStyles.baseXS(context),
),
),
),
),
],
),
),
),
],
);
},
);
},
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(12),
@ -233,7 +314,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
const SizedBox(
height: 20,
),
BlueTextButton(
CustomTextButton(
text: "Create new QR code",
onTap: () async {
unawaited(Navigator.of(context).push(

View file

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/main_db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class ReceivingAddressesView extends ConsumerWidget {
const ReceivingAddressesView({
Key? key,
required this.walletId,
required this.isDesktop,
this.clipboard = const ClipboardWrapper(),
}) : super(key: key);
static const String routeName = "/receivingAddressesView";
final String walletId;
final bool isDesktop;
final ClipboardInterface clipboard;
@override
Widget build(BuildContext context, WidgetRef ref) {
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
backgroundColor:
Theme.of(context).extension<StackColors>()!.backgroundAppBar,
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Receiving addresses",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
child: FutureBuilder(
future: MainDB.instance
.getAddresses(walletId)
.filter()
.subTypeEqualTo(AddressSubType.receiving)
.and()
.not()
.typeEqualTo(AddressType.nonWallet)
.sortByDerivationIndexDesc()
.findAll(),
builder: (context, AsyncSnapshot<List<Address>> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.data != null) {
// listview
return ListView.separated(
itemCount: snapshot.data!.length,
separatorBuilder: (_, __) => Container(
height: 10,
),
itemBuilder: (_, index) => AddressCard(
address: snapshot.data![index],
),
);
} else {
return const Center(
child: LoadingIndicator(
height: 200,
width: 200,
),
);
}
},
),
);
}
}
class AddressCard extends StatelessWidget {
const AddressCard({
Key? key,
required this.address,
}) : super(key: key);
final Address address;
@override
Widget build(BuildContext context) {
return RoundedWhiteContainer(
child: Row(
children: [
Text(
address.value,
style: STextStyles.itemSubtitle12(context),
)
],
),
);
}
}

View file

@ -1304,7 +1304,7 @@ class _SendViewState extends ConsumerState<SendView> {
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
BlueTextButton(
CustomTextButton(
text: "Send all ${coin.ticker}",
onTap: () async {
if (coin == Coin.firo ||

View file

@ -484,7 +484,7 @@ class AboutView extends ConsumerWidget {
const SizedBox(
height: 4,
),
BlueTextButton(
CustomTextButton(
text: "https://stackwallet.com",
onTap: () {
launchUrl(

View file

@ -282,7 +282,7 @@ class _DebugViewState extends ConsumerState<DebugView> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BlueTextButton(
CustomTextButton(
text: "Save Debug Info to clipboard",
onTap: () async {
try {
@ -350,7 +350,7 @@ class _DebugViewState extends ConsumerState<DebugView> {
},
),
const Spacer(),
BlueTextButton(
CustomTextButton(
text: "Save logs to file",
onTap: () async {
final systemfile = SWBFileSystem();

View file

@ -92,7 +92,7 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> {
),
textAlign: TextAlign.left,
),
BlueTextButton(
CustomTextButton(
text: "Add new node",
onTap: () {
Navigator.of(context).pushNamed(

View file

@ -327,7 +327,7 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BlueTextButton(
CustomTextButton(
text: "Back up now",
onTap: () {
ref.read(autoSWBServiceProvider).doBackup();
@ -448,7 +448,7 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> {
height: 20,
),
Center(
child: BlueTextButton(
child: CustomTextButton(
text: "Edit Auto Backup",
onTap: () async {
Navigator.of(context)

View file

@ -749,7 +749,7 @@ class _WalletNetworkSettingsViewState
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.smallMed12(context),
),
BlueTextButton(
CustomTextButton(
text: "Add new node",
onTap: () {
Navigator.of(context).pushNamed(
@ -880,7 +880,7 @@ class _WalletNetworkSettingsViewState
top: 16,
bottom: 6,
),
child: BlueTextButton(
child: CustomTextButton(
text: "Rescan",
onTap: () async {
await Navigator.of(context).push(

View file

@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart';
import 'package:stackwallet/providers/blockchain/dogecoin/current_height_provider.dart';
import 'package:stackwallet/providers/global/trades_service_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/route_generator.dart';
@ -21,6 +20,8 @@ import 'package:stackwallet/widgets/trade_card.dart';
import 'package:stackwallet/widgets/transaction_card.dart';
import 'package:tuple/tuple.dart';
import '../../../utilities/enums/coin_enum.dart';
class TransactionsList extends ConsumerStatefulWidget {
const TransactionsList({
Key? key,
@ -68,11 +69,18 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
BuildContext context,
Transaction tx,
BorderRadius? radius,
Coin coin,
) {
final matchingTrades = ref
.read(tradesServiceProvider)
.trades
.where((e) => e.payInTxid == tx.txid || e.payOutTxid == tx.txid);
final isConfirmed = tx.isConfirmed(
ref.watch(
widget.managerProvider.select((value) => value.currentHeight)),
coin.requiredConfirmations);
if (tx.type == TransactionType.outgoing && matchingTrades.isNotEmpty) {
final trade = matchingTrades.first;
return Container(
@ -85,7 +93,9 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
children: [
TransactionCard(
// this may mess with combined firo transactions
key: Key(tx.txid + tx.type.name + tx.address.value.toString()), //
key: isConfirmed
? Key(tx.txid + tx.type.name + tx.address.value.toString())
: UniqueKey(), //
transaction: tx,
walletId: widget.walletId,
),
@ -180,7 +190,9 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
),
child: TransactionCard(
// this may mess with combined firo transactions
key: Key(tx.txid + tx.type.name + tx.address.value.toString()), //
key: isConfirmed
? Key(tx.txid + tx.type.name + tx.address.value.toString())
: UniqueKey(),
transaction: tx,
walletId: widget.walletId,
),
@ -188,13 +200,6 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
}
}
void updateHeightProvider(Manager manager) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ref.read(currentHeightProvider(manager.coin).state).state =
manager.currentHeight;
});
}
@override
void initState() {
managerProvider = widget.managerProvider;
@ -203,14 +208,9 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
@override
Widget build(BuildContext context) {
// final managerProvider = ref
// .watch(walletsChangeNotifierProvider)
// .getManagerProvider(widget.walletId);
final manager = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(widget.walletId)));
updateHeightProvider(manager);
return FutureBuilder(
future: manager.transactions,
builder: (fbContext, AsyncSnapshot<List<Transaction>> snapshot) {
@ -264,7 +264,7 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
radius = _borderRadiusFirst;
}
final tx = _transactions2[index];
return itemBuilder(context, tx, radius);
return itemBuilder(context, tx, radius, manager.coin);
},
separatorBuilder: (context, index) {
return Container(
@ -291,7 +291,7 @@ class _TransactionsListState extends ConsumerState<TransactionsList> {
radius = _borderRadiusFirst;
}
final tx = _transactions2[index];
return itemBuilder(context, tx, radius);
return itemBuilder(context, tx, radius, manager.coin);
},
),
);

View file

@ -175,7 +175,18 @@ class _WalletNavigationBarState extends ConsumerState<WalletNavigationBar> {
children: [
Text(
"Paynym",
style: STextStyles.w600_12(context),
style: STextStyles.buttonSmall(context),
),
const SizedBox(
width: 16,
),
SvgPicture.asset(
Assets.svg.robotHead,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.bottomNavIconIcon,
),
],
),
@ -360,62 +371,6 @@ class _WalletNavigationBarState extends ConsumerState<WalletNavigationBar> {
),
),
),
if (ref.watch(walletsChangeNotifierProvider.select((value) =>
value.getManager(widget.walletId).hasPaynymSupport)))
RawMaterialButton(
constraints: const BoxConstraints(
minWidth: 66,
),
onPressed: () {
if (scale == 0) {
setState(() {
scale = 1;
});
} else if (scale == 1) {
setState(() {
scale = 0;
});
}
},
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
widget.height / 2.0,
),
),
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Spacer(),
const SizedBox(
height: 2,
),
SvgPicture.asset(
Assets.svg.bars,
width: 20,
height: 20,
),
const SizedBox(
height: 6,
),
Text(
"More",
style: STextStyles.buttonSmall(context),
),
const Spacer(),
],
),
),
),
),
const SizedBox(
width: 12,
),
if (widget.coin.hasBuySupport)
RawMaterialButton(
constraints: const BoxConstraints(
@ -455,10 +410,65 @@ class _WalletNavigationBarState extends ConsumerState<WalletNavigationBar> {
),
),
),
if (widget.coin.hasBuySupport)
const SizedBox(
width: 12,
if (ref.watch(walletsChangeNotifierProvider.select((value) =>
value.getManager(widget.walletId).hasPaynymSupport)))
RawMaterialButton(
constraints: const BoxConstraints(
minWidth: 66,
),
onPressed: () {
if (scale == 0) {
setState(() {
scale = 1;
});
} else if (scale == 1) {
setState(() {
scale = 0;
});
}
},
splashColor:
Theme.of(context).extension<StackColors>()!.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
widget.height / 2.0,
),
),
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Spacer(),
const SizedBox(
height: 2,
),
SvgPicture.asset(
Assets.svg.bars,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.bottomNavIconIcon,
),
const SizedBox(
height: 6,
),
Text(
"More",
style: STextStyles.buttonSmall(context),
),
const Spacer(),
],
),
),
),
),
const SizedBox(
width: 12,
),
],
),
),

View file

@ -10,7 +10,6 @@ import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_search_filter_view.dart';
import 'package:stackwallet/providers/blockchain/dogecoin/current_height_provider.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
@ -131,7 +130,9 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> {
// check if address book name contains
contains |= contacts
.where((e) =>
e.addresses.where((a) => a.address == tx.address).isNotEmpty &&
e.addresses
.where((a) => a.address == tx.address.value?.value)
.isNotEmpty &&
e.name.toLowerCase().contains(keyword))
.isNotEmpty;
@ -853,7 +854,8 @@ class _DesktopTransactionCardRowState
prefix = "";
}
final currentHeight = ref.watch(currentHeightProvider(coin).state).state;
final currentHeight = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).currentHeight));
return Material(
color: Theme.of(context).extension<StackColors>()!.popupBG,

View file

@ -11,7 +11,6 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/providers/blockchain/dogecoin/current_height_provider.dart';
import 'package:stackwallet/providers/global/address_book_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
@ -298,7 +297,8 @@ class _TransactionDetailsViewState
@override
Widget build(BuildContext context) {
final currentHeight = ref.watch(currentHeightProvider(coin).state).state;
final currentHeight = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).currentHeight));
return ConditionalParent(
condition: !isDesktop,
@ -1092,7 +1092,7 @@ class _TransactionDetailsViewState
height: 8,
),
if (coin != Coin.epicCash)
BlueTextButton(
CustomTextButton(
text: "Open in block explorer",
onTap: () async {
final uri =

View file

@ -649,7 +649,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
.textDark3,
),
),
BlueTextButton(
CustomTextButton(
text: "See all",
onTap: () {
Navigator.of(context).pushNamed(
@ -774,9 +774,6 @@ class _WalletViewState extends ConsumerState<WalletView> {
);
},
onBuyPressed: () {
// TODO set default coin to currently open wallet here by passing it as an argument
// final coin = ref.read(managerProvider).coin;
unawaited(Navigator.of(context).pushNamed(
BuyInWalletView.routeName,
arguments: coin,

View file

@ -24,7 +24,7 @@ class AllWallets extends StatelessWidget {
color: Theme.of(context).extension<StackColors>()!.textDark,
),
),
BlueTextButton(
CustomTextButton(
text: "Add new",
onTap: () {
Navigator.of(context).pushNamed(AddWalletView.routeName);

View file

@ -66,7 +66,7 @@ class DesktopAddressCard extends StatelessWidget {
),
Row(
children: [
BlueTextButton(
CustomTextButton(
text: "Copy",
onTap: () {
clipboard.setData(
@ -87,7 +87,7 @@ class DesktopAddressCard extends StatelessWidget {
if (contactId != "default")
Consumer(
builder: (context, ref, child) {
return BlueTextButton(
return CustomTextButton(
text: "Edit",
onTap: () async {
ref.refresh(

View file

@ -182,7 +182,7 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> {
style:
STextStyles.desktopTextExtraExtraSmall(context),
),
BlueTextButton(
CustomTextButton(
text: "Add new",
onTap: () async {
ref.refresh(

View file

@ -300,8 +300,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
),
if (isStackCoin(ref.watch(desktopExchangeModelProvider
.select((value) => value!.receiveTicker))))
BlueTextButton(
text: "Choose from stack",
CustomTextButton(
text: "Choose from Stack",
onTap: selectRecipientAddressFromStack,
),
],
@ -432,8 +432,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
),
if (isStackCoin(ref.watch(desktopExchangeModelProvider
.select((value) => value!.sendTicker))))
BlueTextButton(
text: "Choose from stack",
CustomTextButton(
text: "Choose from Stack",
onTap: selectRefundAddressFromStack,
),
],

View file

@ -221,7 +221,7 @@ class _DesktopChooseFromStackState
const SizedBox(
width: 80,
),
BlueTextButton(
CustomTextButton(
text: "Select wallet",
onTap: () async {
final address =

View file

@ -72,7 +72,7 @@ class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> {
"Recent trades",
style: STextStyles.desktopTextExtraExtraSmall(context),
),
BlueTextButton(
CustomTextButton(
text: "See all",
onTap: () {
Navigator.of(context)

View file

@ -39,7 +39,7 @@ class DesktopFavoriteWallets extends ConsumerWidget {
.textFieldActiveSearchIconRight,
),
),
BlueTextButton(
CustomTextButton(
text: "Edit",
onTap: () {
Navigator.of(context).pushNamed(ManageFavoritesView.routeName);

View file

@ -38,7 +38,7 @@ class _MyWalletsState extends ConsumerState<MyWallets> {
),
),
const Spacer(),
BlueTextButton(
CustomTextButton(
text: "Add new wallet",
onTap: () {
Navigator.of(

View file

@ -133,7 +133,7 @@ class _ContactListItemState extends ConsumerState<ContactListItem> {
],
),
),
BlueTextButton(
CustomTextButton(
text: "Select wallet",
onTap: () {
Navigator.of(context).pop(e);

View file

@ -987,7 +987,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
),
textAlign: TextAlign.left,
),
BlueTextButton(
CustomTextButton(
text: "Send all ${coin.ticker}",
onTap: sendAllTapped,
),

View file

@ -37,7 +37,7 @@ class _RecentDesktopTransactionsState
.textFieldActiveSearchIconLeft,
),
),
BlueTextButton(
CustomTextButton(
text: "See all",
onTap: () {
Navigator.of(context).pushNamed(

View file

@ -261,7 +261,7 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> {
const SizedBox(
height: 60,
),
BlueTextButton(
CustomTextButton(
text: "Forgot password?",
textSize: 20,
onTap: () {

View file

@ -450,7 +450,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> {
STextStyles.itemSubtitle(
context),
),
BlueTextButton(
CustomTextButton(
text: "Back up now",
onTap: () {
ref

View file

@ -678,7 +678,7 @@ class DesktopAboutView extends ConsumerWidget {
const SizedBox(
height: 2,
),
BlueTextButton(
CustomTextButton(
text:
"https://stackwallet.com",
onTap: () {

View file

@ -1,6 +0,0 @@
import 'package:dart_numerics/dart_numerics.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
final currentHeightProvider =
StateProvider.family<int, Coin>((ref, coin) => int64MaxValue);

View file

@ -46,6 +46,7 @@ import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart';
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
import 'package:stackwallet/pages/receive_view/receive_view.dart';
import 'package:stackwallet/pages/receive_view/receiving_addresses_view.dart';
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
import 'package:stackwallet/pages/send_view/send_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart';
@ -820,6 +821,21 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ReceivingAddressesView.routeName:
if (args is Tuple2<String, bool>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ReceivingAddressesView(
walletId: args.item1,
isDesktop: args.item2,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case SendView.routeName:
if (args is Tuple2<String, Coin>) {
return getRoute(

View file

@ -1,4 +1,8 @@
enum BuyExceptionType { generic, serializeResponseError }
enum BuyExceptionType {
generic,
serializeResponseError,
cryptoAmountOutOfRange,
}
class BuyException implements Exception {
String errorMessage;

View file

@ -210,7 +210,20 @@ class SimplexAPI {
BuyResponse<SimplexQuote> _parseQuote(dynamic jsonArray) {
try {
String cryptoAmount = "${jsonArray['digital_money']['amount']}";
// final Map<String, dynamic> lol =
// Map<String, dynamic>.from(jsonArray as Map);
double? cryptoAmount = jsonArray['digital_money']?['amount'] as double?;
if (cryptoAmount == null) {
String error = jsonArray['error'] as String;
return BuyResponse(
exception: BuyException(
error,
BuyExceptionType.cryptoAmountOutOfRange,
),
);
}
SimplexQuote quote = jsonArray['quote'] as SimplexQuote;
final SimplexQuote _quote = SimplexQuote(
@ -277,9 +290,9 @@ class SimplexAPI {
}
final jsonArray = jsonDecode(res.body); // TODO check if valid json
if (jsonArray.containsKey('error') as bool) {
if (jsonArray['error'] == true || jsonArray['error'] == 'true') {
throw Exception(jsonArray['message']);
}
if (jsonArray['error'] == true || jsonArray['error'] == 'true') {
throw Exception(jsonArray['message']);
}
}
SimplexOrder _order = SimplexOrder(

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:bech32/bech32.dart';
import 'package:bip32/bip32.dart' as bip32;
@ -40,6 +41,7 @@ import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/paynym_is_api.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart';
import 'package:uuid/uuid.dart';
@ -218,6 +220,19 @@ class BitcoinWallet extends CoinServiceAPI
.findFirst()) ??
await _generateAddressForChain(1, 0, DerivePathTypeExt.primaryFor(coin));
Future<String> get currentChangeAddressP2PKH async =>
(await _currentChangeAddressP2PKH).value;
Future<isar_models.Address> get _currentChangeAddressP2PKH async =>
(await db
.getAddresses(walletId)
.filter()
.typeEqualTo(isar_models.AddressType.p2pkh)
.subTypeEqualTo(isar_models.AddressSubType.change)
.sortByDerivationIndexDesc()
.findFirst()) ??
await _generateAddressForChain(1, 0, DerivePathType.bip44);
@override
Future<void> exit() async {
_hasCalledExit = true;
@ -251,6 +266,14 @@ class BitcoinWallet extends CoinServiceAPI
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -683,16 +706,28 @@ class BitcoinWallet extends CoinServiceAPI
...p2shChangeAddressArray,
]);
// generate to ensure notification address is in db before refreshing transactions
await getMyNotificationAddress(DerivePathType.bip44);
// get own payment code
final myCode = await getPaymentCode(DerivePathType.bip44);
// refresh transactions to pick up any received notification transactions
await _refreshTransactions();
final Set<String> codesToCheck = {};
final nym = await PaynymIsApi().nym(myCode.toString());
if (nym.value != null) {
for (final follower in nym.value!.followers) {
codesToCheck.add(follower.code);
}
for (final following in nym.value!.following) {
codesToCheck.add(following.code);
}
}
// restore paynym transactions
await restoreAllHistory(
maxUnusedAddressGap: maxUnusedAddressGap,
maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
paymentCodeStrings: codesToCheck,
);
await _updateUTXOs();
@ -919,6 +954,17 @@ class BitcoinWallet extends CoinServiceAPI
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId));
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId));
final myCode = await getPaymentCode(DerivePathType.bip44);
final Set<String> codesToCheck = {};
final nym = await PaynymIsApi().nym(myCode.toString());
if (nym.value != null) {
for (final follower in nym.value!.followers) {
codesToCheck.add(follower.code);
}
for (final following in nym.value!.following) {
codesToCheck.add(following.code);
}
}
final currentHeight = await chainHeight;
const storedHeight = 1; //await storedChainHeight;
@ -935,6 +981,7 @@ class BitcoinWallet extends CoinServiceAPI
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
await _checkCurrentReceivingAddressesForTransactions();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.4, walletId));
await checkAllCurrentReceivingPaynymAddressesForTransactions();
final fetchFuture = _refreshTransactions();
@ -955,6 +1002,7 @@ class BitcoinWallet extends CoinServiceAPI
.fire(RefreshPercentChangedEvent(0.80, walletId));
await fetchFuture;
await checkForNotificationTransactionsTo(codesToCheck);
await getAllTxsToWatch();
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.90, walletId));
@ -1305,15 +1353,18 @@ class BitcoinWallet extends CoinServiceAPI
secureStorage: secureStore,
getMnemonic: () => mnemonic,
getChainHeight: () => chainHeight,
getCurrentChangeAddress: () => currentChangeAddress,
getCurrentChangeAddress: () => currentChangeAddressP2PKH,
estimateTxFee: estimateTxFee,
prepareSend: prepareSend,
getTxCount: getTxCount,
fetchBuildTxData: fetchBuildTxData,
refresh: refresh,
checkChangeAddressForTransactions: _checkChangeAddressForTransactions,
checkChangeAddressForTransactions:
_checkP2PKHChangeAddressForTransactions,
addDerivation: addDerivation,
addDerivations: addDerivations,
dustLimitP2PKH: DUST_LIMIT_P2PKH,
minConfirms: MINIMUM_CONFIRMATIONS,
);
}
@ -1809,7 +1860,7 @@ class BitcoinWallet extends CoinServiceAPI
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
@ -1956,6 +2007,50 @@ class BitcoinWallet extends CoinServiceAPI
}
}
Future<void> _checkP2PKHChangeAddressForTransactions() async {
try {
final currentChange = await _currentChangeAddressP2PKH;
final int txCount = await getTxCount(address: currentChange.value);
Logging.instance.log(
'Number of txs for current change address $currentChange: $txCount',
level: LogLevel.Info);
if (txCount >= 1 || currentChange.derivationIndex < 0) {
// First increment the change index
final newChangeIndex = currentChange.derivationIndex + 1;
// Use new index to derive a new change address
final newChangeAddress = await _generateAddressForChain(
1, newChangeIndex, DerivePathType.bip44);
final existing = await db
.getAddresses(walletId)
.filter()
.valueEqualTo(newChangeAddress.value)
.findFirst();
if (existing == null) {
// Add that new change address
await db.putAddress(newChangeAddress);
} else {
// we need to update the address
await db.updateAddress(existing, newChangeAddress);
}
// keep checking until address with no tx history is set as current
await _checkP2PKHChangeAddressForTransactions();
}
} on SocketException catch (se, s) {
Logging.instance.log(
"SocketException caught in _checkReceivingAddressForTransactions(${DerivePathType.bip44}): $se\n$s",
level: LogLevel.Error);
return;
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip44}): $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
Future<void> _checkCurrentReceivingAddressesForTransactions() async {
try {
// for (final type in DerivePathType.values) {
@ -2185,6 +2280,8 @@ class BitcoinWallet extends CoinServiceAPI
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
level: LogLevel.Info);
Logging.instance.log("availableOutputs.length: ${availableOutputs.length}",
level: LogLevel.Info);
Logging.instance
.log("spendableOutputs: $spendableOutputs", level: LogLevel.Info);
Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue",
@ -2277,24 +2374,38 @@ class BitcoinWallet extends CoinServiceAPI
return transactionObject;
}
final int vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
satoshiAmountToSend,
satoshisBeingUsed - satoshiAmountToSend - 1
], // dust limit is the minimum amount a change output should be
))["vSize"] as int;
final int vSizeForOneOutput;
try {
vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
} catch (e) {
Logging.instance.log("vSizeForOneOutput: $e", level: LogLevel.Error);
rethrow;
}
final int vSizeForTwoOutPuts;
try {
vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(
1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
satoshiAmountToSend,
max(0, satoshisBeingUsed - satoshiAmountToSend - 1),
],
))["vSize"] as int;
} catch (e) {
Logging.instance.log("vSizeForTwoOutPuts: $e", level: LogLevel.Error);
rethrow;
}
// Assume 1 output, only for recipient and no change
final feeForOneOutput = estimateTxFee(

View file

@ -217,6 +217,14 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -1665,7 +1673,7 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
@ -2593,13 +2601,12 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB {
final n = output["n"];
if (n != null && n == utxosToUse[i].vout) {
String address = output["scriptPubKey"]["addresses"][0] as String;
if (bitbox.Address.detectFormat(address) ==
if (bitbox.Address.detectFormat(address) !=
bitbox.Address.formatCashAddr) {
if (validateCashAddr(address)) {
address = bitbox.Address.toLegacyAddress(address);
} else {
throw Exception(
"Unsupported address found during fetchBuildTxData(): $address");
try {
address = bitbox.Address.toCashAddress(address);
} catch (_) {
rethrow;
}
}
if (!addressTxid.containsKey(address)) {

View file

@ -226,6 +226,14 @@ class DogecoinWallet extends CoinServiceAPI
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -1612,7 +1620,7 @@ class DogecoinWallet extends CoinServiceAPI
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});

View file

@ -1416,6 +1416,14 @@ class EpicCashWallet extends CoinServiceAPI
});
await updateCachedChainHeight(latestHeight!);
if (latestHeight! > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return latestHeight!;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",

View file

@ -3665,7 +3665,7 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});
@ -4897,6 +4897,14 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive {
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",

View file

@ -241,6 +241,14 @@ class LitecoinWallet extends CoinServiceAPI
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -1776,7 +1784,7 @@ class LitecoinWallet extends CoinServiceAPI
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});

View file

@ -236,6 +236,14 @@ class NamecoinWallet extends CoinServiceAPI
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -1755,7 +1763,7 @@ class NamecoinWallet extends CoinServiceAPI
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});

View file

@ -231,6 +231,14 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
final result = await _electrumXClient.getBlockHeadTip();
final height = result["height"] as int;
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
@ -1651,7 +1659,7 @@ class ParticlWallet extends CoinServiceAPI with WalletCache, WalletDB {
// TODO move this out of here and into IDB
await db.isar.writeTxn(() async {
await db.isar.utxos.clear();
await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll();
await db.isar.utxos.putAll(outputArray);
});

View file

@ -189,14 +189,16 @@ mixin ElectrumXParsing {
TransactionSubType txSubType = TransactionSubType.none;
if (this is PaynymWalletInterface && outs.length > 1 && ins.isNotEmpty) {
List<String>? scriptChunks = outs[1].scriptPubKeyAsm?.split(" ");
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
final blindedPaymentCode = scriptChunks![1];
final bytes = blindedPaymentCode.fromHex;
for (int i = 0; i < outs.length; i++) {
List<String>? scriptChunks = outs[i].scriptPubKeyAsm?.split(" ");
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
final blindedPaymentCode = scriptChunks![1];
final bytes = blindedPaymentCode.fromHex;
// https://en.bitcoin.it/wiki/BIP_0047#Sending
if (bytes.length == 80 && bytes.first == 1) {
txSubType = TransactionSubType.bip47Notification;
// https://en.bitcoin.it/wiki/BIP_0047#Sending
if (bytes.length == 80 && bytes.first == 1) {
txSubType = TransactionSubType.bip47Notification;
}
}
}
}

View file

@ -15,8 +15,8 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart';
import 'package:stackwallet/exceptions/wallet/paynym_send_exception.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/utilities/bip32_utils.dart';
import 'package:stackwallet/utilities/bip47_utils.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
@ -35,6 +35,8 @@ mixin PaynymWalletInterface {
late final MainDB _db;
late final ElectrumX _electrumXClient;
late final SecureStorageInterface _secureStorage;
late final int _dustLimitP2PKH;
late final int _minConfirms;
// passed in wallet functions
late final Future<List<String>> Function() _getMnemonic;
@ -79,6 +81,8 @@ mixin PaynymWalletInterface {
required MainDB db,
required ElectrumX electrumXClient,
required SecureStorageInterface secureStorage,
required int dustLimitP2PKH,
required int minConfirms,
required Future<List<String>> Function() getMnemonic,
required Future<int> Function() getChainHeight,
required Future<String> Function() getCurrentChangeAddress,
@ -125,6 +129,8 @@ mixin PaynymWalletInterface {
_db = db;
_electrumXClient = electrumXClient;
_secureStorage = secureStorage;
_dustLimitP2PKH = dustLimitP2PKH;
_minConfirms = minConfirms;
_getMnemonic = getMnemonic;
_getChainHeight = getChainHeight;
_getCurrentChangeAddress = getCurrentChangeAddress;
@ -142,21 +148,33 @@ mixin PaynymWalletInterface {
btc_dart.NetworkType get networkType => _network;
Future<Address> currentReceivingPaynymAddress(PaymentCode sender) async {
final key = await lookupKey(sender.toString());
final keys = await lookupKey(sender.toString());
final address = await _db
.getAddresses(_walletId)
.filter()
.subTypeEqualTo(AddressSubType.paynymReceive)
.and()
.otherDataEqualTo(key)
.and()
.otherDataIsNotNull()
.anyOf<String, Address>(keys, (q, String e) => q.otherDataEqualTo(e))
.sortByDerivationIndexDesc()
.findFirst();
if (address == null) {
final generatedAddress = await _generatePaynymReceivingAddress(sender, 0);
await _db.putAddress(generatedAddress);
final existing = await _db
.getAddresses(_walletId)
.filter()
.valueEqualTo(generatedAddress.value)
.findFirst();
if (existing == null) {
// Add that new address
await _db.putAddress(generatedAddress);
} else {
// we need to update the address
await _db.updateAddress(existing, generatedAddress);
}
return currentReceivingPaynymAddress(sender);
} else {
return address;
@ -197,7 +215,22 @@ mixin PaynymWalletInterface {
sender,
address.derivationIndex + 1,
);
await _db.putAddress(nextAddress);
final existing = await _db
.getAddresses(_walletId)
.filter()
.valueEqualTo(nextAddress.value)
.findFirst();
if (existing == null) {
// Add that new address
await _db.putAddress(nextAddress);
} else {
// we need to update the address
await _db.updateAddress(existing, nextAddress);
}
// keep checking until address with no tx history is set as current
await checkCurrentPaynymReceivingAddressForTransactions(sender);
}
}
@ -296,15 +329,13 @@ mixin PaynymWalletInterface {
const maxCount = 2147483647;
for (int i = startIndex; i < maxCount; i++) {
final key = await lookupKey(pCode.toString());
final keys = await lookupKey(pCode.toString());
final address = await _db
.getAddresses(_walletId)
.filter()
.subTypeEqualTo(AddressSubType.paynymSend)
.and()
.otherDataEqualTo(key)
.and()
.otherDataIsNotNull()
.anyOf<String, Address>(keys, (q, String e) => q.otherDataEqualTo(e))
.and()
.derivationIndexEqualTo(i)
.findFirst();
@ -353,7 +384,7 @@ mixin PaynymWalletInterface {
int additionalOutputs = 0,
List<UTXO>? utxos,
}) async {
const amountToSend = DUST_LIMIT;
final amountToSend = _dustLimitP2PKH;
final List<UTXO> availableOutputs =
utxos ?? await _db.getUTXOs(_walletId).findAll();
final List<UTXO> spendableOutputs = [];
@ -362,8 +393,8 @@ mixin PaynymWalletInterface {
// Build list of spendable outputs and totaling their satoshi amount
for (var i = 0; i < availableOutputs.length; i++) {
if (availableOutputs[i].isBlocked == false &&
availableOutputs[i].isConfirmed(
await _getChainHeight(), MINIMUM_CONFIRMATIONS) ==
availableOutputs[i]
.isConfirmed(await _getChainHeight(), _minConfirms) ==
true) {
spendableOutputs.add(availableOutputs[i]);
spendableSatoshiValue += availableOutputs[i].value;
@ -433,22 +464,24 @@ mixin PaynymWalletInterface {
feeRatePerKB: selectedTxFeeRate,
);
if (feeForNoChange < vSizeForNoChange * 1000) {
feeForNoChange = vSizeForNoChange * 1000;
}
if (feeForWithChange < vSizeForWithChange * 1000) {
feeForWithChange = vSizeForWithChange * 1000;
if (_coin == Coin.dogecoin || _coin == Coin.dogecoinTestNet) {
if (feeForNoChange < vSizeForNoChange * 1000) {
feeForNoChange = vSizeForNoChange * 1000;
}
if (feeForWithChange < vSizeForWithChange * 1000) {
feeForWithChange = vSizeForWithChange * 1000;
}
}
if (satoshisBeingUsed - amountToSend > feeForNoChange + DUST_LIMIT) {
if (satoshisBeingUsed - amountToSend > feeForNoChange + _dustLimitP2PKH) {
// try to add change output due to "left over" amount being greater than
// the estimated fee + the dust limit
int changeAmount = satoshisBeingUsed - amountToSend - feeForWithChange;
// check estimates are correct and build notification tx
if (changeAmount >= DUST_LIMIT &&
if (changeAmount >= _dustLimitP2PKH &&
satoshisBeingUsed - amountToSend - changeAmount == feeForWithChange) {
final txn = await _createNotificationTx(
var txn = await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
@ -457,6 +490,18 @@ mixin PaynymWalletInterface {
int feeBeingPaid = satoshisBeingUsed - amountToSend - changeAmount;
// make sure minimum fee is accurate if that is being used
if (txn.item2 - feeBeingPaid == 1) {
changeAmount -= 1;
feeBeingPaid += 1;
txn = await _createNotificationTx(
targetPaymentCodeString: targetPaymentCodeString,
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
change: changeAmount,
);
}
Map<String, dynamic> transactionObject = {
"hex": txn.item1,
"recipientPaynym": targetPaymentCodeString,
@ -569,10 +614,13 @@ mixin PaynymWalletInterface {
txb.addInput(
utxo.txid,
txPointIndex,
null,
utxoSigningData[utxo.txid]["output"] as Uint8List,
);
// todo: modify address once segwit support is in our bip47
txb.addOutput(targetPaymentCode.notificationAddressP2PKH(), DUST_LIMIT);
txb.addOutput(
targetPaymentCode.notificationAddressP2PKH(), _dustLimitP2PKH);
txb.addOutput(opReturnScript, 0);
// TODO: add possible change output and mark output as dangerous
@ -586,15 +634,18 @@ mixin PaynymWalletInterface {
txb.sign(
vin: 0,
keyPair: myKeyPair,
witnessValue: utxo.value,
witnessScript: utxoSigningData[utxo.txid]["redeemScript"] as Uint8List?,
);
// sign rest of possible inputs
for (var i = 1; i < utxosToUse.length - 1; i++) {
for (var i = 1; i < utxosToUse.length; i++) {
final txid = utxosToUse[i].txid;
txb.sign(
vin: i,
keyPair: utxoSigningData[txid]["keyPair"] as btc_dart.ECPair,
// witnessValue: utxosToUse[i].value,
witnessValue: utxosToUse[i].value,
witnessScript: utxoSigningData[utxo.txid]["redeemScript"] as Uint8List?,
);
}
@ -655,6 +706,13 @@ mixin PaynymWalletInterface {
if (paymentCodeString == unBlindedPaymentCode.toString()) {
return true;
}
if (tx.address.value?.otherData != null) {
final code = await paymentCodeStringByKey(tx.address.value!.otherData!);
if (code == paymentCodeString) {
return true;
}
}
}
// otherwise return no
@ -671,8 +729,13 @@ mixin PaynymWalletInterface {
}
try {
final blindedCode =
transaction.outputs.elementAt(1).scriptPubKeyAsm!.split(" ")[1];
final blindedCodeBytes =
Bip47Utils.getBlindedPaymentCodeBytesFrom(transaction);
// transaction does not contain a payment code
if (blindedCodeBytes == null) {
return null;
}
final designatedInput = transaction.inputs.first;
@ -693,7 +756,7 @@ mixin PaynymWalletInterface {
final mask = PaymentCode.getMask(S.ecdhSecret(), rev);
final unBlindedPayload = PaymentCode.blind(blindedCode.fromHex, mask);
final unBlindedPayload = PaymentCode.blind(blindedCodeBytes, mask);
final unBlindedPaymentCode =
PaymentCode.initFromPayload(unBlindedPayload);
@ -718,33 +781,99 @@ mixin PaynymWalletInterface {
.subTypeEqualTo(TransactionSubType.bip47Notification)
.findAll();
List<PaymentCode> unBlindedList = [];
List<PaymentCode> codes = [];
for (final tx in txns) {
final unBlinded = await unBlindedPaymentCodeFromTransaction(
transaction: tx,
myNotificationAddress: myAddress,
);
if (unBlinded != null &&
unBlindedList
.where((e) => e.toString() == unBlinded.toString())
.isEmpty) {
unBlindedList.add(unBlinded);
// tx is sent so we can check the address's otherData for the code String
if (tx.type == TransactionType.outgoing &&
tx.address.value?.otherData != null) {
final codeString =
await paymentCodeStringByKey(tx.address.value!.otherData!);
if (codeString != null &&
codes.where((e) => e.toString() == codeString).isEmpty) {
codes.add(PaymentCode.fromPaymentCode(codeString, _network));
}
} else {
// otherwise we need to un blind the code
final unBlinded = await unBlindedPaymentCodeFromTransaction(
transaction: tx,
myNotificationAddress: myAddress,
);
if (unBlinded != null &&
codes.where((e) => e.toString() == unBlinded.toString()).isEmpty) {
codes.add(unBlinded);
}
}
}
return unBlindedList;
return codes;
}
Future<void> checkForNotificationTransactionsTo(
Set<String> otherCodeStrings) async {
final sentNotificationTransactions = await _db
.getTransactions(_walletId)
.filter()
.subTypeEqualTo(TransactionSubType.bip47Notification)
.and()
.typeEqualTo(TransactionType.outgoing)
.findAll();
final List<PaymentCode> codes = [];
for (final codeString in otherCodeStrings) {
codes.add(PaymentCode.fromPaymentCode(codeString, _network));
}
for (final tx in sentNotificationTransactions) {
if (tx.address.value != null && tx.address.value!.otherData == null) {
final oldAddress =
await _db.getAddress(_walletId, tx.address.value!.value);
for (final code in codes) {
final notificationAddress = code.notificationAddressP2PKH();
if (notificationAddress == oldAddress!.value) {
final address = Address(
walletId: _walletId,
value: notificationAddress,
publicKey: [],
derivationIndex: 0,
type: oldAddress.type,
subType: AddressSubType.paynymNotification,
otherData: await storeCode(code.toString()),
);
await _db.updateAddress(oldAddress, address);
}
}
}
}
}
Future<void> restoreAllHistory({
required int maxUnusedAddressGap,
required int maxNumberOfIndexesToCheck,
required Set<String> paymentCodeStrings,
}) async {
final codes = await getAllPaymentCodesFromNotificationTransactions();
final List<PaymentCode> extraCodes = [];
for (final codeString in paymentCodeStrings) {
if (codes.where((e) => e.toString() == codeString).isEmpty) {
final extraCode = PaymentCode.fromPaymentCode(codeString, _network);
if (extraCode.isValid()) {
extraCodes.add(extraCode);
}
}
}
codes.addAll(extraCodes);
final List<Future<void>> futures = [];
for (final code in codes) {
futures.add(restoreHistoryWith(
code, maxUnusedAddressGap, maxNumberOfIndexesToCheck));
futures.add(
restoreHistoryWith(
code,
maxUnusedAddressGap,
maxNumberOfIndexesToCheck,
),
);
}
await Future.wait(futures);
@ -1082,16 +1211,17 @@ mixin PaynymWalletInterface {
}
/// look up a key that corresponds to a payment code string
Future<String?> lookupKey(String paymentCodeString) async {
Future<List<String>> lookupKey(String paymentCodeString) async {
final keys =
(await _secureStorage.keys).where((e) => e.startsWith(kPCodeKeyPrefix));
final List<String> result = [];
for (final key in keys) {
final value = await _secureStorage.read(key: key);
if (value == paymentCodeString) {
return key;
result.add(key);
}
}
return null;
return result;
}
/// fetch a payment code string

View file

@ -194,6 +194,8 @@ class _SVG {
String get exitDesktop => "assets/svg/exit-desktop.svg";
String get keys => "assets/svg/keys.svg";
String get arrowDown => "assets/svg/arrow-down.svg";
String get robotHead => "assets/svg/robot-head.svg";
String get whirlPool => "assets/svg/whirlpool.svg";
String get ellipse1 => "assets/svg/Ellipse-43.svg";
String get ellipse2 => "assets/svg/Ellipse-42.svg";
@ -262,6 +264,7 @@ class _PNG {
const _PNG();
String get stack => "assets/images/stack.png";
String get unclaimedPaynym => "assets/images/unclaimed.png";
String get splash => "assets/images/splash.png";
String get monero => "assets/images/monero.png";

View file

@ -0,0 +1,37 @@
import 'dart:typed_data';
import 'package:bip47/src/util.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/output.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
abstract class Bip47Utils {
/// looks at tx outputs and returns a blinded payment code if found
static Uint8List? getBlindedPaymentCodeBytesFrom(Transaction transaction) {
for (int i = 0; i < transaction.outputs.length; i++) {
final bytes = getBlindedPaymentCodeBytesFromOutput(
transaction.outputs.elementAt(i));
if (bytes != null) {
return bytes;
}
}
return null;
}
static Uint8List? getBlindedPaymentCodeBytesFromOutput(Output output) {
Uint8List? blindedCodeBytes;
List<String>? scriptChunks = output.scriptPubKeyAsm?.split(" ");
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
final blindedPaymentCode = scriptChunks![1];
final bytes = blindedPaymentCode.fromHex;
// https://en.bitcoin.it/wiki/BIP_0047#Sending
if (bytes.length == 80 && bytes.first == 1) {
blindedCodeBytes = bytes;
}
}
return blindedCodeBytes;
}
}

View file

@ -3,7 +3,7 @@ abstract class FeaturedPaynyms {
// static const String samouraiWalletDevFund =
// "PM8TJYkuSdYXJnwDBq8ChfinfXv3srxhQrx3eoEwbSw51wMjdo9JJ2DsycwT3gt3zHQ7cV1grvabMmmf1Btj6fY7tgkgSz9B8MZuR3kjYfgMLMURJCXN";
static const String stackWallet =
"PM8TJPdEeH3A77h4xJYQeXPWix2W5yAJrzVQ8ggET1n92utnc57FXCoH94Z2wUSJNfGwkX1kNDTCQLkHecVsjQHGkDE8MUyWE4xWJcc1EDDYCeSSBfLL";
"PM8TJdQcNk27JpxGRtNR7Hnh8VkJk4Nf17BthLx89fM3iX3UL2YshyaiTAvKgTCVvpgsAgY1DbojkAaUd3Rcn48NEn4uUBuqkaSddgKL8TPAAEQXNuE6";
static Map<String, String> get featured => {
"Stack Wallet": stackWallet,

View file

@ -13,31 +13,31 @@ class STextStyles {
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
}
}
@ -932,6 +932,76 @@ class STextStyles {
}
}
static TextStyle w600_14(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
}
}
static TextStyle w500_14(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
}
}
static TextStyle w500_12(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
@ -967,6 +1037,41 @@ class STextStyles {
}
}
static TextStyle w500_10(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 10,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 10,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 10,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 10,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 10,
);
}
}
static TextStyle syncPercent(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
@ -1030,7 +1135,7 @@ class STextStyles {
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark,
color: _theme(context).bottomNavIconIcon,
fontWeight: FontWeight.w500,
fontSize: 12,
);

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
abstract class Util {
@ -22,6 +23,14 @@ abstract class Util {
return Platform.isLinux || Platform.isMacOS || Platform.isWindows;
}
static Future<bool> get isIPad async {
final deviceInfo = (await DeviceInfoPlugin().deviceInfo);
if (deviceInfo is IosDeviceInfo) {
return (deviceInfo).name?.toLowerCase().contains("ipad") == true;
}
return false;
}
static MaterialColor createMaterialColor(Color color) {
List<double> strengths = <double>[.05];
final swatch = <int, Color>{};

View file

@ -1,17 +1,17 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/ui/color_theme_provider.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
class BlueTextButton extends ConsumerStatefulWidget {
const BlueTextButton({
class _CustomTextButton extends StatefulWidget {
const _CustomTextButton({
Key? key,
required this.text,
required this.enabledColor,
required this.disabledColor,
this.onTap,
this.enabled = true,
this.textSize,
@ -21,12 +21,14 @@ class BlueTextButton extends ConsumerStatefulWidget {
final VoidCallback? onTap;
final bool enabled;
final double? textSize;
final Color enabledColor;
final Color disabledColor;
@override
ConsumerState<BlueTextButton> createState() => _BlueTextButtonState();
State<_CustomTextButton> createState() => _CustomTextButtonState();
}
class _BlueTextButtonState extends ConsumerState<BlueTextButton>
class _CustomTextButtonState extends State<_CustomTextButton>
with SingleTickerProviderStateMixin {
AnimationController? controller;
Animation<dynamic>? animation;
@ -37,18 +39,14 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton>
@override
void initState() {
if (widget.enabled) {
color = ref.read(colorThemeProvider.state).state.buttonTextBorderless;
color = widget.enabledColor;
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
);
animation = ColorTween(
begin: ref.read(colorThemeProvider.state).state.buttonTextBorderless,
end: ref
.read(colorThemeProvider.state)
.state
.buttonTextBorderless
.withOpacity(0.4),
begin: widget.enabledColor,
end: widget.enabledColor.withOpacity(0.4),
).animate(controller!);
animation!.addListener(() {
@ -57,7 +55,7 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton>
});
});
} else {
color = ref.read(colorThemeProvider.state).state.textSubtitle1;
color = widget.disabledColor;
}
super.initState();
@ -116,3 +114,32 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton>
);
}
}
class CustomTextButton extends StatelessWidget {
const CustomTextButton({
Key? key,
required this.text,
this.onTap,
this.enabled = true,
this.textSize,
}) : super(key: key);
final String text;
final VoidCallback? onTap;
final bool enabled;
final double? textSize;
@override
Widget build(BuildContext context) {
return _CustomTextButton(
key: UniqueKey(),
text: text,
enabledColor:
Theme.of(context).extension<StackColors>()!.buttonTextBorderless,
disabledColor: Theme.of(context).extension<StackColors>()!.textSubtitle1,
enabled: enabled,
textSize: textSize,
onTap: onTap,
);
}
}

View file

@ -272,8 +272,8 @@ class _PaynymFollowToggleButtonState
switch (widget.style) {
case PaynymFollowToggleButtonStyle.primary:
return PrimaryButton(
width: isDesktop ? 120 : 84,
buttonHeight: isDesktop ? ButtonHeight.s : ButtonHeight.l,
width: isDesktop ? 120 : 100,
buttonHeight: isDesktop ? ButtonHeight.s : ButtonHeight.xl,
label: isFollowing ? "Unfollow" : "Follow",
onPressed: _onPressed,
);
@ -281,15 +281,15 @@ class _PaynymFollowToggleButtonState
case PaynymFollowToggleButtonStyle.detailsPopup:
return SecondaryButton(
label: isFollowing ? "Unfollow" : "Follow",
buttonHeight: ButtonHeight.l,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: SvgPicture.asset(
isFollowing ? Assets.svg.userMinus : Assets.svg.userPlus,
width: 10,
height: 10,
width: 16,
height: 16,
color:
Theme.of(context).extension<StackColors>()!.buttonTextSecondary,
),
iconSpacing: 4,
onPressed: _onPressed,
);

View file

@ -78,6 +78,16 @@ class SecondaryButton extends StatelessWidget {
.buttonTextSecondaryDisabled,
);
}
if (buttonHeight == ButtonHeight.xl) {
return STextStyles.button(context).copyWith(
fontSize: 14,
color: enabled
? Theme.of(context).extension<StackColors>()!.buttonTextSecondary
: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondaryDisabled,
);
}
return STextStyles.button(context).copyWith(
color: enabled
? Theme.of(context).extension<StackColors>()!.buttonTextSecondary

View file

@ -261,7 +261,7 @@ class _NodeCardState extends ConsumerState<NodeCard> {
const SizedBox(
width: 66,
),
BlueTextButton(
CustomTextButton(
text: "Connect",
enabled: _status == "Disconnected",
onTap: () async {
@ -285,7 +285,7 @@ class _NodeCardState extends ConsumerState<NodeCard> {
const SizedBox(
width: 48,
),
BlueTextButton(
CustomTextButton(
text: "Details",
onTap: () {
Navigator.of(context).pushNamed(

View file

@ -6,7 +6,6 @@ import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/blockchain/dogecoin/current_height_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -121,7 +120,8 @@ class _TransactionCardState extends ConsumerState<TransactionCard> {
}
}
final currentHeight = ref.watch(currentHeightProvider(coin).state).state;
final currentHeight = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).currentHeight));
return Material(
color: Theme.of(context).extension<StackColors>()!.popupBG,

View file

@ -14,7 +14,7 @@ import flutter_secure_storage_macos
import isar_flutter_libs
import package_info_plus_macos
import path_provider_macos
import share_plus_macos
import share_plus
import shared_preferences_macos
import stack_wallet_backup
import url_launcher_macos

View file

@ -1237,21 +1237,7 @@ packages:
name: share_plus
url: "https://pub.dartlang.org"
source: hosted
version: "4.5.3"
share_plus_linux:
dependency: transitive
description:
name: share_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
share_plus_macos:
dependency: transitive
description:
name: share_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "6.3.0"
share_plus_platform_interface:
dependency: transitive
description:
@ -1259,20 +1245,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
share_plus_web:
dependency: transitive
description:
name: share_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
share_plus_windows:
dependency: transitive
description:
name: share_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
shared_preferences:
dependency: transitive
description:

View file

@ -11,7 +11,7 @@ description: Stack Wallet
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.5.36+113
version: 1.5.36+117
environment:
sdk: ">=2.17.0 <3.0.0"
@ -126,7 +126,7 @@ dependencies:
tuple: ^2.0.0
flutter_riverpod: ^1.0.3
qr_flutter: ^4.0.0
share_plus: ^4.0.10
share_plus: ^6.3.0
emojis: ^0.9.9
pointycastle: ^3.6.0
package_info_plus: ^1.4.2
@ -205,6 +205,7 @@ flutter:
- assets/svg/circle-check.svg
- assets/svg/clipboard.svg
- assets/images/stack.png
- assets/images/unclaimed.png
- assets/images/monero.png
- assets/images/wownero.png
- assets/images/firo.png
@ -309,6 +310,8 @@ flutter:
- assets/svg/plus-circle.svg
- assets/svg/circle-plus-filled.svg
- assets/svg/configuration.svg
- assets/svg/robot-head.svg
- assets/svg/whirlpool.svg
# coin icons
- assets/svg/coin_icons/Bitcoin.svg
- assets/svg/coin_icons/Litecoin.svg

View file

@ -80,6 +80,8 @@ void main() {
when(wallets.getManager("wallet-id"))
.thenAnswer((realInvocation) => Manager(wallet));
when(wallet.storedChainHeight).thenAnswer((_) => 6000000);
//
await tester.pumpWidget(
ProviderScope(
@ -173,6 +175,7 @@ void main() {
.thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00));
when(wallet.coin).thenAnswer((_) => Coin.firo);
when(wallet.storedChainHeight).thenAnswer((_) => 6000000);
when(wallets.getManager("wallet-id"))
.thenAnswer((realInvocation) => Manager(wallet));
@ -271,6 +274,8 @@ void main() {
when(wallets.getManager("wallet-id"))
.thenAnswer((realInvocation) => Manager(wallet));
when(wallet.storedChainHeight).thenAnswer((_) => 6000000);
await tester.pumpWidget(
ProviderScope(
overrides: [
@ -358,6 +363,8 @@ void main() {
when(wallets.getManager("wallet id"))
.thenAnswer((realInvocation) => Manager(wallet));
when(wallet.storedChainHeight).thenAnswer((_) => 6000000);
mockingjay
.when(() => navigator.pushNamed("/transactionDetails",
arguments: Tuple3(tx, Coin.firo, "wallet id")))
@ -395,6 +402,7 @@ void main() {
verify(mockPrefs.currency).called(2);
verify(mockLocaleService.locale).called(4);
verify(wallet.coin.ticker).called(2);
verify(wallet.storedChainHeight).called(2);
verifyNoMoreInteractions(wallet);
verifyNoMoreInteractions(mockLocaleService);

View file

@ -11,6 +11,7 @@
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <stack_wallet_backup/stack_wallet_backup_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_size/window_size_plugin.h>
@ -26,6 +27,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
StackWalletBackupPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("StackWalletBackupPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(

View file

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows
isar_flutter_libs
permission_handler_windows
share_plus
stack_wallet_backup
url_launcher_windows
window_size