Internal handoff · iOS

App launch setup for beginners

This document explains how the app starts: Splash → ATT → Adjust → OneSignal tags → In-App Message → Home. Follow it step by step. You do not need deep SDK experience.

1. Who does what

Area Owner
Splash screen UI, ATT prompt, Adjust SDK, fetching IDs, sending tags, waiting for OneSignal user, firing app_ready, navigating to Home You (app developer)
OneSignal dashboard: In-App Message HTML, Full Screen layout, trigger app_ready = true, offer URL via WebView replacement (not iframe); LP close uses error:// Already done by the OneSignal owner — do not recreate or change it
Your goal Make the iOS app initialize SDKs correctly, send the right tags, fire the trigger, and only then move from Splash to Home — while the In-App Message can stay on top. OneSignal App ID and Adjust token are provided by the human in the implementation prompt (not guessed).

2. What we want the user to see

  1. App opens → Splash image fills the screen.
  2. iOS may ask tracking permission (ATT).
  3. While still on splash, an In-App Message (IAM) appears (offer page).
  4. After the IAM is visible (or after a timeout), the app switches to Home underneath the IAM (no animation flash).
  5. The IAM stays on top. Home is ready behind it.

The offer is shown by OneSignal IAM (not a custom in-app browser screen).

3. Simple words (glossary)

TermMeaning
Splash The first full-screen image while the app prepares.
ATT App Tracking Transparency — Apple’s “Allow tracking?” popup (iOS 14+).
Adjust Attribution SDK. Gives us adid, and helps with idfa / idfv / campaign.
OneSignal Used here for In-App Messages only (not push notifications in this flow).
IAM In-App Message — a full-screen web message shown by OneSignal over the app.
Tag A key/value saved on the OneSignal user (example: idfa → device ID).
Trigger A condition that tells OneSignal “show this IAM now”. We use app_ready = true.
onesignalId OneSignal’s user id. We wait for it (with timeout) before relying on tags/IAM.

4. The launch flow (order of work)

Work in parallel where possible. Do not wait one-after-another if steps can overlap.

1

App launch (AppDelegate)

Initialize OneSignal immediately. Keep IAM paused so it does not show too early.

2

Show Splash (RootView)

Start the launch coordinator. Show splash image.

3

Start two things together

(A) Wait for OneSignal user id (max ~12 seconds)
(B) Show ATT → then init Adjust → fetch adid, idfa, idfv in parallel
Also listen for Adjust attribution and send campaign when it arrives (do not block for it).

4

Send tags to OneSignal

After Adjust IDs are ready and OneSignal user wait finished (or timed out), call addTags with mmp_id, idfa, idfv.

5

Unpause IAM + set trigger

Only after tags: addTrigger("app_ready", "true") then unpause. Wait until IAM did display (or timeout ~20s). Do not insert a multi-second sleep after tags.

6

Go to Home

After IAM is shown (+ short hold) or timeout: switch to Home with animations disabled. IAM can stay on top.

Important Wall-clock time should be roughly max(ATT+Adjust IDs, OneSignal user) plus IAM display time — not the sum of every step one after another.

5. Dependencies, frameworks & Info.plist

OneSignal (SPM / packages to link)

Add the OneSignal iOS SDK (5.x) and link these products to your app target:

Product / frameworkRequired?Why
OneSignalFramework Yes Core OneSignal SDK (user, tags, init).
OneSignalInAppMessages Yes In-App Messages (IAM). Without this, IAM will not show.
OneSignalLocation No Not needed for this flow.
OneSignalExtension No Notification Service Extension only — do not use for this IAM-only flow.
Do not ask for push permission You do not need push notification capabilities or the Notification Service Extension for this flow.

Adjust (SPM + system frameworks)

Link AdjustSdk (not Adjust WebBridge). Also add these system frameworks to the app target (usually Optional / Weak):

FrameworkRequired?Why
AdjustSdk (SPM) Yes Attribution, adid, campaign, ID helpers.
AdSupport.framework Yes IDFA access.
AppTrackingTransparency.framework Yes ATT permission prompt (iOS 14+).
AdServices.framework Yes (recommended) Apple Search Ads / AdServices attribution support.
StoreKit.framework Yes (recommended) SKAdNetwork APIs.
WebKit.framework Yes (recommended) Used with Adjust / web surfaces; also needed for OneSignal IAM WebViews.
AdjustWebBridge No Not used in this flow — do not link it.

