iOS Integration Guide
This guide covers how to integrate the Hubble SDK into an iOS application using WKWebView.
1. Setup the WebView
Use WKWebView with a WKUserContentController configured for the message bridge:
iOS offers two WebView options: WKWebView and SFSafariViewController. You must use WKWebView for the Hubble SDK integration. Using SFSafariViewController will cause Hubble's fraud detection checks to fail, resulting in blocked transactions and a broken user experience.
var webview: WKWebView!
override func loadView() {
let userContentController = WKUserContentController()
userContentController.add(self, name: "bridge") // web-to-native handler
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
webview = WKWebView(frame: .zero, configuration: configuration)
webview.navigationDelegate = self
webview.uiDelegate = self
view = webview
}
The SDK sends messages to a handler named "bridge". If you register with a different name, events will not be received.
2. Initialization
Initialize the view controller with your credentials:
| Parameter | Required | Description |
|---|---|---|
clientId | Yes | Client ID provided by the Hubble team |
appSecret | Yes | App secret provided by the Hubble team |
token | Conditional | SSO token for the current user. Required unless lazy login is enabled. |
appVersion | No | App version string. Defaults to "10000". |
deviceId | No | Device identifier for analytics. |
For iOS, set wrap-plt=ios.
init(clientId: String, appSecret: String, token: String? = nil) {
self.clientId = clientId
self.appSecret = appSecret
self.token = token
super.init(nibName: nil, bundle: nil)
}
3. Load the WebView
Build the URL and load it:
override func viewDidLoad() {
var components = URLComponents(string: "https://sdk.myhubble.money/")!
// For dev: use "https://sdk.dev.myhubble.money/"
var queryItems = [
URLQueryItem(name: "clientId", value: clientId),
URLQueryItem(name: "appSecret", value: appSecret),
URLQueryItem(name: "wrap-plt", value: "ios"),
]
if let token = token {
queryItems.append(URLQueryItem(name: "token", value: token))
}
components.queryItems = queryItems
webview.load(URLRequest(url: components.url!))
webview.allowsBackForwardNavigationGestures = true
webview.configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
}
4. Handling Navigation
This configuration is what enables your app to open the UPI app — without it, it will fail to do so.
Implement decidePolicyFor navigationAction to keep Hubble and payment gateway URLs in the WebView, while handing every other URL — UPI schemes (upi://, phonepe://, tez://), bank pages, external links — to the system:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
if isInternalHost(url.host) {
decisionHandler(.allow) // load inside the SDK webview
return
}
UIApplication.shared.open(url) // UPI apps, bank pages, external browser
decisionHandler(.cancel)
}
private func isInternalHost(_ host: String?) -> Bool {
guard let host = host else { return false }
return host == "myhubble.money" || host.hasSuffix(".myhubble.money")
|| host == "razorpay.com" || host.hasSuffix(".razorpay.com")
}
Match on the host, not the full URL. This covers both sdk.myhubble.money and sdk.dev.myhubble.money without an env switch, and — critically — allows Razorpay through. The earlier version of this snippet matched only the Hubble base URL, which sent the payment gateway to Safari and broke the flow: the user completes payment outside the WebView and the SDK never sees the result.
Enable swipe-to-go-back navigation with: webview.allowsBackForwardNavigationGestures = true
5. Handling Events
The SDK communicates with your application by sending events. There are two types:
Action Events
SDK lifecycle and navigation:
| Action | When It Fires | What You Should Do |
|---|---|---|
app_ready | SDK has finished loading | Show the WebView / iframe. Hide your loading spinner. |
close | User tapped the close or back button in the SDK | Dismiss the WebView / iframe. Navigate the user back. |
error | SDK failed to load (invalid credentials, network error, SSO failure) | Hide the WebView. Show a user-friendly error with a retry option. |
The close action is the only way the SDK tells your application that the user wants to leave. If you do not handle it, the user will be stuck inside the SDK with no way to navigate back. This is one of the most common integration issues.
Analytics Events
User interaction tracking:
{ "type": "analytics", "event": "payment_success", "properties": { "amount": 500 } }
Forward analytics events to your analytics provider (Mixpanel, CleverTap, Amplitude, etc.) to track SDK usage.
For a complete list of events, see the Full Events Reference.
Setting Up the Event Bridge
Conform to WKScriptMessageHandler and parse incoming JSON messages:
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
guard let body = message.body as? String,
let data = body.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let type = json["type"] as? String else { return }
if type == "action" {
let action = json["action"] as? String
if action == "close" {
self.dismiss(animated: true)
} else if action == "app_ready" {
print("Hubble SDK is ready")
} else if action == "error" {
print("Hubble SDK failed to load")
}
} else if type == "analytics" {
let event = json["event"] as? String ?? ""
let properties = json["properties"] as? [String: Any] ?? [:]
// Forward to your analytics provider
}
}
6. Payment Configuration
The Hubble SDK supports UPI and credit/debit card payments. Credit/debit card payments are not enabled by default - contact Hubble support to enable them.
UPI on iOS
This configuration is what enables your app to open the UPI app — without it, it will fail to do so.
On iOS, a WebView cannot detect which UPI apps are installed on the user's device. This is a platform restriction. Because of this, the SDK displays icons for popular UPI apps (Google Pay, PhonePe, Paytm, CRED, BHIM) regardless of whether they are actually installed.
Passing Installed UPI Apps (Recommended)
To improve the user experience, your native iOS app can detect which UPI apps are installed and pass this list to the SDK. The SDK will then display only the UPI apps available on the device.
Add the following to your app's Info.plist under LSApplicationQueriesSchemes:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>phonepe</string>
<string>tez</string>
<string>paytmmp</string>
<string>cred</string>
<string>bhim</string>
</array>
With these schemes declared, use UIApplication.shared.canOpenURL() to check which apps are installed and pass the list to the SDK.
Without passing the installed apps list, users may tap a UPI app icon only to find it is not installed. This creates a poor payment experience.
Both pieces are required and neither works without the other: LSApplicationQueriesSchemes lets your app see which UPI apps are installed, and the navigation handling in Section 4 is what actually opens them.
For the full list of URL schemes to declare, see the Payment Configuration guide.
Share your equivalent of the two snippets in this guide — your decidePolicyFor navigationAction implementation and your LSApplicationQueriesSchemes array — with the Hubble team. These are the first things we check when UPI apps do not open.
iOS WebView Background Behavior
When a user taps a UPI app, they leave your app to complete the payment. iOS may suspend the WKWebView's JavaScript execution while your app is in the background. When the user returns, the SDK resumes polling. In most cases this works fine, but the payment screen may appear stuck for a few seconds.
Listen for applicationDidBecomeActive or sceneDidBecomeActive and reload the WebView if the payment screen appears stuck.
Best Practices
- Wrap in a SafeAreaView: Always wrap the WebView in a safe-area-aware container so content is not hidden behind the notch or status bar. In UIKit, pin the WKWebView's constraints to
view.safeAreaLayoutGuide; in SwiftUI, keep the WebView inside the default safe area (do not apply.ignoresSafeArea()). - Remove the script message handler: In
deinit, calluserContentController.removeScriptMessageHandler(forName: "bridge")to avoid memory leaks. - Create a fresh WebView each time: Do not cache or reuse the WebView between sessions.
- Show a loading state: Keep the WebView hidden until
app_readyfires. - Handle new window requests: Implement
createWebViewWithin yourWKUIDelegateto handle payment gateways that open in a new window.