SubSovereign
All guides

SubSovereign sur iOS — guide d’intégration

Ce guide permet à votre application Apple (iPhone, iPad, Apple TV ou Mac) de passer de « Je ne sais pas qui m’a payé » à « Mon application débloque les bonnes fonctionnalités pour le bon utilisateur, vérifié sur mon propre serveur ». Il est conçu pour être suivi étape par étape, sans expérience préalable en outils d’abonnement requise.

Ce que SubSovereign fait pour vous

Vos utilisateurs s’abonnent via l’App Store. SubSovereign répond de manière fiable à une seule question pour votre application : pour quoi cet utilisateur a-t-il réellement payé ?

  • Votre application demande à SubSovereign le droit d’accès de l’utilisateur — les fonctionnalités qu’il a débloquées.
  • La validation du reçu s’effectue côté serveur, directement avec Apple (via l’API App Store Server), afin qu’une application modifiée ne puisse pas simuler un abonnement.
  • Le paywall — l’écran qui propose vos offres — est configuré à distance, ce qui vous permet de modifier les prix, les essais et les libellés sans publier une nouvelle version de l’application.
  • La fonction de rétractation (Compliance Passport) — le contrôle de résiliation légal — est également générée par le SDK, dans la langue de l’utilisateur, afin qu’un avis légal ne puisse jamais être adouci en argument marketing.
  • Il est auto-hébergé : SubSovereign s’exécute sur votre infrastructure, les données de vos utilisateurs restent chez vous, et il n’y a aucune part sur les revenus — vous conservez 100 % de ce que vos utilisateurs paient.

Vous ne faites jamais confiance à l’appareil. L’appareil pose la question ; le serveur décide. Le même code fonctionne sur iOS, tvOS (Apple TV) et macOS.

Avant de commencer

Vous aurez besoin de :

  1. Un serveur SubSovereign en cours d’exécution (votre déploiement auto-hébergé) — le SDK pointe vers son URL.
  2. Une application enregistrée dans le tableau de bord, qui vous fournit un appId et une clé API (l’identifiant que votre application utilise pour communiquer avec le serveur).
  3. Des niveaux d’accès créés — les niveaux d’accès (paliers) que votre application accorde, par exemple pro, chacun lié dans le tableau de bord aux identifiants de produits de l’App Store que vos utilisateurs achètent.
  4. StoreKit 2 configuré. SubSovereign valide et suit les achats ; il ne remplace pas StoreKit. Gérez les achats avec StoreKit 2 comme d’habitude — SubSovereign intervient juste après une transaction réussie pour la vérifier et l’enregistrer.

Côté code, vous aurez besoin de Swift concurrency (async/await) et d’une cible de déploiement de iOS 15 / tvOS 15 / macOS 12 ou ultérieure.

Étape 1 — Ajouter le SDK

Ajoutez SubSovereign avec Swift Package Manager — dans Xcode, Fichier ▸ Ajouter des packages… et pointez vers le package du SDK SubSovereign (ou ajoutez-le à vos dépendances dans Package.swift). Puis importez-le :

import SubSovereign

Étape 2 — Configurer une seule fois, au démarrage de l’application

Configurez le SDK une seule fois — un bon endroit est l’initialisation de votre App, ou juste après la connexion de l’utilisateur. Vous lui fournissez votre clé API, l’ID de l’application, l’URL de votre serveur, un identifiant stable pour cet utilisateur, et sa langue.

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

Quelques points importants à connaître :

  • userId vous appartient. Utilisez l’identifiant stable que vous utilisez déjà pour un utilisateur connecté, et la même valeur à chaque fois, afin que l’accès suive l’utilisateur sur tous ses appareils. Si un autre utilisateur se connecte, appelez configure à nouveau avec le nouveau userId.
  • baseURL pointe vers votre serveur — le SDK utilise par défaut un espace réservé ; définissez votre propre déploiement.
  • Le SDK est annoté @MainActor, donc appelez-le depuis l’acteur principal (les vues SwiftUI et .task conviennent).

Étape 3 — Vérifier ce à quoi l’utilisateur peut accéder

