SDKs mobile
Swift (iOS)
SDK nativo do Paywallo para iOS e macOS. Swift Package Manager, async/await e os mesmos eventos do SDK React Native.
Requisitos
- iOS 16 ou superior · macOS 12 ou superior
- Swift 5.9 ou superior (Xcode 15+)
- Capability In-App Purchase no target (o SDK usa StoreKit 2)
- Instalação por Swift Package Manager
Usa React Native ou Expo? A instalação é outra — vá pra .
Instalar o pacote
No Xcode: File → Add Package Dependencies… e cole a URL do repositório. Use a regra Up to Next Major a partir de 2.6.0 e marque o produto PaywalloSDK no seu target.
https://github.com/Virex-Tech/paywallo-ios-sdk.gitSe o app é um pacote SwiftPM, declare no manifesto:
// Package.swift
dependencies: [
.package(url: "https://github.com/Virex-Tech/paywallo-ios-sdk.git", from: "2.6.0")
],
targets: [
.target(
name: "MeuApp",
dependencies: [
.product(name: "PaywalloSDK", package: "paywallo-ios-sdk")
]
)
]Não há CocoaPods nem Carthage: a distribuição é só por Swift Package Manager. Adicione também a capability In-App Purchase no target — o SDK usa StoreKit 2 pra processar e validar compras.
Inicializar o SDK
Não existe Provider como no React Native: você chama initialize uma vez, no início do ciclo de vida do app. É async throws e é seguro chamar de novo — a segunda chamada espera a primeira em vez de reinicializar. A appKey vem do dashboard, na aba SDK.
Resolva o ATT antes de inicializar (iOS)
Peça a permissão de App Tracking Transparency (ATT) e espere o usuário responder antes de chamar initialize. Se o SDK subir primeiro, ele inicializa com attStatus=undetermined, sem IDFA e sem madid no CAPI, o que degrada a atribuição do install. Timing e copy do prompt são responsabilidade do app e decidem a taxa de opt-in: medido nos nossos clientes, o Fitcal converte 42,0% contra 1,8% do PeptPro, 23x de variação. Requer NSUserTrackingUsageDescription no Info.plist.
Dá pra deixar o SDK pedir o ATT por você com requestATT: true, mas aí o prompt aparece no boot, que costuma ser o pior momento possível. O caminho recomendado é pedir na sua própria tela e só depois inicializar:
import AppTrackingTransparency
import PaywalloSDK
func bootstrap() async {
// 1. Pede o ATT e ESPERA o usuário responder
_ = await ATTrackingManager.requestTrackingAuthorization()
// 2. Só agora inicializa o SDK — o IDFA já está resolvido
try? await PaywalloClient.shared.initialize(config)
}Abaixo, os campos que valem a pena configurar em PaywalloInitConfig. Só appKey é obrigatório:
import PaywalloSDK
import SwiftUI
@main
struct MeuApp: App {
init() {
Task {
do {
try await PaywalloClient.shared.initialize(
PaywalloInitConfig(
// Obrigatório: obtido no dashboard, aba SDK
appKey: "pk_xxxxxxxx",
// Logs do SDK no console. Deixe false em release
debug: false,
// .production (padrão) ou .sandbox pra testar IAP sem cobrar
environment: .production,
// Sessão automática por foreground/background (padrão: true)
autoStartSession: true,
// Dispara o prompt de ATT dentro do init. Ver aviso acima
requestATT: false,
// Textos da tela de erro do paywall (todos opcionais)
errorStrings: PaywallErrorStrings(
title: "Ops",
retry: "Tentar novamente",
close: "Fechar"
),
// Placement pré-carregado no boot: present fica quase instantâneo
autoPreloadCampaign: "onboarding",
// Push notifications via APNs (padrão: true)
notifications: true,
// Erros do SDK em runtime, pra logar no seu crash reporter
onError: { error in print("[Paywallo]", error) }
)
)
} catch {
// O SDK já tenta 3 vezes sozinho (2s, 5s, 10s) antes de chegar aqui
print("[Paywallo] init falhou:", error)
}
}
}
var body: some Scene {
WindowGroup { ContentView() }
}
}O SDK dispara $app_installed sozinho durante o init, não chame na mão. Mas disparar sozinho não é acertar sozinho: a qualidade da atribuição desse evento depende do ATT já estar resolvido nesse momento.
Registrar o presenter do paywall
No iOS nativo o SDK não desenha o paywall por você — quem apresenta a UI é o app. Sem um presenter registrado, presentPaywall retorna presented: false e nada aparece na tela. Registre uma vez, no boot:
// No boot, depois do initialize:
PaywalloClient.shared.registerPaywallPresenter { config, products in
// Sua UI: apresente o paywall e só retorne quando o usuário decidir.
// O SDK entrega config (layout do dashboard) e os produtos já carregados.
let outcome = await MinhaTelaDePaywall.apresentar(config: config, products: products)
return PaywallResult(
presented: true,
purchased: outcome.comprou,
productId: outcome.productId,
transactionId: outcome.transactionId,
cancelled: outcome.fechou,
restored: outcome.restaurou
)
}O SDK entrega as peças prontas pra montar essa tela: PaywallPresenter (um UIViewController que hospeda a WebView do layout feito no dashboard), PaywalloWebView, PaywallTracking e StoreKitManager pra compra. Pra campanhas com teste A/B, o equivalente é registerCampaignPresenter.
Verificar que funcionou
Ligue debug: true no config e rode o app. Com o SDK de pé, o console mostra as chamadas de rede com o prefixo [Paywallo:Api]. Em código, isReady() responde se o init já terminou.
Chamadas no boot: use waitUntilReady() antes de identify e track no cold start. Sem isso, identify lança ClientError(NOT_INITIALIZED) se o init nem começou.
// No boot, antes de qualquer identify/track:
await PaywalloClient.shared.waitUntilReady()
try await PaywalloClient.shared.identify(
IdentifyOptions(
email: user.email,
// userId é obrigatório pra linkar o device ao usuário
properties: ["userId": AnyCodable(user.id)]
)
)
PaywalloClient.shared.track("app_pronto")
print("distinctId:", PaywalloClient.shared.getDistinctId())Se o evento não aparecer no dashboard em alguns segundos, confira nesta ordem: appKey do app certo, environment batendo com o filtro do painel, e o device com rede — o SDK tem fila offline durável e só entrega quando a conexão volta. getOfflineQueueSize() mostra quantos eventos estão presos.
Identificar o usuário
identify é o que liga o device a uma pessoa e o que enriquece os eventos enviados pro CAPI do Meta e do TikTok. Quanto mais campos, melhor a qualidade da correspondência. Valores livres em properties vão embrulhados em AnyCodable.
try await PaywalloClient.shared.identify(
IdentifyOptions(
email: "user@email.com",
properties: [
"userId": AnyCodable(user.id),
"plano": AnyCodable("free")
],
phone: "+5511999999999",
firstName: "Maria",
lastName: "Silva",
dateOfBirth: "1995-03-14",
gender: .female
)
)
// Logout: novo anônimo, SDK continua pronto
await PaywalloClient.shared.reset()Chamar identify antes do init terminar não perde dado: o SDK enfileira e reaplica quando fica pronto. O que cada campo faz na atribuição está em .
Enviar eventos
track não é async e não lança: só enfileira no batcher, que entrega em background e sobrevive a app fechado. Use priority: .critical pro que não pode atrasar.
// Simples
PaywalloClient.shared.track("tela_aberta")
// Com propriedades
PaywalloClient.shared.track(
"receita_favoritada",
properties: ["receitaId": AnyCodable(42), "origem": AnyCodable("busca")]
)
// Crítico: fura a fila do batch e vai na frente
PaywalloClient.shared.track("checkout_iniciado", priority: .critical)A lista de eventos automáticos e as convenções de nome estão em .
Exibir paywalls e campanhas
Com o presenter registrado, apresentar é uma linha. Pré-carregar a campanha no boot (ou via autoPreloadCampaign) faz a primeira exibição sair sem espera de rede.
// Paywall direto por placement
let result = await PaywalloClient.shared.presentPaywall(placement: "onboarding")
if result.purchased || result.restored {
liberarConteudoPremium()
}
// Campanha (teste A/B de paywall configurado no dashboard)
await PaywalloClient.shared.preloadCampaign(placement: "onboarding")
let campaign = await PaywalloClient.shared.presentCampaign(placement: "onboarding")Assinatura e restauração
A compra em si acontece no seu presenter, via StoreKit 2. O que o client expõe é o estado: consulta de assinatura, gate de conteúdo e restore.
// Checagem rápida (usa cache)
let ativo = await PaywalloClient.shared.hasActiveSubscription()
// Detalhe da assinatura
if let sub = await PaywalloClient.shared.getSubscription(forceRefresh: true) {
print(sub.productId, sub.status, sub.expiresAt as Any)
}
// Gate: mostra o paywall se não tiver assinatura e devolve o resultado
let liberado = await PaywalloClient.shared.requireSubscription(paywallPlacement: "premium")
// Botão de restaurar compras
let restore = try await PaywalloClient.shared.restorePurchases()
print(restore.success, restore.restoredProducts)A validação é server-side e depende das credenciais da . Sem isso, a assinatura não aparece no painel nem chega ao CAPI.
Onboarding
Cada step vira uma etapa do funil de onboarding no dashboard. order define a posição e timeOnPrevS mede o tempo gasto na tela anterior.
try await PaywalloClient.shared.onboardingStep(stepName: "objetivo", order: 1)
try await PaywalloClient.shared.onboardingStep(stepName: "peso", order: 2, timeOnPrevS: 4.2)
try await PaywalloClient.shared.onboardingComplete()Como o funil é montado e onde ver o dropout: .
Push notifications (APNs)
Ligado por padrão (notifications: true). Você pede a permissão quando fizer sentido no seu fluxo e repassa o token do APNs pro SDK.
// 1. Permissão (o SDK registra o device no Paywallo sozinho)
let status = await PaywalloClient.shared.requestPushPermission()
guard status == .granted || status == .provisional else { return }
// 2. No AppDelegate, repasse o token do APNs
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
Task { await PaywalloClient.shared.setApnsToken(token) }
}Superwall
A ponte com o Superwall não liga sozinha no iOS nativo: é preciso chamar startSuperwallBridge depois do init. Ela espelha os eventos do Superwall pro Paywallo e empurra os atributos de atribuição pw_* pro Superwall, que é o que permite segmentar audiência por origem.
import PaywalloSDK
import SuperwallKit
// Depois do initialize:
startSuperwallBridge(attribution: AttributionTracker())Sem o SuperwallKit linkado no projeto, a função existe e não faz nada. A configuração do lado do painel está em . Já a integração com o Meta (FBSDKCoreKit) é diferente: essa liga sozinha, basta ter o SDK do Meta no projeto.
Diferenças em relação ao SDK React Native
- Não existe Provider: você chama initialize() e pronto.
- presentPaywall exige registerPaywallPresenter antes — a UI é montada pelo app.
- Não existe purchase() no client. A compra roda no seu presenter, via StoreKit 2.
- Não existe setCurrentLanguage/setDefaultLanguage: o idioma vem sempre do dispositivo.
- Não existe conversion value de SKAdNetwork. Se o app precisa disso hoje, é o SDK React Native ou implementação própria.
- A ponte do Superwall é manual (startSuperwallBridge); no React Native ela é automática.
Essa página foi útil?
Desenvolvido e mantido por Virex