SubSovereign localizzazione su iOS — guida all’integrazione
Questa guida accompagna la tua app Apple (iPhone, iPad, Apple TV o Mac) da “non so chi mi ha pagato” a “la mia app sblocca le funzionalità giuste per l’utente giusto, verificato sul mio server”. È scritta per essere seguita dall’inizio alla fine, senza richiedere esperienza pregressa con strumenti per abbonamenti.
Cosa fa SubSovereign per te
I tuoi utenti si abbonano tramite l’App Store. SubSovereign risponde in modo affidabile a una sola domanda per la tua app: cosa ha effettivamente pagato questo utente?
- La tua app chiede a SubSovereign il diritto di accesso dell’utente — ciò che ha sbloccato.
- La validazione della ricevuta avviene sul server, direttamente con Apple (tramite l’App Store Server API), quindi un’app modificata non può falsificare un abbonamento.
- Il paywall — la schermata che offre i tuoi piani — è configurato da remoto, quindi puoi modificare prezzi, prove gratuite e testi senza pubblicare una nuova versione dell’app.
- La funzione di recesso dalla legge — il controllo di cancellazione obbligatorio — è gestita anch’essa dall’SDK, nella lingua dell’utente, per evitare che una comunicazione legale venga attenuata in messaggio promozionale.
- È self-hosted: SubSovereign gira sulla tua infrastruttura, i dati dei tuoi utenti restano con te e non c’è alcuna percentuale sul ricavo — tu trattengo il 100% di ciò che pagano gli utenti.
Non ti fidi del dispositivo. Il dispositivo chiede; il server decide. Lo stesso codice funziona su iOS, tvOS (Apple TV) e macOS.
Prima di iniziare
Avrai bisogno di:
- Un server SubSovereign in esecuzione (la tua distribuzione self-hosted) — l’SDK punta al suo URL.
- Un’app registrata nella dashboard, che ti fornisce un
appIde una chiave API (la credenziale che la tua app usa per comunicare con il server). - Livelli di accesso creati — i livelli di accesso (tier) che la tua app concede, ad esempio
pro, ciascuno collegato nella dashboard agli ID dei prodotti dell’App Store che gli utenti acquistano. - StoreKit 2 configurato. SubSovereign valida e traccia gli acquisti; non sostituisce StoreKit. Gestisci gli acquisti con StoreKit 2 come di consueto — SubSovereign interviene subito dopo una transazione andata a buon fine per verificarla e registrarla.
Dal punto di vista del codice, ti servono Swift concurrency (async/await) e un target di distribuzione di iOS 15 / tvOS 15 / macOS 12 o versioni successive.
Passo 1 — Aggiungi l’SDK
Aggiungi SubSovereign tramite Swift Package Manager — in Xcode, vai su File ▸ Aggiungi pacchetti… e punta al pacchetto dell’SDK SubSovereign (o aggiungilo alle dipendenze del tuo Package.swift). Poi importalo:
import SubSovereign
Passo 2 — Configura una sola volta, all’avvio dell’app
Configura l’SDK una sola volta — un buon punto è l’inizializzazione della tua App, o subito dopo l’accesso dell’utente. Forniscigli la tua chiave API, l’ID dell’app, l’URL del tuo server, un identificatore stabile per questo utente e la sua lingua.
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
)
)
Ecco alcune cose da sapere:
userIdè tuo. Usa qualsiasi ID stabile che già utilizzi per un utente autenticato e lo stesso valore ogni volta, in modo che l’accesso segua l’utente tra i dispositivi. Se un utente diverso si autentica, richiamaconfiguredi nuovo con il nuovouserId.baseURLpunta al tuo server — il valore predefinito dell’SDK è un segnaposto; imposta il tuo deployment.- L’SDK è annotato con
@MainActor, quindi chiamalo dal main actor (le viste SwiftUI e.tasksono adatte).
Passo 3 — Verifica cosa può fare l’utente
Chiama checkEntitlements() per scoprire cosa l’utente ha sbloccato. Fallo all’avvio e di nuovo subito dopo un acquisto. È una chiamata async che può generare un errore (throw), quindi avvolgila in un 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 è il sì/no rapido sul risultato che hai già ottenuto. Per proteggere una singola funzionalità in una riga, usa il metodo con lo stesso nome — restituisce false in caso di errore, quindi un problema di rete non sblocca nulla:
if await SubSovereign.shared.hasAccess(accessLevelName: "pro") {
unlockProFeatures()
}
Usalo per una protezione economica su una singola funzionalità. Per la decisione di accesso a livello di app, usa checkEntitlements() e i consigli su Gestione degli errori qui sotto — durante un’interruzione, vuoi che un cliente pagante rimanga con l’accesso noto per ultimo, non che venga bloccato.
Se la tua app ha più livelli, guarda dentro result.entitlements — ciascuno indica il livello di accesso attivo. Abbinalo al nome che hai assegnato al livello nella dashboard; accessLevelId è l’ID interno del server, non quel nome:
let isPro = result.entitlements.contains { $0.isActive && $0.accessLevelName.lowercased() == "pro" }
Ogni diritto di accesso include anche expiresAt, willRenew e lo store da cui proviene. Il risultato include anche fromCache, che segnala se il server ha risposto dalla sua cache — l’SDK stesso non memorizza cache e ogni chiamata va al tuo server.
Passo 4 — Vendi un abbonamento
Mostra il paywall
Recupera il paywall dal server invece di codificare i prezzi, così puoi avviare una promozione o modificare una prova gratuita senza rilasciare una nuova versione:
if let paywall = try? await SubSovereign.shared.getPaywallConfig() {
renderPaywall(paywall) // headline, features, products…
} else {
renderFallbackPaywall() // your built-in default
}
PaywallConfig ti fornisce una headline, una subheadline, una lista di features, i products da offrire (ognuno con un displayPrice, period, trialDays e un badge opzionale), il testo della call-to-action e il testo del piè di pagina. Puoi costruire tu stesso la schermata da questi dati — o lasciare che l’SDK la disegni. PaywallView renderizza il paywall che hai progettato nella dashboard con SwiftUI nativo, nei colori del tenant, su iPhone, iPad o Apple TV, e ti restituisce il prodotto scelto dal cliente:
PaywallView(config: paywall) { productId in
// start the StoreKit purchase for `productId` - see "Complete the purchase" below
}
Un colore che non è un colore non raggiunge mai lo schermo: la vista sceglie un’alternativa leggibile e il nostro testo sbiadito viene mantenuto sopra il limite di leggibilità, indipendentemente dai colori che hai scelto.
Completa l’acquisto, poi verificalo
Esegui l’acquisto tramite StoreKit 2 come faresti normalmente. Quando ottieni una Transaction verificata, passala a SubSovereign in modo che il server possa validarla direttamente con Apple e concedere il livello di accesso:
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
}
}
Questo è l’intero modello di fiducia: l’acquisto è reale solo dopo che il server lo ha confermato con Apple.
Passo 5 — La funzione di recesso dalla legge (Compliance Passport)
Se vendi abbonamenti a consumatori dell’UE, la Direttiva (UE) 2023/2673 richiede una funzione di recesso chiaramente etichettata — il controllo di cancellazione. L’SDK la fornisce già pronta e viene disegnata in due fasi: recupera le impostazioni per la tua app, poi disegna il controllo con esse.
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)")
}
}
}
}
Non puoi modificare il testo. Le parole provengono dal tuo server SubSovereign nella lingua del cliente e non c’è modo di passare un testo personalizzato: un parametro label che in passato accettava testo libero ora è ignorato, con un avviso registrato nell’integratore. Una comunicazione legale che può essere riformulata è una comunicazione che può essere attenuata in messaggio promozionale, ed è per questo che non puoi passare un colore.
🚩 Passa le strings, altrimenti la comunicazione di recesso sarà in inglese per ogni cliente. Il controllo visualizza ciò che gli viene passato e non recupera le parole da solo — lo stesso contratto deve valere su ogni piattaforma, inclusa quella in cui un componente UI non può fare rete. La lingua che ottieni è decisa dal locale che hai configurato al Passo 2, perché la chiamata delle impostazioni lo invia. Se il recupero fallisce, il controllo appare comunque, in inglese, piuttosto che non apparire: non fornire una funzione di recesso visibile al consumatore è una tua violazione; mostrarla nella lingua sbagliata non lo è. Registra l’errore; non mostrarlo mai al cliente.
Aspetto — .light, .dark o .auto (predefinito), e nient’altro. Il controllo disegna la propria scheda e il proprio testo, quindi è leggibile su qualsiasi schermo. .auto segue l’aspetto del dispositivo su iPhone, iPad o Mac ed è sempre scuro su Apple TV. Non c’è modo di passare un colore.
Un limite su Apple oggi. La data sull’avviso è formattata secondo il locale del dispositivo, non del cliente.
Il label è una tua scelta, non una tua scrittura. labelKey è la scelta che hai fatto nella console tra le due etichette autorizzate — Recedi dal contratto qui o Disdici qui — e il controllo mostra le parole del server per esso nella lingua del cliente. Se disegni tu stesso il controllo invece di WithdrawalView, invia tramite try await SubSovereign.shared.withdraw(subscriptionId:); è esattamente ciò che fa il controllo e restituisce la stessa WithdrawalReceipt.
Mostra il pulsante etichettato, conferma una volta — senza offerte di retention o sondaggi prima, come richiede la legge — invia idempotentemente (premere Riprova dopo un errore riutilizza lo stesso ID, quindi un nuovo tentativo non può creare un secondo recesso), e mostra l’avviso con la sua data. Spetta a te applicare un controllo su enabled, come sopra; il controllo stesso non fa rete oltre alla sottomissione.
Passo 6 — Privacy e GDPR
Registra il consenso dove lo raccogli e rispetta le aspettative di Apple in materia di diritti sui dati — l’SDK espone il consenso, l’interruttore CCPA per la non vendita, la cancellazione e l’esportazione:
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 deve essere uno tra analytics, marketing, personalisation o consumption_data_sharing — esattamente questi, in minuscolo. Tutto il resto genera un errore .invalidArgument e nulla viene registrato: un consenso è un atto legale e l’SDK non registrerà un consenso per uno scopo per cui il cliente non è stato interpellato. jurisdiction predefinito è "GDPR" e policyVersion è "1.0" — passali tuoi se differiscono.
Passo 7 — Feature flag e attribuzione
I flag che imposti nella dashboard e l’attribuzione del primo contatto. Entrambi sono best-effort: non generano mai errori e, in caso di problema, lo segnalano nel log unificato (sottosistema com.subsovereign.sdk) piuttosto che all’utente — un flag mancante, non un booleano, o irraggiungibile viene interpretato come spento.
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")
Gestione degli errori
Ogni chiamata che genera errori restituisce un SubSovereignError tipizzato — .notConfigured, .networkError(Error), .serverError(code, message) o .invalidArgument(message). Intercettalo e decidi cosa fare invece di lasciarlo emergere come crash:
- Successo — usa il valore.
- Fallimento — registralo, mantieni l’utente con il suo ultimo accesso noto, riprova dopo. Non bloccare mai un utente pagante perché di un problema di rete momentaneo.
Best practice
- Verifica all’avvio. Chiama
checkEntitlements()quando l’app si avvia (ad esempio da un.taskSwiftUI) in modo che la protezione sia corretta prima che l’utente raggiunga una funzionalità bloccata. - Verifica dopo l’acquisto. Subito dopo una
validateApplePurchaseandata a buon fine, richiamacheckEntitlements()di nuovo in modo che l’interfaccia si aggiorni immediatamente. - Non fidarti del client. Non memorizzare “è pro” nell’app e trattarlo come verità — chiedi al server; il server ha verificato la transazione con Apple.
- Un
userIdper utente reale. Mantienilo stabile in modo che l’accesso segua l’utente tra i suoi dispositivi Apple e riconfigura quando cambia l’utente autenticato.
Riferimento rapido
| Vuoi… | Chiama |
|---|---|
| Configurare l’SDK | SubSovereign.shared.configure(config) |
| Vedere cosa ha sbloccato l’utente | try await checkEntitlements() → EntitlementResult |
| Mostrare il paywall da remoto | try await getPaywallConfig() → PaywallConfig |
| Disegnare il paywall in modo nativo | PaywallView(config:onSelectProduct:) |
| Verificare un acquisto StoreKit 2 | try await validateApplePurchase(transaction:accessLevelId:) |
| Completare una transazione | await finishTransaction(transaction) |
| Proteggere una funzionalità, fail-closed | await hasAccess(accessLevelName:) → Bool |
| Recuperare le impostazioni di recesso | try await getWithdrawalConfig() → WithdrawalConfig |
| Disegnare il controllo di cancellazione obbligatorio | WithdrawalView(subscriptionId:strings:labelKey:appearance:) |
| Inviare un recesso dal tuo controllo | try await withdraw(subscriptionId:) → WithdrawalReceipt |
| Leggere i feature flag della dashboard | await getFeatureFlags() → [String: Bool] |
| Registrare l’attribuzione del primo contatto | await recordAttributionTouch(utmSource:utmCampaign:) |
| Registrare il consenso GDPR | try await recordConsent(purpose:granted:) |
| Rispettare una richiesta CCPA di non vendita | try await setDoNotSell(enabled:) |
| Cancellare / esportare i dati di un utente | try await requestErasure() / exportMyData() |
Passi successivi
- Fai lo stesso sulle altre piattaforme — gli SDK Android, Web/JavaScript e Roku seguono la stessa struttura.
- Nuovo a questi concetti? Leggi Come funziona SubSovereign.