diff --git a/CHANGELOG.md b/CHANGELOG.md index 2efb89c1..4a267ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.0.0-beta18 + +- Fixes some issues with optionalNamespaces +- Fixes an issue where using includedWallets was causing a exception +- UI fixes and bug fixes + ## 3.0.0-beta17 - UI fixes diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index 256a0760..eb121227 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -3,9 +3,6 @@ import 'package:flutter/material.dart'; import 'package:web3modal_flutter/web3modal_flutter.dart'; import 'package:walletconnect_flutter_dapp/widgets/session_widget.dart'; -import 'package:walletconnect_flutter_dapp/models/chain_metadata.dart'; -import 'package:walletconnect_flutter_dapp/utils/crypto/chain_data_wrapper.dart'; -import 'package:walletconnect_flutter_dapp/utils/crypto/helpers.dart'; import 'package:walletconnect_flutter_dapp/utils/dart_defines.dart'; import 'package:walletconnect_flutter_dapp/utils/string_constants.dart'; @@ -18,17 +15,20 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - IWeb3App? _web3App; + late W3MService _w3mService; bool _initialized = false; @override void initState() { super.initState(); - _initialize(); + _initializeService(); } - void _initialize() async { - _web3App = await Web3App.createInstance( + void _initializeService() async { + // See https://docs.walletconnect.com/web3modal/flutter/custom-chains + W3MChainPresets.chains.putIfAbsent('42220', () => _exampleCustomChain); + + _w3mService = W3MService( projectId: DartDefines.projectId, logLevel: LogLevel.error, metadata: const PairingMetadata( @@ -42,44 +42,45 @@ class _MyHomePageState extends State { ), ), ); + await _w3mService.init(); - _web3App!.onSessionPing.subscribe(_onSessionPing); - _web3App!.onSessionEvent.subscribe(_onSessionEvent); + // If you want to support just one chain uncomment this line and avoid using W3MNetworkSelectButton() + // _w3mService.selectChain(W3MChainPresets.chains['137']); - await _web3App!.init(); - - // Loop through all the chain data - for (final ChainMetadata chain in ChainDataWrapper.chains) { - // Loop through the events for that chain - for (final event in getChainEvents(chain.type)) { - _web3App!.registerEventHandler( - chainId: chain.w3mChainInfo.namespace, - event: event, - handler: null, - ); - } - } + _w3mService.addListener(_serviceListener); + _w3mService.web3App?.onSessionEvent.subscribe(_onSessionEvent); + _w3mService.web3App?.onSessionConnect.subscribe(_onSessionConnect); + _w3mService.web3App?.onSessionDelete.subscribe(_onSessionDelete); setState(() => _initialized = true); } @override void dispose() { - _web3App!.onSessionPing.unsubscribe(_onSessionPing); - _web3App!.onSessionEvent.unsubscribe(_onSessionEvent); + _w3mService.web3App?.onSessionEvent.unsubscribe(_onSessionEvent); + _w3mService.web3App?.onSessionConnect.unsubscribe(_onSessionConnect); + _w3mService.web3App?.onSessionDelete.unsubscribe(_onSessionDelete); super.dispose(); } + void _serviceListener() { + setState(() {}); + } + + void _onSessionEvent(SessionEvent? args) { + debugPrint('[$runtimeType] _onSessionEvent $args'); + } + + void _onSessionConnect(SessionConnect? args) { + debugPrint('[$runtimeType] _onSessionConnect $args'); + } + + void _onSessionDelete(SessionDelete? args) { + debugPrint('[$runtimeType] _onSessionDelete $args'); + } + @override Widget build(BuildContext context) { - if (!_initialized) { - return Center( - child: CircularProgressIndicator( - color: Web3ModalTheme.colorsOf(context).accent100, - ), - ); - } - return Scaffold( backgroundColor: Web3ModalTheme.colorsOf(context).background300, appBar: AppBar( @@ -96,173 +97,47 @@ class _MyHomePageState extends State { ), ], ), - body: _W3MPage(web3App: _web3App!), + body: Builder(builder: (context) { + if (!_initialized) { + return Center( + child: CircularProgressIndicator( + color: Web3ModalTheme.colorsOf(context).accent100, + ), + ); + } + return SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _ButtonsView(w3mService: _w3mService), + const Divider(height: 0.0), + _ConnectedView(w3mService: _w3mService) + ], + ), + ); + }), ); } - - void _onSessionPing(SessionPing? args) { - debugPrint('[$runtimeType] ${StringConstants.receivedPing}: $args'); - } - - void _onSessionEvent(SessionEvent? args) { - debugPrint('[$runtimeType] ${StringConstants.receivedEvent}: $args'); - } } -class _W3MPage extends StatefulWidget { - const _W3MPage({required this.web3App}); - final IWeb3App web3App; - - @override - State<_W3MPage> createState() => _W3MPageState(); -} - -class _W3MPageState extends State<_W3MPage> { - late IWeb3App _web3App; - late W3MService _w3mService; - bool _isConnected = false; - - @override - void initState() { - super.initState(); - _web3App = widget.web3App; - _web3App.onSessionConnect.subscribe(_onWeb3AppConnect); - _web3App.onSessionDelete.subscribe(_onWeb3AppDisconnect); - - _initializeService(); - } - - void _initializeService() async { - _w3mService = W3MService( - web3App: _web3App, - logLevel: LogLevel.error, - // featuredWalletIds: { - // 'f2436c67184f158d1beda5df53298ee84abfc367581e4505134b5bcf5f46697d', - // '8a0ee50d1f22f6651afcae7eb4253e52a3310b90af5daef78a8c4929a9bb99d4', - // 'f5b4eeb6015d66be3f5940a895cbaa49ef3439e518cd771270e6b553b48f31d2', - // }, - ); - - // See https://docs.walletconnect.com/web3modal/flutter/custom-chains - W3MChainPresets.chains.putIfAbsent('42220', () => myCustomChain); - W3MChainPresets.chains.putIfAbsent('11155111', () => sepoliaTestnet); - await _w3mService.init(); - // _w3mService.selectChain(myCustomChain); - - setState(() { - _isConnected = _web3App.sessions.getAll().isNotEmpty; - }); - } - - @override - void dispose() { - _web3App.onSessionConnect.unsubscribe(_onWeb3AppConnect); - _web3App.onSessionDelete.unsubscribe(_onWeb3AppDisconnect); - super.dispose(); - } - - void _onWeb3AppConnect(SessionConnect? args) { - // If we connect, default to barebones - setState(() { - _isConnected = true; - }); - } - - void _onWeb3AppDisconnect(SessionDelete? args) { - setState(() { - _isConnected = false; - }); - } +class _ButtonsView extends StatelessWidget { + const _ButtonsView({required this.w3mService}); + final W3MService w3mService; @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox.square(dimension: 8.0), - Visibility( - visible: !_isConnected, - child: W3MNetworkSelectButton(service: _w3mService), - ), - W3MConnectWalletButton( - service: _w3mService, - state: ConnectButtonState.none, - ), - const SizedBox.square(dimension: 8.0), - const Divider(height: 0.0), - Visibility( - visible: _isConnected, - child: _ConnectedView(w3mService: _w3mService), - ), - ], - ), + return Column( + children: [ + const SizedBox.square(dimension: 8.0), + Visibility( + visible: !w3mService.isConnected, + child: W3MNetworkSelectButton(service: w3mService), + ), + W3MConnectWalletButton(service: w3mService), + const SizedBox.square(dimension: 8.0), + ], ); } - - W3MChainInfo get myCustomChain => W3MChainInfo( - chainName: 'Celo', - namespace: 'eip155:42220', - chainId: '42220', - tokenName: 'CELO', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: [ - 'personal_sign', - 'eth_signTypedData', - 'eth_sendTransaction', - ], - chains: ['eip155:42220'], - events: [ - 'chainChanged', - 'accountsChanged', - ], - ), - }, - optionalNamespaces: { - 'eip155': const RequiredNamespace( - methods: [ - 'wallet_switchEthereumChain', - 'wallet_addEthereumChain', - ], - chains: ['eip155:42220'], - events: [], - ), - }, - rpcUrl: 'https://1rpc.io/celo', - blockExplorer: W3MBlockExplorer( - name: 'Celo Scan', - url: 'https://celoscan.io', - ), - ); - - W3MChainInfo sepoliaTestnet = W3MChainInfo( - chainName: 'Sepolia Test Network', - namespace: 'eip155:11155111', - chainId: '11155111', - tokenName: 'SETH', - chainIcon: - 'https://assets-global.website-files.com/5f973c97cf5aea614f93a26c/6495cd7e2f11ba72bd274ef6_alchemy-rpc-node-provider-logo.png', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:11155111'], - events: EthConstants.ethEvents, - ), - }, - optionalNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, - chains: ['eip155:11155111'], - events: [], - ), - }, - rpcUrl: 'https://rpc.sepolia.org', - blockExplorer: W3MBlockExplorer( - name: 'Sepolia Etherscan', - url: 'https://sepolia.etherscan.io', - ), - ); } class _ConnectedView extends StatelessWidget { @@ -271,6 +146,9 @@ class _ConnectedView extends StatelessWidget { @override Widget build(BuildContext context) { + if (!w3mService.isConnected) { + return const SizedBox.shrink(); + } return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -279,6 +157,7 @@ class _ConnectedView extends StatelessWidget { SessionWidget( session: w3mService.web3App!.sessions.getAll().first, web3App: w3mService.web3App!, + selectedChain: w3mService.selectedChain!, launchRedirect: () { w3mService.launchConnectedWallet(); }, @@ -288,3 +167,22 @@ class _ConnectedView extends StatelessWidget { ); } } + +final _exampleCustomChain = W3MChainInfo( + chainName: 'Celo', + namespace: 'eip155:42220', + chainId: '42220', + tokenName: 'CELO', + optionalNamespaces: { + 'eip155': const RequiredNamespace( + methods: EthConstants.allMethods, + chains: ['eip155:42220'], + events: EthConstants.allEvents, + ), + }, + rpcUrl: 'https://1rpc.io/celo', + blockExplorer: W3MBlockExplorer( + name: 'Celo Scan', + url: 'https://celoscan.io', + ), +); diff --git a/example/lib/utils/crypto/eip155.dart b/example/lib/utils/crypto/eip155.dart index 8d2e0218..e800d1b0 100644 --- a/example/lib/utils/crypto/eip155.dart +++ b/example/lib/utils/crypto/eip155.dart @@ -10,7 +10,7 @@ import 'package:walletconnect_flutter_dapp/utils/crypto/web3dart_extension.dart' enum EIP155UIMethods { personalSign, - ethSignTypedData, + ethSignTypedDataV4, ethSendTransaction, testContractCall, } @@ -71,7 +71,8 @@ class EIP155 { EIP155UIMethods.personalSign: 'personal_sign', // EIP155Methods.ethSign: 'eth_sign', // EIP155Methods.ethSignTransaction: 'eth_signTransaction', - EIP155UIMethods.ethSignTypedData: 'eth_signTypedData', + // EIP155UIMethods.ethSignTypedData: 'eth_signTypedData', + EIP155UIMethods.ethSignTypedDataV4: 'eth_signTypedData_v4', EIP155UIMethods.testContractCall: 'test_contractCall', EIP155UIMethods.ethSendTransaction: 'eth_sendTransaction', // EIP155Methods.walletSwitchEthereumChain: 'wallet_switchEthereumChain', @@ -90,6 +91,7 @@ class EIP155 { required String chainId, required String address, }) { + final id = int.parse(chainId.split(':')[1]); switch (method) { case EIP155UIMethods.personalSign: return personalSign( @@ -99,13 +101,13 @@ class EIP155 { address: address, data: testSignData, ); - case EIP155UIMethods.ethSignTypedData: - return ethSignTypedData( + case EIP155UIMethods.ethSignTypedDataV4: + return ethSignTypedDataV4( web3App: web3App, topic: topic, chainId: chainId, address: address, - data: testSignTypedData(int.parse(chainId.split(':')[1])), + data: typedData(id), ); case EIP155UIMethods.testContractCall: return testContractCall( @@ -176,7 +178,7 @@ class EIP155 { ); } - static Future ethSignTypedData({ + static Future ethSignTypedDataV4({ required IWeb3App web3App, required String topic, required String chainId, @@ -187,7 +189,7 @@ class EIP155 { topic: topic, chainId: chainId, request: SessionRequestParams( - method: methods[EIP155UIMethods.ethSignTypedData]!, + method: methods[EIP155UIMethods.ethSignTypedDataV4]!, params: [address, data], ), ); diff --git a/example/lib/utils/crypto/test_data.dart b/example/lib/utils/crypto/test_data.dart index b6defa2c..584fb2ee 100644 --- a/example/lib/utils/crypto/test_data.dart +++ b/example/lib/utils/crypto/test_data.dart @@ -1,37 +1,6 @@ -import 'dart:convert'; +// import 'dart:convert'; -const String testSignData = 'Test walletconnect_flutter_dapp data'; -String testSignTypedData(int chainId) => jsonEncode({ - 'types': { - 'Person': [ - {'name': 'name', 'type': 'string'}, - {'name': 'wallet', 'type': 'address'} - ], - 'Mail': [ - {'name': 'from', 'type': 'Person'}, - {'name': 'to', 'type': 'Person'}, - {'name': 'contents', 'type': 'string'} - ] - }, - 'message': { - 'from': { - 'name': 'Cow', - 'wallet': '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' - }, - 'to': { - 'name': 'Bob', - 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' - }, - 'contents': 'Hello, Bob!' - }, - 'domain': { - 'name': 'Ether Mail', - 'version': '1', - 'chainId': chainId, - 'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' - }, - 'primaryType': 'Mail' - }); +const String testSignData = 'Test Web3Modal data'; String typedData(int chainId) => ''' { diff --git a/example/lib/widgets/session_widget.dart b/example/lib/widgets/session_widget.dart index 3735cebe..0734e0a1 100644 --- a/example/lib/widgets/session_widget.dart +++ b/example/lib/widgets/session_widget.dart @@ -15,10 +15,12 @@ class SessionWidget extends StatefulWidget { required this.session, required this.web3App, required this.launchRedirect, + required this.selectedChain, }); final SessionData session; final IWeb3App web3App; + final W3MChainInfo selectedChain; final void Function() launchRedirect; @override @@ -32,25 +34,50 @@ class SessionWidgetState extends State { const SizedBox(height: StyleConstants.linear16), Text( widget.session.peer.metadata.name, - style: StyleConstants.titleText, + style: Web3ModalTheme.getDataOf(context).textStyles.title600.copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), textAlign: TextAlign.center, ), - const SizedBox(height: StyleConstants.linear16), - Text('${StringConstants.sessionTopic}${widget.session.topic}'), + const SizedBox(height: StyleConstants.linear8), + Text( + StringConstants.sessionTopic, + style: Web3ModalTheme.getDataOf(context).textStyles.small600.copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), + ), + Text( + widget.session.topic, + style: Web3ModalTheme.getDataOf(context).textStyles.small400.copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), + ), ]; + children.addAll([ + const SizedBox(height: StyleConstants.linear8), + Text( + 'Approved methods:', + style: Web3ModalTheme.getDataOf(context).textStyles.small600.copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), + ), + ]); + children.add(_buildChainApprovedMethodsTiles()); + children.add(const SizedBox.square(dimension: 8.0)); + // Get all of the accounts final List namespaceAccounts = []; // Loop through the namespaces, and get the accounts - for (final Namespace namespace in widget.session.namespaces.values) { + for (final namespace in widget.session.namespaces.values) { namespaceAccounts.addAll(namespace.accounts); } - // Loop through the namespace accounts and build the widgets - for (final String namespaceAccount in namespaceAccounts) { - children.add(_buildAccountWidget(namespaceAccount)); - } + final namespace = namespaceAccounts.firstWhere( + (nsa) => nsa.split(':')[1] == widget.selectedChain.chainId, + ); + children.add(_buildAccountWidget(namespace)); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -61,33 +88,47 @@ class SessionWidgetState extends State { } Widget _buildAccountWidget(String namespaceAccount) { - final String chainId = NamespaceUtils.getChainFromAccount( - namespaceAccount, - ); - final String account = NamespaceUtils.getAccount( - namespaceAccount, - ); - final ChainMetadata chainMetadata = getChainMetadataFromChain(chainId); + final chainId = NamespaceUtils.getChainFromAccount(namespaceAccount); + final account = NamespaceUtils.getAccount(namespaceAccount); + final chainMetadata = getChainMetadataFromChain(chainId); final List children = [ Text( chainMetadata.w3mChainInfo.chainName, - style: StyleConstants.subtitleText, + style: Web3ModalTheme.getDataOf(context).textStyles.title600.copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), ), const SizedBox(height: StyleConstants.linear8), - Text(account, textAlign: TextAlign.center), - const SizedBox(height: StyleConstants.linear8), - const Text( - StringConstants.methods, - style: StyleConstants.buttonText, + Text( + account, + style: Web3ModalTheme.getDataOf(context).textStyles.small400.copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), ), ]; + children.addAll([ + const SizedBox(height: StyleConstants.linear8), + Text( + StringConstants.methods, + style: + Web3ModalTheme.getDataOf(context).textStyles.paragraph600.copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), + ), + ]); children.addAll(_buildChainMethodButtons(chainMetadata, account)); children.addAll([ const SizedBox(height: StyleConstants.linear8), - const Text(StringConstants.events, style: StyleConstants.buttonText), + Text( + StringConstants.events, + style: + Web3ModalTheme.getDataOf(context).textStyles.paragraph600.copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), + ), ]); children.add(_buildChainEventsTiles(chainMetadata)); @@ -144,8 +185,12 @@ class SessionWidgetState extends State { ), child: Text( method, - style: StyleConstants.buttonText, - textAlign: TextAlign.center, + style: Web3ModalTheme.getDataOf(context) + .textStyles + .small600 + .copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), ), ), ), @@ -155,6 +200,20 @@ class SessionWidgetState extends State { return buttons; } + Widget _buildChainApprovedMethodsTiles() { + String methods = ''; + final approvedMethods = widget.session.namespaces['eip155']?.methods ?? []; + for (final method in approvedMethods) { + methods += '$method, '; + } + return Text( + methods, + style: Web3ModalTheme.getDataOf(context).textStyles.small400.copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), + ); + } + Widget _buildChainEventsTiles(ChainMetadata chainMetadata) { final List values = []; // Add Methods @@ -178,8 +237,10 @@ class SessionWidgetState extends State { ), child: Text( event, - style: StyleConstants.buttonText, - textAlign: TextAlign.center, + style: + Web3ModalTheme.getDataOf(context).textStyles.small400.copyWith( + color: Web3ModalTheme.colorsOf(context).foreground100, + ), ), ), ); diff --git a/example/pubspec.lock b/example/pubspec.lock index 84d0b0e5..548a92f9 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1073,10 +1073,10 @@ packages: dependency: "direct main" description: name: walletconnect_flutter_v2 - sha256: "55315779dd94b5b38754c8abcccba6e8f159083e799945dc49441a20dc73cffd" + sha256: "00005750b01c5b924fc028e329bee5a77f79691c8c537709536e8ef27360510a" url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.9" watcher: dependency: transitive description: @@ -1107,7 +1107,7 @@ packages: path: ".." relative: true source: path - version: "3.0.0-beta17" + version: "3.0.0-beta18" web_socket_channel: dependency: transitive description: diff --git a/lib/constants/eth_constants.dart b/lib/constants/eth_constants.dart index 6645e979..6d65149c 100644 --- a/lib/constants/eth_constants.dart +++ b/lib/constants/eth_constants.dart @@ -1,14 +1,41 @@ class EthConstants { - static const ethRequiredMethods = [ + static const walletSwitchEthChain = 'wallet_switchEthereumChain'; + static const walletAddEthChain = 'wallet_addEthereumChain'; + static const requiredMethods = [ + 'eth_sendTransaction', 'personal_sign', + ]; + static const optionalMethods = [ + 'eth_accounts', + 'eth_requestAccounts', + 'eth_sendRawTransaction', + 'eth_sign', + 'eth_signTransaction', 'eth_signTypedData', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', 'eth_sendTransaction', + 'personal_sign', + walletSwitchEthChain, + walletAddEthChain, + 'wallet_getPermissions', + 'wallet_requestPermissions', + 'wallet_registerOnboarding', + 'wallet_watchAsset', + 'wallet_scanQRCode', ]; - static const walletSwitchEthChain = 'wallet_switchEthereumChain'; - static const walletAddEthChain = 'wallet_addEthereumChain'; - static const ethOptionalMethods = [walletSwitchEthChain, walletAddEthChain]; - static const ethMethods = [...ethRequiredMethods, ...ethOptionalMethods]; + static const allMethods = [...requiredMethods, ...optionalMethods]; + // static const chainChanged = 'chainChanged'; static const accountsChanged = 'accountsChanged'; - static const ethEvents = [chainChanged, accountsChanged]; + static const requiredEvents = [ + chainChanged, + accountsChanged, + ]; + static const optionalEvents = [ + 'message', + 'disconnect', + 'connect', + ]; + static const allEvents = [...requiredEvents, ...optionalEvents]; } diff --git a/lib/constants/string_constants.dart b/lib/constants/string_constants.dart index 254f5611..b0aa8304 100644 --- a/lib/constants/string_constants.dart +++ b/lib/constants/string_constants.dart @@ -1,8 +1,8 @@ class StringConstants { // Request Headers static const X_SDK_TYPE = 'w3m'; - static const X_SDK_VERSION = '3.0.0-beta17'; - static const X_CORE_SDK_VERSION = 'flutter_v2.1.8'; + static const X_SDK_VERSION = '3.0.0-beta18'; + static const X_CORE_SDK_VERSION = 'flutter_v2.1.9'; // UI static const String selectNetwork = 'Select network'; diff --git a/lib/models/w3m_chain_info.dart b/lib/models/w3m_chain_info.dart index e8283b3c..e7298059 100644 --- a/lib/models/w3m_chain_info.dart +++ b/lib/models/w3m_chain_info.dart @@ -9,11 +9,11 @@ class W3MChainInfo with _$W3MChainInfo { required String chainName, required String chainId, required String namespace, - String? chainIcon, required String tokenName, - required Map requiredNamespaces, - required Map optionalNamespaces, required String rpcUrl, + @Default({}) Map requiredNamespaces, + @Default({}) Map optionalNamespaces, + String? chainIcon, W3MBlockExplorer? blockExplorer, }) = _W3MChainInfo; } @@ -25,3 +25,25 @@ class W3MBlockExplorer with _$W3MBlockExplorer { required String url, }) = _W3MBlockExplorer; } + +class W3MNamespace { + const W3MNamespace({ + this.chains, + required this.methods, + required this.events, + }); + + final List? chains; + final List methods; + final List events; +} + +extension W3MNamespaceExtension on W3MNamespace { + RequiredNamespace toRequired() { + return RequiredNamespace( + chains: chains, + methods: methods, + events: events, + ); + } +} diff --git a/lib/models/w3m_chain_info.freezed.dart b/lib/models/w3m_chain_info.freezed.dart index 0f6153ec..8c2652a9 100644 --- a/lib/models/w3m_chain_info.freezed.dart +++ b/lib/models/w3m_chain_info.freezed.dart @@ -19,13 +19,13 @@ mixin _$W3MChainInfo { String get chainName => throw _privateConstructorUsedError; String get chainId => throw _privateConstructorUsedError; String get namespace => throw _privateConstructorUsedError; - String? get chainIcon => throw _privateConstructorUsedError; String get tokenName => throw _privateConstructorUsedError; + String get rpcUrl => throw _privateConstructorUsedError; Map get requiredNamespaces => throw _privateConstructorUsedError; Map get optionalNamespaces => throw _privateConstructorUsedError; - String get rpcUrl => throw _privateConstructorUsedError; + String? get chainIcon => throw _privateConstructorUsedError; W3MBlockExplorer? get blockExplorer => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -43,11 +43,11 @@ abstract class $W3MChainInfoCopyWith<$Res> { {String chainName, String chainId, String namespace, - String? chainIcon, String tokenName, + String rpcUrl, Map requiredNamespaces, Map optionalNamespaces, - String rpcUrl, + String? chainIcon, W3MBlockExplorer? blockExplorer}); $W3MBlockExplorerCopyWith<$Res>? get blockExplorer; @@ -69,11 +69,11 @@ class _$W3MChainInfoCopyWithImpl<$Res, $Val extends W3MChainInfo> Object? chainName = null, Object? chainId = null, Object? namespace = null, - Object? chainIcon = freezed, Object? tokenName = null, + Object? rpcUrl = null, Object? requiredNamespaces = null, Object? optionalNamespaces = null, - Object? rpcUrl = null, + Object? chainIcon = freezed, Object? blockExplorer = freezed, }) { return _then(_value.copyWith( @@ -89,14 +89,14 @@ class _$W3MChainInfoCopyWithImpl<$Res, $Val extends W3MChainInfo> ? _value.namespace : namespace // ignore: cast_nullable_to_non_nullable as String, - chainIcon: freezed == chainIcon - ? _value.chainIcon - : chainIcon // ignore: cast_nullable_to_non_nullable - as String?, tokenName: null == tokenName ? _value.tokenName : tokenName // ignore: cast_nullable_to_non_nullable as String, + rpcUrl: null == rpcUrl + ? _value.rpcUrl + : rpcUrl // ignore: cast_nullable_to_non_nullable + as String, requiredNamespaces: null == requiredNamespaces ? _value.requiredNamespaces : requiredNamespaces // ignore: cast_nullable_to_non_nullable @@ -105,10 +105,10 @@ class _$W3MChainInfoCopyWithImpl<$Res, $Val extends W3MChainInfo> ? _value.optionalNamespaces : optionalNamespaces // ignore: cast_nullable_to_non_nullable as Map, - rpcUrl: null == rpcUrl - ? _value.rpcUrl - : rpcUrl // ignore: cast_nullable_to_non_nullable - as String, + chainIcon: freezed == chainIcon + ? _value.chainIcon + : chainIcon // ignore: cast_nullable_to_non_nullable + as String?, blockExplorer: freezed == blockExplorer ? _value.blockExplorer : blockExplorer // ignore: cast_nullable_to_non_nullable @@ -141,11 +141,11 @@ abstract class _$$_W3MChainInfoCopyWith<$Res> {String chainName, String chainId, String namespace, - String? chainIcon, String tokenName, + String rpcUrl, Map requiredNamespaces, Map optionalNamespaces, - String rpcUrl, + String? chainIcon, W3MBlockExplorer? blockExplorer}); @override @@ -166,11 +166,11 @@ class __$$_W3MChainInfoCopyWithImpl<$Res> Object? chainName = null, Object? chainId = null, Object? namespace = null, - Object? chainIcon = freezed, Object? tokenName = null, + Object? rpcUrl = null, Object? requiredNamespaces = null, Object? optionalNamespaces = null, - Object? rpcUrl = null, + Object? chainIcon = freezed, Object? blockExplorer = freezed, }) { return _then(_$_W3MChainInfo( @@ -186,14 +186,14 @@ class __$$_W3MChainInfoCopyWithImpl<$Res> ? _value.namespace : namespace // ignore: cast_nullable_to_non_nullable as String, - chainIcon: freezed == chainIcon - ? _value.chainIcon - : chainIcon // ignore: cast_nullable_to_non_nullable - as String?, tokenName: null == tokenName ? _value.tokenName : tokenName // ignore: cast_nullable_to_non_nullable as String, + rpcUrl: null == rpcUrl + ? _value.rpcUrl + : rpcUrl // ignore: cast_nullable_to_non_nullable + as String, requiredNamespaces: null == requiredNamespaces ? _value._requiredNamespaces : requiredNamespaces // ignore: cast_nullable_to_non_nullable @@ -202,10 +202,10 @@ class __$$_W3MChainInfoCopyWithImpl<$Res> ? _value._optionalNamespaces : optionalNamespaces // ignore: cast_nullable_to_non_nullable as Map, - rpcUrl: null == rpcUrl - ? _value.rpcUrl - : rpcUrl // ignore: cast_nullable_to_non_nullable - as String, + chainIcon: freezed == chainIcon + ? _value.chainIcon + : chainIcon // ignore: cast_nullable_to_non_nullable + as String?, blockExplorer: freezed == blockExplorer ? _value.blockExplorer : blockExplorer // ignore: cast_nullable_to_non_nullable @@ -221,11 +221,11 @@ class _$_W3MChainInfo implements _W3MChainInfo { {required this.chainName, required this.chainId, required this.namespace, - this.chainIcon, required this.tokenName, - required final Map requiredNamespaces, - required final Map optionalNamespaces, required this.rpcUrl, + final Map requiredNamespaces = const {}, + final Map optionalNamespaces = const {}, + this.chainIcon, this.blockExplorer}) : _requiredNamespaces = requiredNamespaces, _optionalNamespaces = optionalNamespaces; @@ -237,11 +237,12 @@ class _$_W3MChainInfo implements _W3MChainInfo { @override final String namespace; @override - final String? chainIcon; - @override final String tokenName; + @override + final String rpcUrl; final Map _requiredNamespaces; @override + @JsonKey() Map get requiredNamespaces { if (_requiredNamespaces is EqualUnmodifiableMapView) return _requiredNamespaces; @@ -251,6 +252,7 @@ class _$_W3MChainInfo implements _W3MChainInfo { final Map _optionalNamespaces; @override + @JsonKey() Map get optionalNamespaces { if (_optionalNamespaces is EqualUnmodifiableMapView) return _optionalNamespaces; @@ -259,13 +261,13 @@ class _$_W3MChainInfo implements _W3MChainInfo { } @override - final String rpcUrl; + final String? chainIcon; @override final W3MBlockExplorer? blockExplorer; @override String toString() { - return 'W3MChainInfo(chainName: $chainName, chainId: $chainId, namespace: $namespace, chainIcon: $chainIcon, tokenName: $tokenName, requiredNamespaces: $requiredNamespaces, optionalNamespaces: $optionalNamespaces, rpcUrl: $rpcUrl, blockExplorer: $blockExplorer)'; + return 'W3MChainInfo(chainName: $chainName, chainId: $chainId, namespace: $namespace, tokenName: $tokenName, rpcUrl: $rpcUrl, requiredNamespaces: $requiredNamespaces, optionalNamespaces: $optionalNamespaces, chainIcon: $chainIcon, blockExplorer: $blockExplorer)'; } @override @@ -278,15 +280,15 @@ class _$_W3MChainInfo implements _W3MChainInfo { (identical(other.chainId, chainId) || other.chainId == chainId) && (identical(other.namespace, namespace) || other.namespace == namespace) && - (identical(other.chainIcon, chainIcon) || - other.chainIcon == chainIcon) && (identical(other.tokenName, tokenName) || other.tokenName == tokenName) && + (identical(other.rpcUrl, rpcUrl) || other.rpcUrl == rpcUrl) && const DeepCollectionEquality() .equals(other._requiredNamespaces, _requiredNamespaces) && const DeepCollectionEquality() .equals(other._optionalNamespaces, _optionalNamespaces) && - (identical(other.rpcUrl, rpcUrl) || other.rpcUrl == rpcUrl) && + (identical(other.chainIcon, chainIcon) || + other.chainIcon == chainIcon) && (identical(other.blockExplorer, blockExplorer) || other.blockExplorer == blockExplorer)); } @@ -297,11 +299,11 @@ class _$_W3MChainInfo implements _W3MChainInfo { chainName, chainId, namespace, - chainIcon, tokenName, + rpcUrl, const DeepCollectionEquality().hash(_requiredNamespaces), const DeepCollectionEquality().hash(_optionalNamespaces), - rpcUrl, + chainIcon, blockExplorer); @JsonKey(ignore: true) @@ -316,11 +318,11 @@ abstract class _W3MChainInfo implements W3MChainInfo { {required final String chainName, required final String chainId, required final String namespace, - final String? chainIcon, required final String tokenName, - required final Map requiredNamespaces, - required final Map optionalNamespaces, required final String rpcUrl, + final Map requiredNamespaces, + final Map optionalNamespaces, + final String? chainIcon, final W3MBlockExplorer? blockExplorer}) = _$_W3MChainInfo; @override @@ -330,15 +332,15 @@ abstract class _W3MChainInfo implements W3MChainInfo { @override String get namespace; @override - String? get chainIcon; - @override String get tokenName; @override + String get rpcUrl; + @override Map get requiredNamespaces; @override Map get optionalNamespaces; @override - String get rpcUrl; + String? get chainIcon; @override W3MBlockExplorer? get blockExplorer; @override diff --git a/lib/pages/connect_wallet_page.dart b/lib/pages/connect_wallet_page.dart index b5a6c11a..55741044 100644 --- a/lib/pages/connect_wallet_page.dart +++ b/lib/pages/connect_wallet_page.dart @@ -137,23 +137,21 @@ class _ConnectWalletPageState extends State visible: errorConnection, child: Container( decoration: BoxDecoration( + color: themeColors.background125, borderRadius: BorderRadius.all( Radius.circular(30.0), ), ), padding: const EdgeInsets.all(1.0), clipBehavior: Clip.antiAlias, - child: ColoredBox( - color: themeColors.background125, - child: RoundedIcon( - assetPath: 'assets/icons/close.svg', - assetColor: themeColors.error100, - circleColor: - themeColors.error100.withOpacity(0.2), - borderColor: themeColors.background125, - padding: 4.0, - size: 22.0, - ), + child: RoundedIcon( + assetPath: 'assets/icons/close.svg', + assetColor: themeColors.error100, + circleColor: + themeColors.error100.withOpacity(0.2), + borderColor: themeColors.background125, + padding: 4.0, + size: 24.0, ), ), ), @@ -212,7 +210,8 @@ class _ConnectWalletPageState extends State Visibility( visible: isPortrait && _selectedSegment != SegmentOption.browser && - !errorConnection, + !errorConnection && + walletInstalled, child: SimpleIconButton( onTap: () => service.connectSelectedWallet(), leftIcon: 'assets/icons/refresh_back.svg', @@ -248,7 +247,8 @@ class _ConnectWalletPageState extends State Visibility( visible: !isPortrait && _selectedSegment != SegmentOption.browser && - !errorConnection, + !errorConnection && + walletInstalled, child: SimpleIconButton( onTap: () => service.connectSelectedWallet(), leftIcon: 'assets/icons/refresh_back.svg', diff --git a/lib/pages/get_wallet_page.dart b/lib/pages/get_wallet_page.dart index 4a71ea5a..52b08481 100644 --- a/lib/pages/get_wallet_page.dart +++ b/lib/pages/get_wallet_page.dart @@ -33,7 +33,7 @@ class GetWalletPage extends StatelessWidget { body: ConstrainedBox( constraints: BoxConstraints(maxHeight: maxHeight), child: ExplorerServiceItemsListener( - builder: (context, initialised, items) { + builder: (context, initialised, items, _) { if (!initialised) { return const ContentLoading(); } diff --git a/lib/pages/qr_code_page.dart b/lib/pages/qr_code_page.dart index 7e849a5a..ea4596f5 100644 --- a/lib/pages/qr_code_page.dart +++ b/lib/pages/qr_code_page.dart @@ -31,25 +31,34 @@ class _QRCodePageState extends State { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { _service = Web3ModalProvider.of(context).service; - _service!.addListener(_rebuildListener); + _service!.addListener(_buildWidget); _service!.onPairingExpire.subscribe(_onPairingExpire); + _service?.onWalletConnectionError.subscribe(_onError); await _service!.buildConnectionUri(); - _qrQodeWidget = QRCodeWidget( - uri: _service!.wcUri!, - logoPath: 'assets/png/logo_wc.png', - ); }); } - void _rebuildListener() => setState(() {}); + void _buildWidget() => setState(() { + _qrQodeWidget = QRCodeWidget( + uri: _service!.wcUri!, + logoPath: 'assets/png/logo_wc.png', + ); + }); + void _onPairingExpire(EventArgs? args) async { await _service!.buildConnectionUri(); + setState(() {}); + } + + void _onError(EventArgs? args) { + _showUserRejection(); } @override void dispose() async { + _service?.onWalletConnectionError.unsubscribe(_onError); _service!.onPairingExpire.unsubscribe(_onPairingExpire); - _service!.removeListener(_rebuildListener); + _service!.removeListener(_buildWidget); _service!.expirePreviousInactivePairings(); super.dispose(); } @@ -58,6 +67,7 @@ class _QRCodePageState extends State { Widget build(BuildContext context) { final themeData = Web3ModalTheme.getDataOf(context); final themeColors = Web3ModalTheme.colorsOf(context); + final radiuses = Web3ModalTheme.radiusesOf(context); final isPortrait = ResponsiveData.isPortrait(context); return Web3ModalNavbar( @@ -69,7 +79,16 @@ class _QRCodePageState extends State { children: [ Padding( padding: EdgeInsets.all(kPadding16), - child: _qrQodeWidget ?? SizedBox.shrink(), + child: _qrQodeWidget ?? + AspectRatio( + aspectRatio: 1.0, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radiuses.radiusL), + color: themeColors.grayGlass005, + ), + ), + ), ), Container( constraints: BoxConstraints( @@ -122,4 +141,8 @@ class _QRCodePageState extends State { ToastMessage(type: ToastType.success, text: 'Link copied'), ); } + + void _showUserRejection() => toastUtils.instance.show( + ToastMessage(type: ToastType.error, text: 'User rejected'), + ); } diff --git a/lib/pages/select_network_page.dart b/lib/pages/select_network_page.dart index 44320b07..64517e1c 100644 --- a/lib/pages/select_network_page.dart +++ b/lib/pages/select_network_page.dart @@ -42,7 +42,6 @@ class SelectNetworkPage extends StatelessWidget { child: NetworkServiceItemsListener( builder: (context, initialised, items) { if (!initialised) { - // TODO replace with LoadingItems return const ContentLoading(); } return NetworksGrid( diff --git a/lib/pages/wallets_list_long_page.dart b/lib/pages/wallets_list_long_page.dart index 5b7592be..180503f3 100644 --- a/lib/pages/wallets_list_long_page.dart +++ b/lib/pages/wallets_list_long_page.dart @@ -1,8 +1,11 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:web3modal_flutter/constants/key_constants.dart'; import 'package:web3modal_flutter/pages/connect_wallet_page.dart'; import 'package:web3modal_flutter/services/explorer_service/explorer_service_singleton.dart'; +import 'package:web3modal_flutter/theme/constants.dart'; import 'package:web3modal_flutter/widgets/widget_stack/widget_stack_singleton.dart'; import 'package:web3modal_flutter/widgets/miscellaneous/responsive_container.dart'; import 'package:web3modal_flutter/widgets/web3modal_provider.dart'; @@ -10,7 +13,6 @@ import 'package:web3modal_flutter/widgets/lists/wallets_grid.dart'; import 'package:web3modal_flutter/widgets/value_listenable_builders/explorer_service_items_listener.dart'; import 'package:web3modal_flutter/widgets/miscellaneous/all_wallets_header.dart'; import 'package:web3modal_flutter/widgets/navigation/navbar.dart'; -import 'package:web3modal_flutter/widgets/miscellaneous/content_loading.dart'; class WalletsListLongPage extends StatefulWidget { const WalletsListLongPage() @@ -51,13 +53,19 @@ class _WalletsListLongPageState extends State { } Future _paginate() { - setState(() => _paginating = true); + setState(() => _paginating = explorerService.instance!.canPaginate); return explorerService.instance!.paginate(); } @override Widget build(BuildContext context) { final service = Web3ModalProvider.of(context).service; + final totalListings = explorerService.instance!.totalListings.value; + final rows = (totalListings / 4.0).ceil(); + final maxHeight = (rows * kGridItemHeight) + + (kPadding16 * 2.0) + + ResponsiveData.paddingBottomOf(context); + final isSearchAvailable = totalListings >= 20; return Web3ModalNavbar( title: 'All wallets', onTapTitle: () => _controller.animateTo( @@ -75,23 +83,34 @@ class _WalletsListLongPageState extends State { safeAreaRight: true, body: ConstrainedBox( constraints: BoxConstraints( - maxHeight: ResponsiveData.maxHeightOf(context), + maxHeight: !isSearchAvailable + ? maxHeight + : ResponsiveData.maxHeightOf(context), ), child: Column( children: [ - const AllWalletsHeader(), + isSearchAvailable ? const AllWalletsHeader() : SizedBox.shrink(), Expanded( child: NotificationListener( onNotification: _processScrollNotification, child: ExplorerServiceItemsListener( listen: !_paginating, - builder: (context, initialised, items) { - if (!initialised) { - // TODO replace with LoadingItems - return const ContentLoading(); + builder: (context, initialised, items, searching) { + if (!initialised || searching) { + return WalletsGrid( + paddingTop: isSearchAvailable ? 0.0 : kPadding16, + showLoading: true, + loadingCount: + items.isNotEmpty ? min(16, items.length) : 16, + scrollController: _controller, + itemList: [], + ); } + final isPortrait = ResponsiveData.isPortrait(context); return WalletsGrid( - isPaginating: _paginating, + paddingTop: isSearchAvailable ? 0.0 : kPadding16, + showLoading: _paginating, + loadingCount: isPortrait ? 4 : 8, scrollController: _controller, onTapWallet: (data) async { service.selectWallet(data); diff --git a/lib/pages/wallets_list_short_page.dart b/lib/pages/wallets_list_short_page.dart index 1338ef0c..c042171b 100644 --- a/lib/pages/wallets_list_short_page.dart +++ b/lib/pages/wallets_list_short_page.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:web3modal_flutter/pages/about_wallets.dart'; @@ -25,17 +27,11 @@ class WalletsListShortPage extends StatefulWidget { } class _WalletsListShortPageState extends State { - @override - void initState() { - super.initState(); - explorerService.instance!.fetchInitialWallets(); - } - @override Widget build(BuildContext context) { final service = Web3ModalProvider.of(context).service; final isPortrait = ResponsiveData.isPortrait(context); - final maxHeight = isPortrait + double maxHeight = isPortrait ? (kListItemHeight * 6) : ResponsiveData.maxHeightOf(context); return Web3ModalNavbar( @@ -48,34 +44,47 @@ class _WalletsListShortPageState extends State { ), safeAreaLeft: true, safeAreaRight: true, - body: ConstrainedBox( - constraints: BoxConstraints(maxHeight: maxHeight), - child: ExplorerServiceItemsListener( - builder: (context, initialised, items) { - if (!initialised || items.isEmpty) { - return const WalletsList(isLoading: true, itemList: []); - } - final itemsToShow = items.getRange(0, kShortWalletListCount); - return WalletsList( + body: ExplorerServiceItemsListener( + builder: (context, initialised, items, _) { + if (!initialised || items.isEmpty) { + return ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxHeight), + child: const WalletsList( + isLoading: true, + itemList: [], + ), + ); + } + final itemsCount = min(kShortWalletListCount, items.length); + final itemsToShow = items.getRange(0, itemsCount); + if (itemsCount < kShortWalletListCount && isPortrait) { + maxHeight = kListItemHeight * (itemsCount + 1); + } + return ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxHeight), + child: WalletsList( onTapWallet: (data) { service.selectWallet(data); widgetStack.instance.push(const ConnectWalletPage()); }, itemList: itemsToShow.toList(), - lastItem: AllWalletsItem( - trailing: ValueListenableBuilder( - valueListenable: explorerService.instance!.totalListings, - builder: (context, value, _) { - return WalletItemChip(value: value.lazyCount); - }, - ), - onTap: () { - widgetStack.instance.push(const WalletsListLongPage()); - }, - ), - ); - }, - ), + lastItem: (itemsCount < kShortWalletListCount) + ? null + : AllWalletsItem( + trailing: ValueListenableBuilder( + valueListenable: + explorerService.instance!.totalListings, + builder: (context, value, _) { + return WalletItemChip(value: value.lazyCount); + }, + ), + onTap: () { + widgetStack.instance.push(const WalletsListLongPage()); + }, + ), + ), + ); + }, ), ); } diff --git a/lib/services/explorer_service/explorer_service.dart b/lib/services/explorer_service/explorer_service.dart index caece577..94aeece0 100644 --- a/lib/services/explorer_service/explorer_service.dart +++ b/lib/services/explorer_service/explorer_service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:math'; @@ -6,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:universal_io/io.dart'; import 'package:web3modal_flutter/services/explorer_service/models/redirect.dart'; +import 'package:web3modal_flutter/utils/debouncer.dart'; import 'package:web3modal_flutter/utils/url/url_utils_singleton.dart'; import 'package:web3modal_flutter/constants/string_constants.dart'; import 'package:web3modal_flutter/models/w3m_wallet_info.dart'; @@ -27,6 +29,9 @@ class ExplorerService implements IExplorerService { late RequestParams _requestParams; + @override + ValueNotifier initialized = ValueNotifier(false); + String _recentWalletId = ''; @override String get recentWalletId => _recentWalletId; @@ -37,12 +42,16 @@ class ExplorerService implements IExplorerService { @override ValueNotifier totalListings = ValueNotifier(0); + List _listings = []; + @override ValueNotifier> listings = ValueNotifier([]); - List _listings = []; Set _installedWalletIds = {}; + @override + ValueNotifier isSearching = ValueNotifier(false); + @override Set? featuredWalletIds; @@ -50,8 +59,7 @@ class ExplorerService implements IExplorerService { Set? includedWalletIds; String? get _includedWalletsParam { - final includedIds = includedWalletIds = (includedWalletIds ?? {}) - ..removeAll(_installedWalletIds); + final includedIds = (includedWalletIds ?? {}); return includedIds.isNotEmpty ? includedIds.join(',') : null; } @@ -64,8 +72,11 @@ class ExplorerService implements IExplorerService { return excludedIds.isNotEmpty ? excludedIds.join(',') : null; } + int _prevCount = 0; + + bool _canPaginate = true; @override - ValueNotifier initialized = ValueNotifier(false); + bool get canPaginate => _canPaginate; ExplorerService({ required this.projectId, @@ -85,6 +96,7 @@ class ExplorerService implements IExplorerService { W3MLoggerUtil.logger.t('[$runtimeType] init()'); await _getInstalledWalletIds(); + await _fetchInitialWallets(); initialized.value = true; W3MLoggerUtil.logger.t('[$runtimeType] init() done'); @@ -95,14 +107,7 @@ class ExplorerService implements IExplorerService { _installedWalletIds = Set.from(installed.map((e) => e.id)); } - Future _getRecentWalletAndOrder() async { - final recentWalletId = - storageService.instance.getString(StringConstants.recentWallet); - await updateRecentPosition(recentWalletId); - } - - @override - Future fetchInitialWallets() async { + Future _fetchInitialWallets() async { totalListings.value = 0; final allListings = await Future.wait([ _fetchInstalledListings(), @@ -116,12 +121,16 @@ class ExplorerService implements IExplorerService { await _getRecentWalletAndOrder(); } + Future _getRecentWalletAndOrder() async { + final recentWalletId = + storageService.instance.getString(StringConstants.recentWallet); + await updateRecentPosition(recentWalletId); + } + @override Future paginate() async { + if (!canPaginate) return; final newParams = _requestParams.nextPage(); - final totalCount = totalListings.value; - if (newParams.page * newParams.entries > totalCount) return; - int entries = _defaultEntriesCount - _listings.length; if (entries <= 0) { _requestParams = newParams.copyWith(entries: _defaultEntriesCount); @@ -133,42 +142,17 @@ class ExplorerService implements IExplorerService { exclude: exclude, ); } - W3MLoggerUtil.logger - .t('[$runtimeType] paginate ${_requestParams.toJson()}'); final newListings = await _fetchListings( params: _requestParams, updateCount: false, ); - _listings = [..._listings, ...newListings]; listings.value = _listings; - } - - @override - String getWalletImageUrl(String imageId) => - '$_apiUrl/getWalletImage/$imageId'; - - @override - String getAssetImageUrl(String imageId) { - if (imageId.contains('http')) { - return imageId; - } - return '$_apiUrl/public/getAssetImage/$imageId'; - } - - @override - WalletRedirect? getWalletRedirectByName(Listing listing) { - final wallet = listings.value.firstWhereOrNull( - (item) => listing.id == item.listing.id, - ); - if (wallet == null) { - return null; + if (newListings.length < _prevCount) { + _canPaginate = false; + } else { + _prevCount = newListings.length; } - return WalletRedirect( - mobile: wallet.listing.mobileLink, - desktop: wallet.listing.desktopLink, - web: wallet.listing.webappLink, - ); } Future> _fetchNativeAppData() async { @@ -216,7 +200,7 @@ class ExplorerService implements IExplorerService { bool firstCall = false, }) async { final installedCount = _installedWalletIds.length; - final includedCount = _includedWalletsParam?.length ?? 0; + final includedCount = includedWalletIds?.length ?? 0; final toQuery = 20 - installedCount + includedCount; final entriesCount = firstCall ? max(toQuery, 16) : _defaultEntriesCount; _requestParams = RequestParams( @@ -295,29 +279,62 @@ class ExplorerService implements IExplorerService { } final q = query.toLowerCase(); - final filtered = _listings.where((w) { - final name = w.listing.name.toLowerCase(); - return name.contains(q); - }).toList(); - listings.value = filtered; - - W3MLoggerUtil.logger.t('[$runtimeType] search $q'); await _searchListings(query: q); } Future _searchListings({String? query}) async { - final exclude = _listings.map((e) => e.listing.id).toList().join(','); + isSearching.value = true; + + final includedIds = (includedWalletIds ?? {}); + final include = includedIds.isNotEmpty ? includedIds.join(',') : null; + final excludedIds = (excludedWalletIds ?? {}); + final exclude = excludedIds.isNotEmpty ? excludedIds.join(',') : null; + + W3MLoggerUtil.logger.t('[$runtimeType] search $query'); + final newListins = await _fetchListings( - params: _requestParams.copyWith( + params: RequestParams( page: 1, + entries: 100, search: query, + include: include, exclude: exclude, ), updateCount: false, ); - listings.value = [...listings.value, ...newListins]; + listings.value = newListins; W3MLoggerUtil.logger.t('[$runtimeType] _searchListings $query'); + _debouncer.run(() => isSearching.value = false); + } + + final _debouncer = Debouncer(milliseconds: 300); + + @override + String getWalletImageUrl(String imageId) => + '$_apiUrl/getWalletImage/$imageId'; + + @override + String getAssetImageUrl(String imageId) { + if (imageId.contains('http')) { + return imageId; + } + return '$_apiUrl/public/getAssetImage/$imageId'; + } + + @override + WalletRedirect? getWalletRedirect(Listing listing) { + final wallet = listings.value.firstWhereOrNull( + (item) => listing.id == item.listing.id, + ); + if (wallet == null) { + return null; + } + return WalletRedirect( + mobile: wallet.listing.mobileLink, + desktop: wallet.listing.desktopLink, + web: wallet.listing.webappLink, + ); } String _getPlatformType() { diff --git a/lib/services/explorer_service/i_explorer_service.dart b/lib/services/explorer_service/i_explorer_service.dart index de2c250e..abfdc130 100644 --- a/lib/services/explorer_service/i_explorer_service.dart +++ b/lib/services/explorer_service/i_explorer_service.dart @@ -28,15 +28,16 @@ abstract class IExplorerService { /// Init service Future init(); - /// fetch initial page of wallets including the installed ones - Future fetchInitialWallets(); - /// paginate subsequent wallets Future paginate(); + bool get canPaginate; + /// search for a wallet void search({String? query}); + ValueNotifier isSearching = ValueNotifier(false); + /// update the recently used position to the top list Future updateRecentPosition(String? recentId); @@ -44,5 +45,5 @@ abstract class IExplorerService { String getAssetImageUrl(String imageId); - WalletRedirect? getWalletRedirectByName(Listing listing); + WalletRedirect? getWalletRedirect(Listing listing); } diff --git a/lib/services/explorer_service/models/api_response.dart b/lib/services/explorer_service/models/api_response.dart index b063330f..d551ad9a 100644 --- a/lib/services/explorer_service/models/api_response.dart +++ b/lib/services/explorer_service/models/api_response.dart @@ -214,7 +214,7 @@ class RequestParams { this.platform, }); - Map toJson() { + Map toJson({bool short = false}) { Map params = { 'page': page.toString(), 'entries': entries.toString(), @@ -222,10 +222,10 @@ class RequestParams { if ((search ?? '').isNotEmpty) { params['search'] = search; } - if ((include ?? '').isNotEmpty) { + if ((include ?? '').isNotEmpty && !short) { params['include'] = include; } - if ((exclude ?? '').isNotEmpty) { + if ((exclude ?? '').isNotEmpty && !short) { params['exclude'] = exclude; } if ((platform ?? '').isNotEmpty) { diff --git a/lib/services/w3m_service/i_w3m_service.dart b/lib/services/w3m_service/i_w3m_service.dart index 8fa62b84..101820ef 100644 --- a/lib/services/w3m_service/i_w3m_service.dart +++ b/lib/services/w3m_service/i_w3m_service.dart @@ -1,6 +1,5 @@ import 'package:event/event.dart'; import 'package:flutter/material.dart'; -import 'package:walletconnect_flutter_v2/apis/sign_api/models/proposal_models.dart'; import 'package:walletconnect_flutter_v2/apis/sign_api/models/session_models.dart'; import 'package:walletconnect_flutter_v2/apis/web3app/i_web3app.dart'; import 'package:web3modal_flutter/models/w3m_wallet_info.dart'; @@ -17,6 +16,8 @@ enum W3MServiceStatus { bool get isLoading => this == initializing; } +/// Either a [projectId] and [metadata] must be provided or an already created [web3App]. +/// optionalNamespaces is mostly not needed, if you use it, the values set here will override every optionalNamespaces set in evey chain abstract class IW3MService with ChangeNotifier { /// Whether or not this object has been initialized. W3MServiceStatus get status; @@ -25,11 +26,7 @@ abstract class IW3MService with ChangeNotifier { /// Otherwise, it will be null. dynamic get initError; - /// The required namespaces that will be used when connecting to the wallet - Map get requiredNamespaces; - - /// The optional namespaces that will be used when connecting to the wallet - Map get optionalNamespaces; + bool get hasNamespaces; /// The object that manages sessions, authentication, events, and requests for WalletConnect. IWeb3App? get web3App; diff --git a/lib/services/w3m_service/w3m_service.dart b/lib/services/w3m_service/w3m_service.dart index 709b1c54..604d6633 100644 --- a/lib/services/w3m_service/w3m_service.dart +++ b/lib/services/w3m_service/w3m_service.dart @@ -5,8 +5,6 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; - -import 'package:web3modal_flutter/constants/namespaces.dart'; import 'package:web3modal_flutter/constants/string_constants.dart'; import 'package:web3modal_flutter/models/w3m_wallet_info.dart'; import 'package:web3modal_flutter/services/explorer_service/explorer_service.dart'; @@ -41,6 +39,8 @@ class W3MServiceException implements Exception { W3MServiceException(this.message, [this.stackTrace]) : super(); } +/// Either a [projectId] and [metadata] must be provided or an already created [web3App]. +/// optionalNamespaces is mostly not needed, if you use it, the values set here will override every optionalNamespaces set in evey chain class W3MService with ChangeNotifier implements IW3MService { var _projectId = ''; @@ -59,13 +59,11 @@ class W3MService with ChangeNotifier implements IW3MService { W3MWalletInfo? get selectedWallet => _selectedWallet; Map _requiredNamespaces = {}; - @override - Map get requiredNamespaces => _requiredNamespaces; + Map _optionalNamespaces = {}; - Map _optionalNamespaces = - NamespaceConstants.ethereum; @override - Map get optionalNamespaces => _optionalNamespaces; + bool get hasNamespaces => + _requiredNamespaces.isNotEmpty || _optionalNamespaces.isNotEmpty; ConnectResponse? connectResponse; Future? get sessionFuture => connectResponse?.session.future; @@ -120,14 +118,13 @@ class W3MService with ChangeNotifier implements IW3MService { IWeb3App? web3App, String? projectId, PairingMetadata? metadata, - Map? requiredNamespaces, - Map? optionalNamespaces, + Map? optionalNamespaces, Set? featuredWalletIds, Set? includedWalletIds, Set? excludedWalletIds, LogLevel logLevel = LogLevel.nothing, }) { - if (web3App == null && projectId == null && metadata == null) { + if (web3App == null && metadata == null) { throw ArgumentError( 'Either a projectId and metadata must be provided or an already created web3App.', ); @@ -140,11 +137,32 @@ class W3MService with ChangeNotifier implements IW3MService { ); _projectId = projectId ?? _web3App!.core.projectId; - if (requiredNamespaces != null) { - _requiredNamespaces = requiredNamespaces; - } if (optionalNamespaces != null) { - _optionalNamespaces = optionalNamespaces; + // Set the optional namespaces declared by the user on W3MService object + _optionalNamespaces = optionalNamespaces.map( + (key, value) => MapEntry( + key, + RequiredNamespace( + chains: value.chains ?? + W3MChainPresets.chains.values.map((e) { + return e.namespace; + }).toList(), + methods: value.methods, + events: value.events, + ), + ), + ); + } else { + // Set the optional namespaces to everything in our chain presets + _optionalNamespaces = { + 'eip155': RequiredNamespace( + methods: EthConstants.optionalMethods, + chains: W3MChainPresets.chains.values.map((e) { + return e.namespace; + }).toList(), + events: EthConstants.optionalEvents, + ), + }; } explorerService.instance = ExplorerService( @@ -204,19 +222,9 @@ class W3MService with ChangeNotifier implements IW3MService { } } - // Set the optional namespaces to everything in our asset util. - final List chainIds = []; - for (final chain in W3MChainPresets.chains.values) { - chainIds.add(chain.namespace); + if (_optionalNamespaces.isNotEmpty) { + _setOptionalNamespaces(_optionalNamespaces); } - final Map optionalNamespaces = { - 'eip155': RequiredNamespace( - methods: EthConstants.ethMethods, - chains: chainIds, - events: EthConstants.ethEvents, - ), - }; - _setOptionalNamespaces(optionalNamespaces); // Get the chainId of the chain we are connected to. await _selectChainFromStoredId(); @@ -256,7 +264,7 @@ class W3MService with ChangeNotifier implements IW3MService { namespaces: _currentSession!.namespaces, ); if (chainIds.isNotEmpty) { - final String chainId = chainIds.first.split(':')[1]; + final chainId = chainIds.first.split(':')[1]; // If we have the chain in our presets, set it as the selected chain if (W3MChainPresets.chains.containsKey(chainId)) { await selectChain(W3MChainPresets.chains[chainId]!); @@ -299,8 +307,6 @@ class W3MService with ChangeNotifier implements IW3MService { // If the chain is null, disconnect and stop. if (chainInfo == null) { - _currentSelectedChain = null; - _setRequiredNamespaces({}); await disconnect(); return; } @@ -345,6 +351,8 @@ class W3MService with ChangeNotifier implements IW3MService { } void _setEthChain(W3MChainInfo chainInfo) async { + final isSwitch = _currentSelectedChain?.chainId != chainInfo.chainId; + _currentSelectedChain = chainInfo; // Get the token/chain icon _tokenImageUrl = _getTokenImage(chainInfo); @@ -356,13 +364,17 @@ class W3MService with ChangeNotifier implements IW3MService { _currentSelectedChain!.chainId, ); - // Set the requiredNamespace to be the selected chain - // This will also notify listeners _setRequiredNamespaces(_currentSelectedChain!.requiredNamespaces); - _notify(); + if (_optionalNamespaces.isEmpty) { + _setOptionalNamespaces(_currentSelectedChain!.optionalNamespaces); + } W3MLoggerUtil.logger.t('[$runtimeType] setSelectedChain success'); _loadAccountData(); + + if (isSwitch) { + debugPrint('Chain Set'); + } } String _getTokenImage(W3MChainInfo chainInfo) { @@ -384,7 +396,7 @@ class W3MService with ChangeNotifier implements IW3MService { _isOpen = true; // Reset the explorer - explorerService.instance!.search(query: ''); + explorerService.instance!.search(query: null); widgetStack.instance.clear(); _context = context; @@ -495,8 +507,8 @@ class W3MService with ChangeNotifier implements IW3MService { if (!_isConnected) { W3MLoggerUtil.logger.t( '[$runtimeType] Connecting to WalletConnect, ' - 'required namespaces: $requiredNamespaces, ' - 'optional namespaces: $optionalNamespaces', + 'required namespaces: $_requiredNamespaces, ' + 'optional namespaces: $_optionalNamespaces', ); if (connectResponse != null) { @@ -508,8 +520,8 @@ class W3MService with ChangeNotifier implements IW3MService { } connectResponse = await _web3App!.connect( - requiredNamespaces: requiredNamespaces, - optionalNamespaces: optionalNamespaces, + requiredNamespaces: _requiredNamespaces, + optionalNamespaces: _optionalNamespaces, ); _notify(); @@ -542,10 +554,10 @@ class W3MService with ChangeNotifier implements IW3MService { .i('[$runtimeType] Rebuilding session, ending future'); return; } on JsonRpcError catch (e) { + W3MLoggerUtil.logger.e('[$runtimeType] Error connecting to wallet: $e'); if (_isUserRejectedError(e)) { onWalletConnectionError.broadcast(); } - W3MLoggerUtil.logger.e('[$runtimeType] Error connecting to wallet: $e'); return await expirePreviousInactivePairings(); } } @@ -773,7 +785,8 @@ class W3MService with ChangeNotifier implements IW3MService { if (e is JsonRpcError) { final stringError = e.toJson().toString().toLowerCase(); final userRejected = stringError.contains('rejected'); - return userRejected; + final userDisapproved = stringError.contains('user disapproved'); + return userRejected || userDisapproved; } return false; } @@ -790,14 +803,12 @@ class W3MService with ChangeNotifier implements IW3MService { reason: const WalletConnectError(code: 0, message: 'User disconnected'), ); - // As a failsafe (If the session is expired for example), set the session to null and notify listeners - if (_currentSession != null && - _currentSession!.topic == toDisconnect.topic) { - return _cleanSession(); - } + return _cleanSession(); } void _cleanSession() { + _currentSelectedChain = null; + _setRequiredNamespaces({}); _isConnected = false; _address = ''; _currentSession = null; @@ -809,7 +820,7 @@ class W3MService with ChangeNotifier implements IW3MService { final listing = _selectedWallet?.listing; if (listing == null) return null; - return explorerService.instance?.getWalletRedirectByName(listing); + return explorerService.instance?.getWalletRedirect(listing); } WalletRedirect? get sessionWalletRedirect { diff --git a/lib/theme/w3m_colors.dart b/lib/theme/w3m_colors.dart index 93058477..87329ea5 100644 --- a/lib/theme/w3m_colors.dart +++ b/lib/theme/w3m_colors.dart @@ -101,7 +101,7 @@ class Web3ModalColors with _$Web3ModalColors { foreground300: Color(0xFF9EA9A9), // background100: Color(0xFFFFFFFF), - background125: Color(0xFFF5FAFA), + background125: Color(0xFFFFFFFF), background150: Color(0xFFF3F8F8), background175: Color(0xFFEEF4F4), background200: Color(0xFFEAF1F1), diff --git a/lib/utils/debouncer.dart b/lib/utils/debouncer.dart new file mode 100644 index 00000000..c5bcdada --- /dev/null +++ b/lib/utils/debouncer.dart @@ -0,0 +1,20 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; + +class Debouncer { + final int milliseconds; + Timer? _timer; + + Debouncer({required this.milliseconds}); + + void run(VoidCallback action) { + if (_timer?.isActive == true) { + _timer?.cancel(); + _timer = null; + } + _timer = Timer( + Duration(milliseconds: milliseconds), + action, + ); + } +} diff --git a/lib/utils/url/url_utils.dart b/lib/utils/url/url_utils.dart index cc195b91..d7b74650 100644 --- a/lib/utils/url/url_utils.dart +++ b/lib/utils/url/url_utils.dart @@ -1,4 +1,5 @@ import 'package:appcheck/appcheck.dart'; +import 'package:flutter/foundation.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:web3modal_flutter/services/explorer_service/models/redirect.dart'; import 'package:web3modal_flutter/utils/core/core_utils_singleton.dart'; @@ -58,14 +59,10 @@ class UrlUtils extends IUrlUtils { if (p == PlatformExact.android) { return await androidAppCheck(uri); } else if (p == PlatformExact.iOS) { - return await canLaunchUrlFunc( - Uri.parse( - uri, - ), - ); + return await canLaunchUrlFunc(Uri.parse(uri)); } - } catch (_) { - // print(e); + } catch (e) { + debugPrint('[$runtimeType] isInstalled $e'); } } @@ -115,12 +112,9 @@ class UrlUtils extends IUrlUtils { ) : redirect.desktopUri; } - if (await canLaunchUrl(uriToOpen!)) { - await launchUrlFunc( - uriToOpen, - mode: LaunchMode.externalApplication, - ); - } else { + try { + await launchUrlFunc(uriToOpen!, mode: LaunchMode.externalApplication); + } catch (_) { throw LaunchUrlException('App not installed'); } } catch (e) { diff --git a/lib/utils/util.dart b/lib/utils/util.dart index c2122a1f..3741abe9 100644 --- a/lib/utils/util.dart +++ b/lib/utils/util.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; class Util { - static String shorten( - String value, { - bool short = false, - }) { + static String shorten(String value, {bool short = false}) { return short && value.length > 8 ? '${value.substring(0, 8)}..' : value; } @@ -71,4 +69,17 @@ class Util { return [tintedR, tintedG, tintedB]; } + + static Set getChainsFromNamespace(Map ns) { + return ns['eip155']?.chains?.toSet() ?? {}; + } + + static Set getMethodsFromNamespace( + Map ns) { + return ns['eip155']?.methods.toSet() ?? {}; + } + + static Set getEventsFromNamespace(Map ns) { + return ns['eip155']?.events.toSet() ?? {}; + } } diff --git a/lib/utils/w3m_chains_presets.dart b/lib/utils/w3m_chains_presets.dart index a2c0f599..954f2ad0 100644 --- a/lib/utils/w3m_chains_presets.dart +++ b/lib/utils/w3m_chains_presets.dart @@ -11,18 +11,11 @@ class W3MChainPresets { chainId: '1', chainIcon: chainImagesId['1'], tokenName: 'ETH', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:1'], - events: EthConstants.ethEvents, - ), - }, optionalNamespaces: { 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, + methods: EthConstants.allMethods, chains: ['eip155:1'], - events: [], + events: EthConstants.allEvents, ), }, rpcUrl: 'https://eth.drpc.org', @@ -37,18 +30,11 @@ class W3MChainPresets { chainId: '42161', chainIcon: chainImagesId['42161'], tokenName: 'ARB', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:42161'], - events: EthConstants.ethEvents, - ), - }, optionalNamespaces: { 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, + methods: EthConstants.allMethods, chains: ['eip155:42161'], - events: [], + events: EthConstants.allEvents, ), }, rpcUrl: 'https://arbitrum.blockpi.network/v1/rpc/public', @@ -63,18 +49,11 @@ class W3MChainPresets { chainId: '137', chainIcon: chainImagesId['137'], tokenName: 'MATIC', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:137'], - events: EthConstants.ethEvents, - ), - }, optionalNamespaces: { 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, + methods: EthConstants.allMethods, chains: ['eip155:137'], - events: [], + events: EthConstants.allEvents, ), }, rpcUrl: 'https://polygon.drpc.org', @@ -89,18 +68,11 @@ class W3MChainPresets { chainId: '43114', chainIcon: chainImagesId['43114'], tokenName: 'AVAX', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:43114'], - events: EthConstants.ethEvents, - ), - }, optionalNamespaces: { 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, + methods: EthConstants.allMethods, chains: ['eip155:43114'], - events: [], + events: EthConstants.allEvents, ), }, rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', @@ -115,18 +87,11 @@ class W3MChainPresets { chainId: '56', chainIcon: chainImagesId['56'], tokenName: 'BNB', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:56'], - events: EthConstants.ethEvents, - ), - }, optionalNamespaces: { 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, + methods: EthConstants.allMethods, chains: ['eip155:56'], - events: [], + events: EthConstants.allEvents, ), }, rpcUrl: 'https://bsc-dataseed.binance.org/', @@ -141,18 +106,11 @@ class W3MChainPresets { chainId: '10', chainIcon: chainImagesId['10'], tokenName: 'OP', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:10'], - events: EthConstants.ethEvents, - ), - }, optionalNamespaces: { 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, + methods: EthConstants.allMethods, chains: ['eip155:10'], - events: [], + events: EthConstants.allEvents, ), }, rpcUrl: 'https://mainnet.optimism.io/', @@ -163,18 +121,11 @@ class W3MChainPresets { chainId: '250', chainIcon: chainImagesId['250'], tokenName: 'FTM', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:250'], - events: EthConstants.ethEvents, - ), - }, optionalNamespaces: { 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, + methods: EthConstants.allMethods, chains: ['eip155:250'], - events: [], + events: EthConstants.allEvents, ), }, rpcUrl: 'https://rpc.ftm.tools/', @@ -189,18 +140,11 @@ class W3MChainPresets { chainId: '9001', chainIcon: chainImagesId['9001'], tokenName: 'EVMOS', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:9001'], - events: EthConstants.ethEvents, - ), - }, optionalNamespaces: { 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, + methods: EthConstants.allMethods, chains: ['eip155:9001'], - events: [], + events: EthConstants.allEvents, ), }, rpcUrl: 'https://evmos-evm.publicnode.com', @@ -211,18 +155,11 @@ class W3MChainPresets { chainId: '4689', chainIcon: chainImagesId['4689'], tokenName: 'IOTX', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:4689'], - events: EthConstants.ethEvents, - ), - }, optionalNamespaces: { 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, + methods: EthConstants.allMethods, chains: ['eip155:4689'], - events: [], + events: EthConstants.allEvents, ), }, rpcUrl: 'https://rpc.ankr.com/iotex', @@ -237,18 +174,11 @@ class W3MChainPresets { chainId: '1088', chainIcon: chainImagesId['1088'], tokenName: 'METIS', - requiredNamespaces: { - 'eip155': const RequiredNamespace( - methods: EthConstants.ethRequiredMethods, - chains: ['eip155:1088'], - events: EthConstants.ethEvents, - ), - }, optionalNamespaces: { 'eip155': const RequiredNamespace( - methods: EthConstants.ethOptionalMethods, + methods: EthConstants.allMethods, chains: ['eip155:1088'], - events: [], + events: EthConstants.allEvents, ), }, rpcUrl: 'https://metis-mainnet.public.blastapi.io', diff --git a/lib/widgets/avatars/w3m_account_avatar.dart b/lib/widgets/avatars/w3m_account_avatar.dart index 20602718..28def7fd 100644 --- a/lib/widgets/avatars/w3m_account_avatar.dart +++ b/lib/widgets/avatars/w3m_account_avatar.dart @@ -51,16 +51,8 @@ class _W3MAccountAvatarState extends State { BlendMode.saturation, ), child: (_avatarUrl ?? '').isNotEmpty - ? CachedNetworkImage( - imageUrl: _avatarUrl!, - fadeOutDuration: Duration.zero, - fadeInDuration: Duration.zero, - placeholderFadeInDuration: Duration.zero, - ) - : GradientOrb( - address: _address, - size: widget.size, - ), + ? CachedNetworkImage(imageUrl: _avatarUrl!) + : GradientOrb(address: _address, size: widget.size), ), ), ); diff --git a/lib/widgets/avatars/w3m_wallet_avatar.dart b/lib/widgets/avatars/w3m_wallet_avatar.dart index 22f2ff75..e549accf 100644 --- a/lib/widgets/avatars/w3m_wallet_avatar.dart +++ b/lib/widgets/avatars/w3m_wallet_avatar.dart @@ -51,9 +51,6 @@ class W3MListAvatar extends StatelessWidget { child: CachedNetworkImage( imageUrl: imageUrl!, httpHeaders: coreUtils.instance.getAPIHeaders(projectId), - fadeOutDuration: Duration.zero, - fadeInDuration: Duration.zero, - placeholderFadeInDuration: Duration.zero, errorWidget: (context, url, error) => ColoredBox( color: themeColors.grayGlass005, ), diff --git a/lib/widgets/icons/rounded_icon.dart b/lib/widgets/icons/rounded_icon.dart index 881be082..d2c520ec 100644 --- a/lib/widgets/icons/rounded_icon.dart +++ b/lib/widgets/icons/rounded_icon.dart @@ -48,9 +48,6 @@ class RoundedIcon extends StatelessWidget { height: size, fit: BoxFit.fill, httpHeaders: coreUtils.instance.getAPIHeaders(projectId), - fadeOutDuration: Duration.zero, - fadeInDuration: Duration.zero, - placeholderFadeInDuration: Duration.zero, errorWidget: (context, url, error) => ColoredBox( color: themeColors.grayGlass005, ), diff --git a/lib/widgets/lists/wallets_grid.dart b/lib/widgets/lists/wallets_grid.dart index 9f1b5693..29d6c637 100644 --- a/lib/widgets/lists/wallets_grid.dart +++ b/lib/widgets/lists/wallets_grid.dart @@ -14,13 +14,17 @@ class WalletsGrid extends StatelessWidget { super.key, required this.itemList, this.onTapWallet, - this.isPaginating = false, + this.showLoading = false, + this.loadingCount = 8, this.scrollController, + this.paddingTop = 0.0, }); final List> itemList; final Function(W3MWalletInfo walletInfo)? onTapWallet; - final bool isPaginating; + final bool showLoading; + final int loadingCount; final ScrollController? scrollController; + final double paddingTop; @override Widget build(BuildContext context) { @@ -37,27 +41,17 @@ class WalletsGrid extends StatelessWidget { ) .toList(); - if (isPaginating) { - final isLandscape = !ResponsiveData.isPortrait(context); - final loadingList = [ - const WalletGridItem(title: ''), - const WalletGridItem(title: ''), - const WalletGridItem(title: ''), - const WalletGridItem(title: ''), - ] - .map( - (e) => SizedBox( - child: Shimmer.fromColors( - baseColor: themeColors.grayGlass100, - highlightColor: themeColors.grayGlass025, - child: const WalletGridItem(title: ''), - ), + if (showLoading) { + for (var i = 0; i < loadingCount; i++) { + children.add( + SizedBox( + child: Shimmer.fromColors( + baseColor: themeColors.grayGlass100, + highlightColor: themeColors.grayGlass025, + child: const WalletGridItem(title: ''), ), - ) - .toList(); - children.addAll(loadingList); - if (isLandscape) { - children.addAll(loadingList); + ), + ); } } @@ -68,6 +62,7 @@ class WalletsGrid extends StatelessWidget { bottom: kPadding12 + ResponsiveData.paddingBottomOf(context), left: kPadding6, right: kPadding6, + top: paddingTop, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: ResponsiveData.gridAxisCountOf(context), diff --git a/lib/widgets/miscellaneous/responsive_container.dart b/lib/widgets/miscellaneous/responsive_container.dart index da78384a..a7be6aae 100644 --- a/lib/widgets/miscellaneous/responsive_container.dart +++ b/lib/widgets/miscellaneous/responsive_container.dart @@ -20,7 +20,7 @@ class ResponsiveContainer extends StatelessWidget { final screenWidth = constraints.maxWidth; final isPortrait = orientation == Orientation.portrait; final realMaxHeight = - isPortrait ? screenHeight - (kNavbarHeight * 2) : screenHeight; + isPortrait ? screenHeight * 0.75 : screenHeight; final realMinHeight = isPortrait ? 0.0 : realMaxHeight; final data = MediaQueryData.fromView(View.of(context)); diff --git a/lib/widgets/miscellaneous/searchbar.dart b/lib/widgets/miscellaneous/searchbar.dart index 35f01832..4bcc9149 100644 --- a/lib/widgets/miscellaneous/searchbar.dart +++ b/lib/widgets/miscellaneous/searchbar.dart @@ -1,10 +1,9 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:web3modal_flutter/theme/constants.dart'; import 'package:web3modal_flutter/theme/w3m_theme.dart'; import 'package:web3modal_flutter/utils/asset_util.dart'; +import 'package:web3modal_flutter/utils/debouncer.dart'; class Web3ModalSearchBar extends StatefulWidget { const Web3ModalSearchBar({ @@ -25,7 +24,7 @@ class _Web3ModalSearchBarState extends State with TickerProviderStateMixin { final _controller = TextEditingController(); final _focusNode = FocusNode(); - final _debouncer = _Debouncer(milliseconds: 200); + final _debouncer = Debouncer(milliseconds: 300); late DecorationTween _decorationTween = DecorationTween( begin: BoxDecoration( @@ -61,39 +60,49 @@ class _Web3ModalSearchBarState extends State void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final themeColors = Web3ModalTheme.colorsOf(context); - final radiuses = Web3ModalTheme.radiusesOf(context); - _decorationTween = DecorationTween( - begin: BoxDecoration( - borderRadius: BorderRadius.circular(radiuses.radiusXS), - boxShadow: [ - BoxShadow( - color: Colors.transparent, - offset: Offset.zero, - blurRadius: 0.0, - spreadRadius: 1.0, - blurStyle: BlurStyle.normal, - ), - ], - ), - end: BoxDecoration( - borderRadius: BorderRadius.circular(radiuses.radiusXS), - boxShadow: [ - BoxShadow( - color: themeColors.accenGlass015, - offset: Offset.zero, - blurRadius: 0.0, - spreadRadius: 1.0, - blurStyle: BlurStyle.normal, - ), - ], - ), - ); + _setDecoration(); _controller.addListener(_updateState); _focusNode.addListener(_updateState); }); } + void _setDecoration() { + final themeColors = Web3ModalTheme.colorsOf(context); + final radiuses = Web3ModalTheme.radiusesOf(context); + _decorationTween = DecorationTween( + begin: BoxDecoration( + borderRadius: BorderRadius.circular(radiuses.radiusXS), + boxShadow: [ + BoxShadow( + color: Colors.transparent, + offset: Offset.zero, + blurRadius: 0.0, + spreadRadius: 1.0, + blurStyle: BlurStyle.normal, + ), + ], + ), + end: BoxDecoration( + borderRadius: BorderRadius.circular(radiuses.radiusXS), + boxShadow: [ + BoxShadow( + color: themeColors.accenGlass015, + offset: Offset.zero, + blurRadius: 0.0, + spreadRadius: 1.0, + blurStyle: BlurStyle.normal, + ), + ], + ), + ); + } + + @override + void didUpdateWidget(covariant Web3ModalSearchBar oldWidget) { + super.didUpdateWidget(oldWidget); + _setDecoration(); + } + bool _hasFocus = false; void _updateState() { if (_focusNode.hasFocus && !_hasFocus) { @@ -134,143 +143,116 @@ class _Web3ModalSearchBarState extends State child: Container( height: kSearchFieldHeight + 8.0, padding: const EdgeInsets.all(4.0), - child: Stack( - children: [ - Container( - height: kSearchFieldHeight + 8.0, - width: double.infinity, - decoration: BoxDecoration( - color: themeColors.background125, - borderRadius: BorderRadius.circular(radiuses.radius2XS), - ), - ), - TextFormField( - focusNode: _focusNode, - controller: _controller, - onChanged: (value) { - _debouncer.run(() => widget.onTextChanged(value)); - }, - onTapOutside: (_) { - widget.onDismissKeyboard?.call(false); - }, - textAlignVertical: TextAlignVertical.center, - style: TextStyle( - color: themeColors.foreground100, - height: 1.5, - ), - cursorColor: themeColors.accent100, - enableSuggestions: false, - autocorrect: false, - cursorHeight: 16.0, - decoration: InputDecoration( - isDense: true, - prefixIcon: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 16.0, - height: 16.0, - margin: const EdgeInsets.only(left: kPadding12), - child: GestureDetector( - onTap: () { - _controller.clear(); - widget.onDismissKeyboard?.call(true); - }, - child: SvgPicture.asset( - 'assets/icons/search.svg', - package: 'web3modal_flutter', - height: 10.0, - width: 10.0, - colorFilter: ColorFilter.mode( - themeColors.foreground275, - BlendMode.srcIn, - ), - ), + child: TextFormField( + focusNode: _focusNode, + controller: _controller, + onChanged: (value) { + _debouncer.run(() => widget.onTextChanged(value)); + }, + onTapOutside: (_) { + widget.onDismissKeyboard?.call(false); + }, + textAlignVertical: TextAlignVertical.center, + style: TextStyle( + color: themeColors.foreground100, + height: 1.5, + ), + cursorColor: themeColors.accent100, + enableSuggestions: false, + autocorrect: false, + cursorHeight: 16.0, + decoration: InputDecoration( + isDense: true, + prefixIcon: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 16.0, + height: 16.0, + margin: const EdgeInsets.only(left: kPadding12), + child: GestureDetector( + onTap: () { + _controller.clear(); + widget.onDismissKeyboard?.call(true); + }, + child: SvgPicture.asset( + 'assets/icons/search.svg', + package: 'web3modal_flutter', + height: 10.0, + width: 10.0, + colorFilter: ColorFilter.mode( + themeColors.foreground275, + BlendMode.srcIn, ), ), - ], - ), - prefixIconConstraints: const BoxConstraints( - maxHeight: kSearchFieldHeight, - minHeight: kSearchFieldHeight, - maxWidth: 36.0, - minWidth: 36.0, - ), - labelStyle: themeData.textStyles.paragraph500.copyWith( - color: themeColors.inverse100, + ), ), - hintText: widget.hint, - hintStyle: themeData.textStyles.paragraph500.copyWith( - color: themeColors.foreground275, - height: 1.5, - ), - suffixIcon: _controller.value.text.isNotEmpty || - _focusNode.hasFocus - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Container( - width: 18.0, - height: 18.0, - margin: const EdgeInsets.only(right: kPadding12), - child: GestureDetector( - onTap: () { - _controller.clear(); - widget.onDismissKeyboard?.call(true); - }, - child: SvgPicture.asset( - AssetUtil.getThemedAsset( - context, - 'input_cancel.svg', - ), - package: 'web3modal_flutter', - height: 10.0, - width: 10.0, - ), + ], + ), + prefixIconConstraints: const BoxConstraints( + maxHeight: kSearchFieldHeight, + minHeight: kSearchFieldHeight, + maxWidth: 36.0, + minWidth: 36.0, + ), + labelStyle: themeData.textStyles.paragraph500.copyWith( + color: themeColors.inverse100, + ), + hintText: widget.hint, + hintStyle: themeData.textStyles.paragraph500.copyWith( + color: themeColors.foreground275, + height: 1.5, + ), + suffixIcon: _controller.value.text.isNotEmpty || _focusNode.hasFocus + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + width: 18.0, + height: 18.0, + margin: const EdgeInsets.only(right: kPadding12), + child: GestureDetector( + onTap: () { + _controller.clear(); + widget.onDismissKeyboard?.call(true); + }, + child: SvgPicture.asset( + AssetUtil.getThemedAsset( + context, + 'input_cancel.svg', ), + package: 'web3modal_flutter', + height: 10.0, + width: 10.0, ), - ], - ) - : null, - suffixIconConstraints: const BoxConstraints( - maxHeight: kSearchFieldHeight, - minHeight: kSearchFieldHeight, - maxWidth: 36.0, - minWidth: 36.0, - ), - border: unfocusedBorder, - errorBorder: unfocusedBorder, - enabledBorder: unfocusedBorder, - disabledBorder: unfocusedBorder, - focusedBorder: focusedBorder, - filled: true, - fillColor: themeColors.grayGlass005, - contentPadding: const EdgeInsets.all(0.0), - ), + ), + ), + ], + ) + : null, + suffixIconConstraints: const BoxConstraints( + maxHeight: kSearchFieldHeight, + minHeight: kSearchFieldHeight, + maxWidth: 36.0, + minWidth: 36.0, ), - ], + border: unfocusedBorder, + errorBorder: unfocusedBorder, + enabledBorder: unfocusedBorder, + disabledBorder: unfocusedBorder, + focusedBorder: focusedBorder, + filled: true, + fillColor: themeColors.grayGlass005, + contentPadding: const EdgeInsets.all(0.0), + ), ), ), ); } } -class _Debouncer { - final int milliseconds; - Timer? _timer; - - _Debouncer({required this.milliseconds}); - - void run(VoidCallback action) { - if (_timer?.isActive ?? false) { - _timer?.cancel(); - } - _timer = Timer(Duration(milliseconds: milliseconds), action); - } -} - // ignore: unused_element class _DecoratedInputBorder extends InputBorder { _DecoratedInputBorder({required this.child, required this.shadow}) diff --git a/lib/widgets/value_listenable_builders/explorer_service_items_listener.dart b/lib/widgets/value_listenable_builders/explorer_service_items_listener.dart index 0a4233c1..a3283e8d 100644 --- a/lib/widgets/value_listenable_builders/explorer_service_items_listener.dart +++ b/lib/widgets/value_listenable_builders/explorer_service_items_listener.dart @@ -14,6 +14,7 @@ class ExplorerServiceItemsListener extends StatefulWidget { BuildContext context, bool initialised, List> items, + bool searching, ) builder; final bool listen; @@ -32,15 +33,23 @@ class _ExplorerServiceItemsListenerState valueListenable: explorerService.instance!.initialized, builder: (context, initialised, _) { if (!initialised) { - return widget.builder(context, initialised, []); + return widget.builder(context, initialised, [], false); } - return ValueListenableBuilder>( - valueListenable: explorerService.instance!.listings, - builder: (context, items, _) { - if (widget.listen) { - _items = items.toGridItems(); + return ValueListenableBuilder( + valueListenable: explorerService.instance!.isSearching, + builder: (context, searching, _) { + if (searching) { + return widget.builder(context, initialised, _items, searching); } - return widget.builder(context, initialised, _items); + return ValueListenableBuilder>( + valueListenable: explorerService.instance!.listings, + builder: (context, items, _) { + if (widget.listen) { + _items = items.toGridItems(); + } + return widget.builder(context, initialised, _items, false); + }, + ); }, ); }, diff --git a/lib/widgets/w3m_account_button.dart b/lib/widgets/w3m_account_button.dart index 9d87a1d8..ff6731c4 100644 --- a/lib/widgets/w3m_account_button.dart +++ b/lib/widgets/w3m_account_button.dart @@ -25,9 +25,9 @@ class W3MAccountButton extends StatefulWidget { } class _W3MAccountButtonState extends State { - String? _address; - String? _tokenImage; String _balance = BalanceButton.balanceDefault; + String _address = ''; + String? _tokenImage; String? _tokenName; @override @@ -45,7 +45,7 @@ class _W3MAccountButtonState extends State { void _w3mServiceUpdated() { setState(() { - _address = widget.service.address; + _address = widget.service.address ?? ''; _tokenImage = widget.service.tokenImageUrl; _balance = BalanceButton.balanceDefault; if (widget.service.chainBalance != null) { @@ -60,20 +60,15 @@ class _W3MAccountButtonState extends State { @override Widget build(BuildContext context) { - final themeData = Web3ModalTheme.getDataOf(context); - final textStyle = widget.size == BaseButtonSize.small - ? themeData.textStyles.small600 - : themeData.textStyles.paragraph600; final themeColors = Web3ModalTheme.colorsOf(context); final radiuses = Web3ModalTheme.radiusesOf(context); final borderRadius = radiuses.isSquare() ? 0.0 : widget.size.height / 2; - final innerBorderRadius = - radiuses.isSquare() ? 0.0 : BaseButtonSize.small.height / 2; + final enabled = _address.isNotEmpty && widget.service.status.isInitialized; // TODO this button should be able to be disable by passing a null onTap action // I should decouple an AccountButton from W3MAccountButton like on ConnectButton and NetworkButton return BaseButton( size: widget.size, - onTap: widget.service.status.isInitialized ? _onTap : null, + onTap: enabled ? _onTap : null, overridePadding: MaterialStateProperty.all( const EdgeInsets.only(left: 4.0, right: 4.0), ), @@ -115,89 +110,122 @@ class _W3MAccountButtonState extends State { tokenImage: _tokenImage, iconSize: widget.size.iconSize + 4.0, buttonSize: widget.size, - onTap: _onTap, + onTap: enabled ? _onTap : null, ), const SizedBox.square(dimension: 4.0), - Padding( - padding: EdgeInsets.only( - top: widget.size == BaseButtonSize.small ? 4.0 : 0.0, - bottom: widget.size == BaseButtonSize.small ? 4.0 : 0.0, - ), - child: BaseButton( - size: BaseButtonSize.small, - onTap: _onTap, - overridePadding: MaterialStateProperty.all( - EdgeInsets.only( - left: widget.size == BaseButtonSize.small ? 4.0 : 6.0, - right: 8.0, - ), - ), - buttonStyle: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith( - (states) { - if (states.contains(MaterialState.disabled)) { - return themeColors.grayGlass005; - } - return themeColors.grayGlass010; - }, - ), - foregroundColor: MaterialStateProperty.resolveWith( - (states) { - if (states.contains(MaterialState.disabled)) { - return themeColors.grayGlass015; - } - return themeColors.foreground175; - }, - ), - shape: - MaterialStateProperty.resolveWith( - (states) { - return RoundedRectangleBorder( - side: states.contains(MaterialState.disabled) - ? BorderSide( - color: themeColors.grayGlass005, - width: 1.0, - ) - : BorderSide( - color: themeColors.grayGlass010, - width: 1.0, - ), - borderRadius: BorderRadius.circular(innerBorderRadius), - ); - }, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(widget.size.iconSize), - border: Border.all( + _AddressButton( + address: _address, + buttonSize: widget.size, + service: widget.service, + onTap: enabled ? _onTap : null, + ), + ], + ), + ); + } +} + +class _AddressButton extends StatelessWidget { + const _AddressButton({ + required this.buttonSize, + required this.address, + required this.service, + required this.onTap, + }); + final BaseButtonSize buttonSize; + final VoidCallback? onTap; + final String address; + final IW3MService service; + + @override + Widget build(BuildContext context) { + if (address.isEmpty) { + return SizedBox.shrink(); + } + final themeData = Web3ModalTheme.getDataOf(context); + final textStyle = buttonSize == BaseButtonSize.small + ? themeData.textStyles.small600 + : themeData.textStyles.paragraph600; + final themeColors = Web3ModalTheme.colorsOf(context); + final radiuses = Web3ModalTheme.radiusesOf(context); + final innerBorderRadius = + radiuses.isSquare() ? 0.0 : BaseButtonSize.small.height / 2; + return Padding( + padding: EdgeInsets.only( + top: buttonSize == BaseButtonSize.small ? 4.0 : 0.0, + bottom: buttonSize == BaseButtonSize.small ? 4.0 : 0.0, + ), + child: BaseButton( + size: BaseButtonSize.small, + onTap: onTap, + overridePadding: MaterialStateProperty.all( + EdgeInsets.only( + left: buttonSize == BaseButtonSize.small ? 4.0 : 6.0, + right: 8.0, + ), + ), + buttonStyle: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.disabled)) { + return themeColors.grayGlass005; + } + return themeColors.grayGlass010; + }, + ), + foregroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.disabled)) { + return themeColors.grayGlass015; + } + return themeColors.foreground175; + }, + ), + shape: MaterialStateProperty.resolveWith( + (states) { + return RoundedRectangleBorder( + side: states.contains(MaterialState.disabled) + ? BorderSide( color: themeColors.grayGlass005, width: 1.0, - strokeAlign: BorderSide.strokeAlignInside, + ) + : BorderSide( + color: themeColors.grayGlass010, + width: 1.0, ), - ), - child: W3MAccountAvatar( - service: widget.service, - size: widget.size.iconSize, - disabled: false, - ), - ), - const SizedBox.square(dimension: 4.0), - Text( - Util.truncate( - _address ?? '', - length: widget.size == BaseButtonSize.small ? 2 : 4, - ), - style: textStyle, - ), - ], + borderRadius: BorderRadius.circular(innerBorderRadius), + ); + }, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(buttonSize.iconSize), + border: Border.all( + color: themeColors.grayGlass005, + width: 1.0, + strokeAlign: BorderSide.strokeAlignInside, + ), + ), + child: W3MAccountAvatar( + service: service, + size: buttonSize.iconSize, + disabled: false, ), ), - ), - ], + const SizedBox.square(dimension: 4.0), + Text( + Util.truncate( + address, + length: buttonSize == BaseButtonSize.small ? 2 : 4, + ), + style: textStyle, + ), + ], + ), ), ); } diff --git a/lib/widgets/w3m_connect_wallet_button.dart b/lib/widgets/w3m_connect_wallet_button.dart index d6dd90fe..59ec6ff8 100644 --- a/lib/widgets/w3m_connect_wallet_button.dart +++ b/lib/widgets/w3m_connect_wallet_button.dart @@ -76,7 +76,7 @@ class _W3MConnectWalletButtonState extends State { return setState(() => _state = ConnectButtonState.connected); } // Case 1.5: No required namespaces - else if (widget.service.requiredNamespaces.isEmpty) { + else if (!widget.service.hasNamespaces) { return setState(() => _state = ConnectButtonState.disabled); } // Case 2: Is not open and is not connected diff --git a/lib/widgets/widget_stack/transition_container.dart b/lib/widgets/widget_stack/transition_container.dart index 8fc4fc7e..11e3d466 100644 --- a/lib/widgets/widget_stack/transition_container.dart +++ b/lib/widgets/widget_stack/transition_container.dart @@ -16,8 +16,8 @@ class TransitionContainer extends StatefulWidget { class _TransitionContainerState extends State with TickerProviderStateMixin { - static const fadeDuration = Duration(milliseconds: 200); - static const resizeDuration = Duration(milliseconds: 150); + static const fadeDuration = Duration(milliseconds: 150); + static const resizeDuration = Duration(milliseconds: 100); late AnimationController _fadeController; late Animation _fadeAnimation; late Animation _scaleAnimation; diff --git a/pubspec.lock b/pubspec.lock index 064bf677..2d46e8a9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1025,10 +1025,10 @@ packages: dependency: "direct main" description: name: walletconnect_flutter_v2 - sha256: "55315779dd94b5b38754c8abcccba6e8f159083e799945dc49441a20dc73cffd" + sha256: "00005750b01c5b924fc028e329bee5a77f79691c8c537709536e8ef27360510a" url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.9" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b085d26f..b897f8d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: web3modal_flutter description: "WalletConnect Web3Modal: Simple, intuitive wallet login. With this drop-in UI SDK, enable any wallet's users to seamlessly log in to your app and enjoy a unified experience" -version: 3.0.0-beta17 +version: 3.0.0-beta18 repository: https://github.com/WalletConnect/Web3ModalFlutter environment: @@ -26,7 +26,7 @@ dependencies: shimmer: ^3.0.0 universal_io: ^2.2.0 url_launcher: ^6.1.11 - walletconnect_flutter_v2: ^2.1.8 + walletconnect_flutter_v2: ^2.1.9 web3dart: ^2.6.1 dev_dependencies: diff --git a/test/mock_classes.mocks.dart b/test/mock_classes.mocks.dart index ee3ced8f..7de0b996 100644 --- a/test/mock_classes.mocks.dart +++ b/test/mock_classes.mocks.dart @@ -291,6 +291,22 @@ class MockExplorerService extends _i1.Mock implements _i13.ExplorerService { _i1.throwOnMissingStub(this); } + @override + _i2.ValueNotifier get initialized => (super.noSuchMethod( + Invocation.getter(#initialized), + returnValue: _FakeValueNotifier_0( + this, + Invocation.getter(#initialized), + ), + ) as _i2.ValueNotifier); + @override + set initialized(_i2.ValueNotifier? _initialized) => super.noSuchMethod( + Invocation.setter( + #initialized, + _initialized, + ), + returnValueForMissingStub: null, + ); @override String get projectId => (super.noSuchMethod( Invocation.getter(#projectId), @@ -332,6 +348,22 @@ class MockExplorerService extends _i1.Mock implements _i13.ExplorerService { returnValueForMissingStub: null, ); @override + _i2.ValueNotifier get isSearching => (super.noSuchMethod( + Invocation.getter(#isSearching), + returnValue: _FakeValueNotifier_0( + this, + Invocation.getter(#isSearching), + ), + ) as _i2.ValueNotifier); + @override + set isSearching(_i2.ValueNotifier? _isSearching) => super.noSuchMethod( + Invocation.setter( + #isSearching, + _isSearching, + ), + returnValueForMissingStub: null, + ); + @override set featuredWalletIds(Set? _featuredWalletIds) => super.noSuchMethod( Invocation.setter( #featuredWalletIds, @@ -356,27 +388,16 @@ class MockExplorerService extends _i1.Mock implements _i13.ExplorerService { returnValueForMissingStub: null, ); @override - _i2.ValueNotifier get initialized => (super.noSuchMethod( - Invocation.getter(#initialized), - returnValue: _FakeValueNotifier_0( - this, - Invocation.getter(#initialized), - ), - ) as _i2.ValueNotifier); - @override - set initialized(_i2.ValueNotifier? _initialized) => super.noSuchMethod( - Invocation.setter( - #initialized, - _initialized, - ), - returnValueForMissingStub: null, - ); - @override String get recentWalletId => (super.noSuchMethod( Invocation.getter(#recentWalletId), returnValue: '', ) as String); @override + bool get canPaginate => (super.noSuchMethod( + Invocation.getter(#canPaginate), + returnValue: false, + ) as bool); + @override _i15.Future init() => (super.noSuchMethod( Invocation.method( #init, @@ -386,24 +407,34 @@ class MockExplorerService extends _i1.Mock implements _i13.ExplorerService { returnValueForMissingStub: _i15.Future.value(), ) as _i15.Future); @override - _i15.Future fetchInitialWallets() => (super.noSuchMethod( + _i15.Future paginate() => (super.noSuchMethod( Invocation.method( - #fetchInitialWallets, + #paginate, [], ), returnValue: _i15.Future.value(), returnValueForMissingStub: _i15.Future.value(), ) as _i15.Future); @override - _i15.Future paginate() => (super.noSuchMethod( + _i15.Future updateRecentPosition(String? recentId) => + (super.noSuchMethod( Invocation.method( - #paginate, - [], + #updateRecentPosition, + [recentId], ), returnValue: _i15.Future.value(), returnValueForMissingStub: _i15.Future.value(), ) as _i15.Future); @override + void search({String? query}) => super.noSuchMethod( + Invocation.method( + #search, + [], + {#query: query}, + ), + returnValueForMissingStub: null, + ); + @override String getWalletImageUrl(String? imageId) => (super.noSuchMethod( Invocation.method( #getWalletImageUrl, @@ -420,30 +451,11 @@ class MockExplorerService extends _i1.Mock implements _i13.ExplorerService { returnValue: '', ) as String); @override - _i16.WalletRedirect? getWalletRedirectByName(_i17.Listing? listing) => + _i16.WalletRedirect? getWalletRedirect(_i17.Listing? listing) => (super.noSuchMethod(Invocation.method( - #getWalletRedirectByName, + #getWalletRedirect, [listing], )) as _i16.WalletRedirect?); - @override - _i15.Future updateRecentPosition(String? recentId) => - (super.noSuchMethod( - Invocation.method( - #updateRecentPosition, - [recentId], - ), - returnValue: _i15.Future.value(), - returnValueForMissingStub: _i15.Future.value(), - ) as _i15.Future); - @override - void search({String? query}) => super.noSuchMethod( - Invocation.method( - #search, - [], - {#query: query}, - ), - returnValueForMissingStub: null, - ); } /// A class which mocks [W3MService]. @@ -485,17 +497,10 @@ class MockW3MService extends _i1.Mock implements _i18.W3MService { returnValue: _i19.W3MServiceStatus.idle, ) as _i19.W3MServiceStatus); @override - Map get requiredNamespaces => - (super.noSuchMethod( - Invocation.getter(#requiredNamespaces), - returnValue: {}, - ) as Map); - @override - Map get optionalNamespaces => - (super.noSuchMethod( - Invocation.getter(#optionalNamespaces), - returnValue: {}, - ) as Map); + bool get hasNamespaces => (super.noSuchMethod( + Invocation.getter(#hasNamespaces), + returnValue: false, + ) as bool); @override bool get isOpen => (super.noSuchMethod( Invocation.getter(#isOpen), diff --git a/test/w3m_service/w3m_service_unit_test.dart b/test/w3m_service/w3m_service_unit_test.dart index c3776096..9b651baf 100644 --- a/test/w3m_service/w3m_service_unit_test.dart +++ b/test/w3m_service/w3m_service_unit_test.dart @@ -104,7 +104,7 @@ void main() { when(mockStorageService.setString(StringConstants.selectedChainId, any)) .thenAnswer((_) => Future.value(true)); when(es.getAssetImageUrl('imageId')).thenReturn('abc'); - when(es.getWalletRedirectByName(anyNamed('name'))).thenReturn(null); + when(es.getWalletRedirect(anyNamed('name'))).thenReturn(null); when(mockEVMService.getBalance(any, any)).thenAnswer( (_) => Future.value(1.0), ); @@ -159,21 +159,6 @@ void main() { verify(mockStorageService.init()).called(1); expect(service.status, W3MServiceStatus.initialized); - final List chainIds = []; - for (final String id in W3MChainPresets.chains.keys) { - chainIds.add('eip155:$id'); - } - final Map optionalNamespaces = { - 'eip155': RequiredNamespace( - methods: EthConstants.ethMethods, - chains: chainIds, - events: EthConstants.ethEvents, - ), - }; - expect( - service.optionalNamespaces, - optionalNamespaces, - ); expect(counter, 2); await service.init(); @@ -250,10 +235,6 @@ void main() { verify(mockEVMService.getBalance(any, any)).called(1); verify(mockBlockchainApiUtils.getIdentity(any, any)).called(1); expect(service.selectedChain, W3MChainPresets.chains['1']); - expect( - service.requiredNamespaces, - W3MChainPresets.chains['1']!.requiredNamespaces, - ); // Chain swap to polygon await service.selectChain(W3MChainPresets.chains['137']!); @@ -267,10 +248,6 @@ void main() { verify(mockEVMService.getBalance(any, any)).called(1); verify(mockBlockchainApiUtils.getIdentity(any, any)).called(1); expect(service.selectedChain, W3MChainPresets.chains['137']); - expect( - service.requiredNamespaces, - W3MChainPresets.chains['137']!.requiredNamespaces, - ); // Setting selected chain to null will disconnect await service.selectChain(null); @@ -290,10 +267,6 @@ void main() { expect(service.selectedChain, null); expect(service.chainBalance, null); expect(service.tokenImageUrl, null); - expect( - service.requiredNamespaces, - {}, - ); expect(counter, 8); // setRequiredNamespaces, disconnect }); @@ -316,7 +289,7 @@ void main() { request: anyNamed('request'), ), ).called(1); - verify(es.getWalletRedirectByName(anyNamed('name'))).called(1); + verify(es.getWalletRedirect(anyNamed('name'))).called(1); verify( mockUrlUtils.launchUrl(any), ).called(1);