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

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

Important

appliedExternalAttributionProviders solo reporta Apple Ads. La atribución de otros proveedores es visible en los perfiles de usuario y está disponible en los filtros de segmento, pero aún no aquí.

Antes de empezar

Necesitas:

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

Los ejemplos usan los nombres de la API de SDK 4.1. En SDK 4.0, la propiedad del perfil se llama appliedAttributionSources. En SDK 3.x, esa propiedad tiene el mismo nombre anterior, y los paywalls se obtienen con getPaywall/getPaywallForDefaultAudience, que devuelven un AdaptyPaywall. Consulta Migrar Adapty iOS SDK a v4.1.

Cómo funciona

Tras Adapty.activate(), el SDK solicita en segundo plano la atribución de Apple Ads a Apple 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 lo siguiente:

  • La atribución de Apple Ads aún no se ha procesado para este perfil.
  • No ha llegado ninguna atribución.
  • La atribución llegó de otro proveedor, que este array no reporta.

Incluso con un array vacío, getFlow es seguro de llamar — Adapty resuelve la solicitud según la audiencia que coincida con el estado actual del perfil, normalmente la audiencia predeterminada.

Important

La espera solo se aplica al primer lanzamiento. Una vez que se ha registrado la atribución de Apple Ads, se almacena permanentemente en el perfil. En cada lanzamiento posterior, el perfil en caché ya incluye .appleAds en appliedExternalAttributionProviders, didLoadLatestProfile se activa con ese valor de forma inmediata y getFlow 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 the paywall via Adapty.getFlow(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 se muestra a continuación compite con la atribución contra un tiempo de espera, precarga el paywall de la audiencia predeterminada en paralelo y devuelve el paywall que corresponda. El código que llama a esta función espera una única función async — sin delegados ni indicadores de estado que gestionar en el punto de llamada.

ProfileObserver es un singleton reutilizable que publica actualizaciones de perfil desde AdaptyDelegate. FlowLoader.getFlowOrDefault ejecuta la carrera usando un TaskGroup de concurrencia estructurada:

  • Si la atribución llega dentro del timeout, devuelve el paywall segmentado mediante getFlow(placementId:).
  • Si el timeout expira primero, devuelve el paywall de audiencia por defecto precargado mediante 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
        }
    }
}

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 flow = try await FlowLoader.getFlowOrDefault(
        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.