In Xcode: select the app target → GeneralFrameworks, Libraries, and Embedded Content (and/or Package Products).

Info.plist keys you must have

KeyWhy
NSUserTrackingUsageDescription Required by Apple for the ATT popup text.
OneSignal_in_app_message_hide_gray_overlay = true Cleaner full-screen IAM (less gray chrome).
OneSignal_in_app_message_hide_drop_shadow = true Cleaner full-screen IAM (no drop shadow).
NSAdvertisingAttributionReportEndpoint Send SKAdNetwork postback copies to Adjust: https://adjust-skadnetwork.com
AttributionCopyEndpoint AdAttributionKit postback copies to Adjust: https://adjust-skadnetwork.com
SKAdNetworkItems Network IDs for Meta / Google / TikTok / Snapchat — see next section.

6. SKAdNetwork (Meta, Google, TikTok, Snapchat)

Add these IDs under SKAdNetworkItems in Info.plist so Apple can attribute installs when you advertise on those networks (with Adjust). All values must be lowercase.

NetworkSKAdNetworkIdentifier
Meta (Facebook)v9wttpbfk9.skadnetwork
Meta (Instagram)n38lu8286q.skadnetwork
Googlecstr6suwn9.skadnetwork
TikTok22mmun2rn5.skadnetwork
TikTok / Pangle238da6jt44.skadnetwork
TikTok / Panglegta9lk7p23.skadnetwork
Snapchat424m5254lk.skadnetwork
Snapchat8s468mfl3y.skadnetwork
<key>SKAdNetworkItems</key>
<array>
  <dict><key>SKAdNetworkIdentifier</key><string>v9wttpbfk9.skadnetwork</string></dict>
  <dict><key>SKAdNetworkIdentifier</key><string>n38lu8286q.skadnetwork</string></dict>
  <dict><key>SKAdNetworkIdentifier</key><string>cstr6suwn9.skadnetwork</string></dict>
  <dict><key>SKAdNetworkIdentifier</key><string>22mmun2rn5.skadnetwork</string></dict>
  <dict><key>SKAdNetworkIdentifier</key><string>238da6jt44.skadnetwork</string></dict>
  <dict><key>SKAdNetworkIdentifier</key><string>gta9lk7p23.skadnetwork</string></dict>
  <dict><key>SKAdNetworkIdentifier</key><string>424m5254lk.skadnetwork</string></dict>
  <dict><key>SKAdNetworkIdentifier</key><string>8s468mfl3y.skadnetwork</string></dict>
</array>

<key>NSAdvertisingAttributionReportEndpoint</key>
<string>https://adjust-skadnetwork.com</string>
<key>AttributionCopyEndpoint</key>
<string>https://adjust-skadnetwork.com</string>
Also in Adjust / ad dashboards (not Xcode) Connect Meta, Google, TikTok, and Snap partners in Adjust. For Snapchat, create a Snap App ID and paste it into Adjust.

7. Files you will create / touch

Keep launch / IAM code separate from your product UI:

