Mostrar un paywall orientado a AA en el primer lanzamiento con el SDK de iOS

Apple Ads (AA) la atribución llega de forma asíncrona después de Adapty.activate(). Si llamas a getPaywall demasiado pronto, la atribución puede no haber llegado aún y Adapty resuelve el placement según la audiencia por defecto, omitiendo tus paywalls segmentados por AA. AdaptyProfile.appliedExternalAttributionProviders permite a la app detectar cuándo se ha aplicado la atribución de AA al perfil, de modo que la solicitud de paywall pueda esperar hasta que la segmentación de AA se resuelva correctamente.

Antes de empezar

Necesitas:

  • Adapty iOS SDK 3.17.1 o posterior.
  • Apple Ads configurado para la app en Adapty. Consulta Apple Ads.
Note

Los ejemplos a continuación usan el nombre de propiedad del SDK 4.1+ appliedExternalAttributionProviders. En las versiones del SDK 3.17.1–4.0, la propiedad se llama appliedAttributionSources.

Cómo funciona

Después de Adapty.activate(), el SDK solicita la atribución de Apple Ads a Apple en segundo plano y reenvía el resultado al backend de Adapty. Cuando AA se convierte en la fuente de atribución activa del perfil, el SDK entrega un AdaptyProfile actualizado cuyo array appliedExternalAttributionProviders contiene .appleAds.

Un array vacío puede significar cualquiera de las siguientes situaciones:

  • La atribución de Apple Ads aún no ha sido procesada para este perfil.
  • No ha llegado ninguna atribución.

Incluso con un array vacío, getPaywall sigue siendo seguro de llamar — Adapty resuelve la solicitud contra la audiencia que coincida con el estado actual del perfil, normalmente la audiencia por defecto.

Important

La espera solo aplica al primer inicio. Una vez que se ha registrado la atribución de Apple Ads, queda almacenada en el perfil de forma permanente. En cada inicio posterior, el perfil en caché ya incluye .appleAds en appliedExternalAttributionProviders, didLoadLatestProfile se activa con ese valor de inmediato, y getPaywall devuelve el paywall segmentado por Apple Ads sin ningún retraso.

Implementación

En el primer lanzamiento, controla la aparición de .appleAds en el perfil y aplica un timeout estricto: si la atribución de Apple Ads nunca llega, esos usuarios igualmente deben ver un paywall.

  1. Activa el SDK. Consulta Instalar y configurar el SDK de iOS.
  2. Suscríbete a las actualizaciones del perfil implementando AdaptyDelegate y el método didLoadLatestProfile. Si aún no has configurado el delegado, consulta Escuchar actualizaciones de suscripción.
  3. Observa .appleAds en appliedExternalAttributionProviders. Cuando aparezca, solicita el paywall — Adapty devolverá la variante segmentada por AA:
extension <YourAdaptyDelegateImpl>: AdaptyDelegate {
    nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
        if profile.appliedExternalAttributionProviders.contains(where: { $0 == .appleAds }) {
            // load paywall via Adapty.getPaywall(placementId:)
        }
    }
}
  1. Inicia un temporizador de 3 a 5 segundos en paralelo con la suscripción. Si el temporizador se dispara antes de que aparezca .appleAds, solicita el paywall de todas formas: Cualquiera de los dos caminos que se active primero debe cargar el paywall; el otro debe ignorarse. Usa un único indicador de estado (por ejemplo, hasLoadedPaywall) para deduplicar y evitar que el paywall se solicite dos veces. Configura un paywall de respaldo para el placement para que el usuario nunca se quede bloqueado si la solicitud de red falla.

Ejemplo completo

La implementación que sigue ejecuta en paralelo la espera de la atribución con un timeout y la precarga del paywall de la audiencia por defecto, devolviendo el paywall apropiado según el resultado. El código que llama solo necesita hacer await a una única función asíncrona: sin delegados ni flags de estado en el punto de llamada.

ProfileObserver es un singleton reutilizable que publica actualizaciones del perfil desde AdaptyDelegate. PaywallLoader.getPaywallOrDefault ejecuta la carrera mediante un TaskGroup de concurrencia estructurada:

  • Si la atribución llega dentro del timeout, devuelve el paywall segmentado mediante getPaywall(placementId:).
  • Si el timeout expira primero, devuelve el paywall de la audiencia predeterminada prefetchado mediante getPaywallForDefaultAudience(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 PaywallLoader {
    static func getPaywallOrDefault(
        placementId: String,
        timeout: TimeInterval
    ) async throws -> AdaptyPaywall {
        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 defaultPaywallTask = Task {
            try await Adapty.getPaywallForDefaultAudience(placementId: placementId)
        }

        do {
            // Race two child tasks: whichever finishes first wins.
            let result = try await withThrowingTaskGroup(of: AdaptyPaywall.self) { group in
                // 1. Wait for attribution, then ask Adapty for the segmented paywall.
                group.addTask {
                    await waitForAttribution()
                    return try await Adapty.getPaywall(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.
            defaultPaywallTask.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 defaultPaywallTask.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
        }
    }
}

Conecta ProfileObserver a AdaptyDelegate una vez, después de que Adapty.activate() termine:

Adapty.delegate = ProfileObserver.shared

Llámalo desde la pantalla de inicio:

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

Si tu app ya usa un AdaptyDelegate para otros fines (por ejemplo, escuchar actualizaciones de suscripción), reenvía didLoadLatestProfile a ProfileObserver.shared desde tu delegado existente en lugar de establecer Adapty.delegate = ProfileObserver.shared.