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