mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
implement-payjoin (#1949)
* Initial Payjoin * Initial Payjoin * More payjoin stuff * Minor fixes * Minor fixes * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Fix minor bug causes by data inconsistency in the btc utxos * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Initial Payjoin * Initial Payjoin * More payjoin stuff * Minor fixes * Minor fixes * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Fix minor bug causes by data inconsistency in the btc utxos * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Fix Rebase issues * Move PJ Receiver to isolate * Add Payjoin Setting * Payjoin Sender are now isolated * Added Payjoin sessions to tx overview. Fix Fee issue with payjoin * Clean up code * Fix taproot for payjoin * Fix CI Errors * Add Payjoin UI elements and details page * Add Payjoin UI elements and details page * Fix Translations * feat: Detect Payjoin URIs in pasted text and show to the User sending Payjoin * feat: rename pjUri to payjoinURI for more code clarity * Update res/values/strings_pl.arb Co-authored-by: cyan <cyjan@mrcyjanek.net> * Update cw_bitcoin/lib/payjoin/manager.dart Co-authored-by: cyan <cyjan@mrcyjanek.net> * Update cw_bitcoin/lib/payjoin/manager.dart Co-authored-by: cyan <cyjan@mrcyjanek.net> * feat: Disable Payjoin per default * feat: Disable Payjoin fully if disabled or no Inputs available * feat: Resume Payjoin if app comes back to foreground * chore: Revert overly aggressive code formats * feat: show correct Payjoin amount for receivers * feat: Improved payjoin status * feat: Show payjoin errors on payjoin details screen * deps: update flutter to 3.27.4 * feat: Revert localisations * bug: Remove duplicate transaction id on payjoin details * style: remove double await in payjoin sender * refactor(cw_bitcoin): Refactor method signatures and convert constructor to factory * refactor(cw_bitcoin): Refactor wallet service and PSBT signer for cleaner code Removed unnecessary `CakeHive` dependency and refactored `BitcoinWallet` initialization to use `payjoinSessionSource`. Improved code readability in `PsbtSigner` by reformatting lines and simplifying constructor methods for `UtxoWithPrivateKey`. * fix: Resume Payjoin Sessions and load PJUri after sleep * feat: Add "Copy Payjoin URL button" to receive screen * fix: Add "Payjoin enabled"-Box below QR Code on the receive screen * fix: Set payjoin_enabled color to black independent of the theme * refactor: Payjoin session management and cleanup unused code. --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> Co-authored-by: cyan <cyjan@mrcyjanek.net>
This commit is contained in:
co-authored by
cyan
Omar Hatem
parent
4a08e18f00
commit
82e3ebf4fa
+62
-20
@@ -109,26 +109,29 @@ class CWBitcoin extends Bitcoin {
|
||||
required TransactionPriority priority,
|
||||
int? feeRate,
|
||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
String? payjoinUri,
|
||||
}) {
|
||||
final bitcoinFeeRate =
|
||||
priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
|
||||
priority == BitcoinTransactionPriority.custom && feeRate != null
|
||||
? feeRate
|
||||
: null;
|
||||
return BitcoinTransactionCredentials(
|
||||
outputs
|
||||
.map((out) => OutputInfo(
|
||||
fiatAmount: out.fiatAmount,
|
||||
cryptoAmount: out.cryptoAmount,
|
||||
address: out.address,
|
||||
note: out.note,
|
||||
sendAll: out.sendAll,
|
||||
extractedAddress: out.extractedAddress,
|
||||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount,
|
||||
memo: out.memo))
|
||||
.toList(),
|
||||
priority: priority as BitcoinTransactionPriority,
|
||||
feeRate: bitcoinFeeRate,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
outputs
|
||||
.map((out) => OutputInfo(
|
||||
fiatAmount: out.fiatAmount,
|
||||
cryptoAmount: out.cryptoAmount,
|
||||
address: out.address,
|
||||
note: out.note,
|
||||
sendAll: out.sendAll,
|
||||
extractedAddress: out.extractedAddress,
|
||||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount,
|
||||
memo: out.memo))
|
||||
.toList(),
|
||||
priority: priority as BitcoinTransactionPriority,
|
||||
feeRate: bitcoinFeeRate,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
payjoinUri: payjoinUri);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -224,9 +227,14 @@ class CWBitcoin extends Bitcoin {
|
||||
await bitcoinWallet.updateAllUnspents();
|
||||
}
|
||||
|
||||
WalletService createBitcoinWalletService(Box<WalletInfo> walletInfoSource,
|
||||
Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect) {
|
||||
return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
|
||||
WalletService createBitcoinWalletService(
|
||||
Box<WalletInfo> walletInfoSource,
|
||||
Box<UnspentCoinsInfo> unspentCoinSource,
|
||||
Box<PayjoinSession> payjoinSessionSource,
|
||||
bool alwaysScan,
|
||||
bool isDirect) {
|
||||
return BitcoinWalletService(walletInfoSource, unspentCoinSource,
|
||||
payjoinSessionSource, alwaysScan, isDirect);
|
||||
}
|
||||
|
||||
WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource,
|
||||
@@ -550,6 +558,10 @@ class CWBitcoin extends Bitcoin {
|
||||
return option is BitcoinReceivePageOption;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isPayjoinAvailable(Object wallet) =>
|
||||
(wallet is BitcoinWallet) && (wallet as BitcoinWallet).isPayjoinAvailable;
|
||||
|
||||
@override
|
||||
BitcoinAddressType getOptionToType(ReceivePageOption option) {
|
||||
return (option as BitcoinReceivePageOption).toType();
|
||||
@@ -706,4 +718,34 @@ class CWBitcoin extends Bitcoin {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String getPayjoinEndpoint(Object wallet) {
|
||||
final _wallet = wallet as ElectrumWallet;
|
||||
if (!isPayjoinAvailable(wallet)) return '';
|
||||
return (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void updatePayjoinState(Object wallet, bool value) {
|
||||
final _wallet = wallet as ElectrumWallet;
|
||||
if (value) {
|
||||
(_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin();
|
||||
} else {
|
||||
stopPayjoinSessions(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void resumePayjoinSessions(Object wallet) {
|
||||
final _wallet = wallet as ElectrumWallet;
|
||||
(_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin();
|
||||
}
|
||||
|
||||
@override
|
||||
void stopPayjoinSessions(Object wallet) {
|
||||
final _wallet = wallet as ElectrumWallet;
|
||||
(_wallet.walletAddresses as BitcoinWalletAddresses).payjoinManager.cleanupSessions();
|
||||
(_wallet.walletAddresses as BitcoinWalletAddresses).currentPayjoinReceiver = null;
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -51,6 +51,7 @@ import 'package:cake_wallet/entities/wallet_manager.dart';
|
||||
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
|
||||
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
|
||||
import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
|
||||
import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
|
||||
@@ -58,7 +59,10 @@ import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
|
||||
import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
|
||||
import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
|
||||
import 'package:cw_core/payjoin_session.dart';
|
||||
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
|
||||
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
|
||||
import 'package:cake_wallet/entities/preferences_key.dart';
|
||||
@@ -286,6 +290,7 @@ late Box<ExchangeTemplate> _exchangeTemplates;
|
||||
late Box<TransactionDescription> _transactionDescriptionBox;
|
||||
late Box<Order> _ordersSource;
|
||||
late Box<UnspentCoinsInfo> _unspentCoinsInfoSource;
|
||||
late Box<PayjoinSession> _payjoinSessionSource;
|
||||
late Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource;
|
||||
|
||||
Future<void> setup({
|
||||
@@ -299,6 +304,7 @@ Future<void> setup({
|
||||
required Box<TransactionDescription> transactionDescriptionBox,
|
||||
required Box<Order> ordersSource,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
|
||||
required Box<PayjoinSession> payjoinSessionSource,
|
||||
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfoSource,
|
||||
required SecureStorage secureStorage,
|
||||
required GlobalKey<NavigatorState> navigatorKey,
|
||||
@@ -313,6 +319,7 @@ Future<void> setup({
|
||||
_transactionDescriptionBox = transactionDescriptionBox;
|
||||
_ordersSource = ordersSource;
|
||||
_unspentCoinsInfoSource = unspentCoinsInfoSource;
|
||||
_payjoinSessionSource = payjoinSessionSource;
|
||||
_anonpayInvoiceInfoSource = anonpayInvoiceInfoSource;
|
||||
|
||||
if (!_isSetupFinished) {
|
||||
@@ -354,6 +361,8 @@ Future<void> setup({
|
||||
TradesStore(tradesSource: _tradesSource, settingsStore: getIt.get<SettingsStore>()));
|
||||
getIt.registerSingleton<OrdersStore>(
|
||||
OrdersStore(ordersSource: _ordersSource, settingsStore: getIt.get<SettingsStore>()));
|
||||
getIt.registerFactory(() =>
|
||||
PayjoinTransactionsStore(payjoinSessionSource: _payjoinSessionSource));
|
||||
getIt.registerSingleton<TradeFilterStore>(TradeFilterStore());
|
||||
getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore(getIt.get<AppStore>()));
|
||||
getIt.registerSingleton<FiatConversionStore>(FiatConversionStore());
|
||||
@@ -507,6 +516,7 @@ Future<void> setup({
|
||||
yatStore: getIt.get<YatStore>(),
|
||||
ordersStore: getIt.get<OrdersStore>(),
|
||||
anonpayTransactionsStore: getIt.get<AnonpayTransactionsStore>(),
|
||||
payjoinTransactionsStore: getIt.get<PayjoinTransactionsStore>(),
|
||||
sharedPreferences: getIt.get<SharedPreferences>(),
|
||||
keyService: getIt.get<KeyService>()));
|
||||
|
||||
@@ -1095,6 +1105,7 @@ Future<void> setup({
|
||||
return bitcoin!.createBitcoinWalletService(
|
||||
_walletInfoSource,
|
||||
_unspentCoinsInfoSource,
|
||||
_payjoinSessionSource,
|
||||
getIt.get<SettingsStore>().silentPaymentsAlwaysScan,
|
||||
SettingsStoreBase.walletPasswordDirectInput,
|
||||
);
|
||||
@@ -1423,6 +1434,15 @@ Future<void> setup({
|
||||
settingsStore: getIt.get<SettingsStore>(),
|
||||
));
|
||||
|
||||
getIt.registerFactoryParam<PayjoinDetailsViewModel, String, TransactionInfo?>(
|
||||
(String sessionId, TransactionInfo? transactionInfo) =>
|
||||
PayjoinDetailsViewModel(
|
||||
sessionId,
|
||||
transactionInfo,
|
||||
payjoinSessionSource: _payjoinSessionSource,
|
||||
settingsStore: getIt.get<SettingsStore>(),
|
||||
));
|
||||
|
||||
getIt.registerFactoryParam<AnonPayReceivePage, AnonpayInfoBase, void>(
|
||||
(AnonpayInfoBase anonpayInvoiceInfo, _) =>
|
||||
AnonPayReceivePage(invoiceInfo: anonpayInvoiceInfo));
|
||||
@@ -1431,6 +1451,11 @@ Future<void> setup({
|
||||
(AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsPage(
|
||||
anonpayDetailsViewModel: getIt.get<AnonpayDetailsViewModel>(param1: anonpayInvoiceInfo)));
|
||||
|
||||
getIt.registerFactoryParam<PayjoinDetailsPage, String, TransactionInfo?>(
|
||||
(String sessionId, TransactionInfo? transactionInfo) => PayjoinDetailsPage(
|
||||
payjoinDetailsViewModel: getIt.get<PayjoinDetailsViewModel>(
|
||||
param1: sessionId, param2: transactionInfo)));
|
||||
|
||||
getIt.registerFactoryParam<HomeSettingsPage, BalanceViewModel, void>((balanceViewModel, _) =>
|
||||
HomeSettingsPage(getIt.get<HomeSettingsViewModel>(param1: balanceViewModel)));
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ class PreferencesKey {
|
||||
static const lookupsOpenAlias = 'looks_up_open_alias';
|
||||
static const lookupsENS = 'looks_up_ens';
|
||||
static const lookupsWellKnown = 'looks_up_well_known';
|
||||
static const usePayjoin = 'use_payjoin';
|
||||
static const showCameraConsent = 'show_camera_consent';
|
||||
static const showDecredInfoCard = 'show_decred_info_card';
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import 'package:cw_core/cake_hive.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:cw_core/mweb_utxo.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/payjoin_session.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
@@ -178,6 +179,10 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
|
||||
CakeHive.registerAdapter(MwebUtxoAdapter());
|
||||
}
|
||||
|
||||
if (!CakeHive.isAdapterRegistered(PayjoinSession.typeId)) {
|
||||
CakeHive.registerAdapter(PayjoinSessionAdapter());
|
||||
}
|
||||
|
||||
final secureStorage = secureStorageShared;
|
||||
final transactionDescriptionsBoxKey =
|
||||
await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
|
||||
@@ -197,6 +202,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
|
||||
final exchangeTemplates = await CakeHive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
|
||||
final anonpayInvoiceInfo = await CakeHive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
|
||||
final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
|
||||
final payjoinSessionSource = await CakeHive.openBox<PayjoinSession>(PayjoinSession.boxName);
|
||||
|
||||
final havenSeedStoreBoxKey =
|
||||
await getEncryptionKey(secureStorage: secureStorage, forKey: HavenSeedStore.boxKey);
|
||||
@@ -219,6 +225,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
|
||||
exchangeTemplates: exchangeTemplates,
|
||||
transactionDescriptions: transactionDescriptions,
|
||||
secureStorage: secureStorage,
|
||||
payjoinSessionSource: payjoinSessionSource,
|
||||
anonpayInvoiceInfo: anonpayInvoiceInfo,
|
||||
havenSeedStore: havenSeedStore,
|
||||
initialMigrationVersion: 49,
|
||||
@@ -241,6 +248,7 @@ Future<void> initialSetup(
|
||||
required SecureStorage secureStorage,
|
||||
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
|
||||
required Box<PayjoinSession> payjoinSessionSource,
|
||||
required Box<HavenSeedStore> havenSeedStore,
|
||||
int initialMigrationVersion = 15, }) async {
|
||||
LanguageService.loadLocaleList();
|
||||
@@ -266,6 +274,7 @@ Future<void> initialSetup(
|
||||
ordersSource: ordersSource,
|
||||
anonpayInvoiceInfoSource: anonpayInvoiceInfo,
|
||||
unspentCoinsInfoSource: unspentCoinsInfoSource,
|
||||
payjoinSessionSource: payjoinSessionSource,
|
||||
navigatorKey: navigatorKey,
|
||||
secureStorage: secureStorage,
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
|
||||
import 'package:cake_wallet/entities/fiat_api_mode.dart';
|
||||
@@ -78,6 +79,10 @@ void startCurrentWalletChangeReaction(
|
||||
_setAutoGenerateSubaddressStatus(wallet, settingsStore);
|
||||
}
|
||||
|
||||
if (wallet.type == WalletType.bitcoin) {
|
||||
bitcoin!.updatePayjoinState(wallet, settingsStore.usePayjoin);
|
||||
}
|
||||
|
||||
await wallet.connectToNode(node: node);
|
||||
if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) {
|
||||
final powNode = settingsStore.getCurrentPowNode(wallet.type);
|
||||
|
||||
@@ -58,6 +58,7 @@ import 'package:cake_wallet/src/screens/new_wallet/wallet_group_existing_seed_de
|
||||
import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart';
|
||||
import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart';
|
||||
import 'package:cake_wallet/src/screens/order_details/order_details_page.dart';
|
||||
import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
|
||||
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
|
||||
@@ -719,6 +720,14 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||
return CupertinoPageRoute<void>(
|
||||
builder: (_) => getIt.get<AnonpayDetailsPage>(param1: anonInvoiceViewData));
|
||||
|
||||
case Routes.payjoinDetails:
|
||||
final arguments = settings.arguments as List;
|
||||
final sessionId = arguments.first as String;
|
||||
final transactionInfo = arguments[1] as TransactionInfo?;
|
||||
return CupertinoPageRoute<void>(
|
||||
builder: (_) => getIt.get<PayjoinDetailsPage>(
|
||||
param1: sessionId, param2: transactionInfo));
|
||||
|
||||
case Routes.desktop_actions:
|
||||
return PageRouteBuilder(
|
||||
opaque: false,
|
||||
|
||||
@@ -15,6 +15,7 @@ class Routes {
|
||||
static const dashboard = '/dashboard';
|
||||
static const send = '/send';
|
||||
static const transactionDetails = '/transaction_info';
|
||||
static const payjoinDetails = '/transaction_info/payjoin';
|
||||
static const bumpFeePage = '/bump_fee_page';
|
||||
static const receive = '/receive';
|
||||
static const newSubaddress = '/new_subaddress';
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/widgets/payjoin_transaction_row.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/widgets/trade_row.dart';
|
||||
import 'package:cake_wallet/themes/extensions/placeholder_theme.dart';
|
||||
import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
@@ -143,6 +145,25 @@ class TransactionsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (item is PayjoinTransactionListItem) {
|
||||
final session = item.session;
|
||||
|
||||
return PayjoinTransactionRow(
|
||||
key: item.key,
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.payjoinDetails,
|
||||
arguments: [item.sessionId, item.transaction],
|
||||
),
|
||||
currency: "BTC",
|
||||
state: item.status,
|
||||
amount: bitcoin!.formatterBitcoinAmountToString(
|
||||
amount: session.amount.toInt()),
|
||||
createdAt: DateFormat('HH:mm')
|
||||
.format(session.inProgressSince!),
|
||||
isSending: session.isSenderSession,
|
||||
);
|
||||
}
|
||||
|
||||
if (item is TradeListItem) {
|
||||
final trade = item.trade;
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PayjoinTransactionRow extends StatelessWidget {
|
||||
PayjoinTransactionRow({
|
||||
required this.createdAt,
|
||||
required this.currency,
|
||||
required this.onTap,
|
||||
required this.amount,
|
||||
required this.state,
|
||||
required this.isSending,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final String createdAt;
|
||||
final String amount;
|
||||
final String currency;
|
||||
final String state;
|
||||
final bool isSending;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_getImage(),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${isSending ? S.current.outgoing : S.current.incoming} Payjoin",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context)
|
||||
.extension<DashboardPageTheme>()!
|
||||
.textColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
amount + ' ' + currency,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context)
|
||||
.extension<DashboardPageTheme>()!
|
||||
.textColor,
|
||||
),
|
||||
)
|
||||
]),
|
||||
SizedBox(height: 5),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
createdAt,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.extension<CakeTextTheme>()!
|
||||
.dateSectionRowColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
state,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.extension<CakeTextTheme>()!
|
||||
.dateSectionRowColor,
|
||||
),
|
||||
),
|
||||
])
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _getImage() => ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Image.asset('assets/images/payjoin.png', width: 36, height: 36));
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart';
|
||||
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
|
||||
import 'package:cake_wallet/src/widgets/list_row.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list_card.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_list_status_row.dart';
|
||||
import 'package:cake_wallet/utils/show_bar.dart';
|
||||
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class PayjoinDetailsPage extends BasePage {
|
||||
PayjoinDetailsPage({required this.payjoinDetailsViewModel});
|
||||
|
||||
@override
|
||||
String get title => S.current.payjoin_details;
|
||||
|
||||
final PayjoinDetailsViewModel payjoinDetailsViewModel;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => PayjoinDetailsPageBody(payjoinDetailsViewModel);
|
||||
}
|
||||
|
||||
class PayjoinDetailsPageBody extends StatefulWidget {
|
||||
PayjoinDetailsPageBody(this.payjoinDetailsViewModel);
|
||||
|
||||
final PayjoinDetailsViewModel payjoinDetailsViewModel;
|
||||
|
||||
@override
|
||||
State<PayjoinDetailsPageBody> createState() => _PayjoinDetailsPageBodyState();
|
||||
}
|
||||
|
||||
class _PayjoinDetailsPageBodyState extends State<PayjoinDetailsPageBody> {
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
widget.payjoinDetailsViewModel.listener.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SectionStandardList(
|
||||
sectionCount: 1,
|
||||
itemCounter: (int _) => widget.payjoinDetailsViewModel.items.length,
|
||||
itemBuilder: (__, index) {
|
||||
final item = widget.payjoinDetailsViewModel.items[index];
|
||||
|
||||
if (item is DetailsListStatusItem) {
|
||||
return StandardListStatusRow(
|
||||
title: item.title,
|
||||
value: item.value,
|
||||
status: item.status,
|
||||
);
|
||||
}
|
||||
|
||||
if (item is TradeDetailsListCardItem) {
|
||||
return TradeDetailsStandardListCard(
|
||||
id: item.id,
|
||||
create: item.createdAt,
|
||||
pair: item.pair,
|
||||
currentTheme: widget.payjoinDetailsViewModel.settingsStore.currentTheme.type,
|
||||
onTap: item.onTap,
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: item.value));
|
||||
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
|
||||
},
|
||||
child: ListRow(title: '${item.title}:', value: item.value),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:cake_wallet/entities/qr_view_data.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||
import 'package:cake_wallet/themes/extensions/picker_theme.dart';
|
||||
import 'package:cake_wallet/themes/extensions/qr_code_theme.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
@@ -91,26 +93,54 @@ class QRWidget extends StatelessWidget {
|
||||
child: Hero(
|
||||
tag: Key(heroTag ?? addressUri.toString()),
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 3,
|
||||
color: Theme.of(context)
|
||||
.extension<DashboardPageTheme>()!
|
||||
.textColor,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 3,
|
||||
color: Colors.white,
|
||||
child: Container(
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide.none),
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(5)),
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(3),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: QrImage(
|
||||
data: addressUri.toString(),
|
||||
),
|
||||
),
|
||||
child: QrImage(data: addressUri.toString())),
|
||||
),
|
||||
if (addressListViewModel
|
||||
.payjoinEndpoint.isNotEmpty) ...[
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
),
|
||||
child: Image.asset(
|
||||
'assets/images/payjoin.png',
|
||||
width: 20,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
S.of(context).payjoin_enabled,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -179,7 +209,25 @@ class QRWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
if (addressListViewModel.payjoinEndpoint.isNotEmpty) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
child: PrimaryImageButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(
|
||||
text: addressListViewModel.payjoinEndpoint));
|
||||
showBar<void>(context, S.of(context).copied_to_clipboard);
|
||||
},
|
||||
image: Image.asset('assets/images/payjoin.png', width: 25,),
|
||||
text: S.of(context).copy_payjoin_url,
|
||||
color: Theme.of(context).cardColor,
|
||||
textColor: Theme.of(context)
|
||||
.extension<CakeTextTheme>()!
|
||||
.buttonTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/core/auth_service.dart';
|
||||
import 'package:cake_wallet/core/totp_request_details.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
@@ -136,6 +136,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
|
||||
setState(() => _setInactive(true));
|
||||
}
|
||||
|
||||
if (widget.appStore.wallet?.type == WalletType.bitcoin) {
|
||||
bitcoin!.stopPayjoinSessions(widget.appStore.wallet!);
|
||||
}
|
||||
|
||||
break;
|
||||
case AppLifecycleState.resumed:
|
||||
widget.authService.requireAuth().then((value) {
|
||||
@@ -145,6 +149,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
|
||||
});
|
||||
}
|
||||
});
|
||||
if (widget.appStore.wallet?.type == WalletType.bitcoin &&
|
||||
widget.appStore.settingsStore.usePayjoin) {
|
||||
bitcoin!.resumePayjoinSessions(widget.appStore.wallet!);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -463,7 +463,7 @@ class SendPage extends BasePage {
|
||||
},
|
||||
);
|
||||
},
|
||||
text: S.of(context).send,
|
||||
text: sendViewModel.payjoinUri != null ? S.of(context).send_payjoin : S.of(context).send,
|
||||
color: Theme.of(context).primaryColor,
|
||||
textColor: Colors.white,
|
||||
isLoading: sendViewModel.state is IsExecutingState ||
|
||||
|
||||
@@ -188,19 +188,23 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
sendViewModel.createOpenCryptoPayTransaction(uri.toString());
|
||||
} else {
|
||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||
addressController.text = paymentRequest.address;
|
||||
cryptoAmountController.text = paymentRequest.amount;
|
||||
noteController.text = paymentRequest.note;
|
||||
}
|
||||
},
|
||||
options: [
|
||||
AddressTextFieldOption.paste,
|
||||
AddressTextFieldOption.qrCode,
|
||||
AddressTextFieldOption.addressBook
|
||||
],
|
||||
buttonColor: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
|
||||
borderColor: Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
|
||||
textStyle:
|
||||
if (sendViewModel.usePayjoin) {
|
||||
sendViewModel.payjoinUri = paymentRequest.pjUri;
|
||||
}
|
||||
addressController.text = paymentRequest.address;
|
||||
cryptoAmountController.text = paymentRequest.amount;
|
||||
noteController.text = paymentRequest.note;}
|
||||
},
|
||||
options: [
|
||||
AddressTextFieldOption.paste,
|
||||
AddressTextFieldOption.qrCode,
|
||||
AddressTextFieldOption.addressBook
|
||||
],
|
||||
buttonColor:
|
||||
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
|
||||
borderColor:
|
||||
Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
|
||||
textStyle:
|
||||
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
|
||||
@@ -49,6 +49,14 @@ class PrivacyPage extends BasePage {
|
||||
_privacySettingsViewModel.setExchangeApiMode(mode),
|
||||
),
|
||||
),
|
||||
if (_privacySettingsViewModel.canUsePayjoin)
|
||||
SettingsSwitcherCell(
|
||||
title: S.of(context).use_payjoin,
|
||||
value: _privacySettingsViewModel.usePayjoin,
|
||||
onValueChange: (BuildContext _, bool value) {
|
||||
_privacySettingsViewModel.setUsePayjoin(value);
|
||||
},
|
||||
),
|
||||
SettingsSwitcherCell(
|
||||
title: S.current.settings_save_recipient_address,
|
||||
value: _privacySettingsViewModel.shouldSaveRecipientAddress,
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.d
|
||||
|
||||
class DetailsListStatusItem extends StandartListItem {
|
||||
DetailsListStatusItem(
|
||||
{required String title, required String value})
|
||||
{required String title, required String value, this.status})
|
||||
: super(title: title, value: value);
|
||||
|
||||
final String? status; // waiting, action required, created, fetching, finished, success
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/entities/contact_base.dart';
|
||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/utils/permission_handler.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||
import 'package:cake_wallet/entities/contact_base.dart';
|
||||
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
|
||||
import 'package:cake_wallet/utils/permission_handler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
|
||||
|
||||
|
||||
class AddressTextField<T extends Currency> extends StatelessWidget{
|
||||
class AddressTextField<T extends Currency> extends StatelessWidget {
|
||||
AddressTextField({
|
||||
required this.controller,
|
||||
this.isActive = true,
|
||||
@@ -234,9 +233,7 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
|
||||
if (!isCameraPermissionGranted) return;
|
||||
final code = await presentQRScanner(context);
|
||||
if (code == null) return;
|
||||
if (code.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (code.isEmpty) return;
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(code);
|
||||
@@ -259,7 +256,8 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
|
||||
}
|
||||
|
||||
Future<void> _presetWalletAddressPicker(BuildContext context) async {
|
||||
final address = await Navigator.of(context).pushNamed(Routes.pickerWalletAddress);
|
||||
final address =
|
||||
await Navigator.of(context).pushNamed(Routes.pickerWalletAddress);
|
||||
|
||||
if (address is String) {
|
||||
controller?.text = address;
|
||||
@@ -272,7 +270,13 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
|
||||
final address = clipboard?.text ?? '';
|
||||
|
||||
if (address.isNotEmpty) {
|
||||
controller?.text = address;
|
||||
try {
|
||||
final uri = Uri.parse(address);
|
||||
controller?.text = uri.path;
|
||||
onURIScanned?.call(uri);
|
||||
} catch (_) {
|
||||
controller?.text = address;
|
||||
}
|
||||
}
|
||||
|
||||
onPushPasteButton?.call(context);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||
import 'package:cake_wallet/palette.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -7,10 +6,11 @@ import 'package:cake_wallet/themes/extensions/address_theme.dart';
|
||||
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
|
||||
|
||||
class StandardListStatusRow extends StatelessWidget {
|
||||
StandardListStatusRow({required this.title, required this.value});
|
||||
StandardListStatusRow({required this.title, required this.value, this.status});
|
||||
|
||||
final String title;
|
||||
final String value;
|
||||
final String? status; // waiting, action required, created, fetching, finished, success
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -43,7 +43,7 @@ class StandardListStatusRow extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
SyncIndicatorIcon(
|
||||
boolMode: false,
|
||||
value: value,
|
||||
value: status ?? value,
|
||||
size: 6,
|
||||
),
|
||||
SizedBox(
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
|
||||
import 'package:cw_core/payjoin_session.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'payjoin_transactions_store.g.dart';
|
||||
|
||||
class PayjoinTransactionsStore = PayjoinTransactionsStoreBase
|
||||
with _$PayjoinTransactionsStore;
|
||||
|
||||
abstract class PayjoinTransactionsStoreBase with Store {
|
||||
PayjoinTransactionsStoreBase({
|
||||
required this.payjoinSessionSource,
|
||||
}) : transactions = <PayjoinTransactionListItem>[] {
|
||||
payjoinSessionSource.watch().listen((_) => updateTransactionList());
|
||||
updateTransactionList();
|
||||
}
|
||||
|
||||
Box<PayjoinSession> payjoinSessionSource;
|
||||
|
||||
@observable
|
||||
List<PayjoinTransactionListItem> transactions;
|
||||
|
||||
@action
|
||||
Future<void> updateTransactionList() async {
|
||||
final updatedTransactions = <PayjoinTransactionListItem>[];
|
||||
payjoinSessionSource.toMap().forEach((dynamic key, PayjoinSession session) {
|
||||
if ([
|
||||
PayjoinSessionStatus.inProgress.name,
|
||||
PayjoinSessionStatus.success.name,
|
||||
PayjoinSessionStatus.unrecoverable.name
|
||||
].contains(session.status) &&
|
||||
session.inProgressSince != null) {
|
||||
updatedTransactions.add(PayjoinTransactionListItem(
|
||||
sessionId: key as String,
|
||||
session: session,
|
||||
key: ValueKey('payjoin_transaction_list_item_${key}_key'),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
transactions = updatedTransactions;
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,7 @@ abstract class SettingsStoreBase with Store {
|
||||
required this.lookupsOpenAlias,
|
||||
required this.lookupsENS,
|
||||
required this.lookupsWellKnown,
|
||||
required this.usePayjoin,
|
||||
required this.customBitcoinFeeRate,
|
||||
required this.silentPaymentsCardDisplay,
|
||||
required this.silentPaymentsAlwaysScan,
|
||||
@@ -483,6 +484,11 @@ abstract class SettingsStoreBase with Store {
|
||||
(bool looksUpWellKnown) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
|
||||
|
||||
reaction(
|
||||
(_) => usePayjoin,
|
||||
(bool usePayjoin) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.usePayjoin, usePayjoin));
|
||||
|
||||
// secure storage keys:
|
||||
reaction(
|
||||
(_) => allowBiometricalAuthentication,
|
||||
@@ -802,6 +808,9 @@ abstract class SettingsStoreBase with Store {
|
||||
@observable
|
||||
bool lookupsWellKnown;
|
||||
|
||||
@observable
|
||||
bool usePayjoin;
|
||||
|
||||
@observable
|
||||
SyncMode currentSyncMode;
|
||||
|
||||
@@ -1009,6 +1018,7 @@ abstract class SettingsStoreBase with Store {
|
||||
final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
|
||||
final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
|
||||
final lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true;
|
||||
final usePayjoin = sharedPreferences.getBool(PreferencesKey.usePayjoin) ?? false;
|
||||
final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
|
||||
final silentPaymentsCardDisplay =
|
||||
sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
|
||||
@@ -1311,6 +1321,7 @@ abstract class SettingsStoreBase with Store {
|
||||
lookupsOpenAlias: lookupsOpenAlias,
|
||||
lookupsENS: lookupsENS,
|
||||
lookupsWellKnown: lookupsWellKnown,
|
||||
usePayjoin: usePayjoin,
|
||||
customBitcoinFeeRate: customBitcoinFeeRate,
|
||||
silentPaymentsCardDisplay: silentPaymentsCardDisplay,
|
||||
silentPaymentsAlwaysScan: silentPaymentsAlwaysScan,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/nano/nano.dart';
|
||||
|
||||
class PaymentRequest {
|
||||
PaymentRequest(this.address, this.amount, this.note, this.scheme, {this.callbackUrl, this.callbackMessage});
|
||||
PaymentRequest(this.address, this.amount, this.note, this.scheme, this.pjUri,
|
||||
{this.callbackUrl, this.callbackMessage});
|
||||
|
||||
factory PaymentRequest.fromUri(Uri? uri) {
|
||||
var address = "";
|
||||
@@ -12,8 +12,13 @@ class PaymentRequest {
|
||||
String? walletType;
|
||||
String? callbackUrl;
|
||||
String? callbackMessage;
|
||||
String? pjUri;
|
||||
|
||||
if (uri != null) {
|
||||
if (uri.queryParameters['pj'] != null) {
|
||||
pjUri = uri.toString();
|
||||
}
|
||||
|
||||
address = uri.queryParameters['address'] ?? uri.path;
|
||||
amount = uri.queryParameters['tx_amount'] ?? uri.queryParameters['amount'] ?? "";
|
||||
note = uri.queryParameters['tx_description'] ?? uri.queryParameters['message'] ?? "";
|
||||
@@ -42,6 +47,7 @@ class PaymentRequest {
|
||||
amount,
|
||||
note,
|
||||
scheme,
|
||||
pjUri,
|
||||
callbackUrl: callbackUrl,
|
||||
callbackMessage: callbackMessage,
|
||||
);
|
||||
@@ -51,6 +57,7 @@ class PaymentRequest {
|
||||
final String amount;
|
||||
final String note;
|
||||
final String scheme;
|
||||
final String? pjUri;
|
||||
final String? callbackUrl;
|
||||
final String? callbackMessage;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ import 'package:cake_wallet/entities/service_status.dart';
|
||||
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart' as wow;
|
||||
import 'package:cake_wallet/nano/nano.dart';
|
||||
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/orders_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/trades_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
|
||||
@@ -29,9 +29,12 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/filter_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
|
||||
import 'package:cake_wallet/wallet_type_utils.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart' as wow;
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/cake_hive.dart';
|
||||
@@ -70,6 +73,7 @@ abstract class DashboardViewModelBase with Store {
|
||||
required this.yatStore,
|
||||
required this.ordersStore,
|
||||
required this.anonpayTransactionsStore,
|
||||
required this.payjoinTransactionsStore,
|
||||
required this.sharedPreferences,
|
||||
required this.keyService})
|
||||
: hasTradeAction = true,
|
||||
@@ -408,9 +412,16 @@ abstract class DashboardViewModelBase with Store {
|
||||
ordersStore.orders.where((item) => item.order.walletId == wallet.id).toList();
|
||||
|
||||
@computed
|
||||
List<AnonpayTransactionListItem> get anonpayTransactons => anonpayTransactionsStore.transactions
|
||||
.where((item) => item.transaction.walletId == wallet.id)
|
||||
.toList();
|
||||
List<AnonpayTransactionListItem> get anonpayTransactions =>
|
||||
anonpayTransactionsStore.transactions
|
||||
.where((item) => item.transaction.walletId == wallet.id)
|
||||
.toList();
|
||||
|
||||
@computed
|
||||
List<PayjoinTransactionListItem> get payjoinTransactions =>
|
||||
payjoinTransactionsStore.transactions
|
||||
.where((item) => item.session.walletId == wallet.id)
|
||||
.toList();
|
||||
|
||||
@computed
|
||||
double get price => balanceViewModel.price;
|
||||
@@ -423,11 +434,27 @@ abstract class DashboardViewModelBase with Store {
|
||||
List<ActionListItem> get items {
|
||||
final _items = <ActionListItem>[];
|
||||
|
||||
_items.addAll(
|
||||
transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactons]));
|
||||
_items.addAll(transactionFilterStore
|
||||
.filtered(transactions: [...transactions, ...anonpayTransactions]));
|
||||
_items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet));
|
||||
_items.addAll(orders);
|
||||
|
||||
if (payjoinTransactions.isNotEmpty) {
|
||||
final _payjoinTransactions = payjoinTransactions;
|
||||
_items.forEach((e) {
|
||||
if (e is TransactionListItem &&
|
||||
_payjoinTransactions
|
||||
.any((t) => t.session.txId == e.transaction.id)) {
|
||||
_payjoinTransactions
|
||||
.firstWhere((t) => t.session.txId == e.transaction.id)
|
||||
.transaction = e.transaction;
|
||||
}
|
||||
});
|
||||
_items.addAll(_payjoinTransactions);
|
||||
_items.removeWhere((e) => (e is TransactionListItem &&
|
||||
_payjoinTransactions.any((t) => t.session.txId == e.transaction.id)));
|
||||
}
|
||||
|
||||
return formattedItemsList(_items);
|
||||
}
|
||||
|
||||
@@ -755,6 +782,8 @@ abstract class DashboardViewModelBase with Store {
|
||||
|
||||
TransactionFilterStore transactionFilterStore;
|
||||
|
||||
PayjoinTransactionsStore payjoinTransactionsStore;
|
||||
|
||||
Map<String, List<FilterItem>> filterItems;
|
||||
|
||||
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
|
||||
import 'package:cw_core/payjoin_session.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
|
||||
class PayjoinTransactionListItem extends ActionListItem {
|
||||
PayjoinTransactionListItem({
|
||||
required this.sessionId,
|
||||
required this.session,
|
||||
required super.key,
|
||||
});
|
||||
|
||||
final String sessionId;
|
||||
final PayjoinSession session;
|
||||
TransactionInfo? transaction;
|
||||
|
||||
@override
|
||||
DateTime get date => session.inProgressSince!;
|
||||
|
||||
String get status {
|
||||
switch (session.status) {
|
||||
case 'success':
|
||||
if (transaction?.isPending == true)
|
||||
return S.current.payjoin_request_awaiting_tx;
|
||||
return S.current.successful;
|
||||
case 'inProgress':
|
||||
return S.current.payjoin_request_in_progress;
|
||||
case 'unrecoverable':
|
||||
return S.current.error;
|
||||
default:
|
||||
return session.status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart';
|
||||
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/utils/date_formatter.dart';
|
||||
import 'package:cw_core/payjoin_session.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'payjoin_details_view_model.g.dart';
|
||||
|
||||
class PayjoinDetailsViewModel = PayjoinDetailsViewModelBase
|
||||
with _$PayjoinDetailsViewModel;
|
||||
|
||||
abstract class PayjoinDetailsViewModelBase with Store {
|
||||
PayjoinDetailsViewModelBase(
|
||||
this.payjoinSessionId,
|
||||
this.transactionInfo, {
|
||||
required this.payjoinSessionSource,
|
||||
required this.settingsStore,
|
||||
}) : items = ObservableList<StandartListItem>(),
|
||||
payjoinSession = payjoinSessionSource.get(payjoinSessionId)! {
|
||||
listener = payjoinSessionSource.watch().listen((e) {
|
||||
if (e.key == payjoinSessionId) _updateItems();
|
||||
});
|
||||
_updateItems();
|
||||
}
|
||||
|
||||
final Box<PayjoinSession> payjoinSessionSource;
|
||||
final SettingsStore settingsStore;
|
||||
final String payjoinSessionId;
|
||||
final TransactionInfo? transactionInfo;
|
||||
|
||||
@observable
|
||||
late PayjoinSession payjoinSession;
|
||||
|
||||
final ObservableList<StandartListItem> items;
|
||||
|
||||
late final StreamSubscription<BoxEvent> listener;
|
||||
|
||||
Timer? timer;
|
||||
|
||||
@action
|
||||
void _updateItems() {
|
||||
final dateFormat = DateFormatter.withCurrentLocal();
|
||||
items.clear();
|
||||
items.addAll([
|
||||
DetailsListStatusItem(
|
||||
title: S.current.status,
|
||||
value: _getStatusString(),
|
||||
status: payjoinSession.status,
|
||||
),
|
||||
TradeDetailsListCardItem(
|
||||
id: "${payjoinSession.isSenderSession ? S.current.outgoing : S.current.incoming} Payjoin",
|
||||
createdAt:
|
||||
dateFormat.format(payjoinSession.inProgressSince!).toString(),
|
||||
pair:
|
||||
'${bitcoin!.formatterBitcoinAmountToString(amount: payjoinSession.amount.toInt())} BTC',
|
||||
onTap: (_) {},
|
||||
),
|
||||
if (payjoinSession.error?.isNotEmpty == true)
|
||||
StandartListItem(
|
||||
title: S.current.error,
|
||||
value: payjoinSession.error!,
|
||||
),
|
||||
if (payjoinSession.txId?.isNotEmpty == true)
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_transaction_id,
|
||||
value: payjoinSession.txId!,
|
||||
key: ValueKey('standard_list_item_transaction_details_id_key'),
|
||||
)
|
||||
]);
|
||||
|
||||
if (transactionInfo != null) {
|
||||
items.addAll([
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_date,
|
||||
value: dateFormat.format(transactionInfo!.date),
|
||||
key: ValueKey('standard_list_item_transaction_details_date_key'),
|
||||
),
|
||||
StandartListItem(
|
||||
title: S.current.confirmations,
|
||||
value: transactionInfo!.confirmations.toString(),
|
||||
key: ValueKey('standard_list_item_transaction_confirmations_key'),
|
||||
),
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_height,
|
||||
value: '${transactionInfo!.height}',
|
||||
key: ValueKey('standard_list_item_transaction_details_height_key'),
|
||||
),
|
||||
if (transactionInfo!.feeFormatted()?.isNotEmpty ?? false)
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_fee,
|
||||
value: transactionInfo!.feeFormatted()!,
|
||||
key: ValueKey('standard_list_item_transaction_details_fee_key'),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatusString() {
|
||||
switch (payjoinSession.status) {
|
||||
case 'success':
|
||||
if (transactionInfo?.isPending == true)
|
||||
return S.current.payjoin_request_awaiting_tx;
|
||||
return S.current.successful;
|
||||
case 'inProgress':
|
||||
return S.current.payjoin_request_in_progress;
|
||||
case 'unrecoverable':
|
||||
return S.current.error;
|
||||
default:
|
||||
return payjoinSession.status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -619,6 +619,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
priority: priority!,
|
||||
feeRate: feesViewModel.customBitcoinFeeRate,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
payjoinUri: _settingsStore.usePayjoin ? payjoinUri : null,
|
||||
);
|
||||
case WalletType.litecoin:
|
||||
return bitcoin!.createBitcoinTransactionCredentials(
|
||||
@@ -855,4 +856,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@computed
|
||||
bool get usePayjoin => _settingsStore.usePayjoin;
|
||||
|
||||
@observable
|
||||
String? payjoinUri;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
|
||||
import 'package:cake_wallet/entities/exchange_api_mode.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
@@ -32,20 +33,19 @@ abstract class PrivacySettingsViewModelBase with Store {
|
||||
@action
|
||||
void setAutoGenerateSubaddresses(bool value) {
|
||||
_wallet.isEnabledAutoGenerateSubaddress = value;
|
||||
if (value) {
|
||||
_settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.enabled;
|
||||
} else {
|
||||
_settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled;
|
||||
}
|
||||
_settingsStore.autoGenerateSubaddressStatus = value
|
||||
? AutoGenerateSubaddressStatus.enabled
|
||||
: AutoGenerateSubaddressStatus.disabled;
|
||||
}
|
||||
|
||||
bool get isAutoGenerateSubaddressesVisible =>
|
||||
_wallet.type == WalletType.monero ||
|
||||
_wallet.type == WalletType.wownero ||
|
||||
_wallet.type == WalletType.bitcoin ||
|
||||
_wallet.type == WalletType.litecoin ||
|
||||
_wallet.type == WalletType.bitcoinCash ||
|
||||
_wallet.type == WalletType.decred;
|
||||
bool get isAutoGenerateSubaddressesVisible => [
|
||||
WalletType.monero,
|
||||
WalletType.wownero,
|
||||
WalletType.bitcoin,
|
||||
WalletType.litecoin,
|
||||
WalletType.bitcoinCash,
|
||||
WalletType.decred
|
||||
].contains(_wallet.type);
|
||||
|
||||
bool get isMoneroWallet => _wallet.type == WalletType.monero;
|
||||
|
||||
@@ -100,6 +100,9 @@ abstract class PrivacySettingsViewModelBase with Store {
|
||||
@computed
|
||||
bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
|
||||
|
||||
@computed
|
||||
bool get usePayjoin => _settingsStore.usePayjoin;
|
||||
|
||||
bool get canUseEtherscan => _wallet.type == WalletType.ethereum;
|
||||
|
||||
bool get canUsePolygonScan => _wallet.type == WalletType.polygon;
|
||||
@@ -108,6 +111,8 @@ abstract class PrivacySettingsViewModelBase with Store {
|
||||
|
||||
bool get canUseMempoolFeeAPI => _wallet.type == WalletType.bitcoin;
|
||||
|
||||
bool get canUsePayjoin => _wallet.type == WalletType.bitcoin;
|
||||
|
||||
@action
|
||||
void setShouldSaveRecipientAddress(bool value) =>
|
||||
_settingsStore.shouldSaveRecipientAddress = value;
|
||||
@@ -170,7 +175,12 @@ abstract class PrivacySettingsViewModelBase with Store {
|
||||
}
|
||||
|
||||
@action
|
||||
void setUseMempoolFeeAPI(bool value) {
|
||||
_settingsStore.useMempoolFeeAPI = value;
|
||||
void setUseMempoolFeeAPI(bool value) =>
|
||||
_settingsStore.useMempoolFeeAPI = value;
|
||||
|
||||
@action
|
||||
void setUsePayjoin(bool value) {
|
||||
_settingsStore.usePayjoin = value;
|
||||
bitcoin!.updatePayjoinState(_wallet, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:developer' as dev;
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/core/fiat_conversion_service.dart';
|
||||
@@ -72,17 +73,21 @@ class HavenURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class BitcoinURI extends PaymentURI {
|
||||
BitcoinURI({required super.amount, required super.address});
|
||||
BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
|
||||
|
||||
final String pjUri;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'bitcoin:$address';
|
||||
final qp = <String, String>{};
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
|
||||
if (pjUri.isNotEmpty) {
|
||||
qp['pjos'] = '0';
|
||||
qp['pj'] = pjUri;
|
||||
}
|
||||
|
||||
return base;
|
||||
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +305,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
||||
WalletAddressListItem get address =>
|
||||
WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false);
|
||||
|
||||
@computed
|
||||
String get payjoinEndpoint => wallet.type == WalletType.bitcoin
|
||||
? bitcoin!.getPayjoinEndpoint(wallet)
|
||||
: "";
|
||||
|
||||
@computed
|
||||
PaymentURI get uri {
|
||||
switch (wallet.type) {
|
||||
@@ -308,7 +318,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
||||
case WalletType.haven:
|
||||
return HavenURI(amount: amount, address: address.address);
|
||||
case WalletType.bitcoin:
|
||||
return BitcoinURI(amount: amount, address: address.address);
|
||||
return BitcoinURI(
|
||||
amount: amount,
|
||||
address: address.address,
|
||||
pjUri: payjoinEndpoint);
|
||||
case WalletType.litecoin:
|
||||
return LitecoinURI(amount: amount, address: address.address);
|
||||
case WalletType.ethereum:
|
||||
|
||||
Reference in New Issue
Block a user