YourApp/
  Launch/          ← Splash, ATT, Adjust, OneSignal, IAM gate
    AppDelegate.swift
    LaunchCoordinator.swift   (SplashView + RootView)
    OneSignalManager.swift
    AdjustManager.swift
    IAMErrorSchemeBridge.swift  (error:// close + scroll)
    WaitingTimeLog.swift      (optional)
  App/             ← Your real home / product screens
    HomeView.swift            (sample uses ContentView)
  YourApp.swift               (@main → RootView)
FileFolderResponsibility
AppDelegate.swift Launch/ Initialize OneSignal as early as possible (IAM paused).
LaunchCoordinator.swift Launch/ Orchestrates splash → ATT → Adjust → tags → IAM → Home.
OneSignalManager.swift Launch/ Init, tags, wait for user id, trigger, IAM lifecycle.
AdjustManager.swift Launch/ ATT, Adjust init, fetch IDs, campaign callback → tag.
IAMErrorSchemeBridge.swift Launch/ Dismiss IAM on LP error://; re-enable WebView scroll for long offer pages.
WaitingTimeLog.swift (optional) Launch/ Debug timing logs. Filter console by waitingTime.
HomeView.swift (or your main UI) App/ Product home only — not part of the IAM launch flow.
YourApp.swift (@main) root Attach AppDelegate + show RootView.

In RootView, replace HomeView() / ContentView() with your real home screen. Full samples: .

8. OneSignal in the app (your job)

Use your real OneSignal App ID in code.

// Pseudo-code
OneSignal.initialize(appId, withLaunchOptions: launchOptions)
OneSignal.InAppMessages.paused = true   // IMPORTANT: do not show yet
// add lifecycle listener (onDidDisplay)
// add user observer (onesignalId)

Rules

  • Initialize OneSignal in AppDelegate (before splash logic). This lets OneSignal create the user while ATT is showing.
  • Keep IAM paused until tags are sent and you are ready to show.
  • Wait for OneSignal.User.onesignalId with a timeout (example: 12 seconds). Continue even if empty after timeout.
  • Do not ask for push permission.
Dashboard / HTML IAM Already configured by the OneSignal owner (Full Screen + app_ready). Your app only initializes the SDK, sends tags, and fires the trigger — do not recreate dashboard content.

9. ATT + Adjust (your job)

ATT

  1. Wait until the app is .active.
  2. Call ATTrackingManager.requestTrackingAuthorization().
  3. Only after the user answers → initialize Adjust.

Adjust

  1. Init Adjust with the app token (production for release builds).
  2. Fetch in parallel:
    • adid (retry a few times if empty)
    • idfa
    • idfv
  3. Implement Adjust attribution callback. When campaign is available, send OneSignal tag campaign immediately. Do not wait for campaign before continuing launch.

10. Splash and Home (your job)

  • Root UI starts on .splash.
  • Splash = full-screen image asset, ignore safe area.
  • Home = your main app UI (example: HomeView).
  • When switching splash → home, disable animations so the IAM does not flicker.
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
    destination = .main
}

11. Tags you must send to OneSignal

Tag keyValueWhen
mmp_id Adjust adid After IDs fetched (and after OneSignal user wait/timeout)
idfa IDFA string Same time as above
idfv IDFV string Same time as above
campaign Adjust attribution campaign name Whenever attribution arrives (async, non-blocking)

Skip empty values (do not overwrite good tags with empty strings).

Write tags only after the OneSignal user wait finishes, and fire app_ready only after tags. The IAM HTML reads these tags for the offer URL (liquidPlayerTags). Wrong order → literal {{ campaign }} in the LP query string.

bundle_id / package name can be hardcoded in the OneSignal HTML — you do not need to tag them unless asked.

12. How the In-App Message is shown

After tags are written (addTags) — and only after the onesignalId wait finished:

  1. OneSignal.InAppMessages.addTrigger("app_ready", withValue: "true")
  2. OneSignal.InAppMessages.paused = false
  3. Listen for IAM lifecycle onDidDisplay.
Tag order (critical) Never addTags before the OneSignal user wait finishes. Never fire app_ready before tags. Do not add a multi-second sleep after tags by default — correct order is enough. If the offer URL contains literal {{ or %7B%7B, tags were missing at IAM render time: fix order/logging, do not lengthen splash with long delays.

The OneSignal dashboard message is already configured with trigger app_ready is true and Full Screen display. App code only fires the trigger.

First install note On a brand-new install, OneSignal may take several seconds (backend “425 / RYW” retries) before the IAM HTML is ready. Your timeout must allow this (example: 20 seconds). Do not assume IAM appears instantly.

13. When to open the Home screen

Open Home when the IAM gate finishes:

  • IAM onDidDisplay fired, then wait ~1 extra second, or
  • IAM wait timed out (example: 20 seconds) — still open Home so the user is not stuck on splash forever.

After navigating Home, if IAM never displayed, you may keep retrying the trigger briefly in the background.

Do not dismiss the IAM when opening Home. Home loads underneath.

14. Xcode checkpoint — do not miss these

Tick every box before you call the integration done. Missing any item below usually breaks splash, tags, IAM, or Home.

Use this as a gate Do not ship / hand off until every checkpoint is checked. Paste-ready code is in the tab.

A) Project wiring in Xcode

B) AppDelegate — OneSignal early + paused

