OnlangKit renders ONLang flows with your own UIKit views, over DivKit: no web views, no downloaded code. This sample takes the paywall you ship today and serves it through ONLang, with today’s screen as the fallback.
YOU NEED
An ONLang project: onlang init in your app’s repository
Your app’s publishable key (onl_pk_…) and the delivery URL
iOS 15+, Xcode 26 (Swift 6.2+), Swift Package Manager
The paywall you ship today: it stays your screen when ONLang is switched off
01
Add the Swift package
Xcode → File → Add Package Dependencies. Depend on OnlangKit (rendering over DivKit) and OnlangStoreKit (StoreKit 2), or on OnlangRevenueCat if RevenueCat handles your purchases.
Package.swift
.package(url: "https://github.com/onlang-ai/onlang-ios.git", from: "1.0.0"),
// only with RevenueCat:
.package(url: "https://github.com/onlang-ai/onlang-ios-revenuecat.git", from: "1.0.0"),
The packages are distributed to early-access teams. They are commercial software under the ONLang SDK License: free for 14 days per app, then 0.49% of every order or $49 a month.
02
Describe your components
The registry is the contract between your app and the server: which components exist, which props they take, and how long a text may be. The compiler checks every hypothesis against it.
Each registered component is your own UIView, built from the props the server chose: store prices filled in, copy in the user’s locale. DivKit sizes it from the registry’s fixed height, or from its Auto Layout fitting size.
Onlang+Registry.swift
import OnlangKit
let registry = OnlangRegistry()
.register("PaywallIntro") { props, _ in
AcmeIntro(title: props["headline"] as? String ?? "", body: props["body"] as? String ?? "")
}
// Interactive: handles its own touches, and reports them to ONLang
.register("PlanToggle", interactive: true) { props, ctx in
AcmePlanToggle(plans: props["products"] as? [String] ?? []) { plan in
ctx.fire(URL(string: "onlang://select?product=\(plan)")!)
}
}
// Not interactive: no gesture of its own. ONLang wraps it and routes the tap
.register("PurchaseButton") { props, _ in
AcmeButtonLabel(title: props["title"] as? String ?? "")
}
04
Configure once, at launch
Billing and prices stay yours. StoreKitPurchases buys through StoreKit 2 and fills in {{price.annual}} and friends from the App Store; map registry product names to App Store ids if they differ.
Purchases carry the install id as their appAccountToken, so App Store renewals and refunds reach the experiment that showed the paywall. With RevenueCat, RevenueCatPurchases sets the subscriber attribute onlang_install_id. If you buy outside ONLang, pass Onlang.installId the same way.
Users in Russia: during the mobile-internet whitelist restrictions, phones on mobile data can’t reach the main hosts. Set baseUrl to https://delivery.ru.onlang.io, ONLang’s node in Moscow: same key, releases and experiments, and paywalls already seen keep loading.
05
Present a placement
On .failed or .killed, show your own paywall: that is your fallback when nothing can load, and when the placement is switched off.
So the very first launch works offline, add onlang/<placement>.<locale>.json to the app bundle as a folder reference: a JSON array of the flow’s bundles. onlang compose writes them, one file per screen. After the first launch, the SDK caches what delivery serves.
Hypotheses are code in your repository. onlang lint compiles every combination and checks it (lengths, price disclosure, legal links) before anything ships.
terminal
onlang hypothesis create --id paywall.cta.copy --kind tokens --screen paywall \
--key cta.title --rationale "Name the outcome, not the step" \
--variant "outcome=Get my plan" --variant "trial=Try Pro free for {{trial.annual}}"
onlang lint # every assignment compiled and checked
ONLANG_TOKEN=onl_sk_… onlang publish # delivery serves the release at once
ONLANG_TOKEN=onl_sk_… onlang experiment create --placement paywall --holdout 0.1 --start
ONLANG_TOKEN=onl_sk_… onlang experiment status <id> # uplift vs holdout, 95% interval
08
Ask for consent, and forward background uploads
Until your app records a choice, events wait on the device (up to 30 days) and nothing is sent. Uploads run in a background URLSession, so they finish while the app is suspended.
settings screen · AppDelegate.swift
Onlang.setConsent(.granted) // sends what is queued
Onlang.setConsent(.denied) // deletes the queue, records nothingfunc application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
Onlang.handleEventsForBackgroundURLSession(identifier, completionHandler: completionHandler)
}
09
Know where the off switch is
One command takes the placement off ONLang. Delivery answers within a second, and the flow finishes with .killed, even on devices that go offline afterwards: neither the cache nor the bundled flow is shown, so users see your own paywall until you switch it back on.
terminal
onlang kill paywall --reason "INC-12: CTA does nothing on Android 15"
onlang kill --status
onlang kill paywall --lift
What the SDK does for you
Loads network → cache → the bundled flow; bundles are content-addressed and verified (SHA-256)
Renders your registered UIViews with DivKit: no web views, no downloaded code
Records impressions, dwell and scroll depth, taps with rage and dead clicks, plan switches and purchases
Queues events on the device and uploads them in the background, batched and retried; ingest drops duplicates
Rules of thumb
Billing, prices and entitlement stay in your app; ONLang never charges anyone
A component that isn’t interactive must not handle touches itself
Copy that has to be true about the device or the account belongs in a component, not a token
Treat .failed and .killed the same: show your own paywall
The kit suite renders the same 45 golden and fuzz bundles as the Android and React Native SDKs on the iOS Simulator, with pixel snapshots; a host app runs real touches and StoreKit 2 purchases against StoreKit Testing. OnlangKit adds 395 KB to a release app over DivKit.
Bring your iOS paywall.
Early-access teams get the SDK, the CLI and a hand with the first placement.