@wireai/activation 0.14.3 → 0.16.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 (85) hide show
  1. package/AGENTS.md +96 -21
  2. package/CHANGELOG.md +780 -0
  3. package/INTEGRATION_PROMPT.md +61 -23
  4. package/README.md +110 -31
  5. package/dist/analytics/index.d.mts +35 -13
  6. package/dist/analytics/index.d.ts +35 -13
  7. package/dist/analytics/index.js +288 -127
  8. package/dist/analytics/index.js.map +1 -1
  9. package/dist/analytics/index.mjs +288 -127
  10. package/dist/analytics/index.mjs.map +1 -1
  11. package/dist/coachmarks/index.d.mts +15 -1
  12. package/dist/coachmarks/index.d.ts +15 -1
  13. package/dist/coachmarks/index.js +120 -22
  14. package/dist/coachmarks/index.js.map +1 -1
  15. package/dist/coachmarks/index.mjs +120 -22
  16. package/dist/coachmarks/index.mjs.map +1 -1
  17. package/dist/{currentSession-CUvTOchb.d.mts → currentSession-Bz7G6lno.d.mts} +35 -39
  18. package/dist/{currentSession-CW_5Mq4O.d.ts → currentSession-z-CZ55ad.d.ts} +35 -39
  19. package/dist/{decision-Bgo17oH7.d.mts → decision-3vWLuBlO.d.ts} +11 -2
  20. package/dist/{decision-Bkh_LigV.d.ts → decision-yBj2AyPW.d.mts} +11 -2
  21. package/dist/index.d.mts +5 -2
  22. package/dist/index.d.ts +5 -2
  23. package/dist/index.js +125 -36
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.mjs +125 -36
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/questionnaire/index.d.mts +3 -16
  28. package/dist/questionnaire/index.d.ts +3 -16
  29. package/dist/questionnaire/index.js +202 -46
  30. package/dist/questionnaire/index.js.map +1 -1
  31. package/dist/questionnaire/index.mjs +203 -47
  32. package/dist/questionnaire/index.mjs.map +1 -1
  33. package/dist/reviews/index.d.mts +10 -6
  34. package/dist/reviews/index.d.ts +10 -6
  35. package/dist/reviews/index.js +269 -52
  36. package/dist/reviews/index.js.map +1 -1
  37. package/dist/reviews/index.mjs +270 -53
  38. package/dist/reviews/index.mjs.map +1 -1
  39. package/dist/showcase/index.d.mts +1 -1
  40. package/dist/showcase/index.d.ts +1 -1
  41. package/dist/showcase/index.js +106 -20
  42. package/dist/showcase/index.js.map +1 -1
  43. package/dist/showcase/index.mjs +107 -21
  44. package/dist/showcase/index.mjs.map +1 -1
  45. package/dist/{transport-j5gFfJhK.d.mts → transport-s5QxA-ci.d.mts} +18 -13
  46. package/dist/{transport-B_0SgCBe.d.ts → transport-xqqSFqxs.d.ts} +18 -13
  47. package/dist/{types-Cju-1_jT.d.mts → types-Byx306Kv.d.mts} +25 -10
  48. package/dist/{types-BcmagF6K.d.mts → types-D_0B0yay.d.mts} +7 -2
  49. package/dist/{types-BcmagF6K.d.ts → types-D_0B0yay.d.ts} +7 -2
  50. package/dist/{types-h2BZvl1t.d.ts → types-tdATL5z0.d.ts} +25 -10
  51. package/llms.txt +9 -9
  52. package/package.json +6 -9
  53. package/src/WireOnboarding.tsx +4 -1
  54. package/src/analytics/currentSession.ts +141 -4
  55. package/src/analytics/index.ts +6 -1
  56. package/src/analytics/reportClientEvent.ts +19 -10
  57. package/src/analytics/useAnalytics.ts +74 -15
  58. package/src/analytics/wireDoctor.ts +152 -7
  59. package/src/coachmarks/CoachmarkProvider.tsx +26 -5
  60. package/src/coachmarks/runtime.ts +156 -2
  61. package/src/coachmarks/types.ts +7 -2
  62. package/src/coachmarks/useCoachmarkTour.ts +51 -1
  63. package/src/context/deviceId.ts +72 -6
  64. package/src/features/WireFeaturesProvider.tsx +72 -12
  65. package/src/features/fetchWireFeatures.ts +49 -11
  66. package/src/features/useWireFeatures.ts +39 -3
  67. package/src/identity/identityRecord.ts +15 -2
  68. package/src/questionnaire/QuestionnaireGate.tsx +40 -1
  69. package/src/questionnaire/transport.ts +22 -8
  70. package/src/questionnaire/useQuestionnaireGate.ts +58 -7
  71. package/src/reviews/ReviewGate.tsx +125 -37
  72. package/src/reviews/decision.ts +11 -1
  73. package/src/reviews/idempotency.ts +109 -0
  74. package/src/reviews/index.ts +4 -2
  75. package/src/reviews/runtime.ts +44 -13
  76. package/src/reviews/transport.ts +39 -20
  77. package/src/reviews/types.ts +7 -0
  78. package/src/reviews/useReviewGate.ts +57 -7
  79. package/src/session-analytics/lifecycle.ts +16 -0
  80. package/src/session-analytics/useLifecycleEvents.ts +30 -2
  81. package/src/session-analytics/useSessionStart.ts +22 -2
  82. package/src/showcase/FeatureShowcase.tsx +50 -3
  83. package/src/types.ts +15 -8
  84. package/src/utils/submitResult.ts +17 -9
  85. package/src/utils/withDeadline.ts +70 -0
