diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b2d0bcc..9282873 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "iterable-sdk", - "version": "26.10.5-beta", - "description": "Takes an Android or iOS app — native or React Native — from nothing to a push notification proven on a device: Firebase or Apple (APNs) provisioning, version-pinned SDK integration, silent-failure pitfalls, and device-side proof that a push actually arrived. Android proof runs on a real device; iOS proof runs on the iOS Simulator.", + "version": "26.10.6-beta", + "description": "Takes an Android or iOS app — native or React Native — from nothing to a push notification proven on a device: Firebase or Apple (APNs) provisioning, version-pinned SDK integration, silent-failure pitfalls, and device-side proof that a push actually arrived. Android proof runs on a device or emulator; iOS proof runs on the iOS Simulator.", "author": { "name": "Iterable" }, "repository": "https://github.com/Iterable/iterable-sdk-skill", "license": "MIT", diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index c06104b..3bc6a2f 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -3,7 +3,7 @@ "owner": { "name": "Iterable" }, "metadata": { "description": "Iterable's official skills for AI coding assistants — mobile SDK integration guidance.", - "version": "26.10.5-beta" + "version": "26.10.6-beta" }, "plugins": [ { diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index df71769..012d16d 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "iterable-sdk", - "version": "26.10.5-beta", - "description": "Takes an Android or iOS app — native or React Native — from nothing to a push notification proven on a device: Firebase or Apple (APNs) provisioning, version-pinned SDK integration, silent-failure pitfalls, and device-side proof that a push actually arrived. Android proof runs on a real device; iOS proof runs on the iOS Simulator.", + "version": "26.10.6-beta", + "description": "Takes an Android or iOS app — native or React Native — from nothing to a push notification proven on a device: Firebase or Apple (APNs) provisioning, version-pinned SDK integration, silent-failure pitfalls, and device-side proof that a push actually arrived. Android proof runs on a device or emulator; iOS proof runs on the iOS Simulator.", "author": { "name": "Iterable" }, "repository": "https://github.com/Iterable/iterable-sdk-skill", "license": "MIT", diff --git a/INTENDED-FLOW.md b/INTENDED-FLOW.md index afb6138..e70a278 100644 --- a/INTENDED-FLOW.md +++ b/INTENDED-FLOW.md @@ -108,7 +108,10 @@ integration or Apple provisioning flow: 3. A new integration completed in the current session may record its exact built artifact; a standalone verification reuses the installed application and reports that the current source/build is not linked to that proof. - `ios-install` never builds. Standard proof never relaunches the app. + `ios-install` never builds. Standard proof never relaunches the app on its + own; the identity screen's **Relaunch the app to read the identity it signs + in with** option (`ios-prepare-proof read-identity`) does, and only when the + developer picks it. 4. The verifier checks the exact user and device registration remotely. On a Simulator it also checks that Iterable holds that Simulator's current APNs token. Before requesting one missing manual action it performs short-lived, @@ -156,21 +159,27 @@ Missing proof metadata is not evidence that the SDK integration is missing. A React Native app runs the Android path and then the iOS path, one after the other, under the `iterable-react-native` skill: -1. Record the scope once — features, identity mode, region, JWT policy. Both - platforms use that one record. -2. **Android first.** The Android ladder provisions Firebase and Iterable, the - skill writes the integration, and `iterable-android-verify` proves a push. - No iOS step starts before that verdict is green. -3. **iOS second.** The iOS provider path offers to reuse both Iterable keys, - the region, No JWT, and the test email from the Android setup, carries over - the feature scope from step 1 (passed with `--features`, shown again for +1. Confirm the scope once, in the skill's Step 0 — features and identity mode. + The Android ladder records none of it. The iOS path receives those answers + as `--features` / `--identity`, marks their origin (`scope_source` / + `identity_source` = `react-native-step0`), and shows them again before + recording them. Region and JWT policy come from the Android setup. +2. **Android first.** The Android ladder provisions Firebase and Iterable, + `ios-provision import-from-android --confirm` copies the Mobile key into the + file the JavaScript app reads (it runs no Apple step), the skill writes the + integration, and `iterable-android-verify` proves a push. No iOS step starts + before that verdict is green. +3. **iOS second.** The iOS provider path already holds both Iterable keys, the + region, No JWT, and the test email from that import, carries over the + feature scope from step 1 (passed with `--features`, shown again for confirmation), and adds the Apple/APNs prerequisites. The skill writes the native push code (bare) or relies on the Expo plugin, and `iterable-ios-verify` proves a push on a Release Simulator build. 4. The app is integrated when the verifier has passed for every platform in scope. One green platform is not done. -The Mobile key reaches the app as a gitignored file the tool writes — bare +The Mobile key reaches the app as a gitignored file the tool writes during the +Android phase — bare React Native `.iterable/rn/iterable.local.ts`, Expo `EXPO_PUBLIC_ITERABLE_API_KEY` in `.env.local` — and the app loads it so that a checkout without the file still builds. diff --git a/README.md b/README.md index 4e23193..0fb6a08 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Iterable SDK skills > **Private beta.** Use of these skills is subject to Iterable's -> [Beta Terms](https://iterable.com/legal/beta-terms/). This documentation is -> confidential, may contain inaccuracies, and may change during the beta. +> [Beta Terms](https://iterable.com/legal/beta-terms/). This documentation may +> contain inaccuracies and may change during the beta. Use these skills with Claude Code, Cursor, or Codex to integrate Iterable's native Android, native iOS, or React Native SDK. The workflows gather and prove @@ -26,6 +26,10 @@ The skills can work independently. A new push integration normally follows: `iterable-provision` → platform integration skill → matching platform verification skill +For a React Native app with both platforms in scope, the workflow finishes +Android first, including a proved push, and then sets up iOS. The iOS setup +reuses the Iterable keys, region, and feature scope from the Android setup. + Skills are model-invoked. Describe the outcome you want rather than running a skill by name. For example: @@ -41,7 +45,7 @@ skill by name. For example: - Native iOS SDK integration. - React Native integration for Android and iOS, including Expo. - Firebase provisioning and device-side push verification for Android. -- Apple/APNs prerequisite guidance and iOS runtime proof. +- Apple/APNs prerequisite guidance and iOS push proof on the iOS Simulator. - Push notifications, in-app messages, mobile inbox, embedded messaging, deep links, JWT authentication, event tracking, and user profiles. - Unknown user activation for native Android and native iOS. @@ -52,16 +56,20 @@ Web SDK integration is not included. ### Accounts -- An Iterable project. The provisioning skill can guide you through creating - the required Iterable API keys and mobile configuration. +- An Iterable project, and an Iterable role that can create API keys and + mobile apps. The provisioning skill can guide you through creating the + required Iterable API keys and mobile configuration. - A Google account with access to a Firebase project, or permission to create - one, when setting up Android push. + one, when setting up Android push. The account must be able to enable APIs, + add Firebase, and create a service account and its key. - Apple Developer Program access to the app's team when setting up native iOS - push. The workflow cannot replace Apple sign-in, 2FA, agreements, or the - one-time download of a newly created APNs private key. + push. Registering the App ID, enabling Push Notifications, and creating an + APNs key require the **Account Holder** or **Admin** role. The workflow + cannot replace Apple sign-in, 2FA, agreements, or the one-time download of a + newly created APNs private key. -You authenticate with Google and Iterable yourself. The plugin does not ask for -or handle your account passwords. +You authenticate with Google, Apple, and Iterable yourself. The plugin does not +ask for or handle your account passwords. ### Command-line tools @@ -78,7 +86,7 @@ xcodebuild -version |---|---|---| | `gcloud` | Google authentication, Firebase projects, service accounts, IAM, and key creation | [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) or `brew install --cask gcloud-cli` | | `adb` | Selecting an Android device and reading the installed app, SDK registration, and arriving notification | Android Studio's SDK Manager or `brew install --cask android-platform-tools` | -| Xcode command-line tools | Building, signing, installing, and observing native iOS apps | Install Xcode, then select it with `xcode-select` | +| Xcode 14 or later | Building, signing, installing, and observing native iOS apps | Install the full Xcode app, not only the Command Line Tools, then select it with `xcode-select` | | Node.js | Parsing command and API output | [nodejs.org](https://nodejs.org) or `brew install node` | The scripts also use Bash, `curl`, and standard Unix utilities. They do not @@ -97,8 +105,13 @@ only one is connected, so the proof is performed on the device you intend to test. For native iOS, development push can be proved on an eligible booted iOS 16+ -Simulator or a signed physical iPhone. Production/TestFlight proof requires a -physical iPhone. +Simulator or a signed physical iPhone. A Simulator receives real APNs sandbox +pushes only on macOS 13 or later with Xcode 14 or later, on a Mac with Apple +silicon or a T2 chip. Production/TestFlight proof requires a physical iPhone. + +React Native and Expo apps are proved on iOS with a Release build, which embeds +the JavaScript bundle and runs without Metro. Expo apps need a development +build (`expo-dev-client`); Expo Go cannot load the SDK's native module. During native iOS push setup, the developer chooses whether to reuse an existing Apple configuration, follow a guided Account Holder or Admin @@ -115,16 +128,38 @@ one APNs sandbox probe. No extra tools are needed beyond Node, curl, and Xcode. /plugin install iterable-sdk@iterable ``` -Start a new Claude Code session after installation. Skills are loaded when a -session starts. +Run `/reload-plugins`, or start a new Claude Code session, to load the skills. To receive updates automatically, open `/plugin`, select **Marketplaces**, -select **iterable**, and enable auto-update. +select **iterable**, and select **Enable auto-update**. ### Cursor -Cursor requires version 3.9 or later. Clone the repository and link the skill -directories: +Cursor requires version 3.9 or later. Install the skills as a plugin: + +1. Open **Customize** in the sidebar. +2. Select **+ Add**, then **From GitHub Repository**. +3. Enter `https://github.com/Iterable/iterable-sdk-skill`. +4. Find **iterable-sdk**, select **Install**, and choose the project or user + scope. + +The plugin installs the skills, the scripts they run, and the Context7 MCP +server. Team admins can also add the repository as a team marketplace. + +To test a local checkout, clone it into Cursor's local plugin folder: + +```bash +git clone https://github.com/Iterable/iterable-sdk-skill.git ~/.cursor/plugins/local/iterable-sdk +``` + +Run **Developer: Reload Window**, then confirm the skills in **Customize**. +Cursor skips a symlink in that folder that points to a clone elsewhere. On +Teams and Enterprise plans, an admin must allow local plugin imports. + +#### Fallback: link the skill directories + +If you can't install plugins, clone the repository and link the skill +directories. This installs the skills without the Context7 MCP server: ```bash git clone --depth 1 https://github.com/Iterable/iterable-sdk-skill.git ~/iterable-skills @@ -134,7 +169,8 @@ for skill in iterable-provision iterable-android iterable-android-verify iterabl done ``` -Run **Developer: Reload Window**, then start a new Agent chat. +Keep the clone in place, because the skills run scripts from it. Run +**Developer: Reload Window**, then start a new Agent chat. ### Codex @@ -155,15 +191,16 @@ codex plugin add iterable-sdk@iterable ## What happens during Android push setup -1. The integration skill inspects the application and adds the required SDK - configuration. +1. The integration skill inspects the application and agrees the integration + scope with you. 2. If Firebase or Iterable prerequisites are missing, the provisioning skill explains what it needs to read and asks for permission before continuing. 3. The workflow creates or selects the Firebase resources and service account. 4. It guides you through the Iterable dashboard actions that do not have a - public API: API keys, the mobile app, and the Firebase push integration. -5. Your coding assistant builds and installs the application using the - project's existing toolchain. + public API: API keys, the mobile app, the Firebase push integration (with + FCM type **Data notifications**), and the test user's email address. +5. The integration skill adds the SDK configuration, and your coding assistant + builds and installs the application using the project's existing toolchain. 6. The workflow opens the selected application's launcher activity. You sign in as the test user and respond to any Android permission prompts. 7. The verification skill sends a proof notification and checks Android's @@ -173,10 +210,36 @@ The component that performs an action does not verify its own result. For example, the sender records which proof notification it sent, while a separate read-only verifier checks the device for that notification. +## What happens during iOS push setup + +1. The integration skill inspects the application and agrees the scope and the + user identity with you, once. Provisioning reuses these answers. +2. If Apple prerequisites are missing, the provisioning skill guides you through + Apple Developer one step at a time: the App ID, Push Notifications, and the + APNs key. It checks the APNs key with one sandbox probe. +3. It guides you through the Iterable Mobile and Server-side API keys and the + iOS app with its APNs credential, and checks the keys with live calls. +4. The integration skill writes the SDK code. Your coding assistant builds the + app and installs it on the Simulator. +5. You sign in to the app and allow notifications. +6. The verification skill confirms that Iterable holds the Simulator's device + token, sends one marked proof notification, and reads its arrival from the + Simulator's own notification records. + +If the app already sends its own push notifications, the integration skill asks +whether to keep them alongside Iterable's or use Iterable notifications only. +Iterable's proof completes the integration; checking the app's own +notifications afterwards is optional. + ## Security and data handling - The repository's scripts run locally and make authenticated HTTPS requests - to Google, Firebase, and Iterable. + to Google, Firebase, Apple, and Iterable. +- Push proof creates or updates one proof template in your Iterable project and + sends it only to the test user. The test identity becomes a real user profile + in that project. +- The plugin also adds the Context7 MCP server (no API key) for documentation + lookups and, in Claude Code, five helper subagents. - You authenticate in your own browser or `gcloud` session. The scripts do not collect account passwords or attempt to bypass sign-in, CAPTCHA, or terms-of-service screens. @@ -201,8 +264,9 @@ organization's configuration. Install plugins only from sources you trust. the documented environment limits. - Creating Iterable API keys, mobile apps, and push integrations requires dashboard interaction because these actions do not have public APIs. -- Core verification can run for a JWT-enabled integration, but the app's JWT - backend behavior requires separate feature-specific evidence. +- Android push proof needs a Mobile API key with **Require JWT** turned off. On + iOS, core verification can run for a JWT-enabled integration, but the app's + JWT backend behavior requires separate feature-specific evidence. - Some bundled source articles cover multiple mobile platforms and may include snippets that are not relevant to the current application. - These skills focus on SDK integration. To work with campaign or user data, @@ -211,24 +275,58 @@ organization's configuration. Install plugins only from sources you trust. ## Updating -For Claude Code, update the marketplace before updating the plugin: +For Claude Code, update the marketplace before updating the plugin. Run these +commands in a terminal: -```text -/plugin marketplace update iterable -/plugin update iterable-sdk@iterable +```bash +claude plugin marketplace update iterable +claude plugin update iterable-sdk@iterable ``` -Start a new session after updating. With auto-update enabled, Claude Code may -ask you to reload plugins after a newer version is downloaded. +Then run `/reload-plugins`, or start a new session. With auto-update enabled, +Claude Code may ask you to reload plugins after a newer version is downloaded. -For Cursor's clone-and-link installation: +For Codex: + +```bash +codex plugin marketplace upgrade iterable +``` + +Then start a new Codex session. + +For Cursor, manage the installed plugin from **Customize**. For a local +plugin checkout, run `git pull` in `~/.cursor/plugins/local/iterable-sdk` and +reload the window. For the clone-and-link fallback: ```bash cd ~/iterable-skills git pull ``` -Then reload the Cursor window and start a new Agent chat. +Then run the link loop again so that new skills are linked, reload the Cursor +window, and start a new Agent chat. + +## Uninstalling + +```bash +# Claude Code (add --scope project or --scope local if you installed there) +claude plugin uninstall iterable-sdk@iterable +claude plugin marketplace remove iterable + +# Codex +codex plugin remove iterable-sdk@iterable +codex plugin marketplace remove iterable +``` + +For Cursor, uninstall the plugin from **Customize**, or delete +`~/.cursor/plugins/local/iterable-sdk` for a local checkout. For the +clone-and-link fallback, delete the links in `~/.cursor/skills/` and the +`~/iterable-skills` clone. + +Uninstalling the plugin leaves cloud resources in place. Before you uninstall, +run `.iterable/teardown` from your project to remove the Google service account, +its role binding and keys, and the local artifacts that Android setup created. +Apple resources and your APNs key are not removed. ## Contributing and architecture @@ -241,4 +339,4 @@ Engineering references: - [`bin/README.md`](bin/README.md) — onboarding program architecture. - [`INTENDED-FLOW.md`](INTENDED-FLOW.md) — authoritative onboarding sequence. -- [`REVIEW.md`](REVIEW.md) — generated SDK documentation review. +- [`REVIEW.md`](REVIEW.md) — how to review refreshed SDK reference docs. diff --git a/bin/agent b/bin/agent index 31c8bf3..a30afc4 100755 --- a/bin/agent +++ b/bin/agent @@ -9,6 +9,7 @@ # bin/agent approve firebase record the developer's yes to changing their project # bin/agent set DRIVER=agent they asked you to run the setup instead of the wizard # bin/agent question K [N] one question to put to the developer, as data +# bin/agent preflight local files only: what exists, and provision/integrate/verify # # `next.question` is how the wizard's screens reach a chat: the context to show, and the # two to four answers with the commands that enact each one. The words are the tool's @@ -57,6 +58,88 @@ if [[ "${1:-}" == question ]]; then exit 0 fi +# Before ws_init for the same reason as `question`: an integration skill runs this before +# anybody has agreed to anything, so it reads local files only and writes none. +if [[ "${1:-}" == preflight ]]; then + source "$BIN/ios-project.sh" + APP="$(dirname "$WS")" + FW="$(ios_project_framework "$APP")"; [[ "$FW" == native ]] && FW=android + jarr() { local s="" x; printf '['; for x in "$@"; do printf '%s%s' "$s" "$(jstr "$x")"; s=","; done; printf ']'; } + fake() { shopt -s nocasematch; [[ -z "$1" || "$1" =~ (your[_-]|replace[_-]?(me|with)|placeholder|dummy|changeme|example|xxxx|[<]) ]]; local r=$?; shopt -u nocasematch; return $r; } + declare -a MISS=() FAKES=() GS_FOUND=() DO=() + for f in "$GS_JSON" "$APP/android/app/google-services.json" "$APP/app/google-services.json" "$APP/google-services.json"; do + [[ -f "$f" ]] || continue + if grep -Eiq 'your[_-]|placeholder|dummy|changeme' "$f"; then FAKES+=("$f"); else GS_FOUND+=("$f"); fi + done + GS_IN_APP=false + for f in "${GS_FOUND[@]+"${GS_FOUND[@]}"}"; do [[ "$f" == "$GS_JSON" ]] || GS_IN_APP=true; done + ((${#GS_FOUND[@]})) || MISS+=("google-services.json") + MOBILE=false; SERVER=false + if fake "$ITBL_MOBILE_KEY"; then [[ -z "$ITBL_MOBILE_KEY" ]] || FAKES+=("$ITBL_ENV (ITBL_MOBILE_KEY)"); MISS+=("Iterable Mobile key"); else MOBILE=true; fi + if fake "$ITBL_SERVER_KEY"; then [[ -z "$ITBL_SERVER_KEY" ]] || FAKES+=("$ITBL_ENV (ITBL_SERVER_KEY)"); MISS+=("Iterable Server-side key"); else SERVER=true; fi + APP_KEY=null APP_KEY_PATH="" + case "$FW" in + react-native) APP_KEY_PATH="$APP/.iterable/rn/iterable.local.ts" + v="$(sed -nE 's/.*ITERABLE_API_KEY[^"]*"([^"]*)".*/\1/p' "$APP_KEY_PATH" 2>/dev/null | head -1)" ;; + expo) APP_KEY_PATH="$APP/.env.local" + v="$(sed -n 's/^EXPO_PUBLIC_ITERABLE_API_KEY=//p' "$APP_KEY_PATH" 2>/dev/null | head -1)" ;; + esac + if [[ -n "$APP_KEY_PATH" ]]; then + if fake "$v"; then APP_KEY=false; [[ -z "$v" ]] || FAKES+=("$APP_KEY_PATH"); else APP_KEY=true; fi + fi + for f in "$APP/.iterable/rn/iterable.local.ts.example" "$APP/.env.local.example" \ + "$APP/android/app/google-services.json.example" "$APP/app/google-services.json.example"; do + [[ -f "$f" ]] && FAKES+=("$f") + done + OPEN_GATE=""; PROVEN=false; PUSH=false + if [[ -f "$WS/state.tsv" ]]; then + OPEN_GATE="$(awk -F'\t' '$2=="red"||$2=="pending"{print $1; exit}' "$WS/state.tsv")" + [[ "$OPEN_GATE" =~ ^G([0-9]|1[0-2])$ ]] || PROVEN=true + push_proven && PUSH=true + fi + SDK=false + if [[ "$FW" == android ]]; then + grep -qs 'com.iterable:iterableapi\|iterableapi' "$APP"/app/build.gradle* "$APP"/gradle/libs.versions.toml && SDK=true + else + grep -qs '"@iterable/react-native-sdk"' "$APP/package.json" && SDK=true + fi + PROVISION="$TOOL_ROOT/iterable-provision/SKILL.md" + if ((${#MISS[@]})) || [[ "$PROVEN" != true ]]; then + KIND=provision OPEN="$PROVISION"; DO=("$BIN/agent question opening") + LIST="$(IFS=,; printf '%s' "${MISS[*]+"${MISS[*]}"}")" + SUMMARY="Prerequisites are missing or not yet proven${LIST:+ (missing: ${LIST//,/, })}. Write no code and no placeholder. Open $PROVISION now, follow it, and run its first command: $BIN/agent question opening" + elif [[ "$GS_IN_APP" != true || "$APP_KEY" == false || "$SDK" != true ]]; then + KIND=integrate OPEN="" + if [[ "$GS_IN_APP" != true ]]; then + case "$FW" in + react-native) DO+=("cp '$GS_JSON' android/app/google-services.json") ;; + android) DO+=("cp '$GS_JSON' app/google-services.json") ;; + esac + fi + if [[ "$APP_KEY" == false ]]; then + [[ "$FW" == expo ]] && ! git -C "$APP" check-ignore -q .env.local 2>/dev/null && DO+=("printf '.env*.local\\n' >> .gitignore") + DO+=("$BIN/ios-provision import-from-android --confirm") + fi + SUMMARY="Prerequisites are proven. Run these commands from the app root, then write the Android integration from the skill you came from." + [[ "$FW" == expo ]] && SUMMARY="$SUMMARY Point expo.android.googleServicesFile at a copy of $GS_JSON." + elif [[ "$PUSH" != true ]]; then + KIND=verify OPEN="$TOOL_ROOT/iterable-android-verify/SKILL.md"; DO=("$BIN/agent") + SUMMARY="Integration files are in place. Build and install the app, then open $OPEN and follow it." + else + KIND=done OPEN="" + SUMMARY="A push reached the Android device." + [[ "$FW" == android ]] || { DO=("$BIN/ios-provision status"); SUMMARY="$SUMMARY Android is finished. If iOS is in scope, start it with the command below and the --features from Step 0."; } + fi + printf '{"tool":"iterable-onboard","command":"preflight","local_only":true,"app_root":%s,"framework":%s,' "$(jstr "$APP")" "$(jstr "$FW")" + printf '"present":{"google_services_json":%s,"google_services_json_in_app":%s,"mobile_key":%s,"server_key":%s,"app_key_file":%s,"provisioning_proven":%s,"sdk_dependency":%s,"push_proven":%s},' \ + "$( ((${#GS_FOUND[@]})) && echo true || echo false)" "$GS_IN_APP" "$MOBILE" "$SERVER" "$APP_KEY" "$PROVEN" "$SDK" "$PUSH" + printf '"app_key_file":%s,"missing":%s,"placeholders":%s,' "$(jstr "$APP_KEY_PATH")" \ + "$(jarr "${MISS[@]+"${MISS[@]}"}")" "$(jarr "${FAKES[@]+"${FAKES[@]}"}")" + printf '"next":{"kind":%s,"open":%s,"commands":%s,"summary":%s}}\n' "$(jstr "$KIND")" \ + "$([[ -n "$OPEN" ]] && jstr "$OPEN" || echo null)" "$(jarr "${DO[@]+"${DO[@]}"}")" "$(jstr "$SUMMARY")" + exit 0 +fi + # So the shortcuts a developer gets handed exist from the first call, not from # whichever later one happened to run the verifier. ws_init diff --git a/bin/gates b/bin/gates index 392c029..fc863ce 100755 --- a/bin/gates +++ b/bin/gates @@ -12,6 +12,9 @@ FIRST_RED="" FAILED=0 STATE="$WS/state.tsv" ws_init; : > "$STATE" +# Written only when a run blocks on the developer, so one left by an earlier run would +# send whoever reads it to a step this run may not be on. +rm -f "$WS/ASK.md" # The spinner hides the cursor, and a hidden cursor outlives the process — Ctrl-C out of # a slow gate would otherwise leave the developer's own shell without one. INT is named diff --git a/iterable-android-verify/SKILL.md b/iterable-android-verify/SKILL.md index 28d657e..7ed4722 100644 --- a/iterable-android-verify/SKILL.md +++ b/iterable-android-verify/SKILL.md @@ -31,7 +31,10 @@ the human question, and wait. ## Finding the scripts -Start in the directory containing this `SKILL.md` and walk up to the first +Resolve symlinks first: use the real path of this `SKILL.md` (`cd -P` / +`pwd -P`), because a skill linked into `~/.cursor/skills` has no `bin/` above +the link. +Start in the directory containing it and walk up to the first directory containing a `bin/agent`; call that ``. If you reach the filesystem root without finding it, the install is incomplete—say so and stop. Never reimplement a gate by hand: a check you improvise is one nobody has @@ -85,6 +88,7 @@ upstream is broken tests a system that is still wrong. | `install_app` | Their app is not on the device, or the installed APK does not carry the SDK. Build and install it, run `/bin/launch-app`, then re-run the agent. The check reads `dumpsys package` for the Iterable messaging service, so it catches "integrated the SDK, forgot to reinstall"—invisible to anything that only reads the repository. | | `run_app` | Run `/bin/launch-app` first, then relay `next.question`. It asks them to sign in as `target.email` and allow notifications, and one option re-runs the checks. Opening the launcher activity is yours; driving the UI or answering the permission dialog is never yours. | | `send_proof` | Announce it, then run `/bin/proof-push`. It sends one marked push and deliberately does not check whether it arrived. Then re-run `/bin/agent`. | +| `investigate` | A device-half check is red: no FCM token, the token not registered in Iterable, or a sent push that did not arrive. It has no command. Relay `next.summary` verbatim; it is the diagnosis of a defect, not pending work. Find the cause (the platform skill's `PITFALLS.md`) before changing anything, never re-send to get a pass, and re-run `/bin/agent` once it is fixed. | | `campaign_send` | Server-side corroboration needs `ITBL_CAMPAIGN_ID`; without it there is nothing to read. Optional. | | `iterable_keys`, `provision`, `choose_target`, … | Not this skill's half—hand back to `iterable-provision`. | | `done` | The push arrived. Say which push, on which device, and how long after the send. If `next.summary` also says the app has its own push path, relay that line and offer the optional after-integration check: the developer triggers one of the app's own notifications and confirms it shows. Run it only if they ask. It never changes the verdict. | diff --git a/iterable-android/PITFALLS.md b/iterable-android/PITFALLS.md index 303feda..02974e1 100644 --- a/iterable-android/PITFALLS.md +++ b/iterable-android/PITFALLS.md @@ -221,7 +221,7 @@ hot-path subset; the full list lives here and is loaded on demand. load that file **explicitly** — this is the part that's easy to miss: **`local.properties` is NOT exposed as Gradle project properties.** A project's existing `getPropertyIfDefined`/`hasProperty`-style helper (or the - `OPEN_EXCHANGE_RATES_API_KEY` pattern many repos already have) *looks* like + `SOME_SERVICE_API_KEY` pattern many repos already have) *looks* like it reads `local.properties` but does not — it only sees `gradle.properties` / `-P` flags, so it silently yields an empty key. Read the file yourself with `Properties()`. diff --git a/iterable-android/SKILL.md b/iterable-android/SKILL.md index bc6cb7f..292f2dd 100644 --- a/iterable-android/SKILL.md +++ b/iterable-android/SKILL.md @@ -19,6 +19,16 @@ description: >- # Iterable Android SDK +## Start here — do these in order, before anything else + +1. Find ``: from the real path of this `SKILL.md` (`cd -P "$(dirname )" && pwd -P`), walk up to the first folder that contains `bin/agent`. +2. From the app root, run `/bin/agent preflight` before you ask anything or edit anything. It reads local files only. +3. Do what its `next` says: open `next.open` if it is set, then run `next.commands` in order. + - `provision`: open `/iterable-provision/SKILL.md` now and follow it. Its first command is `/bin/agent question opening`. Write no code until it hands back. + - `integrate`: run `next.commands`, then do Step 0 and write the integration. + - `verify`: open `/iterable-android-verify/SKILL.md` and follow it. +4. Never write a placeholder, dummy, `.example` or hard-coded key, or a fake `google-services.json`. A missing input comes from provisioning, or you stop and say so. + You are working with the **Iterable Android SDK** — [`com.iterable:iterableapi`](https://central.sonatype.com/artifact/com.iterable/iterableapi) (core) and `com.iterable:iterableapi-ui` (UI components for in-app, inbox, @@ -35,6 +45,18 @@ human readers but omit several silent-failure traps documented in ## Step 0 — Agree on scope BEFORE writing code +**Prerequisites question — ask it in the first round, with the scope.** "Do you +already have the Iterable Mobile key, the Iterable mobile app with its push +integration, and a Firebase project with this app's `google-services.json`?" + +- **Not yet — set them up** (recommended): open `/iterable-provision/SKILL.md` + now and run `/bin/agent question opening`. +- **Yes, I have them**: still open `iterable-provision`; it finds, reuses and + proves what exists. Never ask for a key in the chat. + +When `/bin/agent preflight` already returned `integrate` or `verify`, the +files exist: say so and only confirm. + Do this **first**, before Preflight and before any edits. The SDK covers many features — push, in-app, mobile inbox, embedded, deep links, event tracking, user profiles. Do **not** assume the developer wants all of them, and do **not** @@ -186,7 +208,7 @@ If `iterable-provision` is installed, its `bin/agent` carries this as a `notice` field — relay that verbatim instead, so the tool and the conversation say the same thing. Either way say it once, plainly, and do not soften it into "I've double-checked everything". Proof is the next section, and the strongest form of it is a real push -landing on a real device—`iterable-android-verify` does that. +landing on a device or emulator—`iterable-android-verify` does that. ### Proving it works when you don't have the dashboard @@ -262,11 +284,10 @@ When blocked on any of these, **surface it to the developer and pause that part of the work** — don't silently degrade the integration to keep compiling. **A green build is not the deliverable, and this is the way it actually fails.** -Observed in a live demo: the Firebase project had no Android app registered, so there -was no real `google-services.json`. The push work should have stopped there. Instead -it kept going, made the build pass, knew the Firebase half was unfinished, and then -ended the turn without naming a way forward. Two rules from that, and they are not -negotiable: +The typical case: the Firebase project has no Android app registered, so there is no +real `google-services.json`. The push work should stop there. The failure is to keep +going, make the build pass, know the Firebase half is unfinished, and then end the +turn without naming a way forward. Two rules, and they are not negotiable: - **A missing prerequisite stops the push work, immediately** — before the Gradle edit, not after. Do the parts of the agreed scope that genuinely don't depend on it @@ -305,12 +326,15 @@ it reads, what it will not do — before it runs anything else. Let it. Do not r deciding that provisioning is needed is not permission to begin it, and a project picker is not a consent screen. -To check it is installed: walk up from this `SKILL.md` to the first directory +Check it is installed. Resolve symlinks first: use the real path of this `SKILL.md` (`cd -P` / +`pwd -P`), because a skill linked into `~/.cursor/skills` has no `bin/` above +the link. Then walk up from it to the first directory containing a `bin/agent`. If there isn't one, this Preflight stops as written below, and the table of dashboard paths is what you give them. -Once provisioning reports `done`, **you own copying the file into the app -module**: `.iterable/artifacts/google-services.json` → `app/google-services.json`, +Once provisioning's `next.kind` reaches `install_app` (or `run_app`) — the Google +and Iterable halves are green and `.iterable/artifacts/google-services.json` exists — +**you own copying the file into the app module**: `.iterable/artifacts/google-services.json` → `app/google-services.json`, shown as part of your one confirmed diff. Nothing else writes into their source tree. @@ -811,8 +835,10 @@ before writing any code is the fastest way to compact mid-task. native Android app. - Native iOS — that is the sibling skill `iterable-ios`. The Web SDK is not covered by these skills. -- Iterable platform / dashboard configuration. Direct the user to - [support.iterable.com](https://support.iterable.com) for dashboard help. +- Iterable platform / dashboard configuration. Open the `reference/` doc that + covers the step (*Slug routing*), as Preflight says; point to + [support.iterable.com](https://support.iterable.com) only when no shipped doc + covers it. - JWT *server-side* implementation. The skill assumes the team has, or will build, a token-minting endpoint. The `identifying-the-user` doc covers the client side only. diff --git a/iterable-ios-verify/SKILL.md b/iterable-ios-verify/SKILL.md index 2c3c84a..1a16779 100644 --- a/iterable-ios-verify/SKILL.md +++ b/iterable-ios-verify/SKILL.md @@ -13,7 +13,10 @@ description: >- ## Finding the scripts -Start in the directory containing this `SKILL.md` and walk up to the first +Resolve symlinks first: use the real path of this `SKILL.md` (`cd -P` / +`pwd -P`), because a skill linked into `~/.cursor/skills` has no `bin/` above +the link. +Start in the directory containing it and walk up to the first directory containing `bin/ios-verify`; call that ``. If you reach the filesystem root without finding it, the install is incomplete—say so and stop. Never reimplement a gate by hand: a check you improvise is one nobody has @@ -157,10 +160,10 @@ When `next.kind` is: | `repair_device_connection` / `retry_device_read` | The device inventory was unreadable. Repair the connection as the summary says; `retry_device_read` re-runs the verifier once. | | `build_app` | Build first and record the resulting exact `.app` path. `ios-install` never builds. | | `install_app` | Run `ios-install` for the already-built exact `.app`, then re-run. | -| `prepare_delivery` / `prepare_delivery_b` | Physical iPhone or instrumented run only. Run the exact purpose-scoped SpringBoard inspection from `next.command`. | -| `deliver_user_a` / `deliver_user_b` | Physical iPhone or instrumented run only. Announce the exact identity and marker, then run the exact confirmed `ios-deliver target` command. | -| `observe_user_targeted` / `retry_delivery_observation` / `observe_user_b` | Physical iPhone or instrumented run only. Run the exact `ios-observe delivery` command without resending. | -| `inspect_delivery` / `inspect_delivery_logs` | Physical iPhone or instrumented run only. Run the exact diagnostic command; these never authorize another send. | +| `prepare_delivery` / `prepare_delivery_b` | Instrumented run only. Run the exact purpose-scoped SpringBoard inspection from `next.command`. | +| `deliver_user_a` / `deliver_user_b` | Instrumented run only. Announce the exact identity and marker, then run the exact confirmed `ios-deliver target` command. | +| `observe_user_targeted` / `retry_delivery_observation` / `observe_user_b` | Instrumented run only. Run the exact `ios-observe delivery` command without resending. | +| `inspect_delivery` / `inspect_delivery_logs` | Instrumented run only. Run the exact diagnostic command; these never authorize another send. | | `review_send_manifest` | Stop: the send manifest is invalid or incomplete. Do not send again until the run is reviewed or restarted. | | `launch_app` / `restore_app` | Instrumented run only. Run the exact `ios-launch` command, then re-run. | | `observe_tap` / `observe_logout` / `review_instrumentation` | Instrumented run only. The developer performs the recorded tap or logout; `review_instrumentation` reports missing journal evidence without repeating a UI action. | @@ -185,8 +188,9 @@ that receipt: it re-reads the same Simulator store and log itself, and after the developer clears Notification Center it accepts only the full log chain (received with this nonce, requested, saved, delivered). -On a physical iPhone and in instrumented runs, `ios-launch`, `ios-send`, and -`ios-observe` stay separate actors and `ios-deliver` sequences them: it waits +A standard run on a physical iPhone also sends through `ios-prove`, and the +developer reads the code back (`readback_code`). In instrumented runs, +`ios-launch`, `ios-send`, and `ios-observe` stay separate actors and `ios-deliver` sequences them: it waits for a secure observer arm receipt before invoking the sender once. `ios-observe inspect` non-interactively reads accessibility state and a bounded, redacted relevant log tail, then caches a mode-`0600`, short-lived diff --git a/iterable-ios/PITFALLS.md b/iterable-ios/PITFALLS.md index 4c5a240..eb18bed 100644 --- a/iterable-ios/PITFALLS.md +++ b/iterable-ios/PITFALLS.md @@ -304,9 +304,9 @@ Benchmark-app policy, not a general rule: [`eval/scenarios/ios-benchmark.md`](.. - **Cause:** The App ID does not have Push Notifications enabled on the team (or the APNs key is restricted to other Bundle IDs). APNs then refuses every push for the app with `400 TopicDisallowed`, whichever key signs it, and - Iterable does not surface that refusal. Proven live: two different keys on the - same team both got `TopicDisallowed` before Push was enabled on the App ID, - and both got `200` after. + Iterable does not surface that refusal. Swapping the APNs key does not help: + every key on the team gets `TopicDisallowed` until Push is enabled on the App + ID, and `200` after. - **Fix:** Run `ios-verify check-apple`, which asks Apple directly with one silent push and names the cause. In Apple Developer → Identifiers, check the team selector, open the App ID, tick Push Notifications, Save, and Confirm; diff --git a/iterable-ios/SKILL.md b/iterable-ios/SKILL.md index dfa8a02..937f196 100644 --- a/iterable-ios/SKILL.md +++ b/iterable-ios/SKILL.md @@ -20,6 +20,16 @@ description: >- # Iterable iOS SDK +## Start here — do these in order, before anything else + +1. Find ``: from the real path of this `SKILL.md` (`cd -P "$(dirname )" && pwd -P`), walk up to the first folder that contains `bin/ios-provision`. +2. From the app root, run `/bin/ios-provision status` before you edit anything. It reads local files only and makes no Apple or Iterable request. +3. Act on its `next.kind`: + - `integrate_app`: do Step 0, then write the integration. + - `verify_app`: open `/iterable-ios-verify/SKILL.md` and follow it. + - anything else: prerequisites are missing. Ask Step 0's questions, then open `/iterable-provision/SKILL.md` and follow its iOS path, starting with `/bin/ios-provision status --features --identity `. Put each `next.question` to the developer and run only the command of the option they pick. Write no code until it reaches `integrate_app`. +4. Never write a placeholder, dummy, `.example` or hard-coded key in place of a real one. A missing input comes from provisioning, or you stop and say so. + You are working with **Iterable's Swift SDK**, [`Iterable/iterable-swift-sdk`](https://github.com/Iterable/iterable-swift-sdk): `IterableSDK` (the app target) and `IterableAppExtensions` (the Notification @@ -64,6 +74,18 @@ about facts you could read: provider stops receiving or showing pushes. Don't delete the provider's code unless they confirm that diff. +**Prerequisites question — ask it in the first round, with the scope.** "Do you +already have the Iterable Mobile key, the Iterable iOS app with its APNs push +credential, and an APNs `.p8` key for this Bundle ID?" + +- **Not yet — set them up** (recommended): open `/iterable-provision/SKILL.md` + now and follow its iOS path from `/bin/ios-provision status`. +- **Yes, I have them**: still use that path; it records and proves what exists. + Never ask for a key in the chat. + +When `/bin/ios-provision status` already returned `integrate_app` or +`verify_app`, say so and only confirm. + Then ask which features they want. Use the host's question tool (Claude Code: `AskUserQuestion` with `multiSelect: true`; Cursor: `AskQuestion` with `allow_multiple: true`), **at most 4 options per question**, otherwise plain @@ -190,8 +212,10 @@ question, and run only the command carried by the option they pick. Do not background or delegate the whole flow, don't edit the app in parallel while it waits on Apple, Iterable, or the developer, and never use the Android/Firebase provisioner (`gcp-provisioner`) for iOS. On the Apple side the developer -registers the Bundle ID with Push Notifications in the Apple Developer portal, -or Xcode's automatic signing does it. +registers the Bundle ID with Push Notifications in the Apple Developer portal. +Xcode's automatic signing registers it when it makes a provisioning profile +for a device build; a Simulator-only build does not make Xcode register it, so +for a new app it is usually missing. **Never fabricate a prerequisite to make the build pass.** No fake or empty key presented as working, no server-side key in the app, no identity wired to a @@ -697,6 +721,9 @@ func requestNotificationPermission() async { `.denied` means stop asking; offer the app's page in system Settings instead. `.authorized`, `.provisional`, and `.ephemeral` mean don't prompt again. +That timing is for native apps. A bare React Native app asks at launch in +`AppDelegate`, as `iterable-react-native` says; keep it there. + --- ## Project setup (plain `.xcodeproj`) @@ -1002,9 +1029,11 @@ to act again.** Its last answer was about the build you replaced. When push is in scope, add this file and call `IterableProofMarker.log()` on the line after `IterableAPI.initialize(...)` in `start` and after the -`setEmail` / `setUserId` call in `identify`. At proof time the verifier -relaunches the app with `-iterableProofIdentity YES`, reads the identity from -the Simulator's log, and offers it once Iterable holds this Simulator's device +`setEmail` / `setUserId` call in `identify`. At proof time the identity +screen's **Relaunch the app to read the identity it signs in with** option +(`ios-prepare-proof read-identity --confirm`) relaunches the app with +`-iterableProofIdentity YES`, reads the identity from the Simulator's log, and +offers it once Iterable holds this Simulator's device token on that user, so nobody has to dig out a generated user ID. A normal launch prints nothing; the launch argument, not `#if DEBUG`, gates it, because a Release build can be the one proved. @@ -1029,7 +1058,7 @@ as the last step of the handoff; `ios-verify inspect` and the verifier's done summary warn `proof_identity_marker_present` until it is gone. If you already know the signed-in identity from device evidence (the app's log, a debugger read), pass it as `/bin/ios-prepare-proof status --identity-candidate -`; the tool checks it against Iterable and offers it. Never type an +` (`` as `iterable-ios-verify` finds it); the tool checks it against Iterable and offers it. Never type an identity into the proof screen on the developer's behalf. An iOS 16+ Simulator on macOS 13+ (Apple silicon or T2 Mac) receives real APNs diff --git a/iterable-provision/SKILL.md b/iterable-provision/SKILL.md index 4e6aab4..967c3c3 100644 --- a/iterable-provision/SKILL.md +++ b/iterable-provision/SKILL.md @@ -129,12 +129,13 @@ Inspect the app before invoking a program: - For an iOS target owned by a React Native or Expo integration, keep the React Native skill in charge of application code. Use the iOS provisioning path only for the Apple and Iterable prerequisites it explicitly needs. Run - the Android program first: the iOS path then offers to reuse its Mobile and - Server-side keys, region, No JWT policy, and test email, plus the feature - scope the React Native skill confirmed when passed with `--features`, and - writes the Mobile key where the JavaScript app reads it - (bare React Native `.iterable/rn/iterable.local.ts`, Expo - `EXPO_PUBLIC_ITERABLE_API_KEY` in a gitignored `.env.local`). + the Android program first. Its `import-from-android --confirm` reuses the + Android Mobile and Server-side keys, region, No JWT policy, and test email, + and writes the Mobile key where the JavaScript app reads it (bare React + Native `.iterable/rn/iterable.local.ts`, Expo `EXPO_PUBLIC_ITERABLE_API_KEY` + in a gitignored `.env.local`). It runs no Apple step, so the React Native + skill runs it during the Android phase; the feature scope that skill + confirmed follows in the iOS phase, passed with `--features`. Do not run the Android gate ladder against an iOS project. Its Firebase, `adb`, package-name, and FCM conclusions do not apply to APNs. @@ -154,16 +155,19 @@ recollection of what you just did. ## Finding the scripts Hosts copy a plugin into a cache on install and set no environment variable, so no -path here is stable. Start in the directory containing this `SKILL.md` and walk up +path here is stable. Resolve symlinks first: use the real path of this `SKILL.md` (`cd -P` / +`pwd -P`), because a skill linked into `~/.cursor/skills` has no `bin/` above +the link. Start in the directory containing it and walk up until you reach the first directory containing a `bin/agent`. Call that ``. If you reach the filesystem root without finding it, the install is incomplete. Say so and stop. Do not guess at a path, and do not reimplement any of these checks by hand. -## Before you run anything +## Android: before you run anything -The first command is not the ladder. It is the question that asks whether to start one: +This section is the Android program's; the iOS path starts as *Choose the platform before +running a ladder* says. On Android, the first command is not the ladder. It is the question that asks whether to start one: ``` /bin/agent question opening @@ -171,13 +175,13 @@ The first command is not the ladder. It is the question that asks whether to sta **Ask it, and wait for an answer, before you run any other command here.** It spends no credentials, reads nothing, and writes nothing — not even a workspace directory — so it -is safe to be the first thing that happens. Render it exactly as *When the tool hands you -a question* says: `context` above the selector, verbatim, then the options. +is safe to be the first thing that happens. Render it exactly as *Rendering a tool screen* says: +`context` printed above the selector, verbatim, then `prompt` whole and the options. This ordering is the point and it is easy to get wrong. A status read runs the whole eighteen-gate ladder against their Firebase projects, so a question carried in its output -arrives *after* the work it was asking about. Twice now an agent decided provisioning was -needed, ran it, and presented a project picker — and a project picker is not a consent +arrives *after* the work it was asking about. The usual mistake is to decide provisioning +is needed, run it, and present a project picker — and a project picker is not a consent screen. **Deciding that setup is needed is not permission to start it.** Announcing what you are about to do is not either. @@ -234,8 +238,9 @@ Render it as one `AskUserQuestion`: version of it still comes back "yes". - `requires`, when it is not empty, goes in the same message. It is what they have to do themselves, in their own terminal. -- `header` is the chip, `prompt` is the question, and each entry in `options` is one - option — `label` as the label, `description` as the description. +- `header` is the chip and `prompt` goes in whole, line breaks included: on a step screen + it carries the step itself, with the question as its last line. Each entry in + `options` is one option — `label` as the label, `description` as the description. - When they pick one, run that option's `commands` in order, then read the ladder again. - An option with a non-empty `relay` has **no** commands for you. That is a command only the developer can run — it hosts a sign-in, or takes a key with the echo off. Print it, @@ -342,8 +347,9 @@ relay the block, do not look for a flag that gets you past it, and never set | `project_unreachable` | Relay `next.summary` verbatim — it is a permissions answer from Google, not something to work around — and then ask `next.question`, which offers aiming somewhere else. A typo'd project id and a project they have no access to look identical from here, and both are answered at that screen. | | `register_app` | Their project has no Android app for this package. Ask `next.question` — this is the one change the limits promise never happens unless they ask outright, so its yes is separate from the provisioning yes and recorded with `/bin/agent approve create-app`. Never `CREATE_APP=1`. | | `iterable_keys` | Four steps in their Iterable account that only they can do. Ask `next.question`, then walk them **one at a time** — see *The four Iterable steps* below. The two keys reach the workspace with `iterable-keys --take server\|mobile`, off their clipboard; never into this conversation. | -| `install_app` / `run_app` | The Android device half—hand off to `iterable-android-verify`. Until a device is on record `next.question` is the device picker, worth asking even when only one is attached: a phone left plugged in to charge is "the only device", takes the proof push, and the developer watching an emulator reports that nothing arrived. The verifier may open the selected app after installation; signing in and allowing notifications still happen inside their app, by their hand. | +| `install_app` / `run_app` | The Google and Iterable halves are green. If the app has no Iterable integration yet, continue as *Where this goes next* says first. Otherwise this is the Android device half—hand off to `iterable-android-verify`. Until a device is on record `next.question` is the device picker, worth asking even when only one is attached: a phone left plugged in to charge is "the only device", takes the proof push, and the developer watching an emulator reports that nothing arrived. The verifier may open the selected app after installation; signing in and allowing notifications still happen inside their app, by their hand. | | `send_proof` | The Android device half—hand off to `iterable-android-verify`. | +| `investigate` | A device-half check is red (token, registration, or delivery). Relay `next.summary` verbatim—it is a defect, not pending work—and hand off to `iterable-android-verify`, which owns it. | | `done` | A real push reached the device. Say which device and chain onward. | `next.command` is the command for that step when there is one. Run it; do not @@ -468,15 +474,16 @@ between is to keep it to **one step per message**. these four cannot be automated, and that the keys never come through this conversation. Ask it. Then, for each step the chain gives you: -1. Run `/bin/agent question iterable_step N`. Its `context` **is** the step — the - numbered clicks, the URL, and the one or two arrows that matter — so show every line of - it above the selector, as it came out. Do not summarise it, do not retype the URLs, do +1. Run `/bin/agent question iterable_step N`. Its `prompt` **is** the step — the + numbered clicks, the URL, and the one or two arrows that matter — with the question as + its last line, so pass it to `AskUserQuestion` whole, as it came out; its `context` + is the relay instruction, printed above the selector as usual. Do not summarise it, do not retype the URLs, do not merge two steps into one message. A shortened step is the most expensive mistake in this whole setup: "FCM type: Data notifications" condensed to "enable notifications" breaks push while every other check still reads as configured. 2. Then wait. Step N+1 does not exist until they answer, and the answer names it. If you - find yourself writing the instructions for a dashboard screen, you have skipped the - `context` — nothing here needs you to compose a step, and a composed one has already + find yourself writing the instructions for a dashboard screen, you have cut the + `prompt` — nothing here needs you to compose a step, and a composed one has already told a developer to rename a file this tool stopped writing. 3. Its options arrive with the commands that enact them, so the walk cannot lose its place or skip a screen. On step 1 two of them are `--take server` and `--take mobile`: @@ -511,7 +518,7 @@ writes `.iterable/.env` at mode 0600 with three empty names — never overwritin value, because an Iterable key is shown once. Without arguments and without a terminal it prints all four at once — that is the form for a script, not for a conversation. You do not need to call it on the chat path: `agent question iterable_step N` already -carries the same clicks in its `context`, from the same source. +carries the same clicks in its `prompt`, from the same source. - **Never ask anyone to paste a key into the conversation.** Not as a question, not as an `AskUserQuestion` option, not "just to check the format". `--take` exists so @@ -572,8 +579,11 @@ Both are human steps, so they are the two most worth saying out loud: ## Where this goes next -When `next.kind` is `done` for the Google and Iterable halves, announce the -transition in one line and continue into the platform skill — `iterable-android` or +When `next.kind` reaches `install_app` — or `run_app`, if their app is already +installed — the Google and Iterable halves are green (G0–G12) and +`.iterable/artifacts/google-services.json` exists. Do not wait for `done`: that means a +push reached the device, which cannot happen before the integration is written. Announce +the transition in one line and continue into the platform skill — `iterable-android` or `iterable-react-native` — to write the integration. Do not ask whether to continue. The platform skill owns copying `.iterable/artifacts/google-services.json` into diff --git a/iterable-provision/reference/ios.md b/iterable-provision/reference/ios.md index d98569e..3325a65 100644 --- a/iterable-provision/reference/ios.md +++ b/iterable-provision/reference/ios.md @@ -122,7 +122,10 @@ recommended option passes the same `--features` and `--identity` to `import-from-android`, which records them as `carry-scope` would. A carried identity takes precedence over the Android test email. Without the flags the feature questions follow as usual. **Set up iOS separately** asks everything -again. In a monorepo the Android workspace sits +again. A React Native app usually ran `import-from-android --confirm` without +flags during its Android phase, to deliver the Mobile key to the JavaScript +app; `import_android` is then already answered, and the Step 0 flags passed +to `status` return the `carry_scope` screen instead. In a monorepo the Android workspace sits at the repository root, so the search walks up from the app root to the git toplevel, takes the nearest `.iterable/.env`, and records that path as `iterable.imported_from`. Android package auto-detection does not support @@ -556,13 +559,13 @@ Keep proof levels distinct: - the dashboard's direct Test Push is optional diagnosis when identity association needs to be separated from the APNs path. -Simulator pitfalls seen in live runs: +Simulator pitfalls: - A Simulator needs the registered App ID too, with Push Notifications enabled. For a never-registered Bundle ID, or one without Push, APNs answers `400 TopicDisallowed` for every key on the team; Iterable still returns HTTP - 200 for the send, so nothing arrives and nothing says why (proven live: two - keys both refused before Push was ticked, both accepted after). + 200 for the send, so nothing arrives and nothing says why. Swapping the APNs + key does not help: every key is refused until Push is ticked. `ios-verify check-apple` asks Apple directly and names this. `DeviceTokenNotForTopic` instead means the push targeted a different Bundle ID than the app that registered the token. @@ -665,7 +668,15 @@ reviewing app code: The verifier cannot distinguish a repeated manual action from duplicate event tracking after both have reached Iterable. -Use the separate sender actor for user-targeted proof: +Standard proof sends through `ios-prove`, once, when the verifier's +`next.command` names it: + +```text +/bin/ios-prove --confirm +``` + +Instrumented runs only: the separate sender actor sends the user-targeted +marker: ```text /bin/ios-observe inspect --kind notifications --purpose pre-send diff --git a/iterable-react-native/PITFALLS.md b/iterable-react-native/PITFALLS.md index c66d38b..d666f0d 100644 --- a/iterable-react-native/PITFALLS.md +++ b/iterable-react-native/PITFALLS.md @@ -24,9 +24,10 @@ config-plugin traps (build-time and configuration-time). Use them only after Step 0 classified the project as Expo. **#12** is Xcode **27** on iOS (bare **and** Expo): Device Hub launch and resource-bundle compile floors — not an Iterable API change. **#15** is native Android and applies -to **both** workflows, but only when the app already sends push. #3's -*symptom* is both workflows; its **bare** native fix (manifest + -`PermissionsAndroid`) is wrong on Expo — branch first. **#16** applies to every +to **both** workflows, but only when the app already sends push. #3 is +both workflows, and both request the permission at runtime with +`PermissionsAndroid`; only its **bare** manifest edit is wrong on Expo, where +the plugin declares it — branch first. **#16** applies to every app with deep links; **#17** is Expo only. **#18** is the install, both workflows. **#19–#20** are the iOS Release Simulator build (bare, and Expo when built with `xcodebuild`); **#21** is JavaScript, both workflows; **#22** is bare @@ -123,11 +124,11 @@ iOS (on Expo, #14). Concrete gaps still open on 3.2.1: - - **SDK-594:** `getUnreadInboxMessagesCount` is on the TurboModule spec and + - **Unread inbox count:** `getUnreadInboxMessagesCount` is on the TurboModule spec and implemented natively. `IterableInAppManager` exposes `getMessages` / `getInboxMessages` / `showMessage` / `setReadForMessage` and **no** unread-count wrapper. - - **SDK-593:** `setDeviceAttribute` is used internally to stamp + - **Device attributes:** `setDeviceAttribute` is used internally to stamp `reactNativeSDKVersion` at init. It is not on the TurboModule spec and not on `Iterable`. - `logout` is **not** a Spec method. The public call is `Iterable.logout()` @@ -135,9 +136,10 @@ iOS (on Expo, #14). `setEmail(null)` and `setUserId(null)`). - **Fix:** Only import from `@iterable/react-native-sdk`. Do not import `NativeRNIterableAPI`, `RNIterableAPI`, or `IterableApi`. Do not invent - wrappers for spec-only methods. Until SDK-594 ships, derive an unread - count from `Iterable.inAppManager.getInboxMessages()` (filter unread) - rather than calling the native module. Native configuration (FCM, APNs, + wrappers for spec-only methods. Until the SDK adds an unread-count + wrapper, derive an unread count from + `Iterable.inAppManager.getInboxMessages()` (filter unread) rather than + calling the native module. Native configuration (FCM, APNs, `POST_NOTIFICATIONS`) is still required; it is not a JS method call. See `reference/overview.md` and the feature slug for the task. @@ -193,8 +195,9 @@ iOS (on Expo, #14). --- -Pitfalls 7–13 apply only when Step 0 detected Expo. Skip them on a bare -React Native project. Do not introduce `@iterable/expo-plugin` there. +Pitfalls 7–11, 13, 14 and 17 apply only when Step 0 detected Expo; #12 is +Xcode 27 on both workflows. Skip the Expo ones on a bare React Native +project. Do not introduce `@iterable/expo-plugin` there. ## 7. Expo: SDK and plugin do not work in Expo Go @@ -324,8 +327,8 @@ the Xcode 27 guidance — the generated `reference/` corpus does not carry it. - **Bare:** in the host `Podfile`, after `react_native_post_install`, raise every Pods target — resource bundles included — that is unset or below **15.1**, then re-run `pod install`. **15.0** is the Xcode 27 floor; - **15.1** matches React Native's own minimum and Iterable's bare RN demo, so - nothing is left at exactly 15.0: + **15.1** matches React Native's own minimum, so nothing is left at exactly + 15.0: ```ruby post_install do |installer| @@ -341,8 +344,8 @@ the Xcode 27 guidance — the generated `reference/` corpus does not carry it. Keep any existing `{fmt}` C++17 workaround (pitfall #11); this lift is separate. - - **Expo SDK 58:** a fresh `prebuild` left no Pods target below 15.0 in our - trial, so no lift is needed. Check rather than assume: + - **Expo SDK 58:** a fresh `prebuild` normally leaves no Pods target below + 15.0, so no lift is needed. Check rather than assume: `grep -h IPHONEOS_DEPLOYMENT_TARGET ios/Pods/Pods.xcodeproj/project.pbxproj | sort -u`. - **Older Expo SDKs:** if that check shows a target below 15.0, lift it in an **Expo config plugin** at `prebuild` — never by committing `ios/Podfile` or @@ -500,8 +503,10 @@ Pitfall 15 applies to **both** workflows — any app that already sends push. (say, from an old `ios/build`) is whatever was built there last. - **Fix:** Build with the exact command in `SKILL.md` → Bare iOS (`-derivedDataPath build`), install - `ios/build/Build/Products/Release-iphonesimulator/.app`, and pass that - path to `ios-prepare-proof start --app-path`. + `ios/build/Build/Products/Release-iphonesimulator/.app`, and bind it + through the verifier's **App build** screen, or with + `ios-prepare-proof restart --confirm --app-path ` when a run already + exists. ## 20. iOS: code signing disabled strips `aps-environment` diff --git a/iterable-react-native/SKILL.md b/iterable-react-native/SKILL.md index 87eab8c..b10b085 100644 --- a/iterable-react-native/SKILL.md +++ b/iterable-react-native/SKILL.md @@ -21,6 +21,17 @@ description: >- # Iterable React Native SDK +## Start here — do these in order, before anything else + +1. Find ``: from the real path of this `SKILL.md` (`cd -P "$(dirname )" && pwd -P`), walk up to the first folder that contains `bin/agent`. +2. From the app root, run `/bin/agent preflight` before you ask anything or edit anything. It reads local files only. +3. Do what its `next` says: open `next.open` if it is set, then run `next.commands` in order. + - `provision`: open `/iterable-provision/SKILL.md` now and follow it. Its first command is `/bin/agent question opening`. Write no code until it hands back. + - `integrate`: run `next.commands`, then do Step 0 and write the Android integration. + - `verify`: open `/iterable-android-verify/SKILL.md` and follow it. +4. Never write a placeholder, dummy, `.example` or hard-coded key, or a fake `google-services.json`. A missing input comes from provisioning, or you stop and say so. +5. Android first. Make no iOS edit until `iterable-android-verify` is green. Then, for iOS, run `/bin/ios-provision status --features ` and follow `iterable-provision`'s iOS path until its `next.kind` is `integrate_app`. + You are working with the **Iterable React Native SDK** — [`@iterable/react-native-sdk`](https://www.npmjs.com/package/@iterable/react-native-sdk). It is a JavaScript layer over Iterable's native Android and iOS SDKs. @@ -55,6 +66,18 @@ cost the developer a permission prompt. Do this **first**, before Preflight and before any edits. +**Prerequisites question — ask it in the first round, with the scope.** "Do you +already have the Iterable Mobile key, the Iterable mobile app with its push +integration, and a Firebase project with this app's `google-services.json`?" + +- **Not yet — set them up** (recommended): open `/iterable-provision/SKILL.md` + now and run `/bin/agent question opening`. +- **Yes, I have them**: still open `iterable-provision`; it finds, reuses and + proves what exists. Never ask for a key in the chat. + +When `/bin/agent preflight` already returned `integrate` or `verify`, the +files exist: say so and only confirm. + **Detect the workflow from the project, do not ask which docs to use.** - **Expo** if `app.json` / `app.config.js` / `app.config.ts` has an `expo` @@ -180,13 +203,17 @@ doc for a native step. When the scope covers both platforms, finish one before starting the other: -1. **Android:** provision with `iterable-provision`, integrate, and prove it — - `iterable-android-verify` green — before any iOS work starts. -2. **iOS:** `iterable-provision`'s iOS path offers to reuse what the Android - phase recorded — both Iterable keys (Mobile and Server-side), the region, - No JWT, and the test email. Pass the scope confirmed in Step 0 as - `--features ` from the table above, first to `ios-provision status`, - so the import screen names it, and its recommended option carries it into +1. **Android:** provision with `iterable-provision`, deliver the Mobile key to + the JavaScript app ([placing it](#if-they-dont-have-an-input-yet)), integrate, + and prove it — `iterable-android-verify` green — before any iOS work starts. + The key import runs no Apple step, so it is not iOS work. +2. **iOS:** `iterable-provision`'s iOS path already holds what that import + copied — both Iterable keys (Mobile and Server-side), the region, No JWT, + and the test email. Pass the scope confirmed in Step 0 as + `--features ` from the table above, first to `ios-provision status`: + it comes back as one `carry_scope` screen whose **Use these answers** + records it. If the import never ran, the `import_android` screen names the + scope instead and its recommended option carries it into `import-from-android`. The final scope screen shows it again before anything is confirmed. Pass only what the developer confirmed; without `--features` the scope is asked again. The path then adds the Apple/APNs @@ -247,7 +274,7 @@ everything". | Input | Needed when | If missing | |---|---|---| | **Workflow** — Expo vs bare | Always | Detect from the project (Step 0). If both signals exist, ask. | -| **Mobile API key** | Always | Never hardcode it into a tracked file. `iterable-provision` writes the copy the app reads: bare RN `.iterable/rn/iterable.local.ts`, Expo `EXPO_PUBLIC_ITERABLE_API_KEY` in a gitignored `.env.local` ([loading it](#loading-the-key)). | +| **Mobile API key** | Always | Never hardcode it into a tracked file. The Android program keeps it in `.iterable/.env`, which the app does not read; `ios-provision import-from-android --confirm` writes the copy the app reads — bare RN `.iterable/rn/iterable.local.ts`, Expo `EXPO_PUBLIC_ITERABLE_API_KEY` in a gitignored `.env.local` — during the Android phase ([placing it](#if-they-dont-have-an-input-yet), [loading it](#loading-the-key)). | | **Identity model** — `setEmail` vs `setUserId`, and where the value comes from | Always | [Project type first](#identity-project-type-first). Never guess. | | **JWT?** — is the mobile key JWT-protected? | Always | Ask, and **assume yes until told otherwise** (it is on by default for client-side keys and is permanent). If yes, `authHandler` is mandatory (rule 1) **and the team needs a backend endpoint that mints the tokens** — if they don't have one, raise it as a blocker instead of signing in the app. | | **Data region** — US or EU | Always | Ask if they sign in at `app.eu.iterable.com` (`european-data-center`). See pitfall #2. | @@ -258,10 +285,10 @@ everything". **Never fabricate a prerequisite to make the build pass.** Do not invent an API key, a placeholder `google-services.json`, or a JWT signed in the client. -**A green build is not the deliverable.** Seen live on the Android side: the Firebase -project had no app registered, so there was no real `google-services.json`; the work -carried on anyway, made it compile, and ended the turn without naming a way forward. -So — a missing prerequisite stops the push work *before* the native edit, and you say +**A green build is not the deliverable.** The typical failure: the Firebase project has +no app registered, so there is no real `google-services.json`, and the work carries on +anyway, makes it compile, and ends the turn without naming a way forward. Instead, a +missing prerequisite stops the push work *before* the native edit, and you say which parts of the agreed scope you did not do and why. Then route into `iterable-provision` and conduct it here rather than handing over a command: its wizard offers to register the missing app, and `next.question` gives you each screen to put to @@ -280,19 +307,37 @@ push integration themselves. It produces the three inputs this Preflight cannot invent — a real `google-services.json`, the service-account key that Iterable's push integration asks you to upload, and the dashboard steps that have no API — and proves each -one with a live call. Announce the handoff in one line and route. To check it -is installed, walk up from this `SKILL.md` to the first directory containing a -`bin/agent`; if there isn't one, this Preflight stops as written above. +one with a live call. Announce the handoff in one line and route. Check it is +installed. Resolve symlinks first: use the real path of this `SKILL.md` (`cd -P` / +`pwd -P`), because a skill linked into `~/.cursor/skills` has no `bin/` above +the link. Then walk up from it to the first directory containing a `bin/agent`; if there +isn't one, this Preflight stops as written above. **Routing is not starting.** The first thing that skill does is run `/bin/agent question opening` and put it to the developer before running anything else. Let it, and don't run `/bin/agent` or `discover` yourself to save a step — deciding provisioning is needed is not permission to begin it. -Once it reports `done`, **you own placing the file**: on bare RN copy +Once its `next.kind` reaches `install_app` (or `run_app`) — the Google and Iterable +halves are green and `.iterable/artifacts/google-services.json` exists — **you own +placing the file**: on bare RN copy `.iterable/artifacts/google-services.json` to `android/app/google-services.json`; on Expo put it where `expo.android.googleServicesFile` points, never by hand-editing `android/`. Show it as part of your one confirmed diff. + +**The Mobile key, in the same step.** The Android program keeps it in +`.iterable/.env`, which the JavaScript app never reads. Deliver it with the iOS +program's import, which runs before any Apple step and never prints the key: from the +app root, run `/bin/ios-provision import-from-android --confirm`. It writes +`.iterable/rn/iterable.local.ts` (bare) or `EXPO_PUBLIC_ITERABLE_API_KEY` in +`.env.local` (Expo) at mode `0600`, and starts the self-ignoring `.iterable/ios/` +workspace the iOS phase reuses. On Expo it writes `.env.local` only when git already +ignores it, and does not say when it skipped — so run `git check-ignore -q .env.local` +first and, if that fails, add `.env*.local` to `.gitignore` in the confirmed diff +before the import. Check the result without reading the key: `test -f +.iterable/rn/iterable.local.ts`, or `grep -c '^EXPO_PUBLIC_ITERABLE_API_KEY=' +.env.local`. Leave the JSON's `next` alone: it is the iOS scope screen, which +belongs to the iOS phase. The import runs once per app. In a monorepo whose app sits below the repository root, provisioning does not read the Android package name from the project yet; the developer types it. @@ -432,7 +477,7 @@ Expo). Rule 8 applies whenever the app already sends push of its own. the internal `IterableApi` class. Do not copy Kotlin `IterableApi.getInstance()`, `IterableConfig.Builder()`, or Swift `IterableAPI` into JS. Device attributes and unread-inbox count exist - natively but are not on that facade (pitfall #4, SDK-593, SDK-594). This + natively but are not on that facade (pitfall #4). This package at 3.2.1 ships Android `3.10.1` / iOS `6.7.5` — native APIs newer than those are not reachable (pitfall #5). @@ -458,8 +503,8 @@ Expo). Rule 8 applies whenever the app already sends push of its own. gitignored and regenerated, so a config or plugin change needs a fresh prebuild. On Xcode **27**, use CLI **20.2.0+** (bare) or Expo SDK **58** for Device Hub launch; bare apps also need a `Podfile` lift of Pods - resource-bundle deployment targets, while an Expo SDK 58 prebuild needed none - in our trial — check, don't assume (pitfall **#12**). Never hand-edit + resource-bundle deployment targets, while an Expo SDK 58 prebuild normally needs + none — check, don't assume (pitfall **#12**). Never hand-edit `Pods/`. 8. **If the app already sends push, forward to Iterable — never replace what's @@ -702,9 +747,11 @@ and `[IterableAppIntegration userNotificationCenter:didReceiveNotificationRespon ``` The app is `ios/build/Build/Products/Release-iphonesimulator/.app`. - Install that path with `xcrun simctl install `, and record it - with `/bin/ios-prepare-proof start --confirm --app-path `, or - `restart --confirm --app-path ` when a run already exists. Never install a `.app` found with `find` or from another DerivedData + Install that path with `xcrun simctl install `. The verifier's + **App build** screen then finds it under the app's own `-derivedDataPath` and + recommends binding it; run the option the developer picks, and never append + `--app-path` by hand. When a run already exists, bind the rebuilt `.app` with + `/bin/ios-prepare-proof restart --confirm --app-path `. Never install a `.app` found with `find` or from another DerivedData folder — it can be a stale build (pitfall #19). Never add `CODE_SIGNING_ALLOWED=NO`, `CODE_SIGNING_REQUIRED=NO` or `CODE_SIGN_IDENTITY=""`: an unsigned app carries no `aps-environment` and never gets a token @@ -712,7 +759,8 @@ and `[IterableAppIntegration userNotificationCenter:didReceiveNotificationRespon - **Proof identity marker (temporary):** when push is in scope, add this to `AppDelegate.swift` and call it as the last line of `application(_:didFinishLaunchingWithOptions:)`, before `return true`. At - proof time the verifier relaunches the app with `-iterableProofIdentity YES` + proof time the identity screen's relaunch option (`ios-prepare-proof + read-identity --confirm`) relaunches the app with `-iterableProofIdentity YES` and reads the identity from the Simulator's log, as in `iterable-ios` (*The proof identity marker*). It reads the native SDK, not JavaScript: the identity is stored natively once `Iterable.initialize` and `setEmail` / @@ -881,8 +929,10 @@ you detected), then load feature slugs **as you reach each feature**. - Native Android-only or iOS-only app integrations — those are `iterable-android` and `iterable-ios`. If this project is a native app with no React Native, stop and use the matching native skill. -- Iterable platform / dashboard configuration. Direct the user to - [support.iterable.com](https://support.iterable.com). +- Iterable platform / dashboard configuration. Open the `reference/` doc that + covers the step (*Slug routing*), as Preflight says; point to + [support.iterable.com](https://support.iterable.com) only when no shipped doc + covers it. - JWT *server-side* implementation. Assume the team has, or will build, a token-minting endpoint. diff --git a/tests/agent-preflight.sh b/tests/agent-preflight.sh new file mode 100755 index 0000000..bd9d7bd --- /dev/null +++ b/tests/agent-preflight.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# `bin/agent preflight` runs before any consent, so it must read local files only, +# write nothing, never print a key, and route a fresh app to provisioning. +# +# No network: gcloud and curl are stubs that record any call. + +set -uo pipefail +cd "$(dirname "$0")/.." +ROOT="$PWD" + +FAILED=0 +ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; } +bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; FAILED=1; } + +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT +STUB="$TMP/stub"; mkdir -p "$STUB" +for c in gcloud curl adb firebase; do + printf '#!/bin/sh\necho "%s $*" >> "%s/calls"\nexit 1\n' "$c" "$TMP" > "$STUB/$c"; chmod +x "$STUB/$c" +done +export PATH="$STUB:$PATH" + +MOBILE=0123456789abcdef0123456789abcdef +SERVER=fedcba9876543210fedcba9876543210 + +new_app() { # — a bare React Native app + rm -rf "$1"; mkdir -p "$1/android/app" + ( cd "$1" && git init -q ) + printf '{"dependencies":{"react-native":"0.76.0"}}\n' > "$1/package.json" +} +pf() { ( cd "$1" && "$ROOT/bin/agent" preflight 2>/dev/null ); } +field() { node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{const j=JSON.parse(d);process.stdout.write(String(process.argv[1].split(".").reduce((o,k)=>o?.[k],j)))})' "$1"; } + +echo +echo " bin/agent preflight — local files only, before any consent" +echo + +A="$TMP/fresh"; new_app "$A" +OUT="$(pf "$A")" +[[ "$(field next.kind <<< "$OUT")" == provision ]] \ + && ok "nothing present routes to provision" || bad "fresh app got: $OUT" +[[ "$(field next.commands.0 <<< "$OUT")" == "$ROOT/bin/agent question opening" \ + && "$(field next.open <<< "$OUT")" == "$ROOT/iterable-provision/SKILL.md" ]] \ + && ok "and names the provisioning skill and its first command" || bad "next lacks the opening command: $OUT" +[[ ! -e "$A/.iterable" ]] && ok "and writes no workspace" || bad "preflight created $A/.iterable" + +B="$TMP/ready"; new_app "$B" +mkdir -p "$B/.iterable/artifacts" +printf '{"project_info":{"project_id":"real-project"}}\n' > "$B/.iterable/artifacts/google-services.json" +printf 'ITBL_MOBILE_KEY=%s\nITBL_SERVER_KEY=%s\n' "$MOBILE" "$SERVER" > "$B/.iterable/.env" +printf 'G10\tgreen\thuman\tIterable API keys work\t\nG12\tunverifiable\thuman\tPush integration configured\t\nG13\tpending\thuman\tApp installed with the SDK\t\n' > "$B/.iterable/state.tsv" +OUT="$(pf "$B")" +[[ "$(field next.kind <<< "$OUT")" == integrate ]] \ + && ok "proven prerequisites route to integrate" || bad "ready app got: $OUT" +grep -qF "import-from-android --confirm" <<< "$OUT" && grep -qF "android/app/google-services.json" <<< "$OUT" \ + && ok "and say how the key file and google-services.json reach the app" || bad "integrate lacks the delivery commands: $OUT" +grep -qF -e "$MOBILE" -e "$SERVER" <<< "$OUT" \ + && bad "a key value appears in the output" || ok "no key value in the output" + +cp "$B/.iterable/artifacts/google-services.json" "$B/android/app/" +mkdir -p "$B/.iterable/rn" +printf 'export const ITERABLE_API_KEY = "%s";\n' "$MOBILE" > "$B/.iterable/rn/iterable.local.ts" +node -e 'const f=process.argv[1],p=require(f);p.dependencies["@iterable/react-native-sdk"]="2.0.0";require("fs").writeFileSync(f,JSON.stringify(p))' "$B/package.json" +OUT="$(pf "$B")" +[[ "$(field next.kind <<< "$OUT")" == verify ]] \ + && ok "everything placed routes to verify" || bad "integrated app got: $OUT" +grep -qF "$MOBILE" <<< "$OUT" && bad "the delivered key appears in the output" || ok "and still prints no key" + +printf 'export const ITERABLE_API_KEY = "YOUR_API_KEY";\n' > "$B/.iterable/rn/iterable.local.ts" +OUT="$(pf "$B")" +[[ "$(field present.app_key_file <<< "$OUT")" == false && "$(field next.kind <<< "$OUT")" == integrate ]] \ + && grep -qF "iterable.local.ts" <<< "$(field placeholders <<< "$OUT")" \ + && ok "a placeholder key file is flagged and treated as missing" || bad "placeholder not flagged: $OUT" + +rm "$B/.iterable/rn/iterable.local.ts" +printf 'export const ITERABLE_API_KEY = "";\n' > "$B/.iterable/rn/iterable.local.ts.example" +OUT="$(pf "$B")" +grep -qF "iterable.local.ts.example" <<< "$(field placeholders <<< "$OUT")" \ + && ok "an .example key file is flagged" || bad ".example not flagged: $OUT" + +printf 'ITBL_MOBILE_KEY=YOUR_MOBILE_KEY\nITBL_SERVER_KEY=%s\n' "$SERVER" > "$B/.iterable/.env" +OUT="$(pf "$B")" +[[ "$(field next.kind <<< "$OUT")" == provision && "$(field present.mobile_key <<< "$OUT")" == false ]] \ + && ok "a placeholder Mobile key routes back to provision" || bad "placeholder Mobile key accepted: $OUT" + +[[ ! -s "$TMP/calls" ]] && ok "no gcloud, curl, adb or firebase call was made" \ + || bad "network tools were called: $(tr '\n' ';' < "$TMP/calls")" + +echo +((FAILED)) && { echo " FAILED"; exit 1; } +echo " All good — preflight is local, silent about secrets, and routes in order."