SubSovereign
All guides

SubSovereign auf iOS — Integrationshandbuch

Dieses Handbuch führt Ihre Apple-App (iPhone, iPad, Apple TV oder Mac) von „Ich habe keine Ahnung, wer mir etwas bezahlt hat“ zu „Meine App schaltet die richtigen Funktionen für den richtigen Nutzer frei – verifiziert auf meinem eigenen Server“. Es ist als Schritt-für-Schritt-Anleitung geschrieben und setzt keine Vorkenntnisse mit Abonnement-Tools voraus.

Was SubSovereign für Sie übernimmt

Ihre Nutzer abonnieren über den App Store. SubSovereign beantwortet eine Frage für Ihre App zuverlässig: Wofür hat dieser Nutzer tatsächlich bezahlt?

  • Ihre App fragt SubSovereign nach der Berechtigung des Nutzers – dem Zugang, den er durch einen Kauf oder ein Abo erhalten hat.
  • Die serverseitige Prüfung der Kaufbestätigung erfolgt direkt mit Apple (App Store Server API), sodass eine modifizierte App keine Abos vortäuschen kann.
  • Der Paywall – die Seite, auf der Ihre Pläne angeboten werden – wird remote konfiguriert. Sie können Preise, Testphasen und Texte ändern, ohne eine neue App-Version veröffentlichen zu müssen.
  • Die EU-Rücktrittsfunktion – die gesetzlich vorgeschriebene Kündigungssteuerung – wird ebenfalls vom SDK bereitgestellt, und zwar in der Sprache des Nutzers. So kann ein rechtlicher Hinweis nie zu Marketing verharmlost werden.
  • SubSovereign ist selbstgehostet: Es läuft auf Ihrer Infrastruktur, die Nutzerdaten bleiben bei Ihnen, und es gibt keine Gewinnbeteiligung – Sie behalten 100 % dessen, was Ihre Nutzer zahlen.

Sie vertrauen dem Gerät nie blind. Das Gerät fragt an; der Server entscheidet. Der gleiche Code läuft auf iOS, tvOS (Apple TV) und macOS.

Vorbereitung

Sie benötigen:

  1. Einen laufenden SubSovereign-Server (Ihre selbstgehostete Bereitstellung) – das SDK verweist auf dessen URL.
  2. Eine im Dashboard registrierte App, die Ihnen eine appId und einen API-Schlüssel (die Berechtigung, mit der Ihr App-Server mit SubSovereign kommuniziert) gibt.
  3. Erstellte Zugangsstufen – die Zugangsstufen (Tiers), die Ihre App gewährt, z. B. pro. Jede ist im Dashboard mit den Produkt-IDs des App Store verknüpft, die Ihre Nutzer kaufen.
  4. StoreKit 2 eingerichtet. SubSovereign validiert und verfolgt Käufe; es ersetzt StoreKit nicht. Behandeln Sie Käufe wie gewohnt mit StoreKit 2 – SubSovereign greift erst nach einer erfolgreichen Transaktion ein, um sie zu überprüfen und aufzuzeichnen.

Auf Code-Ebene benötigen Sie Swift Concurrency (async/await) und ein Bereitstellungsziel von iOS 15 / tvOS 15 / macOS 12 oder höher.

Schritt 1 – SDK hinzufügen

Fügen Sie SubSovereign mit Swift Package Manager hinzu – in Xcode unter Datei ▸ Pakete hinzufügen… und geben Sie die URL des SubSovereign-SDK-Pakets an (oder fügen Sie es Ihrer Package.swift-Abhängigkeit hinzu). Importieren Sie es dann:

import SubSovereign

Schritt 2 – Einmalige Konfiguration beim App-Start

Konfigurieren Sie das SDK einmalig – ein guter Ort ist der Initializer Ihrer App oder direkt nach der Anmeldung des Nutzers. Geben Sie den API-Schlüssel, die App-ID, die Server-URL, eine stabile Kennung für diesen Nutzer und dessen Sprache an.

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
    )
)