Appelez checkEntitlements() pour savoir ce que l’utilisateur a débloqué. Faites-le au lancement et à nouveau juste après un achat. Il s’agit d’un appel async qui peut throw, donc enveloppez-le dans 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 est la réponse binaire rapide sur le résultat que vous avez déjà récupéré. Pour protéger une fonctionnalité en une ligne, utilisez la méthode du même nom — elle renvoie false en cas d’erreur, afin qu’une panne réseau ne verrouille rien par accident :

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

Utilisez-la pour une protection simple sur une seule fonctionnalité. Pour la décision d’accès globale de l’application, utilisez checkEntitlements() et les conseils de gestion des erreurs ci-dessous — en cas de panne, vous préférerez maintenir un client payant sur son dernier accès connu plutôt que de le bloquer.

Si votre application propose plusieurs paliers, examinez result.entitlements — chacun porte le nom du niveau d’accès actif. Comparez avec le nom que vous avez donné au niveau dans le tableau de bord ; accessLevelId est l’identifiant interne du serveur, pas ce nom :

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

Chaque droit d’accès inclut également expiresAt, willRenew et le store d’origine. Le résultat inclut aussi fromCache, qui indique si la réponse provient du cache du serveur — le SDK lui-même ne met rien en cache et chaque appel va vers votre serveur.

Étape 4 — Vendre un abonnement

Afficher le paywall

Récupérez le paywall depuis le serveur plutôt que de coder en dur les prix, afin de pouvoir lancer une promotion ou modifier un essai sans publier une nouvelle version :

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

PaywallConfig vous fournit un titre, un sous-titre, une liste de fonctionnalités, les produits à proposer (chacun avec un prixAffiché, une période, des joursEssai et un badge optionnel), le texte d’appel à l’action et le texte de pied de page. Vous construisez l’écran réel — SubSovereign lui indique quoi afficher.

Finaliser l’achat, puis le vérifier

Effectuez l’achat via StoreKit 2 comme vous le feriez normalement. Lorsque vous recevez une Transaction vérifiée, transmettez-la à SubSovereign afin que le serveur puisse la valider directement avec Apple et accorder le niveau d’accès :

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

Voilà tout le modèle de confiance : l’achat n’est réel que lorsque le serveur l’a confirmé avec Apple.

Étape 5 — La fonction de rétractation (Compliance Passport)

Si vous vendez des abonnements à des consommateurs de l’UE, la directive (UE) 2023/2673 impose une fonction de rétractation clairement étiquetée — le contrôle d’annulation. Le SDK l’intègre prêt à l’emploi, et il est généré en deux parties : récupérez les paramètres pour votre application, puis affichez le contrôle avec ceux-ci.

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

Vous ne pouvez pas modifier le libellé. Les mots proviennent de votre serveur SubSovereign dans la langue de l’utilisateur, et il n’existe aucun moyen de transmettre votre propre texte : un paramètre label permettait autrefois d’accepter du texte libre et est désormais ignoré, avec un avertissement enregistré pour l’intégrateur. Un avis légal dont le libellé peut être modifié est un avis qui peut être adouci en argument marketing, ce qui explique aussi pourquoi vous ne pouvez pas transmettre une couleur.

🚩 Transmettez des strings, sinon l’avis légal sera en anglais pour tous les utilisateurs. Le contrôle affiche exactement ce qu’on lui fournit et ne récupère pas lui-même les mots — le même contrat doit s’appliquer sur toutes les plateformes, y compris celles où un composant d’interface ne peut pas effectuer de requête réseau. La langue obtenue est déterminée par le locale que vous avez configuré à l’étape 2, car l’appel de paramètres l’envoie. En cas d’échec de la récupération, le contrôle apparaît quand même, en anglais, plutôt que de ne pas apparaître : omettre de placer une fonction de rétractation visible devant le consommateur constitue une violation de votre part ; l’afficher dans la mauvaise langue ne l’est pas. Enregistrez l’erreur ; ne l’affichez jamais au client.

