POROROCA OTA · DOCUMENTATION
SwiftUI integration
A complete iOS 17+ launch, fallback, rendering, and recovery path.
REQUIREMENTS
Pin the iOS 17+ source package.
Check out a fixed commit. In Xcode choose File → Add Package Dependencies → Add Local…,
select this repository's ios
directory, and link Pororoca
to the app target.
cd /path/to/pororoca-ota/ios
swift test
01 · CONFIGURATION
Load the public key and stable install ID.
import Foundation
func pororocaPublicKey(_ base64: String) throws -> Data {
guard let data = Data(base64Encoded: base64), data.count == 32 else {
throw CocoaError(.fileReadCorruptFile)
}
return data
}
func pororocaInstallID() -> String {
let key = "pororoca.install-id"
if let value = UserDefaults.standard.string(forKey: key) { return value }
let value = UUID().uuidString.lowercased()
UserDefaults.standard.set(value, forKey: key)
return value
}
Use the app-scoped runtime token in app configuration as a publishable identifier. Mobile credentials are extractable, so it cannot inspect or mutate delivery; never substitute a delivery token.
02 · STORE AND CLIENT
Declare exactly what the host supports.
import Pororoca
let support = FileManager.default.urls(
for: .applicationSupportDirectory, in: .userDomainMask)[0]
let store = UpdateStore(
rootURL: support.appendingPathComponent("PororocaOTA", isDirectory: true),
publicKey: try pororocaPublicKey(PUBLIC_KEY_BASE64),
hostCapabilities: [
"paywall": HostCapabilities(EmbeddedPaywall.document.requires)
]
)
let client = UpdateClient(
baseURL: URL(string: "https://pororoca-ota.fly.dev")!,
apiToken: POROROCA_RUNTIME_TOKEN,
app: "your-app-slug", channel: "production",
installID: pororocaInstallID(), store: store
)
Capabilities are an allowlist. Unknown state, actions, slots, resources, or runtime requirements are rejected.
03 · LAUNCH SELECTION
Promote pending, otherwise fall back.
let pendingID = await store.pendingUpdateID()
let selection = try await store.prepareForLaunch()
let source: DocumentSource
let activeUpdateID: String?
switch selection {
case .embedded:
source = .embedded(EmbeddedPaywall.document); activeUpdateID = nil
case let .update(update):
if let url = update.documentURL(for: "paywall") {
source = .file(url); activeUpdateID = update.manifest.updateID
} else {
source = .embedded(EmbeddedPaywall.document); activeUpdateID = nil
}
}
The store verifies from network and disk, retaining pending, current, and previous. Always ship the embedded document.
04 · RENDER AND ACTIONS
Map document requests to compiled behavior.
let host = ScreenHost(
capabilities: HostCapabilities(EmbeddedPaywall.document.requires),
tokens: DictionaryTokenResolver(
colors: ["background": .black, "text": .white, "primary": .blue],
spaces: ["sm": 8, "md": 16, "lg": 24], radii: ["md": 12]),
localizer: DictionaryLocalizer([
"paywall.title": String(localized: "paywall.title")
])
)
OTAScreen(source: source,
state: StateSnapshot(["displayPrice": .string(displayPrice), "isPro": .bool(isPro)]),
host: host) { action, arguments in
switch action {
case "purchase": purchase()
case "restore": restorePurchases()
case "dismiss": dismiss()
default: assertionFailure("Unhandled action: \(action), \(arguments)")
}
}
05 · HEALTH AND RECOVERY
Mark good only after a healthy render.
if activeUpdateID == pendingID, let activeUpdateID {
try await store.markLaunchSuccessful()
try await client.record(event: "applied", updateID: activeUpdateID)
}
Task {
do { _ = try await client.checkForUpdate() }
catch { appDiagnostics.record(error) } // current UI remains active
}
Download only stages. The next launch promotes. If that launch exits before the good marker, the following launch marks the update bad and restores previous, then embedded if necessary.
TROUBLESHOOTING
Follow the failed boundary.
- No update/no error: 204 or an out-of-cohort install is valid.
- Signature failure: public key does not match the export private key.
- Capability rejection: compare document
requireswith host capabilities. - Repeated revert: call the good marker after a real healthy render point.
- Wrong cohort: persist the install ID instead of regenerating it.
Use examples/PaywallSpike as the complete reference host.