Opening external links
Inside our Apps we might have external links that require the user to navigate to a different page, such as Privacy Policy links, in these cases we add a query parameter to the URL named openInExternalBrowser=true to indicate to our customers that those links must be opened in the user's default browser so that we do not lose context of the application.
For most Apps, you should do just fine by opening external links using an In-App browser, but for the Fund product, it is imperative that you open external links in the user's default browser so that they can have a better UX.
Javascript channel
To use our SDK with Flutter, you must add a JavaScriptChannel named FlutterWebViewwhen using the webview_flutter package. This channel allows our server to communicate with WebViews in Flutter. The example below shows the usage of both the Javascript channel and the external link implementation
// ignore_for_file: avoid_print
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
// https://pub.dev/packages/webview_flutter
import 'package:webview_flutter/webview_flutter.dart';
// https://pub.dev/packages/webview_flutter_android
import 'package:webview_flutter_android/webview_flutter_android.dart';
// https://pub.dev/packages/webview_flutter_wkwebview
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
// https://pub.dev/packages/url_launcher
import 'package:url_launcher/url_launcher.dart';
const String sdkMobileServer = 'https://sdk-mobile.cert.zerohash.com/v1';
const String zeroHashAppsURL = 'https://web-sdk.cert.zerohash.com';
class WebViewScreen extends StatefulWidget {
const WebViewScreen({super.key});
@override
WebViewScreenState createState() => WebViewScreenState();
}
class WebViewScreenState extends State<WebViewScreen> {
String token = "";
late WebViewController controller;
@override
void initState() {
super.initState();
fetchData();
}
// Setup webview controller with the token in the URL
// Do not remove FlutterWebView channel, it is required for communication between webview and webpage
void setupController() {
late final PlatformWebViewControllerCreationParams params;
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
controller = WebViewController.fromPlatformCreationParams(params);
if (controller.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
(controller.platform as AndroidWebViewController).setMediaPlaybackRequiresUserGesture(false);
}
controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('FlutterWebView', onMessageReceived: onMessageReceived)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) async {
print('Request URL: ${request.url}');
if (request.url.contains('openInExternalBrowser=true')) {
_launchURL(request.url);
return NavigationDecision.prevent;
}
print('Navigating to internal URL: ${request.url}');
return NavigationDecision.navigate;
},
),
)
..loadRequest(
Uri.parse('$sdkMobileServer?achDepositsJWT=$token&cryptoBuyJWT=$token&cryptoSellJWT=$token&cryptoWithdrawalsJWT=$token&fiatWithdrawalsJWT=$token&userOnboardingJWT=$token&zeroHashAppsURL=$zeroHashAppsURL')
);
}
// Launch URL in external browser
void _launchURL(String url) async {
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
} else {
throw 'Could not launch $url';
}
}
// Fetch token from the server and setup webview controller with the token
Future<void> fetchData() async {
try {
final url = Uri.parse('<API_URL_TO_FETCH_TOKEN>');
final response = await http.post(url,
body: json.encode(
{
'participant_code': '<PARTICIPANT_CODE>',
'permissions': [
'crypto-buy',
'crypto-sell',
'crypto-withdrawals',
'fiat-deposits',
'fiat-withdrawals',
]
}
)
);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Store the data in the 'token' and setup webview controller
var body = json.decode(response.body);
setState(() {
token = body['message']['token'];
});
setupController();
} else {
print('Failed to fetch data: $response');
}
} catch (e) {
// Handle network or API call errors
print('Failed to fetch data: $e');
}
}
// Handle messages received from the webview
void onMessageReceived(JavaScriptMessage message) {
try {
print('Receving message: ${message.message}');
// Parse the message JSON
Map<String, dynamic> messageJson = jsonDecode(message.message);
if (messageJson.containsKey('type')) {
// Indicates that the SDK mobile is ready to receive postMessage
if (messageJson['type'] == 'SDK_MOBILE_READY') {
print('SDK mobile ready, opening modal');
// Open the modal for the app with the identifier 'crypto-sell'
controller.runJavaScript('window.postMessage({ "type": "OPEN_MODAL", "payload": { "appIdentifier": "crypto-buy"}})');
}
}
} catch (e) {
print('Error parsing JSON: $e');
}
}
Widget _buildChild() {
// If the token is empty, show a loading message
if (token.isEmpty) {
return const Center(
child: Text('Fetching token...'),
);
}
// Return the WebViewWidget with the controller
return WebViewWidget(controller: controller);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
resizeToAvoidBottomInset: false, // Setting resizeToAvoidBottomInset to false ensures that the UI layout remains stable when the keyboard or other insets appear, preventing any unintended resizing that could disrupt the user interface.
body: SafeArea(
child: Column(
children: [
Expanded(
child: _buildChild(),
),
],
),
),
),
);
}
}
Fiat Account link (Android)
For Android the experience should not require any changes and at this first phase the user will be sent to the external browser to complete the Application with Plaid, once he finishes that the user will be presented with a "Success" page asking him to return to the Native Application and continue the flow.
Fiat Account link (iOS ASWebAuthenticationSession)
Overview
This guide explains how to add special handling for Plaid authentication flows in your Flutter iOS app using ASWebAuthenticationSession. This provides a native iOS authentication experience and automatically closes when the flow completes.
What This Does
- Detects when users navigate to plaid.com URLs in your WebView
- Launches native iOS ASWebAuthenticationSession instead of loading in WebView
- Automatically closes the authentication session when it detects completion
- Provides better user experience with native iOS authentication UI
Implementation Steps
- Update
onNavigationRequest
onNavigationRequest: (NavigationRequest request) async {
print('Request URL: ${request.url}');
final uri = Uri.parse(request.url);
// Check if the URL contains plaid.com and the platform is iOS
if (uri.host.contains('plaid.com') && Platform.isIOS) {
try {
const platform = MethodChannel('com.example.app/auth_session');
final result = await platform.invokeMethod('startAuthSession', {
'url': request.url,
'callbackURLScheme': 'zerohashapp', // custom scheme
});
print('ASWebAuthenticationSession completed with: $result');
} catch (e) {
print('Failed to start ASWebAuthenticationSession: $e');
}
return NavigationDecision.prevent;
}
if (request.url.contains('openInExternalBrowser=true')) {
_launchURL(request.url);
return NavigationDecision.prevent;
}
print('Navigating to internal URL: ${request.url}');
return NavigationDecision.navigate;
},
- You should update your
AppDelegate.swiftfile to includeASWebAuthenticationSessionsupport,MethodChannelhandler, and presentation context provider so that it handles Plaid authentication flows with native iOS authentication UI and automatically closes when the authentication completes.
Required Changes:- Import
AuthenticationServices - Add
ASWebAuthenticationPresentationContextProvidingprotocol to handle authentication presentation - Create
MethodChannelhandler to receive authentication requests from Flutter - Implement presentation anchor method to specify where the authentication UI appears
Below you can find an example of anAppDelegatefile with these handlers. Feel free to adjust to your needs:
- Import
import UIKit
import Flutter
import AuthenticationServices
@main
@objc class AppDelegate: FlutterAppDelegate, ASWebAuthenticationPresentationContextProviding {
private var authSession: ASWebAuthenticationSession?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let authChannel = FlutterMethodChannel(name: "com.example.app/auth_session",
binaryMessenger: controller.binaryMessenger)
authChannel.setMethodCallHandler { [weak self] call, result in
switch call.method {
case "startAuthSession":
guard
let args = call.arguments as? [String: Any],
let urlString = args["url"] as? String,
let startURL = URL(string: urlString),
let callbackScheme = args["callbackURLScheme"] as? String
else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing arguments", details: nil))
return
}
if #available(iOS 13.0, *) {
self?.authSession = ASWebAuthenticationSession(url: startURL, callbackURLScheme: callbackScheme) { callbackURL, error in
if let error = error {
result(FlutterError(code: "AUTH_SESSION_ERROR", message: error.localizedDescription, details: nil))
} else {
result("session_completed")
}
}
self?.authSession?.presentationContextProvider = self
self?.authSession?.start()
} else {
result(FlutterError(code: "UNSUPPORTED_IOS", message: "Requires iOS 13+", details: nil))
}
default:
result(FlutterMethodNotImplemented)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return window ?? ASPresentationAnchor()
}
}
