Skip to content

iOS Integration

Gift Card Store - iOS WebView integration

The iOS integration uses a WKWebView to render the Hubble WebView, enabling the integration of Hubble’s SDK into your iOS application. This document provides a step-by-step guide for integrating and using the Hubble WebView along with detailed descriptions of the primary functions.

Parameters

ParameterRequiredDescription
clientIdYesClient ID provided by the Hubble team
appSecretYesApp secret provided by the Hubble team
tokenNoAuth token that uniquely identifies your user. Optional if lazy login is enabled.
wrap-pltYesPlatform identifier. Use ios.
appVersionNoApp version string. Defaults to "10000".
deviceIdNoDevice identifier.

To navigate directly to a specific page, append the route path before the query parameters. See the Deeplinks page for the full route mapping.

PageURL Format
Home (default)https://vouchers.myhubble.money/sdk/gc/?clientId=...
Brand purchasehttps://vouchers.myhubble.money/sdk/gc/buy/{brandId}?clientId=...
Searchhttps://vouchers.myhubble.money/sdk/gc/search?clientId=...
Transactionshttps://vouchers.myhubble.money/sdk/gc/transactions?clientId=...
Helphttps://vouchers.myhubble.money/sdk/gc/help?clientId=...

Events

The SDK communicates with the host app via a WKScriptMessageHandler registered with the name bridge. Events are JSON objects with a type field:

Action events — SDK lifecycle and navigation:

{"type": "action", "action": "close"}
{"type": "action", "action": "app_ready"}
{"type": "action", "action": "error"}

Analytics events — user interaction tracking:

{"type": "analytics", "event": "event_name", "properties": {"key": "value"}}
ActionDescription
closeUser tapped close. Dismiss the WebView.
app_readySDK loaded successfully.
errorSDK failed to load. Show an error state.

See the Events page for the full list of analytics events.

Common Issue: Events Not Reaching Analytics If you are not seeing SDK events in your analytics dashboard, check the following:

  1. Verify your event listener is set up BEFORE loading the SDK (events fire immediately on load).
  2. On native platforms, ensure the JavaScript bridge is registered before the WebView loads the URL.
  3. Check for CORS issues if your analytics provider requires domain whitelisting.
  4. On iOS, events sent via WKScriptMessageHandler are delivered on the main thread. If your analytics call blocks the main thread, events may be dropped.

UPI Intent Configuration

The iOS Limitation

On iOS, a WebView cannot detect which UPI apps are installed on the user’s device. This is a platform restriction that cannot be bypassed. Because of this limitation, the SDK displays icons for the most popular UPI apps (Google Pay, PhonePe, Paytm, CRED, BHIM) regardless of whether they are actually installed. When the user taps an app icon, the system attempts to open that app - if it is not installed, the intent will fail silently or show a system error.

Passing Installed UPI Apps

To provide a better 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 that are actually available on the device, instead of showing all popular apps.

To detect installed UPI apps, your app must declare the URL schemes it wants to query. Add the following to your app’s Info.plist under the LSApplicationQueriesSchemes key:

<key>LSApplicationQueriesSchemes</key>
<array>
<string>phonepe</string>
<string>tez</string>
<string>paytmmp</string>
<string>cred</string>
<string>bhim</string>
</array>

With these schemes declared, your app can use UIApplication.shared.canOpenURL() to check which UPI apps are installed, and then pass the list of installed app identifiers to the SDK. The SDK will filter its payment UI to show only those apps.

Why This Matters: Without passing the installed apps list, users may tap a UPI app icon only to find it is not installed on their device. This creates a poor payment experience. Passing the installed apps list eliminates this friction.

iOS WebView Background Behavior

When a user taps a UPI app (e.g., Google Pay) in the SDK, they leave your app to complete the payment. On iOS, the WKWebView may suspend JavaScript execution while your app is in the background. This means the SDK’s payment status polling can pause.

When the user returns to your app after completing the payment in the UPI app, the WebView resumes and the SDK picks up polling again. In most cases, this works fine. However, if the WKWebView has been suspended for a long time, the payment status screen may appear stuck for a few seconds while it catches up.

Tip: Listen for the app returning to the foreground (applicationDidBecomeActive or sceneDidBecomeActive) and reload the WebView if the payment screen appears stuck. This forces the SDK to re-check the payment status immediately.

