Flutter
With AppKit Flutter, you can easily let people interact with multiple EVM compatible wallets and blockchains.
Let's get started with the installation and configuration!
Installation
-
- Add
reown_appkit
as dependency in yourpubspec.yaml
and runflutter pub get
(check out the latest version) - Or simply run
flutter pub add reown_appkit
- Add
-
- Locate your
/ios/Podfile
file and add the following as the first line:
- Locate your
platform :ios, '13.0'
-
- Run
$ pod install
inside/ios
folder.
- Run
-
- You should now be able to run your app with
flutter run --dart-define=PROJECT_ID={your_project_id}
- You should now be able to run your app with
Enable Installed Wallet Detection
To enable AppKit to detect installed wallets on the device, you need to make specific changes to the native sides of the project.
- iOS
- Android
- Open your
Info.plist
file. - Locate the
<key>LSApplicationQueriesSchemes</key>
section. - Add the desired wallet schemes as string entries within the
<array>
. These schemes represent the wallets you want to detect. - Refer to our Info.plist example file for a detailed illustration.
Example:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>metamask</string>
<string>trust</string>
<string>safe</string>
<string>rainbow</string>
<!-- Add other wallet schemes names here -->
</array>
- Open your
AndroidManifest.xml
file. - Add your
<queries>...</queries>
schemes outside of<application />
scope. - Refer to Android Specs for more information.
Example:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<package android:name="io.metamask"/>
<package android:name="com.wallet.crypto.trustapp"/>
<package android:name="io.gnosis.safe"/>
<package android:name="me.rainbow"/>
<!-- Add other wallet schemes names here -->
</queries>
<application>
...
</application>
</manifest>
Coinbase Wallet support
Coinbase Wallet does not use the WalletConnect protocol for communication between the dApp and the wallet.
This means that pairing topic, session topic, session events and other session-related features are not available when connecting to Coinbase Wallet.
However, you can still enable it to seamlessly connect with your dApp with these additional steps.
If you still want to support it, on your iOS and Android native side make the following changes:
- iOS
- Android
- Open your
Info.plist
file. - Locate the
<key>LSApplicationQueriesSchemes</key>
section. - Include
<string>cbwallet</string>
scheme as mentioned above in previous section
Example:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>cbwallet</string>
<!-- Any other scheme previously added -->
</array>
- Make sure pods are installed, otherwise run
pod install
inside your/ios
folder. - Open your
/ios/Runner.xcworkspace
file with Xcode and add the following code inAppDelegate.swift
file:
import CoinbaseWalletSDK
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if #available(iOS 13.0, *) {
if (CoinbaseWalletSDK.isConfigured == true) {
if (try? CoinbaseWalletSDK.shared.handleResponse(url)) == true {
return true
}
}
}
return super.application(app, open: url, options: options)
}
override func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if #available(iOS 13.0, *) {
if (CoinbaseWalletSDK.isConfigured == true) {
if let url = userActivity.webpageURL,
(try? CoinbaseWalletSDK.shared.handleResponse(url)) == true {
return true
}
}
}
return super.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
Checkout out the AppDelegate.swift file from our sample dapp for reference.
- Open your
AndroidManifest.xml
file. - Add
<package android:name="org.toshi"/>
scheme inside<queries>...</queries>
as mentioned above in previous section
Example:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<package android:name="org.toshi"/>
<!-- Any other scheme previously added -->
</queries>
<application>
...
</application>
</manifest>
Disable Coinbase Wallet
Coinbase Wallet is enabled by default even if, in order to function properly, a few steps has to be done as described in the previous section. However, if you don't want to include/support Coinbase Wallet on your app you just need to pass Coinbase Wallet id fd20dc426fb37566d803205b19bbc1d4096b248ac04548e3cfb6b3a38bd033aa
to excludedWalletIds options Array.
Phantom Wallet support
Phantom Wallet support is available from reown_appkit: ^1.4.0-beta03
and is currently in beta. Any feedback is appreciated.
Phantom Wallet does not use the WalletConnect protocol for communication between the dApp and the wallet, instead it provides an internal API mechanism based on deep/universal links.
This means that pairing topic, session topic, session events and other session-related features are not available when connecting to Phantom Wallet and the interaction is really basic. Dapp sends a request, Phantom Wallet responds. That's all.
Furthermore, Phantom Wallet's deep/universal linking mechanism supports interaction exclusively with the Solana network. This means that if you have EVM networks configured in your AppKit instance, they will not be available for use after connecting with Phantom.
In order to support Phantom Wallet interactions a few extra steps has to be performed (only if you haven't implemented Link Mode already).
- First, be sure you already have your redirection back property configure in your dApp's metadata. See Redirect to your dApp
- Then you will have to implement your own Deep Link mechanism on Flutter (and native) side so when a link is received through it, you would just call
await _appKitModal.dispatchEnvelope(link);
As a guidance, here you can see how it's done in our sample dApp:
First, on Flutter side we create an EventChannel
where links are going to be received and passed to dispatchEnvelope()
.
- Flutter
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:reown_appkit/modal/i_appkit_modal_impl.dart';
class DeepLinkHandler {
static const _eventChannel = EventChannel('com.exampledapp/events');
static late IReownAppKitModal _appKitModal;
static void init(IReownAppKitModal appKitModal) {
if (kIsWeb) return;
try {
_appKitModal = appKitModal;
_eventChannel.receiveBroadcastStream().listen(_onLink, onError: _onError);
} catch (e) {
debugPrint('[SampleDapp] checkInitialLink $e');
}
}
static void _onLink(dynamic link) async {
try {
_appKitModal.dispatchEnvelope(link);
} catch (e) {
print(e);
}
}
static void _onError(dynamic error) {
print(error);
}
}
Then, on both native sides we leverage native APIs to capture the app opening link and send it to Flutter side.
- iOS
- Android
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
private static let EVENTS_CHANNEL = "com.exampledapp/events"
private var eventsChannel: FlutterEventChannel?
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)
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 {
print("App launched with Universal Link: \(url.absoluteString)")
let _ = linkStreamHandler.handleLink(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
}
}
package dapp.example
import io.flutter.embedding.android.FlutterActivity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import io.flutter.plugin.common.EventChannel
class MainActivity: FlutterActivity() {
private val eventsChannel = "com.exampledapp/events"
private var linksReceiver: BroadcastReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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
}
}
)
}
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)
}
}
}
}
Links to full examples:
- Android's platform specific code (only
eventsChannel
is important Phantom wise) - iOS's platform specific code (only
eventsChannel
is important Phantom wise) - Dart's specific code (only
eventsChannel
is important Phantom wise)
Constructing a Solana transaction would depend on the library/package of your choice but in our sample dApp's code you can see how we do it using solana_web3
package.
Disable Phantom Wallet
Phantom Wallet is enabled by default even if, in order to function properly, a few steps has to be done as described in the previous section. However, if you don't want to include/support Phantom Wallet on your app you just need to pass Phantom Wallet id a797aa35c0fadbfc1a53e7f675162ed5226968b44a19ee3d24385c64d1d3c393
to excludedWalletIds options Array.
Example
Test Apps
Want to see AppKit in action? Download our sample AppKit apps below and explore what it can do. Enjoy! 😊