feat: Pay Anything from Any Wallet (#2450)

* feat: Pay Anything from Any Wallet

* feat: Trigger payment flow when address is pasted

* chore: Remove unneeded changes

* feat Expand address checks and fix scanning bch qrs

* fix: Add support for dogecoin

* feat: More modifications for Pay from Any Wallet

This change:
- Adds the pay from any wallet flow to externally signed QR codes
- Adds fixes for review comments
- Fixes failing tests

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke
2025-08-21 18:37:50 +03:00
committed by GitHub
co-authored by Omar Hatem
parent 16f058e9e2
commit 13a74133f5
39 changed files with 1228 additions and 27 deletions
@@ -0,0 +1,124 @@
import 'dart:async';
import 'package:cake_wallet/core/universal_address_detector.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
part 'payment_view_model.g.dart';
class PaymentViewModel = PaymentViewModelBase with _$PaymentViewModel;
abstract class PaymentViewModelBase with Store {
PaymentViewModelBase({
required this.appStore,
required this.walletInfoSource,
});
final AppStore appStore;
final Box<WalletInfo> walletInfoSource;
@observable
WalletType? detectedWalletType;
@observable
bool isProcessing = false;
@computed
WalletType get currentWalletType => appStore.wallet!.type;
/// Main entry point - detect address type and check compatibility
@action
Future<PaymentFlowResult> processAddress(String addressData) async {
try {
detectedWalletType = null;
isProcessing = true;
// Detect address type
final detectionResult = UniversalAddressDetector.detectAddress(addressData);
if (!detectionResult.isValid || detectionResult.detectedWalletType == null) {
return PaymentFlowResult.incompatible('Unable to detect address type');
}
detectedWalletType = detectionResult.detectedWalletType!;
// Check if current wallet is compatible
final currentWallet = appStore.wallet;
if (currentWallet != null && currentWallet.type == detectedWalletType) {
return PaymentFlowResult.currentWalletCompatible();
}
final compatibleWallets = getWalletsByType(detectedWalletType!);
switch (compatibleWallets.length) {
case 0:
return PaymentFlowResult.noWallets(detectedWalletType!);
case 1:
return PaymentFlowResult.singleWallet(compatibleWallets.first);
default:
return PaymentFlowResult.multipleWallets(compatibleWallets);
}
} catch (e) {
printV('PaymentViewModel error: $e');
return PaymentFlowResult.error('Payment processing failed: $e');
} finally {
isProcessing = false;
}
}
List<WalletInfo> getWalletsByType(WalletType walletType) {
return walletInfoSource.values.where((wallet) => wallet.type == walletType).toList();
}
}
class PaymentFlowResult {
final PaymentFlowType type;
final String? message;
final WalletInfo? wallet;
final List<WalletInfo> wallets;
final WalletType? walletType;
PaymentFlowResult._({
required this.type,
this.message,
this.wallet,
this.wallets = const [],
this.walletType,
});
/// Current wallet is compatible
factory PaymentFlowResult.currentWalletCompatible() => PaymentFlowResult._(type: PaymentFlowType.currentWalletCompatible);
/// Single compatible wallet available
factory PaymentFlowResult.singleWallet(WalletInfo wallet) =>
PaymentFlowResult._(type: PaymentFlowType.singleWallet, wallet: wallet);
/// Multiple compatible wallets available
factory PaymentFlowResult.multipleWallets(List<WalletInfo> wallets) =>
PaymentFlowResult._(type: PaymentFlowType.multipleWallets, wallets: wallets);
/// No compatible wallets available
factory PaymentFlowResult.noWallets(WalletType walletType) =>
PaymentFlowResult._(type: PaymentFlowType.noWallets, walletType: walletType);
/// Error occurred
factory PaymentFlowResult.error(String message) =>
PaymentFlowResult._(type: PaymentFlowType.error, message: message);
/// Incompatible address
factory PaymentFlowResult.incompatible(String message) =>
PaymentFlowResult._(type: PaymentFlowType.incompatible, message: message);
}
enum PaymentFlowType {
currentWalletCompatible,
singleWallet,
multipleWallets,
noWallets,
error,
incompatible,
}
+2
View File
@@ -37,7 +37,9 @@ import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
@@ -0,0 +1,63 @@
import 'dart:async';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
part 'wallet_switcher_view_model.g.dart';
class WalletSwitcherViewModel = WalletSwitcherViewModelBase with _$WalletSwitcherViewModel;
abstract class WalletSwitcherViewModelBase with Store {
WalletSwitcherViewModelBase({
required this.appStore,
required this.walletLoadingService,
required this.walletInfoSource,
});
final AppStore appStore;
final WalletLoadingService walletLoadingService;
final Box<WalletInfo> walletInfoSource;
@observable
WalletInfo? selectedWallet;
@observable
bool isProcessing = false;
@action
List<WalletInfo> getWallets(WalletType? walletType) {
if (walletType == null) {
return walletInfoSource.values.toList();
}
return walletInfoSource.values.where((wallet) => wallet.type == walletType).toList();
}
@action
void selectWallet(WalletInfo walletInfo) => selectedWallet = walletInfo;
@action
Future<bool> switchToSelectedWallet() async {
if (selectedWallet == null) return false;
try {
isProcessing = true;
final wallet = await walletLoadingService.load(selectedWallet!.type, selectedWallet!.name);
await appStore.changeCurrentWallet(wallet);
return true;
} catch (e) {
printV('Failed to switch wallet: $e');
return false;
} finally {
isProcessing = false;
}
}
}