@@ -81,33 +81,6 @@ type DeviceContext = {
81
81
  */
82
82
  declare const collectDeviceContext: () => DeviceContext;
83
83
 
84
- /**
85
- * reportClientEvent — forward DEVICE-ONLY onboarding events to the Wire AI analytics
86
- * backend (`POST {serverUrl}/v1/events`), completing the funnel for events the server
87
- * can't observe on its own.
88
- *
89
- * The backend already records the server-observable funnel during the A2A flow
90
- * (`session_started`, `screen_shown`, `answer_submitted`, `completed`, `llm_fallback`,
91
- * and even `screen_skipped` — it derives that from the kit's skip sentinel). The one
92
- * event no server request can capture is `dropped`: the user closing the app / unmounting
93
- * the flow without finishing. That's what this reporter is for.
94
- *
95
- * Contract (server: routers/onboarding.py → analytics/events.py):
96
- * POST {serverUrl}/v1/events
97
- * Authorization: Bearer {apiKey}
98
- * { "events": [ { event_type, session_id, screen_index?, component?, question_key?,
99
- * latency_ms?, meta?, device?, user_context? } ] }
100
- * The server fills `app_id` + `environment` from the resolving key (never send app_id),
101
- * and silently skips malformed events — one bad payload never fails the batch.
102
- *
103
- * ⚠️ Correlation: `session_id` MUST equal the A2A `contextId` the server uses to key the
104
- * server-side events, or the funnel report (which groups by `session_id`) treats this as a
105
- * phantom session. See `makeSessionId` + WireOnboarding for how the kit seeds it.
106
- *
107
- * Fire-and-forget: this never throws into the UI and never awaits — analytics must never
108
- * be able to break onboarding.
109
- */
110
-
111
84
  /** Event types a CLIENT may report. The rest of the funnel is server-side; sending those
112
85
  * here would double-count. `screen_skipped` is included for completeness, but the kit does
113
86
  * NOT emit it — the backend already derives it from the skip sentinel (see OnboardingFlow).
@@ -669,10 +642,11 @@ type WireOnboardingProps = {
669
642
  * Build the value with the helper so the wire spelling is decided in one place:
670
643
  *
671
644
  * ```tsx
672
- * <WireOnboarding userContext={activationJoinContext(deviceKey)} ... />
673
- * // no device id of your own? pass `storage` and leave this prop alone the kit injects its
674
- * // own key, and only after it has confirmed the key actually persists (see `autoJoinKey`).
675
- * <WireOnboarding config={{ ...config, storage }} ... />
645
+ * <WireOnboarding config={config} userContext={activationJoinContext(deviceKey)} ... />
646
+ * // no device id of your own? pass the `storage` PROP (it is a prop of this component, never a
647
+ * // field of `WireOnboardingConfig`) and leave this prop alone the kit injects its own key,
648
+ * // and only after it has confirmed the key actually persists (see `autoJoinKey`).
649
+ * <WireOnboarding config={config} storage={storage} ... />
676
650
  * ```
677
651
  *
678
652
  * ⛔ Do NOT hand-build the auto key with `activationJoinContext(resolveAutoDeviceKey({...}))`.
@@ -685,8 +659,11 @@ type WireOnboardingProps = {
685
659
  *
686
660
  * SINCE 0.12.2, leaving it out no longer silently empties the funnel: when you pass `storage` and
687
661
  * this prop carries no `device_key`, the kit injects its OWN per-install key — the same one the
688
- * analytics surfaces mint and persist — so the default wiring joins. Anything you DO pass wins
689
- * verbatim and is never touched. See `autoJoinKey` for the opt-out and the two cases where the kit
662
+ * analytics surfaces mint and persist — so the default wiring joins. A `device_key` you DO pass
663
+ * wins verbatim and is never touched, provided it is a string with at least one non-whitespace
664
+ * character (`resolveIdentity`) — this map also accepts numbers and booleans, and a non-string or
665
+ * all-whitespace value is not a usable join key, so it is read as absent and the auto key is
666
+ * merged over it. See `autoJoinKey` for the opt-out and the two cases where the kit
690
667
  * still cannot fill the gap (no `storage`, or you opted out), which keep warning in dev.
691
668
  *
692
669
  * Never hand-write `userContext={{ deviceKey }}`: the server's device lookup reads `device_key`,
@@ -713,8 +690,11 @@ type WireOnboardingProps = {
713
690
  * `autoJoinKey={false}` if you genuinely want an UNLINKED onboarding session; that restores the
714
691
  * pre-0.12.2 behavior exactly (nothing injected) and the dev warning fires again.
715
692
  *
716
- * Two things this flag does NOT do. It never overrides a `device_key` you passed — a host-supplied
717
- * key always wins, whatever this is set to. And it cannot conjure a key without `storage`: with no
693
+ * Two things this flag does NOT do. It never overrides a `device_key` you actually supplied — a
694
+ * host-supplied key always wins, whatever this is set to, where "supplied" means a string with at
695
+ * least one non-whitespace character (`resolveIdentity`); a non-string or an all-whitespace value
696
+ * is read as absent and the auto key is merged over it. And it cannot conjure a key without
697
+ * `storage`: with no
718
698
  * persistence the kit's id is minted fresh every launch, and a per-launch key corrupts
719
699
  * `min_sessions` instead of merely leaving the join empty, so the kit declines to inject and warns
720
700
  * in dev instead.
@@ -1396,8 +1376,19 @@ declare const resolveAutoDeviceKey: (opts?: ResolveAutoDeviceKeyOptions) => stri
1396
1376
  * follows it. Only a caller that can afford one storage read should use this; the fire-and-forget
1397
1377
  * event paths must stay on the sync function.
1398
1378
  *
1399
- * Never throws or rejects: a missing, hung, or rejecting adapter resolves to the in-memory id, and
1400
- * with no `storage` it resolves immediately (there is nothing to hydrate from).
1379
+ * Never throws or rejects: a missing or rejecting adapter resolves to the in-memory id, and with no
1380
+ * `storage` it resolves immediately (there is nothing to hydrate from).
1381
+ *
1382
+ * ⛔ A HUNG ADAPTER IS THE ONE CASE IT DOES NOT COVER, and this line used to claim it did. The read
1383
+ * underneath is a bare `storage.getItem` with no ceiling of its own, so an adapter that neither
1384
+ * resolves nor rejects (a locked keychain, a wedged native bridge) leaves THIS PROMISE PENDING
1385
+ * FOREVER — it does not fall back to the in-memory id, it simply never answers. Anything that gates
1386
+ * a user-visible or metric-bearing action on the result must therefore race it against a ceiling of
1387
+ * its own: `<WireOnboarding>` does (`AUTO_JOIN_HYDRATION_TIMEOUT_MS`), and so do the two lifecycle
1388
+ * hooks (`session-analytics/useLifecycleEvents` + `useSessionStart`, through the shared
1389
+ * `withTimeout` + `READ_TIMEOUT_MS` in `session/persistedSession`) — a hung read there had silently
1390
+ * killed `app.session_started` and `app.first_open` for the whole process. Use the SYNC
1391
+ * {@link resolveAutoDeviceKey} when you cannot afford to wait at all.
1401
1392
  *
1402
1393
  * ⚠️ IT RETURNS A BARE STRING, so it CANNOT say whether the id survives the launch — a degraded
1403
1394
  * adapter resolves to the in-memory mint and reads identically to a persisted one. No kit surface
@@ -1419,7 +1410,9 @@ declare const hydrateAutoDeviceKey: (opts?: ResolveAutoDeviceKeyOptions) => Prom
1419
1410
  *
1420
1411
  * Resolves `undefined` only when there is no usable id at all. With no `storage` it resolves
1421
1412
  * immediately with `durable: false` — a process-scoped id is exactly what "no persistence" means.
1422
- * Never throws or rejects.
1413
+ * Never throws or rejects — but, exactly like {@link hydrateAutoDeviceKey} above, it can also never
1414
+ * SETTLE on a hung adapter (the underlying `getItem` carries no ceiling), so every caller races it
1415
+ * against one of its own. See the note there.
1423
1416
  */
1424
1417
  declare const hydrateDeviceIdentity: (opts?: ResolveAutoDeviceKeyOptions) => Promise<IdentityRecord | undefined>;
1425
1418
  /** Test-only: forget every auto id + hydration flag so a unit test starts from a clean registry. */
@@ -1432,7 +1425,10 @@ declare const resetAutoDeviceKeys: () => void;
1432
1425
  declare const setCurrentSessionId: (id: string | undefined) => void;
1433
1426
  /** The current per-open `session_id`, or `undefined` when no app-open has been registered yet. */
1434
1427
  declare const getCurrentSessionId: () => string | undefined;
1435
- /** Test-only: forget the current session id so a unit test starts from a clean registry. */
1428
+ /** Test-only: forget the current session id so a unit test starts from a clean registry. Does NOT
1429
+ * clear the MINTED-id record (see `isMintedSessionId`), for the same reason the warn latch below is
1430
+ * not cleared: `makeSessionId()` never repeats, so a remembered fallback id can never again equal a
1431
+ * live one, and the record is overwritten by the next mint anyway. */
1436
1432
  declare const resetCurrentSessionId: () => void;
1437
1433
  /**
1438
1434
  * The current per-open `session_id`, MINTING and registering one when no app-open has been
@@ -81,33 +81,6 @@ type DeviceContext = {
81
81
  */
82
82
  declare const collectDeviceContext: () => DeviceContext;
83
83
 
84
- /**
85
- * reportClientEvent — forward DEVICE-ONLY onboarding events to the Wire AI analytics
86
- * backend (`POST {serverUrl}/v1/events`), completing the funnel for events the server
87
- * can't observe on its own.
88
- *
89
- * The backend already records the server-observable funnel during the A2A flow
90
- * (`session_started`, `screen_shown`, `answer_submitted`, `completed`, `llm_fallback`,
91
- * and even `screen_skipped` — it derives that from the kit's skip sentinel). The one
92
- * event no server request can capture is `dropped`: the user closing the app / unmounting
93
- * the flow without finishing. That's what this reporter is for.
94
- *
95
- * Contract (server: routers/onboarding.py → analytics/events.py):
96
- * POST {serverUrl}/v1/events
97
- * Authorization: Bearer {apiKey}
98
- * { "events": [ { event_type, session_id, screen_index?, component?, question_key?,
99
- * latency_ms?, meta?, device?, user_context? } ] }
100
- * The server fills `app_id` + `environment` from the resolving key (never send app_id),
101
- * and silently skips malformed events — one bad payload never fails the batch.
102
- *
103
- * ⚠️ Correlation: `session_id` MUST equal the A2A `contextId` the server uses to key the
104
- * server-side events, or the funnel report (which groups by `session_id`) treats this as a
105
- * phantom session. See `makeSessionId` + WireOnboarding for how the kit seeds it.
106
- *
107
- * Fire-and-forget: this never throws into the UI and never awaits — analytics must never
108
- * be able to break onboarding.
109
- */
110
-
111
84
  /** Event types a CLIENT may report. The rest of the funnel is server-side; sending those
112
85
  * here would double-count. `screen_skipped` is included for completeness, but the kit does
113
86
  * NOT emit it — the backend already derives it from the skip sentinel (see OnboardingFlow).
@@ -669,10 +642,11 @@ type WireOnboardingProps = {
669
642
  * Build the value with the helper so the wire spelling is decided in one place:
670
643
  *
671
644
  * ```tsx
672
- * <WireOnboarding userContext={activationJoinContext(deviceKey)} ... />
673
- * // no device id of your own? pass `storage` and leave this prop alone the kit injects its
674
- * // own key, and only after it has confirmed the key actually persists (see `autoJoinKey`).
675
- * <WireOnboarding config={{ ...config, storage }} ... />
645
+ * <WireOnboarding config={config} userContext={activationJoinContext(deviceKey)} ... />
646
+ * // no device id of your own? pass the `storage` PROP (it is a prop of this component, never a
647
+ * // field of `WireOnboardingConfig`) and leave this prop alone the kit injects its own key,
648
+ * // and only after it has confirmed the key actually persists (see `autoJoinKey`).
649
+ * <WireOnboarding config={config} storage={storage} ... />
676
650
  * ```
677
651
  *
678
652
  * ⛔ Do NOT hand-build the auto key with `activationJoinContext(resolveAutoDeviceKey({...}))`.
@@ -685,8 +659,11 @@ type WireOnboardingProps = {
685
659
  *
686
660
  * SINCE 0.12.2, leaving it out no longer silently empties the funnel: when you pass `storage` and
687
661
  * this prop carries no `device_key`, the kit injects its OWN per-install key — the same one the
688
- * analytics surfaces mint and persist — so the default wiring joins. Anything you DO pass wins
689
- * verbatim and is never touched. See `autoJoinKey` for the opt-out and the two cases where the kit
662
+ * analytics surfaces mint and persist — so the default wiring joins. A `device_key` you DO pass
663
+ * wins verbatim and is never touched, provided it is a string with at least one non-whitespace
664
+ * character (`resolveIdentity`) — this map also accepts numbers and booleans, and a non-string or
665
+ * all-whitespace value is not a usable join key, so it is read as absent and the auto key is
666
+ * merged over it. See `autoJoinKey` for the opt-out and the two cases where the kit
690
667
  * still cannot fill the gap (no `storage`, or you opted out), which keep warning in dev.
691
668
  *
692
669
  * Never hand-write `userContext={{ deviceKey }}`: the server's device lookup reads `device_key`,
@@ -713,8 +690,11 @@ type WireOnboardingProps = {
713
690
  * `autoJoinKey={false}` if you genuinely want an UNLINKED onboarding session; that restores the
714
691
  * pre-0.12.2 behavior exactly (nothing injected) and the dev warning fires again.
715
692
  *
716
- * Two things this flag does NOT do. It never overrides a `device_key` you passed — a host-supplied
717
- * key always wins, whatever this is set to. And it cannot conjure a key without `storage`: with no
693
+ * Two things this flag does NOT do. It never overrides a `device_key` you actually supplied — a
694
+ * host-supplied key always wins, whatever this is set to, where "supplied" means a string with at
695
+ * least one non-whitespace character (`resolveIdentity`); a non-string or an all-whitespace value
696
+ * is read as absent and the auto key is merged over it. And it cannot conjure a key without
697
+ * `storage`: with no
718
698
  * persistence the kit's id is minted fresh every launch, and a per-launch key corrupts
719
699
  * `min_sessions` instead of merely leaving the join empty, so the kit declines to inject and warns
720
700
  * in dev instead.
@@ -1396,8 +1376,19 @@ declare const resolveAutoDeviceKey: (opts?: ResolveAutoDeviceKeyOptions) => stri
1396
1376
  * follows it. Only a caller that can afford one storage read should use this; the fire-and-forget
1397
1377
  * event paths must stay on the sync function.
1398
1378
  *
1399
- * Never throws or rejects: a missing, hung, or rejecting adapter resolves to the in-memory id, and
1400
- * with no `storage` it resolves immediately (there is nothing to hydrate from).
1379
+ * Never throws or rejects: a missing or rejecting adapter resolves to the in-memory id, and with no
1380
+ * `storage` it resolves immediately (there is nothing to hydrate from).
1381
+ *
1382
+ * ⛔ A HUNG ADAPTER IS THE ONE CASE IT DOES NOT COVER, and this line used to claim it did. The read
1383
+ * underneath is a bare `storage.getItem` with no ceiling of its own, so an adapter that neither
1384
+ * resolves nor rejects (a locked keychain, a wedged native bridge) leaves THIS PROMISE PENDING
1385
+ * FOREVER — it does not fall back to the in-memory id, it simply never answers. Anything that gates
1386
+ * a user-visible or metric-bearing action on the result must therefore race it against a ceiling of
1387
+ * its own: `<WireOnboarding>` does (`AUTO_JOIN_HYDRATION_TIMEOUT_MS`), and so do the two lifecycle
1388
+ * hooks (`session-analytics/useLifecycleEvents` + `useSessionStart`, through the shared
1389
+ * `withTimeout` + `READ_TIMEOUT_MS` in `session/persistedSession`) — a hung read there had silently
1390
+ * killed `app.session_started` and `app.first_open` for the whole process. Use the SYNC
1391
+ * {@link resolveAutoDeviceKey} when you cannot afford to wait at all.
1401
1392
  *
1402
1393
  * ⚠️ IT RETURNS A BARE STRING, so it CANNOT say whether the id survives the launch — a degraded
1403
1394
  * adapter resolves to the in-memory mint and reads identically to a persisted one. No kit surface
@@ -1419,7 +1410,9 @@ declare const hydrateAutoDeviceKey: (opts?: ResolveAutoDeviceKeyOptions) => Prom
1419
1410
  *
1420
1411
  * Resolves `undefined` only when there is no usable id at all. With no `storage` it resolves
1421
1412
  * immediately with `durable: false` — a process-scoped id is exactly what "no persistence" means.
1422
- * Never throws or rejects.
1413
+ * Never throws or rejects — but, exactly like {@link hydrateAutoDeviceKey} above, it can also never
1414
+ * SETTLE on a hung adapter (the underlying `getItem` carries no ceiling), so every caller races it
1415
+ * against one of its own. See the note there.
1423
1416
  */
1424
1417
  declare const hydrateDeviceIdentity: (opts?: ResolveAutoDeviceKeyOptions) => Promise<IdentityRecord | undefined>;
1425
1418
  /** Test-only: forget every auto id + hydration flag so a unit test starts from a clean registry. */
@@ -1432,7 +1425,10 @@ declare const resetAutoDeviceKeys: () => void;
1432
1425
  declare const setCurrentSessionId: (id: string | undefined) => void;
1433
1426
  /** The current per-open `session_id`, or `undefined` when no app-open has been registered yet. */
1434
1427
  declare const getCurrentSessionId: () => string | undefined;
1435
- /** Test-only: forget the current session id so a unit test starts from a clean registry. */
1428
+ /** Test-only: forget the current session id so a unit test starts from a clean registry. Does NOT
1429
+ * clear the MINTED-id record (see `isMintedSessionId`), for the same reason the warn latch below is
1430
+ * not cleared: `makeSessionId()` never repeats, so a remembered fallback id can never again equal a
1431
+ * live one, and the record is overwritten by the next mint anyway. */
1436
1432
  declare const resetCurrentSessionId: () => void;
1437
1433
  /**
1438
1434
  * The current per-open `session_id`, MINTING and registering one when no app-open has been
@@ -1,4 +1,4 @@
1
- import { c as ReviewDecision, j as ReviewSubmission, e as RatingRoute } from './types-Cju-1_jT.mjs';
1
+ import { c as ReviewDecision, j as ReviewSubmission, e as RatingRoute } from './types-tdATL5z0.js';
2
2
 
3
3
  /**
4
4
  * decision.ts — the pure decision logic for the review gate.
@@ -65,7 +65,14 @@ declare const evaluateGate: (rules: GateRules, s: GateSignals) => ReviewDecision
65
65
  * never a button labeled "rate us 5 stars" (see the store-policy notes in the README).
66
66
  */
67
67
  declare const routeRating: (stars: number) => RatingRoute;
68
- /** Build the `POST /v1/reviews` body. Feedback text/contact belong ONLY here, never in events. */
68
+ /**
69
+ * Build the `POST /v1/reviews` body. Feedback text/contact belong ONLY here, never in events.
70
+ *
71
+ * STILL PURE, deliberately: `idempotencyKey` and the `meta.device_key` the server scopes it to are
72
+ * both passed IN rather than resolved here. Reaching into the device-key registry from this function
73
+ * would make the gate logic untestable in isolation, and both values are scoped to an impression
74
+ * this function cannot see — see `reviews/idempotency.ts`.
75
+ */
69
76
  declare const buildReviewSubmission: (input: {
70
77
  stars: number;
71
78
  feedbackText?: string;
@@ -73,6 +80,8 @@ declare const buildReviewSubmission: (input: {
73
80
  contact?: string;
74
81
  sessionId?: string;
75
82
  source?: string;
83
+ /** The impression's idempotency key. Omitted → the server mints a row id and inserts, as before. */
84
+ idempotencyKey?: string;
76
85
  meta?: Record<string, unknown>;
77
86
  }) => ReviewSubmission;
78
87
 
@@ -1,4 +1,4 @@
1
- import { c as ReviewDecision, j as ReviewSubmission, e as RatingRoute } from './types-h2BZvl1t.js';
1
+ import { c as ReviewDecision, j as ReviewSubmission, e as RatingRoute } from './types-Byx306Kv.mjs';
2
2
 
3
3
  /**
4
4
  * decision.ts — the pure decision logic for the review gate.
@@ -65,7 +65,14 @@ declare const evaluateGate: (rules: GateRules, s: GateSignals) => ReviewDecision
65
65
  * never a button labeled "rate us 5 stars" (see the store-policy notes in the README).
66
66
  */
67
67
  declare const routeRating: (stars: number) => RatingRoute;
68
- /** Build the `POST /v1/reviews` body. Feedback text/contact belong ONLY here, never in events. */
68
+ /**
69
+ * Build the `POST /v1/reviews` body. Feedback text/contact belong ONLY here, never in events.
70
+ *
71
+ * STILL PURE, deliberately: `idempotencyKey` and the `meta.device_key` the server scopes it to are
72
+ * both passed IN rather than resolved here. Reaching into the device-key registry from this function
73
+ * would make the gate logic untestable in isolation, and both values are scoped to an impression
74
+ * this function cannot see — see `reviews/idempotency.ts`.
75
+ */
69
76
  declare const buildReviewSubmission: (input: {
70
77
  stars: number;
71
78
  feedbackText?: string;
@@ -73,6 +80,8 @@ declare const buildReviewSubmission: (input: {
73
80
  contact?: string;
74
81
  sessionId?: string;
75
82
  source?: string;
83
+ /** The impression's idempotency key. Omitted → the server mints a row id and inserts, as before. */
84
+ idempotencyKey?: string;
76
85
  meta?: Record<string, unknown>;
77
86
  }) => ReviewSubmission;
78
87
 
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 { I as WireOnboardingProps, J as WireOnboardingConfig, O as OnboardingResult, K as WirePermissionStatus, P as PermissionStage, L as WirePermissionOutcome, S as StepValidator, M as OnboardingEvent, N as OnboardingCopy, C as ClientEventTarget, Q as DeviceContext, T as PermissionScreenConfig, W as WireUserContext, c as ClientEvent, U as PermissionPlacement, g as EnvelopeSource } from './currentSession-CUvTOchb.mjs';
4
- export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, d as ClientEventType, V as DEFAULT_PERMISSION_COPY, X as DeviceFormFactor, D as DeviceKeyStorage, Y as EXTRA_KEY_PREFIX, Z as GENERIC_PERMISSION_COPY, _ as IdentifyOnboardingBinding, $ as IdentifyOnboardingOptions, a0 as IdentityRecord, a1 as IdentitySource, a2 as IdentitySpace, a3 as NOTIFICATIONS_PERMISSION_COPY, a4 as OnboardingProgress, a5 as PermissionScreenCopy, R as ResolveAutoDeviceKeyOptions, a6 as ResolveUserContextOptions, a7 as ResolvedUserContext, a8 as USER_ID_MAX_LENGTH, i as WIRE_ONBOARDING_EVENTS, a9 as WIRE_PERMISSION_EVENTS, j as WireOnboardingEventName, aa as WirePermissionEventName, ab as WirePermissionKind, ac as activationJoinContext, k as analyticsUserIdStorageKey, m as clearPiiFromContext, n as clearUserContext, ad as collectDeviceContext, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, ae as hashEmailFnv1a, af as hostIdentity, ag as hydrateAutoDeviceKey, ah as hydrateDeviceIdentity, ai as identifyOnboarding, aj as isWireScalar, s as looksLikeEmail, t as makeSessionId, ak as mintDeviceId, al as namespaceExtra, am as normalizePermissionStatus, an as permissionEventName, ao as permissionEventProps, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, ap as resetIdentityProvenance, F as resolveAutoDeviceKey, aq as resolveIdentity, ar as resolvePermissionCopy, as as resolveUserContext, at as sanitizeUserId, G as setCurrentSessionId, H as toAnalyticsEvent } from './currentSession-CUvTOchb.mjs';
3
+ import { I as WireOnboardingProps, J as WireOnboardingConfig, O as OnboardingResult, K as WirePermissionStatus, P as PermissionStage, L as WirePermissionOutcome, S as StepValidator, M as OnboardingEvent, N as OnboardingCopy, C as ClientEventTarget, Q as DeviceContext, T as PermissionScreenConfig, W as WireUserContext, c as ClientEvent, U as PermissionPlacement, g as EnvelopeSource } from './currentSession-Bz7G6lno.mjs';
4
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, d as ClientEventType, V as DEFAULT_PERMISSION_COPY, X as DeviceFormFactor, D as DeviceKeyStorage, Y as EXTRA_KEY_PREFIX, Z as GENERIC_PERMISSION_COPY, _ as IdentifyOnboardingBinding, $ as IdentifyOnboardingOptions, a0 as IdentityRecord, a1 as IdentitySource, a2 as IdentitySpace, a3 as NOTIFICATIONS_PERMISSION_COPY, a4 as OnboardingProgress, a5 as PermissionScreenCopy, R as ResolveAutoDeviceKeyOptions, a6 as ResolveUserContextOptions, a7 as ResolvedUserContext, a8 as USER_ID_MAX_LENGTH, i as WIRE_ONBOARDING_EVENTS, a9 as WIRE_PERMISSION_EVENTS, j as WireOnboardingEventName, aa as WirePermissionEventName, ab as WirePermissionKind, ac as activationJoinContext, k as analyticsUserIdStorageKey, m as clearPiiFromContext, n as clearUserContext, ad as collectDeviceContext, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, ae as hashEmailFnv1a, af as hostIdentity, ag as hydrateAutoDeviceKey, ah as hydrateDeviceIdentity, ai as identifyOnboarding, aj as isWireScalar, s as looksLikeEmail, t as makeSessionId, ak as mintDeviceId, al as namespaceExtra, am as normalizePermissionStatus, an as permissionEventName, ao as permissionEventProps, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, ap as resetIdentityProvenance, F as resolveAutoDeviceKey, aq as resolveIdentity, ar as resolvePermissionCopy, as as resolveUserContext, at as sanitizeUserId, G as setCurrentSessionId, H as toAnalyticsEvent } from './currentSession-Bz7G6lno.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';
@@ -1301,6 +1301,7 @@ declare const readProgress: (response?: WireAIResponse) => PartialProgress;
1301
1301
  */
1302
1302
  declare const fetchWireFeatures: (config?: WireFeaturesConfig) => Promise<WireFeatures>;
1303
1303
 
1304
+ /** The flags alone — the original hook, unchanged for every consumer that cannot act on `settled`. */
1304
1305
  declare const useWireFeatures: (config?: WireFeaturesConfig) => WireFeatures;
1305
1306
 
1306
1307
  /**
@@ -2165,6 +2166,8 @@ interface ReportFirstOpenOptions extends LifecycleEventInput {
2165
2166
  * • Race guard: an in-memory latch is set SYNCHRONOUSLY before the async read, so two
2166
2167
  * near-simultaneous calls fire at most once.
2167
2168
  * • Without `storage`: degraded mode — fires once per PROCESS via the latch only (documented).
2169
+ * • WITHOUT A TRANSPORT: no-op, and — the point of the check — the once-ever flag is NOT spent.
2170
+ * See below.
2168
2171
  */
2169
2172
  declare const reportFirstOpen: (opts: ReportFirstOpenOptions) => void;
2170
2173
  /** Options for {@link wireLifecycleEvents}: the shared lifecycle inputs + first-open persistence. */
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
- import { I as WireOnboardingProps, J as WireOnboardingConfig, O as OnboardingResult, K as WirePermissionStatus, P as PermissionStage, L as WirePermissionOutcome, S as StepValidator, M as OnboardingEvent, N as OnboardingCopy, C as ClientEventTarget, Q as DeviceContext, T as PermissionScreenConfig, W as WireUserContext, c as ClientEvent, U as PermissionPlacement, g as EnvelopeSource } from './currentSession-CW_5Mq4O.js';
4
- export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, d as ClientEventType, V as DEFAULT_PERMISSION_COPY, X as DeviceFormFactor, D as DeviceKeyStorage, Y as EXTRA_KEY_PREFIX, Z as GENERIC_PERMISSION_COPY, _ as IdentifyOnboardingBinding, $ as IdentifyOnboardingOptions, a0 as IdentityRecord, a1 as IdentitySource, a2 as IdentitySpace, a3 as NOTIFICATIONS_PERMISSION_COPY, a4 as OnboardingProgress, a5 as PermissionScreenCopy, R as ResolveAutoDeviceKeyOptions, a6 as ResolveUserContextOptions, a7 as ResolvedUserContext, a8 as USER_ID_MAX_LENGTH, i as WIRE_ONBOARDING_EVENTS, a9 as WIRE_PERMISSION_EVENTS, j as WireOnboardingEventName, aa as WirePermissionEventName, ab as WirePermissionKind, ac as activationJoinContext, k as analyticsUserIdStorageKey, m as clearPiiFromContext, n as clearUserContext, ad as collectDeviceContext, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, ae as hashEmailFnv1a, af as hostIdentity, ag as hydrateAutoDeviceKey, ah as hydrateDeviceIdentity, ai as identifyOnboarding, aj as isWireScalar, s as looksLikeEmail, t as makeSessionId, ak as mintDeviceId, al as namespaceExtra, am as normalizePermissionStatus, an as permissionEventName, ao as permissionEventProps, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, ap as resetIdentityProvenance, F as resolveAutoDeviceKey, aq as resolveIdentity, ar as resolvePermissionCopy, as as resolveUserContext, at as sanitizeUserId, G as setCurrentSessionId, H as toAnalyticsEvent } from './currentSession-CW_5Mq4O.js';
3
+ import { I as WireOnboardingProps, J as WireOnboardingConfig, O as OnboardingResult, K as WirePermissionStatus, P as PermissionStage, L as WirePermissionOutcome, S as StepValidator, M as OnboardingEvent, N as OnboardingCopy, C as ClientEventTarget, Q as DeviceContext, T as PermissionScreenConfig, W as WireUserContext, c as ClientEvent, U as PermissionPlacement, g as EnvelopeSource } from './currentSession-z-CZ55ad.js';
4
+ export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, d as ClientEventType, V as DEFAULT_PERMISSION_COPY, X as DeviceFormFactor, D as DeviceKeyStorage, Y as EXTRA_KEY_PREFIX, Z as GENERIC_PERMISSION_COPY, _ as IdentifyOnboardingBinding, $ as IdentifyOnboardingOptions, a0 as IdentityRecord, a1 as IdentitySource, a2 as IdentitySpace, a3 as NOTIFICATIONS_PERMISSION_COPY, a4 as OnboardingProgress, a5 as PermissionScreenCopy, R as ResolveAutoDeviceKeyOptions, a6 as ResolveUserContextOptions, a7 as ResolvedUserContext, a8 as USER_ID_MAX_LENGTH, i as WIRE_ONBOARDING_EVENTS, a9 as WIRE_PERMISSION_EVENTS, j as WireOnboardingEventName, aa as WirePermissionEventName, ab as WirePermissionKind, ac as activationJoinContext, k as analyticsUserIdStorageKey, m as clearPiiFromContext, n as clearUserContext, ad as collectDeviceContext, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, ae as hashEmailFnv1a, af as hostIdentity, ag as hydrateAutoDeviceKey, ah as hydrateDeviceIdentity, ai as identifyOnboarding, aj as isWireScalar, s as looksLikeEmail, t as makeSessionId, ak as mintDeviceId, al as namespaceExtra, am as normalizePermissionStatus, an as permissionEventName, ao as permissionEventProps, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, ap as resetIdentityProvenance, F as resolveAutoDeviceKey, aq as resolveIdentity, ar as resolvePermissionCopy, as as resolveUserContext, at as sanitizeUserId, G as setCurrentSessionId, H as toAnalyticsEvent } from './currentSession-z-CZ55ad.js';
5
5
  import { O as OnboardingTheme } from './types-BKfpdZzX.js';
6
6
  export { a as OnboardingButtonStyle, b as OnboardingColors, c as OnboardingFonts, d as OnboardingRadius, e as OnboardingSpacing } from './types-BKfpdZzX.js';
7
7
  export { C as CenteredModal, a as CenteredModalHandle, b as CenteredModalProps } from './CenteredModal-Cdgns6--.js';
@@ -1301,6 +1301,7 @@ declare const readProgress: (response?: WireAIResponse) => PartialProgress;
1301
1301
  */
1302
1302
  declare const fetchWireFeatures: (config?: WireFeaturesConfig) => Promise<WireFeatures>;
1303
1303
 
1304
+ /** The flags alone — the original hook, unchanged for every consumer that cannot act on `settled`. */
1304
1305
  declare const useWireFeatures: (config?: WireFeaturesConfig) => WireFeatures;
1305
1306
 
1306
1307
  /**
@@ -2165,6 +2166,8 @@ interface ReportFirstOpenOptions extends LifecycleEventInput {
2165
2166
  * • Race guard: an in-memory latch is set SYNCHRONOUSLY before the async read, so two
2166
2167
  * near-simultaneous calls fire at most once.
2167
2168
  * • Without `storage`: degraded mode — fires once per PROCESS via the latch only (documented).
2169
+ * • WITHOUT A TRANSPORT: no-op, and — the point of the check — the once-ever flag is NOT spent.
2170
+ * See below.
2168
2171
  */
2169
2172
  declare const reportFirstOpen: (opts: ReportFirstOpenOptions) => void;
2170
2173
  /** Options for {@link wireLifecycleEvents}: the shared lifecycle inputs + first-open persistence. */