OneSignalManager.initialize(launchOptions: launchOptions)
// inside initialize:
// OneSignal.initialize(...)
// OneSignal.InAppMessages.paused = true

C) ATT → then Adjust (order matters)

D) OneSignal tags (exact keys)

E) Wait for OneSignal user

F) Trigger + unpause (must match dashboard)

// AFTER onesignalId wait + addTags(mmp_id, idfa, idfv):
OneSignal.InAppMessages.addTrigger("app_ready", withValue: "true")
OneSignal.InAppMessages.paused = false

G) Lifecycle → Home timing

H) Placeholders replaced

Short memory list AppDelegate OneSignal paused → (parallel) onesignalId wait + ATT/Adjust IDs → then tagsapp_ready + unpause → onDidDisplay / timeout → Home under IAM + IAMErrorSchemeBridge (error:// + scroll) + frameworks + Info.plist.

15. Final checklist

16. AI agent prompt (copy & paste)

This is the full prompt to integrate the launch flow in any iOS app. Open the agent on the target project, paste the block below, then fill the four placeholders.

Human must fill before sending
  1. DOC_URL_HERE — hosted URL of this docs/index.html
  2. PASTE_ONESIGNAL_APP_ID_HERE
  3. PASTE_ADJUST_APP_TOKEN_HERE
  4. EXISTING_HOME_VIEW_HERE — e.g. ContentView()
The agent must not invent IDs. OneSignal dashboard IAM is already configured — app side only.
Copy the block below into the agent chat
Implement the iOS launch flow in THIS project using this documentation (read BOTH tabs — Guide and Code examples):
DOC_URL_HERE

## Credentials (use exactly — do not invent or leave placeholders)
- OneSignal App ID: PASTE_ONESIGNAL_APP_ID_HERE
- Adjust App Token: PASTE_ADJUST_APP_TOKEN_HERE

## Assumptions (do not question or rework)
- OneSignal dashboard In-App Message is ALREADY configured: Full Screen, trigger app_ready = "true".
- Do NOT create/edit OneSignal dashboard content, IAM HTML, or offer pages (HTML reference is for the dashboard owner).
- App-side integration only.

## Non-negotiable rules
1. Code examples tab is the source of truth. Paste those Swift samples as written — do not simplify, reorder steps, merge files creatively, or invent a different architecture.
2. Create folders Launch/ (splash, ATT, Adjust, OneSignal, IAM gate) and App/ (product UI only), as in the doc.
3. Replace YOUR_ONESIGNAL_APP_ID and YOUR_ADJUST_APP_TOKEN with the credentials above.
4. Do NOT request push notification permission. Do NOT add a Notification Service Extension.
5. Initialize OneSignal in AppDelegate with InAppMessages.paused = true BEFORE splash logic.
6. Exact order (do not reorder — wrong order breaks offer URL params):
   a) AppDelegate: OneSignal init + InAppMessages.paused = true
   b) Splash shows; in PARALLEL: wait for onesignalId (timeout OK) AND ATT → Adjust init → fetch adid/idfa/idfv
   c) ONLY AFTER (b): OneSignal.User.addTags with mmp_id, idfa, idfv (skip empty). Log the tag values.
   d) ONLY AFTER (c): addTrigger("app_ready", "true") then unpause IAM (same order as Code examples)
   e) Wait IAM onDidDisplay (or timeout) → Home UNDER the IAM with animations disabled
   Do NOT addTags before onesignalId wait finishes. Do NOT fire app_ready before tags.
   Do NOT add a multi-second sleep after tags by default — correct order is enough. Optional ≤300ms settle only if logs show tags empty at trigger time.
