@wireai/activation 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +116 -3
  3. package/README.md +82 -0
  4. package/dist/analytics/index.d.mts +2 -2
  5. package/dist/analytics/index.d.ts +2 -2
  6. package/dist/analytics/index.js +61 -2
  7. package/dist/analytics/index.js.map +1 -1
  8. package/dist/analytics/index.mjs +61 -2
  9. package/dist/analytics/index.mjs.map +1 -1
  10. package/dist/{currentSession-_GynvhzT.d.mts → currentSession-ClkLjcJ0.d.mts} +298 -13
  11. package/dist/{currentSession-D7zabMXK.d.ts → currentSession-DOVZEWJl.d.ts} +298 -13
  12. package/dist/index.d.mts +195 -4
  13. package/dist/index.d.ts +195 -4
  14. package/dist/index.js +909 -340
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +693 -145
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/questionnaire/index.js.map +1 -1
  19. package/dist/questionnaire/index.mjs.map +1 -1
  20. package/dist/reviews/index.js +12 -1
  21. package/dist/reviews/index.js.map +1 -1
  22. package/dist/reviews/index.mjs +12 -1
  23. package/dist/reviews/index.mjs.map +1 -1
  24. package/package.json +1 -1
  25. package/src/OnboardingFlow.tsx +131 -0
  26. package/src/WireOnboarding.tsx +63 -2
  27. package/src/analytics/analyticsEvent.ts +16 -1
  28. package/src/analytics/eventQueue.ts +2 -0
  29. package/src/analytics/reportClientEvent.ts +68 -12
  30. package/src/cards/PermissionCard.tsx +438 -0
  31. package/src/cards/index.ts +7 -0
  32. package/src/illustrations/defaultIllustrations.tsx +44 -3
  33. package/src/index.ts +38 -0
  34. package/src/permissions/index.ts +64 -0
  35. package/src/permissions/permissionCopy.ts +87 -0
  36. package/src/permissions/permissionEvents.ts +76 -0
  37. package/src/permissions/permissionMemory.ts +88 -0
  38. package/src/permissions/placement.ts +88 -0
  39. package/src/permissions/types.ts +131 -0
  40. package/src/session/persistedSession.ts +10 -3
  41. package/src/types.ts +66 -4
package/AGENTS.md CHANGED
@@ -48,6 +48,7 @@ subpaths are optional secondary feature modules; import one only if you use it.
48
48
  - `themeFromBrand({ primary })`: derive a full theme from one brand color.
49
49
  - `mergeTheme`, `defaultOnboardingTheme`, `OnboardingThemeProvider`, `useOnboardingTheme`.
50
50
  - `defaultIllustrations`: dependency-free fallback art; spread your own over it.
51
+ - `WIRE_PERMISSION_EVENTS`, `resolvePermissionCopy(...)`: the canonical permission-funnel names and the shipped rationale copy behind `permissionScreens` (see the prop below). The kit imports no native permission module; the host injects `request`.
51
52
  - `createRevenueCatBridge({ analytics, entitlementId })`: the RevenueCat purchase funnel (see "RevenueCat" below).
52
53
  - `activationJoinContext(deviceKey)`: builds the `userContext` value that joins an onboarding session to the app's later events. Every `<WireOnboarding>` needs it (see step 7).
53
54
  - `resolveAutoDeviceKey({ appId, storage })`: the kit's own persisted per-install `device_key`, for a host that owns none.
@@ -65,9 +66,10 @@ subpaths are optional secondary feature modules; import one only if you use it.
65
66
  | `illustrations` | `Record<string, ReactNode>` | no | Host artwork for `InterstitialCard`, keyed by name. `{ ...defaultIllustrations, ...myArt }`. |
66
67
  | `icons` | `Record<string, ReactNode>` | no | Host icon nodes keyed by the semantic vocabulary name the AI emits (`{ instagram: <BrandIg/> }`). Checked FIRST: use it to brand an icon, to add names of your own, or to supply icons without installing `@expo/vector-icons`. Unlisted names fall back to that optional peer, then to no icon. |
67
68
  | `validators` | `Record<string, StepValidator>` | no | Per base-question key (e.g. `username`). Blocks advance + inline error. |
