Afficher un paywall ciblé par Apple Ads au premier lancement dans iOS SDK

L’attribution Apple Ads (AA) arrive de manière asynchrone après Adapty.activate(). Si vous appelez getFlow trop tôt, l’attribution n’est souvent pas encore disponible et Adapty résout le placement sur l’audience par défaut — contournant ainsi vos paywalls segmentés par AA. AdaptyProfile.appliedExternalAttributionProviders permet à l’application de détecter quand l’attribution AA a été appliquée au profil, afin que la requête de paywall puisse attendre que la segmentation AA soit correctement résolue.

Important

appliedExternalAttributionProviders ne signale que les Apple Ads. L’attribution provenant d’autres fournisseurs est visible sur les profils d’utilisateurs et disponible dans les filtres de segment, mais pas encore ici.

Avant de commencer

Vous avez besoin de :

  • SDK iOS Adapty 4.1 ou version ultérieure.
  • Apple Ads configuré pour l’application dans Adapty. Voir Apple Ads.
Note

Les exemples utilisent les noms d’API du SDK 4.1. Avec le SDK 4.0, la propriété de profil s’appelle appliedAttributionSources. Avec le SDK 3.x, cette propriété porte le même nom plus ancien, et les paywalls sont récupérés avec getPaywall/getPaywallForDefaultAudience, qui renvoient un AdaptyPaywall. Voir Migrer le SDK iOS Adapty vers la v4.1.

Comment ça fonctionne

Après Adapty.activate(), le SDK demande l’attribution Apple Ads à Apple en arrière-plan et transmet le résultat au backend d’Adapty. Quand AA devient la source d’attribution active pour le profil, le SDK retourne un AdaptyProfile mis à jour dont le tableau appliedExternalAttributionProviders contient .appleAds.

Un tableau vide peut signifier l’une des choses suivantes :

  • L’attribution Apple Ads n’a pas encore été traitée pour ce profil.
  • Aucune attribution n’est arrivée du tout.
  • L’attribution est arrivée depuis un autre fournisseur, que ce tableau ne signale pas.

Même avec un tableau vide, getFlow peut être appelé en toute sécurité — Adapty résout la requête en fonction de l’audience qui correspond à l’état actuel du profil, généralement l’audience par défaut.

Important

L’attente ne s’applique qu’au premier lancement. Une fois l’attribution Apple Ads enregistrée, elle est stockée définitivement sur le profil. À chaque lancement suivant, le profil en cache contient déjà .appleAds dans appliedExternalAttributionProviders, didLoadLatestProfile se déclenche immédiatement avec cette valeur, et getFlow renvoie le paywall segmenté Apple Ads sans aucun délai.

Implémentation

Au premier lancement, surveillez .appleAds dans le profil et appliquez un délai d’attente strict — si l’attribution Apple Ads n’arrive jamais, ces utilisateurs doivent tout de même voir un paywall.

  1. Activez le SDK. Voir Installer et configurer le SDK iOS.
  2. Abonnez-vous aux mises à jour du profil en vous conformant à AdaptyDelegate et en implémentant didLoadLatestProfile. Si vous n’avez pas encore configuré le délégué, voir Écouter les mises à jour d’abonnement.
  3. Surveillez .appleAds dans appliedExternalAttributionProviders. Quand il apparaît, faites une requête pour le paywall — Adapty retournera la variante segmentée AA :
extension <YourAdaptyDelegateImpl>: AdaptyDelegate {
    nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
        if profile.appliedExternalAttributionProviders.contains(where: { $0 == .appleAds }) {
            // load the paywall via Adapty.getFlow(placementId:)
        }
    }
}
  1. Lancez un minuteur de 3 à 5 secondes en parallèle de l’abonnement. Si le minuteur se déclenche avant l’apparition de .appleAds, demandez le paywall quand même :

Quel que soit le chemin qui s’exécute en premier, c’est lui qui doit charger le paywall ; l’autre chemin doit être ignoré. Utilisez un seul indicateur d’état (par exemple, hasLoadedPaywall) pour éviter les doublons et ne pas récupérer le paywall deux fois. Configurez un paywall de secours pour le placement afin que l’utilisateur ne soit jamais bloqué en cas d’échec de la requête réseau.

