iOS SDK
Paywall cards
A support agent can send an offer card in an iOS chat. The customer sees an optional message and a button that opens your app's RevenueCat paywall. AppNorth records the first tap, not a purchase. Your existing purchase flow owns the transaction.
Add the optional helper
The AppNorth SDK does not depend on RevenueCat. Add RevenueCat and RevenueCatUI from purchases-ios 5.50 or later in the 5.x line, then copy packages/ios/Helpers/AppNorthRevenueCatPaywalls.swift from the integration sources into your app target. This helper is separate source, not a type exported by the AppNorth binary.
Configure Purchases, sign in to the correct RevenueCat account, and install the corresponding AppNorth identity credential before creating the helper. Retain it for the session rather than recreating it in body.
import AppNorth
@MainActor
final class OfferSession {
let helper: AppNorthRevenueCatPaywalls
var syncFailed = false
init(client: AppNorthClient) {
helper = AppNorthRevenueCatPaywalls(client: client)
helper.onError = { [weak self] _ in self?.syncFailed = true }
}
func stop() {
helper.stop()
}
}This snippet requires the copied helper in the same target. Its optional presenter: closure returns the UIKit controller to present above. Supply it for multiwindow apps; the default searches the active scene's key window and top presented controller.
Connect it to chat
Pass helper.paywalls to AppNorthChatView or to AppNorthView for its floating chat entry. The example below works with either the helper's controller or a controller you configure yourself:
import SwiftUI
import AppNorth
struct OffersChat: View {
let client: AppNorthClient
let paywalls: AppNorthPaywalls
var body: some View {
AppNorthChatView(client: client, paywalls: paywalls)
}
}The helper loads all offerings at startup and on foreground, observes customerInfoStream, and presents PaywallView above chat. It dismisses on purchase completion, explicit close, or a restore with an active entitlement. Configure a usable close control in your paywall design and test returning to the conversation.
Offerings without a published paywall are not presentable. A card becomes unavailable when its offering disappears or none of its products remain available. Consumables never count as owned; subscriptions use active product IDs, allowing an offering with other plans to remain available for a plan change.
Report your own catalog
If your app already owns paywall presentation, retain an AppNorthPaywalls controller on the main actor. Pass real localized prices and ISO-8601 periods from your products, not invented display values.
import AppNorth
@MainActor
func configurePaywalls(
controller: AppNorthPaywalls,
catalog: [AppNorthPaywall],
availability: @escaping (String) async -> Bool,
present: @escaping (String) -> Void
) async throws {
try controller.setPaywalls(catalog)
controller.availabilityProvider = availability
controller.onPaywall = present
try await controller.refreshPaywalls(invalidate: true)
}Call refreshPaywalls(invalidate: true) after purchases or customer state changes. Overlapping refreshes coalesce. A missing catalog entry, an unsupported card kind, a non-presentable offering, or a missing presentation callback keeps a card disabled.
Snapshots allow 20 paywalls, 10 products per paywall, and 40 products total. Paywall IDs are 1–64 characters from A-Z a-z 0-9 _ . -; titles are nonempty and at most 60 UTF-16 units. Titles are team-only labels. Product prices are localized strings; periods and trials use forms such as P1M and P7D, with P0D for a one-time product. Invalid or oversized snapshots throw instead of silently dropping offerings.
Metadata and receipts
By default the helper reports device-derived subscription_status, plan, renews_on or expires_on, and environment: sandbox when applicable. Pass syncsSubscriptionMetadata: false to omit them. These are separate from signed identity properties and must not be used as trusted authorization claims. If several subscriptions are active, the displayed plan is the first product ID alphabetically; availability still checks all products.
onOfferOpened receives the message ID and paywall ID once per card in the controller. Repeated taps may reopen the paywall, while server receipts count the first tap once. A failed receipt retries on the next tap or refresh while the controller lives. This callback does not confirm presentation or purchase completion. Cards arrive through chat polling; this SDK has no push delivery API.
Change accounts safely
Call helper.stop(), close chat, and discard the helper before RevenueCat logout or an account switch. Update RevenueCat and AppNorth's credential, then create a new helper. Do not reuse the previous user's catalog or subscription metadata. The controller also invalidates availability when it detects an identity-scope change.
Verify
Open chat with the retained helper and confirm the device's actual offerings appear in the dashboard's Send offer flow.
Send a card, tap it, close the paywall, and confirm you return to chat.
Complete a sandbox purchase and verify availability refreshes from RevenueCat. Use RevenueCat to verify the transaction; an AppNorth open receipt measures only the tap.
Tap repeatedly and confirm one open receipt. Test a removed offering, a missing callback, and a failed receipt followed by reconnecting.
Switch accounts and verify neither the old catalog state nor subscription metadata is reused.