---
updatedAt: 2026-03-31T09:17:24.000Z
agentTools:
  projectIndex: https://docs.zerohash.com/llms.txt
---

# Flutter

### 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 `FlutterWebView`when 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

```dart
// 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(),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```

<br />

## Fiat Account Link

This guide explains how to implement embedded browser authentication for Fiat Account Link using Plaid OAuth flows in your Flutter app on both iOS and Android platforms.

### Overview

When your zerohash SDK WebView initiates a Plaid authentication flow, the Plaid URL will be opened in a secure embedded browser:

* **iOS**: Using `ASWebAuthenticationSession` via MethodChannel
* **Android**: Using `flutter_inappwebview` InAppBrowser class

### What This Does

* Detects when users navigate to `plaid.com` URLs in your\`WebView
* Launches native iOS `ASWebAuthenticationSession` or `flutter_inappwebview` instead of loading in WebView
* Automatically closes the authentication session when it detects completion
* Provides better user experience with native iOS/Android authentication UI

### Dependencies

Add to your `pubspec.yaml`:

```yaml
dependencies:
  webview_flutter: ^4.0.0
  webview_flutter_android: ^3.0.0
  webview_flutter_wkwebview: ^3.0.0
  flutter_inappwebview: ^6.0.0  # Required for Android
  url_launcher: ^6.0.0
```

### Step 1: Create InAppBrowser Class for Android

Add this class to handle Android embedded browser with auto-close:

```dart
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class MyInAppBrowser extends InAppBrowser {
  final String callbackPattern;

  MyInAppBrowser(this.callbackPattern);

  @override
  Future onLoadStart(url) async {
    print("Started loading: $url");
    // Auto-close when reaching callback URL
    if (url != null && url.toString().contains(callbackPattern)) {
      await close();
    }
  }
}
```

### Step 2: Set Up iOS Native Code

You should update your `AppDelegate.swift` file to include `ASWebAuthenticationSession` support, `MethodChannel` handler, 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:**

1. Import `AuthenticationServices`
2. Add `ASWebAuthenticationPresentationContextProviding` protocol to handle authentication presentation
3. Create `MethodChannel` handler to receive authentication requests from Flutter
4. Implement presentation anchor method to specify where the authentication UI appears\
   Below you can find an example of an `AppDelegate` file with these handlers. Feel free to adjust to your needs:

```swift
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()
  }
}
```

### Step 3: Handle Plaid URLs in NavigationDelegate

```java Dart
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

class WebViewScreenState extends State<WebViewScreen> {
  WebViewController? controller;
  bool browserOpened = false;

  void setupController() {
    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(
        NavigationDelegate(
          onNavigationRequest: (NavigationRequest request) async {
            final uri = Uri.parse(request.url);
            // Check if the URL contains plaid.com
            if (uri.host.contains('plaid.com')) {
              if (browserOpened) {
                print('Browser already opened');
                return NavigationDecision.prevent;
              }
              String cleanUrl = request.url
                  .replaceAll('?openInExternalBrowser=true', '')
                  .replaceAll('&openInExternalBrowser=true', '');
              browserOpened = true;  // Mark as opened
              try {
                if (Platform.isIOS) {
                  const platform = MethodChannel('com.example.app/auth_session');
                  final result = await platform.invokeMethod('startAuthSession', {
                    'url': cleanUrl,
                    'callbackURLScheme': 'yourappscheme',  // Use your app's URL scheme
                  });
                } else if (Platform.isAndroid) {
                  final browser = MyInAppBrowser('web-sdk.zerohash.com');
                  await browser.openUrlRequest(
                    urlRequest: URLRequest(url: WebUri(cleanUrl)),
                    settings: InAppBrowserClassSettings(
                      browserSettings: InAppBrowserSettings(
                        hideUrlBar: false,
                        hideToolbarTop: false,
                      ),
                    ),
                  );
                }
              } catch (e) {
                print('Failed to open browser: $e');
              } finally {
                browserOpened = false;
              }
              return NavigationDecision.prevent;
            }
            // Allow other navigation
            if (request.url.contains('openInExternalBrowser=true')) {
              _launchURL(request.url);
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
        ),
      )
  }
}
```