@wireai/activation 0.3.0 → 0.7.0

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 (75) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +57 -0
  3. package/dist/analytics/index.d.mts +15 -2
  4. package/dist/analytics/index.d.ts +15 -2
  5. package/dist/analytics/index.js +224 -14
  6. package/dist/analytics/index.js.map +1 -1
  7. package/dist/analytics/index.mjs +222 -15
  8. package/dist/analytics/index.mjs.map +1 -1
  9. package/dist/coachmarks/index.js +6 -1
  10. package/dist/coachmarks/index.js.map +1 -1
  11. package/dist/coachmarks/index.mjs +6 -1
  12. package/dist/coachmarks/index.mjs.map +1 -1
  13. package/dist/{eventQueue-CA1d8Fmn.d.mts → currentSession-BJBB7i4-.d.mts} +143 -15
  14. package/dist/{eventQueue-CrNB9gzH.d.ts → currentSession-CxnP7gAa.d.ts} +143 -15
  15. package/dist/index.d.mts +40 -39
  16. package/dist/index.d.ts +40 -39
  17. package/dist/index.js +378 -154
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +370 -156
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/questionnaire/index.d.mts +1 -1
  22. package/dist/questionnaire/index.d.ts +1 -1
  23. package/dist/questionnaire/index.js +29 -5
  24. package/dist/questionnaire/index.js.map +1 -1
  25. package/dist/questionnaire/index.mjs +30 -6
  26. package/dist/questionnaire/index.mjs.map +1 -1
  27. package/dist/reviews/index.d.mts +1 -1
  28. package/dist/reviews/index.d.ts +1 -1
  29. package/dist/reviews/index.js +40 -9
  30. package/dist/reviews/index.js.map +1 -1
  31. package/dist/reviews/index.mjs +42 -11
  32. package/dist/reviews/index.mjs.map +1 -1
  33. package/dist/showcase/index.d.mts +1 -14
  34. package/dist/showcase/index.d.ts +1 -14
  35. package/dist/showcase/index.js +8 -2
  36. package/dist/showcase/index.js.map +1 -1
  37. package/dist/showcase/index.mjs +8 -2
  38. package/dist/showcase/index.mjs.map +1 -1
  39. package/package.json +3 -2
  40. package/src/OnboardingFlow.tsx +8 -4
  41. package/src/WireOnboarding.tsx +12 -6
  42. package/src/analytics/analyticsFacade.ts +86 -10
  43. package/src/analytics/contextEnvelope.ts +13 -7
  44. package/src/analytics/currentSession.ts +35 -0
  45. package/src/analytics/eventQueue.ts +5 -0
  46. package/src/analytics/index.ts +3 -0
  47. package/src/analytics/useAnalytics.ts +7 -1
  48. package/src/analytics/useScreenTracking.ts +23 -1
  49. package/src/cards/InterstitialCard.tsx +1 -1
  50. package/src/cards/NumberStepperCard.tsx +12 -7
  51. package/src/cards/StatusCard.tsx +13 -8
  52. package/src/cards/TextInputCard.tsx +10 -5
  53. package/src/components/AnimatedSparkle.tsx +20 -3
  54. package/src/components/Button.tsx +10 -1
  55. package/src/components/CardHandoff.tsx +2 -6
  56. package/src/components/CardLayout.tsx +16 -10
  57. package/src/components/Illustration.tsx +9 -5
  58. package/src/components/LoadingBlock.tsx +44 -29
  59. package/src/components/LoadingScreen.tsx +3 -2
  60. package/src/components/OnboardingScaffold.tsx +21 -23
  61. package/src/context/userContext.ts +210 -0
  62. package/src/device/appVersion.ts +103 -0
  63. package/src/device/deviceContext.ts +17 -5
  64. package/src/features/WireFeaturesProvider.tsx +7 -2
  65. package/src/identity/userIdentity.ts +5 -0
  66. package/src/index.ts +23 -0
  67. package/src/questionnaire/QuestionnaireGate.tsx +15 -3
  68. package/src/reviews/ReviewGate.tsx +27 -7
  69. package/src/reviews/ReviewModal.tsx +4 -0
  70. package/src/session-analytics/reportSessionStart.ts +7 -0
  71. package/src/session-analytics/useLifecycleEvents.ts +4 -2
  72. package/src/session-analytics/useSessionStart.ts +2 -1
  73. package/src/showcase/FeatureShowcase.tsx +2 -1
  74. package/src/types.ts +6 -5
  75. package/src/components/loaderChrome.ts +0 -28
@@ -11,9 +11,12 @@ import { Platform } from 'react-native';
11
11
  * adding a single dependency to the kit or changing a host app's App Privacy / Data Safety
12
12
  * declarations.
13
13
  *
14
- * HARD RULE (why this file has no imports beyond React Native built-ins):
14
+ * HARD RULE (why this file adds no dependency):
15
15
  * The kit stays dependency-free. Everything here comes from `Platform`, `Dimensions`,
16
- * `I18nManager`, and the standard `Intl` global. There are NO advertising IDs, NO
16
+ * `I18nManager`, and the standard `Intl` global plus a best-effort `appVersion` read via
17
+ * `detectAppVersion()`, which itself adds NO dependency (it reaches for `expo-constants` /
18
+ * `expo-application` through a guarded, variable-specifier require that a host without them
19
+ * simply never resolves — see device/appVersion.ts). There are NO advertising IDs, NO
17
20
  * `getUniqueId`/IDFA/GAID/fingerprinting APIs, and nothing that would require a new
18
21
  * privacy-label entry. A host can adopt this without touching its store declarations.
19
22
  *
@@ -55,9 +58,11 @@ type DeviceContext = {
55
58
  /** IANA time zone (e.g. "Europe/Berlin"), from `Intl` when available. */
56
59
  timeZone?: string;
57
60
  /**
58
- * Host app version (e.g. "1.4.2). HOST-INJECTED NOT collected here. `WireOnboarding`
59
- * merges `config.appVersion` into the snapshot; `collectDeviceContext()` never sets it.
60
- * Hosts typically pass it from `expo-constants` (the kit itself adds no dependency).
61
+ * Host app version (e.g. "1.4.2"). BEST-EFFORT auto-detected here via `detectAppVersion()`
62
+ * (reads `expo-constants` / `expo-application` when present; adds no dependency — see
63
+ * device/appVersion.ts). An explicit host-injected `config.appVersion` always WINS: the merge
64
+ * sites (`WireOnboarding`, the session-analytics hooks, the context envelope) overwrite this
65
+ * with the host value when one is supplied. Omitted when neither source yields a version.
61
66
  */
62
67
  appVersion?: string;
63
68
  };