Apparence — .light, .dark ou .auto (par défaut), et rien d’autre. Le contrôle génère sa propre carte et son propre texte, ce qui le rend lisible sur n’importe quel écran. .auto suit l’apparence de l’appareil sur un iPhone, iPad ou Mac, et est toujours en sombre sur un Apple TV. Il n’existe délibérément aucun moyen de transmettre une couleur.

Deux limites actuelles sur Apple. Le libellé choisi dans la console n’est pas encore appliqué ici, et la date sur l’accusé est formatée selon la locale de l’appareil plutôt que celle du client.

Il affiche le bouton étiqueté, confirme une seule fois — sans offre de rétention ni sondage avant, comme l’exige la loi — soumet de manière idempotente (appuyer sur Réessayer après un échec réutilise le même identifiant, donc une nouvelle tentative ne peut pas créer une seconde rétractation), et affiche l’accusé avec sa date. Le contrôle de l’activation (enabled) vous revient ; le contrôle lui-même n’effectue aucun réseau au-delà de la soumission.

Étape 6 — Confidentialité et RGPD

Enregistrez le consentement là où vous le collectez, et respectez les attentes d’Apple en matière de droits des données — le SDK expose le consentement, le commutateur CCPA de non-vente, l’effacement et l’export :

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 doit être l’un des suivants : analytics, marketing, personalisation ou consumption_data_sharing — exactement ceux-là, en minuscules. Tout autre valeur lève une erreur .invalidArgument et rien n’est enregistré : un enregistrement de consentement est un document juridique, et le SDK ne créera pas de fiche pour un objectif que votre client n’a jamais été invité à accepter. jurisdiction prend par défaut la valeur "GDPR" et policyVersion la valeur "1.0" — transmettez vos propres valeurs si elles diffèrent.

Gestion des erreurs

Chaque appel avec levée d’exception échoue avec un SubSovereignError typé — .notConfigured, .networkError(Error), .serverError(code, message) ou .invalidArgument(message). Interceptez-le et décidez de la marche à suivre plutôt que de laisser l’erreur remonter jusqu’à un plantage :

  • Succès — utilisez la valeur.
  • Échec — enregistrez-le, maintenez l’utilisateur sur son dernier accès connu, réessayez plus tard. Ne bloquez jamais un client payant à cause d’une panne réseau temporaire.

Bonnes pratiques

  • Vérifiez au lancement. Appelez checkEntitlements() au démarrage de l’application (par exemple depuis un .task SwiftUI) afin que la protection soit correcte avant que l’utilisateur n’accède à une fonctionnalité protégée.
  • Re-vérifiez après un achat. Juste après un validateApplePurchase réussi, appelez à nouveau checkEntitlements() afin que l’interface se mette à jour immédiatement.
  • Ne faites jamais confiance au client. Ne stockez pas "est pro" dans l’application et ne le traitez pas comme une vérité — demandez au serveur ; le serveur a vérifié la transaction avec Apple.
  • Un seul userId par utilisateur réel. Maintenez-le stable afin que l’accès suive l’utilisateur sur tous ses appareils Apple, et reconfigurez lorsque l’utilisateur connecté change.

Référence rapide

Vous souhaitez… Appelez
Configurer le SDK SubSovereign.shared.configure(config)
Voir ce que l’utilisateur a débloqué try await checkEntitlements()EntitlementResult
Afficher le paywall distant try await getPaywallConfig()PaywallConfig
Vérifier un achat StoreKit 2 try await validateApplePurchase(transaction:accessLevelId:)
Finaliser une transaction await finishTransaction(transaction)
Protéger une fonctionnalité, en mode échec-fermé await hasAccess(accessLevelName:)Bool
Récupérer les paramètres de rétractation try await getWithdrawalConfig()WithdrawalConfig
Afficher le contrôle d’annulation légal WithdrawalView(subscriptionId:strings:appearance:)
Enregistrer un consentement RGPD try await recordConsent(purpose:granted:)
Respecter une demande CCPA de non-vente try await setDoNotSell(enabled:)
Effacer / exporter les données d’un utilisateur try await requestErasure() / exportMyData()

Étapes suivantes