iOS SDK
Swift, iOS 15 and newer. Presents YallaPay checkout as a system sheet, confirms the outcome with the API, and returns a typed result. Three calls: configure, pay, and a check on launch for interrupted payments.
On this page
Before you start, your server must be able to create sessions and your dashboard must have a return target registered. If you have not done that yet, read Dashboard setup and Server integration first.
Requirements
| Language | Swift 5.9 or newer. Objective-C apps can call it through a small Swift file. |
| Minimum iOS | 15.0. |
| Frameworks | Foundation, UIKit, AuthenticationServices. No third-party dependencies. |
| Privacy | Ships a privacy manifest. The SDK collects nothing and uses no tracking domains. |
The SDK relies on system web-authentication sessions that older iOS versions do not provide. If your app supports iOS 13 or 14, raise its deployment target; the SDK will not build against a lower one.
Install
Swift Package Manager — in Xcode, File → Add Package Dependencies, or in Package.swift:
.package(url: "https://github.com/yallapay/yallapay-ios.git", from: "0.1.0")
CocoaPods — in your Podfile:
platform :ios, '15.0' pod 'YallaPay', '~> 0.1'
Then import YallaPay where you use it.
While the SDK is in early access the package is provided directly by YallaPay rather than from a public registry. Email [email protected] for repository access; the package URL and pod name above are the ones it publishes under, so nothing in your project changes later.
Declare your return scheme
Checkout returns the customer to your app through a custom URL scheme. Choose one unique to your brand and declare it in Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.shop</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Or in Xcode: target → Info → URL Types → add one with URL Schemes myapp.
Register the same scheme against your merchant account under Apps & Return URLs, and use myapp://yallapay/return as the return URL when your server creates sessions.
The web-authentication session intercepts its own callback, so the SDK would receive the return even with no Info.plist entry. Without it, though, nothing else can reach your app through the scheme: a banking app returning from 3-D Secure, a link in an email, a QR code. The SDK prints a console warning at configure time if the scheme is not declared.
The scheme must be well-formed (letters, digits, plus, hyphen, dot; starting with a letter) and must not be one of the reserved ones: http, javascript, data, file, about, blob, tel, mailto, sms.
Configure
Call configure once, at launch.
import YallaPay
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
YallaPay.configure(
publishableKey: "pk_live_…",
returnURL: "myapp://yallapay/return"
)
return true
}
In a SwiftUI app without an app delegate, do the same in your App initialiser. For debug builds, use the sandbox and a test key:
#if DEBUG YallaPay.configure(publishableKey: "pk_test_…", returnURL: "myapp://yallapay/return", environment: .sandbox) #else YallaPay.configure(publishableKey: "pk_live_…", returnURL: "myapp://yallapay/return", environment: .live) #endif
A malformed or reserved scheme stops the app at launch on your machine, with a message saying what is wrong, rather than producing a session that takes the money and never calls back in front of a customer.
| publishableKey | Your pk_live_ or pk_test_ key. A key beginning with sk_ is a precondition failure. |
| returnURL | Your scheme plus ://yallapay/return. Validated at once. |
| environment | .live (default) or .sandbox. |
| baseURL | Optional host override for debug builds against your own deployment. Leave nil in anything you ship. |
Take a payment
Ask your server for a session id, then present checkout from a view controller. The call is async and returns when the customer finishes, cancels, or something fails.
import YallaPay
final class CheckoutViewController: UIViewController {
func payOrder(_ orderId: String) {
Task { @MainActor in
let sessionId = try await api.createPaymentSession(orderId) // your backend
switch await YallaPay.pay(from: self, sessionID: sessionId) {
case .completed(_, let transactionID, let method):
showPaid(transactionID, method)
case .canceled:
showCancelled()
case .failed(let error):
showError(error)
}
}
}
}
A completion-handler variant exists for code that is not using async/await:
YallaPay.pay(from: self, sessionID: sessionId) { result in
// same cases; called on the main thread
}
What pay does, in order:
- Reads the session with your publishable key. If it is already paid, expired or failed, returns straight away without presenting anything.
- Remembers the session id in user defaults, so an interrupted payment can be recovered on the next launch.
- Presents checkout as a system web-authentication sheet over your view controller.
- Waits. Either the return URL callback fires, or the customer dismisses the sheet.
- Reads the session again and returns a result based on what the API says, never on the redirect.
- Forgets the remembered session id.
The view controller you pass must be on screen with a window; the sheet is anchored to it. Disable your Pay button while a payment is in flight.
SwiftUI
pay needs a UIViewController to anchor the sheet. From SwiftUI, the simplest bridge is to resolve the key window's root view controller:
@MainActor
func topViewController() -> UIViewController? {
let scene = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first { $0.activationState == .foregroundActive }
var top = scene?.keyWindow?.rootViewController
while let presented = top?.presentedViewController { top = presented }
return top
}
struct PayButton: View {
let sessionId: String
@State private var busy = false
@State private var message = ""
var body: some View {
Button("Pay") {
guard let vc = topViewController() else { return }
busy = true
Task {
switch await YallaPay.pay(from: vc, sessionID: sessionId) {
case .completed(_, let trx, _): message = "Paid. Ref \(trx)"
case .canceled: message = "Cancelled."
case .failed(let error): message = error.message
}
busy = false
}
}
.disabled(busy)
}
}
Handling the result
| .completed(sessionID:transactionID:paymentMethod:) | YallaPay confirmed the session is paid. paymentMethod is .card, .applePay, .googlePay or .unknown. Show a receipt and refresh the order from your server. |
| .canceled | The customer dismissed the sheet without paying. Nothing was charged. The session stays open until it expires, so the customer can try again with the same id. |
| .failed(YallaPayError) | Carries a code and a message safe to show. Act on the code, see the table below. |
func showError(_ error: YallaPayError) {
switch error.code {
case .paymentFailed: alert("Your card was declined. Please try another.")
case .sessionExpired: startNewSession() // ask your server for a fresh one
case .network: recheckLater() // may still have succeeded!
default: alert(error.message)
}
}
It means the phone could not reach YallaPay after checkout closed. The customer may have paid. Call sessionStatus again when connectivity returns, and never show "payment failed" for this code.
Whatever the app shows, ship the order from your server on the webhook. See Webhooks & fulfilment.
Interrupted payments
iOS can terminate your app while the customer is in the checkout sheet: memory pressure, a crash, or the customer force-quitting after switching to their banking app for 3-D Secure. The task awaiting pay is gone with it, and the customer may have paid.
The SDK keeps the in-flight session id in user defaults for this case. On every launch, check for one and resolve it:
// At launch, after configure() if let sessionID = PendingSessionStore.pending { Task { let result = await YallaPay.sessionStatus(sessionID: sessionID) PendingSessionStore.clear() if case .completed(_, let transactionID, _) = result { showPaid(transactionID) // and refresh the order from your server } } }
PendingSessionStore.pendingis the interrupted session id, or nil on almost every launch.sessionStatusreads the session from the API and returns the same three results aspay, without presenting anything.PendingSessionStore.clear()forgets the id once you have acted on the result.
Five lines that run in milliseconds when nothing is pending, and that turn a lost payment into a receipt. Skipping this loses payments.
What the customer sees
Checkout is a YallaPay-hosted page presented in a system web-authentication sheet, the same component banks and identity providers use for sign-in. It slides up over your screen with a Cancel button and the YallaPay domain in its header; that header is provided by iOS and cannot be customised. The session is ephemeral, so no consent alert appears before it and nothing is shared with Safari.
On the page the customer sees your business name, the purpose you gave the session, the amount, an Apple Pay button on devices that support it, and a card form. Card numbers are entered on the YallaPay page, never in your app. 3-D Secure, when a bank requires it, happens inside the same sheet.
When payment completes, checkout redirects to your return scheme, iOS delivers the callback to the SDK, the sheet closes, and pay returns.
Apple Pay
Apple Pay is offered on the checkout page, on devices that support it and have a card enrolled. Because the page is hosted by YallaPay and shown in a system web sheet, your app needs no Apple Pay entitlement, no merchant identifier and no certificate: the page is what Apple sees, and YallaPay holds that configuration.
You cannot test Apple Pay in the simulator. Use a real device against the live environment with a small payment you refund afterwards. A completed Apple Pay payment comes back as .completed with paymentMethod == .applePay.
Error codes
| Code | Meaning and what to do |
|---|---|
| .invalidKey | The publishable key was missing, malformed, for the wrong environment, or not accepted. Fix the key or the environment. |
| .sessionNotFound | No such session for this account. Usually created on the other environment. |
| .sessionExpired | The session passed its expiry before the customer paid. Ask your server for a new one. |
| .paymentFailed | The customer reached checkout and the payment did not succeed. Offer to try again. |
| .presentationFailed | The sheet could not be presented, usually because the view controller is not in a window yet. Present from a view controller that is on screen. |
| .network | Could not reach YallaPay. The payment may still have succeeded. Re-check with sessionStatus. |
| .unknown | YallaPay returned something the SDK does not recognise. Show the message and re-check later. |
YallaPayError conforms to Error and LocalizedError; its errorDescription is the message. It also carries an optional param and the underlying error for network failures.
Reference
| YallaPay.configure(publishableKey:returnURL:environment:baseURL:) | Once, at launch. Validates both the key and the return URL. |
| YallaPay.pay(from:sessionID:) async -> PaymentResult | Presents checkout and resolves the outcome from the API. Main actor. |
| YallaPay.pay(from:sessionID:completion:) | The same, with a completion handler on the main thread. |
| YallaPay.sessionStatus(sessionID:) async -> PaymentResult | Reads a session without presenting anything. Never throws. |
| PendingSessionStore.pending: String? | The session id of an interrupted payment, or nil. |
| PendingSessionStore.clear() | Forget it. |
| PaymentResult | .completed(sessionID:transactionID:paymentMethod:), .canceled, .failed(YallaPayError) |
| PaymentMethod | .card, .applePay, .googlePay, .unknown |
| Environment | .live, .sandbox |
| YallaPayError | code, message, param?, underlying? |
There is deliberately no method to create a session. Creating one needs your secret key, and a secret key inside an IPA is a secret key in everyone's hands.
Sample app
The package includes a SwiftUI sample: a text field for a session id and a Pay button, with the launch-time recovery check. Open Example/YallaPaySample.xcodeproj, set your publishable key in YallaPaySampleApp.swift, run on a simulator, paste a session id created against the same environment, and press Pay.
Troubleshooting
| The app stops at launch with "YallaPay.configure: …" | The return URL is malformed or uses a reserved scheme. The message says which rule failed. Fix the string; this never reaches customers. |
| pod install refuses because of the deployment target | Raise platform :ios in the Podfile and the deployment target in Xcode to 15.0. |
| pay returns .presentationFailed | The view controller passed has no window yet. Present after the view has appeared, from a controller that is on screen. |
| A console warning says the scheme is not declared | Add the CFBundleURLTypes entry. Payments work without it, but other entry points through your scheme do not. |
| The customer paid but pay returned .canceled | The registered scheme, the configured return URL and the session's return URL are not identical. All three must match. |
General problems, test cards and the go-live checklist are on Sandbox & go-live.