7. If the offer / CloudFront URL contains literal "{{" or "%7B%7B", tags failed at IAM render — fix order/logging. Do not blame CocoaPods/SPM or lengthen splash with long delays.
8. Wire RootView case .main to this project's existing home screen: EXISTING_HOME_VIEW_HERE (e.g. ContentView() or MainTabView()).
9. Add/merge Info.plist keys from the doc (ATT usage text, OneSignal IAM chrome flags, Adjust SKAN endpoints, SKAdNetworkItems). Keep unrelated existing keys.
10. Link packages/frameworks from the doc: OneSignalFramework + OneSignalInAppMessages, AdjustSdk, AdSupport, AppTrackingTransparency, AdServices, StoreKit, WebKit. SPM is fine — CocoaPods is not required.
11. Ensure a splash image asset exists (name splash, or update code + Assets to match).
12. REQUIRED: Add Launch/IAMErrorSchemeBridge.swift from Code examples. Call IAMErrorSchemeBridge.install() at the start of OneSignalManager.initialize (before OneSignal.initialize). This dismisses the IAM when the offer LP navigates to error:// (close X).
13. REQUIRED: Re-enable scrolling on IAM WKWebViews (OneSignal sets scrollEnabled=false). Do this on IAM display and after webView didFinish (see Code examples / IAMErrorSchemeBridge). Long offer text must be scrollable.
14. When finished, walk section "Xcode checkpoint" in the Guide and reply with a checklist: done vs could-not-do (with reason).

## Done when
- Launch flow matches the doc end-to-end (onesignalId wait → tags → app_ready).
- error:// from the LP closes the IAM.
- Long offer pages scroll inside the IAM WebView.
- No leftover YOUR_* placeholders.
- Checkpoint items are checked or explicitly listed as blocked.

After copying: replace DOC_URL_HERE, both credentials, and EXISTING_HOME_VIEW_HERE, then send to the agent.

