diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 07b1b05..d5a2275 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "iterable-sdk", - "version": "26.10.9-beta", + "version": "26.10.10-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", diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index ef20b20..02ee9d6 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.9-beta" + "version": "26.10.10-beta" }, "plugins": [ { diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 6fbb488..f02ae5c 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "iterable-sdk", - "version": "26.10.9-beta", + "version": "26.10.10-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", diff --git a/INTENDED-FLOW.md b/INTENDED-FLOW.md index 3a21cb0..2c8bb52 100644 --- a/INTENDED-FLOW.md +++ b/INTENDED-FLOW.md @@ -26,7 +26,8 @@ Numbered because the auditor reports divergence by waypoint. **W1 — Scope.** First prompt, the agent establishes what the developer wants integrated and confirms it. On Android it is recorded, not remembered: `agent preflight` returns `scope` ahead of provisioning and code until `agent question scope` has put the features, the -identity and any existing-push choice to the developer and their picks are on disk. +identity, any existing-push choice, the data region, and the JWT policy to the developer +and their picks are on disk (the same region and JWT questions native iOS asks). An agent that enters through provisioning meets the same gate: `agent question opening` prints the scope screen, and a status read reports `scope`, until those picks are on disk. diff --git a/bin/agent b/bin/agent index cd9d470..57d224d 100755 --- a/bin/agent +++ b/bin/agent @@ -207,6 +207,7 @@ case "${1:-status}" in # Only the scope screens' options carry these, one answer per call. scope) shift + extra_env="" case "${1:-}" in feature) keys=SCOPE_FEATURES @@ -221,13 +222,45 @@ case "${1:-status}" in keys=SCOPE_PUSH v="${2:-}" [[ "$v" == keep-both || "$v" == iterable-only ]] || { printf '{"error":%s}\n' "$(jstr "the app's own push is keep-both or iterable-only; got: '$v'")"; exit 30; } ;; - clear) keys="SCOPE_FEATURES SCOPE_IDENTITY SCOPE_PUSH" v="" ;; - *) printf '{"error":%s}\n' "$(jstr "usage: agent scope feature | identity email|user-id|unknown-user | existing-push keep-both|iterable-only | clear")"; exit 30 ;; + region) + keys=SCOPE_REGION v="${2:-}" + [[ "$v" == US || "$v" == EU ]] || { + printf '{"error":%s}\n' "$(jstr "the region is US or EU; got: '$v'")"; exit 30; } + if [[ "$v" == EU ]]; then extra_env="ITBL_BASE=https://api.eu.iterable.com" + else extra_env="ITBL_BASE="; fi ;; + jwt) + keys="SCOPE_JWT SCOPE_JWT_CHECK" v="${2:-}" + [[ "$v" == true || "$v" == false ]] || { + printf '{"error":%s}\n' "$(jstr "JWT is true or false; got: '$v'")"; exit 30; } + # jwt-check is only a waypoint; a recorded answer clears it. + [[ "$v" == true ]] && v_jwt=true || v_jwt=false + v="$v_jwt" + v_check="" ;; + jwt-check) + keys="SCOPE_JWT_CHECK SCOPE_JWT" v_check=true v="" ;; + jwt-check-clear) + keys=SCOPE_JWT_CHECK v="" ;; + clear) + keys="SCOPE_FEATURES SCOPE_IDENTITY SCOPE_PUSH SCOPE_REGION SCOPE_JWT SCOPE_JWT_CHECK" v="" ;; + *) printf '{"error":%s}\n' "$(jstr "usage: agent scope feature | identity email|user-id|unknown-user | existing-push keep-both|iterable-only | region US|EU | jwt true|false | jwt-check | jwt-check-clear | clear")"; exit 30 ;; esac for k in $keys; do - save_resolved "$k" "$v" && printf -v "$k" '%s' "$v" || + kv="$v" + [[ "$k" == SCOPE_JWT_CHECK ]] && kv="${v_check-}" + save_resolved "$k" "$kv" && printf -v "$k" '%s' "$kv" || { printf '{"error":%s}\n' "$(jstr "the scope was not saved: could not write to $WS")"; exit 30; } done + if [[ -n "$extra_env" ]]; then + ek="${extra_env%%=*}"; ev="${extra_env#*=}" + if [[ -n "$ev" ]]; then + save_env "$ek" "$ev" || { + printf '{"error":%s}\n' "$(jstr "the region was not saved: could not write to $ITBL_ENV")"; exit 30; } + elif [[ -f "$ITBL_ENV" ]]; then + grep -v "^${ek}=" "$ITBL_ENV" > "$ITBL_ENV.tmp" 2>/dev/null || true + mv "$ITBL_ENV.tmp" "$ITBL_ENV" + fi + printf -v "$ek" '%s' "$ev" + fi M="$(scope_missing)" printf '{"recorded":%s,"file":%s,"next":{"command":%s}}\n' "$(scope_json "$M")" "$(jstr "$WS/resolved.env")" \ "$(jstr "$BIN/agent $([[ -n "$M" ]] && printf 'question scope' || printf preflight)")" diff --git a/bin/ios-provision b/bin/ios-provision index 36fa39a..559da7a 100755 --- a/bin/ios-provision +++ b/bin/ios-provision @@ -3704,7 +3704,9 @@ import_from_android() { die "no Android setup with a Mobile key was found at $(android_env_path)" require_ios_next "import-from-android" import_android local mobile server base email user_id region delivery existing server_lines="" android_env + local android_ws jwt_enabled=false scope_jwt="" scope_region="" android_env="$(android_env_path)" + android_ws="$(dirname "$android_env")" mobile="$(android_env_value ITBL_MOBILE_KEY)" server="$(android_env_value ITBL_SERVER_KEY)" base="$(android_env_value ITBL_BASE)" @@ -3712,6 +3714,12 @@ import_from_android() { user_id="$(android_env_value ITBL_USER_ID)" region=US [[ "$base" != https://api.eu.iterable.com ]] || region=EU + if [[ -f "$android_ws/resolved.env" ]]; then + scope_region="$(grep '^SCOPE_REGION=' "$android_ws/resolved.env" 2>/dev/null | tail -1 | cut -d= -f2-)" + scope_jwt="$(grep '^SCOPE_JWT=' "$android_ws/resolved.env" 2>/dev/null | tail -1 | cut -d= -f2-)" + [[ "$scope_region" == US || "$scope_region" == EU ]] && region="$scope_region" + [[ "$scope_jwt" == true ]] && jwt_enabled=true + fi [[ "$mobile" =~ ^[A-Za-z0-9._~+/=-]{20,}$ ]] || die "the Android Mobile key in $(android_env_path) does not look like an Iterable API key" if [[ -n "$server" && ! "$server" =~ ^[A-Za-z0-9._~+/=-]{20,}$ ]]; then @@ -3728,7 +3736,7 @@ import_from_android() { [[ "$delivery" != expo-env ]] || expo_env_ignored "$(app_root)" || deliver=later [[ -z "$server" ]] || server_lines="$(printf '# Read-only verification credential; never linked into the app.\nITERABLE_SERVER_KEY=%s' "$server")" - write_mobile_env "$mobile" "$region" false "$delivery" "$server_lines" import-from-android "$deliver" + write_mobile_env "$mobile" "$region" "$jwt_enabled" "$delivery" "$server_lines" import-from-android "$deliver" printf 'ITERABLE_IMPORTED_FROM=%s\n' "$android_env" >> "$MOBILE_ENV" rm -f "$MOBILE_VALIDATION" "$SERVER_VALIDATION" unset mobile server server_lines @@ -3740,20 +3748,21 @@ import_from_android() { ( umask 077 node -e ' - const [path, current, region, email, source, userId] = process.argv.slice(1); + const [path, current, region, jwtEnabled, email, source, userId] = process.argv.slice(1); const value = JSON.parse(current); value.android_import = "imported"; value.android_import_path = source; value.data_region = region; - value.jwt_enabled = false; + value.jwt_enabled = jwtEnabled === "true"; if (email && !value.identity_mode) value.identity_mode = "email"; else if (userId && !value.identity_mode) value.identity_mode = "user-id"; require("fs").writeFileSync(path, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 }); - ' "$DISCOVERY_META" "$current" "$region" "$email" "$android_env" "$user_id" + ' "$DISCOVERY_META" "$current" "$region" "$jwt_enabled" "$email" "$android_env" "$user_id" ) chmod 600 "$DISCOVERY_META" - printf 'Reused the Android setup from %s: the Mobile%s key, region %s, and No JWT. Both keys are validated again for iOS later; nothing was printed.\n' \ - "$android_env" "$([[ -n "$(env_value ITERABLE_SERVER_KEY)" ]] && printf ' and Server-side')" "$region" >&2 + printf 'Reused the Android setup from %s: the Mobile%s key, region %s, and JWT %s. Both keys are validated again for iOS later; nothing was printed.\n' \ + "$android_env" "$([[ -n "$(env_value ITERABLE_SERVER_KEY)" ]] && printf ' and Server-side')" "$region" \ + "$([[ "$jwt_enabled" == true ]] && printf required || printf 'not required')" >&2 [[ -z "$carried" ]] || printf 'Proposed the feature scope confirmed in the %s: %s. It is recorded only after the confirmation screen.\n' \ "$(ios_q_step0_skill "$(step0_source)")" "$carried" >&2 diff --git a/bin/questions.sh b/bin/questions.sh index f067694..0eb6091 100644 --- a/bin/questions.sh +++ b/bin/questions.sh @@ -153,12 +153,24 @@ scope_missing() { elif [[ -z "${SCOPE_IDENTITY:-}" ]]; then echo identity elif [[ -z "${SCOPE_PUSH:-}" && ",$SCOPE_FEATURES," == *,push-notifications,* && -n "$(own_push)" ]]; then echo existing-push + elif [[ -z "${SCOPE_REGION:-}" ]]; then echo region + elif [[ -z "${SCOPE_JWT:-}" && "${SCOPE_JWT_CHECK:-}" == true ]]; then echo jwt-check + elif [[ -z "${SCOPE_JWT:-}" ]]; then echo jwt + fi +} + +scope_jwt_keys_url() { + if [[ "${SCOPE_REGION:-}" == EU ]]; then + printf 'https://app.eu.iterable.com/settings/apiKeys' + else + printf 'https://app.iterable.com/settings/apiKeys' fi } scope_json() { # - printf '{"features":%s,"identity":%s,"existing_push":%s,"missing":%s}' \ + printf '{"features":%s,"identity":%s,"existing_push":%s,"region":%s,"jwt":%s,"missing":%s}' \ "$(jstr "${SCOPE_FEATURES:-}")" "$(jstr "${SCOPE_IDENTITY:-}")" "$(jstr "${SCOPE_PUSH:-}")" \ + "$(jstr "${SCOPE_REGION:-}")" "$(jstr "${SCOPE_JWT:-}")" \ "$([[ -n "$1" ]] && jstr "$1" || printf null)" } @@ -167,6 +179,9 @@ q_scope() { features) q_scope_features ;; identity) q_scope_identity ;; existing-push) q_scope_push ;; + region) q_scope_region ;; + jwt) q_scope_jwt ;; + jwt-check) q_scope_jwt_check ;; *) q_scope_recorded ;; esac } @@ -299,15 +314,104 @@ EOF printf ']}' } +q_scope_region() { + printf '{"header":"Region","selection":"single","prompt":%s,' "$(jstr "Which Iterable project region does this app use?")" + printf '"context":%s,' "$(q_arr <<'EOF' +The Mobile API key and SDK data region must match the selected Iterable project. A US key cannot be treated as an EU key, or the reverse. +EOF +)" + printf '"requires":[],"recommended":null,"options":[' + q_opt "US project" \ + "Use the standard Iterable US API endpoints." \ + "SCOPE_REGION=US recorded" "" \ + "$BIN/agent scope region US" + printf ',' + q_opt "EU project" \ + "Use the EU Iterable API host for server-side calls and the EU project key." \ + "SCOPE_REGION=EU recorded; ITBL_BASE set to api.eu.iterable.com" "" \ + "$BIN/agent scope region EU" + printf ',' + q_opt "Not sure — help me find out" \ + "Shows how to read the region from the project address, and what to do with no project yet." \ + "nothing recorded; open european-data-center and android-sdk Step 5.2 in the skill, then ask this screen again" "" + printf ']}' +} + +q_scope_jwt() { + printf '{"header":"JWT","selection":"single","prompt":%s,' "$(jstr "Unless your app already has a backend that issues user-specific Iterable JWTs, choose No JWT. Should the Mobile key you create or pick in a later step require JWT?")" + printf '"context":%s,' "$(q_arr <<'EOF' +You do not need a Mobile API key yet. A later step creates one in Iterable, or picks the one you already have, and its Require JWT setting must match this answer. + +Choose JWT only when the app already runs a backend that issues user-specific Iterable JWTs. The app then asks that backend for each token and never mints one itself. A new app without such a backend should choose No JWT. +EOF +)" + printf '"requires":[],"recommended":%s,"options":[' "$(jstr "No JWT — new app or no JWT backend")" + q_opt "No JWT — new app or no JWT backend" \ + "Records No JWT, right for an app without a JWT backend. The Iterable walk creates the Mobile key with Require JWT Off." \ + "SCOPE_JWT=false recorded" "" \ + "$BIN/agent scope jwt false" + printf ',' + q_opt "I already have a Mobile key — check its setting" \ + "Shows where to read the existing key's Require JWT setting, then you answer from what it shows." \ + "opens the JWT check screen; nothing recorded yet" "" \ + "$BIN/agent scope jwt-check" + printf ',' + q_opt "Not sure — help me decide" \ + "Explains the one fact that decides it; for most apps the answer is No JWT." \ + "nothing recorded; open jwt-enabled-api-keys in the skill, then ask this screen again" "" + printf ',' + q_opt "JWT required — we run a JWT backend" \ + "Only when a backend already issues user-specific Iterable JWTs; the integration adds an auth handler that calls it." \ + "SCOPE_JWT=true recorded" "" \ + "$BIN/agent scope jwt true" + printf ']}' +} + +q_scope_jwt_check() { + local url; url="$(scope_jwt_keys_url)" + printf '{"header":"JWT check","selection":"single","prompt":%s,' "$(jstr "Read the setting on the Mobile key this app will use: +1. Open $url (or go to Integrations > API Keys). +2. Find the key whose type is Mobile. If there are several, use the one this app will ship with. +3. Read its JWT column, or open the key and read Require JWT. +4. Answer below with what it shows. + +In Iterable → Integrations → API Keys, read your Mobile key's Require JWT setting — is it on or off?")" + printf '"context":%s,' "$(q_arr <<'EOF' +Nothing is copied here; the key itself is captured securely in a later step. If there is no Mobile key, go back and choose for the new one. +EOF +)" + printf '"requires":[],"recommended":null,"options":[' + q_opt "Require JWT is off" \ + "The existing Mobile key does not require JWT." \ + "SCOPE_JWT=false recorded" "" \ + "$BIN/agent scope jwt false" + printf ',' + q_opt "Require JWT is on" \ + "The existing Mobile key requires JWT; the integration must add an IterableAuthHandler backed by your backend." \ + "SCOPE_JWT=true recorded" "" \ + "$BIN/agent scope jwt true" + printf ',' + q_opt "Show the JWT question again" \ + "Returns to the JWT screen without recording an answer yet." \ + "SCOPE_JWT_CHECK cleared" "" \ + "$BIN/agent scope jwt-check-clear" + printf ']}' +} + q_scope_recorded() { + local jwt_label="not recorded" + [[ "${SCOPE_JWT:-}" == true ]] && jwt_label="required" + [[ "${SCOPE_JWT:-}" == false ]] && jwt_label="not required" printf '{"header":"Scope","selection":"single","prompt":%s,' "$(jstr "Recorded for this app: · Features: ${SCOPE_FEATURES//,/, } · Identity: $SCOPE_IDENTITY · The app's own push: ${SCOPE_PUSH:-not asked, because none was found or push is not in scope} +· Region: ${SCOPE_REGION:-not recorded} +· JWT: $jwt_label Keep these answers?")" printf '"context":%s,' "$(q_arr <<'EOF' -Changing them clears all three, and the questions are asked again from the first. +Changing them clears every recorded scope answer, and the questions are asked again from the first. EOF )" printf '"requires":[],"recommended":%s,"options":[' "$(jstr "Keep these answers")" @@ -318,7 +422,7 @@ EOF printf ',' q_opt "Change the answers" \ "Clears the recorded answers, and the features screen comes back." \ - "SCOPE_FEATURES, SCOPE_IDENTITY and SCOPE_PUSH cleared" "" \ + "all scope fields cleared" "" \ "$BIN/agent scope clear" printf ']}' } diff --git a/iterable-android/SKILL.md b/iterable-android/SKILL.md index afe305d..72f5084 100644 --- a/iterable-android/SKILL.md +++ b/iterable-android/SKILL.md @@ -49,11 +49,13 @@ human readers but omit several silent-failure traps documented in `/bin/agent preflight` returns `scope`, ahead of provisioning and code, until the developer has answered these. `/bin/agent question scope` asks them one screen at a time: the features, the identity (project type first), -and, when the app already handles push itself, whether to keep it. Each -option's command records its answer in `.iterable/resolved.env`; preflight's -`scope` field reports them, and `question scope` offers to change them once all -are in. Never pick an option for the developer. Prerequisites are preflight's -to find: never ask for a key in the chat. +when the app already handles push itself whether to keep it, the Iterable +project region (US or EU), and whether the Mobile key requires JWT (same +screens as native iOS provisioning). Each option's command records its answer +in `.iterable/resolved.env`; EU also records `ITBL_BASE` in `.iterable/.env`. +Preflight's `scope` field reports them, and `question scope` offers to change +them once all are in. Never pick an option for the developer. Prerequisites are +preflight's to find: never ask for a key in the chat. Do **not** assume the developer wants every feature, and do **not** start implementing on a guessed scope. diff --git a/iterable-react-native/SKILL.md b/iterable-react-native/SKILL.md index 137ce31..e79a371 100644 --- a/iterable-react-native/SKILL.md +++ b/iterable-react-native/SKILL.md @@ -68,11 +68,13 @@ cost the developer a permission prompt. `/bin/agent preflight` returns `scope`, ahead of provisioning and code, until the developer has answered these. `/bin/agent question scope` asks them one screen at a time: the features, the identity (project type first), -and, when the app already handles push itself, whether to keep it. Each -option's command records its answer in `.iterable/resolved.env`; preflight's -`scope` field reports them, and `question scope` offers to change them once all -are in. Never pick an option for the developer. Prerequisites are preflight's -to find: never ask for a key in the chat. +when the app already handles push itself whether to keep it, the Iterable +project region (US or EU), and whether the Mobile key requires JWT (same +screens as native iOS provisioning). Each option's command records its answer +in `.iterable/resolved.env`; EU also records `ITBL_BASE` in `.iterable/.env`. +Preflight's `scope` field reports them, and `question scope` offers to change +them once all are in. Never pick an option for the developer. Prerequisites are +preflight's to find: never ask for a key in the chat. **Detect the workflow from the project, do not ask which docs to use.** diff --git a/tests/agent-preflight.sh b/tests/agent-preflight.sh index 1a4addf..6b6c348 100755 --- a/tests/agent-preflight.sh +++ b/tests/agent-preflight.sh @@ -35,7 +35,10 @@ echo " bin/agent preflight — local files only, before any consent" echo # The Step 0 answers, as `agent scope` records them; tests/agent-scope.sh covers that gate. -scoped() { mkdir -p "$1/.iterable"; printf 'SCOPE_FEATURES=push-notifications,sdk-initialization\nSCOPE_IDENTITY=email\n' >> "$1/.iterable/resolved.env"; } +scoped() { + mkdir -p "$1/.iterable" + printf 'SCOPE_FEATURES=push-notifications,sdk-initialization\nSCOPE_IDENTITY=email\nSCOPE_REGION=US\nSCOPE_JWT=false\n' >> "$1/.iterable/resolved.env" +} A="$TMP/fresh"; new_app "$A" OUT="$(pf "$A")" diff --git a/tests/agent-questions.sh b/tests/agent-questions.sh index c90ef8d..b314eb2 100644 --- a/tests/agent-questions.sh +++ b/tests/agent-questions.sh @@ -14,7 +14,7 @@ set -uo pipefail # Step 0 is recorded: these checks are about the setup screens, which the scope gate # holds back until it is (tests/agent-scope.sh covers that gate). -export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both +export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both SCOPE_REGION=US SCOPE_JWT=false cd "$(dirname "$0")/.." FAILED=0 diff --git a/tests/agent-scope.sh b/tests/agent-scope.sh index 508bdef..004e5a4 100755 --- a/tests/agent-scope.sh +++ b/tests/agent-scope.sh @@ -47,9 +47,14 @@ proven() { # — provisioning proven, nothing placed in the app yet printf 'ITBL_MOBILE_KEY=%s\nITBL_SERVER_KEY=%s\n' "$MOBILE" "$SERVER" > "$1/.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' > "$1/.iterable/state.tsv" } -recorded() { # +recorded() { # [existing-push] mkdir -p "$1/.iterable" - printf 'SCOPE_FEATURES=%s\nSCOPE_IDENTITY=%s\n' "$2" "$3" >> "$1/.iterable/resolved.env" + printf 'SCOPE_FEATURES=%s\nSCOPE_IDENTITY=%s\nSCOPE_REGION=US\nSCOPE_JWT=false\n' "$2" "$3" >> "$1/.iterable/resolved.env" + [[ -n "${4:-}" ]] && printf 'SCOPE_PUSH=%s\n' "$4" >> "$1/.iterable/resolved.env" +} +finish_scope() { # — region and JWT after features, identity, and optional push + in_app "$1" scope region US >/dev/null + in_app "$1" scope jwt false >/dev/null } in_app() { local d="$1"; shift; ( cd "$d" && "$ROOT/bin/agent" "$@" 2>/dev/null ); } field() { node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{const j=JSON.parse(d);const v=process.argv[1].split(".").reduce((o,k)=>o?.[k],j);process.stdout.write(typeof v==="object"?JSON.stringify(v):String(v))})' "$1"; } @@ -138,6 +143,11 @@ E2="$(in_app "$A" scope feature inbox)"; rc2=$? || bad "an unoffered value: rc $rc '$E', rc $rc2 '$E2'" o="$(in_app "$A" scope identity user-id)" +[[ "$(field recorded.missing <<< "$o")" == region && "$(field header <<< "$(in_app "$A" question scope)")" == Region ]] \ + && ok "after identity, region is asked before JWT or provisioning" \ + || bad "region was not next after identity: $o" +finish_scope "$A" +o="$(in_app "$A" scope jwt false)" OUT="$(in_app "$A" preflight)" [[ "$(field next.command <<< "$o")" == "$ROOT/bin/agent preflight" && "$(field next.kind <<< "$OUT")" == provision \ && "$(field next.commands.0 <<< "$OUT")" == "$ROOT/bin/agent question opening" \ @@ -150,12 +160,14 @@ grep -qF "not even while the developer works in a dashboard" <<< "$(field next.s R="$(in_app "$A" question scope)" [[ "$(field header <<< "$R")" == Scope ]] && grep -qF "Identity: user-id" <<< "$(field prompt <<< "$R")" \ + && grep -qF "Region: US" <<< "$(field prompt <<< "$R")" && grep -qF "JWT: not required" <<< "$(field prompt <<< "$R")" \ && grep -qF "$ROOT/bin/agent scope clear" <<< "$(cmds <<< "$R")" \ && ok "once recorded, the scope screen shows the answers and offers to change them" \ || bad "no way to see or change the recorded answers: $R" in_app "$A" scope clear >/dev/null -[[ -z "$(rec "$A" SCOPE_FEATURES)$(rec "$A" SCOPE_IDENTITY)" && "$(in_app "$A" preflight | field next.kind)" == scope ]] \ - && ok "changing them clears all three and routes back to scope" || bad "clear left: $(cat "$A/.iterable/resolved.env")" +[[ -z "$(rec "$A" SCOPE_FEATURES)$(rec "$A" SCOPE_IDENTITY)$(rec "$A" SCOPE_REGION)$(rec "$A" SCOPE_JWT)" \ + && "$(in_app "$A" preflight | field next.kind)" == scope ]] \ + && ok "changing them clears every scope field and routes back to scope" || bad "clear left: $(cat "$A/.iterable/resolved.env")" ft="$(field free_text.command <<< "$Q")" ( cd "$A" && eval "${ft//\{\{answer\}\}/initialization only}" >/dev/null 2>&1 ) @@ -187,14 +199,40 @@ X="$(in_app "$N" question scope)" XC="$(cmds <<< "$X")" grep -qF "scope existing-push keep-both" <<< "$XC" && grep -qF "scope existing-push iterable-only" <<< "$XC" \ && in_app "$N" scope existing-push keep-both >/dev/null && [[ "$(rec "$N" SCOPE_PUSH)" == keep-both ]] \ - && [[ "$(in_app "$N" preflight | field next.kind)" == provision ]] \ - && ok "keep both or Iterable only is recorded, and then provisioning comes next" \ + && finish_scope "$N" && [[ "$(in_app "$N" preflight | field next.kind)" == provision ]] \ + && ok "keep both or Iterable only is recorded, then region and JWT, and provisioning comes next" \ || bad "the push answer did not record or route: $XC" N2="$TMP/native-no-push-scope"; native_app "$N2" in_app "$N2" scope feature event-tracking,user-profiles >/dev/null -[[ "$(in_app "$N2" scope identity email | field recorded.missing)" == null ]] \ - && ok "with push out of scope the app's own push is not asked about" || bad "push asked about while out of scope" +[[ "$(in_app "$N2" scope identity email | field recorded.missing)" == region ]] \ + && ok "with push out of scope the app's own push is skipped and region is asked next" \ + || bad "push asked about while out of scope or region skipped" + +echo +echo " Region and JWT — same order as native iOS" +echo + +in_app "$A" scope feature push-notifications >/dev/null +in_app "$A" scope identity email >/dev/null +RG="$(in_app "$A" question scope)" +[[ "$(field header <<< "$RG")" == Region ]] \ + && grep -qF "scope region US" <<< "$(cmds <<< "$RG")" && grep -qF "scope region EU" <<< "$(cmds <<< "$RG")" \ + && ok "the region screen offers US and EU" || bad "region screen: $RG" +in_app "$A" scope region EU >/dev/null +grep -qF 'ITBL_BASE=https://api.eu.iterable.com' "$A/.iterable/.env" \ + && [[ "$(rec "$A" SCOPE_REGION)" == EU ]] \ + && ok "EU records SCOPE_REGION and ITBL_BASE in the workspace env" \ + || bad "EU region did not land: $(cat "$A/.iterable/.env" 2>/dev/null) $(rec "$A" SCOPE_REGION)" +JW="$(in_app "$A" question scope)" +[[ "$(field header <<< "$JW")" == JWT ]] \ + && grep -qF "scope jwt false" <<< "$(cmds <<< "$JW")" && grep -qF "scope jwt true" <<< "$(cmds <<< "$JW")" \ + && grep -qF "scope jwt-check" <<< "$(cmds <<< "$JW")" \ + && ok "the JWT screen matches iOS: no JWT, check existing key, unsure, JWT backend" \ + || bad "JWT screen: $JW" +in_app "$A" scope jwt false >/dev/null +[[ "$(rec "$A" SCOPE_JWT)" == false && -z "$(rec "$A" SCOPE_JWT_CHECK)" ]] \ + && ok "No JWT is recorded and the jwt-check waypoint is cleared" || bad "JWT record: $(rec "$A" SCOPE_JWT) check=$(rec "$A" SCOPE_JWT_CHECK)" echo echo " The other routes" @@ -268,8 +306,7 @@ O="$(in_app "$P" question opening)" || bad "question opening with no scope: header $(field header <<< "$O")" [[ "$(in_app "$P" question choose_driver | field header)" == Features ]] \ && ok "the setup fork is held behind the scope too" || bad "choose_driver asked before the scope" -recorded "$P" push-notifications,sdk-initialization email -in_app "$P" scope existing-push keep-both >/dev/null +recorded "$P" push-notifications,sdk-initialization email keep-both O="$(in_app "$P" question opening)" [[ "$(field header <<< "$O")" != Features && "$(field header <<< "$O")" != Identity && -n "$(field header <<< "$O")" ]] \ && ok "with the scope recorded, the same command is the setup question" \ @@ -281,8 +318,7 @@ O="$(in_app "$S")" && "$(field next.question.header <<< "$O")" == Features ]] \ && ok "a status read before the scope reports scope, with the features screen as its question" \ || bad "status with no scope: $(field next.kind <<< "$O") / $(field next.command <<< "$O")" -recorded "$S" push-notifications,sdk-initialization email -in_app "$S" scope existing-push keep-both >/dev/null +recorded "$S" push-notifications,sdk-initialization email keep-both [[ "$(in_app "$S" | field next.kind)" != scope ]] \ && ok "with the scope recorded, a status read routes to setup again" || bad "status still gated after the scope" @@ -296,7 +332,9 @@ draw "features" scope draw "more features" scope_more in_app "$SA" scope feature push-notifications >/dev/null; draw "identity" scope in_app "$SA" scope identity user-id >/dev/null; draw "the app's own push" scope -in_app "$SA" scope existing-push iterable-only >/dev/null; draw "recorded answers" scope +in_app "$SA" scope existing-push iterable-only >/dev/null; draw "region" scope +in_app "$SA" scope region US >/dev/null; draw "JWT" scope +in_app "$SA" scope jwt false >/dev/null; draw "recorded answers" scope echo ((FAILED)) && { echo " FAILED"; exit 1; } diff --git a/tests/agent-transitions.sh b/tests/agent-transitions.sh index 14f92de..38c3968 100755 --- a/tests/agent-transitions.sh +++ b/tests/agent-transitions.sh @@ -25,7 +25,7 @@ set -uo pipefail # Step 0 is recorded: these checks are about the setup screens, which the scope gate # holds back until it is (tests/agent-scope.sh covers that gate). -export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both +export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both SCOPE_REGION=US SCOPE_JWT=false cd "$(dirname "$0")/.." FAILED=0 diff --git a/tests/text-surface.sh b/tests/text-surface.sh index 73cf1c7..0f4d6be 100755 --- a/tests/text-surface.sh +++ b/tests/text-surface.sh @@ -18,7 +18,7 @@ set -uo pipefail # Step 0 is recorded: these checks are about the setup screens, which the scope gate # holds back until it is (tests/agent-scope.sh covers that gate). -export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both +export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both SCOPE_REGION=US SCOPE_JWT=false cd "$(dirname "$0")/.." FAILED=0