iOS SDK
Native Swift SDK for presenting AgentWallie paywalls in iOS apps. Renders paywall schemas using SwiftUI and handles purchases via StoreKit 2.
Minimum deployment target: iOS 16.0+
Installation
Swift Package Manager
Add the package URL in Xcode:
https://github.com/cynisca/AgentWallieKit.gitOr add to your Package.swift:
dependencies: [
.package(url: "https://github.com/cynisca/AgentWallieKit.git", from: "0.1.0")
]Then add AgentWallieKit to your target's dependencies.
Configuration
Configure the SDK as early as possible in your app lifecycle, typically in your App struct or AppDelegate:
import AgentWallieKit
@main
struct MyApp: App {
init() {
AgentWallie.configure(apiKey: "pk_your_public_key")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}With Options
var options = AgentWallieOptions()
options.networkEnvironment = .production // .production, .staging, or .custom(URL(...))
options.logLevel = .info // .none, .error, .warn, .info, .debug
options.defaultPresentation = .modal
AgentWallie.configure(apiKey: "pk_your_public_key", options: options)configure() must be called exactly once. Calling it again is a no-op and logs a warning.
User Management
Identify
Call identify when you know the user's identity (e.g., after login):
AgentWallie.shared.identify(userId: "user_123")Set User Attributes
Provide attributes for audience targeting:
AgentWallie.shared.setUserAttributes([
"plan": "free",
"onboarding_complete": true,
"session_count": 5,
"age": 28
])These attributes are available for audience filter evaluation (e.g., user.plan, user.session_count).
Reset
Clear the user identity, attributes, entitlements, and experiment assignments (e.g., on logout):
AgentWallie.shared.reset()Presenting Paywalls
Register a Placement
The primary API for showing paywalls. When called, the SDK evaluates campaigns, matches audiences, and presents the appropriate paywall:
AgentWallie.shared.register(placement: "feature_gate") {
// This closure runs if the user already has the required entitlement.
// No paywall is shown -- proceed with the feature.
showProFeature()
}If no campaign matches the placement, the handler closure runs (user proceeds without a paywall).
Programmatic Presentation
Present a specific paywall by ID, bypassing campaign evaluation:
AgentWallie.shared.presentPaywall(id: "pw_abc123")Get Paywall Without Presenting
Retrieve the paywall schema for a placement without showing it:
AgentWallie.shared.getPaywall(forPlacement: "upgrade") { result in
switch result {
case .success(let schema):
print("Paywall: \(schema.name)")
case .failure(let error):
print("No paywall: \(error)")
}
}Subscription Status and Entitlements
The SDK needs to know the user's subscription status to evaluate audience entitlement checks:
// Set subscription status
AgentWallie.shared.subscriptionStatus = .active // .unknown, .active, .inactive, .expired
// Set entitlements
AgentWallie.shared.entitlements = ["pro", "premium_content"]When an audience has an entitlement_check, the SDK skips users who already have that entitlement (no paywall shown, handler runs instead).
Purchases & StoreKit
By default, the SDK handles purchases using StoreKit 2 -- no setup needed. When a user taps a CTA button with action purchase, the SDK resolves the product slot, initiates the StoreKit 2 purchase, validates the receipt server-side, and updates entitlements automatically.
Key capabilities:
- Automatic product resolution -- paywall slots map to real StoreKit products with live prices
- Entitlement management -- purchases grant entitlements that gate paywall display
- Receipt validation -- server-side JWS verification via the receipts API
- Custom billing support -- plug in RevenueCat or any provider via
PurchaseController
For the full integration guide, including custom PurchaseController implementation, expression syntax for prices, entitlement configuration, and troubleshooting, see the dedicated Purchases & StoreKit guide.
Event Tracking
Track custom events for analytics:
AgentWallie.shared.trackEvent(
name: "workout_completed",
properties: ["duration": 45, "type": "strength"]
)Events are batched and sent to POST /v1/events/:public_key periodically.
Delegate Callbacks
Implement AgentWallieDelegate to receive SDK lifecycle events:
class AppCoordinator: AgentWallieDelegate {
init() {
AgentWallie.shared.delegate = self
}
func didPresentPaywall(info: PaywallPresentationInfo) {
print("Showed paywall: \(info.paywallName ?? "unknown")")
}
func didDismissPaywall(info: PaywallPresentationInfo) {
print("Dismissed paywall: \(info.paywallName ?? "unknown")")
}
func didCompletePurchase(productId: String) {
print("Purchased: \(productId)")
// Update subscription status
AgentWallie.shared.subscriptionStatus = .active
}
func didRestorePurchases() {
print("Purchases restored")
}
func handleCustomAction(name: String) {
// Handle custom tap actions from paywall buttons
switch name {
case "show_onboarding":
showOnboarding()
default:
break
}
}
func handleLog(level: LogLevel, message: String) {
// Optional: forward SDK logs to your logging system
}
}PaywallPresentationInfo
| Property | Type | Description |
|---|---|---|
paywallId | String? | Paywall identifier |
paywallName | String? | Paywall display name |
campaignId | String? | Campaign that triggered presentation |
audienceId | String? | Matched audience |
experimentId | String? | Active experiment |
variantId | String? | Assigned variant |
Custom Views
Register native SwiftUI views to embed inside paywalls. Custom views let you go beyond the built-in component types for brand-specific UI, complex interactions, or third-party integrations.
// Register a simple view
AgentWallie.shared.registerView(name: "PremiumHero") {
PremiumHeroView()
}
// Register a context-aware view
AgentWallie.shared.registerView(name: "PricingCard") { context in
PricingCardView(data: context.customData)
}Then reference it in your paywall schema:
{
"type": "custom_view",
"id": "hero",
"props": { "view_name": "PremiumHero" }
}For the full registration API, context properties, action handling, and best practices, see the Custom Views guide.
The paywall schema also supports rich DSL features including conditionals, animations, and gradients that work with all built-in components without any native code changes.
Deep Links
Handle AgentWallie deep links to present paywalls directly:
// In your SceneDelegate or App
func handleURL(_ url: URL) {
AgentWallie.shared.handleDeepLink(url)
}Deep link format: agentwallie://paywall/{paywall_id}
Options Reference
AgentWallieOptions
| Property | Type | Default | Description |
|---|---|---|---|
networkEnvironment | NetworkEnvironment | .production | API environment |
logLevel | LogLevel | .warn | Minimum log level |
defaultPresentation | PaywallPresentation | .modal | Default paywall presentation style |
NetworkEnvironment
| Value | Description |
|---|---|
.production | Production API |
.staging | Staging environment |
.custom(URL(...)) | Custom API URL (positional argument, for local dev) |
LogLevel
| Value | Description |
|---|---|
.none | No logging |
.error | Errors only |
.warn | Warnings and errors |
.info | Info, warnings, and errors |
.debug | All logs including debug |
Local Development
You can render paywall schemas locally without connecting to the AgentWallie API. This is useful for rapid iteration, snapshot testing, and offline development.
Render a JSON Schema Directly
Decode a bundled JSON file and render it with PaywallView:
import AgentWallieKit
// Load from bundled JSON file
guard let url = Bundle.main.url(forResource: "my-paywall", withExtension: "json"),
let data = try? Data(contentsOf: url),
let schema = try? JSONDecoder().decode(PaywallSchema.self, from: data) else {
return
}
// Render directly in SwiftUI
PaywallView(
schema: schema,
onAction: { action, param in
print("Action: \(action)")
},
onDismiss: {
dismiss()
}
)Use the Dev Server with PaywallDevPreview
For live-reloading previews on the iOS Simulator, pair the paywall-dev serve CLI with the PaywallDevPreview app:
- Start the dev server:
npx paywall-dev serve my-paywall.json - Build and launch
PaywallDevPreviewin the Simulator (see Paywall Dev Kit docs) - The preview app polls
http://localhost:3456/schemaand re-renders on every change
This gives you pixel-perfect native rendering with a fast edit-save-preview loop.
See the full Paywall Dev Kit documentation for CLI commands, mock data setup, and the agent workflow.
Supported Features
All paywall schema features are fully supported in the current iOS SDK:
- Product picker:
"cards","horizontal", and"vertical"layouts withshow_savings_badge,savings_text,show_price,selected_border_color, and glow - Expression resolution:
{{ products.selected.price }}and other product expressions resolve reactively as the user selects products, with fallback toAWProduct.displayPricewhen StoreKit fetch fails - Feature list: Per-row styling with
background_color,corner_radius,padding, andborder_color(accent bar) - CTA button: Full styling including
glow_colorshadow effect - Text:
letter_spacing, markdown bold (**text**) - Close button: Configurable via
close_button_style("icon"or"text") - Background gradient: Supported via
background_gradientin paywall settings - Shimmer animation: Supported via
"shimmer"animation type
Full Example
import SwiftUI
import AgentWallieKit
@main
struct FitnessApp: App {
@StateObject private var coordinator = AppCoordinator()
init() {
var options = AgentWallieOptions()
options.logLevel = .info
AgentWallie.configure(apiKey: "pk_live_abc123", options: options)
}
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
AgentWallie.shared.delegate = coordinator
AgentWallie.shared.identify(userId: "user_456")
AgentWallie.shared.setUserAttributes([
"plan": "free",
"workouts_completed": 12
])
}
}
}
}
struct WorkoutView: View {
var body: some View {
Button("Start Advanced Workout") {
AgentWallie.shared.register(placement: "advanced_workout") {
// User has pro entitlement -- let them through
startAdvancedWorkout()
}
}
}
func startAdvancedWorkout() {
// Navigate to workout
}
}
class AppCoordinator: ObservableObject, AgentWallieDelegate {
func didPresentPaywall(info: PaywallPresentationInfo) {}
func didDismissPaywall(info: PaywallPresentationInfo) {}
func didCompletePurchase(productId: String) {
AgentWallie.shared.subscriptionStatus = .active
AgentWallie.shared.entitlements = ["pro"]
}
func didRestorePurchases() {}
func handleCustomAction(name: String) {}
func handleLog(level: LogLevel, message: String) {}
}