@@ -179,11 +184,12 @@ type WireOnboardingConfig = {
179
184
  */
180
185
  metadata?: Record<string, unknown>;
181
186
  /**
182
- * Host app version string (e.g. "1.4.2"). HOST-INJECTED — the kit adds no dependency to
183
- * read it; hosts typically pass it from `expo-constants`
184
- * (`Constants.expoConfig?.version`). Forwarded to the backend on the session metadata and
185
- * on client events (merged into the `device` snapshot as `device.appVersion`) so analytics
186
- * can segment the funnel by app version. Optional; omit if unknown.
187
+ * Host app version string (e.g. "1.4.2"). OPTIONALwhen omitted, the kit makes a best-effort
188
+ * auto-detection from `expo-constants` (`Constants.expoConfig?.version` / `nativeAppVersion`) or
189
+ * `expo-application` (`nativeApplicationVersion`) WITHOUT adding a dependency (a host that lacks
190
+ * those modules just gets no version see device/appVersion.ts). Pass this to override the
191
+ * auto-detected value (it always wins). Forwarded to the backend on the session metadata and on
192
+ * client events (as `device.appVersion`) so analytics can segment the funnel by app version.
187
193
  */
188
194
  appVersion?: string;
189
195
  };
@@ -451,7 +457,7 @@ type ContextEnvelope = {
451
457
  device: DeviceContext;
452
458
  /** Correlation id for this app-open / flow (caller-supplied). */
453
459
  sessionId?: string;
454
- /** Host app version, e.g. "1.4.2" (mirrors `device.appVersion`; host-injected). */
460
+ /** App version, e.g. "1.4.2" (mirrors `device.appVersion`; host-injected, else auto-detected). */
455
461
  appVersion?: string;
456
462
  /** Host native build number, e.g. "412" (from `expo-constants` `nativeBuildVersion`). */
457
463
  appBuild?: string;
@@ -466,9 +472,10 @@ type ContextEnvelopeInput = {
466
472
  networkType?: string;
467
473
  };
468
474
  /**
469
- * Build a fresh context envelope. Reuses `collectDeviceContext()` for the device block and layers
470
- * the host-injected scalars on top. `appVersion` is additionally merged onto `device.appVersion`
471
- * when the device block lacks it (mirroring how `useSessionStart` back-fills the host version).
475
+ * Build a fresh context envelope. Reuses `collectDeviceContext()` for the device block (which
476
+ * already carries a best-effort auto-detected `appVersion`) and layers the host-injected scalars
477
+ * on top. An explicit `input.appVersion` overrides the auto-detected `device.appVersion`, and the
478
+ * outer `appVersion` scalar mirrors whichever version is effective.
472
479
  *
473
480
  * Returns a NEW object on every call (no shared mutable reference), so a caller can hold or mutate
474
481
  * the result without leaking into the next envelope. Never throws — `collectDeviceContext` is
@@ -547,4 +554,125 @@ type EventQueue = {
547
554
  */
548
555
  declare const createEventQueue: (options: EventQueueOptions) => EventQueue;
549
556
 
550
- export { type AnalyticsEvent as A, type ClientEvent as C, type DeviceContext as D, type EventQueueOptions as E, type OnboardingResult as O, type StepValidator as S, WIRE_ONBOARDING_EVENTS as W, type ClientEventTarget as a, type ClientEventType as b, type ContextEnvelope as c, type ContextEnvelopeInput as d, type EnvelopeSource as e, type EventQueue as f, type WireOnboardingEventName as g, buildContextEnvelope as h, createEventQueue as i, reportClientEvents as j, type WireOnboardingProps as k, type WireOnboardingConfig as l, makeSessionId as m, type OnboardingEvent as n, type OnboardingCopy as o, type DeviceFormFactor as p, type OnboardingProgress as q, reportClientEvent as r, collectDeviceContext as s, toAnalyticsEvent as t };
557
+ /**
558
+ * The single, extensible user-context object. A host passes it ONCE (at analytics init) and may
559
+ * update it post-mount (e.g. attach `userId`/`userEmail` at login) via `setUserContext(partial)`.
560
+ * Every field is optional; missing fields are omitted from the wire payload.
561
+ */
562
+ interface WireUserContext {
563
+ /**
564
+ * Host app version, e.g. "1.4.2". EXPLICIT — wins over the #42 auto-detected `device.appVersion`.
565
+ * Lands in `user_context.app_version`. Omitted when neither this nor auto-detect yields a version.
566
+ */
567
+ appVersion?: string;
568
+ /**
569
+ * A stable, non-PII device id the host owns. Lands in `user_context.device_key` (NOT `session_id`),
570
+ * where the server groups a device's sessions. Host-supplied; the kit never mints or reads one.
571
+ */
572
+ deviceKey?: string;
573
+ /**
574
+ * The host's OPAQUE PSEUDONYMOUS user id (their internal id — NOT an email/name/phone). Sanitized +
575
+ * capped (see `sanitizeUserId`) and placed on the event's top-level `user_id`. NEVER the bucket.
576
+ */
577
+ userId?: string;
578
+ /**
579
+ * OPT-IN PII. The user's email, its OWN field (`user_context.user_email`) — NEVER merged into
580
+ * `userId`. The kit NEVER auto-collects this; a host passes it only WITH the user's consent (EU
581
+ * users: treat as personal data). For a non-reversible form, set {@link hashEmail} `true` (the kit
582
+ * folds it with a dependency-free hash and stamps `user_context.user_email_hashed: true`), OR
583
+ * pre-hash host-side with a cryptographic digest and pass that here with `hashEmail` falsy.
584
+ */
585
+ userEmail?: string;
586
+ /**
587
+ * When `true`, {@link userEmail} is folded with the kit's dependency-free {@link hashEmailFnv1a}
588
+ * before it leaves the device, and `user_context.user_email_hashed` is set `true`. NOTE: FNV-1a is
589
+ * a lightweight NON-cryptographic fold (obfuscation, not a secure digest). For a cryptographic
590
+ * hash, compute it host-side (e.g. SHA-256 via `expo-crypto`) and pass the digest as `userEmail`
591
+ * with `hashEmail` falsy. Default: raw email is sent as-is (opt-in already gated it upstream).
592
+ */
593
+ hashEmail?: boolean;
594
+ /**
595
+ * Arbitrary host context (signup method, referral, plan tier…). Each value is coerced to a scalar
596
+ * (`string | number | boolean`; non-scalars and non-finite numbers are DROPPED) and NAMESPACED
597
+ * under a `custom.` key prefix in `user_context` (e.g. `user_context["custom.referral"]`) so it can
598
+ * never collide with a reserved key. No raw PII — use {@link userEmail} for email.
599
+ */
600
+ extra?: Record<string, string | number | boolean>;
601
+ }
602
+ /**
603
+ * The wire-shaped result of {@link resolveUserContext}. `userContext` is the non-PII/opt-in-PII
604
+ * bucket stamped onto the event; `userId` is the top-level opaque id; `appVersion`/`deviceKey` are
605
+ * echoed for callers that also place them elsewhere (e.g. `device.appVersion`). Absent fields are
606
+ * omitted so a caller can spread this without sending empties.
607
+ */
608
+ interface ResolvedUserContext {
609
+ /** The opaque, sanitized user id → the event's top-level `user_id`. Omitted when unset/blank. */
610
+ userId?: string;
611
+ /** The stable device id → `user_context.device_key`. Omitted when unset. */
612
+ deviceKey?: string;
613
+ /** The effective app version (explicit > auto-detected) → `user_context.app_version`. */
614
+ appVersion?: string;
615
+ /** The `user_context` bucket (device_key, app_version, user_email[+ _hashed], custom.*). */
616
+ userContext?: Record<string, string | number | boolean>;
617
+ }
618
+ /** Reserved `user_context` keys the kit itself writes; host `extra` is namespaced away from these. */
619
+ declare const RESERVED_USER_CONTEXT_KEYS: readonly ["device_key", "app_version", "app_build", "network_type", "session_count", "returning", "platform", "user_email", "user_email_hashed"];
620
+ /** The prefix applied to every host `extra` key so it can never collide with a reserved key. */
621
+ declare const EXTRA_KEY_PREFIX: "custom.";
622
+ /** A finite scalar the wire accepts. Non-finite numbers (NaN/Infinity) are NOT scalars here. */
623
+ declare const isWireScalar: (value: unknown) => value is string | number | boolean;
624
+ /**
625
+ * Fold an email to a stable, dependency-free 32-bit FNV-1a hex token (lowercased + trimmed first so
626
+ * the same address always folds identically). This is OBFUSCATION, not a cryptographic digest — it
627
+ * is not collision-resistant. For a real hash, pre-hash host-side and pass the digest as `userEmail`.
628
+ */
629
+ declare const hashEmailFnv1a: (email: string) => string;
630
+ /**
631
+ * Coerce a host `extra` map into the namespaced, scalar-only bucket shape. Every kept value is
632
+ * placed under `custom.<key>`; non-scalar values (objects, arrays, null, functions, NaN/Infinity)
633
+ * are DROPPED. Returns an object (possibly empty).
634
+ */
635
+ declare const namespaceExtra: (extra: Record<string, unknown> | undefined) => Record<string, string | number | boolean>;
636
+ /** Options for {@link resolveUserContext}. */
637
+ interface ResolveUserContextOptions {
638
+ /**
639
+ * The kit's best-effort auto-detected app version (#42; from `detectAppVersion()`/the device
640
+ * snapshot). Used ONLY when the explicit `WireUserContext.appVersion` is absent — explicit wins.
641
+ */
642
+ autoAppVersion?: string;
643
+ }
644
+ /**
645
+ * Merge a {@link WireUserContext} into the wire shape with the precedence rule (explicit field >
646
+ * auto-detected). Pure, never throws. Missing fields are omitted so the result can be spread onto an
647
+ * event without sending empties.
648
+ */
649
+ declare const resolveUserContext: (ctx?: WireUserContext, opts?: ResolveUserContextOptions) => ResolvedUserContext;
650
+
651
+ /**
652
+ * currentSession — a tiny module-level registry of the CURRENT per-open `session_id`.
653
+ *
654
+ * WHY it exists (kills the phantom-session): the per-open emitters (`reportSessionStart` and the
655
+ * `useSessionStart` / `useLifecycleEvents` hooks) mint a fresh `session_id` for each app-open and
656
+ * post `app.session_started` with it — so the SERVER knows that id. But other client paths
657
+ * (`identify`, host `app_event`s through the analytics façade) used to reference a DIFFERENT id
658
+ * (a frozen per-instance id), which the server had never seen, so it back-filled a synthetic
659
+ * `session_started` — inflating session counts (the Morrow/Myelino "phantom-session" bug).
660
+ *
661
+ * This registry is the single seam that lets those paths reuse the LIVE per-open session id the
662
+ * server already ingested. `reportSessionStart` writes the current id here on every open; the façade
663
+ * reads it so `identify`/app-events correlate to the real session instead of minting a phantom.
664
+ *
665
+ * DEPENDENCY-FREE + PROCESS-LOCAL: a plain module variable. It is intentionally NOT persisted — it
666
+ * tracks the CURRENT process's open, and a fresh open always overwrites it. No cross-launch state.
667
+ */
668
+ /**
669
+ * Record the current per-open `session_id`. Called by `reportSessionStart` when it emits an
670
+ * app-open. A blank / non-string id is ignored (the previous id stays current). Idempotent.
671
+ */
672
+ declare const setCurrentSessionId: (id: string | undefined) => void;
673
+ /** The current per-open `session_id`, or `undefined` when no app-open has been registered yet. */
674
+ declare const getCurrentSessionId: () => string | undefined;
675
+ /** Test-only: forget the current session id so a unit test starts from a clean registry. */
676
+ declare const resetCurrentSessionId: () => void;
677
+
678
+ export { type AnalyticsEvent as A, collectDeviceContext as B, type ClientEvent as C, type DeviceContext as D, type EventQueueOptions as E, hashEmailFnv1a as F, isWireScalar as G, namespaceExtra as H, resolveUserContext as I, type OnboardingResult as O, RESERVED_USER_CONTEXT_KEYS as R, type StepValidator as S, type WireUserContext as W, type ClientEventTarget as a, type ClientEventType as b, type ContextEnvelope as c, type ContextEnvelopeInput as d, type EnvelopeSource as e, type EventQueue as f, WIRE_ONBOARDING_EVENTS as g, type WireOnboardingEventName as h, buildContextEnvelope as i, createEventQueue as j, getCurrentSessionId as k, reportClientEvents as l, makeSessionId as m, resetCurrentSessionId as n, type WireOnboardingProps as o, type WireOnboardingConfig as p, type OnboardingEvent as q, reportClientEvent as r, setCurrentSessionId as s, toAnalyticsEvent as t, type OnboardingCopy as u, type DeviceFormFactor as v, EXTRA_KEY_PREFIX as w, type OnboardingProgress as x, type ResolveUserContextOptions as y, type ResolvedUserContext as z };
@@ -11,9 +11,12 @@ import { Platform } from 'react-native';
11
11
  * adding a single dependency to the kit or changing a host app's App Privacy / Data Safety
12
12
  * declarations.
13
13
  *
14
- * HARD RULE (why this file has no imports beyond React Native built-ins):
14
+ * HARD RULE (why this file adds no dependency):
15
15
  * The kit stays dependency-free. Everything here comes from `Platform`, `Dimensions`,
16
- * `I18nManager`, and the standard `Intl` global. There are NO advertising IDs, NO
16
+ * `I18nManager`, and the standard `Intl` global plus a best-effort `appVersion` read via
17
+ * `detectAppVersion()`, which itself adds NO dependency (it reaches for `expo-constants` /
18
+ * `expo-application` through a guarded, variable-specifier require that a host without them
19
+ * simply never resolves — see device/appVersion.ts). There are NO advertising IDs, NO
17
20
  * `getUniqueId`/IDFA/GAID/fingerprinting APIs, and nothing that would require a new
18
21
  * privacy-label entry. A host can adopt this without touching its store declarations.
19
22
  *
@@ -55,9 +58,11 @@ type DeviceContext = {
55
58
  /** IANA time zone (e.g. "Europe/Berlin"), from `Intl` when available. */
56
59
  timeZone?: string;
57
60
  /**
58
- * Host app version (e.g. "1.4.2). HOST-INJECTED NOT collected here. `WireOnboarding`
59
- * merges `config.appVersion` into the snapshot; `collectDeviceContext()` never sets it.
60
- * Hosts typically pass it from `expo-constants` (the kit itself adds no dependency).
61
+ * Host app version (e.g. "1.4.2"). BEST-EFFORT auto-detected here via `detectAppVersion()`
62
+ * (reads `expo-constants` / `expo-application` when present; adds no dependency — see
63
+ * device/appVersion.ts). An explicit host-injected `config.appVersion` always WINS: the merge
64
+ * sites (`WireOnboarding`, the session-analytics hooks, the context envelope) overwrite this
65
+ * with the host value when one is supplied. Omitted when neither source yields a version.
61
66
  */
62
67
  appVersion?: string;
63
68
  };
@@ -179,11 +184,12 @@ type WireOnboardingConfig = {
179
184
  */
180
185
  metadata?: Record<string, unknown>;
181
186
  /**
182
- * Host app version string (e.g. "1.4.2"). HOST-INJECTED — the kit adds no dependency to
183
- * read it; hosts typically pass it from `expo-constants`
184
- * (`Constants.expoConfig?.version`). Forwarded to the backend on the session metadata and
185
- * on client events (merged into the `device` snapshot as `device.appVersion`) so analytics
186
- * can segment the funnel by app version. Optional; omit if unknown.
187
+ * Host app version string (e.g. "1.4.2"). OPTIONALwhen omitted, the kit makes a best-effort
188
+ * auto-detection from `expo-constants` (`Constants.expoConfig?.version` / `nativeAppVersion`) or
189
+ * `expo-application` (`nativeApplicationVersion`) WITHOUT adding a dependency (a host that lacks
190
+ * those modules just gets no version see device/appVersion.ts). Pass this to override the
191
+ * auto-detected value (it always wins). Forwarded to the backend on the session metadata and on
192
+ * client events (as `device.appVersion`) so analytics can segment the funnel by app version.
187
193
  */
188
194
  appVersion?: string;
189
195
  };
@@ -451,7 +457,7 @@ type ContextEnvelope = {
451
457
  device: DeviceContext;
452
458
  /** Correlation id for this app-open / flow (caller-supplied). */
453
459
  sessionId?: string;
454
- /** Host app version, e.g. "1.4.2" (mirrors `device.appVersion`; host-injected). */
460
+ /** App version, e.g. "1.4.2" (mirrors `device.appVersion`; host-injected, else auto-detected). */
455
461
  appVersion?: string;
456
462
  /** Host native build number, e.g. "412" (from `expo-constants` `nativeBuildVersion`). */
457
463
  appBuild?: string;
@@ -466,9 +472,10 @@ type ContextEnvelopeInput = {
466
472
  networkType?: string;
467
473
  };
468
474
  /**
469
- * Build a fresh context envelope. Reuses `collectDeviceContext()` for the device block and layers
470
- * the host-injected scalars on top. `appVersion` is additionally merged onto `device.appVersion`
471
- * when the device block lacks it (mirroring how `useSessionStart` back-fills the host version).
475
+ * Build a fresh context envelope. Reuses `collectDeviceContext()` for the device block (which
476
+ * already carries a best-effort auto-detected `appVersion`) and layers the host-injected scalars
477
+ * on top. An explicit `input.appVersion` overrides the auto-detected `device.appVersion`, and the
478
+ * outer `appVersion` scalar mirrors whichever version is effective.
472
479
  *
473
480
  * Returns a NEW object on every call (no shared mutable reference), so a caller can hold or mutate
474
481
  * the result without leaking into the next envelope. Never throws — `collectDeviceContext` is
@@ -547,4 +554,125 @@ type EventQueue = {
547
554
  */
548
555
  declare const createEventQueue: (options: EventQueueOptions) => EventQueue;
549
556
 
550
- export { type AnalyticsEvent as A, type ClientEvent as C, type DeviceContext as D, type EventQueueOptions as E, type OnboardingResult as O, type StepValidator as S, WIRE_ONBOARDING_EVENTS as W, type ClientEventTarget as a, type ClientEventType as b, type ContextEnvelope as c, type ContextEnvelopeInput as d, type EnvelopeSource as e, type EventQueue as f, type WireOnboardingEventName as g, buildContextEnvelope as h, createEventQueue as i, reportClientEvents as j, type WireOnboardingProps as k, type WireOnboardingConfig as l, makeSessionId as m, type OnboardingEvent as n, type OnboardingCopy as o, type DeviceFormFactor as p, type OnboardingProgress as q, reportClientEvent as r, collectDeviceContext as s, toAnalyticsEvent as t };
557
+ /**
558
+ * The single, extensible user-context object. A host passes it ONCE (at analytics init) and may
559
+ * update it post-mount (e.g. attach `userId`/`userEmail` at login) via `setUserContext(partial)`.
560
+ * Every field is optional; missing fields are omitted from the wire payload.
561
+ */
562
+ interface WireUserContext {
563
+ /**
564
+ * Host app version, e.g. "1.4.2". EXPLICIT — wins over the #42 auto-detected `device.appVersion`.
565
+ * Lands in `user_context.app_version`. Omitted when neither this nor auto-detect yields a version.
566
+ */
567
+ appVersion?: string;
568
+ /**
569
+ * A stable, non-PII device id the host owns. Lands in `user_context.device_key` (NOT `session_id`),
570
+ * where the server groups a device's sessions. Host-supplied; the kit never mints or reads one.
571
+ */
572
+ deviceKey?: string;
573
+ /**
574
+ * The host's OPAQUE PSEUDONYMOUS user id (their internal id — NOT an email/name/phone). Sanitized +
575
+ * capped (see `sanitizeUserId`) and placed on the event's top-level `user_id`. NEVER the bucket.
576
+ */
577
+ userId?: string;
578
+ /**
579
+ * OPT-IN PII. The user's email, its OWN field (`user_context.user_email`) — NEVER merged into
580
+ * `userId`. The kit NEVER auto-collects this; a host passes it only WITH the user's consent (EU
581
+ * users: treat as personal data). For a non-reversible form, set {@link hashEmail} `true` (the kit
582
+ * folds it with a dependency-free hash and stamps `user_context.user_email_hashed: true`), OR
583
+ * pre-hash host-side with a cryptographic digest and pass that here with `hashEmail` falsy.
584
+ */
585
+ userEmail?: string;
586
+ /**
587
+ * When `true`, {@link userEmail} is folded with the kit's dependency-free {@link hashEmailFnv1a}
588
+ * before it leaves the device, and `user_context.user_email_hashed` is set `true`. NOTE: FNV-1a is
589
+ * a lightweight NON-cryptographic fold (obfuscation, not a secure digest). For a cryptographic
590
+ * hash, compute it host-side (e.g. SHA-256 via `expo-crypto`) and pass the digest as `userEmail`
591
+ * with `hashEmail` falsy. Default: raw email is sent as-is (opt-in already gated it upstream).
592
+ */
593
+ hashEmail?: boolean;
594
+ /**
595
+ * Arbitrary host context (signup method, referral, plan tier…). Each value is coerced to a scalar
596
+ * (`string | number | boolean`; non-scalars and non-finite numbers are DROPPED) and NAMESPACED
597
+ * under a `custom.` key prefix in `user_context` (e.g. `user_context["custom.referral"]`) so it can
598
+ * never collide with a reserved key. No raw PII — use {@link userEmail} for email.
599
+ */
600
+ extra?: Record<string, string | number | boolean>;
601
+ }
602
+ /**
603
+ * The wire-shaped result of {@link resolveUserContext}. `userContext` is the non-PII/opt-in-PII
604
+ * bucket stamped onto the event; `userId` is the top-level opaque id; `appVersion`/`deviceKey` are
605
+ * echoed for callers that also place them elsewhere (e.g. `device.appVersion`). Absent fields are
606
+ * omitted so a caller can spread this without sending empties.
607
+ */
608
+ interface ResolvedUserContext {
609
+ /** The opaque, sanitized user id → the event's top-level `user_id`. Omitted when unset/blank. */
610
+ userId?: string;
611
+ /** The stable device id → `user_context.device_key`. Omitted when unset. */
612
+ deviceKey?: string;
613
+ /** The effective app version (explicit > auto-detected) → `user_context.app_version`. */
614
+ appVersion?: string;
615
+ /** The `user_context` bucket (device_key, app_version, user_email[+ _hashed], custom.*). */
616
+ userContext?: Record<string, string | number | boolean>;
617
+ }
618
+ /** Reserved `user_context` keys the kit itself writes; host `extra` is namespaced away from these. */
619
+ declare const RESERVED_USER_CONTEXT_KEYS: readonly ["device_key", "app_version", "app_build", "network_type", "session_count", "returning", "platform", "user_email", "user_email_hashed"];
620
+ /** The prefix applied to every host `extra` key so it can never collide with a reserved key. */
621
+ declare const EXTRA_KEY_PREFIX: "custom.";
622
+ /** A finite scalar the wire accepts. Non-finite numbers (NaN/Infinity) are NOT scalars here. */
623
+ declare const isWireScalar: (value: unknown) => value is string | number | boolean;
624
+ /**
625
+ * Fold an email to a stable, dependency-free 32-bit FNV-1a hex token (lowercased + trimmed first so
626
+ * the same address always folds identically). This is OBFUSCATION, not a cryptographic digest — it
627
+ * is not collision-resistant. For a real hash, pre-hash host-side and pass the digest as `userEmail`.
628
+ */
629
+ declare const hashEmailFnv1a: (email: string) => string;
630
+ /**
631
+ * Coerce a host `extra` map into the namespaced, scalar-only bucket shape. Every kept value is
632
+ * placed under `custom.<key>`; non-scalar values (objects, arrays, null, functions, NaN/Infinity)
633
+ * are DROPPED. Returns an object (possibly empty).
634
+ */
635
+ declare const namespaceExtra: (extra: Record<string, unknown> | undefined) => Record<string, string | number | boolean>;
636
+ /** Options for {@link resolveUserContext}. */
637
+ interface ResolveUserContextOptions {
638
+ /**
639
+ * The kit's best-effort auto-detected app version (#42; from `detectAppVersion()`/the device
640
+ * snapshot). Used ONLY when the explicit `WireUserContext.appVersion` is absent — explicit wins.
641
+ */
642
+ autoAppVersion?: string;
643
+ }
644
+ /**
645
+ * Merge a {@link WireUserContext} into the wire shape with the precedence rule (explicit field >
646
+ * auto-detected). Pure, never throws. Missing fields are omitted so the result can be spread onto an
647
+ * event without sending empties.
648
+ */
649
+ declare const resolveUserContext: (ctx?: WireUserContext, opts?: ResolveUserContextOptions) => ResolvedUserContext;
650
+
651
+ /**
652
+ * currentSession — a tiny module-level registry of the CURRENT per-open `session_id`.
653
+ *
654
+ * WHY it exists (kills the phantom-session): the per-open emitters (`reportSessionStart` and the
655
+ * `useSessionStart` / `useLifecycleEvents` hooks) mint a fresh `session_id` for each app-open and
656
+ * post `app.session_started` with it — so the SERVER knows that id. But other client paths
657
+ * (`identify`, host `app_event`s through the analytics façade) used to reference a DIFFERENT id
658
+ * (a frozen per-instance id), which the server had never seen, so it back-filled a synthetic
659
+ * `session_started` — inflating session counts (the Morrow/Myelino "phantom-session" bug).
660
+ *
661
+ * This registry is the single seam that lets those paths reuse the LIVE per-open session id the
662
+ * server already ingested. `reportSessionStart` writes the current id here on every open; the façade
663
+ * reads it so `identify`/app-events correlate to the real session instead of minting a phantom.
664
+ *
665
+ * DEPENDENCY-FREE + PROCESS-LOCAL: a plain module variable. It is intentionally NOT persisted — it
666
+ * tracks the CURRENT process's open, and a fresh open always overwrites it. No cross-launch state.
667
+ */
668
+ /**
669
+ * Record the current per-open `session_id`. Called by `reportSessionStart` when it emits an
670
+ * app-open. A blank / non-string id is ignored (the previous id stays current). Idempotent.
671
+ */
672
+ declare const setCurrentSessionId: (id: string | undefined) => void;
673
+ /** The current per-open `session_id`, or `undefined` when no app-open has been registered yet. */
674
+ declare const getCurrentSessionId: () => string | undefined;
675
+ /** Test-only: forget the current session id so a unit test starts from a clean registry. */
676
+ declare const resetCurrentSessionId: () => void;
677
+
678
+ export { type AnalyticsEvent as A, collectDeviceContext as B, type ClientEvent as C, type DeviceContext as D, type EventQueueOptions as E, hashEmailFnv1a as F, isWireScalar as G, namespaceExtra as H, resolveUserContext as I, type OnboardingResult as O, RESERVED_USER_CONTEXT_KEYS as R, type StepValidator as S, type WireUserContext as W, type ClientEventTarget as a, type ClientEventType as b, type ContextEnvelope as c, type ContextEnvelopeInput as d, type EnvelopeSource as e, type EventQueue as f, WIRE_ONBOARDING_EVENTS as g, type WireOnboardingEventName as h, buildContextEnvelope as i, createEventQueue as j, getCurrentSessionId as k, reportClientEvents as l, makeSessionId as m, resetCurrentSessionId as n, type WireOnboardingProps as o, type WireOnboardingConfig as p, type OnboardingEvent as q, reportClientEvent as r, setCurrentSessionId as s, toAnalyticsEvent as t, type OnboardingCopy as u, type DeviceFormFactor as v, EXTRA_KEY_PREFIX as w, type OnboardingProgress as x, type ResolveUserContextOptions as y, type ResolvedUserContext as z };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
- import { k as WireOnboardingProps, l as WireOnboardingConfig, O as OnboardingResult, S as StepValidator, n as OnboardingEvent, o as OnboardingCopy, a as ClientEventTarget, D as DeviceContext, C as ClientEvent, e as EnvelopeSource } from './eventQueue-CA1d8Fmn.mjs';
4
- export { A as AnalyticsEvent, b as ClientEventType, p as DeviceFormFactor, q as OnboardingProgress, W as WIRE_ONBOARDING_EVENTS, g as WireOnboardingEventName, s as collectDeviceContext, m as makeSessionId, r as reportClientEvent, j as reportClientEvents, t as toAnalyticsEvent } from './eventQueue-CA1d8Fmn.mjs';
3
+ import { o as WireOnboardingProps, p as WireOnboardingConfig, O as OnboardingResult, S as StepValidator, q as OnboardingEvent, u as OnboardingCopy, a as ClientEventTarget, D as DeviceContext, C as ClientEvent, e as EnvelopeSource } from './currentSession-BJBB7i4-.mjs';
4
+ export { A as AnalyticsEvent, b as ClientEventType, v as DeviceFormFactor, w as EXTRA_KEY_PREFIX, x as OnboardingProgress, R as RESERVED_USER_CONTEXT_KEYS, y as ResolveUserContextOptions, z as ResolvedUserContext, g as WIRE_ONBOARDING_EVENTS, h as WireOnboardingEventName, W as WireUserContext, B as collectDeviceContext, k as getCurrentSessionId, F as hashEmailFnv1a, G as isWireScalar, m as makeSessionId, H as namespaceExtra, r as reportClientEvent, l as reportClientEvents, n as resetCurrentSessionId, I as resolveUserContext, s as setCurrentSessionId, t as toAnalyticsEvent } from './currentSession-BJBB7i4-.mjs';
5
5
  import { O as OnboardingTheme } from './types-BKfpdZzX.mjs';
6
6
  export { a as OnboardingButtonStyle, b as OnboardingColors, c as OnboardingFonts, d as OnboardingRadius, e as OnboardingSpacing } from './types-BKfpdZzX.mjs';
7
7
  export { C as CenteredModal, a as CenteredModalHandle, b as CenteredModalProps } from './CenteredModal-C3qQBHsA.mjs';
@@ -186,8 +186,9 @@ declare const LoadingBlock: React__default.NamedExoticComponent<LoadingBlockProp
186
186
  * WHY a dedicated frame: those slots used to return a bare <LoadingBlock/>, whose flex:1 only
187
187
  * fills whatever the host happened to give it — with no safe area and, if the host didn't
188
188
  * bound it, no height at all (top-anchored). This wraps them in the same SafeAreaView the flow
189
- * uses, so the standalone loaders center exactly like the between-turns loader (which centers
190
- * in the full flow area via the scaffold's loaderChrome offset). One consistent centered look.
189
+ * uses, so the standalone loaders center exactly like the between-turns loader (both fill their
190
+ * parent flex:1 region and center in it — here the SafeAreaView, in the flow the scaffold body).
191
+ * One consistent centered look.
191
192
  */
192
193
 
193
194
  declare const LoadingScreen: React__default.NamedExoticComponent<LoadingBlockProps>;
@@ -825,6 +826,40 @@ declare const attributionMetadata: (a: OnboardingAttribution) => {
825
826
  attribution: Record<string, unknown>;
826
827
  };
827
828
 
829
+ /**
830
+ * appVersion — best-effort, DEPENDENCY-FREE auto-detection of the host app's version string.
831
+ *
832
+ * WHY this exists: analytics segments the funnel `by_app_version`, but that breakdown is only
833
+ * populated when a `device.appVersion` rides the event. `config.appVersion` (see types.ts) has
834
+ * always been the way to supply it — but it is easy for a host to forget, and then the release
835
+ * breakdown is silently empty. This module fills that gap: when the host does NOT pass a version,
836
+ * the kit makes a best-effort read of the app version the host already ships in its Expo config,
837
+ * so the breakdown works out of the box. An explicit `config.appVersion` always WINS over this.
838
+ *
839
+ * WHY it adds NO dependency (the kit's hard rule): `expo-constants` / `expo-application` are read
840
+ * through a GUARDED, VARIABLE-specifier `require`. Passing a variable (not a string literal) keeps
841
+ * Metro/esbuild from statically resolving the module, so a host that does NOT have it installed
842
+ * (e.g. bare React Native) never fails to bundle — the require simply throws at runtime and is
843
+ * swallowed. Nothing is added to `package.json`; nothing is forced on the host.
844
+ *
845
+ * PRIVACY: an app version string is not PII and identifies no user or device, so surfacing it
846
+ * changes no App Privacy / Data Safety declaration (same guarantee as the rest of deviceContext).
847
+ *
848
+ * NEVER THROWS: every read is guarded; a missing/odd value yields `undefined`, never an exception.
849
+ * Analytics must never be able to break onboarding.
850
+ */
851
+ /** A `require`-like resolver. Injectable in tests; production uses the guarded runtime require. */
852
+ type OptionalRequire = (moduleName: string) => unknown;
853
+ /**
854
+ * Detect the host app version, preferring `expo-constants` (`expoConfig.version`, then
855
+ * `nativeAppVersion`) and finally `expo-application` (`nativeApplicationVersion`). Returns the
856
+ * first real string, or `undefined` when none of those are available. Pure and never throws.
857
+ *
858
+ * `requireModule` is injectable so tests can exercise the "found" path without the native modules;
859
+ * production defaults to the guarded runtime require above.
860
+ */
861
+ declare const detectAppVersion: (requireModule?: OptionalRequire) => string | undefined;
862
+
828
863
  /** Max accepted user-id length. Longer strings are truncated (never rejected). Keep in sync
829
864
  * with the server's `USER_ID_MAX_LENGTH` (analytics/events.py). */
830
865
  declare const USER_ID_MAX_LENGTH = 128;
@@ -871,40 +906,6 @@ type IdentifyOnboardingOptions = {
871
906
  */
872
907
  declare const identifyOnboarding: (opts: IdentifyOnboardingOptions) => Promise<boolean>;
873
908
 
874
- /**
875
- * reportSessionStart — the ONE standard emitter for "the user opened the app again".
876
- *
877
- * Every app-open posts a single `app.session_started` event to the Wire analytics backend
878
- * (`POST {serverUrl}/v1/events`). That event does two jobs at once, with ZERO server changes:
879
- *
880
- * 1. SESSION MAPPING. It carries the host's opaque `userId` + a stable `deviceKey`, so the
881
- * backend can group a user's (or a pre-auth device's) opens over time — "when did this
882
- * user last use the app, and how many times". The device_key links the pre-auth opens to
883
- * the user once `userId` arrives (here or via `identifyOnboarding`).
884
- * 2. QUESTIONNAIRE / RETENTION FIRING. It flows into the SAME event stream the server's
885
- * decision engines read, so a questionnaire trigger `{event:"app.session_started", min_count:N}`
886
- * (matched within a session) or a questionnaire `min_sessions:N` (distinct device sessions)
887
- * fires on the user's Nth open with no new server endpoint.
888
- *
889
- * WIRE CONTRACT (server: analytics/events.py + routers/onboarding.py `POST /v1/events`):
890
- * The event is stored as `event_type='app_event'`, `question_key='app.session_started'` — the
891
- * generic app.* namespace the reviews wave shipped. The server's `_event_name(ev)` returns the
892
- * `question_key` for an `app_event`, which is what a trigger's `event` string matches. We do NOT
893
- * send `event_type:'app.session_started'` — that name is not in the server's EVENT_TYPES and
894
- * would be rejected. `device_key` rides in the non-PII `user_context` bucket, where the server's
895
- * `_event_device_key(ev)` reads it to group a device's sessions. `app_id` + `environment` are
896
- * filled server-side from the resolving key (never sent here).
897
- *
898
- * SESSION ID DISTINCTION (important): the `session_id` on this event is a PER-OPEN id (a fresh
899
- * `makeSessionId()` each app-open) — it is NOT the onboarding A2A `contextId`. Onboarding runs
900
- * ONCE (first launch) and owns its own context id; session-start fires on EVERY open, so it needs
901
- * its own per-open id. Grouping over time is done by `device_key`/`user_id`, not by session_id.
902
- *
903
- * Fire-and-forget: like every analytics path in the kit, this never throws into the UI, never
904
- * awaits, and swallows a missing target / bad URL / missing fetch / network error. Analytics must
905
- * never be able to break the app.
906
- */
907
-
908
909
  /** The canonical event name for an app-open. A trigger keys off this exact string. */
909
910
  declare const SESSION_STARTED_EVENT: "app.session_started";
910
911
  /** Options for {@link reportSessionStart}. Everything except `target` is optional so a pre-auth
@@ -1131,4 +1132,4 @@ interface UseLifecycleEventsOptions {
1131
1132
  */
1132
1133
  declare const useLifecycleEvents: (config: LifecycleConfig | undefined, options?: UseLifecycleEventsOptions) => void;
1133
1134
 
1134
- export { AnimatedSparkle, BACKGROUND_SESSION_MS, type CachedFeatures, CardHandoff, type CardHandoffProps, type CardHandoffVariant, ChipSelectCard, ClientEvent, ClientEventTarget, CompletionView, DEFAULT_FEATURES_TTL_MS, DemoOnboarding, type DemoOnboardingProps, DeviceContext, DoneBlock, ErrorBlock, FIRST_OPEN_EVENT, type IdentifyOnboardingOptions, IllustrationProvider, type IllustrationRegistry, InterstitialCard, type LifecycleConfig, type LifecycleEventInput, LoadingBlock, LoadingScreen, NumberStepperCard, type OnboardingAttribution, Button as OnboardingButton, OnboardingCopy, OnboardingEvent, type OnboardingFlagOptions, OnboardingFlow, OnboardingResult, OnboardingScaffold, OnboardingTheme, OnboardingThemeProvider, type ReportFirstOpenOptions, type ReportSessionStartOptions, type ResolveFeaturesOptions, SESSION_STARTED_EVENT, SelectionCard, type SessionStartConfig, StatusCard, StepProgress, StepValidator, TextInputCard, type ThemeFromBrandInput, USER_ID_MAX_LENGTH, type UseLifecycleEventsOptions, type UseSessionStartOptions, type WireConfigOverrides, WireFeatures, WireFeaturesConfig, WireFeaturesProvider, type WireFeaturesProviderProps, type WireLifecycleOptions, WireOnboarding, WireOnboardingConfig, WireOnboardingProps, WireOnboardingStorage, attributionMetadata, defaultIllustrations, defaultOnboardingTheme, defaultWireFeatures, deriveAnswers, featuresCacheKey, featuresEqual, fetchWireFeatures, firstOpenStorageKey, identifyOnboarding, isFeaturesFresh, isOnboardingEnabled, mergeTheme, motionSpec, onboardingComponents, parseWireFeatures, readCachedFeatures, readProgress, reportFirstOpen, reportSessionStart, resetFirstOpenLatch, resetSessionStartGuard, sanitizeUserId, themeFromBrand, useIllustration, useLifecycleEvents, useOnboardingTheme, useReducedMotion, useResolvedFeatures, useSessionStart, useWireFeatures, useWireFeaturesContext, wireConfigFromEnv, wireLifecycleEvents, writeCachedFeatures };
1135
+ export { AnimatedSparkle, BACKGROUND_SESSION_MS, type CachedFeatures, CardHandoff, type CardHandoffProps, type CardHandoffVariant, ChipSelectCard, ClientEvent, ClientEventTarget, CompletionView, DEFAULT_FEATURES_TTL_MS, DemoOnboarding, type DemoOnboardingProps, DeviceContext, DoneBlock, ErrorBlock, FIRST_OPEN_EVENT, type IdentifyOnboardingOptions, IllustrationProvider, type IllustrationRegistry, InterstitialCard, type LifecycleConfig, type LifecycleEventInput, LoadingBlock, LoadingScreen, NumberStepperCard, type OnboardingAttribution, Button as OnboardingButton, OnboardingCopy, OnboardingEvent, type OnboardingFlagOptions, OnboardingFlow, OnboardingResult, OnboardingScaffold, OnboardingTheme, OnboardingThemeProvider, type ReportFirstOpenOptions, type ReportSessionStartOptions, type ResolveFeaturesOptions, SESSION_STARTED_EVENT, SelectionCard, type SessionStartConfig, StatusCard, StepProgress, StepValidator, TextInputCard, type ThemeFromBrandInput, USER_ID_MAX_LENGTH, type UseLifecycleEventsOptions, type UseSessionStartOptions, type WireConfigOverrides, WireFeatures, WireFeaturesConfig, WireFeaturesProvider, type WireFeaturesProviderProps, type WireLifecycleOptions, WireOnboarding, WireOnboardingConfig, WireOnboardingProps, WireOnboardingStorage, attributionMetadata, defaultIllustrations, defaultOnboardingTheme, defaultWireFeatures, deriveAnswers, detectAppVersion, featuresCacheKey, featuresEqual, fetchWireFeatures, firstOpenStorageKey, identifyOnboarding, isFeaturesFresh, isOnboardingEnabled, mergeTheme, motionSpec, onboardingComponents, parseWireFeatures, readCachedFeatures, readProgress, reportFirstOpen, reportSessionStart, resetFirstOpenLatch, resetSessionStartGuard, sanitizeUserId, themeFromBrand, useIllustration, useLifecycleEvents, useOnboardingTheme, useReducedMotion, useResolvedFeatures, useSessionStart, useWireFeatures, useWireFeaturesContext, wireConfigFromEnv, wireLifecycleEvents, writeCachedFeatures };