Einige wichtige Hinweise:

  • userId gehört Ihnen. Verwenden Sie eine stabile ID, die Sie bereits für einen angemeldeten Nutzer haben, und denselben Wert jedes Mal, damit der Zugang über Geräte hinweg verfolgt wird. Meldet sich ein anderer Nutzer an, rufen Sie configure erneut mit der neuen userId auf.
  • baseURL verweist auf Ihren Server – die SDK-Standardeinstellung ist ein Platzhalter; setzen Sie Ihre eigene Bereitstellung ein.
  • Das SDK ist mit @MainActor annotiert, rufen Sie es also vom Haupt-Actor auf (SwiftUI-Views und .task sind geeignet).

Schritt 3 – Prüfen, auf was der Nutzer zugreifen kann

Rufen Sie checkEntitlements() auf, um herauszufinden, was der Nutzer freigeschaltet hat. Führen Sie dies beim Start und erneut direkt nach einem Kauf aus. Es handelt sich um einen async-Aufruf, der throw kann, daher umschließen Sie ihn mit 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 ist die schnelle Ja/Nein-Antwort auf das Ergebnis, das Sie bereits abgerufen haben. Um eine Funktion mit einer Zeile zu schützen, verwenden Sie die Methode mit demselben Namen – sie gibt false bei jedem Fehler zurück, sodass ein Netzwerkproblem nichts versehentlich freischaltet:

if await SubSovereign.shared.hasAccess(accessLevelName: "pro") {
    unlockProFeatures()
}

Verwenden Sie dies für eine einfache Sperre einer einzelnen Funktion. Für die appweite Zugriffsentscheidung verwenden Sie checkEntitlements() und die Fehlerbehandlung unter Fehlerbehandlung unten – bei einem Ausfall möchten Sie einen zahlenden Kunden auf seinem letzten bekannten Zugang behalten, nicht aussperren.

Falls Ihre App mehrere Tiers hat, sehen Sie sich result.entitlements an – jede nennt die aktive Zugangsstufe. Vergleichen Sie mit dem Namen, den Sie der Stufe im Dashboard gegeben haben; accessLevelId ist die interne ID des Servers, nicht dieser Name:

let isPro = result.entitlements.contains { $0.isActive && $0.accessLevelName.lowercased() == "pro" }

Jede Berechtigung enthält auch expiresAt, willRenew und den store, von dem sie stammt. Das Ergebnis enthält zudem fromCache, das angibt, ob der Server aus seinem eigenen Cache geantwortet hat – das SDK selbst hält keinen Cache und jeder Aufruf geht an Ihren Server.

Schritt 4 – Ein Abo verkaufen

Den Paywall anzeigen

Laden Sie den Paywall vom Server, statt Preise hart zu kodieren, sodass Sie ein Angebot durchführen oder eine Testphase ändern können, ohne eine neue Version zu veröffentlichen:

if let paywall = try? await SubSovereign.shared.getPaywallConfig() {
    renderPaywall(paywall)   // headline, features, products…
} else {
    renderFallbackPaywall()  // your built-in default
}

PaywallConfig liefert Ihnen eine headline, subheadline, eine Liste von features, die products zum Anbieten (jeweils mit displayPrice, period, trialDays und optionalem badge), den Call-to-Action-Text und Fußzeilentext. Sie können die Seite selbst aus diesen Daten aufbauen – oder das SDK die Darstellung übernehmen lassen. PaywallView rendert den Paywall, den Sie im Dashboard mit SwiftUI entworfen haben, in den Farben des Tenants, auf einem iPhone, iPad oder Apple TV, und ruft Sie mit dem Produkt zurück, das der Kunde ausgewählt hat:

PaywallView(config: paywall) { productId in
    // start the StoreKit purchase for `productId` - see "Complete the purchase" below
}

Eine Farbe, die keine Farbe ist, erreicht nie den Bildschirm: Die View wählt eine lesbare Alternative und unsere eigene ausgegraute Textfarbe bleibt immer oberhalb der Lesbarkeitsgrenze, unabhängig von Ihren gewählten Farben.

Kauf abschließen und dann prüfen

Führen Sie den Kauf wie gewohnt über StoreKit 2 aus. Wenn Sie eine verifizierte Transaction zurückerhalten, übergeben Sie sie an SubSovereign, damit der Server sie direkt mit Apple validieren und die Zugangsstufe freischalten kann:

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
    }
}

Das ist das gesamte Vertrauensmodell: Der Kauf ist erst dann echt, wenn der Server ihn mit Apple bestätigt hat.

