iOS SDK
Present the Qasimha hosted checkout from an iOS app with the QasimhaCheckout Swift package and handle the result.
QasimhaCheckout is the Swift library your iOS app embeds to accept Qasimha. Your server creates the checkout session and hands the app the paymentUrl; the SDK presents it, watches for the return to your app and reports success, failure or cancellation. It holds no secret key, stores nothing on the device, sends no analytics and has no third-party dependencies.
Requirements
| Item | Requirement |
|---|---|
| iOS | 14.0 or later |
| Swift | 5.9 or later |
| Package managers | Swift Package Manager or CocoaPods |
| Frameworks used | UIKit, SafariServices, AuthenticationServices, WebKit |
| Server | A Qasimha server SDK that creates the checkout session. See Checkout flows. |
Install
In Xcode: File ▸ Add Package Dependencies…, enter the URL of the QasimhaCheckout release repository and choose the QasimhaCheckout product. Or in Package.swift, replacing PACKAGE_URL with that repository URL:
dependencies: [
.package(url: "PACKAGE_URL", from: "0.1.0"),
],
targets: [
.target(name: "MyApp", dependencies: [.product(name: "QasimhaCheckout", package: "ios-sdk")]),
]pod 'QasimhaCheckout', '~> 0.1'Set up the return into your app
How the return reaches the app depends on the URLs you choose. If you use .webAuthenticationSession or .webView presentation, iOS or the SDK catches the return itself and you can skip this section.
Use https:// URLs on a domain your app is associated with. No other app can claim them.
- Add the Associated Domains capability. In the entitlements file:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:shop.example</string>
</array>- Serve
https://shop.example/.well-known/apple-app-site-association(JSON, no extension,application/json):
{
"applinks": {
"details": [
{ "appIDs": ["TEAMID.com.example.myapp"], "components": [ { "/": "/qasimha/*" }, { "/": "/cart" } ] }
]
}
}- Hand every incoming URL to the SDK:
WindowGroup {
ContentView().onOpenURL { url in
_ = QasimhaCheckout.handleReturn(url: url)
}
}func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else { return }
_ = QasimhaCheckout.handleReturn(url: url)
}func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard let url = userActivity.webpageURL else { return false }
return QasimhaCheckout.handleReturn(url: url)
}Use myshop://qasimha/return style URLs when you cannot host an association file. Any app can register the same scheme, so prefer Universal Links in production.
- Register the scheme in
Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>checkout-return</string>
<key>CFBundleURLSchemes</key>
<array><string>myshop</string></array>
</dict>
</array>- Deliver the URL. SwiftUI's
.onOpenURLreceives both custom-scheme and Universal Link opens.
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for context in URLContexts {
_ = QasimhaCheckout.handleReturn(url: context.url)
}
}func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
QasimhaCheckout.handleReturn(url: url)
}handleReturn(url:) returns true when the URL belongs to the checkout in progress; it then closes the sheet and calls your completion. It ignores every other URL.
Start a checkout
Get paymentUrl from your server, then present it with the same three return URLs your server sent when creating the session. cancel defaults to failure when omitted.
import QasimhaCheckout
let options = CheckoutOptions(
paymentURL: session.paymentUrl,
merchantURL: MerchantURL(
success: URL(string: "https://shop.example/qasimha/return?order=1001")!,
failure: URL(string: "https://shop.example/qasimha/return?order=1001")!,
cancel: URL(string: "https://shop.example/cart")!
),
language: "ar"
)
QasimhaCheckout.start(from: viewController, options: options) { result in
switch result {
case .success(let orderId, let orderReferenceId):
// Never trust the return alone: ask your server, which calls orders.get(orderId).
myServer.confirm(orderId) { order in show(order.status) }
case .failure(let status, let orderId, _):
show("not paid: \(status)")
case .cancelled:
show("cancelled")
}
}language ("ar" default, "en") is appended to the payment URL as lang when the URL does not already carry one. Only one checkout can be open at a time.
Presentation modes
Set CheckoutOptions.presentation:
| Mode | What happens | Use it when |
|---|---|---|
.safariViewController (default) | SFSafariViewController sheet with the platform's TLS, autofill and cookie isolation. The return comes back through handleReturn(url:). Dismissing the sheet gives .cancelled. | Almost always. |
.webAuthenticationSession(callbackScheme:) | ASWebAuthenticationSession in an ephemeral browser session. iOS hands the SDK the first navigation to callbackScheme://… without leaving the app. The scheme must be your merchant URLs' scheme. | You use a custom scheme and want no app-delegate wiring. |
.webView | A modal hardened WKWebView inside your app. | Only when the checkout must not leave the app and Qasimha has confirmed every host the journey visits. |
The WebView uses a non-persistent data store, no script message handlers or JavaScript bridges, platform TLS handling (never overridden), and cancels every HTTP authentication challenge. It only loads https hosts on the allow-list: the payment URL's host, your return hosts and options.webView.extraAllowedHosts (empty by default). Payment provider and identity hosts are not known to the SDK, so a WebView journey dead-ends unless you add them.
Handle the result and confirm on your server
status on the return URL | Result |
|---|---|
confirmed on the success URL, with orderId | .success(orderId:orderReferenceId:) |
declined, expired, failed | .failure(status:orderId:orderReferenceId:) |
cancelled, or missing on the cancel URL | .cancelled |
| anything else, or missing elsewhere | .failure(status: "unverified", …) |
SDK failure statuses: unverified (the return could not be read as a confirmation), invalid_options (validation failed before presenting) and already_presenting (a checkout is already open). .cancelled also covers a dismissed sheet, a cancelled ASWebAuthenticationSession and a WebView load that failed closed.
.success means the shopper came back on the success URL. It does not mean money moved. Before fulfilling, showing "paid" or clearing the cart, your server fetches the order with its secret key (GET /api/v1/merchant/orders/{orderId}, orders.get in the server SDKs) and compares status, amount and orderReferenceId. Your server also receives the webhook. A cancelled order stays AwaitingCheckout and expires on its own; your server may cancel it.
Promo view
QasimhaPromoView shows "Split into 4 monthly payments of [riyal symbol] 300" with your publishable key.
let promo = QasimhaPromoView(amount: 1200, publicKey: "pk_test_...", lang: "ar")
view.addSubview(promo)- Set
amount,publicKey,languageorapiBaseURLand the view reloads.font,textColorandaccentColorare adjustable. - Calls
GET /api/v1/widget/planswithX-Public-Key, a 5-second timeout, no cookies and no cache.pk_test_keys go to UAT,pk_live_keys to production.apiBaseURLis only for a local test server (https, orhttponlocalhost). - Fails closed: any invalid key, out-of-range amount, network error or bad response leaves the view hidden with zero size. It never computes an instalment figure.
Release builds
- Strip symbols in release builds as usual.
- The package includes a privacy manifest (
PrivacyInfo.xcprivacy) declaring no tracking, no collected data and no required-reason API use.
Security
- No secret on the device. The SDK never sees
sk_keys. The publishablepk_key can only read plan quotes. - No storage, tracking or fingerprinting. Nothing is written to disk or
UserDefaults. - TLS is the platform's. No certificate pinning and no option to weaken validation.
- PCI DSS: card entry happens on the payment provider's hosted page. You stay in SAQ-A scope.
- App hardening: the SDK does not do jailbreak detection, anti-tamper or anti-debugging. Apps that handle payments should adopt an application-shielding product or the platform's protections (App Attest,
DeviceCheck) according to their own risk assessment.
Troubleshooting
The app does not open when the checkout finishes
Check the Associated Domains entitlement, that apple-app-site-association is served over https as application/json with no extension, and that its components cover your return paths. Make sure every incoming URL reaches handleReturn(url:) in the delegate your app actually uses (SwiftUI, scene or app delegate).
The sheet stays on the success screen
SFSafariViewController opens a Universal Link into your app only from a user tap. The hosted checkout shows a "Return to store" button on every final screen; the shopper's tap brings the result back. Custom-scheme return URLs and .webAuthenticationSession return without a tap.
Result is .failure with status invalid_options
A merchant URL broke a validation rule: not absolute, over 1024 characters, has a fragment, or uses a refused scheme (http outside localhost, javascript, data, file, blob, vbscript, or a scheme shorter than 3 characters).
Result is .failure with status already_presenting
A checkout is already open. Wait for its completion before calling start again.
Result is .failure with status unverified
The return URL matched but did not carry status=confirmed with an orderId. Ask your server to read the order; it decides the outcome.
The WebView journey ends as .cancelled
A host outside the allow-list or a failed load ends the WebView checkout. Add the hosts Qasimha confirms for your journey to options.webView.extraAllowedHosts, or use the default .safariViewController.
The promo view is not shown
It fails closed. Check the key starts with pk_test_ or pk_live_, the amount is within your plan range, and the device can reach the API.
Related
Updated 3 days ago