Exemple complet

L’implémentation ci-dessous met en concurrence l’attribution avec un délai d’expiration, prérécupère le paywall de l’audience par défaut en parallèle, et retourne le paywall approprié. L’appelant attend une seule fonction async — pas de délégués ni d’indicateurs d’état à gérer côté appelant.

ProfileObserver est un singleton réutilisable qui publie les mises à jour de profil depuis AdaptyDelegate. FlowLoader.getFlowOrDefault exécute la course à l’aide d’un TaskGroup de concurrence structurée :

  • Si l’attribution arrive dans le délai imparti (timeout), le paywall segmenté est renvoyé via getFlow(placementId:).
  • Si le timeout expire en premier, le paywall de l’audience par défaut préchargé est renvoyé via getFlowForDefaultAudience(placementId:).

/// Demonstrates how to fetch a paywall that depends on attribution being applied,
/// falling back to the default-audience paywall if attribution doesn't arrive in time.
///
/// Stateless and self-contained: every call kicks off its own default-audience
/// prefetch and races it against attribution + segmented fetch.
enum FlowLoader {
    static func getFlowOrDefault(
        placementId: String,
        timeout: TimeInterval
    ) async throws -> AdaptyFlow {
        struct TimedOut: Error {}

        // Kick off the default-audience request immediately so it has the full
        // `timeout` window to load. We'll either cancel it on success or await
        // its result on timeout — never a duplicate network call.
        let defaultFlowTask = Task {
            try await Adapty.getFlowForDefaultAudience(placementId: placementId)
        }

        do {
            // Race two child tasks: whichever finishes first wins.
            let result = try await withThrowingTaskGroup(of: AdaptyFlow.self) { group in
                // 1. Wait for attribution, then ask Adapty for the segmented paywall.
                group.addTask {
                    await waitForAttribution()
                    return try await Adapty.getFlow(placementId: placementId)
                }
                // 2. Time-bomb: throws `TimedOut` after `timeout` seconds.
                group.addTask {
                    try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
                    throw TimedOut()
                }
                guard let value = try await group.next() else { throw CancellationError() }
                group.cancelAll() // stop the loser (sleeper or the attribution wait).
                return value
            }
            // Segmented paywall won — we no longer need the default-audience prefetch.
            defaultFlowTask.cancel()
            return result
        } catch is TimedOut {
            // Attribution didn't apply in time — return the prefetched default
            // (instant if already done, otherwise we await the in-flight request).
            return try await defaultFlowTask.value
        }
    }

    /// Suspends until a profile with the desired attribution source is observed.
    /// `@Published.values` emits the current profile immediately on subscription,
    /// so this returns on the first iteration if attribution is already applied.
    @MainActor
    private static func waitForAttribution() async {
        for await profile in ProfileObserver.shared.$profile.values {
            if profile?.appliedExternalAttributionProviders.contains(.appleAds) == true { return }
        }
    }
}

@MainActor
final class ProfileObserver: AdaptyDelegate {
    static let shared = ProfileObserver()

    @Published private(set) var profile: AdaptyProfile?

    nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
        Task { @MainActor [weak self] in
            self?.profile = profile
        }
    }
}

Connectez ProfileObserver à AdaptyDelegate une seule fois, après la fin de Adapty.activate() :

Adapty.delegate = ProfileObserver.shared

Appelez depuis l’écran de démarrage :

do {
    let flow = try await FlowLoader.getFlowOrDefault(
        placementId: "YOUR_PLACEMENT_ID",
        timeout: 5
    )
    // present the paywall
} catch {
    // handle the error or show a fallback paywall
}

Si votre application utilise déjà un AdaptyDelegate à d’autres fins (par exemple, écouter les mises à jour d’abonnement), transmettez didLoadLatestProfile à ProfileObserver.shared depuis votre délégué existant plutôt que de définir Adapty.delegate = ProfileObserver.shared.