You can quickly integrate Sardine Crypto On-ramp fiat via a mobile interface. For Mobile URL Webview implemenations, the checkout widget is generated via URL.
***Goal
By the end, you should be able to embed Sardine On-ramp into your mobile app through a WebView
Our Risk SDK is a mandatory requirement to help us fight fraud via device intelligence and behavior biometrics. Sardine’s proprietary technology is adept at this very task and is instrumental in identifying risky devices, behavior, and tools that are used during these sessions (example: VPNs, emulators, remote desktop protocols etc.)
By default, if no parameters are passed, users will be redirected to our standard checkout widget.
client_token is the only required parameter. Additional parameters that are passed will autofill the widget for the user and allow them to skip some screens.
Once the URL has been generated, it can be embedded into your web app as a hyperlink. Below, we have included samples of different ways of integrating the checkout
To assist with development, we have created some code samples which illustrate how your code can be structured to create the checkout url and handle the finished trade.
allowsInlineMediaPlayback should be enabled either via code or through storyboard
Start by importing Webview
Craft the URL based on preferred parameters
Create event handlers that will catch the processed,expired and declined events
Call the WebView component with parameters set as such. These have been set for the optimal user expereince.
Full code should look as such
import { WebView } from ‘react-native-webview’; const uri = "https://crypto.sandbox.sardine.ai/?address=0xjadadhadskaj2123&fiat_amount=1000&&asset_type=usdc&network=ethereum&client_token=123-asd-456"; // URL prepared from above step const javaScriptFunction = ` document.addEventListener('processed', function(data) { const d = data.detail; const v = d ? JSON.stringify(d) : ""; window.ReactNativeWebView && window.ReactNativeWebView.postMessage(v); }); document.addEventListener('expired', function(data) { const d = data.detail; const v = d ? JSON.stringify(d) : ""; window.ReactNativeWebView && window.ReactNativeWebView.postMessage(v); }); document.addEventListener('declined', function(data) { const d = data.detail; const v = d ? JSON.stringify(d) : ""; window.ReactNativeWebView && window.ReactNativeWebView.postMessage(v); }); ` if (uri) { let cryptoURL = uri; if(Platform.OS == "android") { cryptoURL = `${uri}&android_package_name=com.your-app-pakage-name` } else if(Platform.OS == "ios") { cryptoURL = `${uri}&plaid_redirect_url=https://your-domain-here.com` } return ( <View style={baseStyles.flexGrow}> <WebView ref={(webView) => this.webView = webView} source={{ uri: cryptoURL }} mediaPlaybackRequiresUserAction={false} allowInlineMediaPlayback allowsBackForwardNavigationGestures javaScriptEnabled={true} injectedJavaScript={javaScriptFunction} originWhitelist={[‘*’]} onMessage={event => { const orderData = JSON.parse(event.nativeEvent.data); if(orderData) { setTimeout(() => { // handle code for order success this.handleOrderSuccess(orderData) }, 2000); } }} /> </View> ); }
Start by importing Webview
Craft the URL based on preferred parameters
Create event handlers that will catch the processed,expired and declined events
Call the WebView component with parameters set as such. These have been set for the optimal user expereince.
Full code should look as such
import { WebView } from ‘react-native-webview’; const uri = "https://crypto.sandbox.sardine.ai/?address=0xjadadhadskaj2123&fiat_amount=1000&&asset_type=usdc&network=ethereum&client_token=123-asd-456"; // URL prepared from above step const javaScriptFunction = ` document.addEventListener('processed', function(data) { const d = data.detail; const v = d ? JSON.stringify(d) : ""; window.ReactNativeWebView && window.ReactNativeWebView.postMessage(v); }); document.addEventListener('expired', function(data) { const d = data.detail; const v = d ? JSON.stringify(d) : ""; window.ReactNativeWebView && window.ReactNativeWebView.postMessage(v); }); document.addEventListener('declined', function(data) { const d = data.detail; const v = d ? JSON.stringify(d) : ""; window.ReactNativeWebView && window.ReactNativeWebView.postMessage(v); }); ` if (uri) { let cryptoURL = uri; if(Platform.OS == "android") { cryptoURL = `${uri}&android_package_name=com.your-app-pakage-name` } else if(Platform.OS == "ios") { cryptoURL = `${uri}&plaid_redirect_url=https://your-domain-here.com` } return ( <View style={baseStyles.flexGrow}> <WebView ref={(webView) => this.webView = webView} source={{ uri: cryptoURL }} mediaPlaybackRequiresUserAction={false} allowInlineMediaPlayback allowsBackForwardNavigationGestures javaScriptEnabled={true} injectedJavaScript={javaScriptFunction} originWhitelist={[‘*’]} onMessage={event => { const orderData = JSON.parse(event.nativeEvent.data); if(orderData) { setTimeout(() => { // handle code for order success this.handleOrderSuccess(orderData) }, 2000); } }} /> </View> ); }
import UIKitimport WebKitclass CryptoWebController: UIViewController, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler { // Private variables private var webView : WKWebView? private var cryptoAddress = "" private var fiatAmount = "" private var assetType = "" private var network = "" private var supportedTokens = "" private var clientToken = "" // Error response private let errorResponse = CryptoResponse(status: false, data: nil) // Public variables public var completion : ((CryptoResponse)->())? // Constants private enum CONSTANTS : String { case STATUS = "orderStatus" case SUCCESS = "order-success" case FAILURE = "order-fail" case TITLE = "Crypto ACH" case DISMISS = "Dismiss" case ALERT_TITLE = "Confirmation" case ALERT_MESSAGE = "Are you sure you want to dismiss?" case ACTION_YES = "Yes" case ACTION_NO = "No" } convenience init(withAddress address: String, fiatAmount: String, assetType: String, network: String, supportedTokens: String, clientToken: String) { self.init() self.cryptoAddress = address self.fiatAmount = fiatAmount self.assetType = assetType self.network = network self.supportedTokens = supportedTokens self.clientToken = clientToken } override func viewDidLoad() { super.viewDidLoad() self.title = CONSTANTS.TITLE.rawValue self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: CONSTANTS.DISMISS.rawValue, style: .plain, target: self, action: #selector(dismissAction)) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) guard let baseURL = URL(string:"https://crypto.sandbox.sardine.ai") else { return } let scheme = baseURL.scheme let host = baseURL.host let path = "/alpha/6ccfb278-5f94-44fe-bf33-4ea81325713b" let queryItems = [ URLQueryItem(name: "address", value: cryptoAddress), URLQueryItem(name: "fiat_amount", value: fiatAmount), URLQueryItem(name: "asset_type", value: assetType), URLQueryItem(name: "network", value: network), URLQueryItem(name: "supported_tokens", value: supportedTokens), URLQueryItem(name: "client_token", value: clientToken), URLQueryItem(name: "plaid_redirect_url", value: "https://your-domain-here.com"), ] var urlComponents = URLComponents() urlComponents.scheme = scheme urlComponents.host = host urlComponents.path = path urlComponents.queryItems = queryItems guard let cryptoURL = urlComponents.url else { return } let cryptoRequest = URLRequest(url: cryptoURL) let preferences = WKPreferences() let contentController = WKUserContentController() preferences.javaScriptEnabled = true let source: String = """ window.onload = function() { window.document.addEventListener("\(CONSTANTS.SUCCESS.rawValue)", function(data) { const d = data.detail; const v = d ? JSON.stringify(d) : ""; const val = '"' + `${v}` + '"'; window.webkit.messageHandlers.\(CONSTANTS.STATUS.rawValue).postMessage(`${val}`); }); window.document.addEventListener("\(CONSTANTS.FAILURE.rawValue)", function(data) { window.webkit.messageHandlers.\(CONSTANTS.STATUS.rawValue).postMessage("\(CONSTANTS.FAILURE.rawValue)"); }); } """ let script: WKUserScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true) contentController.addUserScript(script) contentController.add(self, name: CONSTANTS.STATUS.rawValue) let configuration = WKWebViewConfiguration() configuration.preferences = preferences configuration.userContentController = contentController self.webView = WKWebView(frame: self.view.bounds, configuration: configuration) self.webView!.allowsBackForwardNavigationGestures = true self.webView!.uiDelegate = self self.webView!.navigationDelegate = self self.view = webView self.webView!.load(cryptoRequest) } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { completion?(errorResponse) } @objc private func dismissAction() { let alert = UIAlertController(title: CONSTANTS.ALERT_TITLE.rawValue, message: CONSTANTS.ALERT_MESSAGE.rawValue, preferredStyle: .alert) alert.addAction(UIAlertAction(title: CONSTANTS.ACTION_YES.rawValue, style: .cancel, handler: { _ in self.navigationController?.dismiss(animated: true, completion: nil) self.completion?(self.errorResponse) })) alert.addAction(UIAlertAction(title: CONSTANTS.ACTION_NO.rawValue, style: .default, handler: nil)) self.present(alert, animated: true, completion: nil) } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { switch message.name { case CONSTANTS.STATUS.rawValue: let isFailure = "\(message.body)" == CONSTANTS.FAILURE.rawValue if !isFailure { DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.navigationController?.dismiss(animated: true, completion: nil) let stringValue = String("\(message.body)".dropFirst().dropLast()) var successResponse = CryptoResponse(status: true, data: nil) guard let strData = stringValue.data(using: .utf8) else { self.completion?(successResponse) return } guard let d = try? JSONDecoder().decode(CryptoDetails.self, from: strData) else { self.completion?(successResponse) return } successResponse.data = d self.completion?(successResponse) } } default: break } }}class CryptoManager { public class func initiateCheckout(withAddress address: String, fiatAmount: String, assetType: String, network: String, supportedTokens: String, clientToken: String, completion : @escaping ((CryptoResponse)->())) { DispatchQueue.main.async { let cryptoVC = CryptoWebController(withAddress: address, fiatAmount: String, assetType: String, network: String, supportedTokens: String, clientToken: String) cryptoVC.completion = completion let navigationVC = UINavigationController(rootViewController: cryptoVC) navigationVC.modalPresentationStyle = .fullScreen self.present(navigationVC, animated: true, completion: nil) } }}
Sardine can fire off events that can be caught by event handlers that can be leveraged to update the user on the order status outside of the Sardine widget
There are two ways to get Order status, either via polling an orders endpoint or providing a redirect URL that we will send the events to
Developers can poll to the /orders endpoint to check its status with the order_id
There may be a delay between when an order was successful and the crypto is delivered to the user’s wallet. This delay is due to the time required for transactions to settle on the chain.
Once you have completed end-to-end testing in sandbox, you can go live by swapping to a set of production keys and updating your URLs. Please see the linked guide for a step-by-step of going live in production.