Usage

1. Setup the WebView

Use WKWebView to integrate Hubble’s WebView. Customize the WKWebView configuration for message handling and navigation.

var webview: WKWebView!
override func loadView() {
let userContentController = WKUserContentController()
userContentController.add(self, name: "bridge") // web to native message handler
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController // Set content controller
webview = WKWebView(frame: .zero, configuration: configuration)
webview.navigationDelegate = self
webview.uiDelegate = self
view = webview
}

2. Initialization

Initialize the HubbleWebViewController with required parameters.

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

Load the Hubble WebView with the proper URL.

override func viewDidLoad() {
var components = URLComponents(string: "https://vouchers.myhubble.money/sdk/gc/")!
// For dev: use "https://vouchers.dev.myhubble.money/sdk/gc/"
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
}

To open a specific page directly (deep link), change the URL path:

// Brand purchase page
var components = URLComponents(string: "https://vouchers.myhubble.money/sdk/gc/buy/uber")!
// Search page
var components = URLComponents(string: "https://vouchers.myhubble.money/sdk/gc/search")!
// Transactions page
var components = URLComponents(string: "https://vouchers.myhubble.money/sdk/gc/transactions")!

4. Handling Navigation

Control navigation within the WebView and redirect external links to the default browser.

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
let baseUrl = URL(string: "https://vouchers.myhubble.money")!
// For dev: use "https://vouchers.dev.myhubble.money"
let paymentGatewayUrl = URL(string: "https://api.razorpay.com")!
if urlMatchesBaseUrl(url, baseUrl: baseUrl) {
decisionHandler(.allow)
} else if urlMatchesBaseUrl(url, baseUrl: paymentGatewayUrl) {
decisionHandler(.allow)
} else {
openURLExternally(url)
decisionHandler(.cancel)
}
}
private func urlMatchesBaseUrl(_ url: URL, baseUrl: URL) -> Bool {
return url.absoluteString.hasPrefix(baseUrl.absoluteString)
}
private func openURLExternally(_ url: URL) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}

5. Handling Events

Add WKScriptMessageHandler to HubbleWebViewController to receive events from the SDK.

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] ?? [:]
print("Analytics event: \(event), properties: \(properties)")
}
}

This integration ensures seamless embedding of Hubble WebView into your iOS application while maintaining proper control over navigation and event handling. Below is the complete iOS snippet for better understanding.

import UIKit
import WebKit
class HubbleWebViewController: UIViewController, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler {
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] ?? [:]
print("Analytics event: \(event), properties: \(properties)")
}
}
private var clientId: String
private var appSecret: String
private var token: String?
init(clientId: String, appSecret: String, token: String? = nil) {
self.clientId = clientId
self.appSecret = appSecret
self.token = token
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var webview: WKWebView!
override func loadView() {
let userContentController = WKUserContentController()
userContentController.add(self, name: "bridge")
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
webview = WKWebView(frame: .zero, configuration: configuration)
webview.navigationDelegate = self
webview.uiDelegate = self
view = webview
webview.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
}
override func viewDidLoad() {
var components = URLComponents(string: "https://vouchers.myhubble.money/sdk/gc/")!
// For dev: use "https://vouchers.dev.myhubble.money/sdk/gc/"
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
}
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if let frame = navigationAction.targetFrame,
frame.isMainFrame {
return nil
}
webView.load(navigationAction.request)
return nil
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
let baseUrl = URL(string: "https://vouchers.myhubble.money")!
// For dev: use "https://vouchers.dev.myhubble.money"
let paymentGatewayUrl = URL(string: "https://api.razorpay.com")!
if urlMatchesBaseUrl(url, baseUrl: baseUrl) {
decisionHandler(.allow)
} else if urlMatchesBaseUrl(url, baseUrl: paymentGatewayUrl) {
decisionHandler(.allow)
} else {
openURLExternally(url)
decisionHandler(.cancel)
}
}
private func urlMatchesBaseUrl(_ url: URL, baseUrl: URL) -> Bool {
return url.absoluteString.hasPrefix(baseUrl.absoluteString)
}
private func openURLExternally(_ url: URL) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}