69
+ | `permissionScreens` | `PermissionScreenConfig[]` | no | Priming screens injected mid-flow (notifications first). `{ permission, placement?, request, getStatus?, openSettings?, copy?, illustration?, onResult? }`. The OS dialog opens ONLY on the primary tap, never on mount; "Maybe later" advances without spending the one native prompt. `placement` is `"start"`, `{ afterCard: n }` or `"beforeEnd"` (default), and an `afterCard` past the end of the stream clamps to `"beforeEnd"`. Zero new dependencies: the host injects `request`, the kit imports no native permission module. Shown once per session and, with `storage`, across an app kill. Not a question: no `key`/`slot_id`, nothing in `answers`, completion never blocks on a grant. |
68
70
  | `onSkip` | `() => void` | no | Retained for back-compat. Per-question Skip is now internal (the kit advances one question on `skippable` screens), so this is no longer wired to that control. |
69
71
  | `onError` | `(err) => void` | no | Backend error/timeout after retries. Host recovery (e.g. static onboarding). `fallbackFlow` takes precedence over it. |
70
- | `onEvent` | `(e: OnboardingEvent) => void` | no | Lifecycle events for analytics (kit owns the loop). Variants: `started`, `resumed`, `turn`, `error`, `retry`, `fallback` (see `OnboardingEvent`). |
72
+ | `onEvent` | `(e: OnboardingEvent) => void` | no | Lifecycle events for analytics (kit owns the loop). Variants: `started`, `resumed`, `turn`, `error`, `retry`, `fallback`, `permission` (see `OnboardingEvent`). |
71
73
  | `copy` | `Partial<OnboardingCopy>` | no | Localize built-in English strings. |
72
74
  | `approxScreens` | `number` | no | Paces the bar; backend `progress.total` wins, so usually unneeded. |
73
75
  | `startMessage` | `string` | no | First backend message. Default `"start"`. |
package/CHANGELOG.md CHANGED
@@ -3,6 +3,116 @@
3
3
  All notable changes to `@wireai/activation` (formerly `wireai-onboarding`).
4
4
  Historical entries below the rename keep the old package name on purpose.
5
5
 
