From 53842b7b0e0728e9cc786b4de4ad146398098dd9 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 27 Aug 2026 11:36:25 +0100 Subject: [PATCH 1/2] fix(context7): stop indexing eval sample transcripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The published Context7 library /iterable/iterable-sdk-skill was serving eval/transcripts/android/*.baseline.sample.md as authoritative answers. Those files are hand-authored *anti-pattern* samples — a query for initializeInBackground returned setAutoPushRegistration(true) alongside a manual registerForPush() (PITFALLS #6), and jwt-auth.baseline.sample.md puts setEmail inside the init callback (PITFALLS #2). `folders` alone was not holding: index results cited sources/, iterable-android/snapshot/, SKILL.md and PITFALLS.md despite folders being ["polished"]. Add iterable-android to folders so the intended surface is declared explicitly, and exclude *.sample.md / *.baseline.md so the eval fixtures cannot be served regardless of folder scoping. Co-Authored-By: Claude Opus 5 --- context7.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/context7.json b/context7.json index 7893c4d..eff701c 100644 --- a/context7.json +++ b/context7.json @@ -5,10 +5,13 @@ "url": "https://context7.com/iterable/iterable-sdk-skill", "public_key": "pk_JMMoVfKCtHoC5E3alGPcX", "folders": [ + "iterable-android", "polished" ], "excludeFiles": [ - "*.layer-a.md" + "*.layer-a.md", + "*.sample.md", + "*.baseline.md" ], "rules": [ "Treat divergence from Iterable's published behavior as a critical issue — never invent SDK APIs, version numbers, or dashboard paths. If the user is on a different SDK version than the snippet's frontmatter `sdk_min_version`, surface the mismatch.", From 8c9091a6e3bf668842b7acb7fa02be8803614c7f Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 27 Aug 2026 12:11:29 +0100 Subject: [PATCH 2/2] refactor: collapse three doc copies into one reference corpus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo carried the same 14 docs three times: sources/ (raw fetch), polished/ (transform output), and iterable-android/snapshot/ (a byte-identical copy of polished/ that CI enforced via snapshot:verify). Only the third was ever read by the agent. The other two existed so that CI could check them against each other, and snapshot:verify existed to catch the copy step someone would inevitably forget. Now there is one copy: iterable-android/reference/, written directly by `pnpm refresh:docs` (fetch + transform in one pass, no intermediate on disk). 12k lines of duplicated markdown deleted. Pipeline: - new src/refresh.ts replaces fetch.ts + polish-layer-a.ts - snapshot.ts deleted (nothing to keep in sync) - validate-polished.ts -> validate-reference.ts; it now also checks that the corpus and pipeline/config agree on which slugs exist, so a config rename that orphans a routing-table slug fails CI - dropped the `snippets:` frontmatter manifest, its schema block, and recompute-manifest.ts. It was ~12KB of hashes and line counts across the corpus whose only consumer was the validator that checked it against itself, and the agent had to skip past it on every read. - dropped `layer`/`polished_at` frontmatter. polished_at regenerated on every run, which meant the refresh workflow's `git diff` change-check ALWAYS reported changes and opened a PR with an empty slug list even when no doc had moved. Provenance now comes from source_sha, so a no-op refresh is a genuine no-op (verified: 0 written, 14 unchanged). CI: - removed check:snippets and lint:chunking. Both hardcoded exit 0, so neither could ever fail a build, and check:snippets pulled a Kotlin 1.9.24 + Java 17 toolchain into every run to produce warnings that were skipped locally anyway. - deleted publish-context7.yml: 4 runs, 4 failures, POSTing to a placeholder URL with a secret that was never provisioned. Context7 crawls on its own schedule; this only produced red Xs. - folded validate-plugins.yml into validate.yml (it existed to avoid the Java setup that validate.yml no longer has). Skill: SKILL.md reads from reference/ and no longer claims Context7 is unpublished-and-to-be-avoided. Dropped the hardcoded "known latest 3.8.0" floor — Maven is at 3.10.1 and it sat six lines under "never trust a number baked into this file". Removed the TODO-PHASE-3 placeholder in .context7-library-id, which nothing referenced. REVIEW.md's docs-refresh flow is rewritten around the question a reviewer can actually answer — does this new guidance contradict PITFALLS.md — instead of the mechanical copy-and-verify steps that no longer exist. Verified: bodies of all 14 docs byte-identical before/after; refresh:docs idempotent on a second run; validate-reference fails correctly on bad frontmatter, an orphaned file, and a missing slug. Co-Authored-By: Claude Opus 5 --- .github/pull_request_template.md | 22 +- .github/workflows/publish-context7.yml | 53 - .github/workflows/refresh-docs.yml | 56 +- .github/workflows/validate-plugins.yml | 57 - .github/workflows/validate.yml | 43 +- README.md | 36 +- REVIEW.md | 94 +- context7.json | 8 +- eval/scenarios/android.yml | 4 +- iterable-android/.context7-library-id | 14 - iterable-android/SKILL.md | 68 +- .../android-app-links.md | 15 - .../{snapshot => reference}/android-sdk.md | 123 -- .../configure-the-android-sdk.md | 19 - .../customizing-mobile-inbox-on-android.md | 79 -- .../deep-linking-with-partners.md | 3 - ...ded-messages-with-iterables-android-sdk.md | 83 -- .../identifying-the-user.md | 51 - .../in-app-messages-on-android.md | 35 - .../push-notification-overview.md | 3 - .../setting-up-android-push-notifications.md | 39 - .../setting-up-mobile-inbox-on-android.md | 3 - .../setting-up-unknown-user-activation.md | 3 - ...cking-events-with-iterables-mobile-sdks.md | 31 - .../reference}/updating-user-profiles.md | 17 +- .../snapshot/updating-user-profiles.md | 341 ----- pipeline/config/android.yml | 30 +- pipeline/package.json | 12 +- ...shed.schema.json => reference.schema.json} | 68 +- pipeline/src/check-snippets.ts | 195 --- pipeline/src/enrich-summary.ts | 8 +- pipeline/src/lib/layer-a.ts | 83 +- pipeline/src/lint-chunking.ts | 159 --- pipeline/src/polish-layer-a.ts | 111 -- pipeline/src/recompute-manifest.ts | 70 - pipeline/src/{fetch.ts => refresh.ts} | 110 +- pipeline/src/snapshot.ts | 138 -- pipeline/src/validate-polished.ts | 162 --- pipeline/src/validate-reference.ts | 113 ++ .../android/android-app-links.polished.md | 148 -- polished/android/android-sdk.polished.md | 1251 ----------------- .../configure-the-android-sdk.polished.md | 365 ----- ...mizing-mobile-inbox-on-android.polished.md | 684 --------- .../deep-linking-with-partners.polished.md | 28 - ...ges-with-iterables-android-sdk.polished.md | 829 ----------- .../android/identifying-the-user.polished.md | 293 ---- .../in-app-messages-on-android.polished.md | 300 ---- .../push-notification-overview.polished.md | 59 - ...-up-android-push-notifications.polished.md | 473 ------- ...ing-up-mobile-inbox-on-android.polished.md | 115 -- ...ing-up-unknown-user-activation.polished.md | 123 -- ...nts-with-iterables-mobile-sdks.polished.md | 338 ----- .../updating-user-profiles.polished.md | 341 ----- sources/android/android-app-links.md | 136 -- sources/android/android-sdk.md | 1156 --------------- sources/android/configure-the-android-sdk.md | 346 ----- .../customizing-mobile-inbox-on-android.md | 650 --------- sources/android/deep-linking-with-partners.md | 21 - ...ded-messages-with-iterables-android-sdk.md | 754 ---------- sources/android/identifying-the-user.md | 245 ---- sources/android/in-app-messages-on-android.md | 290 ---- sources/android/push-notification-overview.md | 50 - .../setting-up-android-push-notifications.md | 445 ------ .../setting-up-mobile-inbox-on-android.md | 132 -- .../setting-up-unknown-user-activation.md | 118 -- ...cking-events-with-iterables-mobile-sdks.md | 308 ---- 66 files changed, 365 insertions(+), 12192 deletions(-) delete mode 100644 .github/workflows/publish-context7.yml delete mode 100644 .github/workflows/validate-plugins.yml delete mode 100644 iterable-android/.context7-library-id rename iterable-android/{snapshot => reference}/android-app-links.md (94%) rename iterable-android/{snapshot => reference}/android-sdk.md (95%) rename iterable-android/{snapshot => reference}/configure-the-android-sdk.md (97%) rename iterable-android/{snapshot => reference}/customizing-mobile-inbox-on-android.md (93%) rename iterable-android/{snapshot => reference}/deep-linking-with-partners.md (94%) rename iterable-android/{snapshot => reference}/embedded-messages-with-iterables-android-sdk.md (95%) rename iterable-android/{snapshot => reference}/identifying-the-user.md (89%) rename iterable-android/{snapshot => reference}/in-app-messages-on-android.md (94%) rename iterable-android/{snapshot => reference}/push-notification-overview.md (97%) rename iterable-android/{snapshot => reference}/setting-up-android-push-notifications.md (96%) rename iterable-android/{snapshot => reference}/setting-up-mobile-inbox-on-android.md (98%) rename iterable-android/{snapshot => reference}/setting-up-unknown-user-activation.md (99%) rename iterable-android/{snapshot => reference}/tracking-events-with-iterables-mobile-sdks.md (95%) rename {sources/android => iterable-android/reference}/updating-user-profiles.md (94%) delete mode 100644 iterable-android/snapshot/updating-user-profiles.md rename pipeline/schema/{polished.schema.json => reference.schema.json} (54%) delete mode 100644 pipeline/src/check-snippets.ts delete mode 100644 pipeline/src/lint-chunking.ts delete mode 100644 pipeline/src/polish-layer-a.ts delete mode 100644 pipeline/src/recompute-manifest.ts rename pipeline/src/{fetch.ts => refresh.ts} (53%) delete mode 100644 pipeline/src/snapshot.ts delete mode 100644 pipeline/src/validate-polished.ts create mode 100644 pipeline/src/validate-reference.ts delete mode 100644 polished/android/android-app-links.polished.md delete mode 100644 polished/android/android-sdk.polished.md delete mode 100644 polished/android/configure-the-android-sdk.polished.md delete mode 100644 polished/android/customizing-mobile-inbox-on-android.polished.md delete mode 100644 polished/android/deep-linking-with-partners.polished.md delete mode 100644 polished/android/embedded-messages-with-iterables-android-sdk.polished.md delete mode 100644 polished/android/identifying-the-user.polished.md delete mode 100644 polished/android/in-app-messages-on-android.polished.md delete mode 100644 polished/android/push-notification-overview.polished.md delete mode 100644 polished/android/setting-up-android-push-notifications.polished.md delete mode 100644 polished/android/setting-up-mobile-inbox-on-android.polished.md delete mode 100644 polished/android/setting-up-unknown-user-activation.polished.md delete mode 100644 polished/android/tracking-events-with-iterables-mobile-sdks.polished.md delete mode 100644 polished/android/updating-user-profiles.polished.md delete mode 100644 sources/android/android-app-links.md delete mode 100644 sources/android/android-sdk.md delete mode 100644 sources/android/configure-the-android-sdk.md delete mode 100644 sources/android/customizing-mobile-inbox-on-android.md delete mode 100644 sources/android/deep-linking-with-partners.md delete mode 100644 sources/android/embedded-messages-with-iterables-android-sdk.md delete mode 100644 sources/android/identifying-the-user.md delete mode 100644 sources/android/in-app-messages-on-android.md delete mode 100644 sources/android/push-notification-overview.md delete mode 100644 sources/android/setting-up-android-push-notifications.md delete mode 100644 sources/android/setting-up-mobile-inbox-on-android.md delete mode 100644 sources/android/setting-up-unknown-user-activation.md delete mode 100644 sources/android/tracking-events-with-iterables-mobile-sdks.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index aa6cdab..795f5c0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,8 +8,8 @@ Full reviewer guide: REVIEW.md ## Type - [ ] Docs refresh (automated, opened by `refresh-docs.yml`) -- [ ] Manual polished-content edit -- [ ] Pipeline / schema / CI / skill change +- [ ] Manual reference-content edit +- [ ] Skill / pipeline / CI change --- @@ -19,35 +19,33 @@ Full reviewer guide: REVIEW.md Reviewer checklist (see [`REVIEW.md`](../REVIEW.md) for the full version): -- [ ] Source diff looks sane; `source_ref` is a commit SHA, not a branch -- [ ] `pnpm snapshot:refresh` committed +- [ ] `source_ref` is a commit SHA, not a branch; the `source.ref` bump matches the docs commit in the body - [ ] `pnpm check:all` green locally -- [ ] Corpus diff read against `sources/`: the transform only reshaped (boilerplate stripped, callouts converted) — no content added, weakened, or reversed +- [ ] **Changed guidance still agrees with `PITFALLS.md`** — if the docs now contradict a pitfall, resolve it in this PR +- [ ] If a referenced heading moved, `SKILL.md`'s routing table updated to match +- [ ] Diff read as documentation: the transform only reshaped (boilerplate stripped, callouts converted) — no content added, weakened, or reversed - [ ] Upstream snippet bugs (if any) tracked as separate issues against `Iterable/iterable-docs` — **not** hand-fixed here --- -## If this is a manual polished-content edit +## If this is a manual reference-content edit Why is the deterministic transform insufficient for this change? (One sentence.) -- [ ] `pnpm recompute:manifest` run for every edited corpus file -- [ ] `pnpm snapshot:refresh` committed - [ ] `pnpm check:all` green locally - [ ] Considered whether the transform (`pipeline/src/lib/layer-a.ts`) could be updated instead of editing by hand +- [ ] Noted that the next `pnpm refresh:docs` will overwrite this file if upstream changes --- -## If this is a pipeline / schema / CI / skill change +## If this is a skill / pipeline / CI change -- [ ] `pnpm typecheck` green - [ ] `pnpm check:all` green - [ ] If changing the schema or a validator, tested both the success and the failure case locally -- [ ] If changing `iterable-android/SKILL.md`, sanity-checked the routing table against `polished//` -- [ ] If changing the snippet manifest format, ran `pnpm recompute:manifest polished/**/*.polished.md` and confirmed no semantic drift +- [ ] If changing `iterable-android/SKILL.md`, sanity-checked the routing table against `iterable-android/reference/` --- diff --git a/.github/workflows/publish-context7.yml b/.github/workflows/publish-context7.yml deleted file mode 100644 index 68fa882..0000000 --- a/.github/workflows/publish-context7.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Publish to Context7 - -# Fires after a refresh PR merges and the published surface (polished docs -# or skill) actually changed. Tells Context7 to re-index our library so -# updates land in agent results without waiting for the scheduled crawl. -# -# STUB: the POST below targets a placeholder endpoint. The real reindex -# URL and the payload shape are pending confirmation from Context7 -# (Lauren). The CONTEXT7_API_KEY repo secret is not provisioned yet — -# until it is, the curl call will fail and the job will exit non-zero. -# That's intentional so a missing secret is loud rather than silent. - -on: - push: - branches: [main] - paths: - - polished/** - - iterable-android/** - - context7.json - workflow_dispatch: - -permissions: - contents: read - -jobs: - publish: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Reindex Context7 library - env: - CONTEXT7_API_KEY: ${{ secrets.CONTEXT7_API_KEY }} - # TODO: replace once Lauren / Context7 confirm the endpoint shape. - CONTEXT7_REINDEX_URL: https://context7.com/api/v1/libraries/iterable/sdks/reindex - run: | - set -euo pipefail - if [[ -z "${CONTEXT7_API_KEY:-}" ]]; then - echo "::error::CONTEXT7_API_KEY secret is not set on this repo." - echo "Provision the secret once Context7 issues a token, then re-run." - exit 1 - fi - echo "Triggering Context7 reindex for commit ${GITHUB_SHA}" - curl --fail-with-body --silent --show-error \ - -X POST "${CONTEXT7_REINDEX_URL}" \ - -H "Authorization: Bearer ${CONTEXT7_API_KEY}" \ - -H "Content-Type: application/json" \ - -d "$(cat <> "$GITHUB_OUTPUT" echo "Resolved ${req} → ${sha}" - - name: Fetch sources (reads private iterable-docs via gh api) + - name: Refresh reference corpus (reads private iterable-docs via gh api) working-directory: pipeline env: GH_TOKEN: ${{ secrets.DOCS_READ_TOKEN }} SOURCE_REF: ${{ steps.ref.outputs.sha }} - run: pnpm fetch:sources -- ${{ github.event.inputs.platform || github.event.client_payload.platform || 'android' }} - - - name: Polish Layer A - working-directory: pipeline - run: pnpm polish:a -- --platform=${{ github.event.inputs.platform || github.event.client_payload.platform || 'android' }} + run: pnpm refresh:docs -- ${{ github.event.inputs.platform || github.event.client_payload.platform || 'android' }} - name: Detect changes id: changes run: | - if git diff --quiet sources/ polished/; then + ref_dir=iterable-android/reference + if git diff --quiet "$ref_dir"; then echo "changed=false" >> "$GITHUB_OUTPUT" exit 0 fi echo "changed=true" >> "$GITHUB_OUTPUT" - # Slugs touched in sources/ this run - slugs=$(git diff --name-only sources/ \ - | sed -E 's|sources/[^/]+/([^/]+)\.md|\1|' \ + slugs=$(git diff --name-only "$ref_dir" \ + | sed -E "s|.*/([^/]+)\.md|\1|" \ | sort -u | paste -sd ', ' -) echo "slugs=${slugs}" >> "$GITHUB_OUTPUT" @@ -111,13 +106,6 @@ jobs: ${{ steps.ref.outputs.sha }} "${{ steps.ref.outputs.label }}" - - name: Refresh snapshot - if: steps.changes.outputs.changed == 'true' - working-directory: pipeline - # polish:a wrote the fresh corpus to polished/; the snapshot must be - # regenerated from it or the PR fails its own snapshot:verify gate. - run: pnpm snapshot:refresh - - name: Open refresh PR if: steps.changes.outputs.changed == 'true' uses: peter-evans/create-pull-request@v6 @@ -132,9 +120,9 @@ jobs: Slugs: ${{ steps.changes.outputs.slugs }} body: | - Automated refresh of `sources/` and the deterministic `polished/` - corpus (Layer A). **No LLM rewrite step** — the polished corpus is - a deterministic transform of the docs. + Automated refresh of `iterable-android/reference/` — the docs corpus + the skill reads. **No LLM step**: the corpus is a deterministic + transform of Iterable's published docs. **Slugs touched:** ${{ steps.changes.outputs.slugs }} **Docs commit:** `${{ steps.ref.outputs.sha }}` @@ -142,18 +130,16 @@ jobs: ## Reviewer steps - 1. Check out this branch. - 2. Spot-check the diff against `sources/` for content fidelity — - the transform only strips boilerplate / normalizes structure, - so headings and code should match the upstream docs. + 1. Read the diff as docs, not code — this is what the agent will + tell developers to do. Check that changed guidance still agrees + with `iterable-android/PITFALLS.md`; if the docs now contradict a + pitfall, the pitfall needs updating in the same PR. + 2. If a slug's headings moved, check the routing table in + `iterable-android/SKILL.md` still points at sections that exist. 3. Confirm the `source.ref` bump in `pipeline/config` matches the docs commit above (provenance stays honest). - 4. Run `pnpm snapshot:refresh` and commit the resulting - `iterable-android/snapshot/` changes. CI's `snapshot:verify` - gate will fail the build otherwise. - 5. Confirm `pnpm check:all` is green locally. - 6. Merge to `main`. Context7 picks up the change on its next - crawl (`context7.json` controls scope). + 4. Merge to `main`. Context7 re-indexes on its next crawl + (`context7.json` controls scope). labels: | docs-refresh automated diff --git a/.github/workflows/validate-plugins.yml b/.github/workflows/validate-plugins.yml deleted file mode 100644 index 490b2d3..0000000 --- a/.github/workflows/validate-plugins.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Validate plugin manifests - -# Lightweight gate for Cursor/Claude plugin packaging — no Java/Kotlin needed. - -on: - push: - branches: [main] - paths: - - .cursor-plugin/** - - .claude-plugin/** - - mcp.json - - .mcp.json - - pipeline/src/validate-plugins.ts - - pipeline/package.json - - .github/workflows/validate-plugins.yml - pull_request: - branches: [main] - paths: - - .cursor-plugin/** - - .claude-plugin/** - - mcp.json - - .mcp.json - - pipeline/src/validate-plugins.ts - - pipeline/package.json - - .github/workflows/validate-plugins.yml - workflow_dispatch: - -permissions: - contents: read - -jobs: - validate-plugins: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - cache-dependency-path: pipeline/pnpm-lock.yaml - - - name: Install pipeline deps - working-directory: pipeline - run: pnpm install --frozen-lockfile - - - name: Validate plugin manifests - working-directory: pipeline - run: pnpm validate:plugins diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 8084b2b..ba966f6 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,27 +1,30 @@ -name: Validate polished corpus +name: Validate -# Runs the pipeline's full validation suite on every push to main and every PR. -# Blocking gates: typecheck, polished-frontmatter schema, layer-a vs polished -# structural diff. Advisory (always exit 0): chunking lint and kotlinc snippet -# check. The snippet check is advisory in v1 because kotlinc -script runs full -# semantic analysis and floods on "unresolved reference" without a classpath; -# tracked follow-up loads the Iterable SDK + android.jar so it can become -# blocking in v2. +# Blocking gates only: typecheck, reference-corpus frontmatter + config/corpus +# slug agreement, and plugin manifests. Every gate here can actually fail. on: push: branches: [main] paths: - - polished/** + - iterable-android/** - pipeline/** - - iterable-android/snapshot/** + - .claude-plugin/** + - .cursor-plugin/** + - mcp.json + - .mcp.json + - context7.json - .github/workflows/validate.yml pull_request: branches: [main] paths: - - polished/** + - iterable-android/** - pipeline/** - - iterable-android/snapshot/** + - .claude-plugin/** + - .cursor-plugin/** + - mcp.json + - .mcp.json + - context7.json - .github/workflows/validate.yml workflow_dispatch: @@ -48,22 +51,6 @@ jobs: cache: pnpm cache-dependency-path: pipeline/pnpm-lock.yaml - - name: Setup Java (kotlinc dependency) - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: "17" - - - name: Install Kotlin compiler - run: | - set -euo pipefail - curl -sL "https://github.com/JetBrains/kotlin/releases/download/v1.9.24/kotlin-compiler-1.9.24.zip" -o /tmp/kotlinc.zip - unzip -q /tmp/kotlinc.zip -d "$HOME" - echo "$HOME/kotlinc/bin" >> "$GITHUB_PATH" - - - name: Verify kotlinc - run: kotlinc -version - - name: Install pipeline deps working-directory: pipeline run: pnpm install --frozen-lockfile diff --git a/README.md b/README.md index 2eff3fd..656eed3 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ when integrating Iterable's SDK into your Android app — push, in-app messages, user identity, and more. Because this skill is built specifically for the Iterable Android SDK, you get: -- **Official docs, always available.** The skill ships a documentation snapshot - (`iterable-android/snapshot/`), so your assistant can reference Iterable's - content even when you're offline. +- **Official docs, always available.** The skill ships Iterable's documentation + inside it (`iterable-android/reference/`), so your assistant can reference + Iterable's content even when you're offline. - **Kotlin-first, version-pinned examples.** Snippets target the Android SDK releases they were validated against — not generic pseudocode. - **Pitfall-aware answers.** The skill includes Iterable-supplied guidance for @@ -126,15 +126,15 @@ start. ## How it works The skill carries a copy of the Iterable documentation inside it -(`iterable-android/snapshot/`), so it always has the docs on hand — even offline. -When your assistant works on an Iterable Android SDK task, the skill activates -and routes it to the right doc slug, pitfalls, and integration checklist in -[`iterable-android/SKILL.md`](iterable-android/SKILL.md). +(`iterable-android/reference/`), so it always has the docs on hand — even +offline. When your assistant works on an Iterable Android SDK task, the skill +activates and routes it to the right doc slug, pitfalls, and integration +checklist in [`iterable-android/SKILL.md`](iterable-android/SKILL.md). -The bundled snapshot is the authoritative doc source today. When Iterable's -source documentation changes, an automated pipeline refreshes the skill's -content (see [Staying current](#staying-current)); pick up updates by updating -your plugin or re-pulling the repo. +That bundled copy is the authoritative doc source — there is nothing to fetch at +runtime. When Iterable's source documentation changes, an automated workflow +refreshes it (see [Staying current](#staying-current)); pick up updates by +updating your plugin or re-pulling the repo. ## What it covers @@ -151,19 +151,19 @@ routing table. When Iterable's docs change, a workflow rebuilds the skill's content and opens a PR for a reviewer to check and merge — updates are never applied automatically. After a release lands, update your plugin install (or re-pull if you cloned) to -pick up the latest snapshot. See [`REVIEW.md`](REVIEW.md) for the reviewer +pick up the latest docs. See [`REVIEW.md`](REVIEW.md) for the reviewer playbook. ## Repo layout ``` -iterable-android/ the installable skill (SKILL.md + PITFALLS.md + snapshot/) -polished/ the docs in agent-ready form -pipeline/ tooling that builds polished/ from sources/, CI-gated -sources/ raw Iterable docs, fetched at pinned commits +iterable-android/ the installable skill — SKILL.md + PITFALLS.md + reference/ + (reference/ is Iterable's docs in agent-ready form) +pipeline/ refresh tooling + validation gates, CI-run +eval/ scenario definitions for scoring skill vs. no-skill answers .claude-plugin/ Claude Code + Codex plugin + marketplace manifests .cursor-plugin/ Cursor plugin + marketplace manifests -context7.json Context7 manifest (future live-doc indexing) +context7.json Context7 indexing manifest mcp.json Context7 MCP server config (Cursor plugin auto-discovery) -.mcp.json same config (Claude Code auto-discovery; kept in sync by CI) +.mcp.json same config (Claude Code auto-discovery) ``` diff --git a/REVIEW.md b/REVIEW.md index 08096ef..6fbaab0 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -1,14 +1,14 @@ # Reviewer guide -The pipeline opens automated PRs whenever Iterable's docs change (see +A workflow opens an automated PR whenever Iterable's docs change (see [`refresh-docs.yml`](.github/workflows/refresh-docs.yml)). This document is the reviewer's playbook for those PRs and for any hand-authored change to -the corpus. +`iterable-android/reference/`. The corpus is a **deterministic transform** of Iterable's docs — **no LLM -rewrites the content**. So review is mostly mechanical: confirm the transform -faithfully reshaped the upstream docs and didn't lose anything. There is no -LLM output to second-guess. +rewrites the content**. There is no LLM output to second-guess, so the review +is not about hallucination. It's about whether the new guidance is something we +want the agent handing to a developer. --- @@ -18,61 +18,56 @@ Three flavors of PR land in this repo: | PR type | Source | Reviewer focus | |---|---|---| -| **Automated docs refresh** | `refresh-docs.yml` dispatch / `workflow_dispatch` | Source diff fidelity + snapshot refreshed | -| **Manual content edit** | a contributor hand-edits a corpus doc | Why is hand-editing needed? Should the transform learn it instead? | -| **Pipeline / schema / CI change** | edits under `pipeline/`, `.github/`, or `iterable-android/` (not snapshot) | Standard code review, plus run `pnpm check:all` | +| **Automated docs refresh** | `refresh-docs.yml` dispatch / `workflow_dispatch` | Does the new guidance still agree with `PITFALLS.md`? | +| **Manual content edit** | a contributor hand-edits a reference doc | Why is hand-editing needed? Should the transform learn it instead? | +| **Skill / pipeline / CI change** | edits to `SKILL.md`, `PITFALLS.md`, `pipeline/`, `.github/` | Standard code review, plus run `pnpm check:all` | The rest of this document is the **docs-refresh** flow because that's the one that happens on cadence. The other two are covered by standard code review. --- -## Step 1 — Verify the source diff looks sane (2 min) +## Step 1 — Check provenance (1 min) -Open the PR. The body lists the touched slugs and points at the auto-fetched -sources. Spot-check: +Open the PR. The body lists the touched slugs and the docs commit. -- [ ] **Source files match Iterable's docs at the pinned commit.** If - `source_ref` in any frontmatter is suspicious (e.g. points to a branch - rather than a commit SHA), stop and investigate — the pin is the pipeline's - one guarantee against moving-target drift. -- [ ] **No source file was added or removed unexpectedly.** A new file - means the pipeline picked up a new Iterable doc; confirm it's intentional. -- [ ] **The slug list in the PR title matches the actual `sources/` diff.** - Mismatch usually means the workflow misparsed slugs — flag, don't merge. +- [ ] **`source_ref` in the changed frontmatter is a 40-char commit SHA**, not a + branch name. The pin is our one guarantee against moving-target drift. +- [ ] **The `source.ref` bump in `pipeline/config` matches the docs commit in + the PR body.** +- [ ] **No doc was added or removed unexpectedly.** The corpus only contains + slugs listed in `pipeline/config/.yml`; `validate:reference` fails + if they disagree, so an add/remove means someone edited the config too. --- -## Step 2 — Refresh the snapshot and run the gates (2 min) - -The transform runs automatically in the refresh workflow, so the `polished/` -diff is already in the PR. You just need to keep the installable snapshot in -sync and confirm the gates pass: +## Step 2 — Run the gates (1 min) ```bash git checkout -cd pipeline -pnpm snapshot:refresh # mirror polished/ → iterable-android/snapshot/ -pnpm check:all # MUST be green before you push +cd pipeline && pnpm check:all ``` -Commit any `iterable-android/snapshot/` changes — CI's `snapshot:verify` gate -will fail the build otherwise. - --- -## Step 3 — Read the diff for what the gates can't catch (5 min) +## Step 3 — Read the diff as documentation (5 min) -The mechanical gates catch frontmatter validity, snippet-manifest consistency, -snapshot drift, and (advisory) snippet syntax. Because the transform is -deterministic, there's no hallucination risk — but it's still worth a human -read of the diff against `sources/`: +This is the part no gate can do. The diff is what the agent will tell a +developer to do, so read it that way: +- [ ] **Does the changed guidance contradict `PITFALLS.md`?** This is the most + important question in the review. The pitfalls encode silent-failure traps; + if Iterable's docs now recommend something a pitfall warns against, one of the + two is wrong. Resolve it in this PR — don't merge a corpus that argues with + itself. +- [ ] **Did headings move?** `SKILL.md`'s routing table points at slugs, and the + upgrade row points at `## Upgrading the SDK` inside `android-sdk`. If a + referenced section was renamed, update the routing table in the same PR. - [ ] **The transform only reshaped, never changed meaning.** Boilerplate removed, callouts converted, blank lines collapsed — but no claim added, - weakened, or reversed. If the diff shows a *content* change that isn't - explained by an upstream edit, the transform has a bug; file it against - `pipeline/`, don't hand-patch the output. + weakened, or reversed. A *content* change not explained by an upstream edit + means the transform has a bug; file it against `pipeline/`, don't hand-patch + the output. - [ ] **Links still resolve.** The transform doesn't touch URLs, but upstream may have changed one. Spot-check any new/changed `support.iterable.com` link. @@ -80,22 +75,19 @@ read of the diff against `sources/`: - [ ] **Snippet errors that came from upstream stay in.** The corpus mirrors the docs; we don't hand-fix upstream typos here. Track them as issues against `Iterable/iterable-docs` and move on. -- [ ] **Snippet-match ≠ snippet-compiles.** `snapshot:verify` proves the - snapshot matches the corpus byte-for-byte; the `kotlinc` gate is advisory - (no classpath). Neither proves a snippet compiles against the pinned SDK. A - doc can be perfectly faithful and still ship a *wrong-overload* example - (real case: the 3-arg `initializeInBackground(context, key, config)` binds - config to the callback slot — see PITFALLS.md #18). When a snippet is - load-bearing, eyeball it against the actual SDK signatures; if it's an SDK - foot-gun rather than a one-doc typo, add it to `PITFALLS.md` rather than - patching the doc. (v2: blocking compile check with the SDK aar closes this.) +- [ ] **Nothing here proves a snippet compiles.** The gates check frontmatter + and slug agreement, not code. A doc can be perfectly faithful and still ship a + *wrong-overload* example (real case: the 3-arg + `initializeInBackground(context, key, config)` binds config to the callback + slot — see PITFALLS.md #18). When a snippet is load-bearing, eyeball it + against the actual SDK signatures; if it's an SDK foot-gun rather than a + one-doc typo, add it to `PITFALLS.md` rather than patching the doc. --- ## Step 4 — Final check before merging (1 min) - [ ] `pnpm check:all` is green locally. -- [ ] `iterable-android/snapshot/` was refreshed in this PR. - [ ] The PR description still accurately describes what changed. --- @@ -134,8 +126,10 @@ docs-refresh PR. iOS/JavaScript snippets (`identifying-the-user`, `updating-user-profiles`, `tracking-events-with-iterables-mobile-sdks`). Deterministic foreign-language stripping is a candidate v1.1 transform. -- `kotlinc -script` snippet check is advisory (no classpath in v1); the - "unresolved reference" warnings are expected. +- **No snippet compile check.** There is no gate that compiles corpus snippets + against the pinned SDK; a reviewer's eye on load-bearing snippets is the only + check. Adding one means an Android toolchain in CI — worth it only if wrong + snippets actually reach developers. If a fix would touch any of the above, open a separate PR scoped to the fix, not a refresh PR. diff --git a/context7.json b/context7.json index eff701c..46db992 100644 --- a/context7.json +++ b/context7.json @@ -1,22 +1,20 @@ { "$schema": "https://context7.com/schema/context7.json", "projectTitle": "Iterable Mobile SDKs", - "description": "Agent-facing reference for Iterable's mobile SDKs. Polished from Iterable's canonical docs with version-pinned snippets, decision tiers, and Kotlin-preferred Android examples. Covers push notifications, in-app messages, mobile inbox, embedded messaging, deep linking, JWT authentication, event tracking, user profiles, and unknown user activation.", + "description": "Agent-facing reference for Iterable's mobile SDKs. Transformed from Iterable's canonical docs with version-pinned snippets, decision tiers, and Kotlin-preferred Android examples. Covers push notifications, in-app messages, mobile inbox, embedded messaging, deep linking, JWT authentication, event tracking, user profiles, and unknown user activation.", "url": "https://context7.com/iterable/iterable-sdk-skill", "public_key": "pk_JMMoVfKCtHoC5E3alGPcX", "folders": [ - "iterable-android", - "polished" + "iterable-android" ], "excludeFiles": [ - "*.layer-a.md", "*.sample.md", "*.baseline.md" ], "rules": [ "Treat divergence from Iterable's published behavior as a critical issue — never invent SDK APIs, version numbers, or dashboard paths. If the user is on a different SDK version than the snippet's frontmatter `sdk_min_version`, surface the mismatch.", "Several Iterable integrations fail silently with no error logs (JWT-required keys with no auth handler, `setEmail` inside the init callback, stale email captured in the auth handler lambda, runtime `POST_NOTIFICATIONS` permission missing on Android 13+, custom URL schemes without `setAllowedProtocols`, in-app handler import path drift). Always surface the relevant trap explicitly when the user touches these areas, even if they didn't ask.", - "For Android, prefer Kotlin snippets over Java. The polish layer drops Java duplicates when a Kotlin equivalent exists.", + "For Android, prefer Kotlin snippets over Java where a doc shows both.", "Frontmatter `sdk_min_version` pins the SDK release each snippet was validated against — surface it when the user asks about version compatibility, or when generating code that depends on an SDK feature introduced after a known older version." ] } diff --git a/eval/scenarios/android.yml b/eval/scenarios/android.yml index dc2c4ff..6cad9f1 100644 --- a/eval/scenarios/android.yml +++ b/eval/scenarios/android.yml @@ -10,7 +10,7 @@ # - require: the pattern SHOULD appear. Present = pass. # - forbid: the pattern should NOT appear (a known foot-gun). Absent = pass. # Each check carries a `weight` and, where it maps to a documented trap, a -# `pitfall` id from iterable-android/PITFALLS.md. The scorer (eval/src/score.ts) +# `pitfall` id from iterable-android/PITFALLS.md. The scorer (pipeline/src/lib/score.ts) # is the only consumer; it never calls a model, so scores are reproducible. # # Authoring rules: @@ -24,7 +24,7 @@ platform: android skill: iterable-android # Snapshot of the corpus/skill version these expectations were written against. -# Bump when PITFALLS.md or the polished corpus changes the expected behavior. +# Bump when PITFALLS.md or the reference corpus changes the expected behavior. expectations_version: 2026-06-16-a scenarios: diff --git a/iterable-android/.context7-library-id b/iterable-android/.context7-library-id deleted file mode 100644 index d98b985..0000000 --- a/iterable-android/.context7-library-id +++ /dev/null @@ -1,14 +0,0 @@ -# Context7 library ID for this skill — single source of truth. -# -# SKILL.md references this file by path so a future org migration -# (franco-zalamena-iterable -> Iterable) is a one-line PR here, not a -# coordinated rewrite across SKILL.md, README.md, and downstream agents. -# -# Format: one library ID per line, comments allowed (lines starting with #). -# The first non-comment, non-empty line is canonical. -# -# Status: PLACEHOLDER until Phase 3 completes — see TODO below. -# After context7.com/add-library returns a library ID, replace the line below -# with the real value and remove this status block. - -TODO-PHASE-3/iterable-sdk-skill diff --git a/iterable-android/SKILL.md b/iterable-android/SKILL.md index fa07882..459188b 100644 --- a/iterable-android/SKILL.md +++ b/iterable-android/SKILL.md @@ -148,9 +148,9 @@ part of the work** — don't silently degrade the integration to keep compiling. — the first `## [x.y.z]` after `## [Unreleased]` is authoritative. If the host project already pins a version (e.g. in a Gradle version - catalog), match it unless the developer asks to upgrade. Known latest as of - this skill revision: **3.8.0** (2026-05) — treat as a floor to sanity-check - the fetch against, not as the answer. + catalog), match it unless the developer asks to upgrade. No latest-version + number is recorded here on purpose — one would go stale between releases and + contradict the rule above. Look it up. - **Minimum Android API:** 21 (Android 5.0). - **Initialize with:** `IterableApi.initializeInBackground(context, apiKey, config, callback)` — this is the **default**; prefer it over the synchronous `initialize(...)` @@ -289,7 +289,7 @@ override fun onCreate() { ``` The full per-feature docs (push, in-app, inbox, embedded, deep links, events, -profiles, UUA) are in the snapshot — see the routing table below. +profiles, UUA) are in `reference/` — see the routing table below. --- @@ -297,24 +297,23 @@ profiles, UUA) are in the snapshot — see the routing table below. This skill keeps only the always-on rules above and `PITFALLS.md` inline. Task-specific guidance (push, in-app, inbox, embedded, JWT, deep links, -event tracking, user profiles, UUA, initialization) lives in a polished -corpus. +event tracking, user profiles, UUA, initialization) lives in `reference/`. > **Say what you're about to read and why — before you read it.** This -> applies to every lookup: a snapshot doc, a version check, a URL you found +> applies to every lookup: a reference doc, a version check, a URL you found > inside a doc. One line each, naming the thing and what you need from it — -> e.g. "Reading `snapshot/setting-up-android-push-notifications.md` for the +> e.g. "Reading `reference/setting-up-android-push-notifications.md` for the > FCM service registration and notification-channel setup." A silent run of > back-to-back fetches leaves the developer watching commands scroll by with > no idea what you're pursuing. One line per lookup, not a status report. -### Source today: the local snapshot (authoritative) +### Read task docs from `reference/` -**Read task docs from [`snapshot/`](snapshot/).** It's a byte-for-byte mirror -of the polished corpus at last release, kept honest by CI (`pnpm -snapshot:verify`). Match the task to a slug (table below) and open -`snapshot/.md`. This is the canonical source right now — don't try -Context7 first. +[`reference/`](reference/) is Iterable's published documentation, transformed +deterministically and shipped with the skill. Match the task to a slug (table +below) and open `reference/.md`. **This is the authoritative source — it +is already on disk, so there is nothing to fetch and no reason to reach for +Context7 or the web for content that lives here.** **One doc per task — don't bulk-load.** Open only the slugs the agreed scope needs, one at a time. The full corpus is ~50k tokens; reading it wholesale @@ -329,23 +328,12 @@ release-by-release upgrade history under `## Upgrading the SDK` — **skip that section** unless the developer is upgrading from a specific older version. For a new integration, `## Installing the SDK` is the part that matters. Where a doc shows the same snippet in both Java and Kotlin, read the one matching the -host project. The `snippets:` block in each doc's YAML frontmatter is CI -validation metadata (hashes, line counts) — it tells you nothing; skip it. +host project. -### Source later: Context7 *(curated library not published yet — do not fetch)* - -The Context7 MCP server **is** connected (the plugin bundles it), but Iterable's -curated library is **not published there yet** — [`.context7-library-id`](.context7-library-id) -is still the placeholder `TODO-PHASE-3/...`. So do **not** call Context7 for -Iterable docs right now: a `resolve-library-id` / `query-docs` lookup would -return some *unrelated public* library, not this skill's vetted corpus. The -[`snapshot/`](snapshot/) is authoritative until the real ID lands. - -Once a real library ID is dropped into that file (first non-comment line; one -that does **not** start with `TODO-`), the flow becomes: read the ID, fetch the -matching slug via the Context7 MCP tool (self-contained — one doc per task, -don't bulk-load), use its snippets verbatim, and surface any `sdk_min_version` -mismatch. +Each doc's YAML frontmatter carries provenance (`source_url`, `source_ref`, +`fetched_at`) and an `sdk_min_version` pin. Cite `source_url` when the developer +asks where guidance came from; surface `sdk_min_version` when it's older than +the version they're on. ### Slug routing @@ -390,11 +378,9 @@ before writing any code is the fastest way to compact mid-task. the upgrade path (Step 0), not the new-integration path. 2. **Check rules 1–5 above** against whatever the user already has. Many "the SDK isn't working" reports are rule violations. -3. **Read the matching slug for the task** before writing code, from whichever - source the "How to use this skill" section marks authoritative (the - `snapshot/` today). Each doc has its own gotchas section that supersedes - generic advice. -4. **For non-obvious traps not covered in the polished doc**, consult +3. **Read the matching slug for the task** from `reference/` before writing + code. Each doc has its own gotchas section that supersedes generic advice. +4. **For non-obvious traps not covered in the reference doc**, consult [`PITFALLS.md`](PITFALLS.md). 5. **Version-check.** If the user is on an older SDK version than the doc's `sdk_min_version`, check the breaking changes before generating code. The @@ -419,10 +405,8 @@ before writing any code is the fastest way to compact mid-task. ## Versioning -This skill is versioned alongside the SDK. Each release of the SDK that -changes public API or agent-relevant behavior triggers a corresponding update -in the polished corpus (`polished/android/`), Context7 re-crawls on its -normal cadence, and `snapshot:refresh` runs as part of the merge to keep -the local fallback aligned. If you see drift between this skill's snippets -and the SDK's current `CHANGELOG.md`, **trust `CHANGELOG.md`** and report -the drift. +This skill is versioned alongside the SDK. When Iterable's docs change, a +refresh PR rewrites `reference/` from the docs at that commit; each doc records +the exact `source_ref` it came from. If you see drift between this skill's +snippets and the SDK's current `CHANGELOG.md`, **trust `CHANGELOG.md`** and +report the drift. diff --git a/iterable-android/snapshot/android-app-links.md b/iterable-android/reference/android-app-links.md similarity index 94% rename from iterable-android/snapshot/android-app-links.md rename to iterable-android/reference/android-app-links.md index 0a9fac1..33825a4 100644 --- a/iterable-android/snapshot/android-app-links.md +++ b/iterable-android/reference/android-app-links.md @@ -11,21 +11,6 @@ source_path: docs/developer-and-api-docs/deep-links/android-app-links/index.md source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d source_sha: 3c933bbc8661bddfeaa8914f3dbb8233d98469c6 fetched_at: 2026-05-25T15:11:45.366Z -polished_at: 2026-08-03T20:42:14.571Z -layer: a -snippets: - - index: 0 - lang: java - hash: "8191331288e0" - line_count: 24 - - index: 1 - lang: java - hash: 7f9532ca4355 - line_count: 7 - - index: 2 - lang: java - hash: 61186d66b7f8 - line_count: 6 summary: Messages sent with Iterable can include Android App Links, which redirect users to your installed mobile app—no browser required. Iterable tracks clicks on these links as expected. diff --git a/iterable-android/snapshot/android-sdk.md b/iterable-android/reference/android-sdk.md similarity index 95% rename from iterable-android/snapshot/android-sdk.md rename to iterable-android/reference/android-sdk.md index 72d889b..b5be146 100644 --- a/iterable-android/snapshot/android-sdk.md +++ b/iterable-android/reference/android-sdk.md @@ -11,129 +11,6 @@ source_path: docs/developer-and-api-docs/iterables-ios-and-android-sdks/android- source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 source_sha: de67a132360a33146dae801ab62c3de1d6846ba9 fetched_at: 2026-08-03T20:41:28.018Z -polished_at: 2026-08-03T20:42:14.561Z -layer: a -snippets: - - index: 0 - lang: groovy - hash: 2497e01c3acd - line_count: 7 - - index: 1 - lang: text - hash: 8cb6f1745a49 - line_count: 1 - - index: 2 - lang: java - hash: c8e337580016 - line_count: 2 - - index: 3 - lang: kotlin - hash: bb240e73f2c3 - line_count: 4 - - index: 4 - lang: kotlin - hash: e982f9fb30e0 - line_count: 4 - - index: 5 - lang: java - hash: 48aba5fe062b - line_count: 4 - - index: 6 - lang: java - hash: 6c37a0b51a76 - line_count: 4 - - index: 7 - lang: java - hash: 88b55d7e366a - line_count: 4 - - index: 8 - lang: java - hash: 6a179b3279eb - line_count: 4 - - index: 9 - lang: java - hash: d2e3e73a8432 - line_count: 5 - - index: 10 - lang: java - hash: 8e54129ce05c - line_count: 24 - - index: 11 - lang: java - hash: 3bc5e2a65010 - line_count: 4 - - index: 12 - lang: java - hash: 836e0bc8e247 - line_count: 9 - - index: 13 - lang: java - hash: 0e273815d107 - line_count: 1 - - index: 14 - lang: java - hash: 16e73bb1286f - line_count: 4 - - index: 15 - lang: java - hash: 698b439a45b3 - line_count: 4 - - index: 16 - lang: java - hash: f362f543301c - line_count: 5 - - index: 17 - lang: java - hash: 11400f57c7ba - line_count: 1 - - index: 18 - lang: java - hash: 0450a5b49d68 - line_count: 12 - - index: 19 - lang: java - hash: e79d0e004450 - line_count: 4 - - index: 20 - lang: java - hash: acfa8af81cce - line_count: 5 - - index: 21 - lang: kotlin - hash: 18b42ba68e56 - line_count: 5 - - index: 22 - lang: java - hash: a16ba32ab078 - line_count: 3 - - index: 23 - lang: java - hash: 6f27d743e5d6 - line_count: 1 - - index: 24 - lang: java - hash: eaacdddcb017 - line_count: 1 - - index: 25 - lang: java - hash: 6b21444c769f - line_count: 5 - - index: 26 - lang: java - hash: 2f3eb2cbfb4e - line_count: 5 - - index: 27 - lang: java - hash: 1ee664bef567 - line_count: 5 - - index: 28 - lang: java - hash: 9edce984f563 - line_count: 4 - - index: 29 - lang: groovy - hash: 744057b87f30 - line_count: 8 summary: This article describes how to install and configure Iterable's [Android SDK](https://github.com/Iterable/iterable-android-sdk). --- diff --git a/iterable-android/snapshot/configure-the-android-sdk.md b/iterable-android/reference/configure-the-android-sdk.md similarity index 97% rename from iterable-android/snapshot/configure-the-android-sdk.md rename to iterable-android/reference/configure-the-android-sdk.md index 2908576..c9382b0 100644 --- a/iterable-android/snapshot/configure-the-android-sdk.md +++ b/iterable-android/reference/configure-the-android-sdk.md @@ -11,25 +11,6 @@ source_path: docs/developer-and-api-docs/unknown-user-activation-dev/configure-t source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d source_sha: fa441fa69f35c816affd3df2d565dbfbd2727ca3 fetched_at: 2026-05-25T15:11:48.790Z -polished_at: 2026-08-03T20:42:14.575Z -layer: a -snippets: - - index: 0 - lang: kotlin - hash: cd5e0463538c - line_count: 6 - - index: 1 - lang: kotlin - hash: dbdbfc1dcc0d - line_count: 49 - - index: 2 - lang: kotlin - hash: c84c6ca4398d - line_count: 17 - - index: 3 - lang: kotlin - hash: d38bf7942b6b - line_count: 4 summary: Follow these instructions to set up Iterable's Android SDK for Unknown User Activation. For general guidance about setting up Iterable's Android SDK, see [Iterable's Android diff --git a/iterable-android/snapshot/customizing-mobile-inbox-on-android.md b/iterable-android/reference/customizing-mobile-inbox-on-android.md similarity index 93% rename from iterable-android/snapshot/customizing-mobile-inbox-on-android.md rename to iterable-android/reference/customizing-mobile-inbox-on-android.md index 62e83de..6056628 100644 --- a/iterable-android/snapshot/customizing-mobile-inbox-on-android.md +++ b/iterable-android/reference/customizing-mobile-inbox-on-android.md @@ -11,85 +11,6 @@ source_path: docs/developer-and-api-docs/in-app-messages/customizing-mobile-inbo source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 source_sha: e156e9f1bf13e4b41507a53dc10093422c8aefac fetched_at: 2026-08-03T20:41:29.489Z -polished_at: 2026-08-03T20:42:14.567Z -layer: a -snippets: - - index: 0 - lang: kotlin - hash: aa9bbb903b24 - line_count: 4 - - index: 1 - lang: kotlin - hash: 3a9eb47ce5a3 - line_count: 4 - - index: 2 - lang: java - hash: 6304913cdac5 - line_count: 5 - - index: 3 - lang: kotlin - hash: 24d64072269d - line_count: 3 - - index: 4 - lang: java - hash: 00a988c5c250 - line_count: 3 - - index: 5 - lang: kotlin - hash: d7b7d23c4c48 - line_count: 4 - - index: 6 - lang: java - hash: d46242e73662 - line_count: 4 - - index: 7 - lang: kotlin - hash: 689b12ae918a - line_count: 4 - - index: 8 - lang: java - hash: 584a986d1076 - line_count: 4 - - index: 9 - lang: kotlin - hash: edcae1e6e4de - line_count: 13 - - index: 10 - lang: java - hash: ef4f78408c7b - line_count: 18 - - index: 11 - lang: kotlin - hash: d121dfcd6053 - line_count: 6 - - index: 12 - lang: kotlin - hash: 6bb02c0de57c - line_count: 10 - - index: 13 - lang: java - hash: 80dd3262733b - line_count: 13 - - index: 14 - lang: kotlin - hash: 3cbc544d3efa - line_count: 8 - - index: 15 - lang: kotlin - hash: 8a0fe4b9b840 - line_count: 10 - - index: 16 - lang: java - hash: 8f7a8b76a82d - line_count: 13 - - index: 17 - lang: kotlin - hash: a28bb9940c6d - line_count: 48 - - index: 18 - lang: java - hash: 1ff85a5943c1 - line_count: 53 summary: A [mobile inbox](https://support.iterable.com/hc/articles/217517406) provides an app-specific place for users to save in-app messages to read later. diff --git a/iterable-android/snapshot/deep-linking-with-partners.md b/iterable-android/reference/deep-linking-with-partners.md similarity index 94% rename from iterable-android/snapshot/deep-linking-with-partners.md rename to iterable-android/reference/deep-linking-with-partners.md index 6129dbe..6681719 100644 --- a/iterable-android/snapshot/deep-linking-with-partners.md +++ b/iterable-android/reference/deep-linking-with-partners.md @@ -11,9 +11,6 @@ source_path: docs/developer-and-api-docs/deep-links/deep-linking-with-partners/i source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d source_sha: 7473a924f2b7eac5a08f7ec66c3fbf60d07089e4 fetched_at: 2026-05-25T15:11:46.035Z -polished_at: 2026-08-03T20:42:14.572Z -layer: a -snippets: [] summary: "Iterable supports deep linking without any third-party integrations—and also with Branch and AppsFlyer. For more information about these integrations, read:" diff --git a/iterable-android/snapshot/embedded-messages-with-iterables-android-sdk.md b/iterable-android/reference/embedded-messages-with-iterables-android-sdk.md similarity index 95% rename from iterable-android/snapshot/embedded-messages-with-iterables-android-sdk.md rename to iterable-android/reference/embedded-messages-with-iterables-android-sdk.md index 4c67603..f752787 100644 --- a/iterable-android/snapshot/embedded-messages-with-iterables-android-sdk.md +++ b/iterable-android/reference/embedded-messages-with-iterables-android-sdk.md @@ -11,89 +11,6 @@ source_path: docs/developer-and-api-docs/embedded-messaging/embedded-messages-wi source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 source_sha: 576150056520b366d5190411e9f28198e70bbeaf fetched_at: 2026-08-03T20:41:31.068Z -polished_at: 2026-08-03T20:42:14.570Z -layer: a -snippets: - - index: 0 - lang: kotlin - hash: d6dd59db2737 - line_count: 18 - - index: 1 - lang: java - hash: e799bb9727c6 - line_count: 4 - - index: 2 - lang: kotlin - hash: f24beeae022b - line_count: 9 - - index: 3 - lang: java - hash: 6c022d00c7c4 - line_count: 4 - - index: 4 - lang: kotlin - hash: ee5898a06175 - line_count: 11 - - index: 5 - lang: kotlin - hash: 2c17add81ecd - line_count: 1 - - index: 6 - lang: kotlin - hash: 146b8a1a8dbf - line_count: 2 - - index: 7 - lang: kotlin - hash: b58673a8e26b - line_count: 11 - - index: 8 - lang: kotlin - hash: e0d3eab032d7 - line_count: 23 - - index: 9 - lang: kotlin - hash: b259e6a45e17 - line_count: 14 - - index: 10 - lang: kotlin - hash: a2932531b4f0 - line_count: 4 - - index: 11 - lang: kotlin - hash: 38e520b049c7 - line_count: 1 - - index: 12 - lang: kotlin - hash: 48c85c7cb874 - line_count: 2 - - index: 13 - lang: xml - hash: d9b27fbb15f5 - line_count: 12 - - index: 14 - lang: kotlin - hash: 3887b4569179 - line_count: 3 - - index: 15 - lang: kotlin - hash: b64c0dd160ee - line_count: 7 - - index: 16 - lang: kotlin - hash: a8b0449c17f6 - line_count: 13 - - index: 17 - lang: kotlin - hash: 97d5fb776fc9 - line_count: 7 - - index: 18 - lang: kotlin - hash: 1048049cb459 - line_count: 8 - - index: 19 - lang: kotlin - hash: 820e05f6b034 - line_count: 7 summary: This article describes the steps you'll need to follow to use Iterable's Android SDK to display embedded messages in your mobile app. --- diff --git a/iterable-android/snapshot/identifying-the-user.md b/iterable-android/reference/identifying-the-user.md similarity index 89% rename from iterable-android/snapshot/identifying-the-user.md rename to iterable-android/reference/identifying-the-user.md index 429f386..d9e6299 100644 --- a/iterable-android/snapshot/identifying-the-user.md +++ b/iterable-android/reference/identifying-the-user.md @@ -11,57 +11,6 @@ source_path: docs/developer-and-api-docs/managing-user-profiles/identifying-the- source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d source_sha: ced31ca29ce63d634a0c4691277a114ed3f0ceb9 fetched_at: 2026-05-25T15:11:46.888Z -polished_at: 2026-08-03T20:42:14.572Z -layer: a -snippets: - - index: 0 - lang: swift - hash: 51664525a08b - line_count: 1 - - index: 1 - lang: objectivec - hash: 464d8c5bbd9b - line_count: 1 - - index: 2 - lang: java - hash: f62eb7b2d632 - line_count: 1 - - index: 3 - lang: swift - hash: 3ce9888afa7f - line_count: 1 - - index: 4 - lang: objectivec - hash: 698cc56d370e - line_count: 1 - - index: 5 - lang: java - hash: 7f2a539b2cba - line_count: 1 - - index: 6 - lang: swift - hash: 13a24526b5c0 - line_count: 3 - - index: 7 - lang: objectivec - hash: de80933755af - line_count: 3 - - index: 8 - lang: java - hash: 6914174d75e4 - line_count: 10 - - index: 9 - lang: swift - hash: 4d2b03419ee5 - line_count: 10 - - index: 10 - lang: objectivec - hash: adb45d47e2cc - line_count: 9 - - index: 11 - lang: java - hash: 1b7317c09349 - line_count: 17 summary: "The Iterable SDK can identify users by email or user ID. To identify a user, you'll need to do two things: specify an email address or user ID, and then call `updateUser` to send that value to Iterable." diff --git a/iterable-android/snapshot/in-app-messages-on-android.md b/iterable-android/reference/in-app-messages-on-android.md similarity index 94% rename from iterable-android/snapshot/in-app-messages-on-android.md rename to iterable-android/reference/in-app-messages-on-android.md index 1e6edba..6bb9970 100644 --- a/iterable-android/snapshot/in-app-messages-on-android.md +++ b/iterable-android/reference/in-app-messages-on-android.md @@ -11,41 +11,6 @@ source_path: docs/developer-and-api-docs/in-app-messages/in-app-messages-on-andr source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 source_sha: 65412ae773eaca59531243ff4775fd4457b5b608 fetched_at: 2026-08-03T20:41:28.539Z -polished_at: 2026-08-03T20:42:14.565Z -layer: a -snippets: - - index: 0 - lang: java - hash: 8db83c22f9a3 - line_count: 18 - - index: 1 - lang: java - hash: 41e8696aa8cc - line_count: 9 - - index: 2 - lang: java - hash: bcfe488ba3d1 - line_count: 10 - - index: 3 - lang: java - hash: a903f600bb27 - line_count: 4 - - index: 4 - lang: kotlin - hash: 28725ee7538d - line_count: 1 - - index: 5 - lang: java - hash: 265c585feb80 - line_count: 1 - - index: 6 - lang: kotlin - hash: 34caae496955 - line_count: 1 - - index: 7 - lang: java - hash: e4f4f768e9bb - line_count: 1 summary: By default, when an in-app message arrives from the server, the SDK automatically shows it if the app is in the foreground. If an in-app message is already showing when the new message arrives, the new message will be shown diff --git a/iterable-android/snapshot/push-notification-overview.md b/iterable-android/reference/push-notification-overview.md similarity index 97% rename from iterable-android/snapshot/push-notification-overview.md rename to iterable-android/reference/push-notification-overview.md index 6397f72..18ed41b 100644 --- a/iterable-android/snapshot/push-notification-overview.md +++ b/iterable-android/reference/push-notification-overview.md @@ -11,9 +11,6 @@ source_path: docs/developer-and-api-docs/push-notifications/push-notification-ov source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d source_sha: 3306f88835e0c1b30e4b4020287d772cfd93ba1e fetched_at: 2026-05-25T15:11:44.170Z -polished_at: 2026-08-03T20:42:14.569Z -layer: a -snippets: [] summary: To alert users about updates, offers, content, and other information that may be immediately relevant, it often makes sense to contact them on their mobile devices. Iterable can send push notification campaigns to your diff --git a/iterable-android/snapshot/setting-up-android-push-notifications.md b/iterable-android/reference/setting-up-android-push-notifications.md similarity index 96% rename from iterable-android/snapshot/setting-up-android-push-notifications.md rename to iterable-android/reference/setting-up-android-push-notifications.md index 41f48f6..faac18f 100644 --- a/iterable-android/snapshot/setting-up-android-push-notifications.md +++ b/iterable-android/reference/setting-up-android-push-notifications.md @@ -11,45 +11,6 @@ source_path: docs/developer-and-api-docs/push-notifications/setting-up-android-p source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d source_sha: 45fa32087746810e08046766184fd4c2eb1acc94 fetched_at: 2026-05-25T15:11:43.429Z -polished_at: 2026-08-03T20:42:14.568Z -layer: a -snippets: - - index: 0 - lang: java - hash: 0450a5b49d68 - line_count: 12 - - index: 1 - lang: xml - hash: 9933acdf8d98 - line_count: 1 - - index: 2 - lang: xml - hash: 5fc786ff2c94 - line_count: 1 - - index: 3 - lang: xml - hash: 153ef25ae0f8 - line_count: 1 - - index: 4 - lang: xml - hash: 7f16912083a3 - line_count: 1 - - index: 5 - lang: xml - hash: 87c8a09ec24e - line_count: 1 - - index: 6 - lang: xml - hash: 6ea88888f62c - line_count: 1 - - index: 7 - lang: xml - hash: 5fc786ff2c94 - line_count: 1 - - index: 8 - lang: xml - hash: 77a45aeee481 - line_count: 1 summary: This guide describes the technical setup necessary to use Iterable to send push notifications to Android devices. --- diff --git a/iterable-android/snapshot/setting-up-mobile-inbox-on-android.md b/iterable-android/reference/setting-up-mobile-inbox-on-android.md similarity index 98% rename from iterable-android/snapshot/setting-up-mobile-inbox-on-android.md rename to iterable-android/reference/setting-up-mobile-inbox-on-android.md index 36a6e21..bff0b64 100644 --- a/iterable-android/snapshot/setting-up-mobile-inbox-on-android.md +++ b/iterable-android/reference/setting-up-mobile-inbox-on-android.md @@ -11,9 +11,6 @@ source_path: docs/developer-and-api-docs/in-app-messages/setting-up-mobile-inbox source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 source_sha: 9b54bece973b76efd0e1eaec0494b0e9d2c2af7c fetched_at: 2026-08-03T20:41:29.000Z -polished_at: 2026-08-03T20:42:14.566Z -layer: a -snippets: [] summary: Apps using version 3.2.0 and later of Iterable's [Android SDK](https://support.iterable.com/hc/articles/360035019712) can save in-app messages to an inbox. This inbox displays a list of saved in-app messages and diff --git a/iterable-android/snapshot/setting-up-unknown-user-activation.md b/iterable-android/reference/setting-up-unknown-user-activation.md similarity index 99% rename from iterable-android/snapshot/setting-up-unknown-user-activation.md rename to iterable-android/reference/setting-up-unknown-user-activation.md index 3aa5d52..a14b075 100644 --- a/iterable-android/snapshot/setting-up-unknown-user-activation.md +++ b/iterable-android/reference/setting-up-unknown-user-activation.md @@ -11,9 +11,6 @@ source_path: docs/developer-and-api-docs/unknown-user-activation-dev/setting-up- source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d source_sha: 45d0ae07bce89a4a4156c3b5e46f6bd4136d41b2 fetched_at: 2026-05-25T15:11:49.365Z -polished_at: 2026-08-03T20:42:14.575Z -layer: a -snippets: [] summary: Unknown User Activation makes it possible to learn about, message, and develop relationships with unidentified users of your mobile app and website. Before you begin setting it up, learn more about how it works in [Unknown User diff --git a/iterable-android/snapshot/tracking-events-with-iterables-mobile-sdks.md b/iterable-android/reference/tracking-events-with-iterables-mobile-sdks.md similarity index 95% rename from iterable-android/snapshot/tracking-events-with-iterables-mobile-sdks.md rename to iterable-android/reference/tracking-events-with-iterables-mobile-sdks.md index b55e60c..bf6de3a 100644 --- a/iterable-android/snapshot/tracking-events-with-iterables-mobile-sdks.md +++ b/iterable-android/reference/tracking-events-with-iterables-mobile-sdks.md @@ -11,37 +11,6 @@ source_path: docs/developer-and-api-docs/event-tracking/tracking-events-with-ite source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d source_sha: 0dbb170bfbd574bf405b33990d2c288ad8dcd153 fetched_at: 2026-05-25T15:11:48.241Z -polished_at: 2026-08-03T20:42:14.574Z -layer: a -snippets: - - index: 0 - lang: swift - hash: aba2ff83f3b2 - line_count: 4 - - index: 1 - lang: objectivec - hash: 402d8334d532 - line_count: 1 - - index: 2 - lang: java - hash: 97bbfc4400f7 - line_count: 4 - - index: 3 - lang: javascript - hash: 98414a37c7ae - line_count: 7 - - index: 4 - lang: swift - hash: f8155acfa770 - line_count: 29 - - index: 5 - lang: objectivec - hash: fb2e320df19b - line_count: 27 - - index: 6 - lang: java - hash: e3a8fdcd2031 - line_count: 33 summary: Iterable's mobile SDKs can track _events_, which correspond to actions taken by your app's users. Events can be related to messages you've sent (for example, a user opening an in-app message) or to a particular feature or piece diff --git a/sources/android/updating-user-profiles.md b/iterable-android/reference/updating-user-profiles.md similarity index 94% rename from sources/android/updating-user-profiles.md rename to iterable-android/reference/updating-user-profiles.md index 30ab585..0fa8f74 100644 --- a/sources/android/updating-user-profiles.md +++ b/iterable-android/reference/updating-user-profiles.md @@ -1,12 +1,20 @@ --- -url: https://support.iterable.com/hc/articles/360035402611 +slug: updating-user-profiles +feature: user-profiles +archetype: identity +sdk_min_version: 3.7.0 +sdk_artifact: iterableapi title: Updating User Profiles -useInNovaDocs: true +source_url: https://support.iterable.com/hc/articles/360035402611 source_repo: Iterable/iterable-docs source_path: docs/developer-and-api-docs/managing-user-profiles/updating-user-profiles/index.md source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d source_sha: 3cca4ebdd1ee9da638428e3ded39de472202fd94 fetched_at: 2026-05-25T15:11:47.716Z +summary: "A user's Iterable profile contains descriptive information about them: + demographic info, preferences, etc. You can use this data to create dynamic + lists and customize the messages you send (by referencing user profile data + with [Handlebars](https://support.iterable.com/hc/articles/35601631606036))." --- # Updating User Profiles @@ -15,10 +23,6 @@ demographic info, preferences, etc. You can use this data to create dynamic lists and customize the messages you send (by referencing user profile data with [Handlebars](https://support.iterable.com/hc/articles/35601631606036)). -## In this article - -[[toc]] - ## Limitations User profiles have a soft limit of 1,000 fields. If you think you'll need more @@ -120,7 +124,6 @@ IterableApi.getInstance().updateUser(datafields); Now, in your messages, you can reference the user's `City` (or any other field) with [Handlebars](https://support.iterable.com/hc/articles/35601631606036), like this: - ```handlebars {{Address.City}} ``` diff --git a/iterable-android/snapshot/updating-user-profiles.md b/iterable-android/snapshot/updating-user-profiles.md deleted file mode 100644 index cfa4755..0000000 --- a/iterable-android/snapshot/updating-user-profiles.md +++ /dev/null @@ -1,341 +0,0 @@ ---- -slug: updating-user-profiles -feature: user-profiles -archetype: identity -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Updating User Profiles -source_url: https://support.iterable.com/hc/articles/360035402611 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/managing-user-profiles/updating-user-profiles/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 3cca4ebdd1ee9da638428e3ded39de472202fd94 -fetched_at: 2026-05-25T15:11:47.716Z -polished_at: 2026-08-03T20:42:14.573Z -layer: a -snippets: - - index: 0 - lang: swift - hash: 98edcca734b9 - line_count: 28 - - index: 1 - lang: objectivec - hash: f643548616dc - line_count: 30 - - index: 2 - lang: java - hash: af02941d202c - line_count: 16 - - index: 3 - lang: handlebars - hash: 36130f23171c - line_count: 1 - - index: 4 - lang: json - hash: ea4da7f4ae5d - line_count: 4 - - index: 5 - lang: json - hash: 73fe86262683 - line_count: 4 - - index: 6 - lang: json - hash: 73fe86262683 - line_count: 4 - - index: 7 - lang: json - hash: 3e53f4645bf8 - line_count: 6 - - index: 8 - lang: swift - hash: 0eb671afc60f - line_count: 20 - - index: 9 - lang: objectivec - hash: 874ed71683e9 - line_count: 25 - - index: 10 - lang: java - hash: a2365a08fdc4 - line_count: 17 -summary: "A user's Iterable profile contains descriptive information about them: - demographic info, preferences, etc. You can use this data to create dynamic - lists and customize the messages you send (by referencing user profile data - with [Handlebars](https://support.iterable.com/hc/articles/35601631606036))." ---- -# Updating User Profiles - -A user's Iterable profile contains descriptive information about them: -demographic info, preferences, etc. You can use this data to create dynamic -lists and customize the messages you send (by referencing user profile data with -[Handlebars](https://support.iterable.com/hc/articles/35601631606036)). - -## Limitations - -User profiles have a soft limit of 1,000 fields. If you think you'll need more -fields, talk to your Iterable Customer Success Manager. - -## How to make the updateUser call - -Here's some sample code that makes an `updateUser` call: - -_Swift_ - -```swift -// The IterableAPI.updateUser(...) can be called anywhere the SDK is accessible -// myFunc() demonstrates this usage -import IterableSDK - -func myFunc() { - let dataField: [String: Any] = [ - "Address": [ - "Street1": "123 Main St", - "Street2": "Apt 1", - "City": "Iter-a-ville", - "State": "CA", - "Zip": "90210" - ] - ] - - IterableAPI.updateUser(dataField, - mergeNestedObjects: false, - onSuccess: myUserUpdateSuccessHandler, - onFailure: myUserUpdateFailureHandler) -} - -func myUserUpdateSuccessHandler(data: [AnyHashable: Any]?) -> () { - print("Successfully sent user update request to Iterable") -} - -func myUserUpdateFailureHandler(reason: String?, data: Data?) -> () { - print("Failure sending user update request to Iterable") -} -``` - -_Objective-C_ - -```objectivec -// The [IterableAPI updateUser:...] can be called anywhere the SDK is accessible -// myFunc demonstrates this usage -@import IterableSDK; - -typedef void (^successHandler)(NSDictionary * _Nullable); -typedef void (^failureHandler)(NSString * _Nullable, NSData * _Nullable); - -- (void)myFunc { - NSDictionary *data = @{ - @"Address": @{ @"Street1": @"123 Main St", - @"Street2": @"Apt 1", - @"City": @"Iter-a-ville", - @"State": @"CA", - @"Zip": @"90210" - } - }; - - [IterableAPI updateUser:data - mergeNestedObjects:NO - onSuccess:myUserUpdateSuccessHandler - onFailure:myUserUpdateFailureHandler]; -} - -successHandler myUserUpdateSuccessHandler = ^(NSDictionary * _Nullable data) { - NSLog(@"Successfully sent user update request to Iterable"); -}; - -failureHandler myUserUpdateFailureHandler = ^(NSString * _Nullable reason, NSData * _Nullable data) { - NSLog(@"Failure sending user update request to Iterable"); -}; -``` - -_Java_ - -```java -JSONObject address = new JSONObject(); -JSONObject datafields = new JSONObject(); - -try { - address.put("Street1", "123 Main St"); - address.put("Street2", "Apt 1"); - address.put("City", "Iter-a-ville"); - address.put("State", "CA"); - address.put("Zip", "90210"); - - datafields.put("dataFields", address); -} catch (JSONException e) { - e.printStackTrace(); -} - -IterableApi.getInstance().updateUser(datafields); -``` - -Now, in your messages, you can reference the user's `City` (or any other field) -with [Handlebars](https://support.iterable.com/hc/articles/35601631606036), like this: - -```handlebars -{{Address.City}} -``` - -### How `mergeNestedObjects` works - -The `mergeNestedObjects` parameter determines whether Iterable should merge -fields included in an `updateUser` request with analogous objects on the user's -profile, or overwrite that data. - -`mergeNestedObjects` only works for **one level of nesting** within objects. It -does **not** work recursively for deeper nested objects, and it does not merge -arrays. - -For objects nested more than one level deep, `mergeNestedObjects` will -**overwrite** the entire nested structure, not merge it. You must include all -existing data along with your updates to preserve deeper nested values. - -`mergeNestedObjects` defaults to `false`. - -For example, consider a user profile that includes the following address object: - -```json -"address": { - "street": "123 Main St", - "city": "San Francisco" -} -``` - -Then, assume that an `updateUser` call includes a similar object: - -```json -"address": { - "state": "CA", - "zipCode": "94105" -} -``` - -If `updateUser` sets `mergeNestedObjects` to `false` (the default value), the -resulting user profile value is: - -```json -"address": { - "state": "CA", - "zipCode": "94105" -} -``` - -However, if `updateUser` sets `mergeNestedObjects` to `true`, the resulting -user profile value is: - -```json -"address": { - "street": "123 Main St", - "city": "San Francisco", - "state": "CA", - "zipCode": "94105" -} -``` - -## When to make the updateUser call - -Call `updateUser` when a user has: - -- Updated their personal information. -- Completed a key step in an onboarding or sales process. For example, you might - want to set `completedOnboarding` to `true`, or assign a value to a field that's - useful for segmentation (setting `testGroup` to `testGroupA` or something - similar). -- Completed a key step in your onboarding, sales or retargeting process. For - example, you may want to add `"completedOnboarding": true` or `"testGroup": "A"` - to the user profile for later segmentation, splitting the users down different - journeys or analyzing later for test comparisons. - -## Tracking anonymous users - -To track anonymous users in Iterable, provide a `userId`. If your project uses -`email` as the only unique identifier, then this causes Iterable to generate a -placeholder email for the user—read -[Handling Anonymous Users](https://support.iterable.com/hc/articles/208499956) -for more info). - -## Taking a user from anonymous to known - -Iterable can convert anonymous users to known users. The example function below -updates the current user's `email` and `userId`. You will likely want to use -this code when your user signs in or self-identifies when signing up. - -_Swift_ - -```swift -let email = "newEmail@example.com" - -// The IterableAPI.updateUser(...) can be added to any method within your code. `yourUserIsNowKnownFunction` is just an example -func yourUserIsNowKnownFunction() { - IterableAPI.updateEmail(email, - onSuccess: myUserUpdateSuccessHandler, - onFailure: myUserUpdateFailureHandler) -} - -func myUserUpdateSuccessHandler(data: [AnyHashable: Any]?) -> () { - print("Successfully sent user update request to Iterable") -} - -func myUserUpdateFailureHandler(reason: String?, data: Data?) -> () { - print("Failure sending user update request to Iterable") - - IterableAPI.email = email - - IterableAPI.updateUser(dataField, mergeNestedObjects: false) -} -``` - -_Objective-C_ - -```objectivec -@import IterableSDK; - -typedef void (^successHandler)(NSDictionary * _Nullable); -typedef void (^failureHandler)(NSString * _Nullable, NSData * _Nullable); - -NSString *email = @"newEmail@example.com"; - -// The [IterableAPI updateUser:...] can be added to any method within your code. `yourUserIsNowKnownFunction` is just an example -- (void)yourUserIsNowKnownFunction { - [IterableAPI updateEmail:email - onSuccess:myUserUpdateSuccessHandler - onFailure:myUserUpdateFailureHandler]; -} - -successHandler myUserUpdateSuccessHandler = ^(NSDictionary * _Nullable data) { - NSLog(@"Successfully sent user update request to Iterable"); -}; - -failureHandler myUserUpdateFailureHandler = ^(NSString * _Nullable reason, NSData * _Nullable data) { - NSLog(@"Failure sending user update request to Iterable"); - - IterableAPI.email = email; - - [IterableAPI updateUser:dataField mergeNestedObjects:false]; -}; -``` - -_Java_ - -```java -final String email = "newEmail@example.com"; - -IterableApi.getInstance().updateEmail(email, new IterableHelper.SuccessHandler() { - @Override - public void onSuccess(JSONObject data) { - System.out.println("sent to Iterable success"); - - } -}, new IterableHelper.FailureHandler() { - @Override - public void onFailure(String reason, JSONObject data) { - System.out.println("sent to Iterable failure"); - IterableApi.getInstance().setEmail(email); - //This assumes your saving your user profile fields in the datafield object locally - IterableApi.getInstance().updateUser(datafields); - } -}); -``` - -There is a chance the `updateEmail` call will fail. The most likely reason is -that the user already exists so we should now update the user call directly in -the `onFailure` handler. diff --git a/pipeline/config/android.yml b/pipeline/config/android.yml index 3d75c6f..d7dc301 100644 --- a/pipeline/config/android.yml +++ b/pipeline/config/android.yml @@ -1,13 +1,9 @@ -# Iterable Android SDK — pipeline config. +# Iterable Android SDK — docs refresh config. # -# Stages 1–3 of the pipeline read this single file: -# - Stage 1 (fetch) : pulls each `articles[].source_path` from `source.repo` -# at `source.ref` into `paths.sources_dir`. -# - Stage 2 (polish) : reads `paths.sources_dir`, picks a prompt based on -# `articles[].archetype`, attaches version pins from -# `sdk.changelog_path`, writes `paths.polished_dir`. -# - Stage 3 (publish): diffs `paths.polished_dir` against `paths.skill_dir` -# for v1; ships to Context7 once `publish.enabled`. +# `pnpm refresh:docs` reads this file: it pulls each `articles[].source_path` +# from `source.repo` at `source.ref`, applies the deterministic transforms, and +# writes `paths.reference_dir`/.md — the corpus the skill reads. One +# stage, no intermediate directories, no LLM. # # Bump `source.ref` deliberately when re-fetching so diffs against the # previous run reflect upstream changes only. @@ -25,21 +21,7 @@ sdk: changelog_path: CHANGELOG.md paths: - sources_dir: sources/android - polished_dir: polished/android - skill_dir: iterable-android - -publish: - enabled: false - context7: - library_name: iterable/android-sdk - version_from: sdk.tag - -# NOTE: v1 has no LLM rewrite stage. The corpus is the deterministic transform -# output only. Editorial steps that once lived in an -# "Layer B" LLM pass — prose voice-shift, Java/Kotlin de-dup, cross-platform code -# stripping — are intentionally not run; any of them could return later as a -# deterministic transform without reintroducing an LLM. + reference_dir: iterable-android/reference articles: - slug: android-sdk diff --git a/pipeline/package.json b/pipeline/package.json index 182d98c..61252d2 100644 --- a/pipeline/package.json +++ b/pipeline/package.json @@ -7,22 +7,16 @@ "node": ">=20" }, "scripts": { - "fetch:sources": "tsx src/fetch.ts", + "refresh:docs": "tsx src/refresh.ts", "set:source-ref": "tsx src/set-source-ref.ts", - "polish:a": "tsx src/polish-layer-a.ts", - "validate:polished": "tsx src/validate-polished.ts", + "validate:reference": "tsx src/validate-reference.ts", "validate:plugins": "tsx src/validate-plugins.ts", - "check:snippets": "tsx src/check-snippets.ts", - "lint:chunking": "tsx src/lint-chunking.ts", - "recompute:manifest": "tsx src/recompute-manifest.ts", "enrich:summary": "tsx src/enrich-summary.ts", - "snapshot:refresh": "tsx src/snapshot.ts refresh", - "snapshot:verify": "tsx src/snapshot.ts verify", "eval:prompts": "tsx src/eval-run.ts prompts", "eval:run": "tsx src/eval-run.ts run", "eval:report": "tsx src/eval-report.ts", "typecheck": "tsc --noEmit", - "check:all": "pnpm typecheck && pnpm validate:polished && pnpm validate:plugins && pnpm snapshot:verify && pnpm check:snippets && pnpm lint:chunking" + "check:all": "pnpm typecheck && pnpm validate:reference && pnpm validate:plugins" }, "devDependencies": { "@types/node": "^22.7.0", diff --git a/pipeline/schema/polished.schema.json b/pipeline/schema/reference.schema.json similarity index 54% rename from pipeline/schema/polished.schema.json rename to pipeline/schema/reference.schema.json index 9531d2d..40c7266 100644 --- a/pipeline/schema/polished.schema.json +++ b/pipeline/schema/reference.schema.json @@ -1,8 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://iterable.dev/schemas/polished.schema.json", - "title": "Polished article frontmatter", - "description": "Frontmatter contract for files under `polished//.polished.md`. Layer A files (`.layer-a.md`) follow the same shape with `layer: a`.", + "$id": "https://iterable.dev/schemas/reference.schema.json", + "title": "Reference article frontmatter", + "description": "Frontmatter contract for the docs corpus under `iterable-android/reference/.md`, written by `pnpm refresh:docs`.", "type": "object", "additionalProperties": false, "required": [ @@ -18,10 +18,7 @@ "source_path", "source_ref", "source_sha", - "fetched_at", - "polished_at", - "layer", - "snippets" + "fetched_at" ], "properties": { "slug": { @@ -43,11 +40,15 @@ "user-profiles", "unknown-user-activation" ], - "description": "Routing tag — matches a feature bucket the agent can ask about." + "description": "Routing tag \u2014 matches a feature bucket the agent can ask about." }, "archetype": { "type": "string", - "enum": ["integration", "feature", "identity"], + "enum": [ + "integration", + "feature", + "identity" + ], "description": "Article archetype tag (integration / feature / identity), carried through from pipeline config." }, "sdk_min_version": { @@ -67,7 +68,7 @@ "type": "string", "minLength": 40, "maxLength": 600, - "description": "Agent-facing 2–3 sentence summary, extracted from the first prose paragraph(s) by `pnpm enrich:summary`. Required." + "description": "Agent-facing 2-3 sentence summary, extracted from the first prose paragraph(s). Required." }, "source_url": { "type": "string", @@ -87,58 +88,21 @@ "source_ref": { "type": "string", "pattern": "^[0-9a-f]{40}$", - "description": "Pinned commit SHA on the upstream docs repo." + "description": "Docs-repo commit this file was last fetched at. Files skipped as unchanged keep their earlier ref, so the corpus can hold a mix; pipeline/config `source.ref` is the latest refresh point." }, "source_sha": { "type": "string", "pattern": "^[0-9a-f]{40}$", - "description": "Git blob SHA of the source file at `source_ref`." + "description": "Git blob SHA of the source file. Compared on refresh to decide whether to rewrite." }, "fetched_at": { "type": "string", - "format": "date-time" - }, - "polished_at": { - "type": "string", - "format": "date-time" - }, - "layer": { - "type": "string", - "enum": ["a", "b"] + "format": "date-time", + "description": "When this file was last rewritten. Unchanged files keep their previous value, so a no-op refresh produces no diff." }, "android_excerpt": { "type": "boolean", - "description": "Optional. Marks a doc sourced from a cross-platform article that still contains foreign-platform (iOS/web) examples. Informational in v1 (the deterministic transform does not strip foreign code)." - }, - "snippets": { - "type": "array", - "description": "Per-fence manifest. Each entry corresponds to a surviving code block in the body, in document order.", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["index", "lang", "hash", "line_count"], - "properties": { - "index": { - "type": "integer", - "minimum": 0, - "description": "Position in document order. Must start at 0 and be contiguous." - }, - "lang": { - "type": "string", - "minLength": 1, - "description": "Markdown code-fence language tag." - }, - "hash": { - "type": "string", - "pattern": "^[0-9a-f]{12}$", - "description": "12-char hex content hash of the code block, used to detect manifest/body drift." - }, - "line_count": { - "type": "integer", - "minimum": 0 - } - } - } + "description": "Optional. Marks a doc sourced from a cross-platform article that still contains foreign-platform (iOS/web) examples. Informational - the transform does not strip foreign code." } } } diff --git a/pipeline/src/check-snippets.ts b/pipeline/src/check-snippets.ts deleted file mode 100644 index 5998ed6..0000000 --- a/pipeline/src/check-snippets.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** - * Advisory Kotlin snippet check. - * - * Extracts every fenced ``` kotlin block from `*.polished.md`, writes each - * to a temp `.kts` file, and runs `kotlinc -script -nowarn`. Reports any - * errors as advisory output. Always exits 0; failures do not block CI. - * - * Why advisory only (v1): - * `kotlinc -script` runs full semantic analysis (name resolution, type - * checking) rather than parse-only. Without a classpath, every reference - * to `IterableApi`, Android framework symbols, AndroidX, or even local - * variables defined outside the snippet fragment fails as "unresolved - * reference". The signal-to-noise ratio at that point is too low to make - * this a blocking gate. - * - * v2 of this script (tracked follow-up) will load the Iterable SDK aar - * (`iterableapi-3.7.0.aar`), `android.jar` from the API 21 platform, and - * the AndroidX subset onto the classpath so name resolution works. At - * that point the gate becomes blocking. - * - * What this still catches (advisory): - * - Pure parse errors (expecting brace, malformed string, etc.) — they - * show up alongside unresolved-reference errors in the output, so a - * reviewer skimming the lint can still spot them. - * - * Behaviour: - * - `kotlinc` on PATH → runs the check, reports errors, exits 0. - * - `kotlinc` missing → warn, exits 0. CI installs Kotlin - * 1.9.24 so the advisory output is always produced there. - * - * Usage: - * pnpm check:snippets - */ - -import { execFileSync, spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = resolve(HERE, "../.."); -const POLISHED_ROOT = resolve(REPO_ROOT, "polished"); - -interface KotlinSnippet { - file: string; - index: number; - body: string; -} - -function relativeToRoot(absPath: string): string { - return absPath.startsWith(REPO_ROOT + "/") - ? absPath.slice(REPO_ROOT.length + 1) - : absPath; -} - -function hasKotlinc(): boolean { - const probe = spawnSync("kotlinc", ["-version"], { stdio: "ignore" }); - return probe.status === 0; -} - -function* walkPolished(): Generator { - for (const platform of readdirSync(POLISHED_ROOT)) { - const dir = resolve(POLISHED_ROOT, platform); - if (!statSync(dir).isDirectory()) continue; - for (const f of readdirSync(dir)) { - if (f.endsWith(".polished.md")) yield resolve(dir, f); - } - } -} - -function extractKotlinSnippets(path: string): KotlinSnippet[] { - const text = readFileSync(path, "utf8"); - const lines = text.split(/\r?\n/); - const openRe = /^ {0,3}```([a-zA-Z0-9_+-]*)\s*$/; - const closeRe = /^ {0,3}```\s*$/; - const out: KotlinSnippet[] = []; - let inFence = false; - let fenceLang = ""; - let buffer: string[] = []; - let snippetIndex = 0; - for (const line of lines) { - const open = openRe.exec(line); - if (!inFence && open) { - inFence = true; - fenceLang = (open[1] ?? "").toLowerCase(); - buffer = []; - continue; - } - if (inFence && closeRe.test(line)) { - if (fenceLang === "kotlin") { - out.push({ file: path, index: snippetIndex, body: buffer.join("\n") }); - } - snippetIndex++; - inFence = false; - fenceLang = ""; - buffer = []; - continue; - } - if (inFence) buffer.push(line); - } - return out; -} - -function runKotlinc(snippet: KotlinSnippet, workDir: string): { ok: boolean; stderr: string } { - const scratchPath = join(workDir, `snippet-${snippet.index}.kts`); - writeFileSync(scratchPath, snippet.body, "utf8"); - const result = spawnSync( - "kotlinc", - ["-script", "-nowarn", scratchPath], - { encoding: "utf8", timeout: 30_000 }, - ); - if (result.status === 0) return { ok: true, stderr: "" }; - const stderr = `${result.stderr ?? ""}${result.stdout ?? ""}`.trim(); - return { ok: false, stderr }; -} - -function main() { - const snippetsByFile = new Map(); - for (const file of walkPolished()) { - const snippets = extractKotlinSnippets(file); - if (snippets.length > 0) snippetsByFile.set(file, snippets); - } - - const totalSnippets = [...snippetsByFile.values()].reduce( - (acc, list) => acc + list.length, - 0, - ); - - if (totalSnippets === 0) { - console.log("OK no kotlin snippets to check"); - return; - } - - if (!hasKotlinc()) { - console.warn( - `WARN kotlinc not on PATH; skipping ${totalSnippets} kotlin snippet check(s).`, - ); - console.warn( - ` Install Kotlin (https://kotlinlang.org/docs/command-line.html) to enable locally.`, - ); - console.warn(` The check is advisory in v1 (no classpath, so most errors`); - console.warn(` will be 'unresolved reference' noise); v2 adds the SDK aar.`); - return; - } - - const versionLine = (() => { - try { - return execFileSync("kotlinc", ["-version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); - } catch { - return "kotlinc"; - } - })(); - console.log(`Using ${versionLine}\n`); - - const workDir = mkdtempSync(join(tmpdir(), "iterable-snippets-")); - let failures = 0; - const failureBlocks: string[] = []; - try { - for (const [file, snippets] of snippetsByFile) { - const rel = relativeToRoot(file); - for (const snippet of snippets) { - const { ok, stderr } = runKotlinc(snippet, workDir); - if (ok) { - process.stdout.write("."); - continue; - } - failures++; - process.stdout.write("F"); - failureBlocks.push( - `${rel} snippet #${snippet.index}\n${stderr.split("\n").map((l) => ` ${l}`).join("\n")}`, - ); - } - } - process.stdout.write("\n\n"); - } finally { - rmSync(workDir, { recursive: true, force: true }); - } - - if (failures === 0) { - console.log(`OK ${totalSnippets} kotlin snippet(s) parsed cleanly`); - return; - } - console.warn(`WARN ${failures} of ${totalSnippets} kotlin snippet(s) reported errors (advisory only):`); - for (const block of failureBlocks) { - console.warn(""); - console.warn(block); - } - console.warn(""); - console.warn("Most errors here will be 'unresolved reference' for SDK / framework symbols."); - console.warn("That is expected with no classpath; v2 of this script (tracked follow-up)"); - console.warn("will load the Iterable SDK + android.jar + AndroidX so this becomes strict."); -} - -main(); diff --git a/pipeline/src/enrich-summary.ts b/pipeline/src/enrich-summary.ts index d17d000..01d26c4 100644 --- a/pipeline/src/enrich-summary.ts +++ b/pipeline/src/enrich-summary.ts @@ -1,11 +1,11 @@ /** - * Populates the `summary` frontmatter field on polished/layer-a markdown - * files using `extractSummary`. Idempotent: skips files whose existing - * summary already matches what the extractor would produce. + * Populates the `summary` frontmatter field on reference markdown files using + * `extractSummary`. Idempotent: skips files whose existing summary already + * matches what the extractor would produce. * * Usage: * pnpm enrich:summary - * pnpm enrich:summary ../polished/android/*.md + * pnpm enrich:summary ../iterable-android/reference/*.md * * Pass `--overwrite` to replace hand-edited summaries with the extractor's * output. By default, files with an existing summary are left alone. diff --git a/pipeline/src/lib/layer-a.ts b/pipeline/src/lib/layer-a.ts index 79d0a7e..18b844d 100644 --- a/pipeline/src/lib/layer-a.ts +++ b/pipeline/src/lib/layer-a.ts @@ -1,15 +1,10 @@ /** - * Stage 2 — Polish, Layer A. - * - * Deterministic markdown transforms applied to a `sources//.md` - * file before the LLM polish step. Each transform is small, reversible, and - * easy to unit-test. Editorial decisions (drop Java when Kotlin sibling - * exists, etc.) are deferred to Layer B. - * - * The output is "Layer A intermediate" markdown — not yet the final polish. + * Deterministic markdown transforms applied to a doc fetched from + * Iterable/iterable-docs before it lands in `iterable-android/reference/`. + * Each transform is small, reversible, and easy to unit-test. There is no LLM + * stage — the reference corpus is the docs reshaped, nothing more. */ -import { createHash } from "node:crypto"; import { splitFrontmatter, joinFrontmatter, type Frontmatter } from "./frontmatter.ts"; import { extractSummary } from "./summary.ts"; @@ -28,25 +23,14 @@ export interface LayerAContext { sdkArtifact: string; } -export interface SnippetEntry { - index: number; - lang: string; - hash: string; - line_count: number; -} - export interface LayerAResult { output: string; - snippets: SnippetEntry[]; + snippetCount: number; } export function applyLayerA(raw: string, ctx: LayerAContext): LayerAResult { const { frontmatter, body } = splitFrontmatter(raw); - - // Manifest is computed against the source body so it captures every - // original snippet — including fences inside containers — regardless of - // how the structural transforms below reshape them. - const snippets = collectSnippets(body); + const snippetCount = countSnippets(body); let working = body; working = stripTocMarker(working); @@ -61,16 +45,18 @@ export function applyLayerA(raw: string, ctx: LayerAContext): LayerAResult { // callouts converted to GH style). const summary = extractSummary(working); - const newFrontmatter = rewriteFrontmatter(frontmatter, ctx, snippets, summary); - return { output: joinFrontmatter(newFrontmatter, working), snippets }; + const newFrontmatter = rewriteFrontmatter(frontmatter, ctx, summary); + return { output: joinFrontmatter(newFrontmatter, working), snippetCount }; } // ── Frontmatter rewrite ───────────────────────────────────────────── +// No generated-at timestamp here on purpose: it would differ on every run, so +// a re-fetch of unchanged docs would look like a change to `git diff` and the +// refresh workflow would open an empty PR every time it fires. function rewriteFrontmatter( fm: Frontmatter, ctx: LayerAContext, - snippets: SnippetEntry[], summary: string | undefined, ): Frontmatter { const out: Frontmatter = { @@ -86,9 +72,6 @@ function rewriteFrontmatter( source_ref: fm.source_ref, source_sha: fm.source_sha, fetched_at: fm.fetched_at, - polished_at: new Date().toISOString(), - layer: "a", - snippets, }; if (summary !== undefined) out.summary = summary; return out; @@ -218,41 +201,17 @@ function collapseBlankLines(text: string): string { * Note: fences indented by 4+ spaces are not fences per CommonMark — they * are part of an indented code block — and are intentionally not matched. */ -export function collectSnippets(text: string): SnippetEntry[] { - const out: SnippetEntry[] = []; - const lines = text.split("\n"); - const openRe = /^ {0,3}```([a-zA-Z0-9_+-]*)\s*$/; - const closeRe = /^ {0,3}```\s*$/; +/** Counts fenced code blocks — reported by the CLI so a refresh shows at a + * glance whether a doc's code changed shape. Not persisted anywhere. */ +export function countSnippets(text: string): number { + const fenceRe = /^ {0,3}```/; + let count = 0; let inFence = false; - let fenceLang = ""; - let fenceLines: string[] = []; - let index = 0; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i] ?? ""; - const open = openRe.exec(line); - if (!inFence && open) { - inFence = true; - fenceLang = open[1] ?? ""; - fenceLines = []; - continue; + for (const line of text.split("\n")) { + if (fenceRe.test(line)) { + if (!inFence) count++; + inFence = !inFence; } - if (inFence && closeRe.test(line)) { - const content = fenceLines.join("\n"); - const hash = createHash("sha256").update(content).digest("hex").slice(0, 12); - out.push({ - index, - lang: fenceLang || "text", - hash, - line_count: fenceLines.length, - }); - index++; - inFence = false; - fenceLang = ""; - fenceLines = []; - continue; - } - if (inFence) fenceLines.push(line); } - return out; + return count; } diff --git a/pipeline/src/lint-chunking.ts b/pipeline/src/lint-chunking.ts deleted file mode 100644 index fea3bcf..0000000 --- a/pipeline/src/lint-chunking.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Non-blocking chunking lint for polished markdown. - * - * Surfaces warnings that hurt agent / Context7 retrieval quality but do - * not invalidate the corpus. Always exits 0; the warnings are advisory. - * - * Checks: - * - Oversize fenced code block (>{MAX_FENCE_LINES} lines). Long fences - * get sliced into chunks by retrievers and break mid-statement. - * - Fenced code block with no language tag. Chunkers and renderers - * can't highlight or filter by language; agents have to guess. - * - Deep heading (h5/h6). Retrievers struggle to anchor on these and - * prefer to break them into separate chunks, fragmenting context. - * - * Usage: `pnpm lint:chunking` - */ - -import { readFileSync, readdirSync, statSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = resolve(HERE, "../.."); -const POLISHED_ROOT = resolve(REPO_ROOT, "polished"); - -const MAX_FENCE_LINES = 60; -// Markdown supports h1–h6. Real-world technical docs occasionally need h5 -// for legitimate sub-sub-sub-sections (e.g. `Step 5.6.1: Register an auth -// handler` nested under `Step 5.6: Handle JWT-enabled API keys`). Anchoring -// gets noticeably worse at h6, so the lint floor sits there. -const MAX_HEADING_DEPTH = 5; - -interface Warning { - file: string; - line: number; - message: string; -} - -function relativeToRoot(absPath: string): string { - return absPath.startsWith(REPO_ROOT + "/") - ? absPath.slice(REPO_ROOT.length + 1) - : absPath; -} - -function* walkPolished(): Generator { - for (const platform of readdirSync(POLISHED_ROOT)) { - const dir = resolve(POLISHED_ROOT, platform); - if (!statSync(dir).isDirectory()) continue; - for (const f of readdirSync(dir)) { - if (f.endsWith(".polished.md")) yield resolve(dir, f); - } - } -} - -function lintFile(file: string): Warning[] { - const out: Warning[] = []; - const rel = relativeToRoot(file); - const lines = readFileSync(file, "utf8").split(/\r?\n/); - - const openRe = /^ {0,3}```([a-zA-Z0-9_+-]*)\s*$/; - const closeRe = /^ {0,3}```\s*$/; - let inFence = false; - let fenceLang = ""; - let fenceStart = -1; - let fenceLineCount = 0; - - let inFrontmatter = false; - let frontmatterClosed = false; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i] ?? ""; - const lineNum = i + 1; - - if (i === 0 && line === "---") { - inFrontmatter = true; - continue; - } - if (inFrontmatter && !frontmatterClosed) { - if (line === "---") { - inFrontmatter = false; - frontmatterClosed = true; - } - continue; - } - - const open = openRe.exec(line); - if (!inFence && open) { - inFence = true; - fenceLang = open[1] ?? ""; - fenceStart = lineNum; - fenceLineCount = 0; - if (fenceLang === "") { - out.push({ - file: rel, - line: lineNum, - message: "fenced code block has no language tag (chunkers and renderers cannot filter by language)", - }); - } - continue; - } - if (inFence && closeRe.test(line)) { - if (fenceLineCount > MAX_FENCE_LINES) { - out.push({ - file: rel, - line: fenceStart, - message: `${fenceLang || "no-lang"} code block is ${fenceLineCount} lines (>${MAX_FENCE_LINES}); retrievers will slice it and may break mid-statement`, - }); - } - inFence = false; - fenceLang = ""; - fenceStart = -1; - fenceLineCount = 0; - continue; - } - if (inFence) { - fenceLineCount++; - continue; - } - - const headingMatch = /^(#{1,6})\s+\S/.exec(line); - if (headingMatch) { - const depth = headingMatch[1]!.length; - if (depth > MAX_HEADING_DEPTH) { - out.push({ - file: rel, - line: lineNum, - message: `heading depth ${depth} (>${MAX_HEADING_DEPTH}); retrievers struggle to anchor on h${depth} and fragment surrounding context`, - }); - } - } - } - return out; -} - -function main() { - const warnings: Warning[] = []; - let files = 0; - for (const file of walkPolished()) { - files++; - warnings.push(...lintFile(file)); - } - - if (warnings.length === 0) { - console.log(`OK ${files} polished file(s) clean (no chunking warnings)`); - return; - } - - console.log(`WARN ${warnings.length} chunking warning(s) across ${files} file(s) — advisory only, does not fail CI:\n`); - let last = ""; - for (const w of warnings) { - if (w.file !== last) { - console.log(` ${w.file}`); - last = w.file; - } - console.log(` L${w.line} ${w.message}`); - } -} - -main(); diff --git a/pipeline/src/polish-layer-a.ts b/pipeline/src/polish-layer-a.ts deleted file mode 100644 index b3cebe0..0000000 --- a/pipeline/src/polish-layer-a.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Stage 2 — Polish, Layer A CLI. - * - * Reads sources//.md, applies the Layer A deterministic - * transforms, and writes the polished corpus to - * /.polished.md — the file every downstream stage - * (snapshot, validate, manifest) consumes. v1 has no Layer B, so Layer A - * output IS the polished corpus; there is no separate promote step. - * - * Usage: - * pnpm polish:a # all articles in the config - * pnpm polish:a -- in-app-messages-on-android # one article by slug - * pnpm polish:a -- --platform=ios # explicit platform - */ - -import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { parse as parseYaml } from "yaml"; -import { applyLayerA, type LayerAContext } from "./lib/layer-a.ts"; - -interface ArticleConfig { - slug: string; - source_path: string; - feature: string; - archetype: "integration" | "feature" | "identity"; -} - -interface PlatformConfig { - platform: string; - source: { repo: string; ref: string; ref_label?: string }; - sdk: { repo: string; tag: string; changelog_path: string }; - paths: { sources_dir: string; polished_dir: string; skill_dir: string }; - publish: { enabled: boolean }; - articles: ArticleConfig[]; -} - -const HERE = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = resolve(HERE, "../.."); -const CONFIG_DIR = resolve(REPO_ROOT, "pipeline/config"); -const SDK_ARTIFACT = "iterableapi"; - -function parseArgs(argv: string[]): { platform: string | undefined; slug: string | undefined } { - let platform: string | undefined; - let slug: string | undefined; - for (const arg of argv) { - const m = /^--platform=(.+)$/.exec(arg); - if (m) platform = m[1]; - else if (!arg.startsWith("--")) slug = arg; - } - return { platform, slug }; -} - -function resolveConfigPath(platformArg: string | undefined): string { - if (platformArg) return resolve(CONFIG_DIR, `${platformArg}.yml`); - const ymls = readdirSync(CONFIG_DIR).filter((f) => f.endsWith(".yml")); - if (ymls.length === 1) return resolve(CONFIG_DIR, ymls[0]!); - throw new Error( - `Multiple configs in ${CONFIG_DIR} (${ymls.join(", ")}). Pass --platform=.`, - ); -} - -function main() { - const { platform, slug } = parseArgs(process.argv.slice(2)); - const configPath = resolveConfigPath(platform); - if (!existsSync(configPath)) { - console.error(`Config not found: ${configPath}`); - process.exit(1); - } - - const config = parseYaml(readFileSync(configPath, "utf8")) as PlatformConfig; - const sourcesDir = resolve(REPO_ROOT, config.paths.sources_dir); - const outDir = resolve(REPO_ROOT, config.paths.polished_dir); - mkdirSync(outDir, { recursive: true }); - - const articles = slug - ? config.articles.filter((a) => a.slug === slug) - : config.articles; - - if (articles.length === 0) { - console.error(`No article with slug "${slug}" in ${configPath}`); - process.exit(1); - } - - console.log( - `Layer A on ${articles.length}/${config.articles.length} ${config.platform} article(s)`, - ); - console.log(` ${config.paths.sources_dir} → ${config.paths.polished_dir}\n`); - - for (const article of articles) { - const inPath = resolve(sourcesDir, `${article.slug}.md`); - if (!existsSync(inPath)) { - console.log(` skip ${article.slug} (not fetched yet — run pnpm fetch:sources)`); - continue; - } - const raw = readFileSync(inPath, "utf8"); - const ctx: LayerAContext = { - slug: article.slug, - feature: article.feature, - archetype: article.archetype, - sdkVersion: config.sdk.tag, - sdkArtifact: SDK_ARTIFACT, - }; - const { output, snippets } = applyLayerA(raw, ctx); - const outPath = resolve(outDir, `${article.slug}.polished.md`); - writeFileSync(outPath, output, "utf8"); - console.log(` write ${article.slug} (${snippets.length} snippet${snippets.length === 1 ? "" : "s"})`); - } -} - -main(); diff --git a/pipeline/src/recompute-manifest.ts b/pipeline/src/recompute-manifest.ts deleted file mode 100644 index 7105056..0000000 --- a/pipeline/src/recompute-manifest.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Recomputes the `snippets` array in the frontmatter of one or more polished - * markdown files by re-running Layer A's snippet collector against the body. - * - * Use when: - * - The body has been manually edited (e.g. surgical re-polish of a - * cross-platform article) and the existing manifest is stale. - * - Layer A's collector logic changed (bug fix, new feature) and the - * manifest needs to be regenerated without otherwise rewriting the body. - * - * Usage: - * pnpm recompute:manifest - * pnpm recompute:manifest polished/android/android-sdk.polished.md - * pnpm recompute:manifest polished/android/*.polished.md - * - * The body is untouched; only the `snippets` field in the frontmatter is - * replaced and `polished_at` is refreshed. - */ - -import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { resolve } from "node:path"; -import { splitFrontmatter, joinFrontmatter } from "./lib/frontmatter.ts"; -import { collectSnippets } from "./lib/layer-a.ts"; - -function main() { - const paths = process.argv.slice(2); - if (paths.length === 0) { - console.error("usage: pnpm recompute:manifest "); - process.exit(2); - } - - let changed = 0; - let unchanged = 0; - for (const arg of paths) { - const filePath = resolve(process.cwd(), arg); - if (!existsSync(filePath)) { - console.error(`skip ${arg} (not found)`); - continue; - } - const raw = readFileSync(filePath, "utf8"); - const { frontmatter, body } = splitFrontmatter(raw); - - const newSnippets = collectSnippets(body); - const oldSnippets = Array.isArray(frontmatter.snippets) - ? frontmatter.snippets - : []; - - const same = - JSON.stringify(oldSnippets) === JSON.stringify(newSnippets); - if (same) { - console.log(`ok ${arg} (${newSnippets.length} snippet${newSnippets.length === 1 ? "" : "s"}, unchanged)`); - unchanged++; - continue; - } - - const updated = { - ...frontmatter, - snippets: newSnippets, - polished_at: new Date().toISOString(), - }; - writeFileSync(filePath, joinFrontmatter(updated, body), "utf8"); - console.log( - `write ${arg} (${oldSnippets.length} → ${newSnippets.length} snippet${newSnippets.length === 1 ? "" : "s"})`, - ); - changed++; - } - console.log(`\n${changed} updated, ${unchanged} unchanged.`); -} - -main(); diff --git a/pipeline/src/fetch.ts b/pipeline/src/refresh.ts similarity index 53% rename from pipeline/src/fetch.ts rename to pipeline/src/refresh.ts index 3192650..d7435be 100644 --- a/pipeline/src/fetch.ts +++ b/pipeline/src/refresh.ts @@ -1,23 +1,24 @@ /** - * Stage 1 — Fetch. + * Refresh the reference corpus. * * Reads pipeline/config/.yml, pulls each listed article from the - * pinned commit of `source.repo` via `gh api`, and writes - * /.md with the original VuePress frontmatter - * preserved plus a fetch stamp appended. + * pinned commit of `source.repo` via `gh api`, applies the deterministic + * transforms, and writes /.md. There is no + * intermediate directory and no LLM step — the corpus is the docs reshaped. * - * Idempotent: if a source file already exists with the same `source_sha`, - * the fetch is skipped. + * Idempotent: an article whose upstream blob sha matches the `source_sha` + * already recorded in the reference file is skipped. That's what makes + * `git diff` after a run mean "upstream actually changed", which the refresh + * workflow relies on to name the touched slugs. * * Usage: - * pnpm fetch:sources # picks the single config in pipeline/config - * pnpm fetch:sources -- android # explicit platform when several configs exist + * pnpm refresh:docs # picks the single config in pipeline/config + * pnpm refresh:docs -- android # explicit platform when several configs exist * * Ref override: set SOURCE_REF to fetch from a specific commit/branch instead * of the config's pinned `source.ref`. The auto-refresh workflow passes the - * docs commit that triggered it (or `master`) so a dispatch actually pulls the - * new docs; `set-source-ref.ts` then writes the resolved SHA back into the - * config. Unset (local/manual runs) → the config pin is used, unchanged. + * docs commit that triggered it; `set-source-ref.ts` then writes the resolved + * SHA back into the config. */ import { execFileSync } from "node:child_process"; @@ -26,6 +27,7 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parse as parseYaml } from "yaml"; import { splitFrontmatter, joinFrontmatter } from "./lib/frontmatter.ts"; +import { applyLayerA, type LayerAContext } from "./lib/layer-a.ts"; interface ArticleConfig { slug: string; @@ -38,8 +40,7 @@ interface PlatformConfig { platform: string; source: { repo: string; ref: string; ref_label?: string }; sdk: { repo: string; tag: string; changelog_path: string }; - paths: { sources_dir: string; polished_dir: string; skill_dir: string }; - publish: { enabled: boolean; context7?: { library_name: string; version_from: string } }; + paths: { reference_dir: string }; articles: ArticleConfig[]; } @@ -52,6 +53,7 @@ interface ContentApiResponse { const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, "../.."); const CONFIG_DIR = resolve(REPO_ROOT, "pipeline/config"); +const SDK_ARTIFACT = "iterableapi"; function ghApiJson(path: string): T { const stdout = execFileSync("gh", ["api", path], { @@ -62,34 +64,14 @@ function ghApiJson(path: string): T { } function fetchArticle(repo: string, ref: string, sourcePath: string) { - const apiPath = `repos/${repo}/contents/${sourcePath}?ref=${ref}`; - const res = ghApiJson(apiPath); - const body = Buffer.from(res.content, res.encoding).toString("utf8"); - return { sha: res.sha, body }; + const res = ghApiJson(`repos/${repo}/contents/${sourcePath}?ref=${ref}`); + return { sha: res.sha, body: Buffer.from(res.content, res.encoding).toString("utf8") }; } function existingSourceSha(filePath: string): string | undefined { if (!existsSync(filePath)) return undefined; - const raw = readFileSync(filePath, "utf8"); - const { frontmatter } = splitFrontmatter(raw); - const sha = frontmatter.source_sha; - return typeof sha === "string" ? sha : undefined; -} - -function stampedSource( - raw: string, - ctx: { sourceRepo: string; sourcePath: string; sourceRef: string; sourceSha: string }, -): string { - const { frontmatter, body } = splitFrontmatter(raw); - const stamped = { - ...frontmatter, - source_repo: ctx.sourceRepo, - source_path: ctx.sourcePath, - source_ref: ctx.sourceRef, - source_sha: ctx.sourceSha, - fetched_at: new Date().toISOString(), - }; - return joinFrontmatter(stamped, body); + const { frontmatter } = splitFrontmatter(readFileSync(filePath, "utf8")); + return typeof frontmatter.source_sha === "string" ? frontmatter.source_sha : undefined; } function resolveConfigPath(platformArg: string | undefined): string { @@ -111,11 +93,9 @@ function main() { } const config = parseYaml(readFileSync(configPath, "utf8")) as PlatformConfig; - const outDir = resolve(REPO_ROOT, config.paths.sources_dir); + const outDir = resolve(REPO_ROOT, config.paths.reference_dir); mkdirSync(outDir, { recursive: true }); - // SOURCE_REF overrides the pinned config ref (see header). A branch name - // (e.g. "master") resolves to its head at fetch time via the contents API. const refOverride = process.env.SOURCE_REF?.trim(); const sourceRef = refOverride || config.source.ref; const refDisplay = refOverride @@ -123,37 +103,53 @@ function main() { : config.source.ref_label ? `${config.source.ref.slice(0, 7)} (${config.source.ref_label})` : config.source.ref.slice(0, 7); + console.log( - `Fetching ${config.articles.length} ${config.platform} articles from ${config.source.repo}@${refDisplay}`, + `Refreshing ${config.articles.length} ${config.platform} articles from ${config.source.repo}@${refDisplay}`, ); - console.log(` → ${config.paths.sources_dir}\n`); + console.log(` → ${config.paths.reference_dir}\n`); - let fetched = 0; + let written = 0; let skipped = 0; for (const article of config.articles) { const outPath = resolve(outDir, `${article.slug}.md`); - const previous = existingSourceSha(outPath); const { sha, body } = fetchArticle(config.source.repo, sourceRef, article.source_path); - if (previous === sha) { + if (existingSourceSha(outPath) === sha) { console.log(` skip ${article.slug} (unchanged)`); skipped++; continue; } - const stamped = stampedSource(body, { - sourceRepo: config.source.repo, - sourcePath: article.source_path, - // Record the resolved ref: when SOURCE_REF is a branch, the blob sha is - // per-file, so keep source_ref as what was requested for provenance. - sourceRef, - sourceSha: sha, - }); - writeFileSync(outPath, stamped, "utf8"); - console.log(` write ${article.slug} (${sha.slice(0, 7)})`); - fetched++; + + const { frontmatter, body: articleBody } = splitFrontmatter(body); + const withProvenance = joinFrontmatter( + { + ...frontmatter, + source_repo: config.source.repo, + source_path: article.source_path, + source_ref: sourceRef, + source_sha: sha, + fetched_at: new Date().toISOString(), + }, + articleBody, + ); + + const ctx: LayerAContext = { + slug: article.slug, + feature: article.feature, + archetype: article.archetype, + sdkVersion: config.sdk.tag, + sdkArtifact: SDK_ARTIFACT, + }; + const { output, snippetCount } = applyLayerA(withProvenance, ctx); + writeFileSync(outPath, output, "utf8"); + console.log( + ` write ${article.slug} (${sha.slice(0, 7)}, ${snippetCount} snippet${snippetCount === 1 ? "" : "s"})`, + ); + written++; } - console.log(`\nDone. ${fetched} written, ${skipped} unchanged.`); + console.log(`\nDone. ${written} written, ${skipped} unchanged.`); } main(); diff --git a/pipeline/src/snapshot.ts b/pipeline/src/snapshot.ts deleted file mode 100644 index 6a8e339..0000000 --- a/pipeline/src/snapshot.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Snapshot tool — keeps `iterable-android/snapshot/` in lockstep with - * `polished/android/*.polished.md`. Two modes: - * - * pnpm snapshot:refresh — overwrite the snapshot dir from polished/android/ - * pnpm snapshot:verify — exit 1 if any file differs; CI gate - * - * Why a snapshot at all? - * - * The skill's primary content source is Context7. The snapshot is the - * offline fallback: if the Context7 fetch fails or returns thin results, - * the agent reads from `iterable-android/snapshot/` and keeps working. - * Without it, a Context7 outage = the skill is dead. - * - * The snapshot must never lag merged changes to `polished/`. `verify` - * runs in CI; if a reviewer updates a polished doc and forgets to run - * `snapshot:refresh`, CI catches it before merge. - * - * Filename mapping: - * - * polished/android/.polished.md -> iterable-android/snapshot/.md - * - * The `.polished` suffix is dropped in the snapshot because the snapshot - * isn't part of the pipeline corpus — it's a static asset shipped with - * the skill. Naming it `.md` keeps the agent-facing path readable. - */ - -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const REPO_ROOT = resolve(__dirname, "../.."); -const POLISHED_DIR = join(REPO_ROOT, "polished", "android"); -const SNAPSHOT_DIR = join(REPO_ROOT, "iterable-android", "snapshot"); -const POLISHED_SUFFIX = ".polished.md"; -const SNAPSHOT_SUFFIX = ".md"; - -interface SnapshotEntry { - slug: string; - polishedPath: string; - snapshotPath: string; - content: string; -} - -function collectPolished(): SnapshotEntry[] { - if (!existsSync(POLISHED_DIR)) { - console.error(`FAIL polished dir not found: ${POLISHED_DIR}`); - process.exit(2); - } - return readdirSync(POLISHED_DIR) - .filter((name) => name.endsWith(POLISHED_SUFFIX)) - .sort() - .map((name) => { - const slug = name.slice(0, -POLISHED_SUFFIX.length); - const polishedPath = join(POLISHED_DIR, name); - return { - slug, - polishedPath, - snapshotPath: join(SNAPSHOT_DIR, slug + SNAPSHOT_SUFFIX), - content: readFileSync(polishedPath, "utf8"), - }; - }); -} - -function collectStaleSnapshots(expectedNames: Set): string[] { - if (!existsSync(SNAPSHOT_DIR)) return []; - return readdirSync(SNAPSHOT_DIR) - .filter((name) => name.endsWith(SNAPSHOT_SUFFIX)) - .filter((name) => !expectedNames.has(name)) - .map((name) => join(SNAPSHOT_DIR, name)); -} - -function refresh(): void { - const entries = collectPolished(); - mkdirSync(SNAPSHOT_DIR, { recursive: true }); - - const expectedNames = new Set(entries.map((e) => e.slug + SNAPSHOT_SUFFIX)); - const stale = collectStaleSnapshots(expectedNames); - for (const path of stale) rmSync(path); - - for (const entry of entries) { - writeFileSync(entry.snapshotPath, entry.content); - } - - console.log( - `OK refreshed ${entries.length} snapshot(s) into iterable-android/snapshot/` + - (stale.length ? ` (removed ${stale.length} stale file(s))` : ""), - ); -} - -function verify(): void { - const entries = collectPolished(); - const expectedNames = new Set(entries.map((e) => e.slug + SNAPSHOT_SUFFIX)); - const issues: string[] = []; - - for (const entry of entries) { - if (!existsSync(entry.snapshotPath)) { - issues.push(`missing snapshot: iterable-android/snapshot/${entry.slug}${SNAPSHOT_SUFFIX}`); - continue; - } - const have = readFileSync(entry.snapshotPath, "utf8"); - if (have !== entry.content) { - issues.push(`drift: iterable-android/snapshot/${entry.slug}${SNAPSHOT_SUFFIX} differs from polished/android/${entry.slug}${POLISHED_SUFFIX}`); - } - } - - for (const path of collectStaleSnapshots(expectedNames)) { - issues.push(`stale snapshot (no matching polished source): ${path.slice(REPO_ROOT.length + 1)}`); - } - - if (issues.length === 0) { - console.log(`OK ${entries.length} snapshot(s) match polished/android/`); - return; - } - console.error(`FAIL ${issues.length} snapshot drift issue(s):`); - for (const issue of issues) console.error(` - ${issue}`); - console.error(`\nRun \`pnpm snapshot:refresh\` to sync the snapshot dir.`); - process.exit(1); -} - -function main(): void { - const mode = process.argv[2]; - switch (mode) { - case "refresh": - refresh(); - return; - case "verify": - verify(); - return; - default: - console.error("usage: tsx src/snapshot.ts "); - process.exit(2); - } -} - -main(); diff --git a/pipeline/src/validate-polished.ts b/pipeline/src/validate-polished.ts deleted file mode 100644 index da3989a..0000000 --- a/pipeline/src/validate-polished.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Validates frontmatter of every `polished//.polished.md` - * file against `pipeline/schema/polished.schema.json`. Also runs a small set - * of structural invariants that JSON Schema can't express directly: - * - * - `snippets[].index` values are contiguous and start at 0. - * - `layer` is `a` — the corpus is deterministic Layer A output only; there - * is no LLM rewrite stage. The `.polished.md` suffix is - * retained for filename stability, but the content is Layer A. - * - Snippet manifest length equals the count of `^```` opening fences in - * the body, so the manifest never drifts away from the file content. - * - * Exit code is non-zero on any failure. Designed to be wired into a - * `pnpm validate:polished` script and a GH Actions check. - */ - -import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import Ajv2020, { type ErrorObject } from "ajv/dist/2020.js"; -import addFormats from "ajv-formats"; -import { splitFrontmatter } from "./lib/frontmatter.ts"; - -const HERE = dirname(fileURLToPath(import.meta.url)); -const REPO_ROOT = resolve(HERE, "../.."); -const SCHEMA_PATH = resolve(REPO_ROOT, "pipeline/schema/polished.schema.json"); -const POLISHED_ROOT = resolve(REPO_ROOT, "polished"); - -interface Issue { - file: string; - message: string; -} - -function loadSchema(): object { - return JSON.parse(readFileSync(SCHEMA_PATH, "utf8")); -} - -function listPolishedFiles(): string[] { - if (!existsSync(POLISHED_ROOT)) return []; - const out: string[] = []; - for (const platform of readdirSync(POLISHED_ROOT)) { - const platformDir = resolve(POLISHED_ROOT, platform); - if (!statSync(platformDir).isDirectory()) continue; - for (const file of readdirSync(platformDir)) { - if (file.endsWith(".polished.md")) { - out.push(resolve(platformDir, file)); - } - } - } - return out.sort(); -} - -function countOpeningFences(body: string): number { - // CommonMark allows code fences to be indented by 0–3 spaces. Stays in - // lockstep with `collectSnippets` in `pipeline/src/lib/layer-a.ts` so the - // manifest never disagrees with what the validator sees. - const fenceRe = /^ {0,3}```/; - let count = 0; - let inside = false; - for (const line of body.split(/\r?\n/)) { - if (fenceRe.test(line)) { - if (!inside) count++; - inside = !inside; - } - } - return count; -} - -function formatAjvErrors(errors: readonly ErrorObject[] | null | undefined): string[] { - if (!errors) return []; - return errors.map((e) => { - const where = e.instancePath || "(root)"; - return `${where} ${e.message ?? "is invalid"}`; - }); -} - -function checkSnippetIndexContiguity( - snippets: Array<{ index: unknown }>, -): string | undefined { - for (let i = 0; i < snippets.length; i++) { - if (snippets[i]?.index !== i) { - return `snippets[${i}].index is ${JSON.stringify(snippets[i]?.index)}, expected ${i} (must be contiguous starting at 0)`; - } - } - return undefined; -} - -function relativeToRoot(absPath: string): string { - return absPath.startsWith(REPO_ROOT + "/") - ? absPath.slice(REPO_ROOT.length + 1) - : absPath; -} - -function main() { - const schema = loadSchema(); - const ajv = new Ajv2020({ allErrors: true, strict: false }); - addFormats(ajv); - const validate = ajv.compile(schema); - - const files = listPolishedFiles(); - if (files.length === 0) { - console.error(`No polished files found under ${POLISHED_ROOT}`); - process.exit(1); - } - - const issues: Issue[] = []; - let ok = 0; - - for (const file of files) { - const rel = relativeToRoot(file); - const raw = readFileSync(file, "utf8"); - const { frontmatter, body } = splitFrontmatter(raw); - - const before = issues.length; - - if (!validate(frontmatter)) { - for (const msg of formatAjvErrors(validate.errors)) { - issues.push({ file: rel, message: msg }); - } - } - - if (frontmatter.layer !== "a") { - issues.push({ - file: rel, - message: `frontmatter \`layer\` is ${JSON.stringify(frontmatter.layer)}, expected \`a\` (corpus is deterministic Layer A only; no LLM stage)`, - }); - } - - const snippets = Array.isArray(frontmatter.snippets) - ? (frontmatter.snippets as Array<{ index: unknown }>) - : []; - const contiguity = checkSnippetIndexContiguity(snippets); - if (contiguity) issues.push({ file: rel, message: contiguity }); - - const bodyFences = countOpeningFences(body); - if (bodyFences !== snippets.length) { - issues.push({ - file: rel, - message: `snippet manifest length ${snippets.length} does not match body opening-fence count ${bodyFences}`, - }); - } - - if (issues.length === before) ok++; - } - - if (issues.length > 0) { - console.error(`FAIL ${issues.length} issue(s) across ${files.length - ok} of ${files.length} file(s):\n`); - let lastFile = ""; - for (const issue of issues) { - if (issue.file !== lastFile) { - console.error(` ${issue.file}`); - lastFile = issue.file; - } - console.error(` - ${issue.message}`); - } - process.exit(1); - } - - console.log(`OK ${files.length} polished file(s) validate against ${relativeToRoot(SCHEMA_PATH)}`); -} - -main(); diff --git a/pipeline/src/validate-reference.ts b/pipeline/src/validate-reference.ts new file mode 100644 index 0000000..da0d5f2 --- /dev/null +++ b/pipeline/src/validate-reference.ts @@ -0,0 +1,113 @@ +/** + * Validates the frontmatter of every `iterable-android/reference/.md` + * against `pipeline/schema/reference.schema.json`, and checks that the corpus + * and the platform config agree on which slugs exist. That second half is what + * catches a config edit that renames a slug without a refresh — the skill's + * routing table points at slugs, so a missing file is a dead link. + * + * Exit code is non-zero on any failure. + */ + +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parse as parseYaml } from "yaml"; +import Ajv2020, { type ErrorObject } from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; +import { splitFrontmatter } from "./lib/frontmatter.ts"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(HERE, "../.."); +const SCHEMA_PATH = resolve(REPO_ROOT, "pipeline/schema/reference.schema.json"); +const CONFIG_DIR = resolve(REPO_ROOT, "pipeline/config"); + +interface PlatformConfig { + platform: string; + paths: { reference_dir: string }; + articles: Array<{ slug: string }>; +} + +interface Issue { + file: string; + message: string; +} + +function relativeToRoot(absPath: string): string { + return absPath.startsWith(REPO_ROOT + "/") ? absPath.slice(REPO_ROOT.length + 1) : absPath; +} + +function loadConfigs(): PlatformConfig[] { + return readdirSync(CONFIG_DIR) + .filter((f) => f.endsWith(".yml")) + .map((f) => parseYaml(readFileSync(resolve(CONFIG_DIR, f), "utf8")) as PlatformConfig); +} + +function formatAjvErrors(errors: readonly ErrorObject[] | null | undefined): string[] { + if (!errors) return []; + return errors.map((e) => `${e.instancePath || "(root)"} ${e.message ?? "is invalid"}`); +} + +function main() { + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + const validate = ajv.compile(JSON.parse(readFileSync(SCHEMA_PATH, "utf8"))); + + const issues: Issue[] = []; + let checked = 0; + + for (const config of loadConfigs()) { + const refDir = resolve(REPO_ROOT, config.paths.reference_dir); + if (!existsSync(refDir)) { + console.error(`FAIL reference dir not found: ${config.paths.reference_dir}`); + process.exit(1); + } + + const present = new Set(readdirSync(refDir).filter((f) => f.endsWith(".md"))); + const expected = new Set(config.articles.map((a) => `${a.slug}.md`)); + + for (const name of expected) { + if (!present.has(name)) { + issues.push({ + file: `${config.paths.reference_dir}/${name}`, + message: `listed in pipeline/config/${config.platform}.yml but missing — run \`pnpm refresh:docs\``, + }); + } + } + for (const name of present) { + if (!expected.has(name)) { + issues.push({ + file: `${config.paths.reference_dir}/${name}`, + message: `not listed in pipeline/config/${config.platform}.yml — stale file, or a missing config entry`, + }); + } + } + + for (const name of [...present].sort()) { + const file = resolve(refDir, name); + const { frontmatter } = splitFrontmatter(readFileSync(file, "utf8")); + checked++; + if (!validate(frontmatter)) { + for (const msg of formatAjvErrors(validate.errors)) { + issues.push({ file: relativeToRoot(file), message: msg }); + } + } + } + } + + if (issues.length > 0) { + console.error(`FAIL ${issues.length} issue(s):\n`); + let lastFile = ""; + for (const issue of issues) { + if (issue.file !== lastFile) { + console.error(` ${issue.file}`); + lastFile = issue.file; + } + console.error(` - ${issue.message}`); + } + process.exit(1); + } + + console.log(`OK ${checked} reference doc(s) validate against ${relativeToRoot(SCHEMA_PATH)}`); +} + +main(); diff --git a/polished/android/android-app-links.polished.md b/polished/android/android-app-links.polished.md deleted file mode 100644 index 0a9fac1..0000000 --- a/polished/android/android-app-links.polished.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -slug: android-app-links -feature: deep-linking -archetype: feature -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Android App Links -source_url: https://support.iterable.com/hc/articles/360035127392 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/deep-links/android-app-links/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 3c933bbc8661bddfeaa8914f3dbb8233d98469c6 -fetched_at: 2026-05-25T15:11:45.366Z -polished_at: 2026-08-03T20:42:14.571Z -layer: a -snippets: - - index: 0 - lang: java - hash: "8191331288e0" - line_count: 24 - - index: 1 - lang: java - hash: 7f9532ca4355 - line_count: 7 - - index: 2 - lang: java - hash: 61186d66b7f8 - line_count: 6 -summary: Messages sent with Iterable can include Android App Links, which - redirect users to your installed mobile app—no browser required. Iterable - tracks clicks on these links as expected. ---- -# Android App Links - -Messages sent with Iterable can include Android App Links, which redirect users -to your installed mobile app—no browser required. Iterable tracks clicks on these -links as expected. - -> [!WARNING] -> You must set up [iOS deep linking](https://support.iterable.com/hc/articles/360035496511) -> before implementing Android deep linking (they rely on similar architecture). - -## Setting up Android App Links - -### 1. Enable Android App Links - -To enable Android App Links, follow these steps: - -- Configure your Iterable project to support deep links. For more information, - read [Configuring Deep Links for Email or SMS](https://support.iterable.com/hc/articles/115002651226). - -- Configure your mobile app to handle Android App Links by following the - [instructions in the Android documentation](https://developer.android.com/training/app-links/index.html). - -- Create intent filters for Iterable URIs by using the [App Links Assistant](https://developer.android.com/studio/write/app-link-indexing) - - - Set the **Host** to your tracking domain. For example, - `https://links..com`. - - - Set the **Path** to use `/a` as the **pathPrefix**. - -- Generate the `assetlinks.json` file - -### 2. Upload `assetlinks.json` - -After you generate an `assetlinks.json` file with your app's fingerprint, -you'll need to provide it to Iterable so that it can be hosted at -`/.well-known/assetlinks.json`. To upload it, -follow the instructions in [Configuring Deep Links for Email or SMS](https://support.iterable.com/hc/articles/115002651226). - -Then, use Google's [Statement List Generator and Tester](https://developers.google.com/digital-asset-links/tools/generator) -to test it out. - -### 3. Determine which links to rewrite - -To determine which links to rewrite as deep links for a given campaign, Iterable -looks at the relevant tracking domain's `apple-app-site-association` file. Even -if you only have an Android app, you'll still need to create this file. To learn -how to do so, read [Configuring Deep Links for Email or SMS](https://support.iterable.com/hc/articles/115002651226). - -### 4. Update your code - -If you already have a `urlHandler`, you can use the same handler for email deep -links by calling `handleAppLink` in the activity that handles all Android App -Links in your app: - -```java -// MainActivity.java -@Override -public void onCreate() { - super.onCreate(); - ... - handleIntent(getIntent()); -} - -@Override -public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - if (intent != null) { - handleIntent(intent); - } -} - -private void handleIntent(Intent intent) { - if (Intent.ACTION_VIEW.equals(intent.getAction()) && intent.getData() != null) { - IterableApi.getInstance().handleAppLink(intent.getDataString()); - // Overwrite the intent to make sure we don't open the deep link - // again when the user opens our app later from the task manager - setIntent(new Intent(Intent.ACTION_MAIN)); - } -} -``` - -Alternatively, call `getAndTrackDeeplink` along with a callback to handle the -original deep link URL. You can use this method for any incoming URLs, as it -will execute the callback without changing the URL for non-Iterable URLs. - -```java -IterableApi.getAndTrackDeeplink(uri, new IterableHelper.IterableActionHandler() { - @Override - public void execute(String result) { - Log.d("HandleDeeplink", "Redirected to: "+ result); - // Handle the original deep link URL here - } -}); -``` - -**💡 TIP** - -To check if a URL is an Iterable deep link before handling it, use the -`isIterableDeepLink` method: - -```java -if (IterableApi.getInstance().isIterableDeepLink(urlString)) { - // URL is an Iterable deep link, handle it with the SDK - IterableApi.getInstance().handleAppLink(urlString); -} else { - // Handle non-Iterable URLs differently if needed -} -``` - -This method returns `true` if the URL matches the Iterable deep link pattern -(URLs containing `/a/` in the path). - -## FAQ - -For answers to common questions about deep links, read the [Deep Link FAQs](https://support.iterable.com/hc/articles/360035624191#deep-link-faqs). - diff --git a/polished/android/android-sdk.polished.md b/polished/android/android-sdk.polished.md deleted file mode 100644 index 72d889b..0000000 --- a/polished/android/android-sdk.polished.md +++ /dev/null @@ -1,1251 +0,0 @@ ---- -slug: android-sdk -feature: integration -archetype: integration -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Iterable's Android SDK -source_url: https://support.iterable.com/hc/articles/360035019712 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/iterables-ios-and-android-sdks/android-sdk/index.md -source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 -source_sha: de67a132360a33146dae801ab62c3de1d6846ba9 -fetched_at: 2026-08-03T20:41:28.018Z -polished_at: 2026-08-03T20:42:14.561Z -layer: a -snippets: - - index: 0 - lang: groovy - hash: 2497e01c3acd - line_count: 7 - - index: 1 - lang: text - hash: 8cb6f1745a49 - line_count: 1 - - index: 2 - lang: java - hash: c8e337580016 - line_count: 2 - - index: 3 - lang: kotlin - hash: bb240e73f2c3 - line_count: 4 - - index: 4 - lang: kotlin - hash: e982f9fb30e0 - line_count: 4 - - index: 5 - lang: java - hash: 48aba5fe062b - line_count: 4 - - index: 6 - lang: java - hash: 6c37a0b51a76 - line_count: 4 - - index: 7 - lang: java - hash: 88b55d7e366a - line_count: 4 - - index: 8 - lang: java - hash: 6a179b3279eb - line_count: 4 - - index: 9 - lang: java - hash: d2e3e73a8432 - line_count: 5 - - index: 10 - lang: java - hash: 8e54129ce05c - line_count: 24 - - index: 11 - lang: java - hash: 3bc5e2a65010 - line_count: 4 - - index: 12 - lang: java - hash: 836e0bc8e247 - line_count: 9 - - index: 13 - lang: java - hash: 0e273815d107 - line_count: 1 - - index: 14 - lang: java - hash: 16e73bb1286f - line_count: 4 - - index: 15 - lang: java - hash: 698b439a45b3 - line_count: 4 - - index: 16 - lang: java - hash: f362f543301c - line_count: 5 - - index: 17 - lang: java - hash: 11400f57c7ba - line_count: 1 - - index: 18 - lang: java - hash: 0450a5b49d68 - line_count: 12 - - index: 19 - lang: java - hash: e79d0e004450 - line_count: 4 - - index: 20 - lang: java - hash: acfa8af81cce - line_count: 5 - - index: 21 - lang: kotlin - hash: 18b42ba68e56 - line_count: 5 - - index: 22 - lang: java - hash: a16ba32ab078 - line_count: 3 - - index: 23 - lang: java - hash: 6f27d743e5d6 - line_count: 1 - - index: 24 - lang: java - hash: eaacdddcb017 - line_count: 1 - - index: 25 - lang: java - hash: 6b21444c769f - line_count: 5 - - index: 26 - lang: java - hash: 2f3eb2cbfb4e - line_count: 5 - - index: 27 - lang: java - hash: 1ee664bef567 - line_count: 5 - - index: 28 - lang: java - hash: 9edce984f563 - line_count: 4 - - index: 29 - lang: groovy - hash: 744057b87f30 - line_count: 8 -summary: This article describes how to install and configure Iterable's [Android - SDK](https://github.com/Iterable/iterable-android-sdk). ---- -# Iterable's Android SDK - -This article describes how to install and configure Iterable's [Android SDK](https://github.com/Iterable/iterable-android-sdk). - -## Supported Android versions - -Iterable's Android SDK supports Android versions 5.0 (API level 21) and -higher. - -## Encrypted data - -Depending on your `minSdkVersion`, Iterable's Android SDK can encrypt some -data at rest. For more information, read [Upgrading to 3.4.10](#upgrading-to-3-4-10). - -## Installing the SDK - -Follow these steps to install Iterable's Android SDK. If you're upgrading from -a previous version, see [Upgrading the SDK](#upgrading-the-sdk). - -> [!WARNING] -> If your app targets API level 22 or lower, read [Upgrading to 3.4.10](#upgrading-to-3-4-10) -> to learn about some adjustments you'll need to make to your Android project. - -### Step 1: Define a mobile app and push integration in Iterable - -Before installing Iterable's Android SDK in your mobile app, tell your Iterable -project about your mobile app. - -To do this, follow the instructions in [Setting up Android Push Notifications](https://support.iterable.com/hc/articles/115000331943), which describe how to: - -- Define a mobile app in your Iterable project. - -- Give that app a _push integration_. A push integration stores configuration - and authentication information Iterable can use to send push notifications to - your app. - - Even if you don't want to send push notifications, Iterable can use your app's - push integration to send _silent_ push notifications, to tell your app that - Iterable has new in-app and embedded messages for it to fetch and display. - -### Step 2: Create a mobile API key - -To make calls to Iterable's's API, the SDK needs a mobile [API key](https://support.iterable.com/hc/articles/360043464871). -To learn how to create one, read [API Keys](https://support.iterable.com/hc/articles/360043464871) - -> [!WARNING] -> **Never** embed server-side API keys in client-side code (whether JavaScript, a -> mobile application or otherwise), since they can be used to access all of your -> project's data. - -For a mobile app, use a mobile API key. For additional security, enable -[JWT authentication](https://support.iterable.com/hc/articles/360050801231), -if you can support it. - -If necessary, you can use different API keys for debug and production builds -of your app. - -### Step 3: Install the SDK - -> [!TIP] -> To determine the latest version of Iterable's Android SDK, see -> [search.maven.org](https://search.maven.org/artifact/com.iterable/iterableapi). - -To use Iterable's Android SDK in your app, add the SDK and Firebase Messaging as -dependencies to your application's `build.gradle`: - -```groovy -dependencies{ - implementation 'com.iterable:iterableapi:3.5.3' - // Optional, contains Inbox UI components: - implementation 'com.iterable:iterableapi-ui:3.5.3' - // Version 17.4.0+ is required for push notifications and in-app message features: - implementation 'com.google.firebase:firebase-messaging:X.X.X' -} -``` - -### Step 4: Configure ProGuard - -If you're using ProGuard when building your Android app, add this line of -ProGuard configuration to your build: - -``` --keep class org.json.** { *; } -``` - -To learn how to do this, check out Android's guide: -[Shrink, obfuscate, and optimize your app](https://developer.android.com/studio/build/shrink-code#add-configuration). - -> [!WARNING] -> If you use ProGuard but skip this step, some SDK features may not work as expected. - -### Step 5: Set SDK configuration options - -To initialize Iterable's Android SDK, create an `IterableConfig` and set its -various configuration options. - -Do this when your application is starting up, usually in the `onCreate` method -of your `Application` class. Then, pass this `IterableConfig` to `IterableApi.initialize`, -along with your API key. - -```java -IterableConfig config = new IterableConfig.Builder().build(); -IterableApi.initialize(context, "", config); -``` - -`IterableConfig` contains various configuration options for the SDK. For -more information, refer to the following sections of this document. Or, take -a look at the `IterableConfig` [source code](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java). - -> [!WARNING] -> - Version [3.4.10](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.4.10) -> of Iterable's Android SDK provides a configuration option to store in-app -> messages in memory, rather than in a local file. For more information, read -> [Encrypted data](#encrypted-data). -> - Don't `initialize` the SDK in the `onCreate` method of an `Activity`. -> Instead, do it when your app is starting up, regardless of whether it has been -> launched to open an activity or in the background, as the result of an incoming -> push notification. - -#### Step 5.1: Background Initialization - -To prevent application not responding (ANR) errors during app startup when using -SDKs that need to initialize on background, initialize the SDK asynchronously -instead of using the standard `initialize()` method. For example: - -```kotlin -// In Application.onCreate() -IterableApi.initializeInBackground(this, "", config) { - // SDK is ready - this callback is optional -} -``` - -To subscribe to initialization completion from multiple places: - -```kotlin -IterableApi.onSDKInitialized { - // This callback will be invoked when initialization completes - // If already initialized, it's called immediately -} -``` - -Background initialization prevents ANRs by: -- Running all initialization work on a background thread. -- Automatically queuing API calls until initialization completes. -- Ensuring that no data is lost during startup. -- Providing callbacks on the main thread when ready. - -> [!WARNING] -> Always wait for initialization to complete before you access SDK internals. -> Then, to ensure that the SDK is ready for use, use the callback methods provided -> above. - -#### Step 5.2: If necessary, configure the SDK to use Iterable's EDC - -If your Iterable project is hosted on Iterable's [European data center (EDC)](https://support.iterable.com/hc/articles/17572750887444), -update your `IterableConfig` to use Iterable's EDC-based API endpoints: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setDataRegion(IterableDataRegion.EU).build(); -IterableApi.initialize(context, "", config); -``` - -#### Step 5.3: Set allowed URL protocols - -Starting with version [`3.4.0`](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.4.0) -of Iterable's Android SDK, you'll need to declare the specific URL protocols -that the SDK can expect to see on incoming links (and that it should handle -as needed). This prevents the SDK from opening links that use unexpected -URL protocols. - - To do this, pass the protocols you'd like the SDK to support (as an array of - strings) to the `setAllowedProtocols` method on `IterableConfig.Builder`. - - For example, this code allows the SDK to handle `http://`, `tel://`, and `mycompany://` - links: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setAllowedProtocols(new String[]{"http", "tel", "mycompany"}).build(); -IterableApi.initialize(context, "", config); -``` - -> [!WARNING] -> Iterable's Android SDK handles `https`, `action`, `itbl`, and `iterable` links, -> regardless of the contents of this array. However, you must explicitly declare any -> other types of URL protocols you'd like the SDK to handle (otherwise, the SDK -> won't open them in the web browser or as deep links). - -#### Step 5.4: Specify whether to store in-app messages in memory - -By default, Iterable's Android SDK stores in-app messages in an unencrypted local -file. If you'd prefer to have SDK store in-app messages in memory instead, use the -`setUseInMemoryStorageForInApps(true)` SDK configuration option (defaults to `false`): - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setUseInMemoryStorageForInApps(true).build(); -IterableApi.initialize(context, "", config); -``` - -For more information about this option, read [Upgrading to 3.4.10](#upgrading-to-3-4-10). - -#### Step 5.5: Specify a push integration name, if necessary - -In [Step 1: Define a mobile app and push integration in Iterable](#step-1-define-a-mobile-app-and-push-integration-in-iterable), -you defined a mobile app in Iterable, and you gave it a push integration. - -Every push integration in Iterable has a name, and that name almost always matches -your Android app's package name (for example, `com.example.app`). By default, -this is what the SDK expects: to find a push integration in your Iterable project -with a name that matches your app's package name. - -> [!TIP] -> To find the name of your app's push integration in Iterable, navigate to -> **Settings > Apps and Websites**, open the mobile app associated with your app, -> find the **Push** section, and look at the **Name** column in the row associated -> with your push integration. - -However, push integrations created in Iterable before August of 2019 can have -custom names. If this is the case for your push integration, tell the SDK the name -of your push integration by calling `setPushIntegrationName` on -`IterableConfig`: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setPushIntegrationName(““).build(); -IterableApi.initialize(context, ““, config); -``` - -#### Step 5.6: Handle JWT-enabled API keys - -If you're using a [JWT-enabled API Key](https://support.iterable.com/hc/articles/360050801231), -you'll need custom code to manage JWT tokens for the signed-in user. - -##### Step 5.6.1: Register an auth handler - -When initializing the SDK, provide an auth handler. The SDK uses the auth -handler to: - -1. Fetch new JWT tokens from your server. -2. Report when a non-null JWT token has been retrieved. -3. Report when there have been failures fetching new JWT tokens. - -The object that you pass to the SDK as an auth manager must implement the -`IterableAuthManager` interface: - -```java -public interface IterableAuthHandler { - String onAuthTokenRequested(); - void onTokenRegistrationSuccessful(String authToken); - void onAuthFailure(AuthFailure authFailure); -} -``` - -For example: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setAuthHandler(new IterableAuthHandler() { - @Override - public String onAuthTokenRequested() { - // Fetch a JWT token for the signed-in user, from your server, and - // return it to the SDK. - return ""; - } - - @Override - public void onTokenRegistrationSuccessful(String authToken) { - // The SDK has retrieved a non-null JWT token for the signed-in user. - // However, the SDK does not validate the token before calling this - // method. - } - - @Override - public void onAuthFailure(AuthFailure authFailure) { - // Inspect the authFailure enum constant and take any necessary action. For - // example, you can pause auth retries (see section 5.5.3, below). - } - }).build(); - IterableApi.initialize(_context, "", config); - ``` - -**`onAuthTokenRequested`** - -The SDK calls `onAuthTokenRequested` when it needs a new JWT token for the -signed-in user. This method should fetch a new JWT token from your server and -return it to the SDK as a string. - -This method is called when: - -- You identify a user by calling `setEmail` or `setUserId`. -- You update a user's email address by calling `updateEmail`. -- The current JWT token has expired, or is about to expire. -- The SDK receives a JWT-related `401` response from Iterable's API. - -**`onTokenRegistrationSuccessful`** - -The SDK calls `onTokenRegistrationSuccessful` after `onAuthTokenRequested` -returns a non-null JWT token. However, other than a null check, the SDK does not -validate the token before calling this method. Generally, you won't need to -implement this method. - -**`onAuthFailure`** - -The SDK calls `onAuthFailure` after it fails to fetch a new JWT token for the -signed-in user. The `AuthFailure` object passed to this method describes the -reason for the failure, along with other information. - -This method is called when: - -- `onAuthTokenRequested` returns `null`. -- `onAuthTokenRequested` throws an exception. -- The SDK receives a JWT-related `401` response from Iterable's API. -- The token returned by `onAuthTokenRequested` is invalid. - -In `onAuthFailure`, to determine the reason for the failure, inspect the -`AuthFailure` object, which has these properties: - -- `userKey` - A string that identifies the user by `userId` or `email`. -- `failedAuthToken` - The JWT token that caused the failure. -- `failedRequestTime` - The timestamp of the failed request, if applicable. -- `failureReason` - An `AuthFailureReason` enum constant that indicates the reason - for the failure. - -`AuthFailureReason` can have these values: - -- `AUTH_TOKEN_EXPIRATION_INVALID` – An auth token's expiration must be less than - one year from its issued-at time. -- `AUTH_TOKEN_EXPIRED` – The token has expired. -- `AUTH_TOKEN_FORMAT_INVALID` – Token has an invalid format (failed a regular - expression check). -- `AUTH_TOKEN_GENERATION_ERROR` – `onAuthTokenRequested` threw an exception. -- `AUTH_TOKEN_GENERIC_ERROR` – Any other error not captured by another constant. -- `AUTH_TOKEN_INVALIDATED` – Iterable has invalidated this token and it cannot - be used. -- `AUTH_TOKEN_NULL` – `onAuthTokenRequested` returned a null JWT token. -- `AUTH_TOKEN_PAYLOAD_INVALID` – Iterable could not decode the token's payload - (`iat`, `exp`, `email`, or `userId`). -- `AUTH_TOKEN_SIGNATURE_INVALID` – Iterable could not validate the token's - authenticity. -- `AUTH_TOKEN_USER_KEY_INVALID` – The token doesn't include an `email` or a `userId`. - Or, one of these values is included, but it references a user that isn't in the - Iterable project. -- `AUTH_TOKEN_MISSING` – The request to Iterable's API did not include a JWT - authorization header. - -> [!TIP] -> You can also provide a JWT token for the current user by passing it directly to -> `setEmail` or `setUserId`. - -##### Step 5.6.2: Set an expiring token refresh period - -To specify how long before the expiration of the user's current JWT token -the SDK should call your [auth token refresh handler](#step-5-6-1-register-an-auth-handler), -to fetch a new token, call `setExpiringAuthTokenRefreshPeriod` on `IterableConfig`: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setExpiringAuthTokenRefreshPeriod(time_in_seconds).build(); -IterableApi.initialize(context, "", config); -``` - -##### Step 5.6.3: Set an auth retry policy - -To control how the SDK handles consecutive JWT token refresh attempts, specify -an auth retry policy. An auth retry policy allows you to control: - -- The number of consecutive times the SDK should attempt to refresh a user's JWT - token, in between successful API calls, before giving up. -- The interval between those attempts. -- A backoff strategy. - -```java -// When creating a RetryPolicy object, specify a maximum number of retries, an -// interval between retries, and a backoff strategy: RetryPolicy.Type.LINEAR or -// RetryPolicy.Type.EXPONENTIAL. The SDK's default RetryPolicy has a maximum of -// 10 retries, an interval of 6 seconds, and a linear backoff strategy. -RetryPolicy retryPolicy = new RetryPolicy(10, 10, RetryPolicy.Type.LINEAR); -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setAuthRetryPolicy(time_in_seconds).build(); -IterableApi.initialize(context, "", config); -``` - -After the SDK reaches the maximum number of consecutive JWT-related request failures, -as configured by your `RetryPolicy`, it stops attempting to refresh the JWT token. - -> [!TIP] -> **Auto-retry for offline processing (3.7.0+)** -> -> In addition to the `RetryPolicy` above (which controls JWT refresh scheduling), -> the SDK supports automatic retry for offline-queued tasks that fail due to JWT -> expiration. When this feature is enabled, the offline task runner pauses -> authenticated tasks on a 401 error, refreshes the JWT, and retries -> automatically. Unauthenticated API calls continue processing while -> authentication is paused. This feature requires no code changes. -> -> This feature is not enabled by default. To turn it on for your project, ask -> your Iterable customer success manager to enable it for your account. - -It's also possible to _manually_ pause JWT token refresh attempts. To do this, -call: - -```java -IterableApi.getInstance().pauseAuthRetries(true); -``` - -When JWT refresh attempts have been paused, they'll only resume after: - -- You provide a new JWT token to the SDK, by calling `setAuthToken`. -- You identify the user by calling `setEmail` or `setUserId`. -- You update the user's email by calling `updateEmail` -- The app restarts. -- You manually pause and unpause JWT token refresh attempts, by calling: - ```java - // If you didn't manually pause JWT refresh attempts in the first place, - // first call pauseAuthRetries(true). Then, call pauseAuthRetries(false). - IterableApi.getInstance().pauseAuthRetries(true); - IterableApi.getInstance().pauseAuthRetries(false); - ``` - -#### Step 5.7: Disable keychain encryption if necessary - -In Android apps with `minSdkVersion` 23 or higher ([Android 6.0](https://developer.android.com/studio/releases/platforms#6.0)) -Iterable's Android SDK encrypts sensitive user data when storing it in the -keychain. This includes the user's `email`, `userId`, and `authToken` (JWT). - -This encryption is enabled by default. However, if you need to disable it, you -can do so by setting the `keychainEncryption` option to `false` when -initializing the SDK: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setKeychainEncryption(false).build(); // Disable encryption for keychain storage -IterableApi.initialize(context, apiKey, config); -``` - -#### Step 5.8: Configure WebView base URL for CORS support, if necessary - -If your in-app or inbox messages load external resources (such as custom fonts or -stylesheets) and you're seeing CORS errors, configure a base URL for the WebView. - -By default, the WebView sends a blank origin when requesting resources. If your -server's CORS policy rejects blank origins, set the base URL to match whatever -origin your server accepts (such as your CDN domain, app domain, or -[https://app.iterable.com](https://app.iterable.com)). - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setWebViewBaseUrl("https://app.iterable.com") // Use https://app.eu.iterable.com for EU - .build(); -IterableApi.initialize(context, "", config); -``` - -### Step 6: Identify the signed-in user - -When you know the user's `email` or `userId`, identify them by calling: - -- `IterableApi.getInstance().setEmail("user@example.com");` -- `IterableApi.getInstance().setUserId("userId");` - -> [!NOTE] -> - Make sure to identify the user _after_ you've specified the configuration -> options on `IterableConfig`, as described in [Step 5: Set SDK configuration options](#step-5-set-sdk-configuration-options). -> - Don't set an email and user ID in the same session. -> - If you've prefetched a JWT auth token, you can pass it directly to `setEmail` -> and `setUserId` (useful to work around race conditions that can sometimes -> occur). - -### Step 7: Handle push notifications - -Next, configure the SDK to handle push notifications. - -> [!TIP] -> If the name of your app's push integration, in Iterable, differs from your -> app's package name (they usually match), make sure to specify your push -> integration name on `IterableConfig`. To learn how to do this, read -> [Step 5.5: Specify a push integration name, if necessary](#step-5-5-specify-a-push-integration-name-if-necessary). - -#### Step 7.1: Register for remote notifications - -Every user + device + app combination can be identified by a unique push _token_, -which is stored on the user's profile in Iterable. Iterable users this token to -send push notifications to the user. - -The SDK _automatically_ saves a push token to the user's profile whenever you -call `setEmail` or `setUserId`. - -However, you can also handle this token registration manually: - -- When initializing the SDK, disable automatic push token registration by - calling `setAutoPushRegistration(false)` on `IterableConfig`. -- Whenever it makes sense, save a device token for the signed-in user to Iterable - by calling `registerForPush` on `IterableApi`: - - ```java - IterableApi.getInstance().registerForPush(); - ``` - -> [!NOTE] -> - Device registration fails when no `email` or `userId` has been set. -> - If you're calling `setEmail` or `setUserId` after the app has already -> launched (for example, when a new user logs in), call `registerForPush` -> to register the device for the current user. - -#### Step 7.2: Handle Firebase push messages and tokens - -The SDK automatically adds a `FirebaseMessagingService` to the app manifest. To -handle incoming push notifications, no extra setup is necessary. - -However, if your application implements its own `FirebaseMessagingService`: - -- Forward `onMessageReceived` calls to `IterableFirebaseMessagingService.handleMessageReceived`. -- Forward `onNewToken` calls to `IterableFirebaseMessagingService.handleTokenRefresh`. - -```java -public class MyFirebaseMessagingService extends FirebaseMessagingService { - - @Override - public void onMessageReceived(RemoteMessage remoteMessage) { - IterableFirebaseMessagingService.handleMessageReceived(this, remoteMessage); - } - - @Override - public void onNewToken(String s) { - IterableFirebaseMessagingService.handleTokenRefresh(); - } -} -``` - -> [!NOTE] -> - This step is mandatory for working with multiple push providers. -> - Firebase has [deprecated `FirebaseInstanceIdService`](https://firebase.google.com/docs/reference/android/com/google/firebase/iid/FirebaseInstanceIdService). -> It has been replaced with `onNewToken`. -> - To handle silent push notifications, use a custom `FirebaseMessagingService`. - -### Step 8: Enable Embedded Messaging if necessary - -To learn how to use Iterable's Android SDK with Embedded Messaging, read -[Embedded Messages with Iterable's Android SDK](https://support.iterable.com/hc/articles/23061877893652). - -## Upgrading the SDK - -This section describes how to upgrade from earlier versions of Iterable's -Android SDK. - -### Upgrading to 3.10.0 - -[Version 3.10.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.10.0) -of Iterable's Android SDK makes manager getters fail gracefully before -initialization, and adds a `DEFER` response for in-app handlers, a -`resumeInAppDisplay()` method, and unknown user criteria fetch callbacks. -**No action is required to upgrade**—all of these changes are backward -compatible. - -#### Manager getters no longer crash before initialization - -In earlier versions, calling `getInAppManager()` or `getEmbeddedManager()` -before `IterableApi.initialize()` threw a `RuntimeException`, which could crash -the host app. Starting with version 3.10.0, these methods log an error and -return a no-op manager instead—it returns empty results and ignores commands, so -a call-ordering mistake no longer crashes your app. - -If you need to detect whether the SDK is initialized before using a manager, use -the new `getInAppManagerOrNull()` and `getEmbeddedManagerOrNull()` methods, which -return `null` (rather than a no-op manager) when the SDK isn't initialized yet. - -```java -IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManagerOrNull(); -if (inAppManager != null) { - // Safe to use; the SDK is initialized. -} -``` - -As always, initialize the SDK in the `onCreate` method of your `Application` -class before calling other SDK methods. - -#### New: `DEFER` response and `resumeInAppDisplay()` for in-app messages - -`IterableInAppHandler.InAppResponse` now includes a `DEFER` value. Unlike `SKIP` -(which permanently drops a message), `DEFER` keeps the message pending so the -SDK reconsiders it later—useful for temporary suppression, such as while a -splash screen is showing. To re-check pending messages on demand once your app -is ready, call the new `IterableInAppManager.resumeInAppDisplay()` method. For -more information, read [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231). - -#### New: unknown user criteria fetch callbacks - -`IterableUnknownUserHandler` now reports the results of unknown user criteria -fetches through two optional methods: `onCriteriaReceived(JSONObject criteria)` -on success and `onCriteriaFetchFailed(String reason)` on failure. Both have -default, no-op implementations, so existing handlers are unaffected. - -For more information, read [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231) -and [Configure the Android SDK](https://support.iterable.com/hc/articles/40078934178836) -in the Unknown User Activation documentation. - -### Upgrading to 3.9.0 - -[Version 3.9.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.9.0) -of Iterable's Android SDK adds in-app message support for Jetpack Compose apps, -a new opt-in toolbar for the mobile inbox, and additional context for push-open -tracking. **No action is required to upgrade**—all of these changes are -backward compatible. - -#### In-app messages in Jetpack Compose apps - -The SDK can now render in-app messages using a new `Dialog`-based renderer -(`IterableInAppDialogNotification`) that doesn't require a `FragmentActivity`. -Apps that host in-app messages in a `FragmentActivity` continue to use the -existing `Fragment`-based rendering; apps that don't (such as those built fully -with Jetpack Compose, using a `ComponentActivity`) automatically fall back to -the `Dialog`-based renderer. As a result, in-app messages now display correctly -in apps built fully with Jetpack Compose, with no additional setup. - -#### New: `IterableInboxToolbarView` for the mobile inbox - -If you use Iterable's [Mobile Inbox](https://support.iterable.com/hc/articles/360038744152), -you can now add an optional toolbar above the inbox list using the new -`IterableInboxToolbarView`. Configure it with the `InboxToolbarOption` sealed -interface: - -- `None` (default) — No toolbar. The inbox behaves exactly as it did in - previous SDK versions. -- `Default` — A title-only toolbar above the inbox list. -- `WithBackButton` — A title plus a back-navigation icon. By default, the back - action calls `OnBackPressedDispatcher`. To override it, have your host - `Activity` or parent `Fragment` implement `IterableInboxToolbarBackListener`. -- `Custom(layoutRes)` — Inflates your own toolbar layout. Views tagged with the - reserved IDs `@id/iterable_reserved_inbox_toolbar_action` and - `@id/iterable_reserved_inbox_toolbar_title` are automatically wired to the - SDK's back handler and title binding, respectively (both are optional). - -Configure the toolbar programmatically with `IterableInboxFragment.newInstance(...)` -(using the new two- or six-argument overloads), or with `IterableInboxActivity` -intent extras (`TOOLBAR_OPTION` and `TOOLBAR_TITLE`). - -> [!WARNING] -> When the toolbar is enabled, the host activity must use a `Theme.AppCompat` -> descendant. - -For more information about customizing the inbox, see -[Customizing Mobile Inbox on Android](https://support.iterable.com/hc/articles/360039189931). - -#### New: `appAlreadyRunning` field on `trackPushOpen` - -`trackPushOpen` now includes an `appAlreadyRunning` field that indicates whether -the app was already running when the push notification was received. A new -`trackPushOpen(int, int, String, boolean, JSONObject)` overload lets you pass -this value; existing overloads default it to `false`, so no changes are required -for existing code. - -#### Fix: `TransactionTooLargeException` crash for large in-app messages - -This release also fixes a `TransactionTooLargeException` crash that could occur -when displaying in-app messages with oversized HTML payloads. The HTML is no -longer serialized into the fragment's saved instance state—it's reloaded from -storage when the fragment is recreated. In-app messages with missing HTML now -dismiss gracefully without registering tracking events, and a warning is logged -for HTML payloads that exceed the recommended size. - -For more information, read [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231) -and [Customizing Mobile Inbox on Android](https://support.iterable.com/hc/articles/360039189931). - -### Upgrading to 3.8.0 - -[Version 3.8.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.8.0) -of Iterable's Android SDK introduces a new configuration option for controlling -how in-app messages interact with system bars, plus refinements to embedded -message views and a security cleanup. **No action is required for most apps**—upgrading -preserves the existing in-app message behavior introduced in 3.6.1. - -#### New: `IterableInAppDisplayMode` for in-app messages - -Since 3.6.1, Iterable's Android SDK has always rendered in-app messages -edge-to-edge, behind the status bar and navigation bar. Starting with 3.8.0, -you can change that behavior globally by setting an `IterableInAppDisplayMode` -on `IterableConfig`: - -```java -IterableConfig config = new IterableConfig.Builder() - .setInAppDisplayMode(IterableInAppDisplayMode.FORCE_RESPECT_BOUNDS) - .build(); - -IterableApi.initialize(context, apiKey, config); -``` - -The available modes are: - -- `FORCE_EDGE_TO_EDGE` (default) — Draws in-app content behind the system - bars, with transparent status and navigation bars. Preserves the behavior - introduced in SDK 3.6.1. -- `FOLLOW_APP_LAYOUT` — Matches the host app's current system bar - configuration. -- `FORCE_FULLSCREEN` — Hides the status bar entirely while in-app messages - are displayed. -- `FORCE_RESPECT_BOUNDS` — Ensures in-app content never overlaps system bars, - keeping UI elements like the close button always accessible. - -If the close button on your fullscreen in-app messages is being obscured by -the status bar on certain devices, switch to `FOLLOW_APP_LAYOUT` or -`FORCE_RESPECT_BOUNDS`. For more information, see [Configuring how in-app messages interact with system bars](https://support.iterable.com/hc/articles/360035537231#configuring-how-in-app-messages-interact-with-system-bars-sdk-v3-8-0-and-above) -in the In-App Messages on Android documentation. - -#### Other changes in 3.8.0 - -- **`imageScaleType` option for embedded message views**: `IterableEmbeddedViewConfig` - exposes a new `imageScaleType` property that controls how the image is - scaled within the 16:9 container of an out-of-the-box embedded message view. - -- **Default values for `IterableEmbeddedViewConfig` parameters**: All - `IterableEmbeddedViewConfig` constructor parameters now have default values, - so you only need to specify the styling options you want to customize. - Existing calls that pass every parameter continue to work unchanged. - -- **Embedded message card layout fixes**: Out-of-the-box embedded message - views render correctly again on cards. The image now displays at a 16:9 - aspect ratio instead of collapsing to zero height, the card container no - longer expands to fill its parent, the missing end margin on the card is - applied, bottom spacing on buttons is no longer cut off, and the image is - properly clipped to the card's rounded corners. - -- **Removed insecure `AES/CBC/PKCS5Padding` encryption**: `IterableDataEncryptor` - now exclusively uses `AES/GCM/NoPadding`. The legacy CBC algorithm was only - used on Android versions below KitKat (API 19), which have been unsupported - since `minSdkVersion` was raised to 21 in SDK 3.5.12. No data migration is - required. - -### Upgrading to 3.7.0 - -[Version 3.7.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.7.0) -introduces two opt-in improvements: an automatic JWT-refresh-and-retry flow for the -offline event queue, and new callbacks for tracking embedded message sync results. -No application code changes are required to upgrade—both improvements are opt-in. - -#### Opt-in: Auto-retry for JWT failures in offline event processing - -When offline event processing is enabled and a queued API call returns a 401 -JWT error, the SDK can now automatically: - -1. Pause processing of authenticated tasks in the offline queue. -2. Refresh the JWT via your registered `IterableAuthHandler`. -3. Retry the failed task with the new token. - -Unauthenticated endpoints (such as `disableDevice`, `mergeUser`, and -`trackConsent`) continue to be processed while authentication is paused, so -unrelated traffic isn't blocked behind a stale token. - -This behavior is disabled by default for existing customers. To enable it for your -project, talk to your Iterable customer success manager. No application code -changes are required once the flag is enabled—the SDK starts using the new behavior -automatically. - -#### Opt-in: Embedded messaging sync callbacks - -`IterableEmbeddedUpdateHandler` now exposes two optional callbacks— -`onEmbeddedMessagingSyncSucceeded()` and `onEmbeddedMessagingSyncFailed(reason)`— -that let your app react to embedded message syncs. Use them to stop a loading -spinner on success or to show fallback content on failure. Both methods have -default empty implementations, so existing code keeps working unchanged. - -For more information, read [Embedded Messages with Iterable's Android SDK](https://support.iterable.com/hc/articles/23061877893652#step-8-set-up-sdk-listeners). - -### Upgrading to 3.6.6 - -[Version 3.6.6](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.6) -of Iterable's Android SDK is a maintenance release. No action is required to -upgrade. - -### Upgrading to 3.6.5 - -Starting with [version 3.6.5](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.5), -the `IterableEmbeddedView` constructor is **deprecated** because it violates -Android Fragment best practices: the system can't recreate the fragment after -configuration changes or process death, which can cause crashes. - -Use the `newInstance` factory method instead: - -```kotlin -// Deprecated: -val messageView = IterableEmbeddedView(ootbType, message, config) - -// Use this instead: -val messageView = IterableEmbeddedView.newInstance(ootbType, message, config) -``` - -The old constructor still works, but it's marked as deprecated and will be -removed in a future SDK release. Update your application code now to avoid a -breaking change later. - -For more information, read [Embedded Messages with Iterable's Android SDK](https://support.iterable.com/hc/articles/23061877893652). - -### Upgrading to 3.6.4 - -[Version 3.6.4](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.4) -makes the `isIterableDeeplink` method public so you can now check whether a URL is -an Iterable deep link before handling it. The method returns `true` when the URL -matches the Iterable deep link pattern (URLs containing `/a/` in the path). - -`isIterableDeeplink` is a **static** method on `IterableApi`: - -```java -if (IterableApi.isIterableDeeplink(urlString)) { - // URL is an Iterable deep link -} -``` - -For more information about deep links in Iterable, read [Android App Links](https://support.iterable.com/hc/articles/360035127392). - -### Upgrading to 3.6.3 - -[Version 3.6.3](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.3) -of Iterable's Android SDK is a maintenance release. No action is required to -upgrade. - -### Upgrading to 3.6.2 - -[Version 3.6.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.2) -adds three opt-in capabilities. No action is required to upgrade. - -- **Background initialization to prevent ANRs**: To run SDK initialization on - a background thread (with API calls automatically queued until ready), call - the new `IterableApi.initializeInBackground()` static method instead of - `IterableApi.initialize()`: - - ```java - IterableApi.initializeInBackground(context, apiKey, config, callback); - ``` - - Use this if running initialization on the main thread is contributing to - Application Not Responding (ANR) errors during app startup. The optional - `callback` (an `IterableInitializationCallback`) is invoked when - initialization completes. - -- **`onSDKInitialized()` callback**: A new static method on `IterableApi` lets - you subscribe a callback to be notified when initialization completes. Use - it when you need to defer SDK-dependent work from multiple call sites—for - example, posting the first event only after the SDK is fully ready. - - ```java - IterableApi.onSDKInitialized(callback); - ``` - -- **`setWebViewBaseUrl()` configuration option**: A new `IterableConfig.Builder` - method that sets the base URL used by WebView-based messages (in-app - messages, inbox, and embedded messages). Set it when you self-host custom - fonts or other external resources that require CORS to load successfully in - a WebView: - - ```java - IterableConfig config = new IterableConfig.Builder() - .setWebViewBaseUrl("https://your-cdn.example.com") - .build(); - - IterableApi.initialize(context, apiKey, config); - ``` - - If not set, the base URL defaults to an empty string (the original behavior). - -### Upgrading to 3.6.1 - -Starting with [version 3.6.1](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.1), -in-app messages render edge-to-edge so they display properly on devices with notches, -cutouts, and system bars. - -By default, the SDK applies white insets to fill the area behind the system -bars. In dark-themed apps, that white can contrast sharply with your in-app -message content. - -If your app uses a dark theme, consider updating the [background overlay](https://support.iterable.com/hc/articles/360044425951#background-overlay) -on your in-app templates to a color that complements your app, and test -existing templates before publishing. - -### Upgrading to 3.6.0 - -To enable Unknown User Activation, upgrade to [version 3.6.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.0) -of Iterable's Android SDK and call `setEnableUnknownUserActivation(true)` on -`IterableConfig.Builder` before initializing the SDK. These code changes are -only required if you want to use Unknown User Activation; otherwise, no -changes are required. - -```java -IterableConfig config = new IterableConfig.Builder() - .setEnableUnknownUserActivation(true) - .build(); - -IterableApi.initialize(context, "", config); -``` - -The SDK also captures user consent on your behalf when this feature is enabled. For full -setup instructions, read [Configure the Android SDK](https://support.iterable.com/hc/articles/40078934178836) -in the Unknown User Activation documentation. - -### Upgrading to 3.5.12 - -- **Supported Android versions**: Beginning with [version 3.5.12](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.12), - Iterable's Android SDK supports Android versions 5.0 (API level 21) and - higher. - -- **Disabling encryption**: By default, encryption is enabled to securely store - sensitive user data. To disable keychain encryption, set the - `setKeychainEncryption` option to `false` when initializing the SDK: - - ```java - IterableConfig config = new IterableConfig.Builder() - .setKeychainEncryption(false) // Disable encryption for keychain storage - .build(); - - IterableApi.initialize(context, apiKey, config); - ``` - -### Upgrading to 3.5.3 - -Starting with [version 3.5.3](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.3), -Iterable's Android SDK provides more insight into JWT refresh failures, to help -you take appropriate action in your application code. - -When a JWT refresh fails (for any of various reasons), the SDK calls -`onAuthFailure(AuthFailure authFailure)` on the `IterableAuthHandler` instance -you provided to the SDK at initialization. The `AuthFailure` object provides -more information about the failure. - -`onAuthFailure(AuthFailure authFailure)` replaces `onTokenRegistrationFailed(Throwable object)`. -If you've implemented that method, you'll need to update your application code. - -For more information, see [Step 5.6.1: Register an auth handler](#step-5-6-1-register-an-auth-handler). - -### Upgrading to 3.5.2 - -When upgrading to [version 3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) -of the SDK, you can make use of the `setAuthRetryPolicy` method on `IterableConfig` -to specify: - -- The maximum number of consecutive JWT-related request failures the SDK should - allow before giving up, Defaults to 10. -- The interval between each retry attempt. Defaults to 6 seconds. -- A backoff strategy: linear or exponential. Defaults to linear. - -### Upgrading to 3.4.10 - -In Android apps with `minSdkVersion` 23 or higher ([Android 6.0](https://developer.android.com/studio/releases/platforms#6.0)) -Iterable's Android SDK now encrypts the following fields when storing them at -rest: - -- `email` — The user's email address. -- `userId` — The user's ID. -- `authToken` — The JWT used to authenticate the user with Iterable's API. - -(Note that Iterable's Android SDK does not store the last push payload at -rest—before or after this update.) - -For more information about this encryption in Iterable's Android SDK, examine -the source code for [`IterableKeychain`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi/src/main/java/com/iterable/iterableapi/IterableKeychain.kt), -a file in Iterable's Android SDK. - -This release also allows you to have your Android apps (regardless of `minSdkVersion`) -store in-app messages in memory, rather than in an unencrypted local file. -However, an unencrypted local file is still the default option. - -To store in-app messages in memory, set the `setUseInMemoryStorageForInApps(true)` -SDK configuration option (defaults to `false`): - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setUseInMemoryStorageForInApps(true).build(); -IterableApi.initialize(context, "", config); -``` - -When users upgrade to a version of your Android app that uses this version of -the SDK (or higher), and you've set this configuration option to `true`, the -local file used for in-app message storage (if it already exists) is deleted -However, no data is lost. - -#### API level 22 and lower - -If your app targets API level 23 or higher, this is a standard SDK upgrade, with -no special instructions. - -If your app targets an API level less than 23, you'll need to make the following -changes to your project (which allow your app to build, even though it won't -encrypt data): - -1. In `AndroidManifest.xml`, add `` - -2. In your app's `app/build.gradle`: - - Add `multiDexEnabled true` to the `default` object, under `android`. - - Add `implementation androidx.multidex:multidex:2.0.1` to the `dependencies`. - -### Upgrading to 3.4.0 - -- Starting with version [3.4.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.4.0) - of Iterable's Android SDK, you'll need to declare the URL protocols that - the SDK should expect to see on incoming links (and then handle as needed). For - more information, read about [Step 5.3: Set allowed URL protocols](#step-5-3-set-allowed-url-protocols), - above. - -- Version 3.4.0 changes two static methods on the `IterableApi` class, `handleAppLink` - and `getAndTrackDeepLink`, to instance methods. To call these methods, you'll - need to first grab an instance of the `IterableApi` class by calling - `IterableApi.getInstance()`. For example, `IterableApi.getInstance().handleAppLink(...)`. - -### Upgrading to 3.3.1 - -To resolve a breaking change introduced in Firebase Cloud Messaging -[version 22.0.0](https://firebase.google.com/support/release-notes/android#messaging_v22-0-0), -[version 3.3.1](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.3.1) -of Iterable's Android SDK bumps the minimum required version of its -Firebase Android dependency to [20.3.0](https://firebase.google.com/support/release-notes/android#messaging_v20-3-0). - -If upgrading to version 3.3.1 causes your app to crash on launch, or your build -to fail, add the following lines to your app's `build.gradle` file: - -```groovy -android { - ... - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - ... -} -``` - -### Upgrading to 3.2.0 - -[Versions 3.2.0 and higher](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.2.0) -depend on the [AndroidX](https://developer.android.com/jetpack/androidx) support -libraries. To use these versions, you'll need to [migrate your app to use AndroidX](https://developer.android.com/jetpack/androidx/migrate). - -### Upgrading from a version prior to 3.1.0 - -- In-app messages - - - `spawnInAppNotification` - - The `spawnInAppNotification` method is no longer needed and will fail to - compile. The SDK now displays in-app messages automatically. There is no need - to poll the server for new messages. - - - Handling manually - - To control when in-app messages display (rather than displaying them - automatically), set `IterableConfig.inAppHandler` (an `IterableInAppHandler` - object). From its `onNewInApp` method, return `InAppResponse.SKIP`. - - To get the queue of available in-app messages, call - `IterableApi.getInstance().getInAppManager().getMessages()`. Then, call - `IterableApi.getInstance().getInAppManager().showMessage(message)` to show a - specific message. - - - Custom actions - - This version of the SDK reserves the `iterable://` URL scheme for - Iterable-defined actions handled by the SDK and the `action://` URL scheme for - custom actions handled by the mobile application's custom action handler. - - If you are currently using the `itbl://` URL scheme for custom actions, the SDK - will still pass these actions to the custom action handler. However, support - for this URL scheme will eventually be removed (timeline TBD), so it is best to - move templates to the `action://` URL scheme as it's possible to do so. - -- Deep links - - - Consolidated deep link URL handling. By default, the SDK handles deep links - with the the URL handler assigned to `IterableConfig`. - - - Checking if a URL is an Iterable deep link: To check if a URL is an Iterable - deep link before handling it, use the `isIterableDeepLink` method: - - ```java - if (IterableApi.getInstance().isIterableDeepLink(urlString)) { - // URL is an Iterable deep link - } - ``` - - This method returns `true` if the URL matches the Iterable deep link pattern - (URLs containing `/a/` in the path). For more information, read - [Android App Links](https://support.iterable.com/hc/articles/360035127392). - -### Migrating from GCM to FCM - -To migrate from GCM (Google Cloud Messaging) to Firebase (Firebase Cloud Messaging) - -- Upgrade the existing Google Cloud project to Firebase. -- Update the server token in the existing GCM-based Iterable push integration, - applying the new Firebase token. -- Update the Android app to support Firebase. - -If you use the same project and integration name for Firebase Cloud Messaging, the -old tokens remain valid and you won't need to re-register existing devices. If -you're using a new project for Firebase Cloud Messaging, and have existing -devices on a GCM project with a different sender ID: - -- Updating the app will generate new tokens for users, but the old tokens remain valid. -- When migrating from one sender ID to another, when initializing Iterable's SDK, - specify `legacyGCMSenderId` on`IterableConfig`. This disables old tokens to make - sure users won't receive duplicate notifications. - -## Troubleshooting - -If you're having trouble installing or initializing the SDK, read -[Testing and Troubleshooting the Iterable SDK](https://support.iterable.com/hc/articles/360035392251). diff --git a/polished/android/configure-the-android-sdk.polished.md b/polished/android/configure-the-android-sdk.polished.md deleted file mode 100644 index 2908576..0000000 --- a/polished/android/configure-the-android-sdk.polished.md +++ /dev/null @@ -1,365 +0,0 @@ ---- -slug: configure-the-android-sdk -feature: unknown-user-activation -archetype: identity -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Configure the Android SDK -source_url: https://support.iterable.com/hc/articles/40078934178836 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/unknown-user-activation-dev/configure-the-android-sdk/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: fa441fa69f35c816affd3df2d565dbfbd2727ca3 -fetched_at: 2026-05-25T15:11:48.790Z -polished_at: 2026-08-03T20:42:14.575Z -layer: a -snippets: - - index: 0 - lang: kotlin - hash: cd5e0463538c - line_count: 6 - - index: 1 - lang: kotlin - hash: dbdbfc1dcc0d - line_count: 49 - - index: 2 - lang: kotlin - hash: c84c6ca4398d - line_count: 17 - - index: 3 - lang: kotlin - hash: d38bf7942b6b - line_count: 4 -summary: Follow these instructions to set up Iterable's Android SDK for Unknown - User Activation. For general guidance about setting up Iterable's Android SDK, - see [Iterable's Android - SDK](https://support.iterable.com/hc/articles/360035019712). ---- -# Configure the Android SDK - -Follow these instructions to set up Iterable's Android SDK for -Unknown User Activation. For general guidance about setting up Iterable's -Android SDK, see [Iterable's Android SDK](https://support.iterable.com/hc/articles/360035019712). - -> [!NOTE] -> Sample code shown in the following sections is for demonstration purposes only; -> it's not exhaustive, and it's not meant to be used directly in your Android -> app. Instead, use it as a guide to understand the various ways you'll interact -> with the SDK when setting up and using Unknown User Activation. - -## Step 1: Import SDK methods - -To use Iterable's SDK in a given file, you'll need to import various -classes. - -```kotlin -import com.iterable.iterableapi.AuthFailure -import com.iterable.iterableapi.IterableApi -import com.iterable.iterableapi.IterableConfig -import com.iterable.iterableapi.IterableUnknownUserHandler -import com.iterable.iterableapi.IterableAuthHandler -import com.iterable.iterableapi.IterableIdentityResolution -``` - -## Step 2: Initialize the SDK and set up callbacks - -Initialize the SDK and, if necessary, set up some JWT and unknown user -callbacks. - -```kotlin -// -// This example creates the IterableConfig in the main activity, but you can create it in -// another place that's convenient for your app's architecture, if necessary. -// -class MainActivity : AppCompatActivity(), IterableUnknownUserHandler, IterableAuthHandler { - - override fun onCreate(savedInstanceState: Bundle?) { - config = IterableConfig.Builder() - .setAuthHandler(this) - .setEnableUnknownUserActivation(true) - .setUnknownUserHandler(this) - .setEventThresholdLimit(100) - .setIdentityResolution(IterableIdentityResolution(true, true)) - .build() - IterableApi.initialize(this, , config) - } - - // - // Fetch a new JWT token for the current or unknown user, from your server. - // Then, return it as a string. - // - override fun onAuthTokenRequested(): String { - // ... - return "" - } - - // - // Handle failures that occur when fetching JWT tokens. - // - override fun onAuthFailure(authFailure: AuthFailure?) { - // ... - } - - // - // The SDK calls onTokenRegistrationSuccessful after onAuthTokenRequested - // returns a non-null JWT token. However, other than a null check, the SDK does not - // validate the token before calling this method. You can leave this method empty. - // - override fun onTokenRegistrationSuccessful(authToken: String?) { - } - - // - // Callback for the SDK to invoke after it creates a userId for an unknown user. - // If necessary, use this method to pass the new userId to your server. - // - override fun onUnknownUserCreated(userId: String) { - // ... - } -} -``` - -Create an `IterableConfig` object, and set the following options: - -1. Only if your Android app uses a JWT-enabled API key, set the `setAuthHandler` - field to an object that implements the `IterableAuthHandler` interface. In the - previous example, the main activity itself implements `IterableAuthHandler`. - - :::warning IMPORTANT - If you don't use a JWT-enabled API key in your Android app, do not set this - field. Setting up auth handlers when they're not needed could lead to unexpected - behavior or errors in the SDK initialization process. - ::: - - The `IterableAuthHandler` object should provide implementations for two methods, - and an empty implementation for another: - - - `onAuthTokenRequested` – The SDK calls this method when it needs a new JWT - token for the current unknown or known user. This method should fetch (from - your server) a new JWT token for a user, and then return it as a string. - `onAuthTokenRequested` is called when: - - The SDK needs a JWT token for a new unknown user. - - You identify a user by calling `setEmail` or `setUserId`. - - You update a user's email address by calling `updateEmail`. - - The current JWT token has expired, or is about to expire. - - The SDK receives a JWT-related 401 response from Iterable's API. - - `onAuthTokenRequested` can be called to fetch and return a JWT token for: - - An unknown user - `IterableApi.getInstance().getUserId()` (since, - for unknown users, the SDK always generates a `userId` — a UUID). - - A known user - `IterableAPI.getInstance().getUserId()` or - `IterableAPI.getInstance.getEmail()`, whichever value you used to - identify the user. - - - `onAuthFailure` – The SDK calls `onAuthFailure` after failing to fetch a - JWT token. The `AuthFailure` object passed to this method describes the - reason for the failure, along with other information. - - - `onTokenRegistrationSuccessful` – The SDK calls - `onTokenRegistrationSuccessful` after `onAuthTokenRequested` returns a - non-null JWT token. However, other than a null check, the SDK does not - validate the token before calling this method. You can leave this method - empty. - -2. To enable Unknown User Activation, set `setEnableUnknownUserActivation` to `true`. - This property defaults to `false`. - -3. (Optional, but strongly recommended for JWT-enabled API keys) To provide a - callback for the SDK to call after it creates a `userId` for a new unknown - user, set `setUnknownUserHandler` to an object that implements the - `IterableUnknownUserHandler` interface. - - The SDK calls the `onUnknownUserCreated` method on this object after a - visitor satisfies your project's unknown user creation criteria and the SDK - generates a `userId` for that user's new unknown user profile — but before the - SDK attempts to fetch a JWT token for the user. - - For example, you might use this method to tell your server about the - SDK-generated unknown userId, to give your server context for subsequent JWT - token requests for that same `userId` (however, to do this, you'll need to - set up an authenticated web service on your server for this method to call). - - :::warning IMPORTANT - If your app uses a JWT-enabled API key, your JWT server needs to know about - SDK-generated unknown `userId` values before it can issue tokens for them. - This callback is the mechanism for that — without it, your JWT server may - fail to issue tokens for new unknown users. - ::: - -4. Set `setEventThresholdLimit` to indicate how many of a visitor's most recent - events the SDK should save in local storage, so that they can be synced later - to Iterable when the user satisfies your profile creation criteria and - receives an unknown user profile. - - This value defaults to `100`. If a visitor triggers more than the maximum number - of events before converting to an unknown user, the first events that were - saved are the first to be deleted. - -5. `setIdentityResolution` – If necessary, use this setter to override the - SDK's default `IterableIdentityResolution` object, which specifies values for - the SDK to use when working with unknown and known users (if you don't specify - an object here, both the following properties default to `true`). - - - `replayOnVisitorToKnown` – When you identify a visitor by calling - `setEmail` or `setUserId`, this field specifies whether the SDK should - replay locally saved visitor data to their known user profile in Iterable. - Defaults to `true`. (When an unknown user profile is first created, the SDK - always replays locally saved data to that profile — this setting only - controls what happens when you identify a visitor before they receive an - unknown user profile.) - - `mergeOnUnknownToKnown` – When you identify an unknown user by calling - `setEmail` or `setUserId`, this field specifies whether the SDK should merge - the unknown user profile with the identified user profile. Defaults to - `true`. (If the identified user profile does not yet exist, a merge - operation creates it. When `mergeOnUnknownToKnown` is `false`, the new user - profile is not created until the SDK tracks a user update or an event. - Without a merge, data on the unknown user profile is lost.) - - :::tip NOTE - If you don't provide an `identityResolution` value, the SDK defaults both - `replayOnVisitorToKnown` and `mergeOnUnknownToKnown` to `true`. You can - override these fields each time you call `setEmail` and `setUserId`. - ::: - - :::tip NOTE - The Android and Web SDKs use `mergeOnUnknownToKnown`, while the iOS SDK - uses `mergeOnUnknownUserToKnown`. - ::: - -6. `setEnableForegroundCriteriaFetch` – Controls whether the SDK re-fetches - profile creation criteria when the app is foregrounded. Defaults to `true`. - Set to `false` if you only want criteria fetched on app launch. - -7. After creating `IterableConfig`, pass it to the `initialize` method on -`IterableApi`, alongside your API key. - -## Step 3: Get user consent and track local data - -Before telling the SDK to track local data about the current visitor, get their -consent to do so. Then, explicitly tell the SDK to start tracking local data. - -```kotlin -// When consent is given -IterableApi.getInstance().setVisitorUsageTracked(true) - -// When consent is revoked (clears locally stored data) -IterableApi.getInstance().setVisitorUsageTracked(false) - -// Track custom events -IterableApi.getInstance().track(eventName, dataFields) - -// Track purchase events -IterableApi.getInstance().trackPurchase(total, items) - -// Track cart update events -IterableApi.getInstance().updateCart(items) - -// Update the user profile -IterableApi.getInstance().updateUser(dataFields) -``` - -When you have user consent, call `setVisitorUsageTracked(true)`. When you do -this: - -- The SDK fetches profile creation criteria for your Iterable project (and - refreshes them on each app launch). Criteria also refreshes on foregrounding - if `enableForegroundCriteriaFetch` is `true` (the default). -- For subsequent calls that track cart updates, purchases, custom events, and - user updates, the SDK stores data in local storage (this data isn't sent to - Iterable yet — it's only stored locally). - -If the user revokes consent, call `setVisitorUsageTracked(false)`. This clears -any locally saved visitor data, and it prevents local storage of visitor data -until `setVisitorUsageTracked(true)` is called again. - -To track events and user updates, call various methods on `IterableApi`, as -shown above. - -> [!WARNING] -> Calling `setVisitorUsageTracked(true)` clears any previously stored local visitor -> data (events, user updates, and session data) before starting fresh tracking. -> If your app calls this method on each launch, any visitor data stored from a -> previous session that was not yet synced to Iterable will be lost. - -## Step 4: Create an unknown user profile - -If and when the visitor to your app satisfies your Iterable project's profile -creation criteria, the SDK creates an unknown user profile for them. - -1. The SDK generates a `userId` (a UUID) to identify the new unknown user profile. -2. The SDK calls `POST /api/unknownuser/events/session` to create the unknown user - profile in Iterable and adds an `unknownSession` event on the profile. -3. The SDK calls your `onUnknownUserCreated` callback, as described above. -4. If you're using a JWT-enabled API key, the SDK then calls the - `onAuthTokenRequested` method you provided, to fetch (from your server) a JWT - token for the new unknown `userId`. This is the first time the SDK will call - this method for this user. -5. The SDK replays locally saved visitor data (cart updates, purchase, user profile - updates, and custom events) to the unknown user profile in Iterable, and then - removes it from local storage. - -> [!NOTE] -> Consent tracking occurs later, after device registration completes, rather than -> as part of the `unknownSession` creation flow described above. - -> [!NOTE] -> This differs from the iOS SDK, which logs consent after fetching a JWT token, -> and from the Web SDK, which logs consent before creating the -> `unknownSession` event. - -## Step 5: Identify the user - -When you know the current user's user ID or email (depending on your Iterable -project type), provide it to the SDK by calling `setUserId` or `setEmail` on -`IterableAPI`. - -```kotlin -// Identify the user by email or userId, providing an identity resolution -// override if necessary. -IterableApi.getInstance().setEmail(email, identityResolutionOverride) -IterableApi.getInstance().setUserId(userId, identityResolutionOverride) -``` - -When you identify the user: - -**If the current user is a visitor** (a user who doesn't have an unknown profile -in Iterable, because they haven't yet satisfied your Iterable project's profile -creation criteria): - -- If `replayOnVisitorToKnown` is set to `true`, the SDK: - - Calls `onAuthTokenRequested` to fetch a JWT token for the known user profile - (if you're using a JWT-enabled API key). - - Sends visitor data from the app's local storage (user profile data and - events) to the known user profile in Iterable. Sending this data to Iterable - creates the known user profile in Iterable if it doesn't already exist. - - Clears visitor data from local storage. - - Sends future user updates and events to the known user profile to Iterable - (not local storage). - -- If `replayOnVisitorToKnown` is set to `false`, the SDK: - - Calls `onAuthTokenRequested` to fetch a JWT token for the known user profile - (if you're using a JWT-enabled API key). - - Clears visitor data from local storage, without sending it to Iterable. - - Sends future user updates and events to the known user profile in Iterable - (not to local storage). If the known user profile doesn't yet exist - in your Iterable project, these updates create it. - -**If the current user is unknown** (has an unknown user profile in Iterable): - -- If `mergeOnUnknownToKnown` is set to `true`, the SDK: - - Calls `onAuthTokenRequested` to fetch a JWT token for the known user profile - (if you're using a JWT-enabled API key). - - Calls the User Merge API to merge the unknown user profile with the known - user profile (including all data). - - If the known profile doesn't yet exist, the user ID or email of the source - profile are updated. - - The API deletes the unknown profile. - It can take a few minutes for all of the data from the unknown user profile - to appear on the known user profile. - -- If `mergeOnUnknownToKnown` is set to `false`, the SDK: - - Calls `onAuthTokenRequested` to fetch a JWT token for the known user profile - (if you're using a JWT-enabled API key). - - Does not call the User Merge API. - - Sends future user updates and events to Iterable, to the known user profile - (not to the unknown user profile). The unknown user profile remains in - Iterable. \ No newline at end of file diff --git a/polished/android/customizing-mobile-inbox-on-android.polished.md b/polished/android/customizing-mobile-inbox-on-android.polished.md deleted file mode 100644 index 62e83de..0000000 --- a/polished/android/customizing-mobile-inbox-on-android.polished.md +++ /dev/null @@ -1,684 +0,0 @@ ---- -slug: customizing-mobile-inbox-on-android -feature: mobile-inbox -archetype: feature -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Customizing Mobile Inbox on Android -source_url: https://support.iterable.com/hc/articles/360039189931 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/in-app-messages/customizing-mobile-inbox-on-android/index.md -source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 -source_sha: e156e9f1bf13e4b41507a53dc10093422c8aefac -fetched_at: 2026-08-03T20:41:29.489Z -polished_at: 2026-08-03T20:42:14.567Z -layer: a -snippets: - - index: 0 - lang: kotlin - hash: aa9bbb903b24 - line_count: 4 - - index: 1 - lang: kotlin - hash: 3a9eb47ce5a3 - line_count: 4 - - index: 2 - lang: java - hash: 6304913cdac5 - line_count: 5 - - index: 3 - lang: kotlin - hash: 24d64072269d - line_count: 3 - - index: 4 - lang: java - hash: 00a988c5c250 - line_count: 3 - - index: 5 - lang: kotlin - hash: d7b7d23c4c48 - line_count: 4 - - index: 6 - lang: java - hash: d46242e73662 - line_count: 4 - - index: 7 - lang: kotlin - hash: 689b12ae918a - line_count: 4 - - index: 8 - lang: java - hash: 584a986d1076 - line_count: 4 - - index: 9 - lang: kotlin - hash: edcae1e6e4de - line_count: 13 - - index: 10 - lang: java - hash: ef4f78408c7b - line_count: 18 - - index: 11 - lang: kotlin - hash: d121dfcd6053 - line_count: 6 - - index: 12 - lang: kotlin - hash: 6bb02c0de57c - line_count: 10 - - index: 13 - lang: java - hash: 80dd3262733b - line_count: 13 - - index: 14 - lang: kotlin - hash: 3cbc544d3efa - line_count: 8 - - index: 15 - lang: kotlin - hash: 8a0fe4b9b840 - line_count: 10 - - index: 16 - lang: java - hash: 8f7a8b76a82d - line_count: 13 - - index: 17 - lang: kotlin - hash: a28bb9940c6d - line_count: 48 - - index: 18 - lang: java - hash: 1ff85a5943c1 - line_count: 53 -summary: A [mobile inbox](https://support.iterable.com/hc/articles/217517406) - provides an app-specific place for users to save in-app messages to read - later. ---- -# Customizing Mobile Inbox on Android - -A [mobile inbox](https://support.iterable.com/hc/articles/217517406) provides an -app-specific place for users to save in-app messages to read later. - -Iterable's [Android SDK](https://support.iterable.com/hc/articles/360035019712) -includes a default user interface for a mobile inbox, and you can customize it -to match your organization's branding and styles, and to display any necessary -fields. - -This document describes different ways to customize the mobile inbox provided by -Iterable's Android SDK. - -## Setting up the mobile inbox - -Before customizing your app's mobile inbox, read -[Setting up Mobile Inbox on Android](https://support.iterable.com/hc/articles/360038744152) -to learn how to set it up and display it. - -## Sample app - -To better undersand how to customize your app's mobile inbox, take a look at the -code in the **Inbox Customization** [sample project](https://github.com/Iterable/iterable-android-sdk/tree/master/sample-apps/inbox-customization) -(found in the same GitHub repository as Iterable's Android SDK). - -## Customizing the mobile inbox - -This section describes how to customize the user interface of the mobile inbox -embedded in your Android mobile app. - -> [!NOTE] -> Some customizations require you to create a subclass of `IterableInboxFragment`, -> and others do not. - -### Empty state - -In an empty mobile inbox, you can display custom text (title and body) to help -orient your users. These values are blank by default, and they'll wrap to -multiple lines if needed. For example: - -Use this code to set this text when using the mobile inbox fragment: - -_Kotlin_ - -```kotlin -var bundle = Bundle() -bundle.putString(IterableConstants.NO_MESSAGES_TITLE,"No saved messages") -bundle.putString(IterableConstants.NO_MESSAGES_BODY, "Check again later!") -val fragment: Fragment = Fragment.instantiate(this, IterableInboxFragment::class.java.name, bundle)) -``` - -Use this code to set the text when using the mobile inbox activity: - -_Kotlin_ - -```kotlin -var intent = Intent(this.context,IterableInboxActivity::class.java) -intent.putExtra(IterableConstants.NO_MESSAGES_TITLE, "No saved messages") -intent.putExtra(IterableConstants.NO_MESSAGES_BODY, "Check again later!") -startActivity(intent) -``` - -_Java_ - -```java -startActivity( - new Intent(getApplicationContext(),IterableInboxActivity.class) - .putExtra(IterableConstants.NO_MESSAGES_TITLE,"No saved messages") - .putExtra(IterableConstants.NO_MESSAGES_BODY,"Check again later!") -); -``` - -### Message display style (popup or navigation) - -A mobile inbox can display messages as popups directly in the inbox view (the -default) or as standalone activities. To change this setting, either: - -- Set an extra for the activity's intent: - - _Kotlin_ - - ```kotlin - val intent = Intent(context, IterableInboxActivity::class.java) - intent.putExtra("inboxMode", InboxMode.ACTIVITY) - startActivity(intent) - ``` - - _Java_ - - ```java - Intent intent = new Intent(getContext(), IterableInboxActivity.class); - intent.putExtra("inboxMode", InboxMode.ACTIVITY); - startActivity(intent); - ``` - -- Pass constructor parameters to the fragment: - - _Kotlin_ - - ```kotlin - val inboxFragment = IterableInboxFragment.newInstance(InboxMode.ACTIVITY, 0) - ``` - - _Java_ - - ```java - IterableInboxFragment inboxFragment = IterableInboxFragment.newInstance(InboxMode.ACTIVITY) - ``` - -### Activity title - -When launching the mobile inbox as an activity, change the title by passing an -`activityTitle` argument in the intent: - -_Kotlin_ - -```kotlin -val intent = Intent(context, IterableInboxActivity::class.java) -intent.putExtra("activityTitle", "My Inbox") -startActivity(intent) -``` - -_Java_ - -```java -Intent intent = new Intent(getContext(), IterableInboxActivity.class); -intent.putExtra("activityTitle", "My Inbox"); -startActivity(intent); -``` - -### Inbox toolbar (SDK v3.9.0 and above) - -Starting with SDK version 3.9.0, you can display an optional toolbar above the -inbox list using `IterableInboxToolbarView`. The toolbar is off by default, -so the inbox behaves exactly as it did in previous SDK versions unless you opt -in. - -Configure the toolbar with the `InboxToolbarOption` sealed interface, which has -these options: - -- `None` (default) — No toolbar. -- `Default` — A title-only toolbar above the inbox list. -- `WithBackButton` — A title plus a back-navigation icon. By default, the back - action calls `OnBackPressedDispatcher`. To customize it, have your host - `Activity` or parent `Fragment` implement `IterableInboxToolbarBackListener`. -- `Custom(layoutRes)` — Inflates your own toolbar layout. To wire your layout to - the SDK, tag views with these reserved IDs (both are optional): - - `@id/iterable_reserved_inbox_toolbar_action` — Automatically wired to the - SDK's back handler. - - `@id/iterable_reserved_inbox_toolbar_title` — Automatically bound to the - toolbar title. - -> [!WARNING] -> When the toolbar is enabled, the host activity must use a `Theme.AppCompat` -> descendant. - -#### Configure the toolbar on the fragment - -Pass an `InboxToolbarOption` (and, optionally, a title) to -`IterableInboxFragment.newInstance(...)`: - -_Kotlin_ - -```kotlin -val inboxFragment = IterableInboxFragment.newInstance( - InboxToolbarOption.WithBackButton, - "My Inbox" -) -``` - -_Java_ - -```java -IterableInboxFragment inboxFragment = IterableInboxFragment.newInstance( - InboxToolbarOption.WithBackButton.INSTANCE, - "My Inbox" -); -``` - -#### Configure the toolbar on the activity - -When launching the inbox as an activity, set the `TOOLBAR_OPTION` and -`TOOLBAR_TITLE` intent extras: - -_Kotlin_ - -```kotlin -val intent = Intent(context, IterableInboxActivity::class.java) -intent.putExtra(IterableInboxFragment.TOOLBAR_OPTION, InboxToolbarOption.WithBackButton) -intent.putExtra(IterableInboxFragment.TOOLBAR_TITLE, "My Inbox") -startActivity(intent) -``` - -_Java_ - -```java -Intent intent = new Intent(getContext(), IterableInboxActivity.class); -intent.putExtra(IterableInboxFragment.TOOLBAR_OPTION, InboxToolbarOption.WithBackButton.INSTANCE); -intent.putExtra(IterableInboxFragment.TOOLBAR_TITLE, "My Inbox"); -startActivity(intent); -``` - -### Cell layout, colors, and font - -> [!TIP] -> In the [sample app](#sample-app), tap **Inbox with Custom Cell** to see an -> example of an inbox that uses custom cells. - -To modify the font, color or layout of inbox cells: - -1. Copy the [`iterable_inbox_item.xml`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/res/layout/iterable_inbox_item.xml) - layout file from [`iterableapi-ui`](https://github.com/Iterable/iterable-android-sdk/tree/master/iterableapi-ui/src/main/res/layout). - Give it a new name, such as `custom_inbox_item.xml`. - -2. In the new file, change the layout, colors and fonts to match your app’s - styles. - -3. Specify this layout ID when launching the activity: - - _Kotlin_ - - ```kotlin - val intent = Intent(context, IterableInboxActivity::class.java) - intent.putExtra("itemLayoutId", R.layout.custom_inbox_item) - startActivity(intent) - ``` - - _Java_ - - ```java - Intent intent = new Intent(getContext(), IterableInboxActivity.class); - intent.putExtra("itemLayoutId", R.layout.custom_inbox_item); - startActivity(intent); - ``` - -4. Alternatively, create the fragment with custom parameters: - - _Kotlin_ - - ```kotlin - val inboxFragment = IterableInboxFragment.newInstance(InboxMode.POPUP, R.layout.custom_inbox_item) - ``` - - _Java_ - - ```java - IterableInboxFragment inboxFragment = IterableInboxFragment.newInstance(InboxMode.POPUP, R.layout.custom_inbox_item); - ``` - -### Date format and visibility - -> [!TIP] -> In the [sample app](#sample-app), tap **Change Date Format** to see an -> example of an inbox that uses custom cells. - -To change the format or visibility of the date field for each message cell, -subclass `IterableInboxFragment` and set a date mapper in `onCreate`. The date -mapper takes an `IterableInAppMessage` and returns a string representing the -creation date of the message. If the date field should be blank, return `null`. - -_Kotlin_ - -```kotlin -class CustomInboxDateMapperFragment : IterableInboxFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setDateMapper { message -> - DateUtils.getRelativeTimeSpanString( - message.createdAt.time, - Date().time, - 0, - DateUtils.FORMAT_ABBREV_ALL - ) - } - } -} -``` - -_Java_ - -```java -public class CustomInboxDateMapperJavaFragment extends IterableInboxFragment implements IterableInboxDateMapper { - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setDateMapper(this); - } - - @Nullable - @Override - public CharSequence mapMessageToDateString(@NonNull IterableInAppMessage message) { - return DateUtils.getRelativeTimeSpanString( - message.getCreatedAt().getTime(), - new Date().getTime(), - 0, - DateUtils.FORMAT_ABBREV_ALL - ); - } -} -``` - -### Filtering messages - -> [!TIP] -> In the [sample app](#sample-app), tap **Filter by Message Type** or -> **Filter by Message Title** to see an example of an inbox that uses custom -> filtering. - -To filter which messages are displayed in the mobile inbox, subclass -`IterableInboxFragment` and call `setFilter` in the `onCreate` method. The filter -should take an `IterableInAppMessage` and return a boolean: `true` to show -the message, `false` otherwise. - -`IterableInboxFilter` is an interface that declares a filter method. - -_Kotlin_ - -```kotlin -class CustomInboxFilterFragment : IterableInboxFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setFilter { message -> message.customPayload?.has("price") == true } - } -} -``` - -Kotlin (alternative implementation): - -```kotlin -class CustomInboxFilterFragment : IterableInboxFragment(), IterableInboxFilter { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setFilter(this) - } - - override fun filter(message: IterableInAppMessage): Boolean { - return message.customPayload?.has("price") == true - } -} -``` - -_Java_ - -```java -public class CustomInboxFilterFragment extends IterableInboxFragment implements IterableInboxFilter { - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setFilter(this); - } - - @Override - public boolean filter(@NonNull IterableInAppMessage message) { - JSONObject payload = message.getCustomPayload(); - return payload != null && payload.has("price"); - } -} -``` - -### Sorting messages - -> [!TIP] -> In the [sample app](#sample-app), tap **Sort by Title Ascending** or -> **Sort by Date Ascending** to see an example of an inbox that changes the way -> messages are sorted. - -By default, Mobile Inbox sorts messages descending by date. However, it is -possible to sort the message order in other ways. - -To sort the messages in the mobile inbox, subclass `IterableInboxFragment` and -set a comparator in `onCreate`. `IterableInboxComparator` is a standard Java -`Comparator` interface: return a negative integer, zero, or a positive integer -when the first message is less than, equal to, or greater than the second. - -_Kotlin_ - -```kotlin -class CustomInboxComparatorFragment : IterableInboxFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setComparator { message1, message2 -> - message1.createdAt.compareTo(message2.createdAt) // Sort by creation date ascending - } - } -} -``` - -Kotlin (alternative implementation): - -```kotlin -class CustomInboxComparatorFragment : IterableInboxFragment(), IterableInboxComparator { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setComparator(this) - } - - override fun compare(message1: IterableInAppMessage, message2: IterableInAppMessage): Int { - return message1.createdAt.compareTo(message2.createdAt) // Sort by creation date ascending - } -} -``` - -```java -public class CustomInboxComparatorJavaFragment extends IterableInboxFragment implements IterableInboxComparator { - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setComparator(this); - } - - @Override - public int compare(@NonNull IterableInAppMessage message1, @NonNull IterableInAppMessage message2) { - // Sort by creation date ascending - return message1.getCreatedAt().compareTo(message2.getCreatedAt()); - } -} -``` - -### Adding fields to the inbox cell - -> [!TIP] -> In the [sample app](#sample-app), tap **Additional Fields** to see an example of -> an inbox that has additional fields on each cell. - -To add additional fields to the items displayed in the mobile inbox: - -1. Copy the [`iterable_inbox_item.xml`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/res/layout/iterable_inbox_item.xml) - layout file from [`iterableapi-ui`](https://github.com/Iterable/iterable-android-sdk/tree/master/iterableapi-ui/src/main/res/layout). - Give it a new name, such as `custom_inbox_item.xml`. - -2. Subclass `IterableInboxFragment` and set the adapter extension. The methods - are similar to the ones in a `RecyclerView` adapter, but instead of a - `RecyclerView.ViewHolder` class, the extension is a plain Java class. See - [`IterableInboxAdapterExtension`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/inbox/IterableInboxAdapterExtension.java) - for more details. - - - Return your custom layout in `getLayoutForViewType`. - - Create a static inner plain Java class for a `ViewHolderExtension`. - - Add fields referencing the new views in your layout. - - Create a constructor with calls to `findViewById` to populate those fields. - - In `createViewHolderExtension`, call your view holder extension’s - constructor and return the result. - - In `onBindViewHolder`, update the UI for the given inbox message using the - standard Iterable ViewHolder (holding references to the standard fields, like - `title`, `subtitle` and others) and your extension object (holding references - to your custom views). - -For a reference implementation, see the example in the next section. - -### Multiple cell layouts - -> [!TIP] -> In the [sample app](#sample-app), tap **Multiple Cell Types** to see an example -> of an inbox that uses multiple cell types. - -To display different inbox items with different interfaces, follow these steps: - -1. Copy the [`iterable_inbox_item.xml`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/res/layout/iterable_inbox_item.xml) - layout file from [`iterableapi-ui`](https://github.com/Iterable/iterable-android-sdk/tree/master/iterableapi-ui/src/main/res/layout). - Give it a new name, such as `custom_inbox_item.xml`. - -2. Subclass `IterableInboxFragment` and set the adapter extension. See - [`IterableInboxAdapterExtension`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/inbox/IterableInboxAdapterExtension.java) - for more details. - -3. Create integer constants for every type of cell you’re planning to have in - your custom inbox. - -4. Return those constants in `getItemViewType` by checking the inbox message - attributes. - -5. The same constants will then be passed to `getLayoutForViewType`. Use them to - return different layouts based on the view type. - -_Kotlin_ - -```kotlin -class CustomInboxFieldsFragment : IterableInboxFragment(), IterableInboxAdapterExtension { - val ITEM_TYPE_DEFAULT = 1 - val ITEM_TYPE_SALE = 2 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setAdapterExtension(this) - } - - override fun getItemViewType(message: IterableInAppMessage): Int { - if (message.customPayload?.has("price") == true) { - return ITEM_TYPE_SALE - } else { - return ITEM_TYPE_DEFAULT - } - } - - override fun getLayoutForViewType(viewType: Int): Int { - if (viewType == ITEM_TYPE_SALE) { - return R.layout.inbox_item_sale - } else { - return R.layout.inbox_item_default - } - } - - override fun createViewHolderExtension(view: View, viewType: Int): ViewHolder? { - if (viewType == ITEM_TYPE_SALE) { - return SaleViewHolder(view) - } else { - return null - } - } - - override fun onBindViewHolder(viewHolder: IterableInboxAdapter.ViewHolder, holderExtension: ViewHolder?, message: IterableInAppMessage) { - if (holderExtension is SaleViewHolder) { - holderExtension.price?.text = message.customPayload?.optString("price") - } - } - - open class ViewHolder - class SaleViewHolder(view: View) : ViewHolder() { - var price: TextView? = null - - init { - this.price = view.findViewById(R.id.price) - } - } -} -``` - -_Java_ - -```java - public class CustomInboxFieldsJavaFragment extends IterableInboxFragment implements IterableInboxAdapterExtension { - private static final int ITEM_TYPE_DEFAULT = 1; - private static final int ITEM_TYPE_SALE = 2; - - @Override - public int getItemViewType(@NonNull IterableInAppMessage message) { - JSONObject payload = message.getCustomPayload(); - if (payload != null && payload.has("price")) { - return ITEM_TYPE_SALE; - } else { - return ITEM_TYPE_DEFAULT; - } - } - - @Override - public int getLayoutForViewType(int viewType) { - if (viewType == ITEM_TYPE_SALE) { - return R.layout.inbox_item_sale; - } else { - return R.layout.inbox_item_default; - } - } - - @Nullable - @Override - public ViewHolder createViewHolderExtension(@NonNull View view, int viewType) { - if (viewType == ITEM_TYPE_SALE) { - return new SaleViewHolder(view); - } else { - return null; - } - } - - @Override - public void onBindViewHolder(@NonNull IterableInboxAdapter.ViewHolder viewHolder, @Nullable ViewHolder holderExtension, @NonNull IterableInAppMessage message) { - if (holderExtension instanceof SaleViewHolder) { - SaleViewHolder saleViewHolder = (SaleViewHolder) holderExtension; - JSONObject payload = message.getCustomPayload(); - if (payload != null) { - saleViewHolder.price.setText(payload.optString("price")); - } - } - } - - static class ViewHolder {} - static class SaleViewHolder extends ViewHolder { - private TextView price; - - SaleViewHolder(@NonNull View view) { - price = view.findViewById(R.id.price); - } - } -} -``` - -### Multiple sections - -Mobile Inbox on Android does not provide built-in support for multiple sections. diff --git a/polished/android/deep-linking-with-partners.polished.md b/polished/android/deep-linking-with-partners.polished.md deleted file mode 100644 index 6129dbe..0000000 --- a/polished/android/deep-linking-with-partners.polished.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -slug: deep-linking-with-partners -feature: deep-linking -archetype: feature -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Deep Linking With Partners -source_url: https://support.iterable.com/hc/articles/360035536251 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/deep-links/deep-linking-with-partners/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 7473a924f2b7eac5a08f7ec66c3fbf60d07089e4 -fetched_at: 2026-05-25T15:11:46.035Z -polished_at: 2026-08-03T20:42:14.572Z -layer: a -snippets: [] -summary: "Iterable supports deep linking without any third-party - integrations—and also with Branch and AppsFlyer. For more information about - these integrations, read:" ---- -# Deep Linking With Partners - -Iterable supports deep linking without any third-party integrations—and also -with Branch and AppsFlyer. For more information about these integrations, read: - -- Iterable's [Branch documentation](https://support.iterable.com/hc/articles/360018347731) -- Iterable's [AppsFlyer documentation](https://support.iterable.com/hc/articles/360024366291) - diff --git a/polished/android/embedded-messages-with-iterables-android-sdk.polished.md b/polished/android/embedded-messages-with-iterables-android-sdk.polished.md deleted file mode 100644 index 4c67603..0000000 --- a/polished/android/embedded-messages-with-iterables-android-sdk.polished.md +++ /dev/null @@ -1,829 +0,0 @@ ---- -slug: embedded-messages-with-iterables-android-sdk -feature: embedded-messaging -archetype: feature -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Embedded Messages with Iterable's Android SDK -source_url: https://support.iterable.com/hc/articles/23061877893652 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/embedded-messaging/embedded-messages-with-iterables-android-sdk/index.md -source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 -source_sha: 576150056520b366d5190411e9f28198e70bbeaf -fetched_at: 2026-08-03T20:41:31.068Z -polished_at: 2026-08-03T20:42:14.570Z -layer: a -snippets: - - index: 0 - lang: kotlin - hash: d6dd59db2737 - line_count: 18 - - index: 1 - lang: java - hash: e799bb9727c6 - line_count: 4 - - index: 2 - lang: kotlin - hash: f24beeae022b - line_count: 9 - - index: 3 - lang: java - hash: 6c022d00c7c4 - line_count: 4 - - index: 4 - lang: kotlin - hash: ee5898a06175 - line_count: 11 - - index: 5 - lang: kotlin - hash: 2c17add81ecd - line_count: 1 - - index: 6 - lang: kotlin - hash: 146b8a1a8dbf - line_count: 2 - - index: 7 - lang: kotlin - hash: b58673a8e26b - line_count: 11 - - index: 8 - lang: kotlin - hash: e0d3eab032d7 - line_count: 23 - - index: 9 - lang: kotlin - hash: b259e6a45e17 - line_count: 14 - - index: 10 - lang: kotlin - hash: a2932531b4f0 - line_count: 4 - - index: 11 - lang: kotlin - hash: 38e520b049c7 - line_count: 1 - - index: 12 - lang: kotlin - hash: 48c85c7cb874 - line_count: 2 - - index: 13 - lang: xml - hash: d9b27fbb15f5 - line_count: 12 - - index: 14 - lang: kotlin - hash: 3887b4569179 - line_count: 3 - - index: 15 - lang: kotlin - hash: b64c0dd160ee - line_count: 7 - - index: 16 - lang: kotlin - hash: a8b0449c17f6 - line_count: 13 - - index: 17 - lang: kotlin - hash: 97d5fb776fc9 - line_count: 7 - - index: 18 - lang: kotlin - hash: 1048049cb459 - line_count: 8 - - index: 19 - lang: kotlin - hash: 820e05f6b034 - line_count: 7 -summary: This article describes the steps you'll need to follow to use - Iterable's Android SDK to display embedded messages in your mobile app. ---- -# Embedded Messages with Iterable's Android SDK - -> [!NOTE] -> To add Embedded Messaging to your Iterable account, talk to your customer success -> manager. - -This article describes the steps you'll need to follow to use Iterable's Android -SDK to display embedded messages in your mobile app. - -This document describes only the steps necessary to use embedded messaging. To -learn about more SDK options, read [Iterable's Android SDK](https://support.iterable.com/hc/articles/360035019712). - -## Step 1: Coordinate with your marketing and design teams - -First, collaborate with your marketing and design teams to determine: - -- Where you'll display embedded messages in your apps (where your _placements_ go). -- Each placement's Iterable-assigned numeric ID (so you can display the right - messages in the right places). -- The data to display in each placement's messages. This determines what fields - your app will expect to find in an embedded message payload. -- The message design for each of your placements. - -## Step 2: Create a Mobile API key - -To use Iterable's Android SDK to display embedded messages, you'll need a mobile -API key. To learn how to create one, read [Creating API keys](https://support.iterable.com/hc/articles/360043464871#creating-api-keys) - -## Step 3: Install Iterable's Android SDK in your app - -To learn how to install Iterable's Android SDK in your app, read -[Iterable's Android SDK](https://support.iterable.com/hc/articles/360035019712). -Embedded Messaging is supported by versions [3.5.0+](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.0) -of Iterable's Android SDK. - -## Step 4: Configure the SDK - -Next, configure the SDK, specify a URL handler and a custom action handler, -enable embedded messaging, set allowed URL protocols, initialize the SDK with an -API key, and identify the user. - -For example: - -```kotlin -val config = IterableConfig.Builder() - .setUrlHandler(this) - .setCustomActionHandler(this) - .setEnableEmbeddedMessaging(true) - // Specify the URL schemes you're expecting to receive in the campaigns you - // send with Iterable. The SDK passes URLs with these URL schemes to your URL - // handler, which can then handle them as necessary. For example, if you - // indicate that "mycompany" is an allowed protocol, the SDK will pass URLs - // such as "mycompany://profile" to your URL handler, which can respond as - // needed. For example, for "mycompany://profile", it might deep link to - // the app's user profile screen. - .setAllowedProtocols(arrayOf("mycompany")) - .build() - -IterableApi.initialize(this, apiKey, config) - -IterableApi.getInstance().setEmail(email) -// IterableApi.getInstance().setUserId(userId) -``` - -> [!NOTE] -> If your project is hosted on [Iterable's EDC](https://support.iterable.com/hc/articles/17572750887444), -> you'll need to [configure the SDK appropriately](https://support.iterable.com/hc/articles/360035019712#step-5-2-if-necessary-configure-the-sdk-to-use-iterable-s-edc). - -### Step 4.1: Define a URL handler - -In the example SDK configuration code above, notice the call to `setUrlHandler` -on `IterableConfig`. The SDK uses the object (which must implement interface -`IterableUrlHandler`) passed to this method to handle two types of URLs: - -- Standard URLs, such as `https://example.com/product/123`. -- URLs with allowed custom URL schemes, such as `mycompany://profile`. The SDK - ignores URLs that use custom URL schemes not specified as allowed protocols - on `IterableConfig` (see above). - - -When a user clicks a button or a link on an incoming message, and the click is -associated with one of the two URL types listed above, the SDK passes that URL to -the URL handler. Then, the URL handler can handle (or not handle) it as it makes -sense. For example, it might open the link in a browser or open it as a deep link. - -The object you provide to `setUrlHandler` must implement interface -`IterableUrlHandler`, which defines this method: - -```java -boolean handleIterableURL( - @NonNull Uri uri, - @NonNull IterableActionContext actionContext -); -``` - -This method should return `true` for URLs it can handle, and `false` for URLs it -cannot handle. - -When the URL handler can't handle a given URL, the SDK checks for another -installed app that can handle that URL (for example, a web browser). If there -are no other apps that can handle the URL, the SDK does nothing. - -Here's an example implementation: - -```kotlin -override fun handleIterableURL(uri: Uri, actionContext: IterableActionContext): Boolean { - val urlString = uri.toString() - // For example, urlString might be: "mycompany://profile" - if (urlString.contains("mycompany://profile")) { - // Navigate the user to the profile page ... - return true - } - return false -} -``` - -In this sample, `handleIterableUrl` handles `mycompany://profile` URLs by -navigating the user to the user profile screen. Then, it returns `true` to -indicate that it handled the URL. - -Implementations of this method will vary, depending on your app architecture -and the URLs you need to handle. However, it's important to remember that this -method is shared across message mediums. Make sure to handle URLs you might -receive from any message type, not just embedded messages. - -### Step 4.2: Define a custom action handler - -Custom actions represent custom functionality you'd like your app to execute — -maybe a deep link, a style update, or another behavior of some kind. Custom -actions use the `action://` URL scheme. - -> [!NOTE] -> In-app messages also support `iterable://dismiss` and `iterable://delete` custom -> actions. Embedded messages do not yet support these actions. The `iterable://` -> URL scheme is reserved for Iterable-specific actions pre-defined by the SDK. -> -> For tips on alternatives to a dismiss action, read [Closing, dismissing, or hiding an embedded message](#closing-dismissing-or-hiding-an-embedded-message). - -Similar to your URL handler, the object specified on `IterableConfig` as your -custom action handler serves as your app's central handler for custom actions. -When a user clicks a button or a link associated with a custom action, this -action is passed to your custom action handler, where you can deal with it -however necessary (usually by executing custom functionality of some kind). - -Your custom action handler must implement interface `IterableCustomActionHandler`, -which defines this method: - -```java -boolean handleIterableCustomAction( - @NonNull IterableAction action, - @NonNull IterableActionContext actionContext -); -``` - -This method should return `true` when it can handle the custom action URL, and -`false` when it cannot. When it returns `false`, the custom action is dropped — -since custom actions are special, non-standard URLs, they cannot be opened by -a web browser. - -For example, here's a sample custom action handler that handles an -`action://joinClass/1` URL: - -```kotlin -override fun handleIterableCustomAction( - action: IterableAction, - actionContext: IterableActionContext -): Boolean { - // The custom action's type is stored in action.type - if (action.type?.contains("joinClass")) - // Sign the user up for a class ... - return true - } - return false -} -``` - -Implementations of this method will vary. However, be sure to account for any -specific custom actions you'll send to your app (in an embedded message, or in -any other kind of message). - -### Closing, dismissing, or hiding an embedded message - -Embedded messages don't have a native "dismiss" action like in-app messages do. -However, you can implement custom logic to control when an embedded message is -displayed to a user. Here are some strategies you can use: - -- **Set an expiration date for the campaign:** Set an expiration date for your - embedded message campaign in Iterable. Once the campaign expires, the message - is no longer displayed to users. This method does not allow end-users to dismiss - the message actively, however. - -- **Implement custom logic based on eligibility criteria:** - For more dynamic control, you can create a solution that changes a user's - eligibility criteria for an embedded message campaign. When a user's eligibility - changes such that they no longer meet the targeting criteria, the embedded message - is no longer returned in their message array the next time the page is - refreshed, or automatically via silent push. - - This approach typically involves: - - - **Updating user profile or list membership:** You can use an Iterable SDK - or API to update a user's profile fields or their membership in a list. This - update can be triggered by a user action (for example, tapping a button in your - app). - - - **Leveraging custom events and journeys:** You can track a custom event in - Iterable when a specific user action occurs (such as a `message_dismissed` - event), then use the event to trigger a journey in Iterable. Within the - journey, you can perform a user profile update or a list membership update - that removes the user from the campaign's eligibility criteria. - -Specific implementation details vary depending on your application, SDK, and -desired user experience. - -## Step 5: Enable support for push notifications in your app - -To alert your app when the signed-in user's embedded message _eligibility_ changes -(that is, when they are newly eligible, or no longer eligible, for some embedded -message campaign in your project), Iterable sends silent push notifications. - -After receiving one of these silent push notifications, the SDK refreshes its -local cache of embedded messages by re-fetching them from Iterable's API. - -To learn how to enable push notifications in your Android app, read -[Setting up Android Push Notifications](https://support.iterable.com/hc/articles/115000331943). - -## Step 6: Fetch embedded messages from Iterable - -When your app first launches, and each time it comes to the foreground, -Iterable's Android SDK automatically refresh a local, on-device cache of -embedded messages for the signed-in user. These are the messages the signed-in -user is _eligible_ to see. - -> [!NOTE] -> A user is _eligible_ for an embedded message campaign if they're selected by its -> associated _eligibility list_ (a standard dynamic list in Iterable). - -At key points during your app's lifecycle, you may want to manually refresh your -app's local cache of embedded messages. For example, as users navigate around, -on pull-to-refresh, etc. - -To refresh the local cache of embedded messages, call: - -```kotlin -IterableApi.getInstance().embeddedManager.syncMessages() -``` - -However, do not poll for new embedded messages at a regular interval. - -> [!NOTE] -> Currently, Iterable's Android SDK does not persist the embedded messages -> downloaded from the server. When your app is restarted, Iterable's Android SDK -> re-fetches the user's embedded messages from Iterable, which can cause the -> creation of multiple [`embeddedReceived`](https://support.iterable.com/hc/articles/23061677642260#embeddedreceived-events) -> events for the same message. - -To fetch embedded messages, Iterable's Android SDK calls: - -[`GET /api/embedded-messaging/messages`](https://support.iterable.com/hc/articles/204780579#get-api-embedded-messaging-messages) - -## Step 7: Track message receipt - -For each embedded message received from Iterable, Iterable's Android SDK -automatically tracks an [`embeddedReceived`](https://support.iterable.com/hc/articles/23061677642260#embeddedreceived-events) -event. Each of these events represents the download of a particular message to a -particular device — but not, necessarily, that the message was displayed or seen -by the user. - -To track message receipt, Iterable's Android SDK calls: - -[`POST /api/embedded-messaging/events/received`](https://support.iterable.com/hc/articles/204780579#post-api-embedded-messaging-events-received) - -## Step 8: Set up SDK listeners - -Now, set up listeners for the SDK to call when new embedded messages arrive -on device, to tell your views to display messages as needed. To add these -listeners, call the following methods on `IterableEmbeddedManager`: - -```kotlin -public fun addUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) -public fun removeUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) -``` - -Typically, a view that displays embedded messages adds itself as a listener -when it appears, and removes itself as a listener when it disappears. For -example, for an activity or a fragment: - -```kotlin -override fun onResume() { - super.onResume() - IterableApi.getInstance().embeddedManager.addUpdateListener(this) - // ... -} - -override fun onPause() { - super.onPause() - IterableApi.getInstance().embeddedManager.removeUpdateListener(this) - // ... -} -``` - -`IterableEmbeddedUpdateHandler`, the interface that listeners must implement, -declares these methods: - -- `fun onMessagesUpdated()` – Called by the SDK to tell your app that embedded - messages have been updated, and that you can grab the local queue and display - them. - -- `fun onEmbeddedMessagingDisabled()` – Called by the SDK when there's a failure - fetching embedded messages from the server. Use this method to hide your - embedded message display or show default content, as needed. - -- `fun onEmbeddedMessagingSyncSucceeded()` – Called when an embedded messaging - sync completes successfully. Use this method to update any loading state in - your UI, or to log that the sync finished. This method has a default empty - implementation, so overriding it is optional. - -- `fun onEmbeddedMessagingSyncFailed(reason: String?)` – Called when an embedded - messaging sync fails. The `reason` parameter contains a failure reason string, - when available (for example, a network or server error message). Use this - method to log failures, show fallback content, or surface non-sensitive error - information to your users. This method has a default empty implementation, so - overriding it is optional. - -For example, a view registered as a listener might have implementations similar -to: - -```kotlin -override fun onMessagesUpdated() { - // Fetch messages for the placement associated with the current view - val messages = embeddedManager.getMessages(placementId) - - // Show or hide messages... - // ... -} - -override fun onEmbeddedMessagingDisabled() { - // Hide embedded UI or show default content - // showFallbackContent() -} - -override fun onEmbeddedMessagingSyncSucceeded() { - // Stop loading indicators, confirm latest content is shown - // hideLoadingSpinner() -} - -override fun onEmbeddedMessagingSyncFailed(reason: String?) { - // Log or surface a non-sensitive error state - // Log.d("Embedded", "Sync failed: ${reason ?: "Unknown error"}") - // showEmbeddedErrorState() -} -``` - -> [!TIP] -> You may want to check the local list of messages right when your view appears, -> _as well as_ when the SDK calls `onMessagesUpdated`. That way, if there are -> messages already available for display when the view first appears, you can show -> them. Otherwise, you can display a loading spinner or hide the embedded message -> view altogether. - -> [!WARNING] -> The SDK does not always call `onMessagesUpdated`, -> `onEmbeddedMessagingSyncSucceeded`, or `onEmbeddedMessagingSyncFailed` on the -> main thread. To prevent crashes, make sure you're on the main thread before -> updating your app's UI to display embedded messages. - -## Step 9: Display embedded messages - -For each incoming embedded message, create a view and add it to your app's user -interface, using the fields included in the message to populate the message -content and set its styles. For example, you might use one `IterableEmbeddedMessage` -to drive the creation of a single banner message, or many of them to drive the -creation of a carousel. - -As you're setting up your embedded message views: - -- Associate each message view with its corresponding `IterableEmbeddedMessage` - object, so you have access to the underlying message (and its `messageId`) when - tracking events. -- Add click handlers where necessary, so you can handle clicks and track them in - Iterable. As messages appear and disappear, track impressions (described in - the next section). - -`IterableEmbeddedMessage` objects have various fields, corresponding to the data -included with your campaign: - -- `metadata` – Identifying information about the campaign. - - `messageId` – The ID of the message. - - `placementId` – The ID of the placement associated with the message. - - `campaignId` – The ID of the campaign associated with the message. - - `isProof` – Whether or not the campaign is a test message. - -- `elements` – What to display, and how to handle interaction. - - `title` – The message's title text. - - `body` – The message's body text. - - `mediaUrl` – The URL of an image associated with the message. - - `mediaUrlCaption` – Text description of the image. - - `defaultAction` – What to do when a user clicks on the message (outside of its buttons). - - `buttons` – Buttons to display. - - `text` – Extra data fields. Not for display. - -- `payload` – Custom JSON data included with the campaign. - -Use this data to build a custom view. Or use one the out-of-the-box views -provided by the SDK, as described below. - -> [!TIP] -> For a look at the JSON payload associated with an embedded message, see -> [`GET /api/embedded-messaging/messages`](https://support.iterable.com/hc/articles/204780579#get-api-embedded-messaging-messages). - -### Out-of-the-box views - -Iterable's Android SDK provides an `IterableEmbeddedView` class you can use to -display embedded messages as a card, a banner, or a notification. For more -information about out-of-the-box views, read [Out-of-the-Box Views for Embedded Messages](https://iterable.zendesk.com/hc/articles/23230946708244). - -You can customize out-of-the-box views, in some ways, to more closely match the -styles of your apps. - -Out-of-the-box views handle clicks, too. To do this, they automatically: - -- Pass URLs and custom actions to the URL and custom action handlers you set up - in [step 4](#step-4-configure-the-sdk). -- Track [`embeddedClick`](https://support.iterable.com/hc/articles/23061677642260#embeddedclick-events) - events. - -> [!NOTE] -> When using out-of-the-box views to display embedded messages, you'll still need to -> manually track sessions and impressions, as described in [step 10](#step-10-track-sessions-and-impression). - -To use an out-of-the-box view, first create an `IterableEmbeddedViewConfig` object, -to declare the styles you'd like the view to use: - -```kotlin -// Grab your app's colors from wherever it makes sense. -val config = IterableEmbeddedViewConfig( - backgroundColor = Color.parseColor("#FFFFFF"), - borderColor = Color.parseColor("#000000"), - borderWidth = 1, - borderCornerRadius = 8f, - primaryBtnBackgroundColor = Color.parseColor("#0000FF"), - primaryBtnTextColor = Color.parseColor("#FFFFFF"), - secondaryBtnBackgroundColor = Color.parseColor("#FFFFFF"), - secondaryBtnTextColor = Color.parseColor("#000000"), - titleTextColor = Color.parseColor("#000000"), - bodyTextColor = Color.parseColor("#000000"), - imageScaleType = ImageView.ScaleType.CENTER_CROP -) -``` - -**💡 TIP — Default values (SDK v3.8.0 and above)** - -Starting with SDK version 3.8.0, all `IterableEmbeddedViewConfig` parameters -have default values, so you only need to specify the styling options you want -to customize. The example above shows every option for reference, but you can pass -just the ones you need. For example: - -```kotlin -val config = IterableEmbeddedViewConfig( - backgroundColor = Color.parseColor("#FFFFFF"), - borderCornerRadius = 8f -) -``` - -All color, border, and text-color parameters default to `null` (which falls -back to the view's built-in styling). The `imageScaleType` parameter defaults -to `ImageView.ScaleType.CENTER_CROP`. - -The `imageScaleType` parameter (added in SDK v3.8.0) controls how the image is -scaled within the 16:9 image container of `CARD` and `BANNER` views. It accepts -any standard Android [`ImageView.ScaleType`](https://developer.android.com/reference/android/widget/ImageView.ScaleType) -value (for example, `CENTER_CROP`, `FIT_CENTER`, or `FIT_XY`). The -`NOTIFICATION` view type does not display an image, so this parameter has no -effect on that view type. - -Then, when it's time to display a message, create the `IterableEmbeddedView` -using the `newInstance` factory method: - -```kotlin -val messageView = IterableEmbeddedView.newInstance(ootbType, message, config) -``` - -This method takes three parameters: - -- A value of type `IterableEmbeddedViewType`, an `enum` with three constants: - - `BANNER` - - `CARD` - - `NOTIFICATION` -- The `IterableEmbeddedMessage` to display. -- The `IterableEmbeddedViewConfig` created above (optional — pass `null` or omit - to use default styles). - -**⚠️ WARNING — Migration from older SDK versions** - -In SDK versions prior to 3.6.5, `IterableEmbeddedView` was instantiated using a -constructor: - -```kotlin -// Old approach (deprecated — unstable): -val messageView = IterableEmbeddedView(ootbType, message, config) -``` - -This constructor has been **deprecated** because it violates Android Fragment -best practices: the system cannot recreate the fragment after configuration -changes or process death, causing crashes. - -**Use the `newInstance` factory method instead**, as shown above. The old -constructor still works but is marked as deprecated and will be removed in a -future SDK release. - -Then, add the view to your layout. It's important to fully specify the size -of the view, with minimum dimensions, as described in -[Out-of-the-Box Views for Embedded Messages](https://iterable.zendesk.com/hc/articles/23230946708244). - -For example, one way to add an out-of-the-box layout to a view is to swap it -with a "placeholder" view that's already there. For example, this layout -contains a placeholder `FrameLayout`: - -```xml - - - - - - -``` - -When it's time to display the embedded message, you could replace the placeholder -view using code such as: - -```kotlin -val ft: FragmentTransaction = childFragmentManager.beginTransaction() -ft.replace(R.id.placeholder_view, messageView) -ft.commit() -``` - -With this approach, the placeholder view might be an empty state that can remain -in place when there are no embedded messages available, or a view with a loading -spinner, or something similar. - -However, this is just an example. The specific approach you'll take when adding -an out-of-the-box view to your app depends on your app architecture. - -## Step 10: Track sessions and impression - -A _session_ is a period of time when a user is on a screen or page that can -display embedded messages. - -Every session can have many _impressions_. An impression represents the -on-screen appearances of a given embedded message, in context of a session. Each -impression tracks: - -- The total number of times a message appears during a session. -- The total amount of time that message was visible, across all its appearances - in the session. - -To help you track message sessions and impressions (views of a message), -Iterable's Android SDK provides a session manager. Sessions and impressions are -tracked in Iterable as [`embeddedSession`](https://support.iterable.com/hc/articles/23061677642260#embeddedsession-events) -and [`embeddedImpression`](https://support.iterable.com/hc/articles/23061677642260#embeddedimpression-events) -events. - -### Step 10.1: Start a session - -When a user comes to a screen or page in your app where embedded messages are -displayed (in one or more placements), use the session manager to start a -session. To start a session, call: - -```kotlin -// When the screen that displays your embedded message is displayed or comes to -// the foreground -IterableApi - .getInstance() - .embeddedManager - .getEmbeddedSessionManager() - .startSession() -``` - -### Step 10.2: Start and pause impressions - -As messages appear or disappear during an ongoing embedded message session, use -the session manager to track message impressions. - -The session manager tracks the total number of times each message appears during -a session, and the total amount of time each message is on-screen across all -those appearances. To start and pause impressions, call: - -```kotlin -// When a message appears, start an impression (associating it with a placement) -IterableApi - .getInstance() - .embeddedManager - .getEmbeddedSessionManager() - .startImpression(messageId, placementId) - -// When a message disappears… -IterableApi - .getInstance() - .embeddedManager - .getEmbeddedSessionManager() - .pauseImpression(messageId) -``` - -> [!NOTE] -> An embedded message can disappear and reappear many times during a session. -> Because of this, when an embedded message disappears, you don't _end_ its -> impression — you _pause_ it. Then, you start the impression again if and when -> the message reappears. In other words, you start and end sessions, but you start -> and _pause_ impressions. - -Be sure to start and pause impressions when your app goes to and from the -background, too. - -### Step 10.3: End the session, saving impression data to Iterable - -When a user leaves a screen in your app where embedded messages are displayed, -use the session manager to end the active session. This causes the SDK to send -session and impression data back to the server. - -To end a session, call: - -```kotlin -// When a screen that displays embedded messages is dismissed -// or goes to the background -IterableApi - .getInstance() - .embeddedManager - .getEmbeddedSessionManager() - .endSession() -``` - -To track sessions and impressions, Iterable's Android SDK calls: - -[`POST /api/embedded-messaging/events/session`](https://support.iterable.com/hc/articles/204780579#post-api-embedded-messaging-events-session) - -## Step 11: Handle clicks - -Finally, configure your app to handle clicks on embedded messages. When a user -clicks a link or a button, it can be associated with: - -- A standard URL (for example, `https://example.com/products/1`). -- A custom URL scheme (for example, `mycompany://profile`). -- A custom action (for example, `action://joinClass/1`). - -Above, you set up a [URL handler](#step-4-1-define-a-url-handler) for handling -standard URLs and custom URL schemes, and a [custom action handler](#step-4-2-define-a-custom-action-handler) -for handling custom action URLs. - -Now, just listen for clicks, and then tell the SDK to invoke the URL or the -custom action handler (depending on the type of URL that was clicked). - -### Click handling for out-of-the-box views - -If you're using an out-of-the-box view to display embedded messages, you can -skip this step. Out-of-the-box views automatically: - -- Pass standard URLs and URLs with allowed custom URL schemes to the URL handler - you defined above. If your URL handler can't handle the URL (returns `false`), - the SDK attempts to open it with another app that can handle it (for example, - a web browser). -- Pass custom actions to your custom action handler. If your custom action handler - can't handle the custom action (returns `false`), the custom action is dropped - (since it can't be handled by a web browser). -- Track [`embeddedClick`](https://support.iterable.com/hc/articles/23061677642260#embeddedclick-events) - events. - -### Click handling for custom embedded message views - -However, if you're using custom views instead of out-of-the-box views, you'll -need to handle clicks. As you instantiate custom embedded message views in your -Android app: - -- Add click handlers to the message's buttons and links. Do this however it makes - sense for your app. -- Set up a default click handler, to handle clicks on the message but outside - of any particular button or link. - -In your click handlers: - -- Execute any necessary custom application logic (update the UI if needed, etc.). - -- Call `handleEmbeddedClick` on `IterableEmbeddedManager`. This method forwards - URLs and custom actions to the handlers you defined above. For example: - - ```kotlin - IterableApi - .getInstance() - .embeddedManager - .handleEmbeddedClick( - message, - buttonIdentifier, - clickedUrl - ) - ``` - -- Track an [`embeddedClick`](https://support.iterable.com/hc/articles/23061677642260#embeddedclick-events) - event: - - ```kotlin - IterableApi - .getInstance() - .trackEmbeddedClick( - embeddedMessage, - buttonId, - clickedUrl - ) - ``` - - To track clicks, Iterable's Android SDK calls: - - [`POST /api/embedded-messaging/events/click`](https://support.iterable.com/hc/articles/204780579#post-api-embedded-messaging-events-click) - -## Want to learn more? - -- [Out-of-the-Box Views for Embedded Messages](https://iterable.zendesk.com/hc/articles/23230946708244). -- The [GitHub repository for Iterable's Android SDK](https://github.com/iterable/iterable-android-sdk). - In particular, these files: - - [`IterableEmbeddedManager.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi/src/main/java/com/iterable/iterableapi/IterableEmbeddedManager.kt) - - [`EmbeddedSessionManager.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi/src/main/java/com/iterable/iterableapi/EmbeddedSessionManager.kt) - - [`IterableEmbeddedPlacement.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi/src/main/java/com/iterable/iterableapi/IterableEmbeddedPlacement.kt) - - [`IterableEmbeddedView.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedView.kt) - - [`IterableEmbeddedViewConfig.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewConfig.kt) - - [`IterableEmbeddedViewType.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewConfig.kt) - diff --git a/polished/android/identifying-the-user.polished.md b/polished/android/identifying-the-user.polished.md deleted file mode 100644 index 429f386..0000000 --- a/polished/android/identifying-the-user.polished.md +++ /dev/null @@ -1,293 +0,0 @@ ---- -slug: identifying-the-user -feature: user-profiles -archetype: identity -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Identifying the User -source_url: https://support.iterable.com/hc/articles/360035402531 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/managing-user-profiles/identifying-the-user/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: ced31ca29ce63d634a0c4691277a114ed3f0ceb9 -fetched_at: 2026-05-25T15:11:46.888Z -polished_at: 2026-08-03T20:42:14.572Z -layer: a -snippets: - - index: 0 - lang: swift - hash: 51664525a08b - line_count: 1 - - index: 1 - lang: objectivec - hash: 464d8c5bbd9b - line_count: 1 - - index: 2 - lang: java - hash: f62eb7b2d632 - line_count: 1 - - index: 3 - lang: swift - hash: 3ce9888afa7f - line_count: 1 - - index: 4 - lang: objectivec - hash: 698cc56d370e - line_count: 1 - - index: 5 - lang: java - hash: 7f2a539b2cba - line_count: 1 - - index: 6 - lang: swift - hash: 13a24526b5c0 - line_count: 3 - - index: 7 - lang: objectivec - hash: de80933755af - line_count: 3 - - index: 8 - lang: java - hash: 6914174d75e4 - line_count: 10 - - index: 9 - lang: swift - hash: 4d2b03419ee5 - line_count: 10 - - index: 10 - lang: objectivec - hash: adb45d47e2cc - line_count: 9 - - index: 11 - lang: java - hash: 1b7317c09349 - line_count: 17 -summary: "The Iterable SDK can identify users by email or user ID. To identify a - user, you'll need to do two things: specify an email address or user ID, and - then call `updateUser` to send that value to Iterable." ---- -# Identifying the User - -The Iterable SDK can identify users by email or user ID. - -## Overview - -To identify a user, you'll need to do two things: specify an email address or -user ID, and then call `updateUser` to send that value to Iterable. - -## Identifying the user by email - -Email is typically used as the key identify within Iterable because it tracks -across devices to aggregate data between a user's phones, tablet, web-site -activity or even IoT (Internet-of-Things) device. - -Iterable also allows for multi-dimensional nested data types, meaning you can -organize your data based on relevant key values. - -Once you have an email address or user ID for your app's current user, set -`IterableAPI.email` or `IterableAPI.userId`. For example: - -> [!WARNING] -> - Don't specify both `email` and `userId` in the same call, as they will be -> treated as different users by the SDK. Only use one type of identifier, email -> or user ID, to identify the user. -> - Your app will not be able to receive push notifications until you set one -> of these values - -Add this line of code as soon as you know the user's email: - -_Swift_ - -```swift -IterableAPI.email = "user@example.com" -``` - -_Objective-C_ - -```objectivec -IterableAPI.email = @"user@example.com"; -``` - -_Java_ - -```java -IterableApi.getInstance().setEmail("user@example.com"); -``` - -> [!NOTE] -> Please see [User Profile Fields Used by Iterable](https://support.iterable.com/hc/articles/217744303) -> to ensure you don't add data that is specific to set fields in Iterable. - -## Identifying the user by user ID - -Iterable can also identify user by user ID. However, all things being equal, it -is recommended to use email as the key identifier. - -_Swift_ - -```swift -IterableAPI.userId = "user123" -``` - -_Objective-C_ - -```objectivec -IterableAPI.userId = @"user123"; -``` - -_Java_ - -```java -IterableApi.getInstance().setUserId("user123"); -``` - -You can add the `userId` identifier at any point after the `IterableConfig()` -call. - -> [!NOTE] -> - When creating a user by `userId` in an email-based project, Iterable automatically -> assigns the user a `@placeholder.email` email address (a user identifier, not -> a way to message the user). For example, if you create a user with a `userId` -> of `user123`, their user profile will also receive an `email` such as -> `user123+147178873@placeholder.email`. To learn more, read [Handling Anonymous Users](https://support.iterable.com/hc/articles/208499956). -> - A user ID can be up to 52 characters long. - -## Identifying the device of the user - -For Iterable to send push notifications to an iOS device, it must know the -unique token assigned to that device by Apple or Android. - -Iterable uses silent push notifications to tell iOS apps when to fetch new -in-app messages from the server. Because of this, your app must register for -remote notifications with Apple even if you do not plan to send it any push -notifications. - -### Auto push registration - -`IterableConfig.autoPushRegistration` determines whether or not the SDK will: - -- Automatically register for a device token when the SDK is given a new - email address or user ID. Disable the device token for the previous user when - a new user logs in. - -If `IterableConfig.autoPushRegistration` is **true** (the default value): - -- Setting `IterableAPI.email` or `IterableAPI.userId` causes the SDK to - automatically call the `registerForRemoteNotifications()` method on - UIApplication and pass the resulting device token to the - `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)` method on - the app delegate. - -If `IterableConfig.autoPushRegistration` is **false**: - -- After setting `IterableAPI.email` or `IterableAPI.userId`, you must - manually call the `registerForRemoteNotifications()` method on - `UIApplication`. This will fetch the device token from Apple and pass it to - the `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)` method - on the app delegate. - -### Send the device token to Iterable - -> [!NOTE] -> - Iterable users the device token to send push notifications and in-app -> messages. -> - Users do not need to opt in to Apple push notifications for Iterable to get -> the device token. - -To send the device token to Iterable and save it on the current user's -profile, call `IterableAPI.register(token:)` from the -`application(_:didRegisterForRemoteNotificationsWithDeviceToken:)` method on -`UIApplicationDelegate`. For example: - -_Swift_ -```swift -func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - IterableAPI.register(token: deviceToken) -} -``` - -_Objective-C_ -```objectivec -- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { - [IterableAPI registerToken:deviceToken]; -} -``` - -_Java_ -```java -// Iterable SDK automatically registers the push token with Iterable -// whenever setEmail or setUserId is called. - -// If you want to trigger token registration manually, first disable automatic -// registration by calling setAutoPushRegistration(false) on IterableConfig.Builder when initializing the SDK. - -// Then call registerForPush whenever you want to register the token: - -// Only use this line if you want to register push manually. -// IterableApi.getInstance().registerForPush(); -``` - -Once you register for push, you should see the device on the user's Iterable profile. -Access a user's profile in Iterable by navigating to **Audiences > Contact Lookup**. - -## Updating email and user ID - -Use the following code to update both an `email` and a `userId`: - -_Swift_ - -```swift -IterableAPI.updateEmail("", - onSuccess: { _ in - IterableAPI.updateUser( - [JsonKey.userId: ""], - mergeNestedObjects: false, - onSuccess: { _ in - // this needs to be set after both calls have finished successfully - IterableAPI.userId = "" - }, onFailure: nil) -}, onFailure: nil) -``` - -_Objective-C_ - -```objectivec -[IterableAPI updateEmail:@"" - onSuccess:^(NSDictionary * _Nullable data) { - [IterableAPI updateUser:@{@"userId": @""} - mergeNestedObjects:NO - onSuccess:^(NSDictionary * _Nullable data) { - // this needs to be set after both calls have finished successfully - IterableAPI.userId = @""; - } onFailure:nil]; -} onFailure:nil]; -``` - -_Java_ - -```java -IterableApi.getInstance().updateEmail("newEmail@somewhere.com", new IterableHelper.SuccessHandler() { - @Override - public void onSuccess(JSONObject data) { - JSONObject userIDobj = new JSONObject(); - try { - userIDobj.put("userId", "newUserId"); - } catch (JSONException e) { - e.printStackTrace(); - } - IterableApi.getInstance().updateUser(userIDobj); - } -}, new IterableHelper.FailureHandler() { - @Override - public void onFailure(String reason, JSONObject data) { - Log.e(TAG, reason) - } -}); -``` - -## Next steps - -To troubleshoot user identification, see [Testing and Troubleshooting User Profiles](https://support.iterable.com/hc/articles/360035079512). - -If you have already identified the user for your use case, see [Updating User Profiles](https://support.iterable.com/hc/articles/360035402611) -and [User Update Recommendations](https://support.iterable.com/hc/articles/360035031532). diff --git a/polished/android/in-app-messages-on-android.polished.md b/polished/android/in-app-messages-on-android.polished.md deleted file mode 100644 index 1e6edba..0000000 --- a/polished/android/in-app-messages-on-android.polished.md +++ /dev/null @@ -1,300 +0,0 @@ ---- -slug: in-app-messages-on-android -feature: in-app-messages -archetype: feature -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: In-App Messages on Android -source_url: https://support.iterable.com/hc/articles/360035537231 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/in-app-messages/in-app-messages-on-android/index.md -source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 -source_sha: 65412ae773eaca59531243ff4775fd4457b5b608 -fetched_at: 2026-08-03T20:41:28.539Z -polished_at: 2026-08-03T20:42:14.565Z -layer: a -snippets: - - index: 0 - lang: java - hash: 8db83c22f9a3 - line_count: 18 - - index: 1 - lang: java - hash: 41e8696aa8cc - line_count: 9 - - index: 2 - lang: java - hash: bcfe488ba3d1 - line_count: 10 - - index: 3 - lang: java - hash: a903f600bb27 - line_count: 4 - - index: 4 - lang: kotlin - hash: 28725ee7538d - line_count: 1 - - index: 5 - lang: java - hash: 265c585feb80 - line_count: 1 - - index: 6 - lang: kotlin - hash: 34caae496955 - line_count: 1 - - index: 7 - lang: java - hash: e4f4f768e9bb - line_count: 1 -summary: By default, when an in-app message arrives from the server, the SDK - automatically shows it if the app is in the foreground. If an in-app message - is already showing when the new message arrives, the new message will be shown - 30 seconds after the currently displayed in-app message closes ([see how to - change this default value - below](#changing-the-display-interval-between-in-app-messages)). Once an… ---- -# In-App Messages on Android - -## Default behavior - -By default, when an in-app message arrives from the server, the SDK automatically -shows it if the app is in the foreground. If an in-app message is already showing -when the new message arrives, the new message will be shown 30 seconds after the -currently displayed in-app message closes ([see how to change this default value below](#changing-the-display-interval-between-in-app-messages)). -Once an in-app message is shown, it will be "consumed" from the server queue and -removed from the local queue as well. There is no need to write any code to get this -default behavior. - -## Overriding whether to show or skip a particular in-app message - -An incoming in-app message triggers a call to the `onNewInApp` method of -`IterableConfig.inAppHandler` (an `IterableInAppHandler` object). To override the -default behavior, set `inAppHandler` in `IterableConfig` to a custom class that -overrides the `onNewInApp` method. `onNewInApp` should return `InAppResponse.SHOW` -to show the incoming in-app message or `InAppResponse.SKIP` to skip showing it. - -> [!TIP] -> To determine the priority of an `IterableInAppMessage` object, call its -> `getPriorityLevel` method. You can use this priority to help determine whether or -> not to display it. - -```java -class MyInAppHandler implements IterableInAppHandler { - @Override - public InAppResponse onNewInApp(IterableInAppMessage message) { - if (/* add conditions here */) { - return InAppResponse.SHOW; - } else { - return InAppResponse.SKIP; - } - } -} - -// ... - -IterableConfig config = new IterableConfig.Builder() - .setPushIntegrationName("myPushIntegration") - .setInAppHandler(new MyInAppHandler()) - .build(); -IterableApi.initialize(context, "", config); -``` - -### Deferring an in-app message (SDK v3.10.0 and above) - -Starting with SDK version 3.10.0, `onNewInApp` can also return -`InAppResponse.DEFER`. Unlike `SKIP`, which permanently drops the message, -`DEFER` keeps the message pending so the SDK reconsiders it on a later display -pass (for example, on the next foreground, sync, or newly arrived message). This -is useful for temporary, per-message suppression—for example, while a splash -screen is showing. - -```java -class MyInAppHandler implements IterableInAppHandler { - @Override - public InAppResponse onNewInApp(IterableInAppMessage message) { - if (appIsShowingSplashScreen()) { - return InAppResponse.DEFER; - } - return InAppResponse.SHOW; - } -} -``` - -Once your app is ready to display in-app messages, call -`resumeInAppDisplay()` (see [Pausing the display of in-app messages](#pausing-the-display-of-in-app-messages-sdk-v3-2-6-and-above)) -to re-check pending messages immediately, instead of waiting for the next -foreground or sync trigger. - -> [!NOTE] -> In Kotlin, add a `DEFER` branch to any exhaustive `when` expression over -> `InAppResponse`. - -## Getting the local queue of in-app messages - -The SDK keeps the local in-app message queue in sync by checking the server queue -every time the app goes into foreground, and via silent push messages that arrive -from Iterable servers to notify the app whenever a new in-app message is added to -the queue. - -To access the in-app message queue, call -`IterableApi.getInstance().getInAppManager().getMessages()`. To show a message, call -`IterableApi.getInstance().getInAppManager().showMessage(message)`. - -```java -// Get the in-app messages list -IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); -List messages = inAppManager.getMessages(); - -// Show an in-app message -inAppManager.showMessage(message); - -// Show an in-app message without consuming (not removing it from the queue) -inAppManager.showMessage(message, false) - -``` - -## Handling in-app message buttons and links - -The SDK handles in-app message buttons and links as follows: - -- If the URL of the button or link uses the `action://` URL scheme, the SDK passes - the action to `IterableConfig.customActionHandler.handleIterableCustomAction()`. If - `customActionHandler` (an `IterableCustomActionHandler` object) has not been set, - the action will not be handled. - - For the time being, the SDK will treat `itbl://` URLs the same way as `action://` - URLs. However, this behavior will eventually be deprecated (timeline TBD), so it's - best to migrate to the `action://` URL scheme as it's possible to do so. - -- The `iterable://` URL scheme is reserved for action names predefined by - the SDK. If the URL of the button or link uses an `iterable://` URL known - to the SDK, it will be handled automatically and will not be passed to the - custom action handler. For example, buttons or links with URL `iterable://dismiss` - dismiss an in-app message and create in-app click and in-app close events. - -- The SDK passes all other URLs to `IterableConfig.urlHandler.handleIterableURL()`. - If `urlHandler` (an `IterableUrlHandler` object) has not been set, or if it - returns `false` for the provided URL, the URL will be opened by the system - (using a web browser or other application, as applicable). - -## Configuring how in-app messages interact with system bars (SDK v3.8.0 and above) - -By default, Iterable's Android SDK draws in-app messages edge-to-edge, with -content extending behind the status bar and navigation bar. This was the only -behavior in SDK versions 3.6.1 through 3.7.0. - -Starting with SDK version 3.8.0, you can configure how in-app messages interact -with system bars by setting `IterableInAppDisplayMode` on `IterableConfig`. -This setting applies globally to all in-app messages displayed by the SDK. - -The available modes are: - -- `FORCE_EDGE_TO_EDGE` (default) — Forces in-app messages to display - edge-to-edge, drawing content behind the status bar and navigation bar. - This preserves the behavior of previous SDK versions. -- `FOLLOW_APP_LAYOUT` — Matches the host app's current layout configuration. - If your app is edge-to-edge, in-app messages display edge-to-edge; if your - app respects system bar bounds, so do in-app messages. -- `FORCE_FULLSCREEN` — Hides the status bar entirely while in-app messages - are displayed. Uses the legacy `FLAG_FULLSCREEN` on API levels below 30 and - `WindowInsetsController` on API 30 and above. -- `FORCE_RESPECT_BOUNDS` — Ensures in-app content never draws behind the - status bar or navigation bar, keeping UI elements like the close button - always accessible. - -To configure the display mode, call `setInAppDisplayMode()` on -`IterableConfig.Builder`: - -```java -IterableConfig config = new IterableConfig.Builder() - .setInAppDisplayMode(IterableInAppDisplayMode.FOLLOW_APP_LAYOUT) - .build(); -IterableApi.initialize(context, "", config); -``` - -> [!TIP] -> If the close button (or other interactive elements) in your fullscreen in-app -> messages is being obscured by the status bar, switch to `FOLLOW_APP_LAYOUT` -> or `FORCE_RESPECT_BOUNDS`. - -## Displaying in-app messages in Jetpack Compose apps (SDK v3.9.0 and above) - -In SDK versions before 3.9.0, displaying an in-app message required a -`FragmentActivity`, because the SDK rendered in-app messages using a `Fragment`. -This meant that apps built fully with [Jetpack Compose](https://developer.android.com/compose) -(and without the Android fragment framework) couldn't display in-app messages. - -Starting with SDK version 3.9.0, the SDK can also render in-app messages using a -`Dialog`-based renderer (`IterableInAppDialogNotification`) that doesn't require -a `FragmentActivity`. When the current activity is a `FragmentActivity`, the SDK -continues to use the existing `Fragment`-based rendering; when it isn't (for -example, a Compose-based `ComponentActivity`), the SDK falls back to the -`Dialog`-based renderer. As a result, in-app messages now display correctly in -apps built fully with Jetpack Compose. - -No code changes are required to take advantage of this—just upgrade to SDK -version 3.9.0 or later. (The host must still be an `Activity`.) - -> [!WARNING] -> This Compose compatibility applies to **in-app message rendering** only. Iterable's -> mobile inbox UI is still fragment-based and requires a `FragmentActivity` host. For -> more information, see [Setting up Mobile Inbox on Android](https://support.iterable.com/hc/articles/360038744152#displaying-the-mobile-inbox). - -## Changing the display interval between in-app messages - -To customize the time delay between successive in-app messages, set -`inAppDisplayInterval` on `IterableConfig` to an appropriate value in -seconds. The default value is 30 seconds. - -## Pausing the display of in-app messages (SDK v3.2.6 and above) - -In certain areas of your app, you may want to prevent interruptions. To pause the -display of in-app messages, call the following method: - -_Kotlin_ - -```kotlin -IterableApi.getInstance().inAppManager.setAutoDisplayPaused(true) -``` - -_Java_ - -```java -IterableApi.getInstance().getInAppManager().setAutoDisplayPaused(true); -``` - -With this done, the app will not automatically display new in-app messages. -However, it will keep the local queue of in-app messages in sync. - -> [!TIP] -> While in-app message display has been paused, you can still call the `showMessage` -> method on `IterableInAppManager` to manually display messages. - -To resume the display of in-app messages from your app's queue, call -`setAutoDisplayPaused(false)`. - -### Re-evaluating pending in-app messages on demand (SDK v3.10.0 and above) - -Starting with SDK version 3.10.0, you can call `resumeInAppDisplay()` to prompt -the SDK to re-evaluate pending in-app messages once your app is ready to display -them—for example, after a splash screen is dismissed, or after you deferred a -message by returning `InAppResponse.DEFER` from `onNewInApp` (see -[Deferring an in-app message](#deferring-an-in-app-message-sdk-v3-10-0-and-above)). -Without this call, the SDK re-checks pending messages only on its own triggers -(foreground, sync, or a newly arrived message). - -_Kotlin_ - -```kotlin -IterableApi.getInstance().inAppManager.resumeInAppDisplay() -``` - -_Java_ - -```java -IterableApi.getInstance().getInAppManager().resumeInAppDisplay(); -``` - -`resumeInAppDisplay()` is independent of `setAutoDisplayPaused(boolean)`: if -automatic display is paused, this call won't show anything (and logs a warning) -until you also call `setAutoDisplayPaused(false)`. diff --git a/polished/android/push-notification-overview.polished.md b/polished/android/push-notification-overview.polished.md deleted file mode 100644 index 6397f72..0000000 --- a/polished/android/push-notification-overview.polished.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -slug: push-notification-overview -feature: push-notifications -archetype: feature -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Push Notification Overview -source_url: https://support.iterable.com/hc/articles/360035079872 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/push-notifications/push-notification-overview/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 3306f88835e0c1b30e4b4020287d772cfd93ba1e -fetched_at: 2026-05-25T15:11:44.170Z -polished_at: 2026-08-03T20:42:14.569Z -layer: a -snippets: [] -summary: To alert users about updates, offers, content, and other information - that may be immediately relevant, it often makes sense to contact them on - their mobile devices. Iterable can send push notification campaigns to your - users, allowing you reach them when and where it matters. ---- -# Push Notification Overview - -To alert users about updates, offers, content, and other information that may be -immediately relevant, it often makes sense to contact them on their mobile -devices. Iterable can send push notification campaigns to your users, allowing -you reach them when and where it matters. - -You can also use Iterable to send silent push notifications, which wake your app -in the background to perform a task — update a badge count, download some data, -or trigger a request for an app store review. When you send a silent push -notification, you'll define a JSON payload to send along with it, and your app's -code can use this data as needed. - -## Iterable's mobile SDKs - -To make it easy to work with push notification campaigns sent from Iterable, -consider using Iterable's mobile SDKs: - -- [iOS SDK](https://support.iterable.com/hc/articles/360035018152), -- [Android SDK](https://support.iterable.com/hc/articles/360035019712), -- [React Native SDK](https://support.iterable.com/hc/articles/360045714132) - -These SDKs help with: - -- Capturing device tokens and sending them to Iterable. -- Handling rich push notifications, which contain images and action buttons. -- Deep link handling. -- Capturing events (when push notifications are delivered, when users click - on them, etc.). - -## Next steps - -If you're a marketer, work with your mobile engineers to implement the -technical setup described in [Setting up iOS Push Notifications](https://support.iterable.com/hc/articles/115000315806) -and [Setting up Android Push Notifications](https://support.iterable.com/hc/articles/115000331943). - -Then, read [Sending Push Notifications](https://support.iterable.com/hc/articles/115000379086) -to learn how to send a push notification campaign. diff --git a/polished/android/setting-up-android-push-notifications.polished.md b/polished/android/setting-up-android-push-notifications.polished.md deleted file mode 100644 index 41f48f6..0000000 --- a/polished/android/setting-up-android-push-notifications.polished.md +++ /dev/null @@ -1,473 +0,0 @@ ---- -slug: setting-up-android-push-notifications -feature: push-notifications -archetype: feature -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Setting up Android Push Notifications -source_url: https://support.iterable.com/hc/articles/115000331943 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/push-notifications/setting-up-android-push-notifications/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 45fa32087746810e08046766184fd4c2eb1acc94 -fetched_at: 2026-05-25T15:11:43.429Z -polished_at: 2026-08-03T20:42:14.568Z -layer: a -snippets: - - index: 0 - lang: java - hash: 0450a5b49d68 - line_count: 12 - - index: 1 - lang: xml - hash: 9933acdf8d98 - line_count: 1 - - index: 2 - lang: xml - hash: 5fc786ff2c94 - line_count: 1 - - index: 3 - lang: xml - hash: 153ef25ae0f8 - line_count: 1 - - index: 4 - lang: xml - hash: 7f16912083a3 - line_count: 1 - - index: 5 - lang: xml - hash: 87c8a09ec24e - line_count: 1 - - index: 6 - lang: xml - hash: 6ea88888f62c - line_count: 1 - - index: 7 - lang: xml - hash: 5fc786ff2c94 - line_count: 1 - - index: 8 - lang: xml - hash: 77a45aeee481 - line_count: 1 -summary: This guide describes the technical setup necessary to use Iterable to - send push notifications to Android devices. ---- -# Setting up Android Push Notifications - -This guide describes the technical setup necessary to use Iterable to send push -notifications to Android devices. - -> [!TIP] -> For details about push notifications on Android, read Google's [Notifications Overview](https://developer.android.com/guide/topics/ui/notifiers/notifications) -> document. - -## Configuring Iterable to send Android push notifications - -Follow the steps below to configure Iterable to send Android push notifications: - -### Step 1: Set up Firebase for your Android app - -To send Android push notifications, Iterable uses [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) -(FCM). To learn how to set up Firebase for your Android app, read Google's -[Add Firebase to your Android project](https://firebase.google.com/docs/android/setup) -document. - -### Step 2: Create a mobile app in Iterable - -In your Iterable projects, you can define _mobile apps_ that correspond to your -real-world mobile apps. Each mobile app in Iterable stores details about an -app's name, identifier, platform, and store URL. - -To create a mobile app in Iterable: - -1. Navigate to **Settings > Apps and Websites**. - -2. Click **New app or website**. This brings up the **New app or website** page: - - ![Creating a new app or website](https://support.iterable.com/hc/article_attachments/26154725921684/new-app-or-website.png "Creating a new app or website") - -3. For **Name**, enter the name of your app. For example, `Example Push Test`. - -4. For **Platform**, select **Android**. - -5. For **Package name**, enter your app's [package name](https://developer.android.com/studio/build/application-id). - For example, `com.example.pushtest`. - -6. (Optional) For **Store URL**, enter your app's Play Store URL. - -7. Click **Create app**. You'll be taken to the app's details page: - - ![App details page](https://support.iterable.com/hc/article_attachments/26154740912660/app-details.png "App details page") - -### Step 3: Add a push integration to the mobile app - -A _push integration_ stores the credentials Iterable uses to authenticate with -FCM when sending push notifications. Push integrations are stored in the mobile -apps you create in your Iterable project. - -To create a push integration: - -1. Create a service account in Firebase. -2. Create a JSON private key that Iterable can use to authenticate with Firebase. -3. Configure the push integration in Iterable. -4. Send a test push notification. - -> [!WARNING] -> Firebase Cloud Messaging (FCM) has [deprecated their legacy HTTP APIs](https://firebase.google.com/docs/cloud-messaging/migrate-v1) -> and replaced them with the FCM HTTP v1 API. If you have any existing push -> integrations that use legacy FCM HTTP API credentials, you'll need to update -> them. For more information, read [Migrating to the FCM HTTP v1 API for Push Notifications](https://support.iterable.com/hc/articles/26143681644564). - -#### Step 3.1: Create a service account in Firebase - -First, create a [service account](https://firebase.google.com/support/guides/service-accounts) -in Firebase and give it the necessary permissions to send push notifications -on your behalf. - -> [!TIP] -> Read Google's [Create service accounts](https://cloud.google.com/iam/docs/service-accounts-create) -> document for more information. - -To create and configure a service account: - -1. Sign in your Firebase account. Open the Firebase project that contains the - app to which you'll send push notifications. - - ![Choosing an app in Firebase](https://support.iterable.com/hc/article_attachments/26154748820628/firebase-mobile-app-tile.png "Choosing an app in Firebase") - -2. Click the gear button (in the upper-left). From the menu, choose - **Project settings**. - - ![Opening project settings in Firebase](https://support.iterable.com/hc/article_attachments/26154726130452/firebase-project-settings.png "Opening project settings in Firebase") - -3. Navigate to the **Service accounts** tab and click **Manage service account permissions**. - - ![Managing service accounts in Firebase](https://support.iterable.com/hc/article_attachments/26154757616916/firebase-service-accounts.png "Managing service accounts in Firebase") - -5. To create a new service account, click **Create Service Account**. - - ![Creating a service account in Firebase](https://support.iterable.com/hc/article_attachments/26154741147156/firebase-create-service-account.png "Creating a service account in Firebase") - -6. Under **Service account details**, enter a name, account ID, and description. - Then, click **Create and Continue**. - - ![Specifying details for a new Firebase service account](https://support.iterable.com/hc/article_attachments/26154741205524/firebase-service-account-details.png "Specifying details for a new service account in Firebase") - -7. Under **Grant this service account access to project**, select role - [**Firebase Cloud Messaging API Admin**](https://cloud.google.com/iam/docs/understanding-roles#firebasecloudmessaging.admin). - Or, choose a custom role that has the [`cloudmessaging.messages.create`](https://firebase.google.com/docs/projects/iam/permissions#messaging) - permission. Then, click **Continue**. - - ![Giving a Firebase service account access to a project](https://support.iterable.com/hc/article_attachments/26154757815828/firebase-service-account-grant.png "Giving a Firebase service account access to a project") - - :::tip TIP - To learn about creating and managing custom Identity and Access Management (IAM) - roles in Google Cloud, read Google's [Create and manage custom roles](https://cloud.google.com/iam/docs/creating-custom-roles) - document. - ::: - -8. Under **Grant users access to this service account**, leave both fields blank. - - ![Granting user access to a Firebase service account](https://support.iterable.com/hc/article_attachments/26154737500308/firebase-service-account-user-access.png "Granting user access to a Firebase service account") - -9. Click **Done**. You'll be taken back to the **Service Accounts** page. - -#### Step 3.2: Create and download an FCM private key (JSON) - -Now, create and download a JSON private key that Iterable can use to -authenticate with FCM when sending push notifications. - -1. On the **Service Accounts** page, in the row for your new service account, - click the three dots in the **Actions** column. Choose **Manage keys**. - - ![Managing keys for a Firebase service account](https://support.iterable.com/hc/article_attachments/26154741351444/firebase-service-account-manage-keys.png "Managing keys for a Firebase service account") - -2. Click **Create new key**, and then choose **JSON**. - - ![Creating a JSON private key for a Firebase service account](https://support.iterable.com/hc/article_attachments/26154749445396/firebase-service-account-json.png "Creating a JSON private key for a Firebase service account") - -3. Click **Create**. This downloads the JSON private key to your machine. - -#### Step 3.3: Configure the push integration in Iterable - -Back in Iterable, configure your mobile app's push integration: - -1. Navigate to **Settings > Apps and Websites** and open your app. - -2. In the **Push** sections, under **Integrations**, in the **Firebase** row, - click **Configure**. A **Configure Firebase integration** window will appear. - - ![Configuring a Firebase push integration](https://support.iterable.com/hc/article_attachments/26154758067092/configure-firebase-integration.png "Configuring a Firebase push integration") - -3. For **Firebase Cloud Messaging (FCM) type**, choose between: - - - **Notification messages** - The Firebase SDK handles incoming push - notifications. Generally, you should only select this option if you aren't - using Iterable's Android SDK. - - **Data notifications** - Iterable's SDK handles incoming push notifications. - If you're using Iterable's Android SDK, this is usually the right option. - - :::tip TIP - For more information about these options, read [About FCM messages](https://firebase.google.com/docs/cloud-messaging/concept-options), - from Google. - ::: - -4. Upload the JSON file you created above. - -5. Click **Save**. As soon as you do, Iterable starts using these new credentials - to send push notifications to your Android app. - -#### Step 3.4: Send a test push notification - -Finally, a **Test Firebase integration** window appears. Send a test push -notification to make sure that everything works as expected (assuming that you've -set your app up to receive push notifications, as described further down in this -document). - -![Sending a test message](https://support.iterable.com/hc/article_attachments/26154737764756/test-firebase-integration.png "Sending a test message") - -1. Grab a test user's device token: - - - Visit **Audience > User Lookup**. - - Look up an internal user by `email` or `userId` (whatever makes sense in - your project). - - Navigate to the **User fields** tab. - - Open the `devices` array. - - From an Android device where `appPackageName` is your app's package name, - and `endpointEnabled` is `true`, copy the `token` field. - -2. In the **Test Firebase integration** window: - - - Enter the device token and a message. - - Click **Send test**. - - The device should receive the push notification message. If not, check the - configuration of the service account in Firebase, fix as necessary, and try - again. If you're still having trouble, contact Iterable support. - -### Step 4: Install Iterable's Android SDK in your mobile app - -To learn how to install Iterable's Android SDK, read about Iterable's -[Android SDK](https://support.iterable.com/hc/articles/360035019712). - -> [!NOTE] -> You can receive Iterable push notifications without setting up the Android -> SDK. To do so: -> -> - Set up your Android app as described in Google's -> [Set up a Firebase Cloud Messaging client app on Android](https://firebase.google.com/docs/cloud-messaging/android/client) document. -> - Call [`POST /api/users/registerDeviceToken`](https://support.iterable.com/hc/articles/204780579#post-api-users-registerdevicetoken) -> each time the app opens. -> - Call [`POST /api/users/disableDevice`](https://support.iterable.com/hc/articles/204780579#post-api-users-disabledevice) -> each time the user signs out of the app -> - Track push notification opens by calling [`POST /api/events/trackPushOpen`](https://support.iterable.com/hc/articles/204780579#post-api-events-trackpushopen). - -### Step 5: Additional SDK configuration - -#### Handling Firebase push messages and tokens - -The SDK automatically adds a `FirebaseMessagingService` to the app manifest, so -you don't have to do any extra setup to handle incoming push messages. - -If your application implements its own `FirebaseMessagingService`, make sure you -forward `onMessageReceived` and `onNewToken` calls to -`IterableFirebaseMessagingService.handleMessageReceived` and -`IterableFirebaseMessagingService.handleTokenRefresh`, respectively: - -```java -public class MyFirebaseMessagingService extends FirebaseMessagingService { - - @Override - public void onMessageReceived(RemoteMessage remoteMessage) { - IterableFirebaseMessagingService.handleMessageReceived(this, remoteMessage); - } - - @Override - public void onNewToken(String s) { - IterableFirebaseMessagingService.handleTokenRefresh(); - } -} -``` - -To handle silent push notifications, use a custom `FirebaseMessagingService`. - -> [!WARNING] -> The step above is mandatory for handling multiple push providers. - -Note that `FirebaseInstanceIdService` is deprecated and replaced with -`onNewToken` in recent versions of Firebase. - -#### Disabling push notifications to a device - -When a user logs out, you typically want to disable push notifications to -that user/device. This can be accomplished by calling `disablePush`. Please -note that it will only attempt to disable the device if you have previously -called `registerForPush`. - -In order to re-enable push notifications to that device, simply call -`registerForPush` as usual when the user logs back in. - -### Step 6: Use Iterable to send a test Android push notification - -To send a test Android push notification: - -1. In Iterable, navigate to **Audience > User Lookup** and enter the user's - `email` or `userId`. - - - In the `devices` array, find an object where `appPackageName` corresponds - to your app's package name, and `endpointEnabled` is `true`. - - Copy that object's `token`. - -2. Navigate to **Settings > Apps and Websites**. - -3. Click the mobile app to which you'd like to send a push notification. - -4. In the **Integrations** section, click **Test Push**. You'll see a - **Send Test Push** window: - - ![Testing a Firebase integration](https://support.iterable.com/hc/article_attachments/26154737837332/test-push.png "Testing a Firebase integration") - -3. Enter the device token you found above, and specify a test message. - -5. Click **Send test**. - -Monitor the recipient's device to verify that the push notification arrives. - -## Android 13: Push notification permissions - -To learn about the `POST_NOTIFICATIONS` permission introduced in Android 13, -which allows you to prompt users for permission to send push notifications, -check out [this information about Android 13](https://support.iterable.com/hc/articles/360057572291#android-13). - -## Customizing Android push notifications - -The following sections describe the technical setup necessary for various -Android push notification customizations in Iterable. - -For marketer-specific information about how to configure these features in -Iterable when sending a campaign, read [Creating a Push Notification Campaign](https://support.iterable.com/hc/articles/115000379086). - -### Notification color - -Add this line to `AndroidManifest.xml` to specify the notification color: - -```xml - -``` -where `#FFFFFF` can be replaced with a hex representation of a color of your -choice. In stock Android, the notification icon and action buttons will be -tinted with this color. - -You can also use a color resource: - -```xml - -``` - -### Channel name - -Since Android 8.0, Android requires apps to specify a channel for every -notification. Iterable uses one channel for all notification; to customize the -name of this channel, add this to `AndroidManifest.xml`: - -```xml - -``` - -You can also use a string resource to localize the channel name: - -```xml - -``` - -### Badging / dots - -Since Android 8.0, apps can indicate that they've received a notification by -displaying a dot (badge) on their icon. By default, Iterable's Android SDK -displays these badges. However, you can explicitly enable or disable them in -`AndroidManifest.xml`: - -```xml - -``` - -### Sounds - -To add sound to Android push notifications sent with Iterable, follow these -instructions: - -1. Put the necessary sound files in the Android project's **res/raw** folder. - - :::warning IMPORTANT - - Sound file names should be lowercase and should not have any special - characters. - - Take a look at Android's [documentation about supported media formats](https://developer.android.com/guide/topics/media/media-formats#audio-formats). This documentation is not - specific about which formats work for push notifications, so it's best to - test as necessary. - ::: - -2. Navigate to **Content > Templates** and open the push notification template. - -3. Click **Edit details** and scroll down. - -4. Enter the path to the custom sound file in the **Custom sound** field. - - ![Custom sound field](https://support.iterable.com/hc/article_attachments/9922751249556/push-custom-sound.png "Custom sound field") - - :::tip NOTES - - To use the default push notification sound, set this field to `default`. - - Whether or not a device plays the sound or vibrates depends on the - user's [device settings](https://support.google.com/android/answer/9082609). - ::: - -### Deep links - -Iterable push notification templates make it possible to set deep link -URLs for iOS and Android. - -To learn more about using Iterable's Android SDK to handle deep links, -read [Android App Links](https://support.iterable.com/hc/articles/360035127392). - -If your app is not using Iterable's Android SDK, it can still handle a deep -link contained in an Iterable push notification. Iterable provides the deep -link URL in the `defaultAction` object included in the notification's -payload. When this object's `type` field is set to `openUrl`, the `data` -field will contain the deep link URL. - -After a user has opened a tapped on a push notification to open the app, -use the `getPayloadData` method on `IterableApi` to access the notification -payload. - -### Background color - -To set the background color of a push notification, update the -`AndroidManifest.xml` file: - -```xml - -``` - -`#FFFFFF` can be replaced with any hex color. In stock Android, the -notification icon and action buttons will be tinted with this color. - -Alternatively, you can also use a color resource: - -```xml - -``` - -### Custom icons - -By default, push notifications display the application icon. To use a -different icon, place the image resource inside your app's **res/drawable** -directory. Then, edit `AndroidManifest.xml`, adding the following line: - -```xml - -``` - -In this case, `ic_notification_icon` is the name of the notification icon. - -Alternatively, call `setNotificationIcon(String iconName)` to use the custom -icon, referencing the image asset by name and without a file extension. diff --git a/polished/android/setting-up-mobile-inbox-on-android.polished.md b/polished/android/setting-up-mobile-inbox-on-android.polished.md deleted file mode 100644 index 36a6e21..0000000 --- a/polished/android/setting-up-mobile-inbox-on-android.polished.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -slug: setting-up-mobile-inbox-on-android -feature: mobile-inbox -archetype: feature -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Setting up Mobile Inbox on Android -source_url: https://support.iterable.com/hc/articles/360038744152 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/in-app-messages/setting-up-mobile-inbox-on-android/index.md -source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 -source_sha: 9b54bece973b76efd0e1eaec0494b0e9d2c2af7c -fetched_at: 2026-08-03T20:41:29.000Z -polished_at: 2026-08-03T20:42:14.566Z -layer: a -snippets: [] -summary: Apps using version 3.2.0 and later of Iterable's [Android - SDK](https://support.iterable.com/hc/articles/360035019712) can save in-app - messages to an inbox. This inbox displays a list of saved in-app messages and - allows users to read them at their convenience. The SDK provides a default - user interface for the inbox, which can be customized to match your brand's - styles. This document describes how… ---- -# Setting up Mobile Inbox on Android - -Apps using version 3.2.0 and later of Iterable's [Android SDK](https://support.iterable.com/hc/articles/360035019712) -can save in-app messages to an inbox. This inbox displays a list of saved in-app -messages and allows users to read them at their convenience. The SDK provides a -default user interface for the inbox, which can be customized to match your -brand's styles. This document describes how Android developers can add Iterable's -Mobile Inbox functionality to your mobile app. - -To learn how to use Iterable to send in-app messages that users can save to a -mobile inbox, read [Sending In-App Messages](https://support.iterable.com/hc/articles/360034903151). - -> [!WARNING] -> Versions 3.2.0 and higher of Iterable's Android SDK depend on the -> [AndroidX](https://developer.android.com/jetpack/androidx) support libraries. -> [Migrate your app to use AndroidX](https://developer.android.com/jetpack/androidx/migrate) -> before using version 3.2.0 or higher. - -## Installing Iterable's Android SDK - -To add a mobile inbox to your Android app, first install Iterable's -[Android SDK](https://support.iterable.com/hc/articles/360035019712). - -## Displaying the mobile inbox - -> [!WARNING] -> Iterable's mobile inbox UI is fragment-based: `IterableInboxFragment` requires a -> `FragmentManager`, so its host must be a `FragmentActivity` (or its descendant, -> `AppCompatActivity`). Compose-first apps often use a plain `ComponentActivity` as -> their host, which has no `FragmentManager`—hosting the inbox fragment there -> crashes when the fragment is attached. If your app is Compose-first, change the -> host activity's base class to `FragmentActivity` / `AppCompatActivity` before -> adding the inbox. (Iterable's Android SDK doesn't currently provide a -> Compose-native inbox.) -> -> Note that this requirement applies to the inbox UI only. Starting with SDK -> version 3.9.0, in-app messages themselves render correctly in Compose-first -> apps. For more information, see [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231#displaying-in-app-messages-in-jetpack-compose-apps-sdk-v3-9-0-and-above). - -In your app, show the mobile inbox when the user selects a specific tab or taps -a particular button. - -- To show the inbox as a tab: - - When using a [Navigation](https://developer.android.com/guide/navigation) - component, add the `IterableInboxFragment` to the navigation graph XML: - - ```xml - - ``` - -- To show the inbox as a separate activity in response to a button tap: - - Use the provided `InboxActivity` wrapper: - - _Kotlin_ - - ```kotlin - startActivity(Intent(context, IterableInboxActivity::class.java)) - ``` - - _Java_ - - ```java - startActivity(new Intent(getContext(), IterableInboxActivity.class)); - ``` - -## Syncing a mobile inbox across many devices - -Iterable's iOS and Android SDKs automatically sync a mobile inbox across all the -devices on which a user has logged in to your app. Additionally, they sync the -read state for each message. - -If you're not using one of Iterable's mobile SDKs: - -- To determine whether or not a message has been read, examine its `read` field, - as returned by [`GET /api/inApp/getMessages`](https://support.iterable.com/hc/articles/204780579#get-api-inapp-getmessages). -- To mark a message as read, call [`POST /api/events/trackInAppOpen`](https://support.iterable.com/hc/articles/204780579#post-api-events-trackinappopen). - -> [!NOTE] -> For more information about cross-device read state syncing, see: -> - Iterable's Android SDK, [v3.2.12 release notes](https://support.iterable.com/hc/articles/360027543332#_3-2-12) -> - Iterable's iOS SDK, [v6.2.21 release notes](https://support.iterable.com/hc/articles/360027798391#_6-2-21) - -## Customizing the mobile inbox - -To learn how to customize the mobile inbox in an Android app, read -[Customizing Mobile Inbox on Android](https://support.iterable.com/hc/articles/360039189931). diff --git a/polished/android/setting-up-unknown-user-activation.polished.md b/polished/android/setting-up-unknown-user-activation.polished.md deleted file mode 100644 index 3aa5d52..0000000 --- a/polished/android/setting-up-unknown-user-activation.polished.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -slug: setting-up-unknown-user-activation -feature: unknown-user-activation -archetype: identity -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Setting up Unknown User Activation -source_url: https://support.iterable.com/hc/articles/40078870805396 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/unknown-user-activation-dev/setting-up-unknown-user-activation/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 45d0ae07bce89a4a4156c3b5e46f6bd4136d41b2 -fetched_at: 2026-05-25T15:11:49.365Z -polished_at: 2026-08-03T20:42:14.575Z -layer: a -snippets: [] -summary: Unknown User Activation makes it possible to learn about, message, and - develop relationships with unidentified users of your mobile app and website. - Before you begin setting it up, learn more about how it works in [Unknown User - Activation Overview](https://support.iterable.com/hc/articles/38755339847188). ---- -# Setting up Unknown User Activation - -Unknown User Activation makes it possible to learn about, message, and develop -relationships with unidentified users of your mobile app and website. Before you -begin setting it up, learn more about how it works in [Unknown User Activation Overview](https://support.iterable.com/hc/articles/38755339847188). - -## API keys and JWT considerations - -Unknown User Activation is available for use with Iterable's SDKs. At this time, -Iterable's API does not include endpoints for Unknown User Activation. - -When using Iterable's iOS, Android, or Web SDKs, remember that: - -- You'll need an Iterable API key (of type Web or Mobile). -- For Iterable's Web SDK, JWT-enabled API keys are required. For Iterable's iOS - and Android SDKs, they're optional but recommended. - -JWT-enabled API keys are more secure than standard API keys because they require -your server to authorize each user with a custom JWT token. Iterable's SDKs must -request these tokens from your server for each user; Iterable can't generate them -for you. - -To use JWT-enabled API keys, set up a web service that Iterable's SDKs can call -to get JWT tokens—this is required for all JWT-enabled API keys. Make sure your -JWT server can issue tokens for SDK-generated unknown `userId` values, as well as -for known `userId` or `email` values. You might also consider having your web -service notify you whenever the SDK creates a new unknown user ID, so your -server can issue JWT tokens for those users. - -> [!WARNING] -> Never embed a Server-side API key in a mobile or web app–they can be accessed by -> malicious users to access your project data. - -## Setup tasks -To set up Unknown User Activation, complete the following tasks: - -### Update your JWT server to issue JWT tokens for unknown users - -When implementing Unknown User Activation, if your mobile and web apps use -JWT-enabled API keys (required for Iterable's Web SDK, recommended for -Iterable's iOS and Android SDKs), update your JWT server to return JWT tokens -for unknown users created by Iterable's SDKs. - -To identify an unknown user, Iterable's SDKs can only access the user ID that was -generated by the SDK when the unknown user profile was created — not the -server-assigned placeholder email for that same user. (In email-based projects, -which identify users by email, Iterable assigns each unknown user a placeholder -email address based on, but different from, the SDK-created user ID. However, the -SDK doesn't have access to this email.) - -Because of this, your JWT server must be able to issue JWT tokens as follows: - -- For apps and websites associated with userID-based and hybrid Iterable - projects, your JWT server must be able to issue JWT tokens for: - - SDK-generated `userId` values (UUID values; to authenticate unknown users). - - `userId` values you assign to user profiles when converting them from unknown - to known (to authenticate known users). - -- For email-based projects, your JWT server must be able to issue JWT tokens - for: - - SDK-generated `userId` values (UUID values; to authenticate unknown users - created by the SDK). - - Email addresses you assign to user profiles when converting them from - unknown to known (to authenticate known users). - -> [!NOTE] -> For more information about using JWT-enabled API keys with Unknown -> User Activation, see [API Keys and JWT Considerations](#api-keys-and-jwt-considerations). - -### Define profile creation criteria - -Before enabling Unknown User Activation in your mobile apps and website, make -sure your marketing team has set up test criteria in Iterable that tells the -SDKs when to create unknown user profiles in Iterable. When visitors do not meet -the defined criteria (or if there is no criteria defined), they will continue to -be stored only on-device or in-browser, and you won't see them in your Iterable -project. - -At runtime, Iterable's iOS, Android, and Web SDKs fetch these criteria, evaluate -them, and create unknown user profiles for visitors who satisfy their -requirements. For example, a simple criteria might specify to create unknown -user profiles for visitors with a `viewedProduct` event. - -The Web SDK fetches these criteria after you enable local data tracking (in -response to user consent), and then refreshes them on each page refresh. The iOS -and Android SDKs also fetch these criteria when you enable local data tracking -(again, in response to user consent) or foregrounding, and then again each time the -customer launches the app. - -### Install or update an Iterable SDK - -To use Unknown User Activation with Iterable's SDKs, you'll need to upgrade to one -of the following SDK versions. - -- For the iOS SDK, use version 6.6.0 of Iterable's iOS SDK. - See [Configure the iOS SDK](https://support.iterable.com/hc/articles/40078945603860). - -- For the Android SDK, use version 3.6.0 of Iterable's Android SDK. - See [Configure the Android SDK](https://support.iterable.com/hc/articles/40078934178836). - -- For the Web SDK, use version 2.2.0 of Iterable's Web SDK. - See [Configure the Web SDK](https://support.iterable.com/hc/articles/40079019401236). diff --git a/polished/android/tracking-events-with-iterables-mobile-sdks.polished.md b/polished/android/tracking-events-with-iterables-mobile-sdks.polished.md deleted file mode 100644 index b55e60c..0000000 --- a/polished/android/tracking-events-with-iterables-mobile-sdks.polished.md +++ /dev/null @@ -1,338 +0,0 @@ ---- -slug: tracking-events-with-iterables-mobile-sdks -feature: event-tracking -archetype: feature -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Tracking Events and Purchases with Iterable's Mobile SDKs -source_url: https://support.iterable.com/hc/articles/360035395671 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/event-tracking/tracking-events-with-iterables-mobile-sdks/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 0dbb170bfbd574bf405b33990d2c288ad8dcd153 -fetched_at: 2026-05-25T15:11:48.241Z -polished_at: 2026-08-03T20:42:14.574Z -layer: a -snippets: - - index: 0 - lang: swift - hash: aba2ff83f3b2 - line_count: 4 - - index: 1 - lang: objectivec - hash: 402d8334d532 - line_count: 1 - - index: 2 - lang: java - hash: 97bbfc4400f7 - line_count: 4 - - index: 3 - lang: javascript - hash: 98414a37c7ae - line_count: 7 - - index: 4 - lang: swift - hash: f8155acfa770 - line_count: 29 - - index: 5 - lang: objectivec - hash: fb2e320df19b - line_count: 27 - - index: 6 - lang: java - hash: e3a8fdcd2031 - line_count: 33 -summary: Iterable's mobile SDKs can track _events_, which correspond to actions - taken by your app's users. Events can be related to messages you've sent (for - example, a user opening an in-app message) or to a particular feature or piece - of content in your app (for example, a user signing up for a new account). ---- -# Tracking Events and Purchases with Iterable's Mobile SDKs - -Iterable's mobile SDKs can track _events_, which correspond to actions taken -by your app's users. Events can be related to messages you've sent (for example, -a user opening an in-app message) or to a particular feature or piece of content -in your app (for example, a user signing up for a new account). - -You can use events in segmentation and journeys to help you reach the right -users with the right content. - -> [!NOTE] -> - To reduce noise and make the data you store in Iterable as useful and -> actionable as possible, avoid the temptation to track every custom event you -> can think of. Instead, track events related to important milestones, since -> they can help you reach your customers in useful and engaging ways. -> - To learn more about keeping track of your custom event usage, read -> [Monitoring Custom Event Usage](https://support.iterable.com/hc/articles/360043366492). - -## Custom event names - -In general, it's a good idea to avoid using spaces in custom event names, since -you'll need to use a [special Handlebars syntax](https://support.iterable.com/hc/articles/36530857619348#blank-or-missing-values-in-rendered-content) -to reference a field name that contains a space. - -For example, for an event saved when a user complete's the sign-up process for -your service, `completedSignup` is a good event name but `completed signup` is -not. - -## Tracking custom events - -To use track an event using Iterable's mobile SDKs, use syntax such as: - -_Swift_ - -```swift -IterableAPI.track( - event: "customEvent", - dataFields: ["key": "value"] -) -``` - -_Objective-C_ - -```objectivec -[IterableAPI track:@"custom_event" dataFields:@{@"key": @"value"}]; -``` - -_Java_ - -```java -IterableApi.getInstance().track( - "customEvent", - dataFields -); -``` - -_JavaScript (React Native)_: - -```javascript -Iterable.trackEvent( - "completedOnboarding", - { - "includedProfilePhoto": true, - "favoriteColor": "red" - } -); -``` - -## Tracking purchase events - -To help you [track information related to purchases and revenue](https://support.iterable.com/hc/articles/205480285), -Iterable's mobile SDKs include a `trackPurchase` method. - -_Swift_ - -```swift -// Example dataFields -let dataFields: [String: Any] = [ - "Store_Address": [ - "Street1": "123 Main St", - "Street2": "Apt 1", - "City": "Iter-a-ville", - "State": "CA", - "Zip": "90210" - ] -] - -// Create an array of CommerceItem objects -let item : CommerceItem = CommerceItem( - id: "TOY1", - name: "Red Racecar", - price: 4.99, - quantity: 1, - sku: "RR123", - description: "A small, red racecar.", - url: "https://www.example.com/toys/racecar", - imageUrl: "https://www.example.com/toys/racecar/images/car.png", - categories: ["Toy", "Inexpensive"] -) - -let items = [item] - -// Make the call to Iterable's API -IterableAPI.track(purchase: 4.99, items: items, dataFields: dataFields) -``` - -_Objective-C_ - -```objectivec -// Example dataFields -NSDictionary *dataFields = @{ - @"Store_Address": @{ - @"Street1": @"123 Main St", - @"Street2": @"Apt 1", - @"City": @"Iter-a-ville", - @"State": @"CA", - @"Zip": @"90210" - } -}; - -// Create an array of CommerceItem objects -CommerceItem *item = [[CommerceItem alloc] - initWithId:@"TOY1" - name:@"Red Racecar" - price:@4.99F - quantity:1 - sku:@"RR123" - description:@"A small, red racecar" - url:@"https://www.example.com/toys/racecar" - imageUrl:@"https://www.example.com/toys/racecar/images/car.png" - categories:@[@"Toy", @"Inexpensive"]]; - -NSArray *items = @[item]; - -// Make the call to Iterable's API -[IterableAPI trackPurchase:@4.99F items:items dataFields:dataFields]; -``` - -_Java_ - -```java -// Example dataFields -JSONObject store_address = new JSONObject(); -final JSONObject dataFields = new JSONObject(); - -try { - store_address.put("Street1", "123 Main St"); - store_address.put("Street2", "Apt 1"); - store_address.put("City", "Iter-a-ville"); - store_address.put("State", "CA"); - store_address.put("Zip", "90210"); - datafields.put("dataFields", store_address); -} catch (JSONException e) { - e.printStackTrace(); -} - -// Create an array of CommerceItem objects -CommerceItem item = new CommerceItem( - "TOY1", - "Red Racecar", - 4.99, - 1, - "RR123", - "A small, red racecar", - "https://www.example.com/toys/racecar", - "https://www.example.com/toys/racecar/images/car.png", - new String[] {"Toy", "Inexpensive" } -); - -List items = new ArrayList(); -items.add(item); - -// Make the call to Iterable's API -IterableApi.getInstance().trackPurchase(new Double(4.99), items, dataFields); -``` - -## Offline events processing - -> [!TIP] -> If you'd like to use offline events processing, we'll need to enable it for -> your account. Talk to your customer success manager to get started. - -Mobile apps built with Iterable's mobile SDKs can queue up events created when a -device is offline (for example, because there isn't a network connection -available, or because airplane mode is on), and then send them to Iterable the -next time the app is in the foreground with a network connection. - -To use this feature, upgrade your apps to use: - -- Iterable's iOS SDK, version [`6.4.5+`](https://github.com/Iterable/iterable-swift-sdk/releases/tag/6.4.5) -- Iterable's Android SDK, version [`3.4.7+`](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.4.7) -- Iterable's React Native SDK, version [`1.3.3+`](https://github.com/Iterable/react-native-sdk/releases/tag/1.3.3) - -After you've upgraded your apps to use these SDK versions and your customer -success manager has enabled offline events processing for your account, your -apps will automatically capture and save offline events. Iterable's mobile SDKs -provide offline processing for the following types of events: - -- Purchase -- Update cart -- Push open -- In-app open -- In-app click -- In-app close -- Inbox session -- In-app delivery -- In-app consume -- Embedded message received -- Embedded message click -- Embedded message session -- Custom events tracked manually - -When your app is next in the foreground with an internet connection, it will send -any queued offline events back to Iterable. - -> [!NOTE] -> Iterable's mobile SDKs do not queue up any other API calls to send later (e.g., -> `registerDeviceToken`, `disableDevice`, `updateUser`, `updateSubscription`, or -> `updateEmail`). - -### Timestamps - -When sending offline events to Iterable (when a network connection has been -reestablished), Iterable's mobile SDKs include values for two timestamps: - -`createdAt` - The date and time when the user triggered the event. -`sentAt` - The date and time when the event is sent to Iterable. - -Iterable uses the difference between an event's `sentAt` time and the server time -when the event is received to adjust the event's `sentAt` and `createdAt` times to -be in sync with server time. When you query events from Iterable's API, you'll -see both of these timestamps. - -### Differentiating offline and online events - -To determine if a particular event saved in Iterable was originally captured -offline, check its attributes for mismatched `createdAt` and `sentAt` values. This -indicates that the event was captured at one time and saved at another, as is -the case for offline events. - -If an event doesn't have a `sentAt` value, it was captured online (`sentAt` is a -new field introduced with this feature. - -### Multiple users - -When users sign out of your app, Iterable's mobile SDKs delete any captured -offline events that haven't yet been saved back to Iterable. - -### Storage limits and timeframes - -Iterable's mobile SDKs will capture up to 1000 offline events, and then stop -capturing new offline events. However, there's no limit to the amount of time -for which offline events can be saved on a device. - -### JWT-enabled API keys - -Iterable's mobile SDKs support offline events processing for JWT-enabled API -keys and non JWT-enabled API keys. - -Starting with Android SDK 3.7.0+ and iOS SDK 6.6.8+, the SDKs include an -auto-retry feature for JWT failures during offline processing. When a queued -event encounters a 401 JWT error, the SDK automatically pauses authenticated -task processing, refreshes the JWT token, and retries the failed task. -Unauthenticated API calls (such as `disableDevice` and `mergeUser`) continue -processing while authentication is paused. This feature is controlled by a -remote configuration flag and requires no code changes. - -> [!NOTE] -> On older SDK versions, if the JWT token gets invalidated between the time that -> offline events are queued and the time the device comes back online, those -> queued events may not be saved to Iterable. - -### Increased event volume - -When you have your customer success manager enable offline events processing for -your account (and update to an SDK version that supports it), you may see an -increase in the number of custom events saved in your project (since offline -events that were previously dropped are now captured). However, this isn't -necessarily the case, and depends on the usage patterns of your users. To -monitor custom event usage, use the [Custom Event Usage](https://support.iterable.com/hc/articles/360043366492) -page. - -### Triggering journeys - -Like other events, offline events can trigger journeys when they're eventually -saved back to Iterable. However, it may not be desirable for older events to -trigger journeys that are no longer relevant. Because of this, offline events -saved to Iterable more than 24 hours after their creation will not trigger -journeys. diff --git a/polished/android/updating-user-profiles.polished.md b/polished/android/updating-user-profiles.polished.md deleted file mode 100644 index cfa4755..0000000 --- a/polished/android/updating-user-profiles.polished.md +++ /dev/null @@ -1,341 +0,0 @@ ---- -slug: updating-user-profiles -feature: user-profiles -archetype: identity -sdk_min_version: 3.7.0 -sdk_artifact: iterableapi -title: Updating User Profiles -source_url: https://support.iterable.com/hc/articles/360035402611 -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/managing-user-profiles/updating-user-profiles/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 3cca4ebdd1ee9da638428e3ded39de472202fd94 -fetched_at: 2026-05-25T15:11:47.716Z -polished_at: 2026-08-03T20:42:14.573Z -layer: a -snippets: - - index: 0 - lang: swift - hash: 98edcca734b9 - line_count: 28 - - index: 1 - lang: objectivec - hash: f643548616dc - line_count: 30 - - index: 2 - lang: java - hash: af02941d202c - line_count: 16 - - index: 3 - lang: handlebars - hash: 36130f23171c - line_count: 1 - - index: 4 - lang: json - hash: ea4da7f4ae5d - line_count: 4 - - index: 5 - lang: json - hash: 73fe86262683 - line_count: 4 - - index: 6 - lang: json - hash: 73fe86262683 - line_count: 4 - - index: 7 - lang: json - hash: 3e53f4645bf8 - line_count: 6 - - index: 8 - lang: swift - hash: 0eb671afc60f - line_count: 20 - - index: 9 - lang: objectivec - hash: 874ed71683e9 - line_count: 25 - - index: 10 - lang: java - hash: a2365a08fdc4 - line_count: 17 -summary: "A user's Iterable profile contains descriptive information about them: - demographic info, preferences, etc. You can use this data to create dynamic - lists and customize the messages you send (by referencing user profile data - with [Handlebars](https://support.iterable.com/hc/articles/35601631606036))." ---- -# Updating User Profiles - -A user's Iterable profile contains descriptive information about them: -demographic info, preferences, etc. You can use this data to create dynamic -lists and customize the messages you send (by referencing user profile data with -[Handlebars](https://support.iterable.com/hc/articles/35601631606036)). - -## Limitations - -User profiles have a soft limit of 1,000 fields. If you think you'll need more -fields, talk to your Iterable Customer Success Manager. - -## How to make the updateUser call - -Here's some sample code that makes an `updateUser` call: - -_Swift_ - -```swift -// The IterableAPI.updateUser(...) can be called anywhere the SDK is accessible -// myFunc() demonstrates this usage -import IterableSDK - -func myFunc() { - let dataField: [String: Any] = [ - "Address": [ - "Street1": "123 Main St", - "Street2": "Apt 1", - "City": "Iter-a-ville", - "State": "CA", - "Zip": "90210" - ] - ] - - IterableAPI.updateUser(dataField, - mergeNestedObjects: false, - onSuccess: myUserUpdateSuccessHandler, - onFailure: myUserUpdateFailureHandler) -} - -func myUserUpdateSuccessHandler(data: [AnyHashable: Any]?) -> () { - print("Successfully sent user update request to Iterable") -} - -func myUserUpdateFailureHandler(reason: String?, data: Data?) -> () { - print("Failure sending user update request to Iterable") -} -``` - -_Objective-C_ - -```objectivec -// The [IterableAPI updateUser:...] can be called anywhere the SDK is accessible -// myFunc demonstrates this usage -@import IterableSDK; - -typedef void (^successHandler)(NSDictionary * _Nullable); -typedef void (^failureHandler)(NSString * _Nullable, NSData * _Nullable); - -- (void)myFunc { - NSDictionary *data = @{ - @"Address": @{ @"Street1": @"123 Main St", - @"Street2": @"Apt 1", - @"City": @"Iter-a-ville", - @"State": @"CA", - @"Zip": @"90210" - } - }; - - [IterableAPI updateUser:data - mergeNestedObjects:NO - onSuccess:myUserUpdateSuccessHandler - onFailure:myUserUpdateFailureHandler]; -} - -successHandler myUserUpdateSuccessHandler = ^(NSDictionary * _Nullable data) { - NSLog(@"Successfully sent user update request to Iterable"); -}; - -failureHandler myUserUpdateFailureHandler = ^(NSString * _Nullable reason, NSData * _Nullable data) { - NSLog(@"Failure sending user update request to Iterable"); -}; -``` - -_Java_ - -```java -JSONObject address = new JSONObject(); -JSONObject datafields = new JSONObject(); - -try { - address.put("Street1", "123 Main St"); - address.put("Street2", "Apt 1"); - address.put("City", "Iter-a-ville"); - address.put("State", "CA"); - address.put("Zip", "90210"); - - datafields.put("dataFields", address); -} catch (JSONException e) { - e.printStackTrace(); -} - -IterableApi.getInstance().updateUser(datafields); -``` - -Now, in your messages, you can reference the user's `City` (or any other field) -with [Handlebars](https://support.iterable.com/hc/articles/35601631606036), like this: - -```handlebars -{{Address.City}} -``` - -### How `mergeNestedObjects` works - -The `mergeNestedObjects` parameter determines whether Iterable should merge -fields included in an `updateUser` request with analogous objects on the user's -profile, or overwrite that data. - -`mergeNestedObjects` only works for **one level of nesting** within objects. It -does **not** work recursively for deeper nested objects, and it does not merge -arrays. - -For objects nested more than one level deep, `mergeNestedObjects` will -**overwrite** the entire nested structure, not merge it. You must include all -existing data along with your updates to preserve deeper nested values. - -`mergeNestedObjects` defaults to `false`. - -For example, consider a user profile that includes the following address object: - -```json -"address": { - "street": "123 Main St", - "city": "San Francisco" -} -``` - -Then, assume that an `updateUser` call includes a similar object: - -```json -"address": { - "state": "CA", - "zipCode": "94105" -} -``` - -If `updateUser` sets `mergeNestedObjects` to `false` (the default value), the -resulting user profile value is: - -```json -"address": { - "state": "CA", - "zipCode": "94105" -} -``` - -However, if `updateUser` sets `mergeNestedObjects` to `true`, the resulting -user profile value is: - -```json -"address": { - "street": "123 Main St", - "city": "San Francisco", - "state": "CA", - "zipCode": "94105" -} -``` - -## When to make the updateUser call - -Call `updateUser` when a user has: - -- Updated their personal information. -- Completed a key step in an onboarding or sales process. For example, you might - want to set `completedOnboarding` to `true`, or assign a value to a field that's - useful for segmentation (setting `testGroup` to `testGroupA` or something - similar). -- Completed a key step in your onboarding, sales or retargeting process. For - example, you may want to add `"completedOnboarding": true` or `"testGroup": "A"` - to the user profile for later segmentation, splitting the users down different - journeys or analyzing later for test comparisons. - -## Tracking anonymous users - -To track anonymous users in Iterable, provide a `userId`. If your project uses -`email` as the only unique identifier, then this causes Iterable to generate a -placeholder email for the user—read -[Handling Anonymous Users](https://support.iterable.com/hc/articles/208499956) -for more info). - -## Taking a user from anonymous to known - -Iterable can convert anonymous users to known users. The example function below -updates the current user's `email` and `userId`. You will likely want to use -this code when your user signs in or self-identifies when signing up. - -_Swift_ - -```swift -let email = "newEmail@example.com" - -// The IterableAPI.updateUser(...) can be added to any method within your code. `yourUserIsNowKnownFunction` is just an example -func yourUserIsNowKnownFunction() { - IterableAPI.updateEmail(email, - onSuccess: myUserUpdateSuccessHandler, - onFailure: myUserUpdateFailureHandler) -} - -func myUserUpdateSuccessHandler(data: [AnyHashable: Any]?) -> () { - print("Successfully sent user update request to Iterable") -} - -func myUserUpdateFailureHandler(reason: String?, data: Data?) -> () { - print("Failure sending user update request to Iterable") - - IterableAPI.email = email - - IterableAPI.updateUser(dataField, mergeNestedObjects: false) -} -``` - -_Objective-C_ - -```objectivec -@import IterableSDK; - -typedef void (^successHandler)(NSDictionary * _Nullable); -typedef void (^failureHandler)(NSString * _Nullable, NSData * _Nullable); - -NSString *email = @"newEmail@example.com"; - -// The [IterableAPI updateUser:...] can be added to any method within your code. `yourUserIsNowKnownFunction` is just an example -- (void)yourUserIsNowKnownFunction { - [IterableAPI updateEmail:email - onSuccess:myUserUpdateSuccessHandler - onFailure:myUserUpdateFailureHandler]; -} - -successHandler myUserUpdateSuccessHandler = ^(NSDictionary * _Nullable data) { - NSLog(@"Successfully sent user update request to Iterable"); -}; - -failureHandler myUserUpdateFailureHandler = ^(NSString * _Nullable reason, NSData * _Nullable data) { - NSLog(@"Failure sending user update request to Iterable"); - - IterableAPI.email = email; - - [IterableAPI updateUser:dataField mergeNestedObjects:false]; -}; -``` - -_Java_ - -```java -final String email = "newEmail@example.com"; - -IterableApi.getInstance().updateEmail(email, new IterableHelper.SuccessHandler() { - @Override - public void onSuccess(JSONObject data) { - System.out.println("sent to Iterable success"); - - } -}, new IterableHelper.FailureHandler() { - @Override - public void onFailure(String reason, JSONObject data) { - System.out.println("sent to Iterable failure"); - IterableApi.getInstance().setEmail(email); - //This assumes your saving your user profile fields in the datafield object locally - IterableApi.getInstance().updateUser(datafields); - } -}); -``` - -There is a chance the `updateEmail` call will fail. The most likely reason is -that the user already exists so we should now update the user call directly in -the `onFailure` handler. diff --git a/sources/android/android-app-links.md b/sources/android/android-app-links.md deleted file mode 100644 index c4dbc64..0000000 --- a/sources/android/android-app-links.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/360035127392 -title: Android App Links -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/deep-links/android-app-links/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 3c933bbc8661bddfeaa8914f3dbb8233d98469c6 -fetched_at: 2026-05-25T15:11:45.366Z ---- -# Android App Links - -Messages sent with Iterable can include Android App Links, which redirect users -to your installed mobile app—no browser required. Iterable tracks clicks on these -links as expected. - -![Android Setup](https://iterable.zendesk.com/hc/article_attachments/360041676511/header_android.png "Android Setup") - -:::tip WARNING -You must set up [iOS deep linking](https://support.iterable.com/hc/articles/360035496511) -before implementing Android deep linking (they rely on similar architecture). -::: - -## In this article - -[[toc]] - -## Setting up Android App Links - -### 1. Enable Android App Links - -To enable Android App Links, follow these steps: - -- Configure your Iterable project to support deep links. For more information, - read [Configuring Deep Links for Email or SMS](https://support.iterable.com/hc/articles/115002651226). - -- Configure your mobile app to handle Android App Links by following the - [instructions in the Android documentation](https://developer.android.com/training/app-links/index.html). - -- Create intent filters for Iterable URIs by using the [App Links Assistant](https://developer.android.com/studio/write/app-link-indexing) - - ![App Links Assistant](https://iterable.zendesk.com/hc/article_attachments/360054769991/app-links-assistant.png "App Links Assistant") - - - Set the **Host** to your tracking domain. For example, - `https://links..com`. - - - Set the **Path** to use `/a` as the **pathPrefix**. - -- Generate the `assetlinks.json` file - -### 2. Upload `assetlinks.json` - -After you generate an `assetlinks.json` file with your app's fingerprint, -you'll need to provide it to Iterable so that it can be hosted at -`/.well-known/assetlinks.json`. To upload it, -follow the instructions in [Configuring Deep Links for Email or SMS](https://support.iterable.com/hc/articles/115002651226). - -Then, use Google's [Statement List Generator and Tester](https://developers.google.com/digital-asset-links/tools/generator) -to test it out. - -### 3. Determine which links to rewrite - -To determine which links to rewrite as deep links for a given campaign, Iterable -looks at the relevant tracking domain's `apple-app-site-association` file. Even -if you only have an Android app, you'll still need to create this file. To learn -how to do so, read [Configuring Deep Links for Email or SMS](https://support.iterable.com/hc/articles/115002651226). - -### 4. Update your code - -If you already have a `urlHandler`, you can use the same handler for email deep -links by calling `handleAppLink` in the activity that handles all Android App -Links in your app: - -```java -// MainActivity.java -@Override -public void onCreate() { - super.onCreate(); - ... - handleIntent(getIntent()); -} - -@Override -public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - if (intent != null) { - handleIntent(intent); - } -} - -private void handleIntent(Intent intent) { - if (Intent.ACTION_VIEW.equals(intent.getAction()) && intent.getData() != null) { - IterableApi.getInstance().handleAppLink(intent.getDataString()); - // Overwrite the intent to make sure we don't open the deep link - // again when the user opens our app later from the task manager - setIntent(new Intent(Intent.ACTION_MAIN)); - } -} -``` - -Alternatively, call `getAndTrackDeeplink` along with a callback to handle the -original deep link URL. You can use this method for any incoming URLs, as it -will execute the callback without changing the URL for non-Iterable URLs. - -```java -IterableApi.getAndTrackDeeplink(uri, new IterableHelper.IterableActionHandler() { - @Override - public void execute(String result) { - Log.d("HandleDeeplink", "Redirected to: "+ result); - // Handle the original deep link URL here - } -}); -``` - -:::tip TIP -To check if a URL is an Iterable deep link before handling it, use the -`isIterableDeepLink` method: - -```java -if (IterableApi.getInstance().isIterableDeepLink(urlString)) { - // URL is an Iterable deep link, handle it with the SDK - IterableApi.getInstance().handleAppLink(urlString); -} else { - // Handle non-Iterable URLs differently if needed -} -``` - -This method returns `true` if the URL matches the Iterable deep link pattern -(URLs containing `/a/` in the path). -::: - -## FAQ - -For answers to common questions about deep links, read the [Deep Link FAQs](https://support.iterable.com/hc/articles/360035624191#deep-link-faqs). - - diff --git a/sources/android/android-sdk.md b/sources/android/android-sdk.md deleted file mode 100644 index 73e2708..0000000 --- a/sources/android/android-sdk.md +++ /dev/null @@ -1,1156 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/360035019712 -title: Iterable's Android SDK -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/iterables-ios-and-android-sdks/android-sdk/index.md -source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 -source_sha: de67a132360a33146dae801ab62c3de1d6846ba9 -fetched_at: 2026-08-03T20:41:28.018Z ---- -​ -# Iterable's Android SDK -​ -This article describes how to install and configure Iterable's [Android SDK](https://github.com/Iterable/iterable-android-sdk). - -## In this article - -[[toc]] - -## Supported Android versions - -Iterable's Android SDK supports Android versions 5.0 (API level 21) and -higher. - -## Encrypted data - -Depending on your `minSdkVersion`, Iterable's Android SDK can encrypt some -data at rest. For more information, read [Upgrading to 3.4.10](#upgrading-to-3-4-10). - -## Installing the SDK - -Follow these steps to install Iterable's Android SDK. If you're upgrading from -a previous version, see [Upgrading the SDK](#upgrading-the-sdk). - -:::warning IMPORTANT -If your app targets API level 22 or lower, read [Upgrading to 3.4.10](#upgrading-to-3-4-10) -to learn about some adjustments you'll need to make to your Android project. -::: - -### Step 1: Define a mobile app and push integration in Iterable - -Before installing Iterable's Android SDK in your mobile app, tell your Iterable -project about your mobile app. - -To do this, follow the instructions in [Setting up Android Push Notifications](https://support.iterable.com/hc/articles/115000331943), which describe how to: - -- Define a mobile app in your Iterable project. - -- Give that app a _push integration_. A push integration stores configuration - and authentication information Iterable can use to send push notifications to - your app. - - Even if you don't want to send push notifications, Iterable can use your app's - push integration to send _silent_ push notifications, to tell your app that - Iterable has new in-app and embedded messages for it to fetch and display. - -### Step 2: Create a mobile API key - -To make calls to Iterable's's API, the SDK needs a mobile [API key](https://support.iterable.com/hc/articles/360043464871). -To learn how to create one, read [API Keys](https://support.iterable.com/hc/articles/360043464871) - -:::danger WARNING -**Never** embed server-side API keys in client-side code (whether JavaScript, a -mobile application or otherwise), since they can be used to access all of your -project's data. -::: - -For a mobile app, use a mobile API key. For additional security, enable -[JWT authentication](https://support.iterable.com/hc/articles/360050801231), -if you can support it. - -If necessary, you can use different API keys for debug and production builds -of your app. - -### Step 3: Install the SDK - -:::tip TIP -To determine the latest version of Iterable's Android SDK, see -[search.maven.org](https://search.maven.org/artifact/com.iterable/iterableapi). -::: - -To use Iterable's Android SDK in your app, add the SDK and Firebase Messaging as -dependencies to your application's `build.gradle`: - -```groovy -dependencies{ - implementation 'com.iterable:iterableapi:3.5.3' - // Optional, contains Inbox UI components: - implementation 'com.iterable:iterableapi-ui:3.5.3' - // Version 17.4.0+ is required for push notifications and in-app message features: - implementation 'com.google.firebase:firebase-messaging:X.X.X' -} -``` - -### Step 4: Configure ProGuard - -If you're using ProGuard when building your Android app, add this line of -ProGuard configuration to your build: - -``` --keep class org.json.** { *; } -``` - -To learn how to do this, check out Android's guide: -[Shrink, obfuscate, and optimize your app](https://developer.android.com/studio/build/shrink-code#add-configuration). - -:::warning WARNING -If you use ProGuard but skip this step, some SDK features may not work as expected. -::: - -### Step 5: Set SDK configuration options - -To initialize Iterable's Android SDK, create an `IterableConfig` and set its -various configuration options. - -Do this when your application is starting up, usually in the `onCreate` method -of your `Application` class. Then, pass this `IterableConfig` to `IterableApi.initialize`, -along with your API key. - -```java -IterableConfig config = new IterableConfig.Builder().build(); -IterableApi.initialize(context, "", config); -``` - -`IterableConfig` contains various configuration options for the SDK. For -more information, refer to the following sections of this document. Or, take -a look at the `IterableConfig` [source code](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java). - -:::warning IMPORTANT -- Version [3.4.10](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.4.10) - of Iterable's Android SDK provides a configuration option to store in-app - messages in memory, rather than in a local file. For more information, read - [Encrypted data](#encrypted-data). -- Don't `initialize` the SDK in the `onCreate` method of an `Activity`. - Instead, do it when your app is starting up, regardless of whether it has been - launched to open an activity or in the background, as the result of an incoming - push notification. -::: - -#### Step 5.1: Background Initialization - -To prevent application not responding (ANR) errors during app startup when using -SDKs that need to initialize on background, initialize the SDK asynchronously -instead of using the standard `initialize()` method. For example: - -```kotlin -// In Application.onCreate() -IterableApi.initializeInBackground(this, "", config) { - // SDK is ready - this callback is optional -} -``` - -To subscribe to initialization completion from multiple places: - -```kotlin -IterableApi.onSDKInitialized { - // This callback will be invoked when initialization completes - // If already initialized, it's called immediately -} -``` - -Background initialization prevents ANRs by: -- Running all initialization work on a background thread. -- Automatically queuing API calls until initialization completes. -- Ensuring that no data is lost during startup. -- Providing callbacks on the main thread when ready. - -:::tip IMPORTANT -Always wait for initialization to complete before you access SDK internals. -Then, to ensure that the SDK is ready for use, use the callback methods provided -above. -::: - -#### Step 5.2: If necessary, configure the SDK to use Iterable's EDC - -If your Iterable project is hosted on Iterable's [European data center (EDC)](https://support.iterable.com/hc/articles/17572750887444), -update your `IterableConfig` to use Iterable's EDC-based API endpoints: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setDataRegion(IterableDataRegion.EU).build(); -IterableApi.initialize(context, "", config); -``` - -#### Step 5.3: Set allowed URL protocols - -Starting with version [`3.4.0`](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.4.0) -of Iterable's Android SDK, you'll need to declare the specific URL protocols -that the SDK can expect to see on incoming links (and that it should handle -as needed). This prevents the SDK from opening links that use unexpected -URL protocols. - - To do this, pass the protocols you'd like the SDK to support (as an array of - strings) to the `setAllowedProtocols` method on `IterableConfig.Builder`. - - For example, this code allows the SDK to handle `http://`, `tel://`, and `mycompany://` - links: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setAllowedProtocols(new String[]{"http", "tel", "mycompany"}).build(); -IterableApi.initialize(context, "", config); -``` - -:::warning IMPORTANT -Iterable's Android SDK handles `https`, `action`, `itbl`, and `iterable` links, -regardless of the contents of this array. However, you must explicitly declare any -other types of URL protocols you'd like the SDK to handle (otherwise, the SDK -won't open them in the web browser or as deep links). -::: - -#### Step 5.4: Specify whether to store in-app messages in memory - -By default, Iterable's Android SDK stores in-app messages in an unencrypted local -file. If you'd prefer to have SDK store in-app messages in memory instead, use the -`setUseInMemoryStorageForInApps(true)` SDK configuration option (defaults to `false`): - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setUseInMemoryStorageForInApps(true).build(); -IterableApi.initialize(context, "", config); -``` - -For more information about this option, read [Upgrading to 3.4.10](#upgrading-to-3-4-10). - -#### Step 5.5: Specify a push integration name, if necessary - -In [Step 1: Define a mobile app and push integration in Iterable](#step-1-define-a-mobile-app-and-push-integration-in-iterable), -you defined a mobile app in Iterable, and you gave it a push integration. - -Every push integration in Iterable has a name, and that name almost always matches -your Android app's package name (for example, `com.example.app`). By default, -this is what the SDK expects: to find a push integration in your Iterable project -with a name that matches your app's package name. - -:::tip TIP -To find the name of your app's push integration in Iterable, navigate to -**Settings > Apps and Websites**, open the mobile app associated with your app, -find the **Push** section, and look at the **Name** column in the row associated -with your push integration. -::: - -However, push integrations created in Iterable before August of 2019 can have -custom names. If this is the case for your push integration, tell the SDK the name -of your push integration by calling `setPushIntegrationName` on -`IterableConfig`: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setPushIntegrationName(““).build(); -IterableApi.initialize(context, ““, config); -``` - -#### Step 5.6: Handle JWT-enabled API keys - -If you're using a [JWT-enabled API Key](https://support.iterable.com/hc/articles/360050801231), -you'll need custom code to manage JWT tokens for the signed-in user. - -##### Step 5.6.1: Register an auth handler - -When initializing the SDK, provide an auth handler. The SDK uses the auth -handler to: - -1. Fetch new JWT tokens from your server. -2. Report when a non-null JWT token has been retrieved. -3. Report when there have been failures fetching new JWT tokens. - -The object that you pass to the SDK as an auth manager must implement the -`IterableAuthManager` interface: - -```java -public interface IterableAuthHandler { - String onAuthTokenRequested(); - void onTokenRegistrationSuccessful(String authToken); - void onAuthFailure(AuthFailure authFailure); -} -``` - -For example: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setAuthHandler(new IterableAuthHandler() { - @Override - public String onAuthTokenRequested() { - // Fetch a JWT token for the signed-in user, from your server, and - // return it to the SDK. - return ""; - } - - @Override - public void onTokenRegistrationSuccessful(String authToken) { - // The SDK has retrieved a non-null JWT token for the signed-in user. - // However, the SDK does not validate the token before calling this - // method. - } - - @Override - public void onAuthFailure(AuthFailure authFailure) { - // Inspect the authFailure enum constant and take any necessary action. For - // example, you can pause auth retries (see section 5.5.3, below). - } - }).build(); - IterableApi.initialize(_context, "", config); - ``` - -**`onAuthTokenRequested`** - -The SDK calls `onAuthTokenRequested` when it needs a new JWT token for the -signed-in user. This method should fetch a new JWT token from your server and -return it to the SDK as a string. - -This method is called when: - -- You identify a user by calling `setEmail` or `setUserId`. -- You update a user's email address by calling `updateEmail`. -- The current JWT token has expired, or is about to expire. -- The SDK receives a JWT-related `401` response from Iterable's API. - -**`onTokenRegistrationSuccessful`** - -The SDK calls `onTokenRegistrationSuccessful` after `onAuthTokenRequested` -returns a non-null JWT token. However, other than a null check, the SDK does not -validate the token before calling this method. Generally, you won't need to -implement this method. - -**`onAuthFailure`** - -The SDK calls `onAuthFailure` after it fails to fetch a new JWT token for the -signed-in user. The `AuthFailure` object passed to this method describes the -reason for the failure, along with other information. - -This method is called when: - -- `onAuthTokenRequested` returns `null`. -- `onAuthTokenRequested` throws an exception. -- The SDK receives a JWT-related `401` response from Iterable's API. -- The token returned by `onAuthTokenRequested` is invalid. - -In `onAuthFailure`, to determine the reason for the failure, inspect the -`AuthFailure` object, which has these properties: - -- `userKey` - A string that identifies the user by `userId` or `email`. -- `failedAuthToken` - The JWT token that caused the failure. -- `failedRequestTime` - The timestamp of the failed request, if applicable. -- `failureReason` - An `AuthFailureReason` enum constant that indicates the reason - for the failure. - -`AuthFailureReason` can have these values: - -- `AUTH_TOKEN_EXPIRATION_INVALID` – An auth token's expiration must be less than - one year from its issued-at time. -- `AUTH_TOKEN_EXPIRED` – The token has expired. -- `AUTH_TOKEN_FORMAT_INVALID` – Token has an invalid format (failed a regular - expression check). -- `AUTH_TOKEN_GENERATION_ERROR` – `onAuthTokenRequested` threw an exception. -- `AUTH_TOKEN_GENERIC_ERROR` – Any other error not captured by another constant. -- `AUTH_TOKEN_INVALIDATED` – Iterable has invalidated this token and it cannot - be used. -- `AUTH_TOKEN_NULL` – `onAuthTokenRequested` returned a null JWT token. -- `AUTH_TOKEN_PAYLOAD_INVALID` – Iterable could not decode the token's payload - (`iat`, `exp`, `email`, or `userId`). -- `AUTH_TOKEN_SIGNATURE_INVALID` – Iterable could not validate the token's - authenticity. -- `AUTH_TOKEN_USER_KEY_INVALID` – The token doesn't include an `email` or a `userId`. - Or, one of these values is included, but it references a user that isn't in the - Iterable project. -- `AUTH_TOKEN_MISSING` – The request to Iterable's API did not include a JWT - authorization header. - -:::tip TIP -You can also provide a JWT token for the current user by passing it directly to -`setEmail` or `setUserId`. -::: - -##### Step 5.6.2: Set an expiring token refresh period - -To specify how long before the expiration of the user's current JWT token -the SDK should call your [auth token refresh handler](#step-5-6-1-register-an-auth-handler), -to fetch a new token, call `setExpiringAuthTokenRefreshPeriod` on `IterableConfig`: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setExpiringAuthTokenRefreshPeriod(time_in_seconds).build(); -IterableApi.initialize(context, "", config); -``` - -##### Step 5.6.3: Set an auth retry policy - -To control how the SDK handles consecutive JWT token refresh attempts, specify -an auth retry policy. An auth retry policy allows you to control: - -- The number of consecutive times the SDK should attempt to refresh a user's JWT - token, in between successful API calls, before giving up. -- The interval between those attempts. -- A backoff strategy. - -```java -// When creating a RetryPolicy object, specify a maximum number of retries, an -// interval between retries, and a backoff strategy: RetryPolicy.Type.LINEAR or -// RetryPolicy.Type.EXPONENTIAL. The SDK's default RetryPolicy has a maximum of -// 10 retries, an interval of 6 seconds, and a linear backoff strategy. -RetryPolicy retryPolicy = new RetryPolicy(10, 10, RetryPolicy.Type.LINEAR); -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setAuthRetryPolicy(time_in_seconds).build(); -IterableApi.initialize(context, "", config); -``` - -After the SDK reaches the maximum number of consecutive JWT-related request failures, -as configured by your `RetryPolicy`, it stops attempting to refresh the JWT token. - -:::tip Auto-retry for offline processing (3.7.0+) -In addition to the `RetryPolicy` above (which controls JWT refresh scheduling), -the SDK supports automatic retry for offline-queued tasks that fail due to JWT -expiration. When this feature is enabled, the offline task runner pauses -authenticated tasks on a 401 error, refreshes the JWT, and retries -automatically. Unauthenticated API calls continue processing while -authentication is paused. This feature requires no code changes. - -This feature is not enabled by default. To turn it on for your project, ask -your Iterable customer success manager to enable it for your account. -::: - -It's also possible to _manually_ pause JWT token refresh attempts. To do this, -call: - -```java -IterableApi.getInstance().pauseAuthRetries(true); -``` - -When JWT refresh attempts have been paused, they'll only resume after: - -- You provide a new JWT token to the SDK, by calling `setAuthToken`. -- You identify the user by calling `setEmail` or `setUserId`. -- You update the user's email by calling `updateEmail` -- The app restarts. -- You manually pause and unpause JWT token refresh attempts, by calling: - ```java - // If you didn't manually pause JWT refresh attempts in the first place, - // first call pauseAuthRetries(true). Then, call pauseAuthRetries(false). - IterableApi.getInstance().pauseAuthRetries(true); - IterableApi.getInstance().pauseAuthRetries(false); - ``` - -#### Step 5.7: Disable keychain encryption if necessary - -In Android apps with `minSdkVersion` 23 or higher ([Android 6.0](https://developer.android.com/studio/releases/platforms#6.0)) -Iterable's Android SDK encrypts sensitive user data when storing it in the -keychain. This includes the user's `email`, `userId`, and `authToken` (JWT). - -This encryption is enabled by default. However, if you need to disable it, you -can do so by setting the `keychainEncryption` option to `false` when -initializing the SDK: - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setKeychainEncryption(false).build(); // Disable encryption for keychain storage -IterableApi.initialize(context, apiKey, config); -``` - -#### Step 5.8: Configure WebView base URL for CORS support, if necessary - -If your in-app or inbox messages load external resources (such as custom fonts or -stylesheets) and you're seeing CORS errors, configure a base URL for the WebView. - -By default, the WebView sends a blank origin when requesting resources. If your -server's CORS policy rejects blank origins, set the base URL to match whatever -origin your server accepts (such as your CDN domain, app domain, or -[https://app.iterable.com](https://app.iterable.com)). - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setWebViewBaseUrl("https://app.iterable.com") // Use https://app.eu.iterable.com for EU - .build(); -IterableApi.initialize(context, "", config); -``` - -### Step 6: Identify the signed-in user - -When you know the user's `email` or `userId`, identify them by calling: - -- `IterableApi.getInstance().setEmail("user@example.com");` -- `IterableApi.getInstance().setUserId("userId");` - -:::tip NOTES -- Make sure to identify the user _after_ you've specified the configuration - options on `IterableConfig`, as described in [Step 5: Set SDK configuration options](#step-5-set-sdk-configuration-options). -- Don't set an email and user ID in the same session. -- If you've prefetched a JWT auth token, you can pass it directly to `setEmail` - and `setUserId` (useful to work around race conditions that can sometimes - occur). -::: - -### Step 7: Handle push notifications - -Next, configure the SDK to handle push notifications. - -:::tip TIP -If the name of your app's push integration, in Iterable, differs from your -app's package name (they usually match), make sure to specify your push -integration name on `IterableConfig`. To learn how to do this, read -[Step 5.5: Specify a push integration name, if necessary](#step-5-5-specify-a-push-integration-name-if-necessary). -::: - -#### Step 7.1: Register for remote notifications - -Every user + device + app combination can be identified by a unique push _token_, -which is stored on the user's profile in Iterable. Iterable users this token to -send push notifications to the user. - -The SDK _automatically_ saves a push token to the user's profile whenever you -call `setEmail` or `setUserId`. - -However, you can also handle this token registration manually: - -- When initializing the SDK, disable automatic push token registration by - calling `setAutoPushRegistration(false)` on `IterableConfig`. -- Whenever it makes sense, save a device token for the signed-in user to Iterable - by calling `registerForPush` on `IterableApi`: - - ```java - IterableApi.getInstance().registerForPush(); - ``` - -:::tip NOTES -- Device registration fails when no `email` or `userId` has been set. -- If you're calling `setEmail` or `setUserId` after the app has already - launched (for example, when a new user logs in), call `registerForPush` - to register the device for the current user. -::: - -#### Step 7.2: Handle Firebase push messages and tokens - -The SDK automatically adds a `FirebaseMessagingService` to the app manifest. To -handle incoming push notifications, no extra setup is necessary. - -However, if your application implements its own `FirebaseMessagingService`: - -- Forward `onMessageReceived` calls to `IterableFirebaseMessagingService.handleMessageReceived`. -- Forward `onNewToken` calls to `IterableFirebaseMessagingService.handleTokenRefresh`. - -```java -public class MyFirebaseMessagingService extends FirebaseMessagingService { - - @Override - public void onMessageReceived(RemoteMessage remoteMessage) { - IterableFirebaseMessagingService.handleMessageReceived(this, remoteMessage); - } - - @Override - public void onNewToken(String s) { - IterableFirebaseMessagingService.handleTokenRefresh(); - } -} -``` - -:::tip NOTES -- This step is mandatory for working with multiple push providers. -- Firebase has [deprecated `FirebaseInstanceIdService`](https://firebase.google.com/docs/reference/android/com/google/firebase/iid/FirebaseInstanceIdService). - It has been replaced with `onNewToken`. -- To handle silent push notifications, use a custom `FirebaseMessagingService`. -::: - -### Step 8: Enable Embedded Messaging if necessary - -To learn how to use Iterable's Android SDK with Embedded Messaging, read -[Embedded Messages with Iterable's Android SDK](https://support.iterable.com/hc/articles/23061877893652). - -## Upgrading the SDK - -This section describes how to upgrade from earlier versions of Iterable's -Android SDK. - -### Upgrading to 3.10.0 - -[Version 3.10.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.10.0) -of Iterable's Android SDK makes manager getters fail gracefully before -initialization, and adds a `DEFER` response for in-app handlers, a -`resumeInAppDisplay()` method, and unknown user criteria fetch callbacks. -**No action is required to upgrade**—all of these changes are backward -compatible. - -#### Manager getters no longer crash before initialization - -In earlier versions, calling `getInAppManager()` or `getEmbeddedManager()` -before `IterableApi.initialize()` threw a `RuntimeException`, which could crash -the host app. Starting with version 3.10.0, these methods log an error and -return a no-op manager instead—it returns empty results and ignores commands, so -a call-ordering mistake no longer crashes your app. - -If you need to detect whether the SDK is initialized before using a manager, use -the new `getInAppManagerOrNull()` and `getEmbeddedManagerOrNull()` methods, which -return `null` (rather than a no-op manager) when the SDK isn't initialized yet. - -```java -IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManagerOrNull(); -if (inAppManager != null) { - // Safe to use; the SDK is initialized. -} -``` - -As always, initialize the SDK in the `onCreate` method of your `Application` -class before calling other SDK methods. - -#### New: `DEFER` response and `resumeInAppDisplay()` for in-app messages - -`IterableInAppHandler.InAppResponse` now includes a `DEFER` value. Unlike `SKIP` -(which permanently drops a message), `DEFER` keeps the message pending so the -SDK reconsiders it later—useful for temporary suppression, such as while a -splash screen is showing. To re-check pending messages on demand once your app -is ready, call the new `IterableInAppManager.resumeInAppDisplay()` method. For -more information, read [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231). - -#### New: unknown user criteria fetch callbacks - -`IterableUnknownUserHandler` now reports the results of unknown user criteria -fetches through two optional methods: `onCriteriaReceived(JSONObject criteria)` -on success and `onCriteriaFetchFailed(String reason)` on failure. Both have -default, no-op implementations, so existing handlers are unaffected. - -For more information, read [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231) -and [Configure the Android SDK](https://support.iterable.com/hc/articles/40078934178836) -in the Unknown User Activation documentation. - -### Upgrading to 3.9.0 - -[Version 3.9.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.9.0) -of Iterable's Android SDK adds in-app message support for Jetpack Compose apps, -a new opt-in toolbar for the mobile inbox, and additional context for push-open -tracking. **No action is required to upgrade**—all of these changes are -backward compatible. - -#### In-app messages in Jetpack Compose apps - -The SDK can now render in-app messages using a new `Dialog`-based renderer -(`IterableInAppDialogNotification`) that doesn't require a `FragmentActivity`. -Apps that host in-app messages in a `FragmentActivity` continue to use the -existing `Fragment`-based rendering; apps that don't (such as those built fully -with Jetpack Compose, using a `ComponentActivity`) automatically fall back to -the `Dialog`-based renderer. As a result, in-app messages now display correctly -in apps built fully with Jetpack Compose, with no additional setup. - -#### New: `IterableInboxToolbarView` for the mobile inbox - -If you use Iterable's [Mobile Inbox](https://support.iterable.com/hc/articles/360038744152), -you can now add an optional toolbar above the inbox list using the new -`IterableInboxToolbarView`. Configure it with the `InboxToolbarOption` sealed -interface: - -- `None` (default) — No toolbar. The inbox behaves exactly as it did in - previous SDK versions. -- `Default` — A title-only toolbar above the inbox list. -- `WithBackButton` — A title plus a back-navigation icon. By default, the back - action calls `OnBackPressedDispatcher`. To override it, have your host - `Activity` or parent `Fragment` implement `IterableInboxToolbarBackListener`. -- `Custom(layoutRes)` — Inflates your own toolbar layout. Views tagged with the - reserved IDs `@id/iterable_reserved_inbox_toolbar_action` and - `@id/iterable_reserved_inbox_toolbar_title` are automatically wired to the - SDK's back handler and title binding, respectively (both are optional). - -Configure the toolbar programmatically with `IterableInboxFragment.newInstance(...)` -(using the new two- or six-argument overloads), or with `IterableInboxActivity` -intent extras (`TOOLBAR_OPTION` and `TOOLBAR_TITLE`). - -:::warning IMPORTANT -When the toolbar is enabled, the host activity must use a `Theme.AppCompat` -descendant. -::: - -For more information about customizing the inbox, see -[Customizing Mobile Inbox on Android](https://support.iterable.com/hc/articles/360039189931). - -#### New: `appAlreadyRunning` field on `trackPushOpen` - -`trackPushOpen` now includes an `appAlreadyRunning` field that indicates whether -the app was already running when the push notification was received. A new -`trackPushOpen(int, int, String, boolean, JSONObject)` overload lets you pass -this value; existing overloads default it to `false`, so no changes are required -for existing code. - -#### Fix: `TransactionTooLargeException` crash for large in-app messages - -This release also fixes a `TransactionTooLargeException` crash that could occur -when displaying in-app messages with oversized HTML payloads. The HTML is no -longer serialized into the fragment's saved instance state—it's reloaded from -storage when the fragment is recreated. In-app messages with missing HTML now -dismiss gracefully without registering tracking events, and a warning is logged -for HTML payloads that exceed the recommended size. - -For more information, read [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231) -and [Customizing Mobile Inbox on Android](https://support.iterable.com/hc/articles/360039189931). - -### Upgrading to 3.8.0 - -[Version 3.8.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.8.0) -of Iterable's Android SDK introduces a new configuration option for controlling -how in-app messages interact with system bars, plus refinements to embedded -message views and a security cleanup. **No action is required for most apps**—upgrading -preserves the existing in-app message behavior introduced in 3.6.1. - -#### New: `IterableInAppDisplayMode` for in-app messages - -Since 3.6.1, Iterable's Android SDK has always rendered in-app messages -edge-to-edge, behind the status bar and navigation bar. Starting with 3.8.0, -you can change that behavior globally by setting an `IterableInAppDisplayMode` -on `IterableConfig`: - -```java -IterableConfig config = new IterableConfig.Builder() - .setInAppDisplayMode(IterableInAppDisplayMode.FORCE_RESPECT_BOUNDS) - .build(); - -IterableApi.initialize(context, apiKey, config); -``` - -The available modes are: - -- `FORCE_EDGE_TO_EDGE` (default) — Draws in-app content behind the system - bars, with transparent status and navigation bars. Preserves the behavior - introduced in SDK 3.6.1. -- `FOLLOW_APP_LAYOUT` — Matches the host app's current system bar - configuration. -- `FORCE_FULLSCREEN` — Hides the status bar entirely while in-app messages - are displayed. -- `FORCE_RESPECT_BOUNDS` — Ensures in-app content never overlaps system bars, - keeping UI elements like the close button always accessible. - -If the close button on your fullscreen in-app messages is being obscured by -the status bar on certain devices, switch to `FOLLOW_APP_LAYOUT` or -`FORCE_RESPECT_BOUNDS`. For more information, see [Configuring how in-app messages interact with system bars](https://support.iterable.com/hc/articles/360035537231#configuring-how-in-app-messages-interact-with-system-bars-sdk-v3-8-0-and-above) -in the In-App Messages on Android documentation. - -#### Other changes in 3.8.0 - -- **`imageScaleType` option for embedded message views**: `IterableEmbeddedViewConfig` - exposes a new `imageScaleType` property that controls how the image is - scaled within the 16:9 container of an out-of-the-box embedded message view. - -- **Default values for `IterableEmbeddedViewConfig` parameters**: All - `IterableEmbeddedViewConfig` constructor parameters now have default values, - so you only need to specify the styling options you want to customize. - Existing calls that pass every parameter continue to work unchanged. - -- **Embedded message card layout fixes**: Out-of-the-box embedded message - views render correctly again on cards. The image now displays at a 16:9 - aspect ratio instead of collapsing to zero height, the card container no - longer expands to fill its parent, the missing end margin on the card is - applied, bottom spacing on buttons is no longer cut off, and the image is - properly clipped to the card's rounded corners. - -- **Removed insecure `AES/CBC/PKCS5Padding` encryption**: `IterableDataEncryptor` - now exclusively uses `AES/GCM/NoPadding`. The legacy CBC algorithm was only - used on Android versions below KitKat (API 19), which have been unsupported - since `minSdkVersion` was raised to 21 in SDK 3.5.12. No data migration is - required. - -### Upgrading to 3.7.0 - -[Version 3.7.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.7.0) -introduces two opt-in improvements: an automatic JWT-refresh-and-retry flow for the -offline event queue, and new callbacks for tracking embedded message sync results. -No application code changes are required to upgrade—both improvements are opt-in. - -#### Opt-in: Auto-retry for JWT failures in offline event processing - -When offline event processing is enabled and a queued API call returns a 401 -JWT error, the SDK can now automatically: - -1. Pause processing of authenticated tasks in the offline queue. -2. Refresh the JWT via your registered `IterableAuthHandler`. -3. Retry the failed task with the new token. - -Unauthenticated endpoints (such as `disableDevice`, `mergeUser`, and -`trackConsent`) continue to be processed while authentication is paused, so -unrelated traffic isn't blocked behind a stale token. - -This behavior is disabled by default for existing customers. To enable it for your -project, talk to your Iterable customer success manager. No application code -changes are required once the flag is enabled—the SDK starts using the new behavior -automatically. - -#### Opt-in: Embedded messaging sync callbacks - -`IterableEmbeddedUpdateHandler` now exposes two optional callbacks— -`onEmbeddedMessagingSyncSucceeded()` and `onEmbeddedMessagingSyncFailed(reason)`— -that let your app react to embedded message syncs. Use them to stop a loading -spinner on success or to show fallback content on failure. Both methods have -default empty implementations, so existing code keeps working unchanged. - -For more information, read [Embedded Messages with Iterable's Android SDK](https://support.iterable.com/hc/articles/23061877893652#step-8-set-up-sdk-listeners). - -### Upgrading to 3.6.6 - -[Version 3.6.6](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.6) -of Iterable's Android SDK is a maintenance release. No action is required to -upgrade. - -### Upgrading to 3.6.5 - -Starting with [version 3.6.5](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.5), -the `IterableEmbeddedView` constructor is **deprecated** because it violates -Android Fragment best practices: the system can't recreate the fragment after -configuration changes or process death, which can cause crashes. - -Use the `newInstance` factory method instead: - -```kotlin -// Deprecated: -val messageView = IterableEmbeddedView(ootbType, message, config) - -// Use this instead: -val messageView = IterableEmbeddedView.newInstance(ootbType, message, config) -``` - -The old constructor still works, but it's marked as deprecated and will be -removed in a future SDK release. Update your application code now to avoid a -breaking change later. - -For more information, read [Embedded Messages with Iterable's Android SDK](https://support.iterable.com/hc/articles/23061877893652). - -### Upgrading to 3.6.4 - -[Version 3.6.4](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.4) -makes the `isIterableDeeplink` method public so you can now check whether a URL is -an Iterable deep link before handling it. The method returns `true` when the URL -matches the Iterable deep link pattern (URLs containing `/a/` in the path). - -`isIterableDeeplink` is a **static** method on `IterableApi`: - -```java -if (IterableApi.isIterableDeeplink(urlString)) { - // URL is an Iterable deep link -} -``` - -For more information about deep links in Iterable, read [Android App Links](https://support.iterable.com/hc/articles/360035127392). - -### Upgrading to 3.6.3 - -[Version 3.6.3](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.3) -of Iterable's Android SDK is a maintenance release. No action is required to -upgrade. - -### Upgrading to 3.6.2 - -[Version 3.6.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.2) -adds three opt-in capabilities. No action is required to upgrade. - -- **Background initialization to prevent ANRs**: To run SDK initialization on - a background thread (with API calls automatically queued until ready), call - the new `IterableApi.initializeInBackground()` static method instead of - `IterableApi.initialize()`: - - ```java - IterableApi.initializeInBackground(context, apiKey, config, callback); - ``` - - Use this if running initialization on the main thread is contributing to - Application Not Responding (ANR) errors during app startup. The optional - `callback` (an `IterableInitializationCallback`) is invoked when - initialization completes. - -- **`onSDKInitialized()` callback**: A new static method on `IterableApi` lets - you subscribe a callback to be notified when initialization completes. Use - it when you need to defer SDK-dependent work from multiple call sites—for - example, posting the first event only after the SDK is fully ready. - - ```java - IterableApi.onSDKInitialized(callback); - ``` - -- **`setWebViewBaseUrl()` configuration option**: A new `IterableConfig.Builder` - method that sets the base URL used by WebView-based messages (in-app - messages, inbox, and embedded messages). Set it when you self-host custom - fonts or other external resources that require CORS to load successfully in - a WebView: - - ```java - IterableConfig config = new IterableConfig.Builder() - .setWebViewBaseUrl("https://your-cdn.example.com") - .build(); - - IterableApi.initialize(context, apiKey, config); - ``` - - If not set, the base URL defaults to an empty string (the original behavior). - -### Upgrading to 3.6.1 - -Starting with [version 3.6.1](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.1), -in-app messages render edge-to-edge so they display properly on devices with notches, -cutouts, and system bars. - -By default, the SDK applies white insets to fill the area behind the system -bars. In dark-themed apps, that white can contrast sharply with your in-app -message content. - -If your app uses a dark theme, consider updating the [background overlay](https://support.iterable.com/hc/articles/360044425951#background-overlay) -on your in-app templates to a color that complements your app, and test -existing templates before publishing. - -### Upgrading to 3.6.0 - -To enable Unknown User Activation, upgrade to [version 3.6.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.0) -of Iterable's Android SDK and call `setEnableUnknownUserActivation(true)` on -`IterableConfig.Builder` before initializing the SDK. These code changes are -only required if you want to use Unknown User Activation; otherwise, no -changes are required. - -```java -IterableConfig config = new IterableConfig.Builder() - .setEnableUnknownUserActivation(true) - .build(); - -IterableApi.initialize(context, "", config); -``` - -The SDK also captures user consent on your behalf when this feature is enabled. For full -setup instructions, read [Configure the Android SDK](https://support.iterable.com/hc/articles/40078934178836) -in the Unknown User Activation documentation. - -### Upgrading to 3.5.12 - -- **Supported Android versions**: Beginning with [version 3.5.12](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.12), - Iterable's Android SDK supports Android versions 5.0 (API level 21) and - higher. - -- **Disabling encryption**: By default, encryption is enabled to securely store - sensitive user data. To disable keychain encryption, set the - `setKeychainEncryption` option to `false` when initializing the SDK: - - ```java - IterableConfig config = new IterableConfig.Builder() - .setKeychainEncryption(false) // Disable encryption for keychain storage - .build(); - - IterableApi.initialize(context, apiKey, config); - ``` - -### Upgrading to 3.5.3 - -Starting with [version 3.5.3](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.3), -Iterable's Android SDK provides more insight into JWT refresh failures, to help -you take appropriate action in your application code. - -When a JWT refresh fails (for any of various reasons), the SDK calls -`onAuthFailure(AuthFailure authFailure)` on the `IterableAuthHandler` instance -you provided to the SDK at initialization. The `AuthFailure` object provides -more information about the failure. - -`onAuthFailure(AuthFailure authFailure)` replaces `onTokenRegistrationFailed(Throwable object)`. -If you've implemented that method, you'll need to update your application code. - -For more information, see [Step 5.6.1: Register an auth handler](#step-5-6-1-register-an-auth-handler). - -### Upgrading to 3.5.2 - -When upgrading to [version 3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) -of the SDK, you can make use of the `setAuthRetryPolicy` method on `IterableConfig` -to specify: - -- The maximum number of consecutive JWT-related request failures the SDK should - allow before giving up, Defaults to 10. -- The interval between each retry attempt. Defaults to 6 seconds. -- A backoff strategy: linear or exponential. Defaults to linear. - -### Upgrading to 3.4.10 - -In Android apps with `minSdkVersion` 23 or higher ([Android 6.0](https://developer.android.com/studio/releases/platforms#6.0)) -Iterable's Android SDK now encrypts the following fields when storing them at -rest: - -- `email` — The user's email address. -- `userId` — The user's ID. -- `authToken` — The JWT used to authenticate the user with Iterable's API. - -(Note that Iterable's Android SDK does not store the last push payload at -rest—before or after this update.) - -For more information about this encryption in Iterable's Android SDK, examine -the source code for [`IterableKeychain`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi/src/main/java/com/iterable/iterableapi/IterableKeychain.kt), -a file in Iterable's Android SDK. - -This release also allows you to have your Android apps (regardless of `minSdkVersion`) -store in-app messages in memory, rather than in an unencrypted local file. -However, an unencrypted local file is still the default option. - -To store in-app messages in memory, set the `setUseInMemoryStorageForInApps(true)` -SDK configuration option (defaults to `false`): - -```java -IterableConfig config = new IterableConfig.Builder() - // ... other configuration options ... - .setUseInMemoryStorageForInApps(true).build(); -IterableApi.initialize(context, "", config); -``` - -When users upgrade to a version of your Android app that uses this version of -the SDK (or higher), and you've set this configuration option to `true`, the -local file used for in-app message storage (if it already exists) is deleted -However, no data is lost. - -#### API level 22 and lower - -If your app targets API level 23 or higher, this is a standard SDK upgrade, with -no special instructions. - -If your app targets an API level less than 23, you'll need to make the following -changes to your project (which allow your app to build, even though it won't -encrypt data): - -1. In `AndroidManifest.xml`, add `` - -2. In your app's `app/build.gradle`: - - Add `multiDexEnabled true` to the `default` object, under `android`. - - Add `implementation androidx.multidex:multidex:2.0.1` to the `dependencies`. - -### Upgrading to 3.4.0 - -- Starting with version [3.4.0](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.4.0) - of Iterable's Android SDK, you'll need to declare the URL protocols that - the SDK should expect to see on incoming links (and then handle as needed). For - more information, read about [Step 5.3: Set allowed URL protocols](#step-5-3-set-allowed-url-protocols), - above. - -- Version 3.4.0 changes two static methods on the `IterableApi` class, `handleAppLink` - and `getAndTrackDeepLink`, to instance methods. To call these methods, you'll - need to first grab an instance of the `IterableApi` class by calling - `IterableApi.getInstance()`. For example, `IterableApi.getInstance().handleAppLink(...)`. - -### Upgrading to 3.3.1 - -To resolve a breaking change introduced in Firebase Cloud Messaging -[version 22.0.0](https://firebase.google.com/support/release-notes/android#messaging_v22-0-0), -[version 3.3.1](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.3.1) -of Iterable's Android SDK bumps the minimum required version of its -Firebase Android dependency to [20.3.0](https://firebase.google.com/support/release-notes/android#messaging_v20-3-0). - -If upgrading to version 3.3.1 causes your app to crash on launch, or your build -to fail, add the following lines to your app's `build.gradle` file: - -```groovy -android { - ... - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - ... -} -``` - -### Upgrading to 3.2.0 - -[Versions 3.2.0 and higher](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.2.0) -depend on the [AndroidX](https://developer.android.com/jetpack/androidx) support -libraries. To use these versions, you'll need to [migrate your app to use AndroidX](https://developer.android.com/jetpack/androidx/migrate). - -### Upgrading from a version prior to 3.1.0 - -- In-app messages - - - `spawnInAppNotification` - - The `spawnInAppNotification` method is no longer needed and will fail to - compile. The SDK now displays in-app messages automatically. There is no need - to poll the server for new messages. - - - Handling manually - - To control when in-app messages display (rather than displaying them - automatically), set `IterableConfig.inAppHandler` (an `IterableInAppHandler` - object). From its `onNewInApp` method, return `InAppResponse.SKIP`. - - To get the queue of available in-app messages, call - `IterableApi.getInstance().getInAppManager().getMessages()`. Then, call - `IterableApi.getInstance().getInAppManager().showMessage(message)` to show a - specific message. - - - Custom actions - - This version of the SDK reserves the `iterable://` URL scheme for - Iterable-defined actions handled by the SDK and the `action://` URL scheme for - custom actions handled by the mobile application's custom action handler. - - If you are currently using the `itbl://` URL scheme for custom actions, the SDK - will still pass these actions to the custom action handler. However, support - for this URL scheme will eventually be removed (timeline TBD), so it is best to - move templates to the `action://` URL scheme as it's possible to do so. - -- Deep links - - - Consolidated deep link URL handling. By default, the SDK handles deep links - with the the URL handler assigned to `IterableConfig`. - - - Checking if a URL is an Iterable deep link: To check if a URL is an Iterable - deep link before handling it, use the `isIterableDeepLink` method: - - ```java - if (IterableApi.getInstance().isIterableDeepLink(urlString)) { - // URL is an Iterable deep link - } - ``` - - This method returns `true` if the URL matches the Iterable deep link pattern - (URLs containing `/a/` in the path). For more information, read - [Android App Links](https://support.iterable.com/hc/articles/360035127392). - -### Migrating from GCM to FCM - -To migrate from GCM (Google Cloud Messaging) to Firebase (Firebase Cloud Messaging) - -- Upgrade the existing Google Cloud project to Firebase. -- Update the server token in the existing GCM-based Iterable push integration, - applying the new Firebase token. -- Update the Android app to support Firebase. - -If you use the same project and integration name for Firebase Cloud Messaging, the -old tokens remain valid and you won't need to re-register existing devices. If -you're using a new project for Firebase Cloud Messaging, and have existing -devices on a GCM project with a different sender ID: - -- Updating the app will generate new tokens for users, but the old tokens remain valid. -- When migrating from one sender ID to another, when initializing Iterable's SDK, - specify `legacyGCMSenderId` on`IterableConfig`. This disables old tokens to make - sure users won't receive duplicate notifications. - -## Troubleshooting - -If you're having trouble installing or initializing the SDK, read -[Testing and Troubleshooting the Iterable SDK](https://support.iterable.com/hc/articles/360035392251). - -## Further reading - -- [Identifying the User](https://support.iterable.com/hc/articles/360035402531) -- [Updating User Profiles](https://support.iterable.com/hc/articles/360035402611) -- [Tracking Events with Iterable's Mobile SDKs](https://support.iterable.com/hc/articles/360035395671) -- [Setting up Android Push Notifications](https://support.iterable.com/hc/articles/115000331943) -- [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231) -- [Embedded Messages with Iterable's Android SDK](https://support.iterable.com/hc/articles/23061877893652). -- [Setting up Mobile Inbox on Android](https://support.iterable.com/hc/articles/360038744152) -- [Customizing Mobile Inbox on Android](https://support.iterable.com/hc/articles/360039189931) -- [Android App Links](https://support.iterable.com/hc/articles/360035127392) -- [Deep Links in Push Notifications](https://support.iterable.com/hc/articles/360035453971) -- [Sample Apps that use Iterable's Android SDK](https://github.com/Iterable/iterable-android-sdk/tree/master/sample-apps/inbox-customization) -- [Configuring Deep Links for Email or SMS](https://support.iterable.com/hc/articles/115002651226) -- [JWT-Enabled API Keys](https://support.iterable.com/hc/articles/360050801231) diff --git a/sources/android/configure-the-android-sdk.md b/sources/android/configure-the-android-sdk.md deleted file mode 100644 index 882564b..0000000 --- a/sources/android/configure-the-android-sdk.md +++ /dev/null @@ -1,346 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/40078934178836 -title: Configure the Android SDK -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/unknown-user-activation-dev/configure-the-android-sdk/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: fa441fa69f35c816affd3df2d565dbfbd2727ca3 -fetched_at: 2026-05-25T15:11:48.790Z ---- -# Configure the Android SDK - -Follow these instructions to set up Iterable's Android SDK for -Unknown User Activation. For general guidance about setting up Iterable's -Android SDK, see [Iterable's Android SDK](https://support.iterable.com/hc/articles/360035019712). - -:::tip NOTE -Sample code shown in the following sections is for demonstration purposes only; -it's not exhaustive, and it's not meant to be used directly in your Android -app. Instead, use it as a guide to understand the various ways you'll interact -with the SDK when setting up and using Unknown User Activation. -::: - -## In this article - -[[toc]] - -## Step 1: Import SDK methods - -To use Iterable's SDK in a given file, you'll need to import various -classes. - -```kotlin -import com.iterable.iterableapi.AuthFailure -import com.iterable.iterableapi.IterableApi -import com.iterable.iterableapi.IterableConfig -import com.iterable.iterableapi.IterableUnknownUserHandler -import com.iterable.iterableapi.IterableAuthHandler -import com.iterable.iterableapi.IterableIdentityResolution -``` - -## Step 2: Initialize the SDK and set up callbacks - -Initialize the SDK and, if necessary, set up some JWT and unknown user -callbacks. - -```kotlin -// -// This example creates the IterableConfig in the main activity, but you can create it in -// another place that's convenient for your app's architecture, if necessary. -// -class MainActivity : AppCompatActivity(), IterableUnknownUserHandler, IterableAuthHandler { - - override fun onCreate(savedInstanceState: Bundle?) { - config = IterableConfig.Builder() - .setAuthHandler(this) - .setEnableUnknownUserActivation(true) - .setUnknownUserHandler(this) - .setEventThresholdLimit(100) - .setIdentityResolution(IterableIdentityResolution(true, true)) - .build() - IterableApi.initialize(this, , config) - } - - // - // Fetch a new JWT token for the current or unknown user, from your server. - // Then, return it as a string. - // - override fun onAuthTokenRequested(): String { - // ... - return "" - } - - // - // Handle failures that occur when fetching JWT tokens. - // - override fun onAuthFailure(authFailure: AuthFailure?) { - // ... - } - - // - // The SDK calls onTokenRegistrationSuccessful after onAuthTokenRequested - // returns a non-null JWT token. However, other than a null check, the SDK does not - // validate the token before calling this method. You can leave this method empty. - // - override fun onTokenRegistrationSuccessful(authToken: String?) { - } - - // - // Callback for the SDK to invoke after it creates a userId for an unknown user. - // If necessary, use this method to pass the new userId to your server. - // - override fun onUnknownUserCreated(userId: String) { - // ... - } -} -``` - -Create an `IterableConfig` object, and set the following options: - -1. Only if your Android app uses a JWT-enabled API key, set the `setAuthHandler` - field to an object that implements the `IterableAuthHandler` interface. In the - previous example, the main activity itself implements `IterableAuthHandler`. - - :::warning IMPORTANT - If you don't use a JWT-enabled API key in your Android app, do not set this - field. Setting up auth handlers when they're not needed could lead to unexpected - behavior or errors in the SDK initialization process. - ::: - - The `IterableAuthHandler` object should provide implementations for two methods, - and an empty implementation for another: - - - `onAuthTokenRequested` – The SDK calls this method when it needs a new JWT - token for the current unknown or known user. This method should fetch (from - your server) a new JWT token for a user, and then return it as a string. - `onAuthTokenRequested` is called when: - - The SDK needs a JWT token for a new unknown user. - - You identify a user by calling `setEmail` or `setUserId`. - - You update a user's email address by calling `updateEmail`. - - The current JWT token has expired, or is about to expire. - - The SDK receives a JWT-related 401 response from Iterable's API. - - `onAuthTokenRequested` can be called to fetch and return a JWT token for: - - An unknown user - `IterableApi.getInstance().getUserId()` (since, - for unknown users, the SDK always generates a `userId` — a UUID). - - A known user - `IterableAPI.getInstance().getUserId()` or - `IterableAPI.getInstance.getEmail()`, whichever value you used to - identify the user. - - - `onAuthFailure` – The SDK calls `onAuthFailure` after failing to fetch a - JWT token. The `AuthFailure` object passed to this method describes the - reason for the failure, along with other information. - - - `onTokenRegistrationSuccessful` – The SDK calls - `onTokenRegistrationSuccessful` after `onAuthTokenRequested` returns a - non-null JWT token. However, other than a null check, the SDK does not - validate the token before calling this method. You can leave this method - empty. - -2. To enable Unknown User Activation, set `setEnableUnknownUserActivation` to `true`. - This property defaults to `false`. - -3. (Optional, but strongly recommended for JWT-enabled API keys) To provide a - callback for the SDK to call after it creates a `userId` for a new unknown - user, set `setUnknownUserHandler` to an object that implements the - `IterableUnknownUserHandler` interface. - - The SDK calls the `onUnknownUserCreated` method on this object after a - visitor satisfies your project's unknown user creation criteria and the SDK - generates a `userId` for that user's new unknown user profile — but before the - SDK attempts to fetch a JWT token for the user. - - For example, you might use this method to tell your server about the - SDK-generated unknown userId, to give your server context for subsequent JWT - token requests for that same `userId` (however, to do this, you'll need to - set up an authenticated web service on your server for this method to call). - - :::warning IMPORTANT - If your app uses a JWT-enabled API key, your JWT server needs to know about - SDK-generated unknown `userId` values before it can issue tokens for them. - This callback is the mechanism for that — without it, your JWT server may - fail to issue tokens for new unknown users. - ::: - -4. Set `setEventThresholdLimit` to indicate how many of a visitor's most recent - events the SDK should save in local storage, so that they can be synced later - to Iterable when the user satisfies your profile creation criteria and - receives an unknown user profile. - - This value defaults to `100`. If a visitor triggers more than the maximum number - of events before converting to an unknown user, the first events that were - saved are the first to be deleted. - -5. `setIdentityResolution` – If necessary, use this setter to override the - SDK's default `IterableIdentityResolution` object, which specifies values for - the SDK to use when working with unknown and known users (if you don't specify - an object here, both the following properties default to `true`). - - - `replayOnVisitorToKnown` – When you identify a visitor by calling - `setEmail` or `setUserId`, this field specifies whether the SDK should - replay locally saved visitor data to their known user profile in Iterable. - Defaults to `true`. (When an unknown user profile is first created, the SDK - always replays locally saved data to that profile — this setting only - controls what happens when you identify a visitor before they receive an - unknown user profile.) - - `mergeOnUnknownToKnown` – When you identify an unknown user by calling - `setEmail` or `setUserId`, this field specifies whether the SDK should merge - the unknown user profile with the identified user profile. Defaults to - `true`. (If the identified user profile does not yet exist, a merge - operation creates it. When `mergeOnUnknownToKnown` is `false`, the new user - profile is not created until the SDK tracks a user update or an event. - Without a merge, data on the unknown user profile is lost.) - - :::tip NOTE - If you don't provide an `identityResolution` value, the SDK defaults both - `replayOnVisitorToKnown` and `mergeOnUnknownToKnown` to `true`. You can - override these fields each time you call `setEmail` and `setUserId`. - ::: - - :::tip NOTE - The Android and Web SDKs use `mergeOnUnknownToKnown`, while the iOS SDK - uses `mergeOnUnknownUserToKnown`. - ::: - -6. `setEnableForegroundCriteriaFetch` – Controls whether the SDK re-fetches - profile creation criteria when the app is foregrounded. Defaults to `true`. - Set to `false` if you only want criteria fetched on app launch. - -7. After creating `IterableConfig`, pass it to the `initialize` method on -`IterableApi`, alongside your API key. - -## Step 3: Get user consent and track local data - -Before telling the SDK to track local data about the current visitor, get their -consent to do so. Then, explicitly tell the SDK to start tracking local data. - -```kotlin -// When consent is given -IterableApi.getInstance().setVisitorUsageTracked(true) - -// When consent is revoked (clears locally stored data) -IterableApi.getInstance().setVisitorUsageTracked(false) - -// Track custom events -IterableApi.getInstance().track(eventName, dataFields) - -// Track purchase events -IterableApi.getInstance().trackPurchase(total, items) - -// Track cart update events -IterableApi.getInstance().updateCart(items) - -// Update the user profile -IterableApi.getInstance().updateUser(dataFields) -``` - -When you have user consent, call `setVisitorUsageTracked(true)`. When you do -this: - -- The SDK fetches profile creation criteria for your Iterable project (and - refreshes them on each app launch). Criteria also refreshes on foregrounding - if `enableForegroundCriteriaFetch` is `true` (the default). -- For subsequent calls that track cart updates, purchases, custom events, and - user updates, the SDK stores data in local storage (this data isn't sent to - Iterable yet — it's only stored locally). - -If the user revokes consent, call `setVisitorUsageTracked(false)`. This clears -any locally saved visitor data, and it prevents local storage of visitor data -until `setVisitorUsageTracked(true)` is called again. - -To track events and user updates, call various methods on `IterableApi`, as -shown above. - -:::warning WARNING -Calling `setVisitorUsageTracked(true)` clears any previously stored local visitor -data (events, user updates, and session data) before starting fresh tracking. -If your app calls this method on each launch, any visitor data stored from a -previous session that was not yet synced to Iterable will be lost. -::: - -## Step 4: Create an unknown user profile - -If and when the visitor to your app satisfies your Iterable project's profile -creation criteria, the SDK creates an unknown user profile for them. - -1. The SDK generates a `userId` (a UUID) to identify the new unknown user profile. -2. The SDK calls `POST /api/unknownuser/events/session` to create the unknown user - profile in Iterable and adds an `unknownSession` event on the profile. -3. The SDK calls your `onUnknownUserCreated` callback, as described above. -4. If you're using a JWT-enabled API key, the SDK then calls the - `onAuthTokenRequested` method you provided, to fetch (from your server) a JWT - token for the new unknown `userId`. This is the first time the SDK will call - this method for this user. -5. The SDK replays locally saved visitor data (cart updates, purchase, user profile - updates, and custom events) to the unknown user profile in Iterable, and then - removes it from local storage. - -:::tip NOTE -Consent tracking occurs later, after device registration completes, rather than -as part of the `unknownSession` creation flow described above. -::: - -:::tip NOTE -This differs from the iOS SDK, which logs consent after fetching a JWT token, -and from the Web SDK, which logs consent before creating the -`unknownSession` event. -::: - -## Step 5: Identify the user - -When you know the current user's user ID or email (depending on your Iterable -project type), provide it to the SDK by calling `setUserId` or `setEmail` on -`IterableAPI`. - -```kotlin -// Identify the user by email or userId, providing an identity resolution -// override if necessary. -IterableApi.getInstance().setEmail(email, identityResolutionOverride) -IterableApi.getInstance().setUserId(userId, identityResolutionOverride) -``` - -When you identify the user: - -**If the current user is a visitor** (a user who doesn't have an unknown profile -in Iterable, because they haven't yet satisfied your Iterable project's profile -creation criteria): - -- If `replayOnVisitorToKnown` is set to `true`, the SDK: - - Calls `onAuthTokenRequested` to fetch a JWT token for the known user profile - (if you're using a JWT-enabled API key). - - Sends visitor data from the app's local storage (user profile data and - events) to the known user profile in Iterable. Sending this data to Iterable - creates the known user profile in Iterable if it doesn't already exist. - - Clears visitor data from local storage. - - Sends future user updates and events to the known user profile to Iterable - (not local storage). - -- If `replayOnVisitorToKnown` is set to `false`, the SDK: - - Calls `onAuthTokenRequested` to fetch a JWT token for the known user profile - (if you're using a JWT-enabled API key). - - Clears visitor data from local storage, without sending it to Iterable. - - Sends future user updates and events to the known user profile in Iterable - (not to local storage). If the known user profile doesn't yet exist - in your Iterable project, these updates create it. - -**If the current user is unknown** (has an unknown user profile in Iterable): - -- If `mergeOnUnknownToKnown` is set to `true`, the SDK: - - Calls `onAuthTokenRequested` to fetch a JWT token for the known user profile - (if you're using a JWT-enabled API key). - - Calls the User Merge API to merge the unknown user profile with the known - user profile (including all data). - - If the known profile doesn't yet exist, the user ID or email of the source - profile are updated. - - The API deletes the unknown profile. - It can take a few minutes for all of the data from the unknown user profile - to appear on the known user profile. - -- If `mergeOnUnknownToKnown` is set to `false`, the SDK: - - Calls `onAuthTokenRequested` to fetch a JWT token for the known user profile - (if you're using a JWT-enabled API key). - - Does not call the User Merge API. - - Sends future user updates and events to Iterable, to the known user profile - (not to the unknown user profile). The unknown user profile remains in - Iterable. \ No newline at end of file diff --git a/sources/android/customizing-mobile-inbox-on-android.md b/sources/android/customizing-mobile-inbox-on-android.md deleted file mode 100644 index 403e3f4..0000000 --- a/sources/android/customizing-mobile-inbox-on-android.md +++ /dev/null @@ -1,650 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/360039189931 -title: Customizing Mobile Inbox on Android -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/in-app-messages/customizing-mobile-inbox-on-android/index.md -source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 -source_sha: e156e9f1bf13e4b41507a53dc10093422c8aefac -fetched_at: 2026-08-03T20:41:29.489Z ---- -# Customizing Mobile Inbox on Android - -A [mobile inbox](https://support.iterable.com/hc/articles/217517406) provides an -app-specific place for users to save in-app messages to read later. - -Iterable's [Android SDK](https://support.iterable.com/hc/articles/360035019712) -includes a default user interface for a mobile inbox, and you can customize it -to match your organization's branding and styles, and to display any necessary -fields. - -This document describes different ways to customize the mobile inbox provided by -Iterable's Android SDK. - -## In this article - -[[toc]] - -## Setting up the mobile inbox - -Before customizing your app's mobile inbox, read -[Setting up Mobile Inbox on Android](https://support.iterable.com/hc/articles/360038744152) -to learn how to set it up and display it. - -## Sample app - -![Sample app](https://iterable.zendesk.com/hc/article_attachments/360050615391/sample-app.png "Sample app") - -To better undersand how to customize your app's mobile inbox, take a look at the -code in the **Inbox Customization** [sample project](https://github.com/Iterable/iterable-android-sdk/tree/master/sample-apps/inbox-customization) -(found in the same GitHub repository as Iterable's Android SDK). - -## Customizing the mobile inbox - -This section describes how to customize the user interface of the mobile inbox -embedded in your Android mobile app. - -:::tip NOTE -Some customizations require you to create a subclass of `IterableInboxFragment`, -and others do not. -::: - -### Empty state - -In an empty mobile inbox, you can display custom text (title and body) to help -orient your users. These values are blank by default, and they'll wrap to -multiple lines if needed. For example: - -![Android mobile inbox empty state](https://iterable.zendesk.com/hc/article_attachments/360091087811/android-inbox-empty-state.png "Android mobile inbox empty state") - -Use this code to set this text when using the mobile inbox fragment: - -_Kotlin_ - -```kotlin -var bundle = Bundle() -bundle.putString(IterableConstants.NO_MESSAGES_TITLE,"No saved messages") -bundle.putString(IterableConstants.NO_MESSAGES_BODY, "Check again later!") -val fragment: Fragment = Fragment.instantiate(this, IterableInboxFragment::class.java.name, bundle)) -``` - -Use this code to set the text when using the mobile inbox activity: - -_Kotlin_ - -```kotlin -var intent = Intent(this.context,IterableInboxActivity::class.java) -intent.putExtra(IterableConstants.NO_MESSAGES_TITLE, "No saved messages") -intent.putExtra(IterableConstants.NO_MESSAGES_BODY, "Check again later!") -startActivity(intent) -``` - -_Java_ - -```java -startActivity( - new Intent(getApplicationContext(),IterableInboxActivity.class) - .putExtra(IterableConstants.NO_MESSAGES_TITLE,"No saved messages") - .putExtra(IterableConstants.NO_MESSAGES_BODY,"Check again later!") -); -``` - -### Message display style (popup or navigation) - -A mobile inbox can display messages as popups directly in the inbox view (the -default) or as standalone activities. To change this setting, either: - -- Set an extra for the activity's intent: - - _Kotlin_ - - ```kotlin - val intent = Intent(context, IterableInboxActivity::class.java) - intent.putExtra("inboxMode", InboxMode.ACTIVITY) - startActivity(intent) - ``` - - _Java_ - - ```java - Intent intent = new Intent(getContext(), IterableInboxActivity.class); - intent.putExtra("inboxMode", InboxMode.ACTIVITY); - startActivity(intent); - ``` - -- Pass constructor parameters to the fragment: - - _Kotlin_ - - ```kotlin - val inboxFragment = IterableInboxFragment.newInstance(InboxMode.ACTIVITY, 0) - ``` - - _Java_ - - ```java - IterableInboxFragment inboxFragment = IterableInboxFragment.newInstance(InboxMode.ACTIVITY) - ``` - -### Activity title - -When launching the mobile inbox as an activity, change the title by passing an -`activityTitle` argument in the intent: - -_Kotlin_ - -```kotlin -val intent = Intent(context, IterableInboxActivity::class.java) -intent.putExtra("activityTitle", "My Inbox") -startActivity(intent) -``` - -_Java_ - -```java -Intent intent = new Intent(getContext(), IterableInboxActivity.class); -intent.putExtra("activityTitle", "My Inbox"); -startActivity(intent); -``` - -### Inbox toolbar (SDK v3.9.0 and above) - -Starting with SDK version 3.9.0, you can display an optional toolbar above the -inbox list using `IterableInboxToolbarView`. The toolbar is off by default, -so the inbox behaves exactly as it did in previous SDK versions unless you opt -in. - -Configure the toolbar with the `InboxToolbarOption` sealed interface, which has -these options: - -- `None` (default) — No toolbar. -- `Default` — A title-only toolbar above the inbox list. -- `WithBackButton` — A title plus a back-navigation icon. By default, the back - action calls `OnBackPressedDispatcher`. To customize it, have your host - `Activity` or parent `Fragment` implement `IterableInboxToolbarBackListener`. -- `Custom(layoutRes)` — Inflates your own toolbar layout. To wire your layout to - the SDK, tag views with these reserved IDs (both are optional): - - `@id/iterable_reserved_inbox_toolbar_action` — Automatically wired to the - SDK's back handler. - - `@id/iterable_reserved_inbox_toolbar_title` — Automatically bound to the - toolbar title. - -:::warning IMPORTANT -When the toolbar is enabled, the host activity must use a `Theme.AppCompat` -descendant. -::: - -#### Configure the toolbar on the fragment - -Pass an `InboxToolbarOption` (and, optionally, a title) to -`IterableInboxFragment.newInstance(...)`: - -_Kotlin_ - -```kotlin -val inboxFragment = IterableInboxFragment.newInstance( - InboxToolbarOption.WithBackButton, - "My Inbox" -) -``` - -_Java_ - -```java -IterableInboxFragment inboxFragment = IterableInboxFragment.newInstance( - InboxToolbarOption.WithBackButton.INSTANCE, - "My Inbox" -); -``` - -#### Configure the toolbar on the activity - -When launching the inbox as an activity, set the `TOOLBAR_OPTION` and -`TOOLBAR_TITLE` intent extras: - -_Kotlin_ - -```kotlin -val intent = Intent(context, IterableInboxActivity::class.java) -intent.putExtra(IterableInboxFragment.TOOLBAR_OPTION, InboxToolbarOption.WithBackButton) -intent.putExtra(IterableInboxFragment.TOOLBAR_TITLE, "My Inbox") -startActivity(intent) -``` - -_Java_ - -```java -Intent intent = new Intent(getContext(), IterableInboxActivity.class); -intent.putExtra(IterableInboxFragment.TOOLBAR_OPTION, InboxToolbarOption.WithBackButton.INSTANCE); -intent.putExtra(IterableInboxFragment.TOOLBAR_TITLE, "My Inbox"); -startActivity(intent); -``` - -### Cell layout, colors, and font - -![Inbox cells with a custom layout](https://iterable.zendesk.com/hc/article_attachments/360050615411/custom-layout.png "Inbox cells with a custom layout") - -:::tip TIP -In the [sample app](#sample-app), tap **Inbox with Custom Cell** to see an -example of an inbox that uses custom cells. -::: - -To modify the font, color or layout of inbox cells: - -1. Copy the [`iterable_inbox_item.xml`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/res/layout/iterable_inbox_item.xml) - layout file from [`iterableapi-ui`](https://github.com/Iterable/iterable-android-sdk/tree/master/iterableapi-ui/src/main/res/layout). - Give it a new name, such as `custom_inbox_item.xml`. - -2. In the new file, change the layout, colors and fonts to match your app’s - styles. - -3. Specify this layout ID when launching the activity: - - _Kotlin_ - - ```kotlin - val intent = Intent(context, IterableInboxActivity::class.java) - intent.putExtra("itemLayoutId", R.layout.custom_inbox_item) - startActivity(intent) - ``` - - _Java_ - - ```java - Intent intent = new Intent(getContext(), IterableInboxActivity.class); - intent.putExtra("itemLayoutId", R.layout.custom_inbox_item); - startActivity(intent); - ``` - -4. Alternatively, create the fragment with custom parameters: - - _Kotlin_ - - ```kotlin - val inboxFragment = IterableInboxFragment.newInstance(InboxMode.POPUP, R.layout.custom_inbox_item) - ``` - - _Java_ - - ```java - IterableInboxFragment inboxFragment = IterableInboxFragment.newInstance(InboxMode.POPUP, R.layout.custom_inbox_item); - ``` - -### Date format and visibility - -![Inbox with custom date format](https://iterable.zendesk.com/hc/article_attachments/360050615451/change-date-format.png "Inbox with custom date format") - -:::tip TIP -In the [sample app](#sample-app), tap **Change Date Format** to see an -example of an inbox that uses custom cells. -::: - -To change the format or visibility of the date field for each message cell, -subclass `IterableInboxFragment` and set a date mapper in `onCreate`. The date -mapper takes an `IterableInAppMessage` and returns a string representing the -creation date of the message. If the date field should be blank, return `null`. - -_Kotlin_ - -```kotlin -class CustomInboxDateMapperFragment : IterableInboxFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setDateMapper { message -> - DateUtils.getRelativeTimeSpanString( - message.createdAt.time, - Date().time, - 0, - DateUtils.FORMAT_ABBREV_ALL - ) - } - } -} -``` - -_Java_ - -```java -public class CustomInboxDateMapperJavaFragment extends IterableInboxFragment implements IterableInboxDateMapper { - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setDateMapper(this); - } - - @Nullable - @Override - public CharSequence mapMessageToDateString(@NonNull IterableInAppMessage message) { - return DateUtils.getRelativeTimeSpanString( - message.getCreatedAt().getTime(), - new Date().getTime(), - 0, - DateUtils.FORMAT_ABBREV_ALL - ); - } -} -``` - -### Filtering messages - -![Inbox with filtered messages](https://iterable.zendesk.com/hc/article_attachments/360050615471/filtering-messages.png "Inbox with filtered messages") - -:::tip TIP -In the [sample app](#sample-app), tap **Filter by Message Type** or -**Filter by Message Title** to see an example of an inbox that uses custom -filtering. -::: - -To filter which messages are displayed in the mobile inbox, subclass -`IterableInboxFragment` and call `setFilter` in the `onCreate` method. The filter -should take an `IterableInAppMessage` and return a boolean: `true` to show -the message, `false` otherwise. - -`IterableInboxFilter` is an interface that declares a filter method. - -_Kotlin_ - -```kotlin -class CustomInboxFilterFragment : IterableInboxFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setFilter { message -> message.customPayload?.has("price") == true } - } -} -``` - -Kotlin (alternative implementation): - -```kotlin -class CustomInboxFilterFragment : IterableInboxFragment(), IterableInboxFilter { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setFilter(this) - } - - override fun filter(message: IterableInAppMessage): Boolean { - return message.customPayload?.has("price") == true - } -} -``` - -_Java_ - -```java -public class CustomInboxFilterFragment extends IterableInboxFragment implements IterableInboxFilter { - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setFilter(this); - } - - @Override - public boolean filter(@NonNull IterableInAppMessage message) { - JSONObject payload = message.getCustomPayload(); - return payload != null && payload.has("price"); - } -} -``` - -### Sorting messages - -![Inbox with sorted messages](https://iterable.zendesk.com/hc/article_attachments/360050505852/sorting-messages.png "Inbox with sorted messages") - -:::tip TIP -In the [sample app](#sample-app), tap **Sort by Title Ascending** or -**Sort by Date Ascending** to see an example of an inbox that changes the way -messages are sorted. -::: - -By default, Mobile Inbox sorts messages descending by date. However, it is -possible to sort the message order in other ways. - -To sort the messages in the mobile inbox, subclass `IterableInboxFragment` and -set a comparator in `onCreate`. `IterableInboxComparator` is a standard Java -`Comparator` interface: return a negative integer, zero, or a positive integer -when the first message is less than, equal to, or greater than the second. - -_Kotlin_ - -```kotlin -class CustomInboxComparatorFragment : IterableInboxFragment() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setComparator { message1, message2 -> - message1.createdAt.compareTo(message2.createdAt) // Sort by creation date ascending - } - } -} -``` - -Kotlin (alternative implementation): - -```kotlin -class CustomInboxComparatorFragment : IterableInboxFragment(), IterableInboxComparator { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setComparator(this) - } - - override fun compare(message1: IterableInAppMessage, message2: IterableInAppMessage): Int { - return message1.createdAt.compareTo(message2.createdAt) // Sort by creation date ascending - } -} -``` - -```java -public class CustomInboxComparatorJavaFragment extends IterableInboxFragment implements IterableInboxComparator { - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setComparator(this); - } - - @Override - public int compare(@NonNull IterableInAppMessage message1, @NonNull IterableInAppMessage message2) { - // Sort by creation date ascending - return message1.getCreatedAt().compareTo(message2.getCreatedAt()); - } -} -``` - -### Adding fields to the inbox cell - -![Inbox items with added fields](https://iterable.zendesk.com/hc/article_attachments/360050505872/adding-fields.png "Inbox items with added fields") - -:::tip TIP -In the [sample app](#sample-app), tap **Additional Fields** to see an example of -an inbox that has additional fields on each cell. -::: - -To add additional fields to the items displayed in the mobile inbox: - -1. Copy the [`iterable_inbox_item.xml`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/res/layout/iterable_inbox_item.xml) - layout file from [`iterableapi-ui`](https://github.com/Iterable/iterable-android-sdk/tree/master/iterableapi-ui/src/main/res/layout). - Give it a new name, such as `custom_inbox_item.xml`. - -2. Subclass `IterableInboxFragment` and set the adapter extension. The methods - are similar to the ones in a `RecyclerView` adapter, but instead of a - `RecyclerView.ViewHolder` class, the extension is a plain Java class. See - [`IterableInboxAdapterExtension`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/inbox/IterableInboxAdapterExtension.java) - for more details. - - - Return your custom layout in `getLayoutForViewType`. - - Create a static inner plain Java class for a `ViewHolderExtension`. - - Add fields referencing the new views in your layout. - - Create a constructor with calls to `findViewById` to populate those fields. - - In `createViewHolderExtension`, call your view holder extension’s - constructor and return the result. - - In `onBindViewHolder`, update the UI for the given inbox message using the - standard Iterable ViewHolder (holding references to the standard fields, like - `title`, `subtitle` and others) and your extension object (holding references - to your custom views). - -For a reference implementation, see the example in the next section. - -### Multiple cell layouts - -![Inbox with multiple cell types](https://iterable.zendesk.com/hc/article_attachments/360050505892/multiple-cell-layouts.png "Inbox with multiple cell types") - -:::tip TIP -In the [sample app](#sample-app), tap **Multiple Cell Types** to see an example -of an inbox that uses multiple cell types. -::: - -To display different inbox items with different interfaces, follow these steps: - -1. Copy the [`iterable_inbox_item.xml`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/res/layout/iterable_inbox_item.xml) - layout file from [`iterableapi-ui`](https://github.com/Iterable/iterable-android-sdk/tree/master/iterableapi-ui/src/main/res/layout). - Give it a new name, such as `custom_inbox_item.xml`. - -2. Subclass `IterableInboxFragment` and set the adapter extension. See - [`IterableInboxAdapterExtension`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/inbox/IterableInboxAdapterExtension.java) - for more details. - -3. Create integer constants for every type of cell you’re planning to have in - your custom inbox. - -4. Return those constants in `getItemViewType` by checking the inbox message - attributes. - -5. The same constants will then be passed to `getLayoutForViewType`. Use them to - return different layouts based on the view type. - -_Kotlin_ - -```kotlin -class CustomInboxFieldsFragment : IterableInboxFragment(), IterableInboxAdapterExtension { - val ITEM_TYPE_DEFAULT = 1 - val ITEM_TYPE_SALE = 2 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setAdapterExtension(this) - } - - override fun getItemViewType(message: IterableInAppMessage): Int { - if (message.customPayload?.has("price") == true) { - return ITEM_TYPE_SALE - } else { - return ITEM_TYPE_DEFAULT - } - } - - override fun getLayoutForViewType(viewType: Int): Int { - if (viewType == ITEM_TYPE_SALE) { - return R.layout.inbox_item_sale - } else { - return R.layout.inbox_item_default - } - } - - override fun createViewHolderExtension(view: View, viewType: Int): ViewHolder? { - if (viewType == ITEM_TYPE_SALE) { - return SaleViewHolder(view) - } else { - return null - } - } - - override fun onBindViewHolder(viewHolder: IterableInboxAdapter.ViewHolder, holderExtension: ViewHolder?, message: IterableInAppMessage) { - if (holderExtension is SaleViewHolder) { - holderExtension.price?.text = message.customPayload?.optString("price") - } - } - - open class ViewHolder - class SaleViewHolder(view: View) : ViewHolder() { - var price: TextView? = null - - init { - this.price = view.findViewById(R.id.price) - } - } -} -``` - -_Java_ - -```java - public class CustomInboxFieldsJavaFragment extends IterableInboxFragment implements IterableInboxAdapterExtension { - private static final int ITEM_TYPE_DEFAULT = 1; - private static final int ITEM_TYPE_SALE = 2; - - @Override - public int getItemViewType(@NonNull IterableInAppMessage message) { - JSONObject payload = message.getCustomPayload(); - if (payload != null && payload.has("price")) { - return ITEM_TYPE_SALE; - } else { - return ITEM_TYPE_DEFAULT; - } - } - - @Override - public int getLayoutForViewType(int viewType) { - if (viewType == ITEM_TYPE_SALE) { - return R.layout.inbox_item_sale; - } else { - return R.layout.inbox_item_default; - } - } - - @Nullable - @Override - public ViewHolder createViewHolderExtension(@NonNull View view, int viewType) { - if (viewType == ITEM_TYPE_SALE) { - return new SaleViewHolder(view); - } else { - return null; - } - } - - @Override - public void onBindViewHolder(@NonNull IterableInboxAdapter.ViewHolder viewHolder, @Nullable ViewHolder holderExtension, @NonNull IterableInAppMessage message) { - if (holderExtension instanceof SaleViewHolder) { - SaleViewHolder saleViewHolder = (SaleViewHolder) holderExtension; - JSONObject payload = message.getCustomPayload(); - if (payload != null) { - saleViewHolder.price.setText(payload.optString("price")); - } - } - } - - static class ViewHolder {} - static class SaleViewHolder extends ViewHolder { - private TextView price; - - SaleViewHolder(@NonNull View view) { - price = view.findViewById(R.id.price); - } - } -} -``` - -### Multiple sections - -Mobile Inbox on Android does not provide built-in support for multiple sections. - -## Further reading - -User guides: -- [In-App Messages and Mobile Inbox](https://support.iterable.com/hc/articles/217517406) -- [Sending In-App Messages](https://support.iterable.com/hc/articles/360034903151) -- [Events for In-App Messages and Mobile Inbox](https://support.iterable.com/hc/articles/360038939972) - -Developer documentation: -- Iterable's [iOS SDK](https://support.iterable.com/hc/articles/360035018152) -- Iterable's [Android SDK](https://support.iterable.com/hc/articles/360035019712) -- [In-App Messages Overview](https://support.iterable.com/hc/articles/360035538391) -- [In-App Messages on iOS](https://support.iterable.com/hc/articles/360035536791) -- [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231) -- [Setting up Mobile Inbox on iOS](https://support.iterable.com/hc/articles/360039137271) -- [Setting up Mobile Inbox on Android](https://support.iterable.com/hc/articles/360038744152) -- [Customizing Mobile Inbox on iOS](https://support.iterable.com/hc/articles/360039091471) -- [Animating In-App Messages with CSS](https://support.iterable.com/hc/articles/360035539271) -- [Image Carousels in In-App Messages](https://support.iterable.com/hc/articles/360035171132) -- [Testing and Troubleshooting In-App Messages](https://support.iterable.com/hc/articles/360035623391) -- [In-App Messages Without the SDK](https://support.iterable.com/hc/articles/360018709631) -- [Getting Started with Iterable's API](https://support.iterable.com/hc/articles/41044692130196) -- [API Endpoints and Sample Payloads](https://support.iterable.com/hc/articles/204780579) - diff --git a/sources/android/deep-linking-with-partners.md b/sources/android/deep-linking-with-partners.md deleted file mode 100644 index 6bed06a..0000000 --- a/sources/android/deep-linking-with-partners.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/360035536251 -title: Deep Linking With Partners -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/deep-links/deep-linking-with-partners/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 7473a924f2b7eac5a08f7ec66c3fbf60d07089e4 -fetched_at: 2026-05-25T15:11:46.035Z ---- -# Deep Linking With Partners - -Iterable supports deep linking without any third-party integrations—and also -with Branch and AppsFlyer. For more information about these integrations, read: - -- Iterable's [Branch documentation](https://support.iterable.com/hc/articles/360018347731) -- Iterable's [AppsFlyer documentation](https://support.iterable.com/hc/articles/360024366291) - - - - diff --git a/sources/android/embedded-messages-with-iterables-android-sdk.md b/sources/android/embedded-messages-with-iterables-android-sdk.md deleted file mode 100644 index 500e2a7..0000000 --- a/sources/android/embedded-messages-with-iterables-android-sdk.md +++ /dev/null @@ -1,754 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/23061877893652 -title: Embedded Messages with Iterable's Android SDK -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/embedded-messaging/embedded-messages-with-iterables-android-sdk/index.md -source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 -source_sha: 576150056520b366d5190411e9f28198e70bbeaf -fetched_at: 2026-08-03T20:41:31.068Z ---- -# Embedded Messages with Iterable's Android SDK - -:::tip NOTE -To add Embedded Messaging to your Iterable account, talk to your customer success -manager. -::: - -This article describes the steps you'll need to follow to use Iterable's Android -SDK to display embedded messages in your mobile app. - -This document describes only the steps necessary to use embedded messaging. To -learn about more SDK options, read [Iterable's Android SDK](https://support.iterable.com/hc/articles/360035019712). - -## In this article - -[[toc]] - -## Step 1: Coordinate with your marketing and design teams - -First, collaborate with your marketing and design teams to determine: - -- Where you'll display embedded messages in your apps (where your _placements_ go). -- Each placement's Iterable-assigned numeric ID (so you can display the right - messages in the right places). -- The data to display in each placement's messages. This determines what fields - your app will expect to find in an embedded message payload. -- The message design for each of your placements. - -## Step 2: Create a Mobile API key - -To use Iterable's Android SDK to display embedded messages, you'll need a mobile -API key. To learn how to create one, read [Creating API keys](https://support.iterable.com/hc/articles/360043464871#creating-api-keys) - -## Step 3: Install Iterable's Android SDK in your app - -To learn how to install Iterable's Android SDK in your app, read -[Iterable's Android SDK](https://support.iterable.com/hc/articles/360035019712). -Embedded Messaging is supported by versions [3.5.0+](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.0) -of Iterable's Android SDK. - -## Step 4: Configure the SDK - -Next, configure the SDK, specify a URL handler and a custom action handler, -enable embedded messaging, set allowed URL protocols, initialize the SDK with an -API key, and identify the user. - -For example: - -```kotlin -val config = IterableConfig.Builder() - .setUrlHandler(this) - .setCustomActionHandler(this) - .setEnableEmbeddedMessaging(true) - // Specify the URL schemes you're expecting to receive in the campaigns you - // send with Iterable. The SDK passes URLs with these URL schemes to your URL - // handler, which can then handle them as necessary. For example, if you - // indicate that "mycompany" is an allowed protocol, the SDK will pass URLs - // such as "mycompany://profile" to your URL handler, which can respond as - // needed. For example, for "mycompany://profile", it might deep link to - // the app's user profile screen. - .setAllowedProtocols(arrayOf("mycompany")) - .build() - -IterableApi.initialize(this, apiKey, config) - -IterableApi.getInstance().setEmail(email) -// IterableApi.getInstance().setUserId(userId) -``` - -:::tip NOTE -If your project is hosted on [Iterable's EDC](https://support.iterable.com/hc/articles/17572750887444), -you'll need to [configure the SDK appropriately](https://support.iterable.com/hc/articles/360035019712#step-5-2-if-necessary-configure-the-sdk-to-use-iterable-s-edc). -::: - -### Step 4.1: Define a URL handler - -In the example SDK configuration code above, notice the call to `setUrlHandler` -on `IterableConfig`. The SDK uses the object (which must implement interface -`IterableUrlHandler`) passed to this method to handle two types of URLs: - -- Standard URLs, such as `https://example.com/product/123`. -- URLs with allowed custom URL schemes, such as `mycompany://profile`. The SDK - ignores URLs that use custom URL schemes not specified as allowed protocols - on `IterableConfig` (see above). - - -When a user clicks a button or a link on an incoming message, and the click is -associated with one of the two URL types listed above, the SDK passes that URL to -the URL handler. Then, the URL handler can handle (or not handle) it as it makes -sense. For example, it might open the link in a browser or open it as a deep link. - -The object you provide to `setUrlHandler` must implement interface -`IterableUrlHandler`, which defines this method: - -```java -boolean handleIterableURL( - @NonNull Uri uri, - @NonNull IterableActionContext actionContext -); -``` - -This method should return `true` for URLs it can handle, and `false` for URLs it -cannot handle. - -When the URL handler can't handle a given URL, the SDK checks for another -installed app that can handle that URL (for example, a web browser). If there -are no other apps that can handle the URL, the SDK does nothing. - -Here's an example implementation: - -```kotlin -override fun handleIterableURL(uri: Uri, actionContext: IterableActionContext): Boolean { - val urlString = uri.toString() - // For example, urlString might be: "mycompany://profile" - if (urlString.contains("mycompany://profile")) { - // Navigate the user to the profile page ... - return true - } - return false -} -``` - -In this sample, `handleIterableUrl` handles `mycompany://profile` URLs by -navigating the user to the user profile screen. Then, it returns `true` to -indicate that it handled the URL. - -Implementations of this method will vary, depending on your app architecture -and the URLs you need to handle. However, it's important to remember that this -method is shared across message mediums. Make sure to handle URLs you might -receive from any message type, not just embedded messages. - -### Step 4.2: Define a custom action handler - -Custom actions represent custom functionality you'd like your app to execute — -maybe a deep link, a style update, or another behavior of some kind. Custom -actions use the `action://` URL scheme. - -:::tip NOTE -In-app messages also support `iterable://dismiss` and `iterable://delete` custom -actions. Embedded messages do not yet support these actions. The `iterable://` -URL scheme is reserved for Iterable-specific actions pre-defined by the SDK. - -For tips on alternatives to a dismiss action, read [Closing, dismissing, or hiding an embedded message](#closing-dismissing-or-hiding-an-embedded-message). -::: - -Similar to your URL handler, the object specified on `IterableConfig` as your -custom action handler serves as your app's central handler for custom actions. -When a user clicks a button or a link associated with a custom action, this -action is passed to your custom action handler, where you can deal with it -however necessary (usually by executing custom functionality of some kind). - -Your custom action handler must implement interface `IterableCustomActionHandler`, -which defines this method: - -```java -boolean handleIterableCustomAction( - @NonNull IterableAction action, - @NonNull IterableActionContext actionContext -); -``` - -This method should return `true` when it can handle the custom action URL, and -`false` when it cannot. When it returns `false`, the custom action is dropped — -since custom actions are special, non-standard URLs, they cannot be opened by -a web browser. - -For example, here's a sample custom action handler that handles an -`action://joinClass/1` URL: - -```kotlin -override fun handleIterableCustomAction( - action: IterableAction, - actionContext: IterableActionContext -): Boolean { - // The custom action's type is stored in action.type - if (action.type?.contains("joinClass")) - // Sign the user up for a class ... - return true - } - return false -} -``` - -Implementations of this method will vary. However, be sure to account for any -specific custom actions you'll send to your app (in an embedded message, or in -any other kind of message). - -### Closing, dismissing, or hiding an embedded message - -Embedded messages don't have a native "dismiss" action like in-app messages do. -However, you can implement custom logic to control when an embedded message is -displayed to a user. Here are some strategies you can use: - -- **Set an expiration date for the campaign:** Set an expiration date for your - embedded message campaign in Iterable. Once the campaign expires, the message - is no longer displayed to users. This method does not allow end-users to dismiss - the message actively, however. - -- **Implement custom logic based on eligibility criteria:** - For more dynamic control, you can create a solution that changes a user's - eligibility criteria for an embedded message campaign. When a user's eligibility - changes such that they no longer meet the targeting criteria, the embedded message - is no longer returned in their message array the next time the page is - refreshed, or automatically via silent push. - - This approach typically involves: - - - **Updating user profile or list membership:** You can use an Iterable SDK - or API to update a user's profile fields or their membership in a list. This - update can be triggered by a user action (for example, tapping a button in your - app). - - - **Leveraging custom events and journeys:** You can track a custom event in - Iterable when a specific user action occurs (such as a `message_dismissed` - event), then use the event to trigger a journey in Iterable. Within the - journey, you can perform a user profile update or a list membership update - that removes the user from the campaign's eligibility criteria. - -Specific implementation details vary depending on your application, SDK, and -desired user experience. - -## Step 5: Enable support for push notifications in your app - -To alert your app when the signed-in user's embedded message _eligibility_ changes -(that is, when they are newly eligible, or no longer eligible, for some embedded -message campaign in your project), Iterable sends silent push notifications. - -After receiving one of these silent push notifications, the SDK refreshes its -local cache of embedded messages by re-fetching them from Iterable's API. - -To learn how to enable push notifications in your Android app, read -[Setting up Android Push Notifications](https://support.iterable.com/hc/articles/115000331943). - -## Step 6: Fetch embedded messages from Iterable - -When your app first launches, and each time it comes to the foreground, -Iterable's Android SDK automatically refresh a local, on-device cache of -embedded messages for the signed-in user. These are the messages the signed-in -user is _eligible_ to see. - -:::tip NOTE -A user is _eligible_ for an embedded message campaign if they're selected by its -associated _eligibility list_ (a standard dynamic list in Iterable). -::: - -At key points during your app's lifecycle, you may want to manually refresh your -app's local cache of embedded messages. For example, as users navigate around, -on pull-to-refresh, etc. - -To refresh the local cache of embedded messages, call: - -```kotlin -IterableApi.getInstance().embeddedManager.syncMessages() -``` - -However, do not poll for new embedded messages at a regular interval. - -:::tip NOTE -Currently, Iterable's Android SDK does not persist the embedded messages -downloaded from the server. When your app is restarted, Iterable's Android SDK -re-fetches the user's embedded messages from Iterable, which can cause the -creation of multiple [`embeddedReceived`](https://support.iterable.com/hc/articles/23061677642260#embeddedreceived-events) -events for the same message. -::: - -To fetch embedded messages, Iterable's Android SDK calls: - -[`GET /api/embedded-messaging/messages`](https://support.iterable.com/hc/articles/204780579#get-api-embedded-messaging-messages) - -## Step 7: Track message receipt - -For each embedded message received from Iterable, Iterable's Android SDK -automatically tracks an [`embeddedReceived`](https://support.iterable.com/hc/articles/23061677642260#embeddedreceived-events) -event. Each of these events represents the download of a particular message to a -particular device — but not, necessarily, that the message was displayed or seen -by the user. - -To track message receipt, Iterable's Android SDK calls: - -[`POST /api/embedded-messaging/events/received`](https://support.iterable.com/hc/articles/204780579#post-api-embedded-messaging-events-received) - -## Step 8: Set up SDK listeners - -Now, set up listeners for the SDK to call when new embedded messages arrive -on device, to tell your views to display messages as needed. To add these -listeners, call the following methods on `IterableEmbeddedManager`: - -```kotlin -public fun addUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) -public fun removeUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) -``` - -Typically, a view that displays embedded messages adds itself as a listener -when it appears, and removes itself as a listener when it disappears. For -example, for an activity or a fragment: - -```kotlin -override fun onResume() { - super.onResume() - IterableApi.getInstance().embeddedManager.addUpdateListener(this) - // ... -} - -override fun onPause() { - super.onPause() - IterableApi.getInstance().embeddedManager.removeUpdateListener(this) - // ... -} -``` - -`IterableEmbeddedUpdateHandler`, the interface that listeners must implement, -declares these methods: - -- `fun onMessagesUpdated()` – Called by the SDK to tell your app that embedded - messages have been updated, and that you can grab the local queue and display - them. - -- `fun onEmbeddedMessagingDisabled()` – Called by the SDK when there's a failure - fetching embedded messages from the server. Use this method to hide your - embedded message display or show default content, as needed. - -- `fun onEmbeddedMessagingSyncSucceeded()` – Called when an embedded messaging - sync completes successfully. Use this method to update any loading state in - your UI, or to log that the sync finished. This method has a default empty - implementation, so overriding it is optional. - -- `fun onEmbeddedMessagingSyncFailed(reason: String?)` – Called when an embedded - messaging sync fails. The `reason` parameter contains a failure reason string, - when available (for example, a network or server error message). Use this - method to log failures, show fallback content, or surface non-sensitive error - information to your users. This method has a default empty implementation, so - overriding it is optional. - -For example, a view registered as a listener might have implementations similar -to: - -```kotlin -override fun onMessagesUpdated() { - // Fetch messages for the placement associated with the current view - val messages = embeddedManager.getMessages(placementId) - - // Show or hide messages... - // ... -} - -override fun onEmbeddedMessagingDisabled() { - // Hide embedded UI or show default content - // showFallbackContent() -} - -override fun onEmbeddedMessagingSyncSucceeded() { - // Stop loading indicators, confirm latest content is shown - // hideLoadingSpinner() -} - -override fun onEmbeddedMessagingSyncFailed(reason: String?) { - // Log or surface a non-sensitive error state - // Log.d("Embedded", "Sync failed: ${reason ?: "Unknown error"}") - // showEmbeddedErrorState() -} -``` - -:::tip TIP -You may want to check the local list of messages right when your view appears, -_as well as_ when the SDK calls `onMessagesUpdated`. That way, if there are -messages already available for display when the view first appears, you can show -them. Otherwise, you can display a loading spinner or hide the embedded message -view altogether. -::: - -:::warning IMPORTANT -The SDK does not always call `onMessagesUpdated`, -`onEmbeddedMessagingSyncSucceeded`, or `onEmbeddedMessagingSyncFailed` on the -main thread. To prevent crashes, make sure you're on the main thread before -updating your app's UI to display embedded messages. -::: - -## Step 9: Display embedded messages - -For each incoming embedded message, create a view and add it to your app's user -interface, using the fields included in the message to populate the message -content and set its styles. For example, you might use one `IterableEmbeddedMessage` -to drive the creation of a single banner message, or many of them to drive the -creation of a carousel. - -As you're setting up your embedded message views: - -- Associate each message view with its corresponding `IterableEmbeddedMessage` - object, so you have access to the underlying message (and its `messageId`) when - tracking events. -- Add click handlers where necessary, so you can handle clicks and track them in - Iterable. As messages appear and disappear, track impressions (described in - the next section). - -`IterableEmbeddedMessage` objects have various fields, corresponding to the data -included with your campaign: - -- `metadata` – Identifying information about the campaign. - - `messageId` – The ID of the message. - - `placementId` – The ID of the placement associated with the message. - - `campaignId` – The ID of the campaign associated with the message. - - `isProof` – Whether or not the campaign is a test message. - -- `elements` – What to display, and how to handle interaction. - - `title` – The message's title text. - - `body` – The message's body text. - - `mediaUrl` – The URL of an image associated with the message. - - `mediaUrlCaption` – Text description of the image. - - `defaultAction` – What to do when a user clicks on the message (outside of its buttons). - - `buttons` – Buttons to display. - - `text` – Extra data fields. Not for display. - -- `payload` – Custom JSON data included with the campaign. - -Use this data to build a custom view. Or use one the out-of-the-box views -provided by the SDK, as described below. - -:::tip TIP -For a look at the JSON payload associated with an embedded message, see -[`GET /api/embedded-messaging/messages`](https://support.iterable.com/hc/articles/204780579#get-api-embedded-messaging-messages). -::: - -### Out-of-the-box views - -Iterable's Android SDK provides an `IterableEmbeddedView` class you can use to -display embedded messages as a card, a banner, or a notification. For more -information about out-of-the-box views, read [Out-of-the-Box Views for Embedded Messages](https://iterable.zendesk.com/hc/articles/23230946708244). - -You can customize out-of-the-box views, in some ways, to more closely match the -styles of your apps. - -Out-of-the-box views handle clicks, too. To do this, they automatically: - -- Pass URLs and custom actions to the URL and custom action handlers you set up - in [step 4](#step-4-configure-the-sdk). -- Track [`embeddedClick`](https://support.iterable.com/hc/articles/23061677642260#embeddedclick-events) - events. - -:::tip NOTE -When using out-of-the-box views to display embedded messages, you'll still need to -manually track sessions and impressions, as described in [step 10](#step-10-track-sessions-and-impression). -::: - -To use an out-of-the-box view, first create an `IterableEmbeddedViewConfig` object, -to declare the styles you'd like the view to use: - -```kotlin -// Grab your app's colors from wherever it makes sense. -val config = IterableEmbeddedViewConfig( - backgroundColor = Color.parseColor("#FFFFFF"), - borderColor = Color.parseColor("#000000"), - borderWidth = 1, - borderCornerRadius = 8f, - primaryBtnBackgroundColor = Color.parseColor("#0000FF"), - primaryBtnTextColor = Color.parseColor("#FFFFFF"), - secondaryBtnBackgroundColor = Color.parseColor("#FFFFFF"), - secondaryBtnTextColor = Color.parseColor("#000000"), - titleTextColor = Color.parseColor("#000000"), - bodyTextColor = Color.parseColor("#000000"), - imageScaleType = ImageView.ScaleType.CENTER_CROP -) -``` - -:::tip TIP — Default values (SDK v3.8.0 and above) -Starting with SDK version 3.8.0, all `IterableEmbeddedViewConfig` parameters -have default values, so you only need to specify the styling options you want -to customize. The example above shows every option for reference, but you can pass -just the ones you need. For example: - -```kotlin -val config = IterableEmbeddedViewConfig( - backgroundColor = Color.parseColor("#FFFFFF"), - borderCornerRadius = 8f -) -``` - -All color, border, and text-color parameters default to `null` (which falls -back to the view's built-in styling). The `imageScaleType` parameter defaults -to `ImageView.ScaleType.CENTER_CROP`. -::: - -The `imageScaleType` parameter (added in SDK v3.8.0) controls how the image is -scaled within the 16:9 image container of `CARD` and `BANNER` views. It accepts -any standard Android [`ImageView.ScaleType`](https://developer.android.com/reference/android/widget/ImageView.ScaleType) -value (for example, `CENTER_CROP`, `FIT_CENTER`, or `FIT_XY`). The -`NOTIFICATION` view type does not display an image, so this parameter has no -effect on that view type. - -Then, when it's time to display a message, create the `IterableEmbeddedView` -using the `newInstance` factory method: - -```kotlin -val messageView = IterableEmbeddedView.newInstance(ootbType, message, config) -``` - -This method takes three parameters: - -- A value of type `IterableEmbeddedViewType`, an `enum` with three constants: - - `BANNER` - - `CARD` - - `NOTIFICATION` -- The `IterableEmbeddedMessage` to display. -- The `IterableEmbeddedViewConfig` created above (optional — pass `null` or omit - to use default styles). - -:::warning IMPORTANT — Migration from older SDK versions -In SDK versions prior to 3.6.5, `IterableEmbeddedView` was instantiated using a -constructor: - -```kotlin -// Old approach (deprecated — unstable): -val messageView = IterableEmbeddedView(ootbType, message, config) -``` - -This constructor has been **deprecated** because it violates Android Fragment -best practices: the system cannot recreate the fragment after configuration -changes or process death, causing crashes. - -**Use the `newInstance` factory method instead**, as shown above. The old -constructor still works but is marked as deprecated and will be removed in a -future SDK release. -::: - -Then, add the view to your layout. It's important to fully specify the size -of the view, with minimum dimensions, as described in -[Out-of-the-Box Views for Embedded Messages](https://iterable.zendesk.com/hc/articles/23230946708244). - -For example, one way to add an out-of-the-box layout to a view is to swap it -with a "placeholder" view that's already there. For example, this layout -contains a placeholder `FrameLayout`: - -```xml - - - - - - -``` - -When it's time to display the embedded message, you could replace the placeholder -view using code such as: - -```kotlin -val ft: FragmentTransaction = childFragmentManager.beginTransaction() -ft.replace(R.id.placeholder_view, messageView) -ft.commit() -``` - -With this approach, the placeholder view might be an empty state that can remain -in place when there are no embedded messages available, or a view with a loading -spinner, or something similar. - -However, this is just an example. The specific approach you'll take when adding -an out-of-the-box view to your app depends on your app architecture. - -## Step 10: Track sessions and impression - -A _session_ is a period of time when a user is on a screen or page that can -display embedded messages. - -Every session can have many _impressions_. An impression represents the -on-screen appearances of a given embedded message, in context of a session. Each -impression tracks: - -- The total number of times a message appears during a session. -- The total amount of time that message was visible, across all its appearances - in the session. - -To help you track message sessions and impressions (views of a message), -Iterable's Android SDK provides a session manager. Sessions and impressions are -tracked in Iterable as [`embeddedSession`](https://support.iterable.com/hc/articles/23061677642260#embeddedsession-events) -and [`embeddedImpression`](https://support.iterable.com/hc/articles/23061677642260#embeddedimpression-events) -events. - -### Step 10.1: Start a session - -When a user comes to a screen or page in your app where embedded messages are -displayed (in one or more placements), use the session manager to start a -session. To start a session, call: - -```kotlin -// When the screen that displays your embedded message is displayed or comes to -// the foreground -IterableApi - .getInstance() - .embeddedManager - .getEmbeddedSessionManager() - .startSession() -``` - -### Step 10.2: Start and pause impressions - -As messages appear or disappear during an ongoing embedded message session, use -the session manager to track message impressions. - -The session manager tracks the total number of times each message appears during -a session, and the total amount of time each message is on-screen across all -those appearances. To start and pause impressions, call: - -```kotlin -// When a message appears, start an impression (associating it with a placement) -IterableApi - .getInstance() - .embeddedManager - .getEmbeddedSessionManager() - .startImpression(messageId, placementId) - -// When a message disappears… -IterableApi - .getInstance() - .embeddedManager - .getEmbeddedSessionManager() - .pauseImpression(messageId) -``` - -:::tip NOTE -An embedded message can disappear and reappear many times during a session. -Because of this, when an embedded message disappears, you don't _end_ its -impression — you _pause_ it. Then, you start the impression again if and when -the message reappears. In other words, you start and end sessions, but you start -and _pause_ impressions. -::: - -Be sure to start and pause impressions when your app goes to and from the -background, too. - -### Step 10.3: End the session, saving impression data to Iterable - -When a user leaves a screen in your app where embedded messages are displayed, -use the session manager to end the active session. This causes the SDK to send -session and impression data back to the server. - -To end a session, call: - -```kotlin -// When a screen that displays embedded messages is dismissed -// or goes to the background -IterableApi - .getInstance() - .embeddedManager - .getEmbeddedSessionManager() - .endSession() -``` - -To track sessions and impressions, Iterable's Android SDK calls: - -[`POST /api/embedded-messaging/events/session`](https://support.iterable.com/hc/articles/204780579#post-api-embedded-messaging-events-session) - -## Step 11: Handle clicks - -Finally, configure your app to handle clicks on embedded messages. When a user -clicks a link or a button, it can be associated with: - -- A standard URL (for example, `https://example.com/products/1`). -- A custom URL scheme (for example, `mycompany://profile`). -- A custom action (for example, `action://joinClass/1`). - -Above, you set up a [URL handler](#step-4-1-define-a-url-handler) for handling -standard URLs and custom URL schemes, and a [custom action handler](#step-4-2-define-a-custom-action-handler) -for handling custom action URLs. - -Now, just listen for clicks, and then tell the SDK to invoke the URL or the -custom action handler (depending on the type of URL that was clicked). - -### Click handling for out-of-the-box views - -If you're using an out-of-the-box view to display embedded messages, you can -skip this step. Out-of-the-box views automatically: - -- Pass standard URLs and URLs with allowed custom URL schemes to the URL handler - you defined above. If your URL handler can't handle the URL (returns `false`), - the SDK attempts to open it with another app that can handle it (for example, - a web browser). -- Pass custom actions to your custom action handler. If your custom action handler - can't handle the custom action (returns `false`), the custom action is dropped - (since it can't be handled by a web browser). -- Track [`embeddedClick`](https://support.iterable.com/hc/articles/23061677642260#embeddedclick-events) - events. - -### Click handling for custom embedded message views - -However, if you're using custom views instead of out-of-the-box views, you'll -need to handle clicks. As you instantiate custom embedded message views in your -Android app: - -- Add click handlers to the message's buttons and links. Do this however it makes - sense for your app. -- Set up a default click handler, to handle clicks on the message but outside - of any particular button or link. - -In your click handlers: - -- Execute any necessary custom application logic (update the UI if needed, etc.). - -- Call `handleEmbeddedClick` on `IterableEmbeddedManager`. This method forwards - URLs and custom actions to the handlers you defined above. For example: - - ```kotlin - IterableApi - .getInstance() - .embeddedManager - .handleEmbeddedClick( - message, - buttonIdentifier, - clickedUrl - ) - ``` - -- Track an [`embeddedClick`](https://support.iterable.com/hc/articles/23061677642260#embeddedclick-events) - event: - - ```kotlin - IterableApi - .getInstance() - .trackEmbeddedClick( - embeddedMessage, - buttonId, - clickedUrl - ) - ``` - - To track clicks, Iterable's Android SDK calls: - - [`POST /api/embedded-messaging/events/click`](https://support.iterable.com/hc/articles/204780579#post-api-embedded-messaging-events-click) - -## Want to learn more? - -- [Out-of-the-Box Views for Embedded Messages](https://iterable.zendesk.com/hc/articles/23230946708244). -- The [GitHub repository for Iterable's Android SDK](https://github.com/iterable/iterable-android-sdk). - In particular, these files: - - [`IterableEmbeddedManager.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi/src/main/java/com/iterable/iterableapi/IterableEmbeddedManager.kt) - - [`EmbeddedSessionManager.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi/src/main/java/com/iterable/iterableapi/EmbeddedSessionManager.kt) - - [`IterableEmbeddedPlacement.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi/src/main/java/com/iterable/iterableapi/IterableEmbeddedPlacement.kt) - - [`IterableEmbeddedView.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedView.kt) - - [`IterableEmbeddedViewConfig.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewConfig.kt) - - [`IterableEmbeddedViewType.kt`](https://github.com/Iterable/iterable-android-sdk/blob/master/iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/embedded/IterableEmbeddedViewConfig.kt) - diff --git a/sources/android/identifying-the-user.md b/sources/android/identifying-the-user.md deleted file mode 100644 index 17a67c5..0000000 --- a/sources/android/identifying-the-user.md +++ /dev/null @@ -1,245 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/360035402531 -title: Identifying the User -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/managing-user-profiles/identifying-the-user/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: ced31ca29ce63d634a0c4691277a114ed3f0ceb9 -fetched_at: 2026-05-25T15:11:46.888Z ---- -# Identifying the User - -The Iterable SDK can identify users by email or user ID. - -## In this article - -[[toc]] - -## Overview - -To identify a user, you'll need to do two things: specify an email address or -user ID, and then call `updateUser` to send that value to Iterable. - -## Identifying the user by email - -Email is typically used as the key identify within Iterable because it tracks -across devices to aggregate data between a user's phones, tablet, web-site -activity or even IoT (Internet-of-Things) device. - -Iterable also allows for multi-dimensional nested data types, meaning you can -organize your data based on relevant key values. - -Once you have an email address or user ID for your app's current user, set -`IterableAPI.email` or `IterableAPI.userId`. For example: - -:::warning WARNING -- Don't specify both `email` and `userId` in the same call, as they will be - treated as different users by the SDK. Only use one type of identifier, email - or user ID, to identify the user. -- Your app will not be able to receive push notifications until you set one - of these values -::: - -Add this line of code as soon as you know the user's email: - -_Swift_ - -```swift -IterableAPI.email = "user@example.com" -``` - -_Objective-C_ - -```objectivec -IterableAPI.email = @"user@example.com"; -``` - -_Java_ - -```java -IterableApi.getInstance().setEmail("user@example.com"); -``` - -:::tip NOTES -Please see [User Profile Fields Used by Iterable](https://support.iterable.com/hc/articles/217744303) -to ensure you don't add data that is specific to set fields in Iterable. -::: - -## Identifying the user by user ID - -Iterable can also identify user by user ID. However, all things being equal, it -is recommended to use email as the key identifier. - -_Swift_ - -```swift -IterableAPI.userId = "user123" -``` - -_Objective-C_ - -```objectivec -IterableAPI.userId = @"user123"; -``` - -_Java_ - -```java -IterableApi.getInstance().setUserId("user123"); -``` - -You can add the `userId` identifier at any point after the `IterableConfig()` -call. - - -:::tip NOTES -- When creating a user by `userId` in an email-based project, Iterable automatically - assigns the user a `@placeholder.email` email address (a user identifier, not - a way to message the user). For example, if you create a user with a `userId` - of `user123`, their user profile will also receive an `email` such as - `user123+147178873@placeholder.email`. To learn more, read [Handling Anonymous Users](https://support.iterable.com/hc/articles/208499956). -- A user ID can be up to 52 characters long. - -::: - -## Identifying the device of the user - -For Iterable to send push notifications to an iOS device, it must know the -unique token assigned to that device by Apple or Android. - -Iterable uses silent push notifications to tell iOS apps when to fetch new -in-app messages from the server. Because of this, your app must register for -remote notifications with Apple even if you do not plan to send it any push -notifications. - -### Auto push registration - -`IterableConfig.autoPushRegistration` determines whether or not the SDK will: - -- Automatically register for a device token when the SDK is given a new - email address or user ID. Disable the device token for the previous user when - a new user logs in. - -If `IterableConfig.autoPushRegistration` is **true** (the default value): - -- Setting `IterableAPI.email` or `IterableAPI.userId` causes the SDK to - automatically call the `registerForRemoteNotifications()` method on - UIApplication and pass the resulting device token to the - `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)` method on - the app delegate. - -If `IterableConfig.autoPushRegistration` is **false**: - -- After setting `IterableAPI.email` or `IterableAPI.userId`, you must - manually call the `registerForRemoteNotifications()` method on - `UIApplication`. This will fetch the device token from Apple and pass it to - the `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)` method - on the app delegate. - -### Send the device token to Iterable - -:::tip NOTES -- Iterable users the device token to send push notifications and in-app - messages. -- Users do not need to opt in to Apple push notifications for Iterable to get - the device token. -::: - -To send the device token to Iterable and save it on the current user's -profile, call `IterableAPI.register(token:)` from the -`application(_:didRegisterForRemoteNotificationsWithDeviceToken:)` method on -`UIApplicationDelegate`. For example: - -_Swift_ -```swift -func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - IterableAPI.register(token: deviceToken) -} -``` - -_Objective-C_ -```objectivec -- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { - [IterableAPI registerToken:deviceToken]; -} -``` - -_Java_ -```java -// Iterable SDK automatically registers the push token with Iterable -// whenever setEmail or setUserId is called. - -// If you want to trigger token registration manually, first disable automatic -// registration by calling setAutoPushRegistration(false) on IterableConfig.Builder when initializing the SDK. - -// Then call registerForPush whenever you want to register the token: - -// Only use this line if you want to register push manually. -// IterableApi.getInstance().registerForPush(); -``` - -Once you register for push, you should see the device on the user's Iterable profile. -Access a user's profile in Iterable by navigating to **Audiences > Contact Lookup**. - -## Updating email and user ID - -Use the following code to update both an `email` and a `userId`: - -_Swift_ - -```swift -IterableAPI.updateEmail("", - onSuccess: { _ in - IterableAPI.updateUser( - [JsonKey.userId: ""], - mergeNestedObjects: false, - onSuccess: { _ in - // this needs to be set after both calls have finished successfully - IterableAPI.userId = "" - }, onFailure: nil) -}, onFailure: nil) -``` - -_Objective-C_ - -```objectivec -[IterableAPI updateEmail:@"" - onSuccess:^(NSDictionary * _Nullable data) { - [IterableAPI updateUser:@{@"userId": @""} - mergeNestedObjects:NO - onSuccess:^(NSDictionary * _Nullable data) { - // this needs to be set after both calls have finished successfully - IterableAPI.userId = @""; - } onFailure:nil]; -} onFailure:nil]; -``` - -_Java_ - -```java -IterableApi.getInstance().updateEmail("newEmail@somewhere.com", new IterableHelper.SuccessHandler() { - @Override - public void onSuccess(JSONObject data) { - JSONObject userIDobj = new JSONObject(); - try { - userIDobj.put("userId", "newUserId"); - } catch (JSONException e) { - e.printStackTrace(); - } - IterableApi.getInstance().updateUser(userIDobj); - } -}, new IterableHelper.FailureHandler() { - @Override - public void onFailure(String reason, JSONObject data) { - Log.e(TAG, reason) - } -}); -``` - -## Next steps - -To troubleshoot user identification, see [Testing and Troubleshooting User Profiles](https://support.iterable.com/hc/articles/360035079512). - -If you have already identified the user for your use case, see [Updating User Profiles](https://support.iterable.com/hc/articles/360035402611) -and [User Update Recommendations](https://support.iterable.com/hc/articles/360035031532). diff --git a/sources/android/in-app-messages-on-android.md b/sources/android/in-app-messages-on-android.md deleted file mode 100644 index ef33f54..0000000 --- a/sources/android/in-app-messages-on-android.md +++ /dev/null @@ -1,290 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/360035537231 -title: In-App Messages on Android -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/in-app-messages/in-app-messages-on-android/index.md -source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 -source_sha: 65412ae773eaca59531243ff4775fd4457b5b608 -fetched_at: 2026-08-03T20:41:28.539Z ---- -# In-App Messages on Android - -![Android In-App](https://iterable.zendesk.com/hc/article_attachments/360041501291/android.png "Android In-App") - -## In this article - -[[toc]] - -## Default behavior - -By default, when an in-app message arrives from the server, the SDK automatically -shows it if the app is in the foreground. If an in-app message is already showing -when the new message arrives, the new message will be shown 30 seconds after the -currently displayed in-app message closes ([see how to change this default value below](#changing-the-display-interval-between-in-app-messages)). -Once an in-app message is shown, it will be "consumed" from the server queue and -removed from the local queue as well. There is no need to write any code to get this -default behavior. - -## Overriding whether to show or skip a particular in-app message - -An incoming in-app message triggers a call to the `onNewInApp` method of -`IterableConfig.inAppHandler` (an `IterableInAppHandler` object). To override the -default behavior, set `inAppHandler` in `IterableConfig` to a custom class that -overrides the `onNewInApp` method. `onNewInApp` should return `InAppResponse.SHOW` -to show the incoming in-app message or `InAppResponse.SKIP` to skip showing it. - -:::tip TIP -To determine the priority of an `IterableInAppMessage` object, call its -`getPriorityLevel` method. You can use this priority to help determine whether or -not to display it. -::: - -```java -class MyInAppHandler implements IterableInAppHandler { - @Override - public InAppResponse onNewInApp(IterableInAppMessage message) { - if (/* add conditions here */) { - return InAppResponse.SHOW; - } else { - return InAppResponse.SKIP; - } - } -} - -// ... - -IterableConfig config = new IterableConfig.Builder() - .setPushIntegrationName("myPushIntegration") - .setInAppHandler(new MyInAppHandler()) - .build(); -IterableApi.initialize(context, "", config); -``` - -### Deferring an in-app message (SDK v3.10.0 and above) - -Starting with SDK version 3.10.0, `onNewInApp` can also return -`InAppResponse.DEFER`. Unlike `SKIP`, which permanently drops the message, -`DEFER` keeps the message pending so the SDK reconsiders it on a later display -pass (for example, on the next foreground, sync, or newly arrived message). This -is useful for temporary, per-message suppression—for example, while a splash -screen is showing. - -```java -class MyInAppHandler implements IterableInAppHandler { - @Override - public InAppResponse onNewInApp(IterableInAppMessage message) { - if (appIsShowingSplashScreen()) { - return InAppResponse.DEFER; - } - return InAppResponse.SHOW; - } -} -``` - -Once your app is ready to display in-app messages, call -`resumeInAppDisplay()` (see [Pausing the display of in-app messages](#pausing-the-display-of-in-app-messages-sdk-v3-2-6-and-above)) -to re-check pending messages immediately, instead of waiting for the next -foreground or sync trigger. - -:::info NOTE -In Kotlin, add a `DEFER` branch to any exhaustive `when` expression over -`InAppResponse`. -::: - -## Getting the local queue of in-app messages - -The SDK keeps the local in-app message queue in sync by checking the server queue -every time the app goes into foreground, and via silent push messages that arrive -from Iterable servers to notify the app whenever a new in-app message is added to -the queue. - -To access the in-app message queue, call -`IterableApi.getInstance().getInAppManager().getMessages()`. To show a message, call -`IterableApi.getInstance().getInAppManager().showMessage(message)`. - -```java -// Get the in-app messages list -IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); -List messages = inAppManager.getMessages(); - -// Show an in-app message -inAppManager.showMessage(message); - -// Show an in-app message without consuming (not removing it from the queue) -inAppManager.showMessage(message, false) - -``` - -## Handling in-app message buttons and links - -The SDK handles in-app message buttons and links as follows: - -- If the URL of the button or link uses the `action://` URL scheme, the SDK passes - the action to `IterableConfig.customActionHandler.handleIterableCustomAction()`. If - `customActionHandler` (an `IterableCustomActionHandler` object) has not been set, - the action will not be handled. - - For the time being, the SDK will treat `itbl://` URLs the same way as `action://` - URLs. However, this behavior will eventually be deprecated (timeline TBD), so it's - best to migrate to the `action://` URL scheme as it's possible to do so. - -- The `iterable://` URL scheme is reserved for action names predefined by - the SDK. If the URL of the button or link uses an `iterable://` URL known - to the SDK, it will be handled automatically and will not be passed to the - custom action handler. For example, buttons or links with URL `iterable://dismiss` - dismiss an in-app message and create in-app click and in-app close events. - -- The SDK passes all other URLs to `IterableConfig.urlHandler.handleIterableURL()`. - If `urlHandler` (an `IterableUrlHandler` object) has not been set, or if it - returns `false` for the provided URL, the URL will be opened by the system - (using a web browser or other application, as applicable). - -## Configuring how in-app messages interact with system bars (SDK v3.8.0 and above) - -By default, Iterable's Android SDK draws in-app messages edge-to-edge, with -content extending behind the status bar and navigation bar. This was the only -behavior in SDK versions 3.6.1 through 3.7.0. - -Starting with SDK version 3.8.0, you can configure how in-app messages interact -with system bars by setting `IterableInAppDisplayMode` on `IterableConfig`. -This setting applies globally to all in-app messages displayed by the SDK. - -The available modes are: - -- `FORCE_EDGE_TO_EDGE` (default) — Forces in-app messages to display - edge-to-edge, drawing content behind the status bar and navigation bar. - This preserves the behavior of previous SDK versions. -- `FOLLOW_APP_LAYOUT` — Matches the host app's current layout configuration. - If your app is edge-to-edge, in-app messages display edge-to-edge; if your - app respects system bar bounds, so do in-app messages. -- `FORCE_FULLSCREEN` — Hides the status bar entirely while in-app messages - are displayed. Uses the legacy `FLAG_FULLSCREEN` on API levels below 30 and - `WindowInsetsController` on API 30 and above. -- `FORCE_RESPECT_BOUNDS` — Ensures in-app content never draws behind the - status bar or navigation bar, keeping UI elements like the close button - always accessible. - -To configure the display mode, call `setInAppDisplayMode()` on -`IterableConfig.Builder`: - -```java -IterableConfig config = new IterableConfig.Builder() - .setInAppDisplayMode(IterableInAppDisplayMode.FOLLOW_APP_LAYOUT) - .build(); -IterableApi.initialize(context, "", config); -``` - -:::tip TIP -If the close button (or other interactive elements) in your fullscreen in-app -messages is being obscured by the status bar, switch to `FOLLOW_APP_LAYOUT` -or `FORCE_RESPECT_BOUNDS`. -::: - -## Displaying in-app messages in Jetpack Compose apps (SDK v3.9.0 and above) - -In SDK versions before 3.9.0, displaying an in-app message required a -`FragmentActivity`, because the SDK rendered in-app messages using a `Fragment`. -This meant that apps built fully with [Jetpack Compose](https://developer.android.com/compose) -(and without the Android fragment framework) couldn't display in-app messages. - -Starting with SDK version 3.9.0, the SDK can also render in-app messages using a -`Dialog`-based renderer (`IterableInAppDialogNotification`) that doesn't require -a `FragmentActivity`. When the current activity is a `FragmentActivity`, the SDK -continues to use the existing `Fragment`-based rendering; when it isn't (for -example, a Compose-based `ComponentActivity`), the SDK falls back to the -`Dialog`-based renderer. As a result, in-app messages now display correctly in -apps built fully with Jetpack Compose. - -No code changes are required to take advantage of this—just upgrade to SDK -version 3.9.0 or later. (The host must still be an `Activity`.) - -:::warning IMPORTANT -This Compose compatibility applies to **in-app message rendering** only. Iterable's -mobile inbox UI is still fragment-based and requires a `FragmentActivity` host. For -more information, see [Setting up Mobile Inbox on Android](https://support.iterable.com/hc/articles/360038744152#displaying-the-mobile-inbox). -::: - -## Changing the display interval between in-app messages - -To customize the time delay between successive in-app messages, set -`inAppDisplayInterval` on `IterableConfig` to an appropriate value in -seconds. The default value is 30 seconds. - -## Pausing the display of in-app messages (SDK v3.2.6 and above) - -In certain areas of your app, you may want to prevent interruptions. To pause the -display of in-app messages, call the following method: - -_Kotlin_ - -```kotlin -IterableApi.getInstance().inAppManager.setAutoDisplayPaused(true) -``` - -_Java_ - -```java -IterableApi.getInstance().getInAppManager().setAutoDisplayPaused(true); -``` - -With this done, the app will not automatically display new in-app messages. -However, it will keep the local queue of in-app messages in sync. - -:::tip TIP -While in-app message display has been paused, you can still call the `showMessage` -method on `IterableInAppManager` to manually display messages. -::: - -To resume the display of in-app messages from your app's queue, call -`setAutoDisplayPaused(false)`. - -### Re-evaluating pending in-app messages on demand (SDK v3.10.0 and above) - -Starting with SDK version 3.10.0, you can call `resumeInAppDisplay()` to prompt -the SDK to re-evaluate pending in-app messages once your app is ready to display -them—for example, after a splash screen is dismissed, or after you deferred a -message by returning `InAppResponse.DEFER` from `onNewInApp` (see -[Deferring an in-app message](#deferring-an-in-app-message-sdk-v3-10-0-and-above)). -Without this call, the SDK re-checks pending messages only on its own triggers -(foreground, sync, or a newly arrived message). - -_Kotlin_ - -```kotlin -IterableApi.getInstance().inAppManager.resumeInAppDisplay() -``` - -_Java_ - -```java -IterableApi.getInstance().getInAppManager().resumeInAppDisplay(); -``` - -`resumeInAppDisplay()` is independent of `setAutoDisplayPaused(boolean)`: if -automatic display is paused, this call won't show anything (and logs a warning) -until you also call `setAutoDisplayPaused(false)`. - - -## Further reading - -User guides: -- [In-App Messages and Mobile Inbox](https://support.iterable.com/hc/articles/217517406) -- [Sending In-App Messages](https://support.iterable.com/hc/articles/360034903151) -- [Events for In-App Messages and Mobile Inbox](https://support.iterable.com/hc/articles/360038939972) - -Developer documentation: -- Iterable's [iOS SDK](https://support.iterable.com/hc/articles/360035018152) -- Iterable's [Android SDK](https://support.iterable.com/hc/articles/360035019712) -- [In-App Messages Overview](https://support.iterable.com/hc/articles/360035538391) -- [In-App Messages on iOS](https://support.iterable.com/hc/articles/360035536791) -- [Setting up Mobile Inbox on iOS](https://support.iterable.com/hc/articles/360039137271) -- [Setting up Mobile Inbox on Android](https://support.iterable.com/hc/articles/360038744152) -- [Customizing Mobile Inbox on iOS](https://support.iterable.com/hc/articles/360039091471) -- [Customizing Mobile Inbox on Android](https://support.iterable.com/hc/articles/360039189931) -- [Animating In-App Messages with CSS](https://support.iterable.com/hc/articles/360035539271) -- [Image Carousels in In-App Messages](https://support.iterable.com/hc/articles/360035171132) -- [Testing and Troubleshooting In-App Messages](https://support.iterable.com/hc/articles/360035623391) -- [In-App Messages Without the SDK](https://support.iterable.com/hc/articles/360018709631) -- [Getting Started with Iterable's API](https://support.iterable.com/hc/articles/41044692130196) -- [API Endpoints and Sample Payloads](https://support.iterable.com/hc/articles/204780579) diff --git a/sources/android/push-notification-overview.md b/sources/android/push-notification-overview.md deleted file mode 100644 index 0dfbd35..0000000 --- a/sources/android/push-notification-overview.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/360035079872 -title: Push Notification Overview -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/push-notifications/push-notification-overview/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 3306f88835e0c1b30e4b4020287d772cfd93ba1e -fetched_at: 2026-05-25T15:11:44.170Z ---- -# Push Notification Overview - -![Iterable Push Notifications](https://iterable.zendesk.com/hc/article_attachments/360041338352/push-intro.png "Iterable Push Notifications") - -To alert users about updates, offers, content, and other information that may be -immediately relevant, it often makes sense to contact them on their mobile -devices. Iterable can send push notification campaigns to your users, allowing -you reach them when and where it matters. - -You can also use Iterable to send silent push notifications, which wake your app -in the background to perform a task — update a badge count, download some data, -or trigger a request for an app store review. When you send a silent push -notification, you'll define a JSON payload to send along with it, and your app's -code can use this data as needed. - -## Iterable's mobile SDKs - -To make it easy to work with push notification campaigns sent from Iterable, -consider using Iterable's mobile SDKs: - -- [iOS SDK](https://support.iterable.com/hc/articles/360035018152), -- [Android SDK](https://support.iterable.com/hc/articles/360035019712), -- [React Native SDK](https://support.iterable.com/hc/articles/360045714132) - -These SDKs help with: - -- Capturing device tokens and sending them to Iterable. -- Handling rich push notifications, which contain images and action buttons. -- Deep link handling. -- Capturing events (when push notifications are delivered, when users click - on them, etc.). - -## Next steps - -If you're a marketer, work with your mobile engineers to implement the -technical setup described in [Setting up iOS Push Notifications](https://support.iterable.com/hc/articles/115000315806) -and [Setting up Android Push Notifications](https://support.iterable.com/hc/articles/115000331943). - -Then, read [Sending Push Notifications](https://support.iterable.com/hc/articles/115000379086) -to learn how to send a push notification campaign. diff --git a/sources/android/setting-up-android-push-notifications.md b/sources/android/setting-up-android-push-notifications.md deleted file mode 100644 index e50a202..0000000 --- a/sources/android/setting-up-android-push-notifications.md +++ /dev/null @@ -1,445 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/115000331943 -title: Setting up Android Push Notifications -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/push-notifications/setting-up-android-push-notifications/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 45fa32087746810e08046766184fd4c2eb1acc94 -fetched_at: 2026-05-25T15:11:43.429Z ---- -# Setting up Android Push Notifications - -This guide describes the technical setup necessary to use Iterable to send push -notifications to Android devices. - -:::tip TIP -For details about push notifications on Android, read Google's [Notifications Overview](https://developer.android.com/guide/topics/ui/notifiers/notifications) -document. -::: - -## In this article - -[[toc]] - -## Configuring Iterable to send Android push notifications - -Follow the steps below to configure Iterable to send Android push notifications: - -### Step 1: Set up Firebase for your Android app - -To send Android push notifications, Iterable uses [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) -(FCM). To learn how to set up Firebase for your Android app, read Google's -[Add Firebase to your Android project](https://firebase.google.com/docs/android/setup) -document. - -### Step 2: Create a mobile app in Iterable - -In your Iterable projects, you can define _mobile apps_ that correspond to your -real-world mobile apps. Each mobile app in Iterable stores details about an -app's name, identifier, platform, and store URL. - -To create a mobile app in Iterable: - -1. Navigate to **Settings > Apps and Websites**. - -2. Click **New app or website**. This brings up the **New app or website** page: - - ![Creating a new app or website](https://support.iterable.com/hc/article_attachments/26154725921684/new-app-or-website.png "Creating a new app or website") - -3. For **Name**, enter the name of your app. For example, `Example Push Test`. - -4. For **Platform**, select **Android**. - -5. For **Package name**, enter your app's [package name](https://developer.android.com/studio/build/application-id). - For example, `com.example.pushtest`. - -6. (Optional) For **Store URL**, enter your app's Play Store URL. - -7. Click **Create app**. You'll be taken to the app's details page: - - ![App details page](https://support.iterable.com/hc/article_attachments/26154740912660/app-details.png "App details page") - -### Step 3: Add a push integration to the mobile app - -A _push integration_ stores the credentials Iterable uses to authenticate with -FCM when sending push notifications. Push integrations are stored in the mobile -apps you create in your Iterable project. - -To create a push integration: - -1. Create a service account in Firebase. -2. Create a JSON private key that Iterable can use to authenticate with Firebase. -3. Configure the push integration in Iterable. -4. Send a test push notification. - -:::warning IMPORTANT -Firebase Cloud Messaging (FCM) has [deprecated their legacy HTTP APIs](https://firebase.google.com/docs/cloud-messaging/migrate-v1) -and replaced them with the FCM HTTP v1 API. If you have any existing push -integrations that use legacy FCM HTTP API credentials, you'll need to update -them. For more information, read [Migrating to the FCM HTTP v1 API for Push Notifications](https://support.iterable.com/hc/articles/26143681644564). -::: - -#### Step 3.1: Create a service account in Firebase - -First, create a [service account](https://firebase.google.com/support/guides/service-accounts) -in Firebase and give it the necessary permissions to send push notifications -on your behalf. - -:::tip TIP -Read Google's [Create service accounts](https://cloud.google.com/iam/docs/service-accounts-create) -document for more information. -::: - -To create and configure a service account: - -1. Sign in your Firebase account. Open the Firebase project that contains the - app to which you'll send push notifications. - - ![Choosing an app in Firebase](https://support.iterable.com/hc/article_attachments/26154748820628/firebase-mobile-app-tile.png "Choosing an app in Firebase") - -2. Click the gear button (in the upper-left). From the menu, choose - **Project settings**. - - ![Opening project settings in Firebase](https://support.iterable.com/hc/article_attachments/26154726130452/firebase-project-settings.png "Opening project settings in Firebase") - -3. Navigate to the **Service accounts** tab and click **Manage service account permissions**. - - ![Managing service accounts in Firebase](https://support.iterable.com/hc/article_attachments/26154757616916/firebase-service-accounts.png "Managing service accounts in Firebase") - -5. To create a new service account, click **Create Service Account**. - - ![Creating a service account in Firebase](https://support.iterable.com/hc/article_attachments/26154741147156/firebase-create-service-account.png "Creating a service account in Firebase") - -6. Under **Service account details**, enter a name, account ID, and description. - Then, click **Create and Continue**. - - ![Specifying details for a new Firebase service account](https://support.iterable.com/hc/article_attachments/26154741205524/firebase-service-account-details.png "Specifying details for a new service account in Firebase") - -7. Under **Grant this service account access to project**, select role - [**Firebase Cloud Messaging API Admin**](https://cloud.google.com/iam/docs/understanding-roles#firebasecloudmessaging.admin). - Or, choose a custom role that has the [`cloudmessaging.messages.create`](https://firebase.google.com/docs/projects/iam/permissions#messaging) - permission. Then, click **Continue**. - - ![Giving a Firebase service account access to a project](https://support.iterable.com/hc/article_attachments/26154757815828/firebase-service-account-grant.png "Giving a Firebase service account access to a project") - - :::tip TIP - To learn about creating and managing custom Identity and Access Management (IAM) - roles in Google Cloud, read Google's [Create and manage custom roles](https://cloud.google.com/iam/docs/creating-custom-roles) - document. - ::: - -8. Under **Grant users access to this service account**, leave both fields blank. - - ![Granting user access to a Firebase service account](https://support.iterable.com/hc/article_attachments/26154737500308/firebase-service-account-user-access.png "Granting user access to a Firebase service account") - -9. Click **Done**. You'll be taken back to the **Service Accounts** page. - -#### Step 3.2: Create and download an FCM private key (JSON) - -Now, create and download a JSON private key that Iterable can use to -authenticate with FCM when sending push notifications. - -1. On the **Service Accounts** page, in the row for your new service account, - click the three dots in the **Actions** column. Choose **Manage keys**. - - ![Managing keys for a Firebase service account](https://support.iterable.com/hc/article_attachments/26154741351444/firebase-service-account-manage-keys.png "Managing keys for a Firebase service account") - -2. Click **Create new key**, and then choose **JSON**. - - ![Creating a JSON private key for a Firebase service account](https://support.iterable.com/hc/article_attachments/26154749445396/firebase-service-account-json.png "Creating a JSON private key for a Firebase service account") - -3. Click **Create**. This downloads the JSON private key to your machine. - -#### Step 3.3: Configure the push integration in Iterable - -Back in Iterable, configure your mobile app's push integration: - -1. Navigate to **Settings > Apps and Websites** and open your app. - -2. In the **Push** sections, under **Integrations**, in the **Firebase** row, - click **Configure**. A **Configure Firebase integration** window will appear. - - ![Configuring a Firebase push integration](https://support.iterable.com/hc/article_attachments/26154758067092/configure-firebase-integration.png "Configuring a Firebase push integration") - -3. For **Firebase Cloud Messaging (FCM) type**, choose between: - - - **Notification messages** - The Firebase SDK handles incoming push - notifications. Generally, you should only select this option if you aren't - using Iterable's Android SDK. - - **Data notifications** - Iterable's SDK handles incoming push notifications. - If you're using Iterable's Android SDK, this is usually the right option. - - :::tip TIP - For more information about these options, read [About FCM messages](https://firebase.google.com/docs/cloud-messaging/concept-options), - from Google. - ::: - -4. Upload the JSON file you created above. - -5. Click **Save**. As soon as you do, Iterable starts using these new credentials - to send push notifications to your Android app. - -#### Step 3.4: Send a test push notification - -Finally, a **Test Firebase integration** window appears. Send a test push -notification to make sure that everything works as expected (assuming that you've -set your app up to receive push notifications, as described further down in this -document). - -![Sending a test message](https://support.iterable.com/hc/article_attachments/26154737764756/test-firebase-integration.png "Sending a test message") - -1. Grab a test user's device token: - - - Visit **Audience > User Lookup**. - - Look up an internal user by `email` or `userId` (whatever makes sense in - your project). - - Navigate to the **User fields** tab. - - Open the `devices` array. - - From an Android device where `appPackageName` is your app's package name, - and `endpointEnabled` is `true`, copy the `token` field. - -2. In the **Test Firebase integration** window: - - - Enter the device token and a message. - - Click **Send test**. - - The device should receive the push notification message. If not, check the - configuration of the service account in Firebase, fix as necessary, and try - again. If you're still having trouble, contact Iterable support. - -### Step 4: Install Iterable's Android SDK in your mobile app - -To learn how to install Iterable's Android SDK, read about Iterable's -[Android SDK](https://support.iterable.com/hc/articles/360035019712). - -:::tip NOTE -You can receive Iterable push notifications without setting up the Android -SDK. To do so: - -- Set up your Android app as described in Google's - [Set up a Firebase Cloud Messaging client app on Android](https://firebase.google.com/docs/cloud-messaging/android/client) document. -- Call [`POST /api/users/registerDeviceToken`](https://support.iterable.com/hc/articles/204780579#post-api-users-registerdevicetoken) - each time the app opens. -- Call [`POST /api/users/disableDevice`](https://support.iterable.com/hc/articles/204780579#post-api-users-disabledevice) - each time the user signs out of the app -- Track push notification opens by calling [`POST /api/events/trackPushOpen`](https://support.iterable.com/hc/articles/204780579#post-api-events-trackpushopen). -::: - -### Step 5: Additional SDK configuration - -#### Handling Firebase push messages and tokens - -The SDK automatically adds a `FirebaseMessagingService` to the app manifest, so -you don't have to do any extra setup to handle incoming push messages. - -If your application implements its own `FirebaseMessagingService`, make sure you -forward `onMessageReceived` and `onNewToken` calls to -`IterableFirebaseMessagingService.handleMessageReceived` and -`IterableFirebaseMessagingService.handleTokenRefresh`, respectively: - -```java -public class MyFirebaseMessagingService extends FirebaseMessagingService { - - @Override - public void onMessageReceived(RemoteMessage remoteMessage) { - IterableFirebaseMessagingService.handleMessageReceived(this, remoteMessage); - } - - @Override - public void onNewToken(String s) { - IterableFirebaseMessagingService.handleTokenRefresh(); - } -} -``` - -To handle silent push notifications, use a custom `FirebaseMessagingService`. - -:::warning IMPORTANT -The step above is mandatory for handling multiple push providers. -::: - -Note that `FirebaseInstanceIdService` is deprecated and replaced with -`onNewToken` in recent versions of Firebase. - -#### Disabling push notifications to a device - -When a user logs out, you typically want to disable push notifications to -that user/device. This can be accomplished by calling `disablePush`. Please -note that it will only attempt to disable the device if you have previously -called `registerForPush`. - -In order to re-enable push notifications to that device, simply call -`registerForPush` as usual when the user logs back in. - -### Step 6: Use Iterable to send a test Android push notification - -To send a test Android push notification: - -1. In Iterable, navigate to **Audience > User Lookup** and enter the user's - `email` or `userId`. - - - In the `devices` array, find an object where `appPackageName` corresponds - to your app's package name, and `endpointEnabled` is `true`. - - Copy that object's `token`. - -2. Navigate to **Settings > Apps and Websites**. - -3. Click the mobile app to which you'd like to send a push notification. - -4. In the **Integrations** section, click **Test Push**. You'll see a - **Send Test Push** window: - - ![Testing a Firebase integration](https://support.iterable.com/hc/article_attachments/26154737837332/test-push.png "Testing a Firebase integration") - -3. Enter the device token you found above, and specify a test message. - -5. Click **Send test**. - -Monitor the recipient's device to verify that the push notification arrives. - -## Android 13: Push notification permissions - -To learn about the `POST_NOTIFICATIONS` permission introduced in Android 13, -which allows you to prompt users for permission to send push notifications, -check out [this information about Android 13](https://support.iterable.com/hc/articles/360057572291#android-13). - -## Customizing Android push notifications - -The following sections describe the technical setup necessary for various -Android push notification customizations in Iterable. - -For marketer-specific information about how to configure these features in -Iterable when sending a campaign, read [Creating a Push Notification Campaign](https://support.iterable.com/hc/articles/115000379086). - -### Notification color - -Add this line to `AndroidManifest.xml` to specify the notification color: - -```xml - -``` -where `#FFFFFF` can be replaced with a hex representation of a color of your -choice. In stock Android, the notification icon and action buttons will be -tinted with this color. - -You can also use a color resource: - -```xml - -``` - -### Channel name - -Since Android 8.0, Android requires apps to specify a channel for every -notification. Iterable uses one channel for all notification; to customize the -name of this channel, add this to `AndroidManifest.xml`: - -```xml - -``` - -You can also use a string resource to localize the channel name: - -```xml - -``` - -### Badging / dots - -Since Android 8.0, apps can indicate that they've received a notification by -displaying a dot (badge) on their icon. By default, Iterable's Android SDK -displays these badges. However, you can explicitly enable or disable them in -`AndroidManifest.xml`: - -```xml - -``` - -### Sounds - -To add sound to Android push notifications sent with Iterable, follow these -instructions: - -1. Put the necessary sound files in the Android project's **res/raw** folder. - - :::warning IMPORTANT - - Sound file names should be lowercase and should not have any special - characters. - - Take a look at Android's [documentation about supported media formats](https://developer.android.com/guide/topics/media/media-formats#audio-formats). This documentation is not - specific about which formats work for push notifications, so it's best to - test as necessary. - ::: - -2. Navigate to **Content > Templates** and open the push notification template. - -3. Click **Edit details** and scroll down. - -4. Enter the path to the custom sound file in the **Custom sound** field. - - ![Custom sound field](https://support.iterable.com/hc/article_attachments/9922751249556/push-custom-sound.png "Custom sound field") - - :::tip NOTES - - To use the default push notification sound, set this field to `default`. - - Whether or not a device plays the sound or vibrates depends on the - user's [device settings](https://support.google.com/android/answer/9082609). - ::: - -### Deep links - -Iterable push notification templates make it possible to set deep link -URLs for iOS and Android. - -To learn more about using Iterable's Android SDK to handle deep links, -read [Android App Links](https://support.iterable.com/hc/articles/360035127392). - -If your app is not using Iterable's Android SDK, it can still handle a deep -link contained in an Iterable push notification. Iterable provides the deep -link URL in the `defaultAction` object included in the notification's -payload. When this object's `type` field is set to `openUrl`, the `data` -field will contain the deep link URL. - -After a user has opened a tapped on a push notification to open the app, -use the `getPayloadData` method on `IterableApi` to access the notification -payload. - -### Background color - -To set the background color of a push notification, update the -`AndroidManifest.xml` file: - -```xml - -``` - -`#FFFFFF` can be replaced with any hex color. In stock Android, the -notification icon and action buttons will be tinted with this color. - -Alternatively, you can also use a color resource: - -```xml - -``` - -### Custom icons - -By default, push notifications display the application icon. To use a -different icon, place the image resource inside your app's **res/drawable** -directory. Then, edit `AndroidManifest.xml`, adding the following line: - -```xml - -``` - -In this case, `ic_notification_icon` is the name of the notification icon. - -Alternatively, call `setNotificationIcon(String iconName)` to use the custom -icon, referencing the image asset by name and without a file extension. - -## Further reading - -For more information about push notifications in Iterable, read: - -- [Sending Push Notifications](https://support.iterable.com/hc/articles/115000379086) -- Iterable's [Android SDK](https://support.iterable.com/hc/articles/360035019712) - diff --git a/sources/android/setting-up-mobile-inbox-on-android.md b/sources/android/setting-up-mobile-inbox-on-android.md deleted file mode 100644 index 4065988..0000000 --- a/sources/android/setting-up-mobile-inbox-on-android.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/360038744152 -title: Setting up Mobile Inbox on Android -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/in-app-messages/setting-up-mobile-inbox-on-android/index.md -source_ref: 59c40504c91bc0b13751c5ef5f348810eb0fd4f2 -source_sha: 9b54bece973b76efd0e1eaec0494b0e9d2c2af7c -fetched_at: 2026-08-03T20:41:29.000Z ---- -# Setting up Mobile Inbox on Android - -Apps using version 3.2.0 and later of Iterable's [Android SDK](https://support.iterable.com/hc/articles/360035019712) -can save in-app messages to an inbox. This inbox displays a list of saved in-app -messages and allows users to read them at their convenience. The SDK provides a -default user interface for the inbox, which can be customized to match your -brand's styles. This document describes how Android developers can add Iterable's -Mobile Inbox functionality to your mobile app. - -To learn how to use Iterable to send in-app messages that users can save to a -mobile inbox, read [Sending In-App Messages](https://support.iterable.com/hc/articles/360034903151). - -:::warning IMPORTANT -Versions 3.2.0 and higher of Iterable's Android SDK depend on the -[AndroidX](https://developer.android.com/jetpack/androidx) support libraries. -[Migrate your app to use AndroidX](https://developer.android.com/jetpack/androidx/migrate) -before using version 3.2.0 or higher. -::: - -## In this article - -[[toc]] - -## Installing Iterable's Android SDK - -To add a mobile inbox to your Android app, first install Iterable's -[Android SDK](https://support.iterable.com/hc/articles/360035019712). - -## Displaying the mobile inbox - -:::warning IMPORTANT -Iterable's mobile inbox UI is fragment-based: `IterableInboxFragment` requires a -`FragmentManager`, so its host must be a `FragmentActivity` (or its descendant, -`AppCompatActivity`). Compose-first apps often use a plain `ComponentActivity` as -their host, which has no `FragmentManager`—hosting the inbox fragment there -crashes when the fragment is attached. If your app is Compose-first, change the -host activity's base class to `FragmentActivity` / `AppCompatActivity` before -adding the inbox. (Iterable's Android SDK doesn't currently provide a -Compose-native inbox.) - -Note that this requirement applies to the inbox UI only. Starting with SDK -version 3.9.0, in-app messages themselves render correctly in Compose-first -apps. For more information, see [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231#displaying-in-app-messages-in-jetpack-compose-apps-sdk-v3-9-0-and-above). -::: - -In your app, show the mobile inbox when the user selects a specific tab or taps -a particular button. - -- To show the inbox as a tab: - - When using a [Navigation](https://developer.android.com/guide/navigation) - component, add the `IterableInboxFragment` to the navigation graph XML: - - ```xml - - ``` - -- To show the inbox as a separate activity in response to a button tap: - - Use the provided `InboxActivity` wrapper: - - _Kotlin_ - - ```kotlin - startActivity(Intent(context, IterableInboxActivity::class.java)) - ``` - - _Java_ - - ```java - startActivity(new Intent(getContext(), IterableInboxActivity.class)); - ``` - -## Syncing a mobile inbox across many devices - -Iterable's iOS and Android SDKs automatically sync a mobile inbox across all the -devices on which a user has logged in to your app. Additionally, they sync the -read state for each message. - -If you're not using one of Iterable's mobile SDKs: - -- To determine whether or not a message has been read, examine its `read` field, - as returned by [`GET /api/inApp/getMessages`](https://support.iterable.com/hc/articles/204780579#get-api-inapp-getmessages). -- To mark a message as read, call [`POST /api/events/trackInAppOpen`](https://support.iterable.com/hc/articles/204780579#post-api-events-trackinappopen). - -:::tip NOTE -For more information about cross-device read state syncing, see: -- Iterable's Android SDK, [v3.2.12 release notes](https://support.iterable.com/hc/articles/360027543332#_3-2-12) -- Iterable's iOS SDK, [v6.2.21 release notes](https://support.iterable.com/hc/articles/360027798391#_6-2-21) -::: - -## Customizing the mobile inbox - -To learn how to customize the mobile inbox in an Android app, read -[Customizing Mobile Inbox on Android](https://support.iterable.com/hc/articles/360039189931). - -## Further reading - -User guides: -- [In-App Messages and Mobile Inbox](https://support.iterable.com/hc/articles/217517406) -- [Sending In-App Messages](https://support.iterable.com/hc/articles/360034903151) -- [Events for In-App Messages and Mobile Inbox](https://support.iterable.com/hc/articles/360038939972) - -Developer documentation: -- Iterable's [iOS SDK](https://support.iterable.com/hc/articles/360035018152) -- Iterable's [Android SDK](https://support.iterable.com/hc/articles/360035019712) -- [In-App Messages Overview](https://support.iterable.com/hc/articles/360035538391) -- [In-App Messages on iOS](https://support.iterable.com/hc/articles/360035536791) -- [In-App Messages on Android](https://support.iterable.com/hc/articles/360035537231) -- [Setting up Mobile Inbox on iOS](https://support.iterable.com/hc/articles/360039137271) -- [Customizing Mobile Inbox on iOS](https://support.iterable.com/hc/articles/360039091471) -- [Customizing Mobile Inbox on Android](https://support.iterable.com/hc/articles/360039189931) -- [Animating In-App Messages with CSS](https://support.iterable.com/hc/articles/360035539271) -- [Image Carousels in In-App Messages](https://support.iterable.com/hc/articles/360035171132) -- [Testing and Troubleshooting In-App Messages](https://support.iterable.com/hc/articles/360035623391) -- [In-App Messages Without the SDK](https://support.iterable.com/hc/articles/360018709631) -- [Getting Started with Iterable's API](https://support.iterable.com/hc/articles/41044692130196) -- [API Endpoints and Sample Payloads](https://support.iterable.com/hc/articles/204780579) diff --git a/sources/android/setting-up-unknown-user-activation.md b/sources/android/setting-up-unknown-user-activation.md deleted file mode 100644 index 48620cc..0000000 --- a/sources/android/setting-up-unknown-user-activation.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/40078870805396 -title: Setting up Unknown User Activation -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/unknown-user-activation-dev/setting-up-unknown-user-activation/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 45d0ae07bce89a4a4156c3b5e46f6bd4136d41b2 -fetched_at: 2026-05-25T15:11:49.365Z ---- -# Setting up Unknown User Activation - -Unknown User Activation makes it possible to learn about, message, and develop -relationships with unidentified users of your mobile app and website. Before you -begin setting it up, learn more about how it works in [Unknown User Activation Overview](https://support.iterable.com/hc/articles/38755339847188). - -## In this article - -[[toc]] - -## API keys and JWT considerations - -Unknown User Activation is available for use with Iterable's SDKs. At this time, -Iterable's API does not include endpoints for Unknown User Activation. - -When using Iterable's iOS, Android, or Web SDKs, remember that: - -- You'll need an Iterable API key (of type Web or Mobile). -- For Iterable's Web SDK, JWT-enabled API keys are required. For Iterable's iOS - and Android SDKs, they're optional but recommended. - -JWT-enabled API keys are more secure than standard API keys because they require -your server to authorize each user with a custom JWT token. Iterable's SDKs must -request these tokens from your server for each user; Iterable can't generate them -for you. - -To use JWT-enabled API keys, set up a web service that Iterable's SDKs can call -to get JWT tokens—this is required for all JWT-enabled API keys. Make sure your -JWT server can issue tokens for SDK-generated unknown `userId` values, as well as -for known `userId` or `email` values. You might also consider having your web -service notify you whenever the SDK creates a new unknown user ID, so your -server can issue JWT tokens for those users. - -:::tip WARNING -Never embed a Server-side API key in a mobile or web app–they can be accessed by -malicious users to access your project data. -::: - -## Setup tasks -To set up Unknown User Activation, complete the following tasks: - -### Update your JWT server to issue JWT tokens for unknown users - -When implementing Unknown User Activation, if your mobile and web apps use -JWT-enabled API keys (required for Iterable's Web SDK, recommended for -Iterable's iOS and Android SDKs), update your JWT server to return JWT tokens -for unknown users created by Iterable's SDKs. - -To identify an unknown user, Iterable's SDKs can only access the user ID that was -generated by the SDK when the unknown user profile was created — not the -server-assigned placeholder email for that same user. (In email-based projects, -which identify users by email, Iterable assigns each unknown user a placeholder -email address based on, but different from, the SDK-created user ID. However, the -SDK doesn't have access to this email.) - -Because of this, your JWT server must be able to issue JWT tokens as follows: - -- For apps and websites associated with userID-based and hybrid Iterable - projects, your JWT server must be able to issue JWT tokens for: - - SDK-generated `userId` values (UUID values; to authenticate unknown users). - - `userId` values you assign to user profiles when converting them from unknown - to known (to authenticate known users). - -- For email-based projects, your JWT server must be able to issue JWT tokens - for: - - SDK-generated `userId` values (UUID values; to authenticate unknown users - created by the SDK). - - Email addresses you assign to user profiles when converting them from - unknown to known (to authenticate known users). - -:::tip NOTE -For more information about using JWT-enabled API keys with Unknown -User Activation, see [API Keys and JWT Considerations](#api-keys-and-jwt-considerations). -::: - -### Define profile creation criteria - -Before enabling Unknown User Activation in your mobile apps and website, make -sure your marketing team has set up test criteria in Iterable that tells the -SDKs when to create unknown user profiles in Iterable. When visitors do not meet -the defined criteria (or if there is no criteria defined), they will continue to -be stored only on-device or in-browser, and you won't see them in your Iterable -project. - -At runtime, Iterable's iOS, Android, and Web SDKs fetch these criteria, evaluate -them, and create unknown user profiles for visitors who satisfy their -requirements. For example, a simple criteria might specify to create unknown -user profiles for visitors with a `viewedProduct` event. - -The Web SDK fetches these criteria after you enable local data tracking (in -response to user consent), and then refreshes them on each page refresh. The iOS -and Android SDKs also fetch these criteria when you enable local data tracking -(again, in response to user consent) or foregrounding, and then again each time the -customer launches the app. - -### Install or update an Iterable SDK - -To use Unknown User Activation with Iterable's SDKs, you'll need to upgrade to one -of the following SDK versions. - -- For the iOS SDK, use version 6.6.0 of Iterable's iOS SDK. - See [Configure the iOS SDK](https://support.iterable.com/hc/articles/40078945603860). - -- For the Android SDK, use version 3.6.0 of Iterable's Android SDK. - See [Configure the Android SDK](https://support.iterable.com/hc/articles/40078934178836). - -- For the Web SDK, use version 2.2.0 of Iterable's Web SDK. - See [Configure the Web SDK](https://support.iterable.com/hc/articles/40079019401236). diff --git a/sources/android/tracking-events-with-iterables-mobile-sdks.md b/sources/android/tracking-events-with-iterables-mobile-sdks.md deleted file mode 100644 index 51a77be..0000000 --- a/sources/android/tracking-events-with-iterables-mobile-sdks.md +++ /dev/null @@ -1,308 +0,0 @@ ---- -url: https://support.iterable.com/hc/articles/360035395671 -title: Tracking Events and Purchases with Iterable's Mobile SDKs -useInNovaDocs: true -source_repo: Iterable/iterable-docs -source_path: docs/developer-and-api-docs/event-tracking/tracking-events-with-iterables-mobile-sdks/index.md -source_ref: 16ae7f4a908f84d6eb15fe6f5390f07cc5afe20d -source_sha: 0dbb170bfbd574bf405b33990d2c288ad8dcd153 -fetched_at: 2026-05-25T15:11:48.241Z ---- -# Tracking Events and Purchases with Iterable's Mobile SDKs - -Iterable's mobile SDKs can track _events_, which correspond to actions taken -by your app's users. Events can be related to messages you've sent (for example, -a user opening an in-app message) or to a particular feature or piece of content -in your app (for example, a user signing up for a new account). - -You can use events in segmentation and journeys to help you reach the right -users with the right content. - -:::tip NOTES -- To reduce noise and make the data you store in Iterable as useful and - actionable as possible, avoid the temptation to track every custom event you - can think of. Instead, track events related to important milestones, since - they can help you reach your customers in useful and engaging ways. -- To learn more about keeping track of your custom event usage, read - [Monitoring Custom Event Usage](https://support.iterable.com/hc/articles/360043366492). -::: - -## In this article - -[[toc]] - -## Custom event names - -In general, it's a good idea to avoid using spaces in custom event names, since -you'll need to use a [special Handlebars syntax](https://support.iterable.com/hc/articles/36530857619348#blank-or-missing-values-in-rendered-content) -to reference a field name that contains a space. - -For example, for an event saved when a user complete's the sign-up process for -your service, `completedSignup` is a good event name but `completed signup` is -not. - -## Tracking custom events - -To use track an event using Iterable's mobile SDKs, use syntax such as: - -_Swift_ - -```swift -IterableAPI.track( - event: "customEvent", - dataFields: ["key": "value"] -) -``` - -_Objective-C_ - -```objectivec -[IterableAPI track:@"custom_event" dataFields:@{@"key": @"value"}]; -``` - -_Java_ - -```java -IterableApi.getInstance().track( - "customEvent", - dataFields -); -``` - -_JavaScript (React Native)_: - -```javascript -Iterable.trackEvent( - "completedOnboarding", - { - "includedProfilePhoto": true, - "favoriteColor": "red" - } -); -``` - -## Tracking purchase events - -To help you [track information related to purchases and revenue](https://support.iterable.com/hc/articles/205480285), -Iterable's mobile SDKs include a `trackPurchase` method. - -_Swift_ - -```swift -// Example dataFields -let dataFields: [String: Any] = [ - "Store_Address": [ - "Street1": "123 Main St", - "Street2": "Apt 1", - "City": "Iter-a-ville", - "State": "CA", - "Zip": "90210" - ] -] - - -// Create an array of CommerceItem objects -let item : CommerceItem = CommerceItem( - id: "TOY1", - name: "Red Racecar", - price: 4.99, - quantity: 1, - sku: "RR123", - description: "A small, red racecar.", - url: "https://www.example.com/toys/racecar", - imageUrl: "https://www.example.com/toys/racecar/images/car.png", - categories: ["Toy", "Inexpensive"] -) - -let items = [item] - -// Make the call to Iterable's API -IterableAPI.track(purchase: 4.99, items: items, dataFields: dataFields) -``` - -_Objective-C_ - -```objectivec -// Example dataFields -NSDictionary *dataFields = @{ - @"Store_Address": @{ - @"Street1": @"123 Main St", - @"Street2": @"Apt 1", - @"City": @"Iter-a-ville", - @"State": @"CA", - @"Zip": @"90210" - } -}; - -// Create an array of CommerceItem objects -CommerceItem *item = [[CommerceItem alloc] - initWithId:@"TOY1" - name:@"Red Racecar" - price:@4.99F - quantity:1 - sku:@"RR123" - description:@"A small, red racecar" - url:@"https://www.example.com/toys/racecar" - imageUrl:@"https://www.example.com/toys/racecar/images/car.png" - categories:@[@"Toy", @"Inexpensive"]]; - -NSArray *items = @[item]; - -// Make the call to Iterable's API -[IterableAPI trackPurchase:@4.99F items:items dataFields:dataFields]; -``` - -_Java_ - -```java -// Example dataFields -JSONObject store_address = new JSONObject(); -final JSONObject dataFields = new JSONObject(); - -try { - store_address.put("Street1", "123 Main St"); - store_address.put("Street2", "Apt 1"); - store_address.put("City", "Iter-a-ville"); - store_address.put("State", "CA"); - store_address.put("Zip", "90210"); - datafields.put("dataFields", store_address); -} catch (JSONException e) { - e.printStackTrace(); -} - -// Create an array of CommerceItem objects -CommerceItem item = new CommerceItem( - "TOY1", - "Red Racecar", - 4.99, - 1, - "RR123", - "A small, red racecar", - "https://www.example.com/toys/racecar", - "https://www.example.com/toys/racecar/images/car.png", - new String[] {"Toy", "Inexpensive" } -); - -List items = new ArrayList(); -items.add(item); - -// Make the call to Iterable's API -IterableApi.getInstance().trackPurchase(new Double(4.99), items, dataFields); -``` - -## Offline events processing - -:::tip TIP -If you'd like to use offline events processing, we'll need to enable it for -your account. Talk to your customer success manager to get started. -::: - -Mobile apps built with Iterable's mobile SDKs can queue up events created when a -device is offline (for example, because there isn't a network connection -available, or because airplane mode is on), and then send them to Iterable the -next time the app is in the foreground with a network connection. - -To use this feature, upgrade your apps to use: - -- Iterable's iOS SDK, version [`6.4.5+`](https://github.com/Iterable/iterable-swift-sdk/releases/tag/6.4.5) -- Iterable's Android SDK, version [`3.4.7+`](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.4.7) -- Iterable's React Native SDK, version [`1.3.3+`](https://github.com/Iterable/react-native-sdk/releases/tag/1.3.3) - -After you've upgraded your apps to use these SDK versions and your customer -success manager has enabled offline events processing for your account, your -apps will automatically capture and save offline events. Iterable's mobile SDKs -provide offline processing for the following types of events: - -- Purchase -- Update cart -- Push open -- In-app open -- In-app click -- In-app close -- Inbox session -- In-app delivery -- In-app consume -- Embedded message received -- Embedded message click -- Embedded message session -- Custom events tracked manually - -When your app is next in the foreground with an internet connection, it will send -any queued offline events back to Iterable. - -:::tip INFO -Iterable's mobile SDKs do not queue up any other API calls to send later (e.g., -`registerDeviceToken`, `disableDevice`, `updateUser`, `updateSubscription`, or -`updateEmail`). -::: - -### Timestamps - -When sending offline events to Iterable (when a network connection has been -reestablished), Iterable's mobile SDKs include values for two timestamps: - -`createdAt` - The date and time when the user triggered the event. -`sentAt` - The date and time when the event is sent to Iterable. - -Iterable uses the difference between an event's `sentAt` time and the server time -when the event is received to adjust the event's `sentAt` and `createdAt` times to -be in sync with server time. When you query events from Iterable's API, you'll -see both of these timestamps. - -### Differentiating offline and online events - -To determine if a particular event saved in Iterable was originally captured -offline, check its attributes for mismatched `createdAt` and `sentAt` values. This -indicates that the event was captured at one time and saved at another, as is -the case for offline events. - -If an event doesn't have a `sentAt` value, it was captured online (`sentAt` is a -new field introduced with this feature. - -### Multiple users - -When users sign out of your app, Iterable's mobile SDKs delete any captured -offline events that haven't yet been saved back to Iterable. - -### Storage limits and timeframes - -Iterable's mobile SDKs will capture up to 1000 offline events, and then stop -capturing new offline events. However, there's no limit to the amount of time -for which offline events can be saved on a device. - -### JWT-enabled API keys - -Iterable's mobile SDKs support offline events processing for JWT-enabled API -keys and non JWT-enabled API keys. - -Starting with Android SDK 3.7.0+ and iOS SDK 6.6.8+, the SDKs include an -auto-retry feature for JWT failures during offline processing. When a queued -event encounters a 401 JWT error, the SDK automatically pauses authenticated -task processing, refreshes the JWT token, and retries the failed task. -Unauthenticated API calls (such as `disableDevice` and `mergeUser`) continue -processing while authentication is paused. This feature is controlled by a -remote configuration flag and requires no code changes. - -:::tip NOTE -On older SDK versions, if the JWT token gets invalidated between the time that -offline events are queued and the time the device comes back online, those -queued events may not be saved to Iterable. -::: - -### Increased event volume - -When you have your customer success manager enable offline events processing for -your account (and update to an SDK version that supports it), you may see an -increase in the number of custom events saved in your project (since offline -events that were previously dropped are now captured). However, this isn't -necessarily the case, and depends on the usage patterns of your users. To -monitor custom event usage, use the [Custom Event Usage](https://support.iterable.com/hc/articles/360043366492) -page. - -### Triggering journeys - -Like other events, offline events can trigger journeys when they're eventually -saved back to Iterable. However, it may not be desirable for older events to -trigger journeys that are no longer relevant. Because of this, offline events -saved to Iterable more than 24 hours after their creation will not trigger -journeys.