Schritt 5 – Die EU-Rücktrittsfunktion (Compliance Passport)

Falls Sie Abos an EU-Verbraucher verkaufen, verlangt die Richtlinie (EU) 2023/2673 eine klar gekennzeichnete Rücktrittsfunktion – die Kündigungssteuerung. Das SDK liefert sie fertig mit und stellt sie in zwei Teilen dar: Laden Sie die Einstellungen für Ihre App und zeichnen Sie dann die Steuerung mit diesen Einstellungen.

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)")
            }
        }
    }
}

Sie können die Formulierung nicht ändern. Die Worte stammen von Ihrem SubSovereign-Server in der Sprache des Nutzers, und es gibt keine Möglichkeit, eigenen Text zu übergeben: Ein früherer label-Parameter, der freien Text akzeptierte, wird nun ignoriert und es wird eine Warnung an den Integrator protokolliert. Ein gesetzlicher Hinweis, der umformuliert werden kann, ist einer, der zu Marketing verharmlost werden kann – aus demselben Grund können Sie auch keine Farbe übergeben.

🚩 Übergeben Sie strings, sonst erscheint der gesetzliche Hinweis für jeden Kunden auf Englisch. Die Steuerung rendert, was ihr übergeben wird, und holt sich die Worte nicht selbst – derselbe Vertrag muss auf jeder Plattform gelten, auch auf einer, bei der eine UI-Komponente kein Netzwerk durchführen kann. Die Sprache wird durch die locale bestimmt, die Sie in Schritt 2 konfiguriert haben, da der Einstellungsaufruf sie übermittelt. Falls der Abruf fehlschlägt, erscheint die Steuerung trotzdem auf Englisch, statt gar nicht zu erscheinen: Es ist Ihr Verstoß, dem Verbraucher keine auffindbare Rücktrittsfunktion zu präsentieren; die falsche Sprache ist es nicht. Protokollieren Sie den Fehler; zeigen Sie ihn niemals dem Kunden.

Aussehen – .light, .dark oder .auto (Standard) und nichts anderes. Die Steuerung malt ihre eigene Karte und ihren eigenen Text, sodass sie auf jedem Bildschirm lesbar ist. .auto folgt der Erscheinung des Geräts auf einem iPhone, iPad oder Mac und ist auf einem Apple TV immer dunkel. Es gibt bewusst keine Möglichkeit, eine Farbe zu übergeben.

Eine Einschränkung gilt heute bei Apple. Das Datum in der Bestätigung wird nach der Gebietsschema des Geräts formatiert, nicht nach der des Kunden.

Die Beschriftung wählen Sie aus, schreiben Sie sie nicht selbst. labelKey ist die Auswahl, die Sie in der Konsole zwischen den beiden zugelassenen Beschriftungen getroffen haben – Hier vom Vertrag zurücktreten oder Hier kündigen – und die Steuerung zeigt die serverseitigen Worte dafür in der Sprache des Kunden an. Falls Sie stattdessen eine eigene Steuerung zeichnen, statt WithdrawalView zu verwenden, rufen Sie try await SubSovereign.shared.withdraw(subscriptionId:) auf; genau das macht die Steuerung, und sie gibt dieselbe WithdrawalReceipt zurück.

Sie zeigt die beschriftete Schaltfläche, bestätigt einmalig – ohne Retentionsangebot oder Umfrage davor, wie das Gesetz es verlangt –, sendet idempotent (das erneute Drücken von Erneut versuchen nach einem Fehler verwendet dieselbe ID, sodass ein erneuter Versuch keinen zweiten Rücktritt erzeugen kann) und zeigt die Bestätigung mit ihrem Datum an. Die Sperre über enabled bleibt Ihnen überlassen, wie oben; die Steuerung selbst führt kein Netzwerk jenseits des Sendens aus.

Schritt 6 – Datenschutz und GDPR

