mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
CW-1250: Refactor L2 Chains Code (#2677)
* feat(evm): add ChainConfig model and related classes - Add ChainConfig class for immutable EVM chain configuration - Add ChainCapabilities class for chain feature flags - Add FeeType enum and FeeModel class for fee configuration * feat(evm): add EvmChainRegistry for centralized chain management - Add singleton EvmChainRegistry class with chain configuration storage - Initialize with Ethereum, Polygon, Base, and Arbitrum chains - Add mappings for WalletType, tag, and CAIP-2 to chainId lookups - Provide lookup methods for chain configurations by various identifiers - Support for querying available chains and registered chain IDs * feat(evm): add EVMChainClientFactory and consolidate chain clients - Create EVMChainClientFactory for creating chain-specific clients based on chainId - Move all chain clients (EthereumClient, PolygonClient, BaseClient, ArbitrumClient) into cw_evm/lib/clients/ folder - Refactor EVMChainClient from abstract to concrete class with default implementations - Add default implementations for fetchTransactions, fetchInternalTransactions, and prepareSignedTransactionForSending - Update all client classes to use super(chainId: X) constructor pattern - Factory returns EVMChainClient directly for unregistered chains instead of throwing - Remove dependencies on cw_ethereum, cw_polygon, cw_base, cw_arbitrum packages from pubspec.yaml - Consolidate all EVM chain client implementations in one location for easier maintenance * add ui for evm switcher on eth * add inputs for evm network list edit switches * refactor(evm): Make EVMChainWallet concrete and unify transaction classes - Make EVMChainWallet and EVMChainWalletBase concrete classes (removed abstract) - Implement all previously abstract methods in EVMChainWallet - Make EVMChainTransactionInfo and EVMChainTransactionHistory concrete - Reorganize default tokens into tokens/ folder: - Refactor EVMChainDefaultTokens to import and use token classes from tokens/ folder - Move clients to clients/ folder: - Move evm_chain_formatter.dart to utils - Update chain-specific transaction history classes to pass walletType to fromJson - Update old wallet classes (EthereumWallet, PolygonWallet, etc.) to pass walletType - Maintain compatibility with existing per-chain wallet classes This refactoring enables the unified EVMChainWallet architecture while maintaining full backward compatibility with existing per-chain wallet implementations. * feat(evm): Add chain selection capability to EVMChainWallet - Add selectedChainId observable field to track currently selected chain - Add selectChain(chainId) action method to switch between EVM chains - Add selectedChainConfig computed getter for accessing chain configuration - Initialize selectedChainId from client.chainId in constructor - Add _getClientForCurrentChain() helper method for future use This enables chain switching functionality while maintaining full backward compatibility. The selectedChainId is initialized from the existing client, and all existing methods continue to work unchanged. Future increments will update methods to use selectedChainId internally. * refactor(evm): Update client immediately on chain selection - Update selectChain() to immediately create new client for selected chain - Remove _getClientForCurrentChain() helper method - Simplify all methods to use _client directly instead of helper calls - Client is now always in sync with selectedChainId This improves code simplicity and performance by eliminating repeated client checks. The client is updated synchronously when selectChain() is called, ensuring state consistency. * featr(evm): Add unified evm proxy setup and fix chain initialization - Add unified Evm proxy - Add evm.dart to .gitignore for generated proxy - Add generateEVM() to configure.dart for proxy generation - Fix selectedChainId to initialize from registry based on walletInfo.type Ensures selectedChainId always matches wallet type on initialization and adds unified proxy infrastructure. * refactor(evm): Update backward compat helpers to use registry via proxy - Add registry helper methods to unified EVM proxy - Update all lib/ files to use proxy instead of direct cw_evm imports - Enforce proxy pattern: no direct imports from cw_evm in main app - Update configure.dart to include registry methods in generated proxy Maintains backward compatibility while centralizing chain data access through the unified proxy pattern. * feat(evm): Add chain selection UI to dashboard - Add chain selection methods to DashboardViewModel (isEVMWallet, availableChains, currentChain, selectChain) - Add chain dropdown widget to balance page for EVM wallets - Dropdown shows current chain and allows switching between all registered EVM chains - Only visible for EVM-compatible wallets - Add chain selection methods to EVM proxy (getAllChains, getCurrentChain, selectChain) Users can now switch between EVM chains (Ethereum, Polygon, Base, Arbitrum) directly from the dashboard without creating separate wallets. All chain-related code follows the established proxy pattern. * feat(evm): Add persistence for selected chain ID - Add initialChainId parameter to EVMChainWallet constructor - Save selectedChainId to wallet JSON in toJSON() method - Load selectedChainId from JSON in open() method - Handle backward compatibility: wallets without saved chain ID use wallet type's default chain ID - Client is created with saved chain ID when opening wallet Selected chain preference is now persisted across app sessions. Users will see their previously selected chain when reopening wallets. * chore: Minor cleanup * refactor(evm): Remove old per-chain packages, consolidate to unified cw_evm - Delete cw_ethereum, cw_polygon, cw_base, cw_arbitrum packages - Remove old proxy files (lib/ethereum/, lib/polygon/, lib/base/, lib/arbitrum/) - Update all view models to use unified evm! proxy - Move DEuro functionality to cw_evm/lib/deuro/ - Remove dependencies from pubspec.yaml and configure.dart - Remove walletType parameter from EVM proxy methods All EVM chains now use unified cw_evm package through single proxy interface. lib/ only depends on cw_evm, simplifying architecture and enabling easy addition of new L2 chains. BREAKING CHANGE: Old per-chain packages removed * refactor(evm): Update wallet handling to use unified EVM proxy - Replace individual chain imports (Ethereum, Polygon, Base, Arbitrum) with a single EVM proxy import. - Refactor wallet-related methods across various view models to utilize the new evm proxy for various usecases. This change enhances code maintainability and prepares the codebase for the addition of new EVM chains. * refactor(evm): Update model generator and enhance EVM classes - Modify model_generator.sh to remove previous chain folders from the loop. - Change EVMChainTransactionHistoryBase and EVMChainWalletBase classes to be abstract. - Update import paths in configure.dart to reflect the new structure of the cw_evm package. * add evm switcher icons to pubspec_base * change evm switcher to sliding animation * formatting fix * dpi agnostic width * feat(evm): Enhance chain handling and transaction history management - Introduce getCurrentChainId function to EVMChainTransactionHistoryBase for dynamic transaction history file naming based on the current chain ID. - Update EVMChainTransactionInfo to include chainId, ensuring accurate transaction data representation. - Refactor EVMChainWalletBase to support automatic node connection and transaction history loading upon chain selection. - Implement saveBackup and restoreWalletFilesFromBackup methods in EVMChainWalletService to manage wallet backups based on wallet type. - Update various methods across the codebase to accommodate the new chain handling logic, improving overall functionality and user experience. * chore: Cleanups and updates * feat: add WalletType.evm and deprecate individual EVM chain types Add unified WalletType.evm enum value for all EVM-compatible chains. Deprecate old individual EVM types (ethereum, polygon, base, arbitrum) with guidance to use WalletType.evm for new code. Update serialization/deserialization functions to support WalletType.evm: - serializeToInt() returns 18 for WalletType.evm - deserializeFromInt() handles case 18 - walletTypeToString() and walletTypeToDisplayName() return 'EVM' - cryptoCurrencyToWalletType() maps all EVM currencies to WalletType.evm Old EVM types remain functional for backward compatibility with existing wallets. This is the foundation for unifying EVM chain management. * feat: update EVM detection functions to support WalletType.evm Update all EVM detection and chain helper functions to support the unified WalletType.evm while maintaining backward compatibility with old EVM types. All functions require chainId parameter when used with WalletType.evm, ensuring proper chain-specific behavior. Old EVM types continue to work without changes for backward compatibility. * feat: add currency mapping support for unified EVM wallet type - Update currency mapping to support WalletType.evm with chainId-based currency resolution. - Override EVMChainWallet.currency getter to use selectedChainId from registry. - Replace walletTypeToCryptoCurrency calls with wallet.currency in view models. - Add WalletType.evm cases to switch statements and consolidate EVM transaction list item methods. - Add explorer URL method to EVM interface * feat: Update wallet operations with unified WalletType.evm - Update wallet service to always create new wallets and restores with WalletType.evm, defaulting to Ethereum. - Preserve old wallet types when opening existing wallets. Update wallet instance creation and opening logic to handle unified EVM type with chainId-based config lookup. * Add WalletType.evm support across all switch statements - Update all switch statements and conditionals to include WalletType.evm alongside existing EVM types. - Add chainId-based node selection support for unified EVM wallets. - Fix redundant code in exchange view model. - Ensure backward compatibility with old EVM wallet types. * feat(evm): Implement chainId-based node management for WalletType.evm - Add chainId-based node storage for unified EVM wallets, enabling separate node preferences per chain. - Replace unsafe dynamic casts with type-safe getSelectedChainId method. Update all node retrieval and switching logic to support chainId-based selection. * feat: Update UI components to show unified EVM wallet option - Filter out old EVM wallet types (ethereum, polygon, base, arbitrum) from wallet creation and restore UI, showing only unified WalletType.evm option. - Update wallet type generation to include WalletType.evm instead of individual chain types. - Update configure.dart to generate WalletType.evm in availableWalletTypes - Add filtering checks to prevent old EVM types from appearing * fix: Fix WalletType.evm support and complete remaining edge cases Add missing WalletType.evm cases to switch statements to resolve compilation errors. Complete remaining unification items: - Add WalletType.evm support to node, wallet utils, and view models - Update EVMChainDefaultTokens and EVMChainUtils to support chainId - Fix token initialization and priority fee calculations for WalletType.evm - Update payment view model to handle WalletType.evm with chainId All identified edge cases from the unification plan are now complete. * fix: Fix network switching issues for EVM wallets - Fix null check errors when creating and restoring an EVM wallet - Fix null check errors for balance getter when switching networks - Fix fiat amount display for native currencies in transaction history * refactor: Simplify EVM wallet chain selection in CryptoBalanceWidget - Removed the old dropdown for chain selection and replaced it with a more integrated EvmSwitcher dialog. - Updated EvmSwitcher to accept a list of available chains and handle chain selection more efficiently. - Enhanced EvmSwitcherDataItem to include chainId for better identification of chains. - Improved the overall UI flow for EVM wallet interactions. * refactor: Enhance EVM wallet handling and chainId integration - Simplified chainId checks across various components to ensure consistent handling for WalletType.evm. - Updated EVMChainDefaultTokens and EVMChainUtils checks to allow all EVM wallets for chainId operations - Handled backward compatibility for older wallet types in transaction info * refactor: More enhancement and backward compatibility fixes for EVM Wallet - Introduced a new method to retrieve cryptocurrency by chainId, improving the handling of WalletType.evm. - Updated transaction history management to filter transactions based on the current chainId - Refactored EVMChainTransactionInfo to accept chainId directly - Modified utility functions in EVMChainUtils and TokenUtilities for better chainId handling * refactor: Streamline EVM token retrieval and enhance chainId handling * feat: Introduce comprehensive guide for adding new EVM L2 networks - Added a detailed documentation file outlining the steps to integrate new EVM-compatible L2 networks into Cake Wallet. - Included prerequisites, architecture overview, and a step-by-step guide covering chain configuration, native currency addition, default tokens, and node setup. - Emphasized the unified architecture for EVM chains and backward compatibility considerations. - Removed the outdated guide on adding new L2 networks to streamline documentation. * feat: Reposition EVM switcher button on dashboard page * just adding reminders for the refactoring [skip ci] * more todos [skip ci] * feat: enhance wallet functionality with chainId support - Added chainId to `WalletBase` and updated currency method to use it. - Modified EvmChainRegistry to initialize registry on call. - Updated various components to pass chainId where necessary for currency conversions. - Improved handling of chain IDs in wallet-related view models and UI components for better compatibility with EVM networks. * fix: remove default chainId fallback in payment confirmation widget * feat: Adapt anypay to new unified flow WIP * fix: Issues with handling non evm swaps for anypay * fixes to flow structure * Add persistence to hidden chains on switcher * refactor: simplify equality operator in Erc20Token class * feat: add excluded arbutrum tokens for chainflip * chore: Remove print statement [skip ci] * refactor: dispaly QR image display for evm based on selected chain ID * feat: enhance EVM wallet compatibility check for raw address input * chore: Update doc to reflect changes [skip ci] * feat: Integrate Blink for EVM wallets for anti-sandwich attacks. - Add toggle in privacy settings to enable/disable - When enabled, route all evm broadcast rpc calls via Blink * feat: Integrate Blink for EVM wallets for anti-sandwich attacks. - Add toggle in privacy settings to enable/disable - When enabled, route all evm broadcast rpc calls via Blink * feat: Integrate Blink for EVM wallets for anti-sandwich attacks. - Add toggle in privacy settings to enable/disable - When enabled, route all evm broadcast rpc calls via Blink * feat: Handle same chainId scenario for pasted evm addresses * feat: Add Blink Setting to Advanced Settings Page * fix: Handle Blink supported chains * fix: Fixes to evm swap * fix: Add fallback for currency fetch * fix: Add evm to supported list * fix: Better handle balance errors * fix: Update token check to use contract address comparison, resolves the usdt/usdt0 issue * refactor: Simplify checks by cearing all tokens on chain switch, removing unnecessary currency checks * fix: Simplify checks based from feedback on review * fix: Exclude arbitrum from tx priority using chainId * refactor: Handle chains not having transaction priority within refactor * fix: Unified evm wallet fixes - Handle switching networks while on tx history page - Update NFTs when network is switching on listing page - Fix arbitrum tx priority error on swap screen - Switch check to display deuro from wallet type to chainId - Add fallback check for getting token info for evm - Better error message for max fee per gas less than block base fee error * refactor: Update wallet checks from type to chainId for EVM compatibility checks * refactor: Update eth chain utils and remove verified contract criteria in scam detection checks * refactor: Enhance validation for deuro calls * fix : Remove unneeded validation check for savings edit * feat: Hide EVM chain switcher behind feature flag * refactor: Remove EVM wallet type references and update related logic * chore: Clean up wallet type references and remove repeated imports * feat: Add fallback for derivation path and add arbitrum support to app scripts * fix: NFT popup error * fix: Transaction details fee regression * fix: Fixes and improvements - Add nonce parameter to transaction methods in EVM clients - Remove brackets for create swaps - Display contacts for selected wallettype alone - Remove tx priorty switching for arb * feat: Enhance Arbitrum support across swap providers * fix: Add missing secret keys * fix: display sending currency icon in send from external page * refactor: Handle arbitrum edgecase for wallet list display image * fix: Handle more cases of arbitrum image display for contact book * exploring possible issues with tx history * chore: switch to printV * fix: Add an extra check for keys * fix: Filter out eth spam transaction and modify parsing format * fix: Handle contract decimals for sending tokens * fix: Display proper balance error * Update cw_evm/lib/utils/evm_chain_formatter.dart [skip ci] --------- Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
co-authored by
Robert Malikowski
Omar Hatem
parent
0e4855132c
commit
2a77d359f1
@@ -149,7 +149,9 @@ class _SelectHardwareWalletAccountFormState extends State<SelectHardwareWalletAc
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: SelectButton(
|
||||
image: Image.asset(
|
||||
walletTypeToCryptoCurrency(_walletHardwareRestoreVM.type).iconPath ??
|
||||
getCryptoCurrencyForWalletListItem(
|
||||
_walletHardwareRestoreVM.type,
|
||||
).iconPath ??
|
||||
'',
|
||||
height: 24,
|
||||
width: 24,
|
||||
|
||||
@@ -6,11 +6,13 @@ import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'
|
||||
import 'package:cake_wallet/src/screens/dashboard/widgets/page_indicator.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:cake_wallet/src/widgets/evm_switcher.dart';
|
||||
import 'package:cake_wallet/src/widgets/gradient_background.dart';
|
||||
import 'package:cake_wallet/src/widgets/haven_wallet_removal_popup.dart';
|
||||
import 'package:cake_wallet/src/widgets/services_updates_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/vulnerable_seeds_popup.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/utils/version_comparator.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
@@ -31,7 +33,6 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
||||
import 'package:cake_wallet/main.dart';
|
||||
import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart';
|
||||
import 'package:cake_wallet/themes/core/theme_extension.dart';
|
||||
@@ -144,6 +145,36 @@ class _DashboardPageView extends BasePage {
|
||||
|
||||
@override
|
||||
Widget leading(BuildContext context) {
|
||||
if (FeatureFlag.isEVMChainSwitcherEnabled &&
|
||||
dashboardViewModel.isEVMWallet &&
|
||||
dashboardViewModel.availableChains.isNotEmpty) {
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: Size(50, 30),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => EvmSwitcher(
|
||||
chains: dashboardViewModel.availableChains,
|
||||
currentChain: dashboardViewModel.currentChain,
|
||||
onChainSelected: (chainId) => dashboardViewModel.selectChain(chainId),
|
||||
hiddenChainIds: dashboardViewModel.settingsStore.evmHiddenChainIds,
|
||||
onHiddenChanged: (hidden) =>
|
||||
dashboardViewModel.settingsStore.setEvmHiddenChainIds(hidden),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/evm_switcher.svg',
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 30,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return ServicesUpdatesWidget(
|
||||
|
||||
@@ -207,40 +207,45 @@ class AddressPage extends BasePage {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: addressListViewModel.walletImages.length * 32.0,
|
||||
child: Stack(
|
||||
children: [
|
||||
for (int i = addressListViewModel.walletImages.length - 1; i >= 0; i--)
|
||||
Positioned(
|
||||
left: i * 25.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
width: 3,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: CakeImageWidget(
|
||||
height: 35,
|
||||
width: 35,
|
||||
imageUrl: addressListViewModel.walletImages[i],
|
||||
color: addressListViewModel.walletImages.last ==
|
||||
addressListViewModel.walletImages[i]
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: null,
|
||||
Observer(
|
||||
builder: (_) {
|
||||
final walletImages = addressListViewModel
|
||||
.getWalletImages(addressListViewModel.selectedChainId);
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: walletImages.length * 32.0,
|
||||
child: Stack(
|
||||
children: [
|
||||
for (int i = walletImages.length - 1; i >= 0; i--)
|
||||
Positioned(
|
||||
left: i * 25.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
width: 3,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: CakeImageWidget(
|
||||
height: 35,
|
||||
width: 35,
|
||||
imageUrl: walletImages[i],
|
||||
color: walletImages.last == walletImages[i]
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -22,7 +22,7 @@ class BalancePage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
final isNFTActivated = isNFTACtivatedChain(dashboardViewModel.type);
|
||||
final isNFTActivated = isNFTACtivatedChain(dashboardViewModel.type, dashboardViewModel.wallet.chainId);
|
||||
return DefaultTabController(
|
||||
key: ValueKey<bool>(isNFTActivated),
|
||||
length: isNFTActivated ? 2 : 1,
|
||||
|
||||
@@ -169,6 +169,21 @@ class CryptoBalanceWidget extends StatelessWidget {
|
||||
}),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (dashboardViewModel.balanceViewModel.formattedBalances.isEmpty) {
|
||||
return Center(
|
||||
child: Container(
|
||||
child: Text(
|
||||
'Loading balances...',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
|
||||
@@ -59,7 +59,7 @@ class CakeFeaturesPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Observer(builder: (_) {
|
||||
if (dashboardViewModel.type == WalletType.ethereum) {
|
||||
if (dashboardViewModel.wallet.chainId == 1) {
|
||||
return DashBoardRoundedCardWidget(
|
||||
shadowBlur: dashboardViewModel.getShadowBlur(),
|
||||
shadowSpread: dashboardViewModel.getShadowSpread(),
|
||||
|
||||
@@ -24,11 +24,13 @@ class SignPage extends BasePage {
|
||||
_pages.add(SignForm(
|
||||
key: signFormKey,
|
||||
type: signViewModel.wallet.type,
|
||||
chainId: signViewModel.wallet.chainId,
|
||||
includeAddress: signViewModel.signIncludesAddress,
|
||||
));
|
||||
_pages.add(VerifyForm(
|
||||
key: verifyFormKey,
|
||||
type: signViewModel.wallet.type,
|
||||
chainId: signViewModel.wallet.chainId,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ class SignForm extends StatefulWidget {
|
||||
SignForm({
|
||||
Key? key,
|
||||
required this.type,
|
||||
required this.chainId,
|
||||
required this.includeAddress,
|
||||
}) : super(key: key);
|
||||
|
||||
final WalletType type;
|
||||
final bool includeAddress;
|
||||
|
||||
final int? chainId;
|
||||
|
||||
@override
|
||||
SignFormState createState() => SignFormState();
|
||||
}
|
||||
@@ -76,7 +78,7 @@ class SignFormState extends State<SignForm> with AutomaticKeepAliveClientMixin {
|
||||
onSelectedContact: (contact) {
|
||||
addressController.text = contact.address;
|
||||
},
|
||||
selectedCurrency: walletTypeToCryptoCurrency(widget.type),
|
||||
selectedCurrency: walletTypeToCryptoCurrency(widget.type, chainId: widget.chainId),
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -33,39 +33,40 @@ class TradeRow extends StatelessWidget {
|
||||
final receiveAmountCrypto = to.toString();
|
||||
|
||||
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: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: ImageUtil.getImageFromPath(
|
||||
imagePath: provider.image, height: 36, width: 36),),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 2,
|
||||
child: Container(
|
||||
height: 8,
|
||||
width: 8,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _statusColor(context, swapState),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child:
|
||||
ImageUtil.getImageFromPath(imagePath: provider.image, height: 36, width: 36),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 2,
|
||||
child: Container(
|
||||
height: 8,
|
||||
width: 8,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _statusColor(context, swapState),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
|
||||
@@ -8,10 +8,12 @@ class VerifyForm extends StatefulWidget {
|
||||
VerifyForm({
|
||||
Key? key,
|
||||
required this.type,
|
||||
required this.chainId,
|
||||
}) : super(key: key);
|
||||
|
||||
final WalletType type;
|
||||
|
||||
final int? chainId;
|
||||
|
||||
@override
|
||||
VerifyFormState createState() => VerifyFormState();
|
||||
}
|
||||
@@ -66,7 +68,7 @@ class VerifyFormState extends State<VerifyForm> with AutomaticKeepAliveClientMix
|
||||
onSelectedContact: (contact) {
|
||||
addressController.text = contact.address;
|
||||
},
|
||||
selectedCurrency: walletTypeToCryptoCurrency(widget.type),
|
||||
selectedCurrency: walletTypeToCryptoCurrency(widget.type, chainId: widget.chainId),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
AddressTextField(
|
||||
|
||||
@@ -72,7 +72,7 @@ class ExchangeTradeExternalSendPage extends BasePage {
|
||||
context,
|
||||
Routes.fullscreenQR,
|
||||
arguments: QrViewData(
|
||||
embeddedImagePath: exchangeTradeViewModel.qrImage,
|
||||
embeddedImagePath: exchangeTradeViewModel.trade.from?.iconPath,
|
||||
data: exchangeTradeViewModel.paymentUri?.toString() ??
|
||||
exchangeTradeViewModel.trade.inputAddress ??
|
||||
fetchingLabel,
|
||||
@@ -96,7 +96,7 @@ class ExchangeTradeExternalSendPage extends BasePage {
|
||||
data: exchangeTradeViewModel.paymentUri?.toString() ??
|
||||
exchangeTradeViewModel.trade.inputAddress ??
|
||||
fetchingLabel,
|
||||
embeddedImagePath: exchangeTradeViewModel.qrImage,
|
||||
embeddedImagePath: exchangeTradeViewModel.trade.from?.iconPath,
|
||||
size: 230,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -257,6 +257,14 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
|
||||
widget.privacySettingsViewModel.setDisableBulletin(value);
|
||||
},
|
||||
),
|
||||
if (widget.privacySettingsViewModel.canUseBlinkProtection)
|
||||
SettingsSwitcherCell(
|
||||
title: S.current.use_blink_protection,
|
||||
value: widget.privacySettingsViewModel.useBlinkProtection,
|
||||
onValueChange: (BuildContext _, bool value) {
|
||||
widget.privacySettingsViewModel.setUseBlinkProtection(value);
|
||||
},
|
||||
),
|
||||
SettingsSwitcherCell(
|
||||
title: S.current.add_custom_node,
|
||||
value: widget.privacySettingsViewModel.addCustomNode,
|
||||
|
||||
@@ -143,7 +143,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
|
||||
child: SelectButton(
|
||||
key: ValueKey('new_wallet_type_${type.name}_button_key'),
|
||||
image: Image.asset(
|
||||
walletTypeToCryptoCurrency(type).iconPath ?? '',
|
||||
getCryptoCurrencyForWalletListItem(type).iconPath ?? '',
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
|
||||
@@ -95,7 +95,7 @@ class WalletGroupsDisplayBody extends StatelessWidget {
|
||||
isSelected:
|
||||
walletGroupsDisplayViewModel.selectedSingleWallet == wallet,
|
||||
leadingWidget: Image.asset(
|
||||
walletTypeToCryptoCurrency(wallet.type).iconPath!,
|
||||
getCryptoCurrencyForWalletListItem(wallet.type).iconPath!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
|
||||
@@ -123,7 +123,7 @@ class GroupedWalletExpansionTile extends StatelessWidget {
|
||||
: SizedBox(width: 7),
|
||||
SizedBox(width: 24),
|
||||
Image.asset(
|
||||
walletTypeToCryptoCurrency(item.type).iconPath!,
|
||||
getCryptoCurrencyForWalletListItem(item.type).iconPath!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
|
||||
@@ -37,6 +37,9 @@ import 'package:cake_wallet/src/widgets/address_text_field.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
|
||||
class SendCard extends StatefulWidget {
|
||||
SendCard({
|
||||
@@ -176,6 +179,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
walletSwitcherViewModel,
|
||||
paymentRequest,
|
||||
);
|
||||
|
||||
break;
|
||||
case PaymentFlowType.currentWalletCompatible:
|
||||
case PaymentFlowType.error:
|
||||
@@ -221,7 +225,14 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
paymentRequest,
|
||||
result,
|
||||
),
|
||||
onSwap: () => _handleSwapFlow(paymentViewModel, result),
|
||||
onSwap: (bottomSheetContext) =>
|
||||
_handleSwapFlow(paymentViewModel, result, bottomSheetContext),
|
||||
onSwitchNetwork: () => _handleSwitchNetwork(
|
||||
paymentViewModel,
|
||||
walletSwitcherViewModel,
|
||||
paymentRequest,
|
||||
result,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -245,10 +256,14 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
paymentViewModel: paymentViewModel,
|
||||
paymentRequest: paymentRequest,
|
||||
onNext: (PaymentFlowResult newResult) {
|
||||
if (newResult.addressDetectionResult!.detectedWalletType ==
|
||||
paymentViewModel.currentWalletType) {
|
||||
final selectedChainId = newResult.chainId;
|
||||
final isCompatible = selectedChainId == evm!.getSelectedChainId(sendViewModel.wallet);
|
||||
|
||||
if (isCompatible) {
|
||||
sendViewModel.setSelectedCryptoCurrency(
|
||||
newResult.addressDetectionResult!.detectedCurrency!.title);
|
||||
newResult.addressDetectionResult!.detectedCurrency!.title,
|
||||
);
|
||||
_applyPaymentRequest(paymentRequest);
|
||||
} else {
|
||||
_showPaymentConfirmation(
|
||||
paymentViewModel,
|
||||
@@ -286,9 +301,22 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
final success = await walletSwitcherViewModel.switchToSelectedWallet();
|
||||
|
||||
if (success) {
|
||||
if (isEVMCompatibleChain(sendViewModel.wallet.type) && result.chainId != null) {
|
||||
final appStore = getIt.get<AppStore>();
|
||||
final node = appStore.settingsStore.getCurrentNode(
|
||||
sendViewModel.wallet.type,
|
||||
chainId: result.chainId,
|
||||
);
|
||||
await evm!.selectChain(sendViewModel.wallet, result.chainId!, node: node);
|
||||
}
|
||||
|
||||
await sendViewModel.wallet.updateBalance();
|
||||
sendViewModel
|
||||
.setSelectedCryptoCurrency(result.addressDetectionResult!.detectedCurrency!.title);
|
||||
|
||||
final detectedCurrency = result.addressDetectionResult!.detectedCurrency;
|
||||
if (detectedCurrency != null) {
|
||||
sendViewModel.setSelectedCryptoCurrency(detectedCurrency.title);
|
||||
}
|
||||
|
||||
_applyPaymentRequest(paymentRequest);
|
||||
}
|
||||
}
|
||||
@@ -303,7 +331,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
if (result.wallet != null) {
|
||||
if (result.type == PaymentFlowType.singleWallet && result.wallet != null) {
|
||||
walletSwitcherViewModel.selectWallet(result.wallet!);
|
||||
final success = await walletSwitcherViewModel.switchToSelectedWallet();
|
||||
if (success) {
|
||||
@@ -321,6 +349,17 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// If EVM wallet and chainId is specified, switch to that chain
|
||||
if (isEVMCompatibleChain(sendViewModel.wallet.type) && result.chainId != null) {
|
||||
final appStore = getIt.get<AppStore>();
|
||||
final node = appStore.settingsStore.getCurrentNode(
|
||||
sendViewModel.wallet.type,
|
||||
chainId: result.chainId,
|
||||
);
|
||||
await evm!.selectChain(sendViewModel.wallet, result.chainId!, node: node);
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) {
|
||||
Navigator.of(loadingBottomSheetContext!).pop();
|
||||
@@ -331,6 +370,94 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
.setSelectedCryptoCurrency(result.addressDetectionResult!.detectedCurrency!.title);
|
||||
_applyPaymentRequest(paymentRequest);
|
||||
}
|
||||
} else if (result.wallets.isNotEmpty && result.wallets.length == 1) {
|
||||
walletSwitcherViewModel.selectWallet(result.wallets.first);
|
||||
final success = await walletSwitcherViewModel.switchToSelectedWallet();
|
||||
if (success) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingBottomSheetContext = context;
|
||||
return LoadingBottomSheet(
|
||||
titleText: S.of(context).loading_your_wallet,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// If EVM wallet and chainId is specified, switch to that chain
|
||||
if (isEVMCompatibleChain(sendViewModel.wallet.type) && result.chainId != null) {
|
||||
final appStore = getIt.get<AppStore>();
|
||||
final node = appStore.settingsStore.getCurrentNode(
|
||||
sendViewModel.wallet.type,
|
||||
chainId: result.chainId,
|
||||
);
|
||||
await evm!.selectChain(sendViewModel.wallet, result.chainId!, node: node);
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) {
|
||||
Navigator.of(loadingBottomSheetContext!).pop();
|
||||
}
|
||||
|
||||
await sendViewModel.wallet.updateBalance();
|
||||
sendViewModel
|
||||
.setSelectedCryptoCurrency(result.addressDetectionResult!.detectedCurrency!.title);
|
||||
_applyPaymentRequest(paymentRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleSwitchNetwork(
|
||||
PaymentViewModel paymentViewModel,
|
||||
WalletSwitcherViewModel walletSwitcherViewModel,
|
||||
PaymentRequest paymentRequest,
|
||||
PaymentFlowResult result,
|
||||
) async {
|
||||
if (result.type != PaymentFlowType.evmNetworkSelection || result.wallet == null) return;
|
||||
|
||||
if (context.mounted && Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
try {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingBottomSheetContext = context;
|
||||
return LoadingBottomSheet(
|
||||
titleText: S.of(context).loading_your_wallet,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await paymentViewModel.selectChain();
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) {
|
||||
Navigator.of(loadingBottomSheetContext!).pop();
|
||||
}
|
||||
|
||||
await sendViewModel.wallet.updateBalance();
|
||||
final detectedCurrency = result.addressDetectionResult?.detectedCurrency;
|
||||
if (detectedCurrency != null) {
|
||||
sendViewModel.setSelectedCryptoCurrency(detectedCurrency.title);
|
||||
}
|
||||
_applyPaymentRequest(paymentRequest);
|
||||
} catch (e) {
|
||||
if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) {
|
||||
Navigator.of(loadingBottomSheetContext!).pop();
|
||||
}
|
||||
printV('Switch network error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,10 +473,17 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
noteController.text = paymentRequest.note;
|
||||
}
|
||||
|
||||
Future<void> _handleSwapFlow(PaymentViewModel paymentViewModel, PaymentFlowResult result) async {
|
||||
if (mounted && Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
Future<void> _handleSwapFlow(
|
||||
PaymentViewModel paymentViewModel,
|
||||
PaymentFlowResult result,
|
||||
BuildContext bottomSheetContext,
|
||||
) async {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final bottomSheet = getIt.get<SwapConfirmationBottomSheet>(param1: result);
|
||||
await showModalBottomSheet<Trade?>(
|
||||
context: context,
|
||||
@@ -549,31 +683,21 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
builder: (context, snapshot) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
sendViewModel.balanceViewModel
|
||||
.switchBalanceValue();
|
||||
sendViewModel.balanceViewModel.switchBalanceValue();
|
||||
},
|
||||
child: Observer(builder: (_) {
|
||||
final hidden = sendViewModel
|
||||
.balanceViewModel.displayMode ==
|
||||
final hidden = sendViewModel.balanceViewModel.displayMode ==
|
||||
BalanceDisplayMode.hiddenBalance;
|
||||
return Text(
|
||||
hidden
|
||||
? S.of(context).show_balance_send_page
|
||||
: (snapshot.data ??
|
||||
sendViewModel.balance),
|
||||
: (snapshot.data ?? sendViewModel.balance),
|
||||
// default to balance while loading
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: hidden
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -102,6 +102,14 @@ class PrivacyPage extends BasePage {
|
||||
_privacySettingsViewModel.setDisableBulletin(value);
|
||||
},
|
||||
),
|
||||
if (_privacySettingsViewModel.canUseBlinkProtection)
|
||||
SettingsSwitcherCell(
|
||||
title: S.current.use_blink_protection,
|
||||
value: _privacySettingsViewModel.useBlinkProtection,
|
||||
onValueChange: (BuildContext _, bool value) {
|
||||
_privacySettingsViewModel.setUseBlinkProtection(value);
|
||||
},
|
||||
),
|
||||
if (_privacySettingsViewModel.canUseEtherscan)
|
||||
SettingsSwitcherCell(
|
||||
title: S.current.etherscan_history,
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
|
||||
enum EVMChainId {
|
||||
ethereum,
|
||||
polygon,
|
||||
base,
|
||||
goerli,
|
||||
mumbai,
|
||||
arbitrum,
|
||||
}
|
||||
|
||||
extension EVMChainIdX on EVMChainId {
|
||||
String chain() {
|
||||
String name = '';
|
||||
final chainId = _getChainIdForEnum(this);
|
||||
|
||||
switch (this) {
|
||||
case EVMChainId.ethereum:
|
||||
name = '1';
|
||||
break;
|
||||
case EVMChainId.polygon:
|
||||
name = '137';
|
||||
break;
|
||||
case EVMChainId.base:
|
||||
name = '8453';
|
||||
break;
|
||||
case EVMChainId.goerli:
|
||||
name = '5';
|
||||
break;
|
||||
case EVMChainId.arbitrum:
|
||||
name = '42161';
|
||||
break;
|
||||
case EVMChainId.mumbai:
|
||||
name = '80001';
|
||||
break;
|
||||
}
|
||||
if (chainId == null) return 'eip155:1';
|
||||
|
||||
return 'eip155:$name';
|
||||
return evm!.getCaip2ByChainId(chainId);
|
||||
}
|
||||
|
||||
int? _getChainIdForEnum(EVMChainId id) {
|
||||
return switch (id) {
|
||||
EVMChainId.ethereum => 1,
|
||||
EVMChainId.polygon => 137,
|
||||
EVMChainId.base => 8453,
|
||||
EVMChainId.arbitrum => 42161,
|
||||
};
|
||||
}
|
||||
|
||||
int? get chainId => _getChainIdForEnum(this);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class EvmChainServiceImpl {
|
||||
Web3Client? web3Client,
|
||||
}) : ethClient = web3Client ??
|
||||
Web3Client(
|
||||
appStore.settingsStore.getCurrentNode(appStore.wallet!.type).uri.toString(),
|
||||
_getNodeUriForChain(reference, appStore),
|
||||
ProxyWrapper().getHttpIOClient(),
|
||||
) {
|
||||
for (final event in EventsConstants.allEvents) {
|
||||
@@ -77,6 +77,19 @@ class EvmChainServiceImpl {
|
||||
|
||||
String getChainId() => reference.chain();
|
||||
|
||||
static String _getNodeUriForChain(EVMChainId reference, AppStore appStore) {
|
||||
final walletType = appStore.wallet!.type;
|
||||
|
||||
if (isEVMCompatibleChain(walletType)) {
|
||||
final chainId = reference.chainId;
|
||||
|
||||
return appStore.settingsStore.getCurrentNode(walletType, chainId: chainId).uri.toString();
|
||||
}
|
||||
|
||||
// For old wallet types, use the wallet type directly
|
||||
return appStore.settingsStore.getCurrentNode(walletType).uri.toString();
|
||||
}
|
||||
|
||||
Future<void> personalSign(String topic, dynamic parameters) async {
|
||||
debugPrint('personalSign request: $parameters');
|
||||
|
||||
@@ -502,7 +515,7 @@ class EvmChainServiceImpl {
|
||||
|
||||
// Get the primary type and types
|
||||
final primaryType = typedData['primaryType']?.toString() ?? '';
|
||||
final types = typedData['types'] as Map<String, dynamic>? ?? {};
|
||||
final types = typedData['types'] as Map<String, dynamic>? ?? {};
|
||||
final message = typedData['message'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
// Build a readable message based on the primary type and its structure
|
||||
@@ -584,7 +597,6 @@ $messageDetails''';
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
|
||||
|
||||
final symbol = (decodedResponse['symbol'] ?? '') as String;
|
||||
|
||||
+9
-13
@@ -1,7 +1,4 @@
|
||||
import 'package:cake_wallet/arbitrum/arbitrum.dart';
|
||||
import 'package:cake_wallet/base/base.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/polygon/polygon.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:cake_wallet/solana/solana.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/chain_key_model.dart';
|
||||
@@ -22,13 +19,10 @@ class KeyServiceImpl implements WalletConnectKeyService {
|
||||
static String _getPrivateKeyForWallet(WalletBase wallet) {
|
||||
switch (wallet.type) {
|
||||
case WalletType.ethereum:
|
||||
return ethereum!.getPrivateKey(wallet);
|
||||
case WalletType.polygon:
|
||||
return polygon!.getPrivateKey(wallet);
|
||||
case WalletType.base:
|
||||
return base!.getPrivateKey(wallet);
|
||||
case WalletType.arbitrum:
|
||||
return arbitrum!.getPrivateKey(wallet);
|
||||
return evm!.getPrivateKey(wallet);
|
||||
case WalletType.solana:
|
||||
return solana!.getPrivateKey(wallet);
|
||||
default:
|
||||
@@ -39,13 +33,10 @@ class KeyServiceImpl implements WalletConnectKeyService {
|
||||
static String _getPublicKeyForWallet(WalletBase wallet) {
|
||||
switch (wallet.type) {
|
||||
case WalletType.ethereum:
|
||||
return ethereum!.getPublicKey(wallet);
|
||||
case WalletType.polygon:
|
||||
return polygon!.getPublicKey(wallet);
|
||||
case WalletType.base:
|
||||
return base!.getPublicKey(wallet);
|
||||
case WalletType.arbitrum:
|
||||
return arbitrum!.getPublicKey(wallet);
|
||||
return evm!.getPublicKey(wallet);
|
||||
case WalletType.solana:
|
||||
return solana!.getPublicKey(wallet);
|
||||
default:
|
||||
@@ -82,7 +73,12 @@ class KeyServiceImpl implements WalletConnectKeyService {
|
||||
|
||||
@override
|
||||
List<ChainKeyModel> getKeysForChain(WalletBase wallet) {
|
||||
final chain = getChainNameSpaceAndIdBasedOnWalletType(wallet.type);
|
||||
int? chainId;
|
||||
if (isEVMCompatibleChain(wallet.type)) {
|
||||
final chainInfo = evm!.getCurrentChain(wallet);
|
||||
chainId = chainInfo?.chainId;
|
||||
}
|
||||
final chain = getChainNameSpaceAndIdBasedOnWalletType(wallet.type, chainId: chainId);
|
||||
|
||||
final keys = getKeys(wallet);
|
||||
|
||||
|
||||
@@ -105,14 +105,22 @@ abstract class WalletKitServiceBase with Store {
|
||||
List<ChainKeyModel> chainKeys = walletKeyService.getKeys(appStore.wallet!);
|
||||
for (final chainKey in chainKeys) {
|
||||
for (final chainId in chainKey.chains) {
|
||||
final chainNameSpace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type);
|
||||
if (chainNameSpace == chainId) {
|
||||
final account = '$chainId:${chainKey.publicKey}';
|
||||
debugPrint('registerAccount $account');
|
||||
if (isEVMCompatibleChain(appStore.wallet!.type)) {
|
||||
// Register account for all EVM chains (chainId is already in eip155:format)
|
||||
_walletKit.registerAccount(
|
||||
chainId: chainId,
|
||||
accountAddress: chainKey.publicKey,
|
||||
);
|
||||
} else {
|
||||
final chainNameSpace = getChainNameSpaceAndIdBasedOnWalletType(
|
||||
appStore.wallet!.type,
|
||||
);
|
||||
if (chainNameSpace == chainId) {
|
||||
_walletKit.registerAccount(
|
||||
chainId: chainId,
|
||||
accountAddress: chainKey.publicKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,6 +390,14 @@ abstract class WalletKitServiceBase with Store {
|
||||
@action
|
||||
void _onPairingCreate(PairingEvent? args) {
|
||||
debugPrint('_onPairingCreate $args');
|
||||
|
||||
if (args != null && args.topic != null && args.topic!.isNotEmpty) {
|
||||
// Save the pairing topic when pairing is created
|
||||
savePairingTopicToLocalStorage(args.topic!);
|
||||
|
||||
// Refresh pairings to show the new pairing in the list
|
||||
_refreshPairings();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSessionAuthRequest(SessionAuthRequest? args) async {
|
||||
@@ -562,9 +578,11 @@ abstract class WalletKitServiceBase with Store {
|
||||
final filteredPairings = allPairings.where(
|
||||
(pairing) {
|
||||
bool isInCurrentTopics = currentTopicsForWallet.contains(pairing.topic);
|
||||
bool isActive = pairing.active;
|
||||
// bool isActive = pairing.active;
|
||||
// bool hasSession = sessions.any((session) => session.pairingTopic == pairing.topic);
|
||||
|
||||
return isInCurrentTopics && isActive;
|
||||
// return isInCurrentTopics && isActive;
|
||||
return isInCurrentTopics;
|
||||
},
|
||||
).toList();
|
||||
|
||||
@@ -577,16 +595,30 @@ abstract class WalletKitServiceBase with Store {
|
||||
}
|
||||
|
||||
String getKeyForStoringTopicsForWallet() {
|
||||
List<ChainKeyModel> chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
|
||||
try {
|
||||
// For EVM wallets, use getKeys() to get all EVM keys
|
||||
// since the same address works across all EVM chains. For non-EVM wallets, use getKeysForChain()
|
||||
// to get keys specific to the current chain.
|
||||
List<ChainKeyModel> chainKeys;
|
||||
if (isEVMCompatibleChain(appStore.wallet!.type)) {
|
||||
chainKeys = walletKeyService.getKeys(appStore.wallet!);
|
||||
|
||||
if (chainKeys.isEmpty) {
|
||||
chainKeys = chainKeys
|
||||
.where((key) => key.chains.any((chain) => chain.startsWith('eip155:')))
|
||||
.toList();
|
||||
} else {
|
||||
chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
|
||||
}
|
||||
|
||||
if (chainKeys.isEmpty) return '';
|
||||
|
||||
final publicKey = chainKeys.first.publicKey;
|
||||
if (publicKey.isEmpty) return '';
|
||||
|
||||
return PreferencesKey.walletConnectPairingTopicsListForWallet(publicKey);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
|
||||
final keyForPairingTopic =
|
||||
PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey);
|
||||
|
||||
return keyForPairingTopic;
|
||||
}
|
||||
|
||||
List<String> getPairingTopicsForWallet(String key) {
|
||||
@@ -615,7 +647,8 @@ abstract class WalletKitServiceBase with Store {
|
||||
final pairingTopicsForWallet = getPairingTopicsForWallet(key);
|
||||
|
||||
bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic);
|
||||
debugPrint('Is Pairing Topic Saved: $isPairingTopicAlreadySaved');
|
||||
debugPrint(
|
||||
'Is Pairing Topic Saved: $isPairingTopicAlreadySaved, Key: $key, Topic: $pairingTopic');
|
||||
|
||||
if (!isPairingTopicAlreadySaved) {
|
||||
// Update the list with the most recent pairing topic
|
||||
|
||||
+69
-59
@@ -30,71 +30,81 @@ class BottomSheetListenerState extends State<BottomSheetListener> {
|
||||
}
|
||||
|
||||
Future<void> _showBottomSheet() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (widget.bottomSheetService.currentSheet.value != null) {
|
||||
BottomSheetQueueItemModel item = widget.bottomSheetService.currentSheet.value!;
|
||||
final value = await showModalBottomSheet(
|
||||
context: context,
|
||||
isDismissible: item.isModalDismissible,
|
||||
backgroundColor: Color.fromARGB(0, 0, 0, 0),
|
||||
isScrollControlled: true,
|
||||
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.9),
|
||||
builder: (context) {
|
||||
if (item.closeAfter > 0) {
|
||||
Future.delayed(Duration(seconds: item.closeAfter), () {
|
||||
try {
|
||||
if (!mounted) return;
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
try {
|
||||
final value = await showModalBottomSheet(
|
||||
context: context,
|
||||
isDismissible: item.isModalDismissible,
|
||||
backgroundColor: Color.fromARGB(0, 0, 0, 0),
|
||||
isScrollControlled: true,
|
||||
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.9),
|
||||
builder: (context) {
|
||||
if (item.closeAfter > 0) {
|
||||
Future.delayed(Duration(seconds: item.closeAfter), () {
|
||||
try {
|
||||
if (!mounted) return;
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugPrint('[$runtimeType] close $e $s');
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugPrint('[$runtimeType] close $e $s');
|
||||
}
|
||||
});
|
||||
}
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
});
|
||||
}
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.end,
|
||||
// children: [
|
||||
// IconButton(
|
||||
// color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
// padding: const EdgeInsets.all(0.0),
|
||||
// visualDensity: VisualDensity.compact,
|
||||
// onPressed: () {
|
||||
// if (Navigator.canPop(context)) {
|
||||
// Navigator.pop(context);
|
||||
// }
|
||||
// },
|
||||
// icon: Icon(
|
||||
// Icons.close_sharp,
|
||||
// color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
Flexible(child: item.widget),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.end,
|
||||
// children: [
|
||||
// IconButton(
|
||||
// color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
// padding: const EdgeInsets.all(0.0),
|
||||
// visualDensity: VisualDensity.compact,
|
||||
// onPressed: () {
|
||||
// if (Navigator.canPop(context)) {
|
||||
// Navigator.pop(context);
|
||||
// }
|
||||
// },
|
||||
// icon: Icon(
|
||||
// Icons.close_sharp,
|
||||
// color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
Flexible(child: item.widget),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!item.completer.isCompleted) {
|
||||
item.completer.complete(value);
|
||||
if (!item.completer.isCompleted) {
|
||||
item.completer.complete(value);
|
||||
}
|
||||
widget.bottomSheetService.showNext();
|
||||
} catch (e) {
|
||||
if (!item.completer.isCompleted) {
|
||||
item.completer.complete(null);
|
||||
}
|
||||
widget.bottomSheetService.showNext();
|
||||
}
|
||||
widget.bottomSheetService.showNext();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,20 +48,19 @@ class WalletListPage extends BasePage {
|
||||
String get title => S.current.wallets;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => Observer(
|
||||
builder: (_) {
|
||||
if (walletListViewModel.singleWalletsList.isEmpty && walletListViewModel.multiWalletGroups.isEmpty) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
Widget body(BuildContext context) => Observer(builder: (_) {
|
||||
if (walletListViewModel.singleWalletsList.isEmpty &&
|
||||
walletListViewModel.multiWalletGroups.isEmpty) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return WalletListBody(
|
||||
walletListViewModel: walletListViewModel,
|
||||
authService: authService,
|
||||
onWalletLoaded: onWalletLoaded ?? (context) => Navigator.of(context).pop(),
|
||||
);
|
||||
}
|
||||
return WalletListBody(
|
||||
walletListViewModel: walletListViewModel,
|
||||
authService: authService,
|
||||
onWalletLoaded: onWalletLoaded ?? (context) => Navigator.of(context).pop(),
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget trailing(BuildContext context) {
|
||||
@@ -222,7 +221,7 @@ class WalletListBodyState extends State<WalletListBody> {
|
||||
return item.isCurrent
|
||||
? SizedBox.shrink()
|
||||
: EditWalletButtonWidget(
|
||||
width: 60,
|
||||
width: 64,
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.walletEdit,
|
||||
arguments: WalletEditPageArguments(
|
||||
@@ -288,7 +287,9 @@ class WalletListBodyState extends State<WalletListBody> {
|
||||
)
|
||||
: SizedBox(width: 6),
|
||||
Image.asset(
|
||||
walletTypeToCryptoCurrency(wallet.type).iconPath!,
|
||||
getCryptoCurrencyForWalletListItem(
|
||||
wallet.type,
|
||||
).iconPath!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:cake_wallet/core/universal_address_detector.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
@@ -11,7 +11,6 @@ import 'package:cake_wallet/utils/qr_util.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/utils/token_utilities.dart';
|
||||
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
|
||||
import 'package:cake_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
@@ -60,21 +59,21 @@ class _EVMPaymentFlowContent extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _EVMPaymentFlowContentState extends State<_EVMPaymentFlowContent> {
|
||||
WalletType? selectedNetwork;
|
||||
int? selectedChainId;
|
||||
CryptoCurrency? selectedToken;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedNetwork = WalletType.ethereum;
|
||||
selectedChainId = widget.paymentViewModel.detectedChainId ?? 1;
|
||||
_autoSelectToken();
|
||||
}
|
||||
|
||||
Future<void> _autoSelectToken() async {
|
||||
if (selectedNetwork == null) return;
|
||||
if (selectedChainId == null) return;
|
||||
|
||||
try {
|
||||
final tokens = await TokenUtilities.getAvailableTokensForNetwork(selectedNetwork!);
|
||||
final tokens = await TokenUtilities.getAvailableTokensForChainId(selectedChainId!);
|
||||
if (tokens.isNotEmpty) {
|
||||
setState(() {
|
||||
selectedToken = tokens.first;
|
||||
@@ -124,11 +123,11 @@ class _EVMPaymentFlowContentState extends State<_EVMPaymentFlowContent> {
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
EVMTileWidget(
|
||||
value: selectedNetwork != null
|
||||
? walletTypeToString(selectedNetwork!)
|
||||
value: selectedChainId != null
|
||||
? _getChainName(selectedChainId!)
|
||||
: S.current.select_network,
|
||||
imagePath: selectedNetwork != null ? getChainMonoImage(selectedNetwork!) : null,
|
||||
color: selectedNetwork != null ? Theme.of(context).colorScheme.primary : null,
|
||||
imagePath: selectedChainId != null ? _getChainImagePath(selectedChainId!) : null,
|
||||
color: selectedChainId != null ? Theme.of(context).colorScheme.primary : null,
|
||||
enabled: true,
|
||||
onTap: () => _showNetworkSelection(context),
|
||||
),
|
||||
@@ -136,7 +135,7 @@ class _EVMPaymentFlowContentState extends State<_EVMPaymentFlowContent> {
|
||||
EVMTileWidget(
|
||||
imagePath: selectedToken != null ? selectedToken!.iconPath : null,
|
||||
value: selectedToken != null ? selectedToken!.title : S.current.select_token,
|
||||
enabled: selectedNetwork != null,
|
||||
enabled: selectedChainId != null,
|
||||
onTap: () => _showTokenSelection(context),
|
||||
color: selectedToken == null ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
@@ -145,7 +144,7 @@ class _EVMPaymentFlowContentState extends State<_EVMPaymentFlowContent> {
|
||||
text: S.current.restore_next,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: selectedNetwork != null && selectedToken != null
|
||||
onPressed: selectedChainId != null && selectedToken != null
|
||||
? () async => await _handleNext(context)
|
||||
: null,
|
||||
),
|
||||
@@ -156,30 +155,34 @@ class _EVMPaymentFlowContentState extends State<_EVMPaymentFlowContent> {
|
||||
}
|
||||
|
||||
void _showNetworkSelection(BuildContext context) async {
|
||||
final evmNetworks =
|
||||
availableWalletTypes.where((walletType) => isEVMCompatibleChain(walletType)).toList();
|
||||
final selectedIndex = evmNetworks.indexOf(selectedNetwork ?? WalletType.ethereum);
|
||||
final allChains = evm!.getAllChains();
|
||||
final chainIds = allChains.map((chainInfo) => chainInfo.chainId).toList();
|
||||
|
||||
final selectedIndex = selectedChainId != null ? chainIds.indexOf(selectedChainId!) : 0;
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Picker(
|
||||
items: evmNetworks,
|
||||
displayItem: (WalletType network) => walletTypeToString(network),
|
||||
selectedAtIndex: selectedIndex,
|
||||
items: chainIds,
|
||||
displayItem: (int chainId) => _getChainName(chainId),
|
||||
selectedAtIndex: selectedIndex >= 0 ? selectedIndex : 0,
|
||||
title: S.current.select_network,
|
||||
closeOnItemSelected: true,
|
||||
hasTitleSpacing: true,
|
||||
images: evmNetworks
|
||||
.map((network) => CakeImageWidget(
|
||||
imageUrl: getChainMonoImage(network),
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: Theme.of(context).colorScheme.primary))
|
||||
.toList(),
|
||||
onItemSelected: (WalletType network) {
|
||||
images: chainIds.map((chainId) {
|
||||
final imagePath = _getChainImagePath(chainId);
|
||||
return imagePath != null
|
||||
? CakeImageWidget(
|
||||
imageUrl: imagePath,
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: Theme.of(context).colorScheme.primary)
|
||||
: const SizedBox(width: 20, height: 20);
|
||||
}).toList(),
|
||||
onItemSelected: (int chainId) {
|
||||
setState(() {
|
||||
selectedNetwork = network;
|
||||
selectedChainId = chainId;
|
||||
selectedToken = null;
|
||||
});
|
||||
_autoSelectToken();
|
||||
@@ -189,11 +192,24 @@ class _EVMPaymentFlowContentState extends State<_EVMPaymentFlowContent> {
|
||||
);
|
||||
}
|
||||
|
||||
String _getChainName(int chainId) {
|
||||
final allChains = evm!.getAllChains();
|
||||
final chainInfo = allChains.firstWhere(
|
||||
(chain) => chain.chainId == chainId,
|
||||
orElse: () => ChainInfo(chainId: chainId, name: 'Unknown Network', shortCode: 'unknown'),
|
||||
);
|
||||
return chainInfo.name;
|
||||
}
|
||||
|
||||
String? _getChainImagePath(int chainId) {
|
||||
return getChainMonoImage(WalletType.ethereum, selectedChainId: chainId);
|
||||
}
|
||||
|
||||
void _showTokenSelection(BuildContext context) async {
|
||||
if (selectedNetwork == null) return;
|
||||
if (selectedChainId == null) return;
|
||||
|
||||
try {
|
||||
final availableTokens = await TokenUtilities.getAvailableTokensForNetwork(selectedNetwork!);
|
||||
final availableTokens = await TokenUtilities.getAvailableTokensForChainId(selectedChainId!);
|
||||
|
||||
if (availableTokens.isEmpty) return;
|
||||
|
||||
@@ -220,30 +236,36 @@ class _EVMPaymentFlowContentState extends State<_EVMPaymentFlowContent> {
|
||||
}
|
||||
|
||||
Future<void> _handleNext(BuildContext context) async {
|
||||
if (selectedNetwork == null || selectedToken == null) return;
|
||||
if (selectedChainId == null || selectedToken == null) return;
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
final compatibleWallets = await widget.paymentViewModel.getWalletsByType(selectedNetwork!);
|
||||
final allEVMWallets = await widget.paymentViewModel.getEVMCompatibleWallets();
|
||||
|
||||
final newResult = PaymentFlowResult.evmNetworkSelection(
|
||||
AddressDetectionResult(
|
||||
address: widget.paymentRequest.address,
|
||||
detectedWalletType: selectedNetwork!,
|
||||
detectedCurrency: selectedToken!,
|
||||
isValid: true,
|
||||
amount: widget.paymentRequest.amount,
|
||||
note: widget.paymentRequest.note,
|
||||
scheme: widget.paymentRequest.scheme,
|
||||
pjUri: widget.paymentRequest.pjUri,
|
||||
callbackUrl: widget.paymentRequest.callbackUrl,
|
||||
callbackMessage: widget.paymentRequest.callbackMessage,
|
||||
),
|
||||
compatibleWallets: compatibleWallets,
|
||||
wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
|
||||
final walletType = evm!.getWalletTypeByChainId(selectedChainId!) ?? WalletType.ethereum;
|
||||
|
||||
final detectionResult = AddressDetectionResult(
|
||||
address: widget.paymentRequest.address,
|
||||
detectedWalletType: walletType,
|
||||
detectedCurrency: selectedToken!,
|
||||
chainId: selectedChainId,
|
||||
isValid: true,
|
||||
amount: widget.paymentRequest.amount,
|
||||
note: widget.paymentRequest.note,
|
||||
scheme: widget.paymentRequest.scheme,
|
||||
pjUri: widget.paymentRequest.pjUri,
|
||||
callbackUrl: widget.paymentRequest.callbackUrl,
|
||||
callbackMessage: widget.paymentRequest.callbackMessage,
|
||||
);
|
||||
|
||||
widget.paymentViewModel.detectedWalletType = selectedNetwork!;
|
||||
final newResult = PaymentFlowResult.evmNetworkSelection(
|
||||
detectionResult,
|
||||
compatibleWallets: allEVMWallets,
|
||||
wallet: allEVMWallets.isNotEmpty ? allEVMWallets.first : null,
|
||||
);
|
||||
|
||||
widget.paymentViewModel.detectedWalletType = walletType;
|
||||
|
||||
widget.onNext(newResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:cake_wallet/entities/generate_name.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
@@ -7,6 +9,7 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class PaymentConfirmationBottomSheet extends BaseBottomSheet {
|
||||
@@ -19,6 +22,7 @@ class PaymentConfirmationBottomSheet extends BaseBottomSheet {
|
||||
required this.onSelectWallet,
|
||||
required this.onChangeWallet,
|
||||
required this.onSwap,
|
||||
this.onSwitchNetwork,
|
||||
}) : super(
|
||||
titleText: '',
|
||||
footerType: FooterType.none,
|
||||
@@ -31,7 +35,8 @@ class PaymentConfirmationBottomSheet extends BaseBottomSheet {
|
||||
final PaymentRequest paymentRequest;
|
||||
final VoidCallback onSelectWallet;
|
||||
final VoidCallback onChangeWallet;
|
||||
final VoidCallback onSwap;
|
||||
final void Function(BuildContext) onSwap;
|
||||
final VoidCallback? onSwitchNetwork;
|
||||
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) {
|
||||
@@ -43,6 +48,7 @@ class PaymentConfirmationBottomSheet extends BaseBottomSheet {
|
||||
onSelectWallet: onSelectWallet,
|
||||
onChangeWallet: onChangeWallet,
|
||||
onSwap: onSwap,
|
||||
onSwitchNetwork: onSwitchNetwork,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +62,7 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
required this.onSelectWallet,
|
||||
required this.onChangeWallet,
|
||||
required this.onSwap,
|
||||
this.onSwitchNetwork,
|
||||
});
|
||||
|
||||
final PaymentFlowResult paymentFlowResult;
|
||||
@@ -64,7 +71,8 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
final PaymentRequest paymentRequest;
|
||||
final VoidCallback onSelectWallet;
|
||||
final VoidCallback onChangeWallet;
|
||||
final VoidCallback onSwap;
|
||||
final void Function(BuildContext) onSwap;
|
||||
final VoidCallback? onSwitchNetwork;
|
||||
|
||||
/// Checks if the given address is a MWEB or SP (Silent Payment) address
|
||||
bool _isMwebOrSpAddress(String address) {
|
||||
@@ -86,7 +94,9 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
final currencyName = walletTypeToString(paymentViewModel.detectedWalletType!);
|
||||
final currentWalletName = walletTypeToString(paymentViewModel.currentWalletType);
|
||||
final currentWalletName = isEVMCompatibleChain(paymentViewModel.currentWalletType)
|
||||
? evm!.getChainNameByChainId(paymentViewModel.currentChainId!)
|
||||
: walletTypeToString(paymentViewModel.currentWalletType);
|
||||
|
||||
final hasSingleWallet = paymentFlowResult.type == PaymentFlowType.singleWallet ||
|
||||
paymentFlowResult.wallets.length == 1;
|
||||
@@ -102,6 +112,15 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
final isMwebOrSpAddress =
|
||||
_isMwebOrSpAddress(paymentFlowResult.addressDetectionResult?.address ?? '');
|
||||
|
||||
/// If the wallet is EVM but the detected chainId is different from the currently selected chainId
|
||||
final isEVMWalletButDifferentChainId =
|
||||
paymentFlowResult.type == PaymentFlowType.evmNetworkSelection &&
|
||||
paymentFlowResult.wallet != null &&
|
||||
isEVMCompatibleChain(paymentViewModel.currentWalletType);
|
||||
|
||||
final otherWalletsCount = paymentFlowResult.wallets.length;
|
||||
final hasMultipleOtherWallets = otherWalletsCount > 1;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
child: Column(
|
||||
@@ -121,7 +140,10 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Image.asset(
|
||||
walletTypeToCryptoCurrency(paymentViewModel.detectedWalletType!).iconPath!,
|
||||
getCryptoCurrencyForWalletListItem(
|
||||
paymentViewModel.detectedWalletType!,
|
||||
chainId: paymentViewModel.detectedChainId,
|
||||
).iconPath!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
@@ -141,7 +163,7 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'''Would you like to ${!noAvailableWallets ? 'switch to a $currencyName wallet or' : ''} swap $currentWalletName for ${paymentFlowResult.addressDetectionResult?.detectedCurrency} (${walletTypeToString(paymentFlowResult.walletType!)}) for this payment?''',
|
||||
'''Would you like to ${!noAvailableWallets ? 'switch to a $currencyName wallet or' : ''} swap ${currentWalletName.capitalized()} for ${paymentFlowResult.addressDetectionResult?.detectedCurrency} (${walletTypeToString(paymentFlowResult.walletType!)}) for this payment?''',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
@@ -152,7 +174,10 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
),
|
||||
] else ...[
|
||||
Image.asset(
|
||||
walletTypeToCryptoCurrency(paymentViewModel.detectedWalletType!).iconPath!,
|
||||
getCryptoCurrencyForWalletListItem(
|
||||
paymentViewModel.detectedWalletType!,
|
||||
chainId: paymentViewModel.detectedChainId,
|
||||
).iconPath!,
|
||||
width: 118,
|
||||
height: 118,
|
||||
),
|
||||
@@ -170,7 +195,7 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'''Looks like you scanned a $currencyName address.\n\n'''
|
||||
'''Would you like to ${!noAvailableWallets ? 'switch to a $currencyName wallet or' : ''} swap $currentWalletName for $currencyName for this payment?''',
|
||||
'''Would you like to ${!noAvailableWallets ? 'switch to a $currencyName wallet or' : ''} swap ${currentWalletName.capitalized()} for $currencyName for this payment?''',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
@@ -181,11 +206,44 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 72),
|
||||
if (hasAtLeastOneWallet) ...[
|
||||
if (isEVMWalletButDifferentChainId) ...[
|
||||
if (!isMwebOrSpAddress) ...[
|
||||
PrimaryButton(
|
||||
onPressed: onSwap,
|
||||
text: '${S.current.swap} $currentWalletName',
|
||||
onPressed: () => onSwap(context),
|
||||
text: 'Create In App Swap',
|
||||
color: hasAtLeastOneWallet
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
textColor: hasAtLeastOneWallet
|
||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||
: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
if (onSwitchNetwork != null) ...[
|
||||
PrimaryButton(
|
||||
onPressed: onSwitchNetwork,
|
||||
text:
|
||||
'Switch to ${evm!.getChainNameByChainId(paymentViewModel.detectedChainId!).toUpperCase()} Network',
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
if (hasAtLeastOneWallet) ...[
|
||||
PrimaryButton(
|
||||
onPressed: hasMultipleOtherWallets ? onSelectWallet : onChangeWallet,
|
||||
text: S.current.change_wallet_alert_title,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
] else if (hasAtLeastOneWallet) ...[
|
||||
if (!isMwebOrSpAddress) ...[
|
||||
PrimaryButton(
|
||||
onPressed: () => onSwap(context),
|
||||
text: '${S.current.swap} ${currentWalletName.capitalized()}',
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
@@ -208,8 +266,8 @@ class _PaymentConfirmationContent extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
if (!isMwebOrSpAddress) ...[
|
||||
PrimaryButton(
|
||||
onPressed: onSwap,
|
||||
text: '${S.current.swap} $currentWalletName',
|
||||
onPressed: () => onSwap(context),
|
||||
text: '${S.current.swap} ${currentWalletName.capitalized()}',
|
||||
color: hasAtLeastOneWallet
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:cake_wallet/core/auth_service.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/exchange/limits_state.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart';
|
||||
@@ -166,16 +167,20 @@ class SwapConfirmationContentState extends State<SwapConfirmationContent> {
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
Positioned(
|
||||
bottom: -4,
|
||||
right: -4,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: walletTypeToCryptoCurrency(widget.paymentFlowResult.walletType!)
|
||||
.iconPath!,
|
||||
width: 16,
|
||||
height: 16,
|
||||
if (isEVMCompatibleChain(widget.paymentFlowResult.walletType!)) ...[
|
||||
Positioned(
|
||||
bottom: -4,
|
||||
right: -4,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: getCryptoCurrencyForWalletListItem(
|
||||
widget.paymentFlowResult.walletType!,
|
||||
chainId: widget.paymentFlowResult.chainId,
|
||||
).iconPath!,
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -34,11 +34,14 @@ class _SwapDetailsBottomSheetState extends State<SwapDetailsBottomSheet> {
|
||||
bool _effectsInstalled = false;
|
||||
ReactionDisposer? _exchangeStateReaction;
|
||||
BuildContext? _loadingBottomSheetContext;
|
||||
bool _showingFailureDialog = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _setEffects());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _setEffects();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -49,7 +52,39 @@ class _SwapDetailsBottomSheetState extends State<SwapDetailsBottomSheet> {
|
||||
}
|
||||
|
||||
void _setEffects() {
|
||||
if (_effectsInstalled) return;
|
||||
if (_effectsInstalled) {
|
||||
printV('Swap details bottom sheet effects already installed');
|
||||
return;
|
||||
}
|
||||
|
||||
final initialState = widget.exchangeTradeViewModel.sendViewModel.state;
|
||||
|
||||
if (initialState is FailureState && !_showingFailureDialog) {
|
||||
_showingFailureDialog = true;
|
||||
printV('Initial failure state: $initialState');
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && context.mounted) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return AlertWithOneAction(
|
||||
key: ValueKey('swap_details_send_failure_dialog_key'),
|
||||
buttonKey: ValueKey('swap_details_send_failure_dialog_button_key'),
|
||||
alertTitle: S.of(popupContext).error,
|
||||
alertContent: initialState.error,
|
||||
buttonText: S.of(popupContext).ok,
|
||||
buttonAction: () {
|
||||
_showingFailureDialog = false;
|
||||
Navigator.of(popupContext).pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_showingFailureDialog = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_exchangeStateReaction = reaction(
|
||||
(_) => widget.exchangeTradeViewModel.sendViewModel.state,
|
||||
@@ -61,21 +96,29 @@ class _SwapDetailsBottomSheetState extends State<SwapDetailsBottomSheet> {
|
||||
Navigator.of(_loadingBottomSheetContext!).pop();
|
||||
}
|
||||
|
||||
if (state is FailureState) {
|
||||
if (state is FailureState && !_showingFailureDialog) {
|
||||
_showingFailureDialog = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return AlertWithOneAction(
|
||||
key: ValueKey('swap_details_send_failure_dialog_key'),
|
||||
buttonKey: ValueKey('swap_details_send_failure_dialog_button_key'),
|
||||
alertTitle: S.of(popupContext).error,
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(popupContext).ok,
|
||||
buttonAction: () => Navigator.of(popupContext).pop(),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (mounted && context.mounted) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return AlertWithOneAction(
|
||||
key: ValueKey('swap_details_send_failure_dialog_key'),
|
||||
buttonKey: ValueKey('swap_details_send_failure_dialog_button_key'),
|
||||
alertTitle: S.of(popupContext).error,
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(popupContext).ok,
|
||||
buttonAction: () {
|
||||
_showingFailureDialog = false;
|
||||
Navigator.of(popupContext).pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
_showingFailureDialog = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -272,13 +315,15 @@ class _SwapDetailsContent extends StatelessWidget {
|
||||
children: [
|
||||
_SwapDetailsTile(
|
||||
label: 'You Send',
|
||||
value: '${trade.amount} ${trade.from?.title ?? trade.userCurrencyFrom?.title ?? ''}',
|
||||
value:
|
||||
'${trade.amount} ${trade.from?.title ?? trade.userCurrencyFrom?.title ?? ''}',
|
||||
valueFiatFormatted: exchangeTradeViewModel.sendAmountFiatFormatted,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_SwapDetailsTile(
|
||||
label: 'You Get',
|
||||
value: '${trade.receiveAmount ?? '0'} ${trade.to?.title ?? trade.userCurrencyTo?.title ?? ''}',
|
||||
value:
|
||||
'${trade.receiveAmount ?? '0'} ${trade.to?.title ?? trade.userCurrencyTo?.title ?? ''}',
|
||||
valueFiatFormatted: exchangeTradeViewModel
|
||||
.getReceiveAmountFiatFormatted(trade.receiveAmount ?? '0.0'),
|
||||
),
|
||||
|
||||
@@ -57,7 +57,7 @@ class _WalletSwitcherContent extends StatelessWidget {
|
||||
builder: (context, snapshot) => Observer(
|
||||
builder: (_) {
|
||||
final List<WalletInfo> wallets = (snapshot.data ?? []);
|
||||
|
||||
|
||||
if (viewModel.isProcessing) {
|
||||
return Container(
|
||||
height: 200,
|
||||
@@ -68,7 +68,7 @@ class _WalletSwitcherContent extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return Container(
|
||||
height: 400,
|
||||
child: Column(
|
||||
@@ -79,7 +79,7 @@ class _WalletSwitcherContent extends StatelessWidget {
|
||||
itemCount: wallets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final wallet = wallets[index];
|
||||
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
viewModel.selectWallet(wallet);
|
||||
@@ -97,7 +97,9 @@ class _WalletSwitcherContent extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
walletTypeToCryptoCurrency(wallet.type).iconPath!,
|
||||
getCryptoCurrencyForWalletListItem(
|
||||
wallet.type,
|
||||
).iconPath!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import 'evm_switcher_row.dart';
|
||||
|
||||
class EvmSwitcherDataItem {
|
||||
final String name;
|
||||
final String svgPath;
|
||||
final int chainId;
|
||||
|
||||
const EvmSwitcherDataItem({
|
||||
required this.name,
|
||||
required this.svgPath,
|
||||
required this.chainId,
|
||||
});
|
||||
|
||||
static const ethereum = EvmSwitcherDataItem(
|
||||
name: 'Ethereum',
|
||||
svgPath: 'assets/images/evm_switcher_icons/ethereum.svg',
|
||||
chainId: 1,
|
||||
);
|
||||
|
||||
static const polygon = EvmSwitcherDataItem(
|
||||
name: 'Polygon',
|
||||
svgPath: 'assets/images/evm_switcher_icons/polygon.svg',
|
||||
chainId: 137,
|
||||
);
|
||||
|
||||
static const arbitrum = EvmSwitcherDataItem(
|
||||
name: 'Arbitrum',
|
||||
svgPath: 'assets/images/evm_switcher_icons/arbitrum.svg',
|
||||
chainId: 42161,
|
||||
);
|
||||
|
||||
static const base = EvmSwitcherDataItem(
|
||||
name: 'Base',
|
||||
svgPath: 'assets/images/evm_switcher_icons/base.svg',
|
||||
chainId: 8453,
|
||||
);
|
||||
|
||||
static const items = [
|
||||
ethereum,
|
||||
polygon,
|
||||
arbitrum,
|
||||
base,
|
||||
];
|
||||
}
|
||||
|
||||
String _getSvgPathForChain(String chainName) {
|
||||
final name = chainName.toLowerCase();
|
||||
if (name.contains('ethereum')) {
|
||||
return 'assets/images/evm_switcher_icons/ethereum.svg';
|
||||
} else if (name.contains('polygon')) {
|
||||
return 'assets/images/evm_switcher_icons/polygon.svg';
|
||||
} else if (name.contains('arbitrum')) {
|
||||
return 'assets/images/evm_switcher_icons/arbitrum.svg';
|
||||
} else if (name.contains('base')) {
|
||||
return 'assets/images/evm_switcher_icons/base.svg';
|
||||
}
|
||||
// Default to ethereum if unknown
|
||||
return 'assets/images/evm_switcher_icons/ethereum.svg';
|
||||
}
|
||||
|
||||
class EvmSwitcher extends StatefulWidget {
|
||||
const EvmSwitcher({
|
||||
super.key,
|
||||
required this.chains,
|
||||
required this.currentChain,
|
||||
required this.onChainSelected,
|
||||
required this.hiddenChainIds,
|
||||
required this.onHiddenChanged,
|
||||
});
|
||||
|
||||
final List<ChainInfo> chains;
|
||||
final ChainInfo? currentChain;
|
||||
final Future<void> Function(int chainId) onChainSelected;
|
||||
final Set<int> hiddenChainIds;
|
||||
final void Function(Set<int> hiddenChainIds) onHiddenChanged;
|
||||
|
||||
static const editModeAnimDuration = Duration(milliseconds: 200);
|
||||
|
||||
@override
|
||||
State<EvmSwitcher> createState() => _EvmSwitcherState();
|
||||
}
|
||||
|
||||
class _EvmSwitcherState extends State<EvmSwitcher> {
|
||||
bool _editMode = false;
|
||||
var optionsEnabled = <bool>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_syncOptionsEnabled();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant EvmSwitcher oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.chains != widget.chains ||
|
||||
!_hiddenSetsEqual(oldWidget.hiddenChainIds, widget.hiddenChainIds)) {
|
||||
_syncOptionsEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
void _syncOptionsEnabled() {
|
||||
optionsEnabled = widget.chains
|
||||
.map((chain) => !widget.hiddenChainIds.contains(chain.chainId))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
bool _hiddenSetsEqual(Set<int> a, Set<int> b) =>
|
||||
a.length == b.length && a.containsAll(b);
|
||||
|
||||
int get _selectedIndex {
|
||||
if (widget.currentChain == null) return -1;
|
||||
return widget.chains.indexWhere(
|
||||
(chain) => chain.chainId == widget.currentChain!.chainId,
|
||||
);
|
||||
}
|
||||
|
||||
bool shouldBuildSeparator(int index) {
|
||||
final nextEnabled = optionsEnabled.indexWhere(
|
||||
(e) => e,
|
||||
index + 1,
|
||||
);
|
||||
|
||||
return index != _selectedIndex &&
|
||||
optionsEnabled[index] &&
|
||||
nextEnabled != _selectedIndex &&
|
||||
nextEnabled != -1;
|
||||
}
|
||||
|
||||
Set<int> _currentHiddenChainIds() {
|
||||
final hidden = <int>{};
|
||||
for (var i = 0; i < widget.chains.length; i++) {
|
||||
if (i < optionsEnabled.length && !optionsEnabled[i]) {
|
||||
hidden.add(widget.chains[i].chainId);
|
||||
}
|
||||
}
|
||||
return hidden;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double popupWidth = MediaQuery.of(context).size.width * 0.9;
|
||||
return Center(
|
||||
child: Column(
|
||||
spacing: 25.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_editMode ? "Customize options" : "Select Network",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
duration: EvmSwitcher.editModeAnimDuration,
|
||||
width: popupWidth,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
AnimatedSize(
|
||||
curve: Curves.easeOutCubic,
|
||||
duration: EvmSwitcher.editModeAnimDuration,
|
||||
child: Container(
|
||||
width: _editMode ? 0 : popupWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (optionsEnabled[index]) {
|
||||
final chain = widget.chains[index];
|
||||
final data = EvmSwitcherDataItem(
|
||||
name: chain.name,
|
||||
svgPath: _getSvgPathForChain(chain.name),
|
||||
chainId: chain.chainId,
|
||||
);
|
||||
return EvmSwitcherRow(
|
||||
key: ValueKey(chain.chainId),
|
||||
data: data,
|
||||
editMode: false,
|
||||
selected: index == _selectedIndex,
|
||||
onTap: () {
|
||||
widget.onChainSelected(chain.chainId);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
editSwitchValue: true,
|
||||
animDuration: EvmSwitcher.editModeAnimDuration,
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
if (shouldBuildSeparator(index))
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
));
|
||||
else
|
||||
return Container(height: 1);
|
||||
},
|
||||
itemCount: widget.chains.length),
|
||||
EvmSwitcherAdditionalOption(
|
||||
title: "Customize options",
|
||||
svgPath: "assets/images/evm_switcher_arrow_right.svg",
|
||||
animDuration: EvmSwitcher.editModeAnimDuration,
|
||||
topSeparator: true,
|
||||
bottomSeparator: false,
|
||||
visible: true,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_editMode = true;
|
||||
});
|
||||
},
|
||||
iconOnRight: true)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
curve: Curves.easeOutCubic,
|
||||
duration: EvmSwitcher.editModeAnimDuration,
|
||||
child: Container(
|
||||
width: _editMode ? popupWidth : 0,
|
||||
height: _editMode ? null : 0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
EvmSwitcherAdditionalOption(
|
||||
title: "Back",
|
||||
svgPath: "assets/images/evm_switcher_arrow_left.svg",
|
||||
animDuration: EvmSwitcher.editModeAnimDuration,
|
||||
topSeparator: false,
|
||||
bottomSeparator: true,
|
||||
visible: true,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_editMode = false;
|
||||
});
|
||||
},
|
||||
iconOnRight: false),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final chain = widget.chains[index];
|
||||
final data = EvmSwitcherDataItem(
|
||||
name: chain.name,
|
||||
svgPath: _getSvgPathForChain(chain.name),
|
||||
chainId: chain.chainId,
|
||||
);
|
||||
return EvmSwitcherRow(
|
||||
key: ValueKey(chain.chainId),
|
||||
data: data,
|
||||
editMode: _editMode,
|
||||
selected: index == _selectedIndex,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
optionsEnabled[index] = !optionsEnabled[index];
|
||||
});
|
||||
widget.onHiddenChanged(
|
||||
_currentHiddenChainIds(),
|
||||
);
|
||||
},
|
||||
editSwitchValue: optionsEnabled[index],
|
||||
animDuration: EvmSwitcher.editModeAnimDuration,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
));
|
||||
},
|
||||
itemCount: widget.chains.length),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EvmSwitcherAdditionalOption extends StatelessWidget {
|
||||
const EvmSwitcherAdditionalOption(
|
||||
{super.key,
|
||||
required this.title,
|
||||
required this.svgPath,
|
||||
required this.animDuration,
|
||||
required this.topSeparator,
|
||||
required this.bottomSeparator,
|
||||
required this.visible,
|
||||
required this.onTap,
|
||||
required this.iconOnRight});
|
||||
|
||||
final String title;
|
||||
final String svgPath;
|
||||
final Duration animDuration;
|
||||
final bool topSeparator;
|
||||
final bool bottomSeparator;
|
||||
final bool visible;
|
||||
final VoidCallback onTap;
|
||||
final bool iconOnRight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Container(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
if (topSeparator)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
if (!iconOnRight) SvgPicture.asset(svgPath, width: 16, height: 16),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
if (iconOnRight) SvgPicture.asset(svgPath, width: 16, height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (bottomSeparator)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:cake_wallet/src/widgets/evm_switcher.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_switch.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class EvmSwitcherRow extends StatelessWidget {
|
||||
const EvmSwitcherRow({
|
||||
super.key,
|
||||
required this.editMode,
|
||||
required this.selected,
|
||||
required this.data,
|
||||
required this.onTap,
|
||||
required this.animDuration,
|
||||
required this.editSwitchValue,
|
||||
});
|
||||
|
||||
final bool editMode;
|
||||
final bool selected;
|
||||
final EvmSwitcherDataItem data;
|
||||
final VoidCallback onTap;
|
||||
final Duration animDuration;
|
||||
final bool editSwitchValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color resolvedForegroundColor = editMode || selected
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
|
||||
final Color resolvedBackgroundColor = !editMode && selected
|
||||
? Theme.of(context).colorScheme.surfaceContainerHighest
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest.withAlpha(0);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: animDuration,
|
||||
color: resolvedBackgroundColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
data.svgPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
resolvedForegroundColor, BlendMode.srcIn),
|
||||
),
|
||||
Text(data.name,
|
||||
style: TextStyle(
|
||||
color: resolvedForegroundColor, fontSize: 14)),
|
||||
if (selected && !editMode)
|
||||
SvgPicture.asset("assets/images/evm_switcher_checkmark.svg",
|
||||
width: 18, height: 18),
|
||||
],
|
||||
),
|
||||
if (editMode)
|
||||
StandardSwitch(value: editSwitchValue, onTapped: onTap)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user