6
+ ## [0.13.2] — 2026-07-31
7
+
8
+ The headline is a fix: every `track()` / `screen()` `app_event` was being silently discarded by the
9
+ server behind an HTTP 200, on every kit version since 0.11.0. This release also ships the injectable
10
+ mid-flow permission screens below, and it is the FIRST version whose published tarball is clean of
11
+ the banned client name — the 0.13.0 publish-surface scrub now rides a build-time canary that fails
12
+ the pack if the name ever reappears.
13
+
14
+ ### Fixed
15
+
16
+ - **`ts` crosses the wire as ISO8601, so `app_event`s actually land.**
17
+ (`analytics/reportClientEvent.ts`) The server's event model declares `ts: str | None` and pydantic
18
+ v2 does not coerce a number into it, while the offline queue has stamped `ts = Date.now()` (epoch
19
+ ms) on every queued event since 0.11.0 — so `POST /v1/events` answered HTTP 200 with
20
+ `{written: 0, skipped: N, errors: [{field: "ts", reason: "validation_error"}]}` and 100% of
21
+ `track()` / `screen()` analytics evaporated behind a green response. `buildEventsRequest` — the
22
+ single choke point all four send paths share (offline queue, fire-and-forget, awaitable,
23
+ session-start) — now serializes a numeric `ts` to an ISO8601 UTC string on its way out. The queue
24
+ keeps its numeric stamp (the identical-JSON de-dup signature depends on it), a persisted 0.13.0
25
+ backlog is converted as it drains, a caller-set ISO string passes through untouched, and a
26
+ non-finite or out-of-range number drops the field from that one event instead of killing the batch.
27
+ - **The discard warning names the field the server refused.** (`readEventsAck`) The server's
28
+ `errors[]` entries carry a `field` alongside `reason`; the ack reader threw it away, so a dev build
29
+ warned `validation_error` with no address. It now prints `validation_error (field: ts)` — the
30
+ difference between a one-line fix and an investigation.
31
+
32
+ ### Added
33
+
34
+ Injectable mid-flow permission screens, starting with notifications.
35
+
36
+ Onboarding is where apps ask for notifications and where most of them lose the ask: iOS grants an
37
+ app exactly ONE native prompt for its entire lifetime, so firing it from a mount effect on screen
38
+ one spends it on a user who has been told nothing, and the only route back is a Settings trip almost
39
+ nobody makes. This release ships the priming pattern as a first-class part of the flow. You declare
40
+ a screen, it explains why in your words, and the OS dialog opens only on the user's primary tap.
41
+ "Maybe later" advances the flow with the prompt unspent.
42
+
43
+ Nothing changes for a host that configures no screen: the prop is optional and every path around it
44
+ is byte-identical.
45
+
46
+ - **`permissionScreens` on `<WireOnboarding>`** (`permissions/types.ts`, `cards/PermissionCard.tsx`) -
47
+ one or more priming screens injected into the server-driven card stream at a position you choose.
48
+ The screen carries customizable rationale copy, kit-quality defaults for notifications, optional
49
+ artwork through the existing illustrations registry, and an `onResult` callback per screen.
50
+ **DEPENDENCY-FREE, the RevenueCat-bridge idiom:** the kit imports no `expo-notifications`, no
51
+ `react-native-permissions`, nothing native. The host injects `request` and optionally `getStatus`
52
+ and `openSettings`, exactly the way it hands the RevenueCat bridge real RevenueCat objects. The
53
+ README shows the five-line wiring.
54
+ - **The priming rule, enforced in code and pinned by a canary.** There is no path from mount, from
55
+ an effect, from a timer or from render to the host's `request`; only the primary press handler
56
+ reaches it. The optional `getStatus` probe is a READ that never prompts, and all it decides is
57
+ which primary the screen offers: ask, open settings (status `blocked`, where an ask would show
58
+ nothing at all), or a plain continue (already granted, or a host that supplied no `request`, where
59
+ the kit declines rather than fabricating a button that does nothing).
60
+ - **A watchdog on the host's `request`, which fabricates nothing.** Asking sets a busy state that
61
+ disables both controls, which is right while an OS dialog may be open and fatal if the host's
62
+ `request` never settles at all (a swallowed native callback, a promise nobody resolves): the
63
+ screen would be a dead end with nothing tappable, and completion could never fire. A generous 90
64
+ second ceiling now hands the controls back so the user can retry or skip. On expiry it records NO
65
+ outcome and emits NO event, only a dev warning, because a pending request is not a denial and a
66
+ person can legitimately sit on a permission dialog for minutes. First settlement wins: if the
67
+ original request answers late, after the controls were handed back and possibly after a second
68
+ attempt already resolved, it advances nothing and emits nothing.
69
+ - **Placement semantics with a clamp** (`DEFAULT_PERMISSION_PLACEMENT`, `selectDuePermissionScreen`)
70
+ - `"start"`, `{ afterCard: n }` and `"beforeEnd"` (the default), resolved against the card about
71
+ to render, because the stream length is server-driven and varies per user. An `afterCard` the flow
72
+ never reaches degrades to `"beforeEnd"` instead of silently never showing, which is the failure
73
+ mode that raises no error anywhere and leaves the host looking at a funnel that reads zero.
74
+ - **`WIRE_PERMISSION_EVENTS`** plus `permissionEventName`, `permissionEventProps` and
75
+ `normalizePermissionStatus` (`permissions/permissionEvents.ts`) - the canonical funnel names
76
+ (`wire_permission_screen_shown`, `wire_permission_primer_accepted`, `wire_permission_granted`,
77
+ `wire_permission_denied`, `wire_permission_skipped`, `wire_permission_settings_opened`), mirroring
78
+ `WIRE_PURCHASE_EVENTS`. They land as `app_event` `question_key` values on the same `/v1/events`
79
+ path, carrying the same device snapshot and `user_context`, so the `device_key` join that makes
80
+ every other number real covers these too. Each moment is also surfaced on `onEvent` as
81
+ `{ type: "permission", ... }`.
82
+ - **`resolvePermissionCopy`, `NOTIFICATIONS_PERMISSION_COPY`, `GENERIC_PERMISSION_COPY`,
83
+ `DEFAULT_PERMISSION_COPY`** - the shipped English plus per-line overrides. An override that is
84
+ `undefined` or empty is IGNORED rather than allowed to blank a button, because hosts build this
85
+ object from an i18n layer where a missing translation resolves to `undefined`.
86
+ - **Once per session, kept true across an app KILL** (`permissionStorageKey`,
87
+ `loadSettledPermissions`, `saveSettledPermissions`, `readSettledPermissions`,
88
+ `clearSettledPermissions`). The kit resumes a killed onboarding into the SAME backend session, so
89
+ without a persisted record the resumed mount would re-show a screen the user already answered,
90
+ which on the ask path means a second attempt at the one prompt iOS grants. The record is scoped to
91
+ the session id, one entry, so a genuinely new onboarding still starts clean with nothing having to
92
+ expire it. Same host-injected `storage`, same read ceiling, same best-effort discipline: a
93
+ rejecting adapter costs a re-ask on resume, never a broken or gated flow.
94
+ - **`PermissionCard` and `PERMISSION_CARD_NAME`** - the screen registered as an SDK component, so a
95
+ later server-emitted placement (AI-chosen timing) adopts this exact screen with no rewrite.
96
+ Deliberately NOT part of `onboardingComponents`: that array is what the device advertises as
97
+ renderable, and a backend told it may emit a permission screen could emit one into a host that
98
+ injected no `request`.
99
+ - **A `notifications` entry in `defaultIllustrations`** so a notification screen is never a blank
100
+ box with no host wiring. Register your own under the same name to override it.
101
+
102
+ ### Changed
103
+
104
+ - **`OnboardingEvent` gains a `permission` variant** and `toAnalyticsEvent` maps it to its own
105
+ canonical `wire_permission_*` name rather than folding it into an onboarding one. Additive: a host
106
+ that ignores the new variant is unaffected, and every existing variant maps exactly as before.
107
+ - **A permission screen is NOT a question, structurally.** It sends nothing to the backend, appends
108
+ nothing to the thread, mints no `key` and no `slot_id`, and never touches the progress step, so
109
+ `deriveAnswers`, `readProgress` and the completion semantics cannot tell it happened. Completion
110
+ never blocks on a grant: grant, deny, skip and blocked all continue the flow, and a host callback
111
+ that throws (a native module blowing up, a failed scheduling call) is caught rather than allowed
112
+ to strand the user on the screen.
113
+ - **Notification SCHEDULING is deliberately out of scope.** `onResult` is the seam: schedule your
114
+ first local reminder there, with your own call, the moment a grant lands.
115
+
6
116
  ## [0.13.0] — 2026-07-27