Erfassen Sie die Einwilligung dort, wo Sie sie einholen, und halten Sie die Erwartungen von Apple an Datenrechte ein – das SDK stellt Einwilligungen, den CCPA-Do-not-sell-Schalter, Löschung und Export bereit:

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 muss eines von analytics, marketing, personalisation oder consumption_data_sharing sein – genau diese, kleingeschrieben. Alles andere wirft .invalidArgument auf und es wird nichts aufgezeichnet: Ein Einwilligungsdatensatz ist ein rechtlicher Datensatz, und das SDK wird keinen gegen einen Zweck anlegen, über den der Kunde nie informiert wurde. jurisdiction standardmäßig auf "GDPR" und policyVersion auf "1.0" – übergeben Sie Ihre eigenen Werte, falls sie abweichen.

Schritt 7 – Feature-Flags und Attribution

Flags, die Sie im Dashboard setzen, und First-Touch-Attribution. Beide sind Best-Effort: Sie werfen nie einen Fehler und teilen Probleme im einheitlichen Protokoll (Subsystem com.subsovereign.sdk) mit, statt sie dem Kunden anzuzeigen – ein fehlendes Flag, kein Boolean oder ein unerreichbarer Leseversuch wird als aus interpretiert.

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")

Fehlerbehandlung

Jeder aufrufende Code wirft einen typisierten SubSovereignError.notConfigured, .networkError(Error), .serverError(code, message), .invalidArgument(message) oder .invalidResponse(Error). Letzteres bedeutet, dass der Server über eine funktionierende Verbindung mit einer Antwort geantwortet hat, die das SDK nicht lesen konnte – ein fehlendes oder falsch formatiertes Feld –, sodass erneutes Senden nicht hilft; überprüfen Sie stattdessen den Server oder die SDK-Version, statt die Verbindung:

  • Erfolg – verwenden Sie den Wert.
  • Fehler – protokollieren Sie ihn, behalten Sie den Nutzer auf seinem letzten bekannten Zugang bei und versuchen Sie es später erneut. Sperren Sie niemals einen zahlenden Nutzer wegen eines kurzzeitigen Netzwerkproblems aus.

Best Practices

  • Beim Start prüfen. Rufen Sie checkEntitlements() beim Start der App auf (z. B. aus einer SwiftUI .task), sodass die Sperren korrekt sind, bevor der Nutzer auf eine gesperrte Funktion zugreift.
  • Nach einem Kauf erneut prüfen. Rufen Sie direkt nach einem erfolgreichen validateApplePurchase erneut checkEntitlements() auf, sodass sich die UI sofort aktualisiert.
  • Vertrauen Sie dem Client nie. Speichern Sie nicht „ist Pro“ in der App und behandeln Sie es als Wahrheit – fragen Sie den Server; der Server hat die Transaktion mit Apple validiert.
  • Eine userId pro echter Nutzer. Halten Sie sie stabil, sodass der Zugang über die Apple-Geräte des Nutzers hinweg verfolgt wird, und konfigurieren Sie sie neu, wenn sich der angemeldete Nutzer ändert.

Schnellreferenz

Sie möchten… Aufruf
SDK einrichten SubSovereign.shared.configure(config)
Sehen, was der Nutzer freigeschaltet hat try await checkEntitlements()EntitlementResult
Den entfernten Paywall anzeigen try await getPaywallConfig()PaywallConfig
Den Paywall nativ rendern PaywallView(config:onSelectProduct:)
Einen StoreKit-2-Kauf validieren try await validateApplePurchase(transaction:accessLevelId:)
Eine Transaktion abschließen await finishTransaction(transaction)
Eine Funktion schützen, fail-closed (fehlschlagend geschlossen) await hasAccess(accessLevelName:)Bool
Die Rücktrittseinstellungen abrufen try await getWithdrawalConfig()WithdrawalConfig
Die gesetzliche Kündigungssteuerung rendern WithdrawalView(subscriptionId:strings:labelKey:appearance:)
Einen Rücktritt über Ihre eigene Steuerung einreichen try await withdraw(subscriptionId:)WithdrawalReceipt
Die Feature-Flags des Dashboards lesen await getFeatureFlags()[String: Bool]
First-Touch-Attribution aufzeichnen await recordAttributionTouch(utmSource:utmCampaign:)
GDPR-Einwilligung erfassen try await recordConsent(purpose:granted:)
Eine CCPA-Do-not-sell-Anfrage erfüllen try await setDoNotSell(enabled:)
Nutzerdaten löschen / exportieren try await requestErasure() / exportMyData()

Nächste Schritte