diff --git a/CHANGELOG.md b/CHANGELOG.md index 936ea0d4..17f98f15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.0-beta01 + +- Link Mode! https://docs.walletconnect.com/walletkit/flutter/link-mode + ## 2.3.1 - Added Connectivity check to core and throw exceptions when internet connection is gone diff --git a/example/dapp/android/app/src/main/AndroidManifest.xml b/example/dapp/android/app/src/main/AndroidManifest.xml index 1834973a..ebfc9a9f 100644 --- a/example/dapp/android/app/src/main/AndroidManifest.xml +++ b/example/dapp/android/app/src/main/AndroidManifest.xml @@ -1,18 +1,20 @@ - + + - - + + + - + + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" /> + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/dapp/android/app/src/main/kotlin/com/example/dapp/MainActivity.kt b/example/dapp/android/app/src/main/kotlin/com/example/dapp/MainActivity.kt index db5869cd..c531c201 100644 --- a/example/dapp/android/app/src/main/kotlin/com/example/dapp/MainActivity.kt +++ b/example/dapp/android/app/src/main/kotlin/com/example/dapp/MainActivity.kt @@ -1,6 +1,65 @@ package com.example.dapp import io.flutter.embedding.android.FlutterActivity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel class MainActivity: FlutterActivity() { + private val eventsChannel = "com.walletconnect.flutterdapp/events" + private val methodsChannel = "com.walletconnect.flutterdapp/methods" + + private var initialLink: String? = null + private var linksReceiver: BroadcastReceiver? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val intent: Intent? = intent + initialLink = intent?.data?.toString() + + EventChannel(flutterEngine?.dartExecutor?.binaryMessenger, eventsChannel).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(args: Any?, events: EventChannel.EventSink) { + linksReceiver = createChangeReceiver(events) + } + override fun onCancel(args: Any?) { + linksReceiver = null + } + } + ) + + MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, methodsChannel).setMethodCallHandler { call, result -> + if (call.method == "initialLink") { + if (initialLink != null) { + result.success(initialLink) + } + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + if (intent.action === Intent.ACTION_VIEW) { + linksReceiver?.onReceive(this.applicationContext, intent) + } + } + + fun createChangeReceiver(events: EventChannel.EventSink): BroadcastReceiver? { + return object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val dataString = intent.dataString ?: + events.error("UNAVAILABLE", "Link unavailable", null) + events.success(dataString) + } + } + } } diff --git a/example/dapp/ios/Runner.xcodeproj/project.pbxproj b/example/dapp/ios/Runner.xcodeproj/project.pbxproj index 7c94090d..6a1737b5 100644 --- a/example/dapp/ios/Runner.xcodeproj/project.pbxproj +++ b/example/dapp/ios/Runner.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 0964B3102C494D2000AE1CDA /* Debug-internal.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Debug-internal.xcconfig"; path = "Flutter/Debug-internal.xcconfig"; sourceTree = ""; }; 0964B3112C494D2000AE1CDA /* Release-production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Release-production.xcconfig"; path = "Flutter/Release-production.xcconfig"; sourceTree = ""; }; 0964B3122C49545400AE1CDA /* Info-internal.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-internal.plist"; sourceTree = ""; }; + 09969A8C2C73BC9100B14363 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 2269C513CF66C603D39509BB /* Pods-Runner.debug-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-internal.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-internal.xcconfig"; sourceTree = ""; }; @@ -128,6 +129,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 09969A8C2C73BC9100B14363 /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -380,6 +382,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-internal"; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; @@ -462,6 +465,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-internal"; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; @@ -542,6 +546,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-internal"; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; @@ -625,6 +630,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; @@ -642,7 +648,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.flutterdapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.flutterdapp 1721392810"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.flutterdapp 1724090152"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -761,8 +767,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = ""; @@ -778,7 +785,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.flutterdapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.flutterdapp 1721392810"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.walletconnect.flutterdapp"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -792,6 +799,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; @@ -809,7 +817,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.flutterdapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.flutterdapp 1721392810"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.flutterdapp 1724090152"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/example/dapp/ios/Runner/AppDelegate.swift b/example/dapp/ios/Runner/AppDelegate.swift index 70693e4a..805d27d2 100644 --- a/example/dapp/ios/Runner/AppDelegate.swift +++ b/example/dapp/ios/Runner/AppDelegate.swift @@ -3,11 +3,96 @@ import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } + + private static let EVENTS_CHANNEL = "com.walletconnect.flutterdapp/events" + private static let METHODS_CHANNEL = "com.walletconnect.flutterdapp/methods" + + private var eventsChannel: FlutterEventChannel? + private var methodsChannel: FlutterMethodChannel? + var initialLink: String? + + private let linkStreamHandler = LinkStreamHandler() + + override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + GeneratedPluginRegistrant.register(with: self) + + let controller = window.rootViewController as! FlutterViewController + eventsChannel = FlutterEventChannel(name: AppDelegate.EVENTS_CHANNEL, binaryMessenger: controller.binaryMessenger) + eventsChannel?.setStreamHandler(linkStreamHandler) + + methodsChannel = FlutterMethodChannel(name: AppDelegate.METHODS_CHANNEL, binaryMessenger: controller.binaryMessenger) + methodsChannel?.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in + if (call.method == "initialLink") { + if let link = self?.initialLink { + let handled = self?.linkStreamHandler.handleLink(link) + if (handled == true) { + self?.initialLink = nil + } + } + } + }) + + // Add your deep link handling logic here + if let url = launchOptions?[.url] as? URL { + self.initialLink = url.absoluteString + } + + if let userActivityDictionary = launchOptions?[.userActivityDictionary] as? [String: Any], + let userActivity = userActivityDictionary["UIApplicationLaunchOptionsUserActivityKey"] as? NSUserActivity, + userActivity.activityType == NSUserActivityTypeBrowsingWeb { + + handleIncomingUniversalLink(userActivity: userActivity) + } + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + return linkStreamHandler.handleLink(url.absoluteString) + } + + override func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + if userActivity.activityType == NSUserActivityTypeBrowsingWeb { + handleIncomingUniversalLink(userActivity: userActivity) + return true + } + return false + } + + private func handleIncomingUniversalLink(userActivity: NSUserActivity) { + if let url = userActivity.webpageURL { + // Handle the URL, navigate to appropriate screen + print("App launched with Universal Link: \(url.absoluteString)") + let handled = linkStreamHandler.handleLink(url.absoluteString) + if (!handled){ + self.initialLink = url.absoluteString + } + } + } +} + +class LinkStreamHandler: NSObject, FlutterStreamHandler { + var eventSink: FlutterEventSink? + var queuedLinks = [String]() + + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + self.eventSink = events + queuedLinks.forEach({ events($0) }) + queuedLinks.removeAll() + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + self.eventSink = nil + return nil + } + + func handleLink(_ link: String) -> Bool { + guard let eventSink = eventSink else { + queuedLinks.append(link) + return false + } + eventSink(link) + return true + } } diff --git a/example/dapp/ios/Runner/Runner.entitlements b/example/dapp/ios/Runner/Runner.entitlements new file mode 100644 index 00000000..64aad23c --- /dev/null +++ b/example/dapp/ios/Runner/Runner.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.associated-domains + + applinks:lab.web3modal.com + applinks:dev.lab.web3modal.com + applinks:web3modal-laboratory-git-chores-addedmore-3e0f2b-walletconnect1.vercel.app + + + diff --git a/example/dapp/lib/main.dart b/example/dapp/lib/main.dart index cf5565c8..47b74c4a 100644 --- a/example/dapp/lib/main.dart +++ b/example/dapp/lib/main.dart @@ -13,12 +13,15 @@ import 'package:walletconnect_flutter_v2_dapp/utils/constants.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/crypto/chain_data.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/crypto/helpers.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/dart_defines.dart'; +import 'package:walletconnect_flutter_v2_dapp/utils/deep_link_handler.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/string_constants.dart'; import 'package:walletconnect_flutter_v2_dapp/widgets/event_widget.dart'; import 'package:walletconnect_flutter_v2_dapp/imports.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); + DeepLinkHandler.initListener(); runApp(const MyApp()); } @@ -59,24 +62,46 @@ class _MyHomePageState extends State { initialize(); } - Future initialize() async { + String get _flavor { String flavor = '-${const String.fromEnvironment('FLUTTER_APP_FLAVOR')}'; - flavor = flavor.replaceAll('-production', ''); + return flavor.replaceAll('-production', ''); + } + + String _universalLink() { + Uri link = Uri.parse('https://lab.web3modal.com/flutter_appkit'); + if (_flavor.isNotEmpty) { + return link + .replace(path: '${link.path}_internal') + .replace(host: 'dev.${link.host}') + .toString(); + } + return link.toString(); + } + + Redirect _constructRedirect() { + return Redirect( + native: 'wcflutterdapp$_flavor://', + universal: _universalLink(), + // enable linkMode on Wallet so Dapps can use relay-less connection + // universal: value must be set on cloud config as well + linkMode: true, + ); + } + + Future initialize() async { _web3App = Web3App( core: Core( projectId: DartDefines.projectId, + logLevel: LogLevel.error, ), metadata: PairingMetadata( name: 'Sample dApp Flutter', description: 'WalletConnect\'s sample dapp with Flutter', - url: 'https://walletconnect.com/', + url: _universalLink(), icons: [ 'https://images.prismic.io/wallet-connect/65785a56531ac2845a260732_WalletConnect-App-Logo-1024X1024.png' ], - redirect: Redirect( - native: 'wcflutterdapp$flavor://', - // universal: 'https://walletconnect.com', - ), + redirect: _constructRedirect(), ), ); @@ -100,6 +125,9 @@ class _MyHomePageState extends State { await _web3App!.init(); + DeepLinkHandler.init(_web3App!); + DeepLinkHandler.checkInitialLink(); + // Loop through all the chain data for (final ChainMetadata chain in ChainData.allChains) { // Loop through the events for that chain @@ -140,11 +168,19 @@ class _MyHomePageState extends State { } void _onSessionConnect(SessionConnect? event) { - log('[SampleDapp] _onSessionConnect $event'); + debugPrint('[SampleDapp] _onSessionConnect $event'); + Future.delayed(const Duration(milliseconds: 500), () { + setState(() => _selectedIndex = 2); + }); } void _onSessionAuthResponse(SessionAuthResponse? response) { - log('[SampleDapp] _onSessionAuthResponse $response'); + debugPrint('[SampleDapp] _onSessionAuthResponse $response'); + if (response?.session != null) { + Future.delayed(const Duration(milliseconds: 500), () { + setState(() => _selectedIndex = 2); + }); + } } void _setState(dynamic args) => setState(() {}); @@ -180,9 +216,9 @@ class _MyHomePageState extends State { void _logListener(LogEvent event) { if (event.level == Level.debug) { // TODO send to mixpanel - log('[Mixpanel] ${event.message}'); + log('${event.message}'); } else { - debugPrint('[Logger] ${event.level.name}: ${event.message}'); + debugPrint('${event.message}'); } } @@ -246,11 +282,7 @@ class _MyHomePageState extends State { showUnselectedLabels: true, type: BottomNavigationBarType.fixed, // called when one tab is selected - onTap: (int index) { - setState(() { - _selectedIndex = index; - }); - }, + onTap: (index) => setState(() => _selectedIndex = index), // bottom tab items items: _pageDatas .map( @@ -266,11 +298,7 @@ class _MyHomePageState extends State { Widget _buildNavigationRail() { return NavigationRail( selectedIndex: _selectedIndex, - onDestinationSelected: (int index) { - setState(() { - _selectedIndex = index; - }); - }, + onDestinationSelected: (index) => setState(() => _selectedIndex = index), labelType: NavigationRailLabelType.selected, destinations: _pageDatas .map( diff --git a/example/dapp/lib/pages/connect_page.dart b/example/dapp/lib/pages/connect_page.dart index 64accaf1..17ca2429 100644 --- a/example/dapp/lib/pages/connect_page.dart +++ b/example/dapp/lib/pages/connect_page.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:async'; import 'dart:convert'; @@ -7,14 +9,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - import 'package:walletconnect_flutter_v2_dapp/models/chain_metadata.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/constants.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/crypto/chain_data.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/crypto/eip155.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/crypto/polkadot.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/crypto/solana.dart'; +import 'package:walletconnect_flutter_v2_dapp/utils/sample_wallets.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/string_constants.dart'; import 'package:walletconnect_flutter_v2_dapp/widgets/chain_button.dart'; import 'package:walletconnect_flutter_v2_dapp/imports.dart'; @@ -63,13 +64,6 @@ class ConnectPageState extends State { super.dispose(); } - void setTestnet(bool value) { - if (value != _testnetOnly) { - _selectedChains.clear(); - } - _testnetOnly = value; - } - void _selectChain(ChainMetadata chain) { setState(() { if (_selectedChains.contains(chain)) { @@ -181,48 +175,30 @@ class ConnectPageState extends State { return ListView( padding: const EdgeInsets.symmetric(horizontal: StyleConstants.linear8), children: [ - Column( + const Text( + 'Flutter Dapp', + style: StyleConstants.subtitleText, + textAlign: TextAlign.center, + ), + const SizedBox(height: StyleConstants.linear8), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( - 'Flutter Dapp', - style: StyleConstants.subtitleText, - textAlign: TextAlign.center, + StringConstants.testnetsOnly, + style: StyleConstants.buttonText, ), - FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const SizedBox.shrink(); - } - final v = snapshot.data!.version; - final b = snapshot.data!.buildNumber; - const f = String.fromEnvironment('FLUTTER_APP_FLAVOR'); - return Text('$v-$f ($b) - SDK v$packageVersion'); + Switch( + value: _testnetOnly, + onChanged: (value) { + setState(() { + _selectedChains.clear(); + _testnetOnly = value; + }); }, ), ], ), - SizedBox( - height: StyleConstants.linear48, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - StringConstants.testnetsOnly, - style: StyleConstants.buttonText, - ), - Switch( - value: _testnetOnly, - onChanged: (value) { - setState(() { - _selectedChains.clear(); - _testnetOnly = value; - }); - }, - ), - ], - ), - ), const Text('EVM Chains:', style: StyleConstants.buttonText), const SizedBox(height: StyleConstants.linear8), Wrap( @@ -260,83 +236,163 @@ class ConnectPageState extends State { children: [ const SizedBox(height: StyleConstants.linear8), const Text( - 'Use custom connection:', + 'Connect Session Propose:', style: StyleConstants.buttonText, ), const SizedBox(height: StyleConstants.linear8), - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: _buttonStyle, - onPressed: _selectedChains.isEmpty - ? null - : () => _onConnect( - showToast: (m) async { - await showPlatformToast( - child: Text(m), context: context); - }, - closeModal: () { - if (Navigator.canPop(context)) { - Navigator.of(context).pop(); - } + Column( + children: WCSampleWallets.getSampleWallets().map((wallet) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: ElevatedButton( + style: _buttonStyle, + onPressed: _selectedChains.isEmpty + ? null + : () { + _onConnect( + nativeLink: '${wallet['schema']}', + closeModal: () { + if (Navigator.canPop(context)) { + Navigator.of(context).pop(); + } + }, + showToast: (m) async { + showPlatformToast( + child: Text(m), + context: context, + ); + }, + ); }, - ), - child: const Text( - StringConstants.connect, - style: StyleConstants.buttonText, - ), - ), + child: Text( + '${wallet['name']}', + style: StyleConstants.buttonText, + ), + ), + ); + }).toList(), ), const SizedBox(height: StyleConstants.linear8), - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: _buttonStyle, - onPressed: _selectedChains.isEmpty - ? null - : () => _oneClickAuth( - closeModal: () { - if (Navigator.canPop(context)) { - Navigator.of(context).pop(); - } - }, - showToast: (message) { - showPlatformToast( - child: Text(message), context: context); + const Divider(), + const Text( + '1-Click Auth with LinkMode:', + style: StyleConstants.buttonText, + ), + const SizedBox(height: StyleConstants.linear8), + Column( + children: WCSampleWallets.getSampleWallets().map((wallet) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: ElevatedButton( + style: _buttonStyle, + onPressed: _selectedChains.isEmpty + ? null + : () { + _sessionAuthenticate( + nativeLink: '${wallet['schema']}', + universalLink: '${wallet['universal']}', + closeModal: () { + if (Navigator.canPop(context)) { + Navigator.of(context).pop(); + } + }, + showToast: (message) { + showPlatformToast( + child: Text(message), + context: context, + ); + }, + ); }, - ), - child: const Text( - 'One-Click Auth', - style: StyleConstants.buttonText, - ), + child: Text( + '${wallet['name']}', + style: StyleConstants.buttonText, + ), + ), + ); + }).toList(), + ), + ], + ), + const SizedBox(height: StyleConstants.linear16), + const Divider(height: 1.0), + const SizedBox(height: StyleConstants.linear16), + const Text( + 'Redirect:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Native: '), + Expanded( + child: Text( + '${widget.web3App.metadata.redirect?.native}', + style: const TextStyle(fontWeight: FontWeight.bold), ), ), ], ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Universal: '), + Expanded( + child: Text( + '${widget.web3App.metadata.redirect?.universal}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ], + ), + Row( + children: [ + const Text('Link Mode: '), + Text( + '${widget.web3App.metadata.redirect?.linkMode}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: StyleConstants.linear8), + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox.shrink(); + } + final v = snapshot.data!.version; + final b = snapshot.data!.buildNumber; + const f = String.fromEnvironment('FLUTTER_APP_FLAVOR'); + // return Text('App Version: $v-$f ($b) - SDK v$packageVersion'); + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('App Version: '), + Expanded( + child: Text( + '$v-$f ($b) - SDK v$packageVersion', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ], + ); + }, + ), const SizedBox(height: StyleConstants.linear16), ], ); } - // Future _onConnectWeb() async { - // // `Ethereum.isSupported` is the same as `ethereum != null` - // if (ethereum != null) { - // try { - // // Prompt user to connect to the provider, i.e. confirm the connection modal - // final accounts = await ethereum!.requestAccount(); - // // Get all accounts in node disposal - // debugPrint('accounts ${accounts.join(', ')}'); - // } on EthereumUserRejected { - // debugPrint('User rejected the modal'); - // } - // } - // } - Future _onConnect({ - Function(String message)? showToast, + required String nativeLink, VoidCallback? closeModal, + Function(String message)? showToast, }) async { - debugPrint('[SampleDapp] Creating connection and session'); + debugPrint('[SampleDapp] Creating connection with $nativeLink'); // It is currently safer to send chains approvals on optionalNamespaces // but depending on Wallet implementation you may need to send some (for innstance eip155:1) as required final connectResponse = await widget.web3App.connect( @@ -344,47 +400,79 @@ class ConnectPageState extends State { optionalNamespaces: optionalNamespaces, ); - final encodedUri = Uri.encodeComponent(connectResponse.uri.toString()); - String flavor = '-${const String.fromEnvironment('FLUTTER_APP_FLAVOR')}'; - flavor = flavor.replaceAll('-production', ''); - final uri = 'wcflutterwallet$flavor://wc?uri=$encodedUri'; - if (await canLaunchUrlString(uri)) { - final openApp = await showDialog( - // ignore: use_build_context_synchronously - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: const Text('Do you want to open with Flutter Wallet'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Show QR'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Open'), - ), - ], - ); - }, - ); - if (openApp) { - launchUrlString(uri, mode: LaunchMode.externalApplication); - } else { - _showQrCode(connectResponse.uri.toString()); - } - } else { + try { + final encodedUri = Uri.encodeComponent(connectResponse.uri.toString()); + final uri = '$nativeLink?uri=$encodedUri'; + await WalletConnectUtils.openURL(uri); + } catch (e) { _showQrCode(connectResponse.uri.toString()); } debugPrint('[SampleDapp] Awaiting session proposal settlement'); - final _ = await connectResponse.session.future; + try { + await connectResponse.session.future; + showToast?.call(StringConstants.connectionEstablished); + } on JsonRpcError catch (e) { + showToast?.call(e.message.toString()); + } + closeModal?.call(); + } + + void _sessionAuthenticate({ + required String nativeLink, + required String universalLink, + VoidCallback? closeModal, + Function(String message)? showToast, + }) async { + debugPrint( + '[SampleDapp] Creating authenticate with $nativeLink, $universalLink'); + final methods1 = requiredNamespaces['eip155']?.methods ?? []; + final methods2 = optionalNamespaces['eip155']?.methods ?? []; + final authResponse = await widget.web3App.authenticate( + params: SessionAuthRequestParams( + chains: _selectedChains.map((e) => e.chainId).toList(), + domain: Uri.parse(widget.web3App.metadata.url).authority, + nonce: AuthUtils.generateNonce(), + uri: widget.web3App.metadata.url, + statement: 'Welcome to example flutter app', + methods: {...methods1, ...methods2}.toList(), + ), + walletUniversalLink: universalLink, + ); + + debugPrint('[SampleDapp] authResponse.uri ${authResponse.uri}'); + try { + // If response uri is not universalLink show QR Code + if (authResponse.uri?.authority != Uri.parse(universalLink).authority) { + _showQrCode('${authResponse.uri}', walletScheme: nativeLink); + } else { + await WalletConnectUtils.openURL(authResponse.uri.toString()); + } + } catch (e) { + debugPrint('[SampleDapp] authResponse error $e'); + _showQrCode('${authResponse.uri}', walletScheme: nativeLink); + } + + try { + debugPrint('[SampleDapp] Awaiting 1-CA session'); + final response = await authResponse.completer.future; - showToast?.call(StringConstants.connectionEstablished); + if (response.session != null) { + showToast?.call( + '${StringConstants.authSucceeded} and ${StringConstants.connectionEstablished}', + ); + } else { + final error = response.error ?? response.jsonRpcError; + showToast?.call(error.toString()); + } + } catch (e) { + debugPrint('[SampleDapp] 1-CA $e'); + showToast?.call(StringConstants.connectionFailed); + } closeModal?.call(); } - Future _showQrCode(String uri) async { + Future _showQrCode(String uri, {String walletScheme = ''}) async { // Show the QR code debugPrint('[SampleDapp] Showing QR Code: $uri'); _shouldDismissQrCode = true; @@ -425,164 +513,23 @@ class ConnectPageState extends State { context, MaterialPageRoute( fullscreenDialog: true, - builder: (context) => QRCodeScreen(uri: uri), - ), - ); - } - - void _requestAuth( - SessionConnect? event, { - Function(String message)? showToast, - }) async { - final shouldAuth = await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return AlertDialog( - insetPadding: const EdgeInsets.all(0.0), - contentPadding: const EdgeInsets.all(0.0), - backgroundColor: Colors.white, - title: const Text('Request Auth?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.pop(context, true); - }, - child: const Text('Yes!'), - ), - ], - ); - }, - ); - if (shouldAuth != true) return; - - try { - final pairingTopic = event?.session.pairingTopic; - // Send off an auth request now that the pairing/session is established - final authResponse = await widget.web3App.requestAuth( - pairingTopic: pairingTopic, - params: AuthRequestParams( - chainId: 'eip155:1', - domain: Constants.domain, - aud: Constants.aud, - statement: 'Welcome to example flutter app', + builder: (context) => QRCodeScreen( + uri: uri, + walletScheme: walletScheme, ), - ); - - final scheme = event?.session.peer.metadata.redirect?.native; - String flavor = '-${const String.fromEnvironment('FLUTTER_APP_FLAVOR')}'; - flavor = flavor.replaceAll('-production', ''); - launchUrlString( - scheme ?? 'wcflutterwallet$flavor://', - mode: LaunchMode.externalApplication, - ); - - debugPrint('[SampleDapp] Awaiting authentication response'); - final response = await authResponse.completer.future; - if (response.result != null) { - showToast?.call(StringConstants.authSucceeded); - } else { - final error = response.error ?? response.jsonRpcError; - showToast?.call(error.toString()); - } - } catch (e) { - debugPrint('[SampleDapp] auth $e'); - showToast?.call(StringConstants.connectionFailed); - } - } - - void _oneClickAuth({ - VoidCallback? closeModal, - Function(String message)? showToast, - }) async { - final methods1 = requiredNamespaces['eip155']?.methods ?? []; - final methods2 = optionalNamespaces['eip155']?.methods ?? []; - String flavor = '-${const String.fromEnvironment('FLUTTER_APP_FLAVOR')}'; - flavor = flavor.replaceAll('-production', ''); - final authResponse = await widget.web3App.authenticate( - params: SessionAuthRequestParams( - chains: _selectedChains.map((e) => e.chainId).toList(), - domain: 'wcflutterdapp$flavor://', - nonce: AuthUtils.generateNonce(), - uri: Constants.aud, - statement: 'Welcome to example flutter app', - methods: {...methods1, ...methods2}.toList(), ), ); - - final encodedUri = Uri.encodeComponent(authResponse.uri.toString()); - final uri = 'wcflutterwallet$flavor://wc?uri=$encodedUri'; - - if (await canLaunchUrlString(uri)) { - final openApp = await showDialog( - // ignore: use_build_context_synchronously - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: const Text('Do you want to open with Flutter Wallet'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Show QR'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Open'), - ), - ], - ); - }, - ); - if (openApp) { - launchUrlString(uri, mode: LaunchMode.externalApplication); - } else { - _showQrCode(authResponse.uri.toString()); - } - } else { - _showQrCode(authResponse.uri.toString()); - } - - try { - debugPrint('[SampleDapp] Awaiting 1-CA session'); - final response = await authResponse.completer.future; - - if (response.session != null) { - showToast?.call( - '${StringConstants.authSucceeded} and ${StringConstants.connectionEstablished}', - ); - } else { - final error = response.error ?? response.jsonRpcError; - showToast?.call(error.toString()); - } - } catch (e) { - debugPrint('[SampleDapp] 1-CA $e'); - showToast?.call(StringConstants.connectionFailed); - } - closeModal?.call(); } void _onSessionConnect(SessionConnect? event) async { if (event == null) return; - setState(() { - _selectedChains.clear(); - }); + setState(() => _selectedChains.clear()); if (_shouldDismissQrCode && Navigator.canPop(context)) { _shouldDismissQrCode = false; Navigator.pop(context); } - - _requestAuth( - event, - showToast: (message) { - showPlatformToast(child: Text(message), context: context); - }, - ); } ButtonStyle get _buttonStyle => ButtonStyle( @@ -609,8 +556,13 @@ class ConnectPageState extends State { } class QRCodeScreen extends StatefulWidget { - const QRCodeScreen({super.key, required this.uri}); + const QRCodeScreen({ + super.key, + required this.uri, + this.walletScheme = '', + }); final String uri; + final String walletScheme; @override State createState() => _QRCodeScreenState(); @@ -624,6 +576,7 @@ class _QRCodeScreenState extends State { appBar: AppBar(title: const Text(StringConstants.scanQrCode)), body: _QRCodeView( uri: widget.uri, + walletScheme: widget.walletScheme, ), ), ); @@ -631,8 +584,12 @@ class _QRCodeScreenState extends State { } class _QRCodeView extends StatelessWidget { - const _QRCodeView({required this.uri}); + const _QRCodeView({ + required this.uri, + this.walletScheme = '', + }); final String uri; + final String walletScheme; @override Widget build(BuildContext context) { @@ -656,6 +613,16 @@ class _QRCodeView extends StatelessWidget { }, child: const Text('Copy URL to Clipboard'), ), + Visibility( + visible: walletScheme.isNotEmpty, + child: ElevatedButton( + onPressed: () async { + final encodedUri = Uri.encodeComponent(uri); + await WalletConnectUtils.openURL('$walletScheme?uri=$encodedUri'); + }, + child: const Text('Open Test Wallet'), + ), + ), ], ); } diff --git a/example/dapp/lib/pages/sessions_page.dart b/example/dapp/lib/pages/sessions_page.dart index eea88085..30ca5a9d 100644 --- a/example/dapp/lib/pages/sessions_page.dart +++ b/example/dapp/lib/pages/sessions_page.dart @@ -41,7 +41,6 @@ class SessionsPageState extends State { @override Widget build(BuildContext context) { final List sessions = _activeSessions.values.toList(); - return Center( child: Container( constraints: const BoxConstraints( diff --git a/example/dapp/lib/utils/constants.dart b/example/dapp/lib/utils/constants.dart index 58db7a28..a17612da 100644 --- a/example/dapp/lib/utils/constants.dart +++ b/example/dapp/lib/utils/constants.dart @@ -2,9 +2,6 @@ import 'package:flutter/material.dart'; class Constants { static const smallScreen = 640; - - static const String aud = 'https://walletconnect.org/login'; - static const String domain = 'walletconnect.org'; } class StyleConstants { diff --git a/example/dapp/lib/utils/crypto/eip155.dart b/example/dapp/lib/utils/crypto/eip155.dart index 29b358c1..0b6b747a 100644 --- a/example/dapp/lib/utils/crypto/eip155.dart +++ b/example/dapp/lib/utils/crypto/eip155.dart @@ -1,6 +1,4 @@ import 'dart:convert'; -// ignore: depend_on_referenced_packages -import 'package:convert/convert.dart'; import 'package:walletconnect_flutter_v2_dapp/models/chain_metadata.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/crypto/chain_data.dart'; @@ -156,7 +154,7 @@ class EIP155 { required String message, }) async { final bytes = utf8.encode(message); - final encoded = '0x${hex.encode(bytes)}'; + final encoded = bytesToHex(bytes); return await web3App.request( topic: topic, diff --git a/example/dapp/lib/utils/deep_link_handler.dart b/example/dapp/lib/utils/deep_link_handler.dart new file mode 100644 index 00000000..f3c6960c --- /dev/null +++ b/example/dapp/lib/utils/deep_link_handler.dart @@ -0,0 +1,60 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:walletconnect_flutter_v2_dapp/imports.dart'; + +class DeepLinkHandler { + static const _methodChannel = MethodChannel( + 'com.walletconnect.flutterdapp/methods', + ); + static const _eventChannel = EventChannel( + 'com.walletconnect.flutterdapp/events', + ); + static final waiting = ValueNotifier(false); + static late IWeb3App _web3app; + + static void initListener() { + if (kIsWeb) return; + try { + _eventChannel.receiveBroadcastStream().listen( + _onLink, + onError: _onError, + ); + } catch (e) { + debugPrint('[SampleWallet] [DeepLinkHandler] checkInitialLink $e'); + } + } + + static void init(IWeb3App web3app) { + if (kIsWeb) return; + _web3app = web3app; + } + + static void checkInitialLink() async { + if (kIsWeb) return; + try { + _methodChannel.invokeMethod('initialLink'); + } catch (e) { + debugPrint('[SampleWallet] [DeepLinkHandler] checkInitialLink $e'); + } + } + + static Uri get nativeUri => + Uri.parse(_web3app.metadata.redirect?.native ?? ''); + static Uri get universalUri => + Uri.parse(_web3app.metadata.redirect?.universal ?? ''); + static String get host => universalUri.host; + + static void _onLink(dynamic link) async { + if (link == null) return; + final envelope = WalletConnectUtils.getSearchParamFromURL(link, 'wc_ev'); + if (envelope.isNotEmpty) { + debugPrint('[SampleDapp] is linkMode $link'); + await _web3app.dispatchEnvelope(link); + } + } + + static void _onError(dynamic error) { + debugPrint('[SampleDapp] _onError $error'); + waiting.value = false; + } +} diff --git a/example/dapp/lib/utils/sample_wallets.dart b/example/dapp/lib/utils/sample_wallets.dart new file mode 100644 index 00000000..e7730c6a --- /dev/null +++ b/example/dapp/lib/utils/sample_wallets.dart @@ -0,0 +1,90 @@ +import 'dart:io'; + +class WCSampleWallets { + static List> sampleWalletsInternal() => [ + { + 'name': 'Swift Wallet', + 'platform': ['ios'], + 'id': '123456789012345678901234567890', + 'schema': 'walletapp://', + 'bundleId': 'com.walletconnect.sample.wallet', + 'universal': 'https://lab.web3modal.com/wallet', + }, + { + 'name': 'Flutter Wallet (internal)', + 'platform': ['ios', 'android'], + 'id': '123456789012345678901234567895', + 'schema': 'wcflutterwallet-internal://', + 'bundleId': 'com.walletconnect.flutterwallet.internal', + 'universal': + 'https://dev.lab.web3modal.com/flutter_walletkit_internal', + }, + { + 'name': 'RN Wallet (internal)', + 'platform': ['ios', 'android'], + 'id': '1234567890123456789012345678922', + 'schema': 'rn-web3wallet://wc', + 'bundleId': 'com.walletconnect.web3wallet.rnsample.internal', + 'universal': 'https://lab.web3modal.com/rn_walletkit', + }, + { + 'name': 'Kotlin Wallet (Internal)', + 'platform': ['android'], + 'id': '123456789012345678901234567894', + 'schema': 'kotlin-web3wallet://wc', + 'bundleId': 'com.walletconnect.sample.wallet.internal', + 'universal': + 'https://web3modal-laboratory-git-chore-kotlin-assetlinks-walletconnect1.vercel.app/wallet_internal', + }, + ]; + + static List> sampleWalletsProduction() => [ + { + 'name': 'Swift Wallet', + 'platform': ['ios'], + 'id': '123456789012345678901234567890', + 'schema': 'walletapp://', + 'bundleId': 'com.walletconnect.sample.wallet', + 'universal': 'https://lab.web3modal.com/wallet', + }, + { + 'name': 'Flutter Wallet', + 'platform': ['ios', 'android'], + 'id': '123456789012345678901234567891', + 'schema': 'wcflutterwallet://', + 'bundleId': 'com.walletconnect.flutterwallet', + 'universal': 'https://lab.web3modal.com/flutter_walletkit', + }, + { + 'name': 'RN Wallet', + 'platform': ['ios', 'android'], + 'id': '123456789012345678901234567892', + 'schema': 'rn-web3wallet://wc', + 'bundleId': 'com.walletconnect.web3wallet.rnsample', + 'universal': 'https://lab.web3modal.com/rn_walletkit', + }, + { + 'name': 'Kotlin Wallet', + 'platform': ['android'], + 'id': '123456789012345678901234567893', + 'schema': 'kotlin-web3wallet://wc', + 'bundleId': 'com.walletconnect.sample.wallet', + 'universal': + 'https://web3modal-laboratory-git-chore-kotlin-assetlinks-walletconnect1.vercel.app/wallet_release', + }, + ]; + + static List> getSampleWallets() { + String flavor = '-${const String.fromEnvironment('FLUTTER_APP_FLAVOR')}'; + flavor = flavor.replaceAll('-production', ''); + if (flavor.isNotEmpty) { + return sampleWalletsInternal().where((e) { + return (e['platform'] as List) + .contains(Platform.operatingSystem); + }).toList(); + } + return sampleWalletsProduction().where((e) { + return (e['platform'] as List).contains(Platform.operatingSystem); + }).toList(); + } +} diff --git a/example/dapp/lib/widgets/method_dialog.dart b/example/dapp/lib/widgets/method_dialog.dart index 4b402cf9..10791f9b 100644 --- a/example/dapp/lib/widgets/method_dialog.dart +++ b/example/dapp/lib/widgets/method_dialog.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:convert'; import 'package:fl_toast/fl_toast.dart'; diff --git a/example/dapp/lib/widgets/pairing_item.dart b/example/dapp/lib/widgets/pairing_item.dart index 4ed2dd02..9d503de9 100644 --- a/example/dapp/lib/widgets/pairing_item.dart +++ b/example/dapp/lib/widgets/pairing_item.dart @@ -38,6 +38,7 @@ class PairingItem extends StatelessWidget { Text( pairing.peerMetadata?.url ?? 'Expiry: $expiryDate ($inDays days)', ), + Text(pairing.topic), ], ), ), diff --git a/example/dapp/lib/widgets/session_widget.dart b/example/dapp/lib/widgets/session_widget.dart index 1f20a008..520e308c 100644 --- a/example/dapp/lib/widgets/session_widget.dart +++ b/example/dapp/lib/widgets/session_widget.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher_string.dart'; import 'package:walletconnect_flutter_v2_dapp/models/chain_metadata.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/constants.dart'; @@ -265,13 +264,10 @@ class SessionWidgetState extends State { void _launchWallet() { if (kIsWeb) return; - final walletUrl = widget.session.peer.metadata.redirect?.native; - if ((walletUrl ?? '').isNotEmpty) { - launchUrlString( - walletUrl!, - mode: LaunchMode.externalApplication, - ); - } + widget.web3App.redirectToWallet( + topic: widget.session.topic, + redirect: widget.session.peer.metadata.redirect, + ); } List _buildSepoliaButtons(String address, String chainId) { diff --git a/example/dapp/pubspec.yaml b/example/dapp/pubspec.yaml index 5c60bea9..d788f4ab 100644 --- a/example/dapp/pubspec.yaml +++ b/example/dapp/pubspec.yaml @@ -16,8 +16,6 @@ dependencies: qr_flutter: ^4.0.0 json_annotation: ^4.8.1 fl_toast: ^3.1.0 - url_launcher: ^6.2.2 - # intl: ^0.19.0 package_info_plus: ^7.0.0 walletconnect_modal_flutter: ^2.1.20 diff --git a/example/dapp/web/index.html b/example/dapp/web/index.html index 3bcca361..267bfd2d 100644 --- a/example/dapp/web/index.html +++ b/example/dapp/web/index.html @@ -38,10 +38,6 @@ - - - -