7
117
 
8
118
  The identity-provenance release. Every id-layer defect fixed here is one omission wearing five faces:
@@ -35,8 +145,11 @@ direction: the kit now declines and says so, where it used to fabricate and retu
35
145
  `props.progress` by `readProgress` and preferred over `key` by `deriveAnswers`. `key` is authored from
36
146
  the question's prompt text (tenant flows slugify it and cut at 32 chars), so re-wording a question mints
37
147
  a NEW key and a host reading `answers.interests` silently starts reading `undefined`.
38
- ⚠️ **INERT UNTIL THE SERVER EMITS IT.** No deployed server sends `slot_id` today. Against a server that
39
- never sends it, every path degrades to byte-identical 0.12.2 behaviour. Every fallback is decided
148
+ ⚠️ ~~**INERT UNTIL THE SERVER EMITS IT.** No deployed server sends `slot_id` today.~~
149
+ **CORRECTED 2026-07-28: this is LIVE.** The deployed server (`47dae92`) sends `slot_id` on
150
+ `progress` for every AI-generated question, as `adaptive_<n>`, and for a configured question
151
+ whenever the tenant set one. Where no slot is configured the field is absent and
152
+ every path degrades to byte-identical 0.12.2 behaviour. Every fallback is decided
40
153
  PER CARD, never cached as a per-session "this backend supports slots" verdict, so a thread that mixes
41
154
  slotted and unslotted cards — the real shape during a rollout — keys each card correctly.
42
155
  - **`allowAppSessionFallback` on `identifyOnboarding`** — the opt-in switch for the tier-3 behaviour
@@ -280,7 +393,7 @@ or a client-side default.
280
393
 
281
394
  ## [0.12.0] — 2026-07-27
282
395
 
