SubSovereign na integração em iOS — guia
Este guia leva o seu app Apple (iPhone, iPad, Apple TV ou Mac) de "não tenho ideia de quem me pagou" a "o meu app desbloqueia as funcionalidades certas para o utilizador certo, verificado no meu próprio servidor". Está escrito para ser seguido do início ao fim — não é necessário ter experiência prévia com ferramentas de subscrição.
O que o SubSovereign faz por si
Os seus utilizadores subscrevem através da App Store. O SubSovereign responde, de forma fiável, a uma única pergunta para o seu app: o que é que este utilizador pagou, na realidade?
- O seu app pergunta ao SubSovereign pelo direito de acesso do utilizador — o acesso que foi desbloqueado.
- A validação da recibo é feita no servidor, diretamente com a Apple (App Store Server API), de modo a que um app modificado não possa falsificar uma subscrição.
- O ecrã de pagamento — o ecrã que oferece os seus planos — é configurado remotamente, permitindo-lhe alterar preços, períodos de teste e texto sem lançar uma nova versão do app.
- A função de desistência legal — o controlo de cancelamento obrigatório — também é desenhado pelo SDK, na língua do cliente, de modo a que uma notificação legal nunca possa ser suavizada para marketing.
- É auto-hospedado: o SubSovereign funciona na sua infraestrutura, os dados dos seus utilizadores ficam consigo e não há partilha de receitas — fica com 100% do que os seus utilizadores pagam.
Nunca confia no dispositivo. O dispositivo pergunta; o servidor decide. O mesmo código funciona em iOS, tvOS (Apple TV) e macOS.
Antes de começar
Precisa de:
- Um servidor SubSovereign em execução (a sua implementação auto-hospedada) — o SDK aponta para a respetiva URL.
- Um app registado no painel de controlo, que lhe fornece um
appIde uma chave de API (a credencial que o seu app usa para comunicar com o servidor). - Níveis de acesso criados — os níveis de acesso (tier) que o seu app concede, por exemplo,
pro, cada um ligado no painel de controlo aos IDs de produto da App Store que os seus utilizadores compram. - StoreKit 2 configurado. O SubSovereign valida e regista compras; não substitui o StoreKit. Trate das compras com o StoreKit 2 como habitualmente — o SubSovereign atua logo após uma transação bem-sucedida para a verificar e registar.
Do lado do código, precisa de Swift concorrência (async/await) e de um alvo de implementação de iOS 15 / tvOS 15 / macOS 12 ou superior.
Passo 1 — Adicionar o SDK
Adicione o SubSovereign com o Swift Package Manager — no Xcode, em File ▸ Add Packages… e aponte para o pacote do SDK SubSovereign (ou adicione-o às dependências do seu Package.swift). Depois, importe-o:
import SubSovereign
Passo 2 — Configurar uma única vez, quando o app iniciar
Configure o SDK uma única vez — um bom local é o init da sua App, ou logo após o utilizador iniciar sessão. Forneça-lhe a sua chave de API, o ID do app, a URL do servidor, um identificador estável para este utilizador e a respetiva língua.
SubSovereign.shared.configure(
SubSovereignConfig(
apiKey: "YOUR_APP_API_KEY", // from the dashboard
appId: "your-app-id", // from the dashboard
baseURL: "https://subs.yourdomain.com/api/v1", // YOUR self-hosted server
userId: currentUser.id, // your own stable user id
locale: Locale.current.identifier // the user's language
)
)
Alguns pontos importantes a saber:
userIdé seu. Use qualquer ID estável que já tenha para um utilizador autenticado e o mesmo valor todas as vezes, de modo a que o acesso siga o utilizador entre dispositivos. Se um utilizador diferente iniciar sessão, chameconfigurenovamente com o novouserId.baseURLaponta para o seu servidor — o valor predefinido do SDK é um espaço reservado; defina a sua própria implementação.- O SDK está anotado com
@MainActor, pelo que deve chamá-lo do ator principal (as vistas SwiftUI e.tasksão adequadas).
Passo 3 — Verificar o que o utilizador pode aceder
Chame checkEntitlements() para descobrir o que o utilizador desbloqueou. Faça isto ao iniciar o app e novamente após uma compra. Trata-se de uma chamada async que pode throw, pelo que a envolva em do/catch:
do {
let result = try await SubSovereign.shared.checkEntitlements()
if result.hasAccess {
unlockProFeatures() // the user has paid access
} else {
showFreeExperience() // free tier / show a paywall
}
} catch {
// Network hiccup or server error. Fail gracefully — usually keep the user on
// whatever access they last had, and try again later.
print("Entitlement check failed: \(error.localizedDescription)")
}
result.hasAccess é a resposta rápida sim/não ao resultado que já obteve. Para proteger uma funcionalidade numa única linha, use o método com o mesmo nome — responde com false em caso de erro, pelo que um problema de rede não abre nada indevidamente:
if await SubSovereign.shared.hasAccess(accessLevelName: "pro") {
unlockProFeatures()
}
Use-o para uma proteção simples de uma funcionalidade. Para a decisão de acesso global do app, use checkEntitlements() e as orientações sobre Tratamento de erros abaixo — durante uma interrupção, quer manter um cliente pagante com o último acesso conhecido, não quer bloqueá-lo.
Se o seu app tiver mais do que um tier, consulte result.entitlements — cada um deles indica o nível de acesso ativo. Faça a correspondência com o nome que atribuiu ao nível no painel de controlo; accessLevelId é o ID interno do servidor, não esse nome:
let isPro = result.entitlements.contains { $0.isActive && $0.accessLevelName.lowercased() == "pro" }
Cada direito de acesso também inclui expiresAt, willRenew e a store de onde veio. O resultado também inclui fromCache, que indica se o servidor respondeu a partir do seu próprio cache — o SDK não tem cache e cada chamada é enviada para o seu servidor.
Passo 4 — Vender uma subscrição
Mostrar o ecrã de pagamento
Obtenha o ecrã de pagamento do servidor em vez de codificar os preços, de modo a poder fazer uma promoção ou alterar um período de teste sem lançar uma nova versão:
if let paywall = try? await SubSovereign.shared.getPaywallConfig() {
renderPaywall(paywall) // headline, features, products…
} else {
renderFallbackPaywall() // your built-in default
}
PaywallConfig fornece-lhe um headline, subheadline, uma lista de features, os products a oferecer (cada um com um displayPrice, period, trialDays e badge opcional), o texto de chamada à ação e o texto do rodapé. Pode construir o ecrã sozinho a partir destes elementos — ou deixar que o SDK o desenhe. PaywallView desenha o ecrã de pagamento que desenhou no painel de controlo com SwiftUI nativo, nas cores do inquilino, num iPhone, iPad ou Apple TV, e chama-o de volta com o produto escolhido pelo cliente:
PaywallView(config: paywall) { productId in
// start the StoreKit purchase for `productId` - see "Complete the purchase" below
}
Uma cor que não seja uma cor nunca chega ao ecrã: a vista escolhe uma alternativa legível e o nosso próprio texto desvanecido mantém-se acima do limite de legibilidade, independentemente das cores que escolher.
Concluir a compra e, depois, verificar
Efetue a compra através do StoreKit 2 como habitualmente. Quando obtiver uma Transaction verificada de volta, passe-a ao SubSovereign para que o servidor a possa validar diretamente com a Apple e conceder o nível de acesso:
func buy(_ product: Product, accessLevelId: String) async throws {
let result = try await product.purchase()
guard case .success(let verification) = result else { return } // cancelled, or pending approval
guard case .verified(let transaction) = verification else {
// .unverified means StoreKit could not trust the signature — a tampered receipt.
// Never grant access, and never let this pass silently.
print("Refused an unverified transaction")
return
}
let granted = try await SubSovereign.shared.validateApplePurchase(
transaction: transaction,
accessLevelId: accessLevelId // required; the SERVER grants whatever tier your
// dashboard maps this product to, not this value
)
if granted {
await SubSovereign.shared.finishTransaction(transaction) // tell StoreKit it's done
_ = try await SubSovereign.shared.checkEntitlements() // re-check, then unlock
}
}
Este é o modelo de confiança completo: a compra só é real quando o servidor a confirmou com a Apple.
Passo 5 — A função de desistência legal (Passaporte de Conformidade)
Se vender subscrições a consumidores da UE, a Diretiva (UE) 2023/2673 exige uma função de desistência legal claramente identificada — o controlo de cancelamento. O SDK inclui-o pronto a usar e é desenhado em duas partes: obtenha as definições para o seu app e, depois, desenhe o controlo com elas.
struct CancelSubscription: View {
let subscriptionId: String
@State private var settings: WithdrawalConfig?
var body: some View {
Group {
if settings?.enabled == false {
EmptyView() // switched off for this app in the console
} else {
WithdrawalView(
subscriptionId: subscriptionId,
labelKey: settings?.labelKey, // the label you chose in the console
appearance: .auto // .light, .dark or .auto — and nothing else
)
// The WORDS are not passed in. getWithdrawalConfig() below fetches them in your
// customer's language and the SDK keeps them; this screen reads them from there.
// Fetch FIRST — without it the notice falls back to English for everyone.
}
}
.task {
do {
settings = try await SubSovereign.shared.getWithdrawalConfig()
} catch {
// Log it: a silent failure here is how you ship English to German customers
// and never find out.
print("Withdrawal settings unavailable: \(error.localizedDescription)")
}
}
}
}
Não pode alterar a redação. As palavras vêm do seu servidor SubSovereign na língua do cliente e não há forma de passar o seu próprio texto: um parâmetro label que aceitava texto livre foi ignorado, com um aviso registado para o integrador. Uma notificação legal que possa ser reescrita é uma que pode ser suavizada para marketing, razão pela qual também não pode passar uma cor.
🚩 Passe strings, ou a notificação legal aparecerá em inglês para todos os clientes. O controlo desenha aquilo que recebe e não obtém as palavras sozinho — o mesmo contrato tem de se manter em todas as plataformas, incluindo uma em que um componente de IU não pode efetuar rede. A língua que obtém é decidida pela locale que configurou no Passo 2, porque a chamada de definições envia-a. Se a obtenção falhar, o controlo ainda aparece, em inglês, em vez de não aparecer: não disponibilizar uma função de desistência encontrável ao consumidor é uma sua violação; mostrá-la na língua errada, não. Registe o erro; nunca o mostre ao cliente.
Aspeto — .light, .dark ou .auto (valor predefinido), e nada mais. O controlo pinta o seu próprio cartão e o seu próprio texto, pelo que é legível em qualquer ecrã. .auto segue o aspeto do dispositivo num iPhone, iPad ou Mac e é sempre escuro num Apple TV. Não há forma de passar uma cor.
Uma limitação atual no Apple. A data no reconhecimento é formatada de acordo com a localidade do dispositivo, em vez da do cliente.
A etiqueta é sua para escolher, não para escrever. labelKey é a escolha que fez no painel de controlo entre as duas etiquetas sancionadas — Withdraw from contract here ou cancelar aqui — e o controlo mostra as palavras do servidor para esta etiqueta na língua do cliente. Se desenhar o seu próprio controlo em vez de WithdrawalView, submeta através de try await SubSovereign.shared.withdraw(subscriptionId:); é exatamente o que o controlo faz e devolve o mesmo WithdrawalReceipt.
Mostra o botão etiquetado, confirma uma vez — sem oferta de retenção ou inquérito antes, como exige a lei — envia idempotentemente (pressionar Tentar novamente após uma falha reutiliza o mesmo ID, pelo que uma nova tentativa não pode criar uma segunda desistência) e mostra o reconhecimento com a respetiva data. A decisão sobre enabled cabe a si, como acima; o controlo não efetua qualquer rede para além da submissão.
Passo 6 — Privacidade e GDPR
Registe o consentimento onde o recolher e cumpra as expetativas da Apple relativamente aos direitos de dados — o SDK expõe o consentimento, a opção de não venda CCPA, a eliminação e a exportação:
try await SubSovereign.shared.recordConsent(purpose: "analytics", granted: true)
// CCPA "do not sell" / Global Privacy Control.
// `true` turns the customer's OPT-OUT on; pass `false` when they opt back in.
try await SubSovereign.shared.setDoNotSell(enabled: true)
// If the user asks to be forgotten / to get their data:
try await SubSovereign.shared.requestErasure()
let myData = try await SubSovereign.shared.exportMyData()
purpose tem de ser um de analytics, marketing, personalisation ou consumption_data_sharing — exatamente estes, em minúsculas. Qualquer outro valor lança .invalidArgument e nada é registado: um registo de consentimento é um registo legal e o SDK não vai registar um para um propósito sobre o qual o seu cliente nunca foi questionado. jurisdiction tem como predefinição "GDPR" e policyVersion "1.0" — passe os seus próprios valores se forem diferentes.
Passo 7 — Marcadores de funcionalidades e atribuição
Os marcadores que definiu no painel de controlo e a atribuição do primeiro contacto. Ambos são de tentativa de melhor esforço: nunca lançam erros e, quando algo corre mal, indicam-no no registo unificado (subsistema com.subsovereign.sdk) em vez de ao cliente — um marcador que falta, não um booleano, ou inacessível, é lido como desativado.
let flags = await SubSovereign.shared.getFeatureFlags()
if flags["new_paywall"] == true { showNewPaywall() }
// First touch wins on the server, so this is safe to call on every launch.
await SubSovereign.shared.recordAttributionTouch(utmSource: "newsletter", utmCampaign: "spring")
Tratamento de erros
Todas as chamadas que lançam erros falham com um SubSovereignError tipificado — .notConfigured, .networkError(Error), .serverError(code, message) ou .invalidArgument(message). Intercepte-o e decida o que fazer em vez de deixar que surja como uma falha:
- Sucesso — use o valor.
- Falha — registe-a, mantenha o utilizador com o último acesso conhecido, tente novamente mais tarde. Nunca bloqueie um utilizador pagante por causa de um problema momentâneo de rede.
Melhores práticas
- Verificar ao iniciar. Chame
checkEntitlements()quando o app iniciar (por exemplo, a partir de um.taskSwiftUI) para que a proteção esteja correta antes de o utilizador aceder a uma funcionalidade protegida. - Verificar novamente após comprar. Logo após uma
validateApplePurchasebem-sucedida, chame novamentecheckEntitlements()para que a IU se atualize imediatamente. - Nunca confie no cliente. Não armazene "é pro" no app e trate-o como verdade — pergunte ao servidor; o servidor verificou a transação com a Apple.
- Um
userIdpor utilizador real. Mantenha-o estável para que o acesso siga o utilizador entre os seus dispositivos Apple e reconfigurar quando o utilizador autenticado mudar.
Referência rápida
| Pretende… | Chame |
|---|---|
| Configurar o SDK | SubSovereign.shared.configure(config) |
| Ver o que o utilizador desbloqueou | try await checkEntitlements() → EntitlementResult |
| Mostrar o ecrã de pagamento remoto | try await getPaywallConfig() → PaywallConfig |
| Desenhar o ecrã de pagamento de forma nativa | PaywallView(config:onSelectProduct:) |
| Verificar uma compra StoreKit 2 | try await validateApplePurchase(transaction:accessLevelId:) |
| Concluir uma transação | await finishTransaction(transaction) |
| Proteger uma funcionalidade, falhando em segurança (fail-closed) | await hasAccess(accessLevelName:) → Bool |
| Obter as definições de desistência | try await getWithdrawalConfig() → WithdrawalConfig |
| Desenhar o controlo de cancelamento legal | WithdrawalView(subscriptionId:strings:labelKey:appearance:) |
| Submeter uma desistência a partir do seu próprio controlo | try await withdraw(subscriptionId:) → WithdrawalReceipt |
| Ler os marcadores de funcionalidades do painel de controlo | await getFeatureFlags() → [String: Bool] |
| Registar a atribuição do primeiro contacto | await recordAttributionTouch(utmSource:utmCampaign:) |
| Registar o consentimento GDPR | try await recordConsent(purpose:granted:) |
| Cumprir um pedido CCPA de não venda | try await setDoNotSell(enabled:) |
| Eliminar / exportar os dados de um utilizador | try await requestErasure() / exportMyData() |
Próximos passos
- Faça o mesmo noutras plataformas — os SDKs Android, Web/JavaScript e Roku seguem a mesma estrutura.
- É novo nestes conceitos? Leia Como funciona o SubSovereign.