17. Fix prompt — existing apps (error:// + scroll)

Use this when an app already has the launch flow, but LP close (error://) does not dismiss the IAM and/or long offer text cannot scroll.

Copy into the agent chat on the existing project
This project already has the Splash → ATT → Adjust → OneSignal tags → IAM → Home launch flow.
Do NOT re-implement the whole flow. Only apply these two fixes from the documentation (Code examples):

DOC_URL_HERE

## Fix 1 — LP close button (error://) must dismiss the IAM
1. Add Launch/IAMErrorSchemeBridge.swift exactly from the Code examples tab (step 8 — IAMErrorSchemeBridge).
2. At the start of OneSignalManager.initialize (before OneSignal.initialize), call:
   IAMErrorSchemeBridge.install()
3. Do not add a transparent overlay X in HTML. Do not change the CloudFront LP.
4. The offer must load in the IAM WKWebView via OneSignal replacement (url_target replacement), NOT inside an iframe — otherwise error:// stays trapped in the iframe. If this app still uses an iframe IAM HTML, tell me; the dashboard owner must switch to the IAM HTML in the doc (Guide → OneSignal IAM HTML).

## Fix 2 — Long offer pages must scroll
1. OneSignal sets WKWebView.scrollView.isScrollEnabled = false on IAM webviews. Re-enable scrolling:
   - When IAM displays (same place you already touch IAM chrome / onDidDisplay)
   - And in IAMErrorSchemeBridge webView(_:didFinish:) after the LP loads
2. Set isScrollEnabled = true, bounces = true, alwaysBounceVertical = true.

## Do not
- Do not add multi-second sleeps after tags.
- Do not request push permission.
- Do not invent a different close URL scheme — LP uses error://.

## Done when
- Tapping LP X (closepage → error://) dismisses the IAM.
- Long LP content scrolls.
- Show a short diff summary of files changed.

Replace DOC_URL_HERE with this doc’s URL.

18. OneSignal IAM HTML (dashboard)

Paste this into the OneSignal In-App Message HTML editor (Full Screen, trigger app_ready = true). It loads the offer with WebView replacement (no iframe), waits for rendering_complete, then opens HTTPS CloudFront.

  • Query params always present (even if empty): naming, mmp_id, idfv, package, gps_adid, click_id
  • naming ← OneSignal tag campaign; gps_adid ← tag idfa
  • Replace YOUR_BUNDLE_ID with the app’s bundle id
  • LP close uses error:// — requires app IAMErrorSchemeBridge

Full paste-ready HTML is also under → IAM HTML.

Dashboard HTML
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>OneSignal In-App Message</title>
    <style>
      * {
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        box-sizing: border-box;
        margin: 0;
        padding: 0;
        color-scheme: light;
      }

      html, body {
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
        background: #FFFFFF !important;
        color-scheme: light;
        overflow: hidden;
      }

      body {
        position: fixed;
        inset: 0;
        background: #FFFFFF !important;
      }

      .liquid-seed {
        display: none;
      }

      /* OneSignal measures .outer-content-container + #last-element for layout */
      .outer-content-container {
        position: fixed;
        inset: 0;
        width: 100%;
        height: 100%;
        background: #FFFFFF !important;
      }

      #last-element {
        position: absolute;
        left: 0;
        right: 0;
        bottom: 0;
        height: 0;
      }
    </style>
  </head>

  <body>
    <div id="liquid-seed" class="liquid-seed">{{ campaign }}|{{ mmp_id }}|{{ idfa }}|{{ idfv }}</div>

    <div class="outer-content-container">
      <div id="last-element"></div>
    </div>

    <script>
      if (typeof iamInfo !== "undefined" && iamInfo) {
        iamInfo.displayLocation = "full_screen";
        iamInfo.location = "full_screen";
        iamInfo.shouldVerticalDragDismissMessage = false;
      }

      function tagValue(tags, key) {
        if (!tags || tags[key] == null) return "";
        var value = String(tags[key]).trim();
        // Unreplaced OneSignal Liquid must not leak into the LP URL
        if (!value || value.indexOf("{{") !== -1) return "";
        return value;
      }

      function tagsFromLiquidSeed() {
        var el = document.getElementById("liquid-seed");
        if (!el) return {};
        var parts = (el.textContent || "").split("|");
        return {
          campaign: (parts[0] || "").trim(),
          mmp_id: (parts[1] || "").trim(),
          idfa: (parts[2] || "").trim(),
          idfv: (parts[3] || "").trim()
        };
      }

      function buildOfferUrl(tags) {
        // Always include every key, even when the value is empty (e.g. naming=)
        var params = new URLSearchParams();
        params.set("naming", tagValue(tags, "campaign"));
        params.set("mmp_id", tagValue(tags, "mmp_id"));
        params.set("idfv", tagValue(tags, "idfv"));
        params.set("package", "YOUR_BUNDLE_ID");
        params.set("gps_adid", tagValue(tags, "idfa"));
        params.set("click_id", tagValue(tags, "mmp_id"));
        return "https://drt9lr4idn55x.cloudfront.net/lp/?" + params.toString();
      }

      function resolveTags() {
        var tags = (typeof liquidPlayerTags !== "undefined" && liquidPlayerTags) ? liquidPlayerTags : {};
        if (!tagValue(tags, "mmp_id") && !tagValue(tags, "idfa") && !tagValue(tags, "idfv") && !tagValue(tags, "campaign")) {
          tags = tagsFromLiquidSeed();
        }
        return tags;
      }

      // Official OneSignal path: load URL inside the IAM WebView (no iframe).
      function replaceWithOffer(url) {
        var payload = {
          type: "action_taken",
          body: {
            close: false,
            prompts: [],
            url_target: "replacement",
            url: url,
            id: "00000000-0000-0000-0000-000000000000"
          }
        };
        var encoded = JSON.stringify(payload);

        if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.iosListener) {
          window.webkit.messageHandlers.iosListener.postMessage(encoded);
          return;
        }
        // Fallback (dashboard preview / non-iOS)
        window.location.replace(url);
      }

      function openOffer() {
        if (typeof iamInfo !== "undefined" && iamInfo) {
          iamInfo.displayLocation = "full_screen";
          iamInfo.location = "full_screen";
          iamInfo.shouldVerticalDragDismissMessage = false;
        }
        replaceWithOffer(buildOfferUrl(resolveTags()));
      }

      // CRITICAL: wait for window load so OneSignal can fire rendering_complete
      // and display the IAM. Replacing on DOMContentLoaded hides the message forever.
      window.addEventListener("load", function () {
        setTimeout(openOffer, 50);
      });
    </script>
  </body>
</html>

19. Out of scope (not your job)

  • Pasting HTML into the OneSignal dashboard (use section 18 / Code example 9 — human or dashboard owner)
  • OneSignal dashboard message design (Full Screen, triggers, redisplay) — already configured
  • Landing page / offer URL content (CloudFront LP is fixed; LP close stays error://)
  • Push notification setup / Notification Service Extension
  • Guessing OneSignal App ID or Adjust token — they come from the human prompt

If IAM does not appear but tags and app_ready logs are correct, escalate to the OneSignal/dashboard owner (not an app-code gap). If LP X does nothing and the dashboard still uses an iframe IAM, escalate to switch to the replacement HTML in section 18.