283
- The release Myelino 2.2.0 pins. Ships the RevenueCat path (#52), the identity/counting audit,
396
+ The release the first production consumer pins. Ships the RevenueCat path (#52), the identity/counting audit,
284
397
  the session-id fallback contract, and the transport/config fixes behind a silently dead lifecycle
285
398
  stream (a consumer's `first_open` read 6 all-time while the emitting code was deployed).
286
399
 
package/README.md CHANGED
@@ -235,6 +235,7 @@ import { WireOnboarding } from "@wireai/activation";
235
235
  | `theme` | `Partial<OnboardingTheme>` | Brand colors/fonts/radius/spacing, deep-merged over a neutral default. |
236
236
  | `illustrations` | `Record<string, ReactNode>` | App artwork for `InterstitialCard`, keyed by name. |
237
237
  | `validators` | `Record<string, StepValidator>` | Per-step, keyed by base-question key (e.g. `username`). Blocks advance + shows an inline error. When the backend sends a `progress.slot_id` for a screen, the kit looks up that slot FIRST and falls back to the question key, so a validator map written against today's keys keeps working. |
238
+ | `permissionScreens` | `PermissionScreenConfig[]` | Priming screens injected mid-flow, starting with notifications. The screen explains why, and the OS dialog opens **only** on the primary tap, never on mount. Zero new dependencies: you inject `request`. See [Permission screens](#permission-screens-the-priming-pattern). |
238
239
  | `onSkip` | `() => void` | User skipped. |
239
240
  | `onError` | `(err) => void` | Backend error/timeout — host owns recovery (e.g. route to a static flow). Without it, the kit shows an inline retry. |
240
241
  | `onEvent` | `(e: OnboardingEvent) => void` | `started` / `turn` / `error` — recover per-turn analytics since the kit owns the loop. |
@@ -608,6 +609,87 @@ icons={{
608
609
  Icons are decorative: the label carries the meaning, so an icon stays hidden from screen readers
609
610
  and never becomes an option's accessible name.
610
611
 
612
+ ## Permission screens (the priming pattern)
613
+
614
+ Onboarding is where apps ask for notifications, and it is where most of them lose the ask. iOS gives an app **one** native notification prompt for its entire lifetime. Fire it from a mount effect on screen one and you have spent it on a user who has been told nothing, and the only route back is a trip through the Settings app that almost nobody makes.
615
+
616
+ So the kit ships the priming pattern. You inject a screen into the flow, it explains why in your words, and the OS dialog opens **only** when the user taps the primary button. "Maybe later" advances the flow with the prompt still unspent, so you can ask again in a better moment.
617
+
618
+ ```tsx
619
+ import * as Notifications from "expo-notifications";
620
+ import { WireOnboarding } from "@wireai/activation";
621
+
622
+ <WireOnboarding
623
+ config={config}
624
+ onComplete={persist}
625
+ permissionScreens={[
626
+ {
627
+ permission: "notifications",
628
+ placement: "beforeEnd",
629
+ request: async () => {
630
+ const { status, canAskAgain } = await Notifications.requestPermissionsAsync();
631
+ return status === "granted" ? "granted" : canAskAgain ? "denied" : "blocked";
632
+ },
633
+ getStatus: async () => {
634
+ const { status, canAskAgain } = await Notifications.getPermissionsAsync();
635
+ return status === "granted" ? "granted" : canAskAgain ? "denied" : "blocked";
636
+ },
637
+ onResult: (_permission, outcome) => {
638
+ if (outcome === "granted") scheduleFirstReminder();
639
+ },
640
+ },
641
+ ]}
642
+ />
643
+ ```
644
+
645
+ That is the whole integration, and **the kit adds no dependency for it**. It imports no `expo-notifications`, no `react-native-permissions`, nothing native, the same way the RevenueCat bridge takes real RevenueCat objects without depending on `react-native-purchases`. You own the native call; the kit owns the screen, the timing, and the funnel.
646
+
647
+ ### Where the screen lands
648
+
649
+ The stream is server-driven, so its length changes per user. A placement is therefore resolved against the card about to render, not against a fixed index:
650
+
651
+ | `placement` | Where it shows |
652
+ |---|---|
653
+ | `"beforeEnd"` (default) | Right before the terminal recap, once the user has invested in the flow. |
654
+ | `"start"` | Before the first question. |
655
+ | `{ afterCard: 2 }` | After two questions, so between card 2 and card 3. |
656
+
657
+ An `afterCard` the flow never reaches **clamps to `"beforeEnd"`** instead of silently never showing. That matters more than it sounds: a screen that never appears raises no error anywhere, and the only evidence is a permission funnel that reads zero forever.
658
+
659
+ ### What it guarantees
660
+
661
+ - **The dialog is only ever reached from the primary tap.** No mount effect, no timer, no auto-fire. `getStatus` is a read and never prompts; it only decides which primary the screen offers.
662
+ - **Already blocked?** The primary becomes "Open settings", because an `ask` there would open nothing at all.
663
+ - **Once per session, and it survives an app kill.** With `storage`, a resumed session does not re-ask (the record is keyed to the session id, so a genuinely new onboarding still starts clean).
664
+ - **Completion never blocks on a grant.** Grant, deny, skip and blocked all continue the flow.
665
+ - **It is not a question.** Nothing is sent to the backend, nothing enters the thread, no `key` or `slot_id` is minted, and `onComplete`'s `answers` are identical to the same flow with no screen configured.
666
+
667
+ ### Copy and artwork
668
+
669
+ The kit ships English good enough to ship. Override any single line and the rest of the default stays:
670
+
671
+ ```tsx
672
+ copy: { title: "Never miss a session", primaryLabel: "Turn on reminders" }
673
+ ```
674
+
675
+ Artwork comes from the same `illustrations` registry the cards use, keyed by the permission name, so `illustrations={{ notifications: <MyBell /> }}` is enough. The kit's dependency-free default is used when you register nothing.
676
+
677
+ ### The numbers
678
+
679
+ Each screen reports through the same `/v1/events` path and the same `device_key` join as the rest of the funnel, and every moment is also surfaced on `onEvent` as `{ type: "permission", ... }`:
680
+
681
+ | Event | Fires when |
682
+ |---|---|
683
+ | `wire_permission_screen_shown` | The primer became visible. The denominator. |
684
+ | `wire_permission_primer_accepted` | The primary was tapped, so the OS dialog is about to open. |
685
+ | `wire_permission_granted` / `wire_permission_denied` | What the OS answered (a permanently blocked answer reports as denied with `status: "blocked"`). |
686
+ | `wire_permission_skipped` | "Maybe later". The one native prompt was not spent. |
687
+ | `wire_permission_settings_opened` | A blocked user was sent to Settings. |
688
+
689
+ The gap between `wire_permission_screen_shown` and `wire_permission_primer_accepted` is the number worth watching: it tells you whether your rationale copy works, and it costs nothing to be wrong about, because a user who does not tap has not burned anything.
690
+
691
+ **Scheduling notifications is deliberately out of scope.** `onResult` is the seam: schedule your first local reminder there, with your own `expo-notifications` call, the moment a grant lands.
692
+
611
693
  ## Backend coupling
612
694
 
613
695
  The backend (`wire-rn/examples/dynamic-onboarding/server`) is the source of truth
@@ -1,6 +1,6 @@
1
1
  export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-CF_eHwzC.mjs';
2
- import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-_GynvhzT.mjs';
3
- export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, C as ClearUserContextOptions, b as ClientEvent, c as ClientEventTarget, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-_GynvhzT.mjs';
2
+ import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-ClkLjcJ0.mjs';
3
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, C as ClearUserContextOptions, b as ClientEvent, c as ClientEventTarget, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-ClkLjcJ0.mjs';
4
4
  import '../types-CNUqMK0D.mjs';
5
5
  import '../types-BKfpdZzX.mjs';
6
6
  import '../types-BcmagF6K.mjs';
@@ -1,6 +1,6 @@
1
1
  export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-DsRe4epC.js';
2
- import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-D7zabMXK.js';
3
- export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, C as ClearUserContextOptions, b as ClientEvent, c as ClientEventTarget, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-D7zabMXK.js';
2
+ import { W as WireUserContext, E as EventQueueOptions } from '../currentSession-DOVZEWJl.js';
3
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, C as ClearUserContextOptions, b as ClientEvent, c as ClientEventTarget, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-DOVZEWJl.js';
4
4
  import '../types-Buj9Lw9t.js';
5
5
  import '../types-BKfpdZzX.js';
6
6
  import '../types-BcmagF6K.js';
@@ -402,13 +402,24 @@ var init_Constants = __esm({
402
402
 
403
403
  // src/analytics/reportClientEvent.ts
404
404
  var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
405
+ var toWireEvents = (events) => events.map((event) => {
406
+ if (typeof event.ts !== "number") return event;
407
+ const { ts, ...rest } = event;
408
+ if (!Number.isFinite(ts)) return rest;
409
+ try {
410
+ return { ...rest, ts: new Date(ts).toISOString() };
411
+ } catch {
412
+ return rest;
413
+ }
414
+ });
405
415
  var buildEventsRequest = (target, events) => {
406
416
  if (!(target == null ? void 0 : target.serverUrl) || events.length === 0) return null;
407
417
  try {
408
418
  const url = `${target.serverUrl.replace(/\/$/, "")}/v1/events`;
409
419
  const headers = { "Content-Type": "application/json" };
410
420
  if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
411
- return { url, init: { method: "POST", headers, body: JSON.stringify({ events }) } };
421
+ const body = JSON.stringify({ events: toWireEvents(events) });
422
+ return { url, init: { method: "POST", headers, body } };
412
423
  } catch {
413
424
  return null;
414
425
  }
@@ -420,7 +431,13 @@ var readEventsAck = async (res) => {
420
431
  const body = await Promise.resolve(json.call(res));
421
432
  const skipped = body == null ? void 0 : body.skipped;
422
433
  if (typeof skipped !== "number" || !Number.isFinite(skipped)) return void 0;
423
- const reasons = Array.isArray(body == null ? void 0 : body.errors) ? body.errors.map((e) => e == null ? void 0 : e.reason).filter((r) => typeof r === "string") : [];
434
+ const reasons = Array.isArray(body == null ? void 0 : body.errors) ? body.errors.map((e) => {
435
+ const entry = e;
436
+ const reason = entry == null ? void 0 : entry.reason;
437
+ if (typeof reason !== "string") return void 0;
438
+ const field = entry == null ? void 0 : entry.field;
439
+ return typeof field === "string" && field.length > 0 ? `${reason} (field: ${field})` : reason;
440
+ }).filter((r) => typeof r === "string") : [];
424
441
  return { written: typeof (body == null ? void 0 : body.written) === "number" ? body.written : void 0, skipped, reasons };
425
442
  } catch {
426
443
  return void 0;
@@ -598,6 +615,43 @@ var useScreenTracking = (navigationRef, options = {}) => {
598
615
  }, [navigationRef]);
599
616
  };
600
617
 
618
+ // src/permissions/permissionEvents.ts
619
+ var WIRE_PERMISSION_EVENTS = {
620
+ /** The priming screen became visible. The denominator for every rate below. */
621
+ screenShown: "wire_permission_screen_shown",
622
+ /** The user tapped the primary, so the OS dialog is about to open. The rationale worked. */
623
+ primerAccepted: "wire_permission_primer_accepted",
624
+ /** The OS granted it. */
625
+ granted: "wire_permission_granted",
626
+ /** The OS refused it (a permanently blocked answer reports here too, with `status: "blocked"`). */
627
+ denied: "wire_permission_denied",
628
+ /** The user took the secondary. The one native prompt was NOT spent. */
629
+ skipped: "wire_permission_skipped",
630
+ /** A blocked user was redirected to the OS settings page. */
631
+ settingsOpened: "wire_permission_settings_opened"
632
+ };
633
+ var permissionEventName = (stage) => {
634
+ switch (stage) {
635
+ case "shown":
636
+ return WIRE_PERMISSION_EVENTS.screenShown;
637
+ case "accepted":
638
+ return WIRE_PERMISSION_EVENTS.primerAccepted;
639
+ case "granted":
640
+ return WIRE_PERMISSION_EVENTS.granted;
641
+ case "denied":
642
+ return WIRE_PERMISSION_EVENTS.denied;
643
+ case "skipped":
644
+ return WIRE_PERMISSION_EVENTS.skipped;
645
+ case "settings":
646
+ return WIRE_PERMISSION_EVENTS.settingsOpened;
647
+ default: {
648
+ const _exhaustive = stage;
649
+ return _exhaustive;
650
+ }
651
+ }
652
+ };
653
+ var permissionEventProps = (permission, status) => status ? { permission, status } : { permission };
654
+
601
655
  // src/analytics/analyticsEvent.ts
602
656
  var WIRE_ONBOARDING_EVENTS = {
603
657
  started: "wire_onboarding_started",
@@ -630,6 +684,11 @@ var toAnalyticsEvent = (event) => {
630
684
  };
631
685
  case "fallback":
632
686
  return { name: WIRE_ONBOARDING_EVENTS.fallback, params: { reason: event.reason } };
687
+ case "permission":
688
+ return {
689
+ name: permissionEventName(event.stage),
690
+ params: permissionEventProps(event.permission, event.status)
691
+ };
633
692
  default: {
634
693
  const _exhaustive = event;
635
694
  return _exhaustive;