SubSovereign op iOS — integratiehandleiding
Deze handleiding neemt je Apple-app (iPhone, iPad, Apple TV of Mac) van "Ik weet niet wie er voor me betaald heeft" naar "mijn app ontgrendelt de juiste functies voor de juiste gebruiker, geverifieerd op mijn eigen server". De handleiding is geschreven om stap voor stap te volgen — er wordt geen ervaring met abonnementssoftware verondersteld.
Wat SubSovereign voor je doet
Je gebruikers abonneren zich via de App Store. SubSovereign beantwoordt één vraag voor je app, betrouwbaar: waar heeft deze gebruiker daadwerkelijk voor betaald?
- Je app vraagt SubSovereign naar de toegangsrecht van de gebruiker — de toegang die ze hebben gekregen.
- De server-side ontvangstvalidatie vindt plaats op de server, direct met Apple (App Store Server API), zodat een aangepaste app een abonnement niet kan vervalsen.
- De paywall — het scherm dat je abonnementen aanbiedt — wordt op afstand geconfigureerd, zodat je prijzen, proefperiodes en teksten kunt wijzigen zonder een nieuwe app-versie uit te brengen.
- De herroepingsfunctie (Compliance Passport) — de wettelijk verplichte annuleringsoptie — wordt ook door de SDK getekend, in de taal van de klant, zodat een wettelijke mededeling nooit kan worden verzacht tot marketing.
- Het is self-hosted: SubSovereign draait op jouw infrastructuur, de gegevens van je gebruikers blijven bij jou, en er is geen inkomensdeling — je behoudt 100% van wat je gebruikers betalen.
Je vertrouwt het apparaat nooit. Het apparaat vraagt; de server beslist. Dezelfde code werkt op iOS, tvOS (Apple TV) en macOS.
Voordat je begint
Je hebt het volgende nodig:
- Een draaiende SubSovereign-server (jouw self-hosted deployment) — de SDK wijst naar de URL ervan.
- Een app geregistreerd in het dashboard, die je een
appIden een API-sleutel geeft (de referentie die je app gebruikt om met de server te communiceren). - Toegangsniveaus aangemaakt — de toegangsniveaus (tiers) die je app toekent, bijv.
pro, elk gekoppeld in het dashboard aan de App Store-product-ID’s die je gebruikers kopen. - StoreKit 2 ingesteld. SubSovereign valideert en volgt aankopen; het vervangt niet StoreKit. Behandel aankopen met StoreKit 2 zoals normaal — SubSovereign komt net na een geslaagde transactie om deze te verifiëren en vast te leggen.
Aan de codekant heb je Swift-concurrentie (async/await) en een deployment target van iOS 15 / tvOS 15 / macOS 12 of hoger nodig.
Stap 1 — Voeg de SDK toe
Voeg SubSovereign toe met Swift Package Manager — in Xcode: Bestand ▸ Pakketten toevoegen… en wijs naar het SubSovereign-SDK-pakket (of voeg het toe aan je Package.swift-afhankelijkheden). Importeer het vervolgens:
import SubSovereign
Stap 2 — Configureer eenmalig bij het opstarten van je app
Configureer de SDK één keer — een goede plek is de App-initialisator of direct na het inloggen van de gebruiker. Je geeft de SDK je API-sleutel, app-ID, de URL van je server, een stabiele identificatie voor deze gebruiker en hun taal.
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
)
)
Een paar dingen om te weten:
userIdis van jou. Gebruik wat stabiele ID je al hebt voor een ingelogde gebruiker, en dezelfde waarde elke keer, zodat de toegang met hen meegaat over apparaten. Als een andere gebruiker inlogt, roepconfigureopnieuw aan met de nieuweuserId.baseURLwijst naar jouw server — de standaardwaarde van de SDK is een placeholder; stel je eigen deployment in.- De SDK is geannoteerd met
@MainActor, dus roep deze aan vanaf de hoofdactor (SwiftUI-weergaven en.taskzijn prima).
Stap 3 — Controleer waartoe de gebruiker toegang heeft
Roep checkEntitlements() aan om te achterhalen wat de gebruiker heeft ontgrendeld. Doe dit bij het opstarten en opnieuw direct na een aankoop. Het is een async-aanroep die kan throwen, dus wikkel het in 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 is de snelle ja/nee op het resultaat dat je al hebt opgehaald. Om een functie in één regel af te schermen, gebruik je de methode met dezelfde naam — deze beantwoordt false bij elke fout, dus een netwerkstoring sluit niets per ongeluk open:
if await SubSovereign.shared.hasAccess(accessLevelName: "pro") {
unlockProFeatures()
}
Gebruik dit voor een goedkope afscherming van een enkele functie. Voor de app-brede toegangbeslissing gebruik je checkEntitlements() en de foutafhandeling hieronder onder Fouten afhandelen — tijdens een storing wil je een betalende klant niet buitensluiten, maar wel op hun laatste bekende toegang houden.
Als je app meer dan één tier heeft, kijk dan in result.entitlements — elke entiteit noemt de actieve toegangsniveau. Match op de naam die je aan het niveau hebt gegeven in het dashboard; accessLevelId is de interne ID van de server, niet die naam:
let isPro = result.entitlements.contains { $0.isActive && $0.accessLevelName.lowercased() == "pro" }
Elk toegangsrecht bevat ook expiresAt, willRenew en de store waar het vandaan komt. Het resultaat bevat ook fromCache, dat rapporteert of de server antwoordde vanuit zijn eigen cache — de SDK zelf houdt geen cache en elke aanroep gaat naar je server.
Stap 4 — Verkoop een abonnement
Toon de paywall
Haal de paywall op van de server in plaats van prijzen hard te coderen, zodat je een korting kunt houden of een proefperiode kunt wijzigen zonder een release:
if let paywall = try? await SubSovereign.shared.getPaywallConfig() {
renderPaywall(paywall) // headline, features, products…
} else {
renderFallbackPaywall() // your built-in default
}
PaywallConfig geeft je een headline, subheadline, een lijst met features, de products om aan te bieden (elk met een displayPrice, period, trialDays en optionele badge), de call-to-action-tekst en voettekst. Je bouwt het daadwerkelijke scherm — SubSovereign vertelt wat het moet zeggen.
Voltooi de aankoop, en verifieer deze
Voer de aankoop uit via StoreKit 2 zoals je normaal zou doen. Wanneer je een geverifieerde Transaction terugkrijgt, geef deze dan aan SubSovereign zodat de server deze direct met Apple kan valideren en het toegangsniveau kan toekennen:
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
}
}
Dat is het hele vertrouwensmodel: de aankoop is pas echt wanneer de server deze heeft bevestigd met Apple.
Stap 5 — De herroepingsfunctie (Compliance Passport)
Als je abonnementen verkoopt aan EU-consumenten, vereist Richtlijn (EU) 2023/2673 een duidelijk gelabelde herroepingsfunctie — de annuleeroptie. De SDK levert deze kant-en-klaar, en deze wordt in twee delen getekend: haal de instellingen voor je app op, en teken de controle met deze instellingen.
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,
strings: settings?.strings, // ← without this the notice is ENGLISH for everyone
appearance: .auto // .light, .dark or .auto — and nothing else
)
}
}
.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)")
}
}
}
}
Je kunt de tekst niet wijzigen. De woorden komen van jouw SubSovereign-server in de taal van de klant, en er is geen manier om je eigen tekst door te geven: een label-parameter die eerder vrije tekst accepteerde, wordt nu genegeerd, met een waarschuwing gelogd naar de integrator. Een wettelijke mededeling die kan worden herformuleerd, is er één die kan worden verzacht tot marketing — dezelfde reden waarom je geen kleur kunt doorgeven.
🚩 Geef strings door, anders staat de herroepingsfunctie in het Engels voor elke klant. De controle tekent wat er wordt doorgegeven en haalt de woorden niet zelf op — hetzelfde contract moet op elke platform gelden, inclusief één waar een UI-component geen netwerk kan uitvoeren. Welke taal je krijgt, wordt bepaald door de locale die je hebt geconfigureerd in Stap 2, omdat de instellingen-aanroep deze doorgeeft. Als de ophaling mislukt, verschijnt de controle nog steeds, in het Engels, in plaats van niet te verschijnen: het niet tonen van een vindbare herroepingsfunctie aan de consument is jouw overtreding; het tonen in de verkeerde taal niet. Log de fout; toon deze nooit aan de klant.
Uiterlijk — .light, .dark of .auto (de standaard), en niets anders. De controle tekent zijn eigen kaart en zijn eigen tekst, zodat deze leesbaar is op elk scherm. .auto volgt de apparaatvoorkeur op een iPhone, iPad of Mac en is altijd donker op een Apple TV. Er is opzettelijk geen manier om een kleur door te geven.
Twee beperkingen op Apple vandaag. Het label dat in de console is gekozen, wordt hier nog niet toegepast, en de datum op de erkenning wordt opgemaakt volgens de apparaatvoorkeur van de gebruiker in plaats van de taal van de klant.
De controle toont de gelabelde knop, bevestigt eenmaal — zonder retentieaanbod of enquête ervoor, zoals de wet vereist — dient idempotent in (opnieuw proberen na een storing gebruikt dezelfde id, dus een herhaling kan geen tweede herroeping maken), en toont de erkenning met de datum. Het afschermen op enabled is aan jou; de controle zelf voert geen netwerk uit buiten de submit.
Stap 6 — Privacy en GDPR
Registreer toestemming waar je deze verzamelt, en respecteer de verwachtingen van Apple op het gebied van gegevensrechten — de SDK biedt toestemming, de CCPA-do-not-sell-schakelaar, wissen en exporteren:
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 moet één van analytics, marketing, personalisation of consumption_data_sharing zijn — precies deze, lowercase. Alles anders gooit .invalidArgument en er wordt niets geregistreerd: een toestemmingsregistratie is een juridische registratie, en de SDK zal er geen aanmaken voor een doel waar de klant nooit om is gevraagd. jurisdiction staat standaard op "GDPR" en policyVersion op "1.0" — geef je eigen waarden door als ze afwijken.
Fouten afhandelen
Elke aanroep die throw genereert, mislukt met een getypeerd SubSovereignError — .notConfigured, .networkError(Error), .serverError(code, message) of .invalidArgument(message). Vang deze op en beslis wat je doet in plaats van deze als crash te laten zien:
- Succes — gebruik de waarde.
- Falen — log het, houd de gebruiker op hun laatste bekende toegang, probeer het later opnieuw. Sluit nooit een betalende gebruiker uit vanwege een kortstondige netwerkstoring.
Beste praktijken
- Controleer bij het opstarten. Roep
checkEntitlements()aan wanneer de app start (bijv. vanuit een SwiftUI.task) zodat de afscherming correct is voordat de gebruiker een afgesloten functie tegenkomt. - Controleer opnieuw na een aankoop. Direct na een geslaagde
validateApplePurchaseroep je opnieuwcheckEntitlements()aan, zodat de UI direct wordt bijgewerkt. - Vertrouw nooit de client. Sla "is pro" niet op in de app en behandel het als waarheid — vraag de server; de server heeft de transactie geverifieerd met Apple.
- Één
userIdper echte gebruiker. Houd deze stabiel zodat de toegang met de gebruiker meegaat over hun Apple-apparaten, en configureer opnieuw wanneer de ingelogde gebruiker verandert.
Snelle referentie
| Je wilt… | Roep aan |
|---|---|
| De SDK instellen | SubSovereign.shared.configure(config) |
| Zien wat de gebruiker heeft ontgrendeld | try await checkEntitlements() → EntitlementResult |
| De externe paywall tonen | try await getPaywallConfig() → PaywallConfig |
| Een StoreKit 2-aankoop verifiëren | try await validateApplePurchase(transaction:accessLevelId:) |
| Een transactie afronden | await finishTransaction(transaction) |
| Een functie afschermen, fail-closed | await hasAccess(accessLevelName:) → Bool |
| De herroepingsinstellingen ophalen | try await getWithdrawalConfig() → WithdrawalConfig |
| De wettelijke annuleercontrole tekenen | WithdrawalView(subscriptionId:strings:appearance:) |
| GDPR-toestemming registreren | try await recordConsent(purpose:granted:) |
| Een CCPA-do-not-sell-verzoek honoreren | try await setDoNotSell(enabled:) |
| De gegevens van een gebruiker wissen / exporteren | try await requestErasure() / exportMyData() |
Volgende stappen
- Doe hetzelfde op je andere platforms — de Android, Web/JavaScript en Roku-SDK’s volgen dezelfde structuur.
- Nieuw voor deze concepten? Lees Hoe SubSovereign werkt.