@wireai/activation 0.9.1 → 0.9.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.
- package/dist/analytics/index.d.mts +2 -2
- package/dist/analytics/index.d.ts +2 -2
- package/dist/analytics/index.js +9 -5
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +9 -5
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/{currentSession-Bs2JfTJ8.d.mts → currentSession-CRRQLbAq.d.mts} +16 -0
- package/dist/{currentSession-61dcm3V-.d.ts → currentSession-Dj0cN3Rg.d.ts} +16 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -7
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.js +2 -1
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +2 -1
- package/dist/questionnaire/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/WireOnboarding.tsx +15 -4
- package/src/analytics/eventQueue.ts +28 -9
- package/src/questionnaire/transport.ts +9 -1
- package/src/session/persistedSession.ts +17 -0
- package/src/types.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wireai/activation",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Premium, fully-themable drop-in AI onboarding kit for React Native / Expo, on top of the open-source wireai-rn SDK.",
|
|
6
6
|
"author": "Malik Chohra <malik@getwireai.com>",
|
package/src/WireOnboarding.tsx
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
clearPersistedSession,
|
|
28
28
|
loadPersistedSession,
|
|
29
29
|
sessionStorageKey,
|
|
30
|
+
shouldClearOnComplete,
|
|
30
31
|
DEFAULT_SESSION_TTL_MS,
|
|
31
32
|
type LoadedSession,
|
|
32
33
|
} from "./session/persistedSession";
|
|
@@ -52,6 +53,7 @@ export const WireOnboarding: React.FC<WireOnboardingProps> = ({
|
|
|
52
53
|
storage,
|
|
53
54
|
sessionTtlMs = DEFAULT_SESSION_TTL_MS,
|
|
54
55
|
persistKey,
|
|
56
|
+
retainSessionOnComplete,
|
|
55
57
|
userContext,
|
|
56
58
|
userId,
|
|
57
59
|
}) => {
|
|
@@ -123,15 +125,24 @@ export const WireOnboarding: React.FC<WireOnboardingProps> = ({
|
|
|
123
125
|
};
|
|
124
126
|
}, [storage, storageKey, sessionTtlMs, session]);
|
|
125
127
|
|
|
126
|
-
//
|
|
127
|
-
// device starts a fresh session. Dropped/degraded paths deliberately do NOT clear —
|
|
128
|
+
// By DEFAULT, clear the cached id the moment the flow completes, so the next onboarding on
|
|
129
|
+
// this device starts a fresh session. Dropped/degraded paths deliberately do NOT clear —
|
|
128
130
|
// a dropped session must stay resumable within the TTL.
|
|
131
|
+
//
|
|
132
|
+
// OPT-IN (`retainSessionOnComplete`): a multi-stage / replay-after-complete signup re-enters
|
|
133
|
+
// (often via a `key=` remount) WITHIN one signup. If we cleared, that re-entry would find no
|
|
134
|
+
// seed and mint a fresh `metadata.sessionId` → a phantom second `session_started` on the same
|
|
135
|
+
// funnel. When the host opts in, we LEAVE the seed in place so the re-entry resumes the SAME
|
|
136
|
+
// session (one funnel start); freshness for a genuinely new run then rides the TTL (past
|
|
137
|
+
// `sessionTtlMs` → fresh mint) and an explicit new-run signal (a changed `persistKey`).
|
|
138
|
+
// `shouldClearOnComplete` is the pure, unit-tested seam; the `storage` guard is unchanged, so a
|
|
139
|
+
// host that does not opt in lands on the byte-for-byte legacy clear-on-complete path.
|
|
129
140
|
const handleComplete = useCallback(
|
|
130
141
|
(result: OnboardingResult) => {
|
|
131
|
-
if (storage) clearPersistedSession(storage, storageKey);
|
|
142
|
+
if (storage && shouldClearOnComplete(retainSessionOnComplete)) clearPersistedSession(storage, storageKey);
|
|
132
143
|
onComplete(result);
|
|
133
144
|
},
|
|
134
|
-
[storage, storageKey, onComplete],
|
|
145
|
+
[storage, storageKey, retainSessionOnComplete, onComplete],
|
|
135
146
|
);
|
|
136
147
|
|
|
137
148
|
const sessionId = session?.id ?? "";
|
|
@@ -224,10 +224,25 @@ export const createEventQueue = (options: EventQueueOptions): EventQueue => {
|
|
|
224
224
|
}
|
|
225
225
|
})();
|
|
226
226
|
|
|
227
|
-
// The
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
// The outcome of one POST attempt:
|
|
228
|
+
// "ok" → the server accepted the batch (2xx) → dequeue it.
|
|
229
|
+
// "drop" → a BATCH-SPECIFIC permanent error (this batch will never be accepted no matter how
|
|
230
|
+
// often we retry) → discard it and continue, so it can't block the good events behind
|
|
231
|
+
// it (head-of-line). See DROP_STATUSES.
|
|
232
|
+
// "retry" → transient or tenant-wide (network/5xx/429/auth) → keep the batch and back off.
|
|
233
|
+
type PostResult = "ok" | "drop" | "retry";
|
|
234
|
+
|
|
235
|
+
// Statuses where retrying THIS batch is futile because the batch itself is the problem — a
|
|
236
|
+
// malformed body (400/422), a too-large body (413), or a wrong route (404). Dropping a poison
|
|
237
|
+
// batch is what stops it from stalling the whole backlog. Deliberately NOT here: 401/403 (auth is
|
|
238
|
+
// tenant-wide, not batch-specific — dropping would silently lose EVERY event on a recoverable
|
|
239
|
+
// credential blip, so we keep retrying/pausing instead) and 429/5xx (transient).
|
|
240
|
+
const DROP_STATUSES = new Set([400, 404, 413, 422]);
|
|
241
|
+
|
|
242
|
+
// The queue's OWN awaited POST. Classifies the response to drive dequeue/drop/retry. NEVER throws —
|
|
243
|
+
// a missing fetch, a rejecting network, or an abort resolves to "retry" (batch stays, retry schedules).
|
|
244
|
+
const postBatch = async (events: ClientEvent[]): Promise<PostResult> => {
|
|
245
|
+
if (!target?.serverUrl || events.length === 0) return "retry";
|
|
231
246
|
const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
|
|
232
247
|
const timer = setTimeout(() => controller?.abort(), 15_000);
|
|
233
248
|
try {
|
|
@@ -240,9 +255,12 @@ export const createEventQueue = (options: EventQueueOptions): EventQueue => {
|
|
|
240
255
|
body: JSON.stringify({ events }),
|
|
241
256
|
signal: controller?.signal,
|
|
242
257
|
});
|
|
243
|
-
|
|
258
|
+
if (res && (res as { ok?: boolean }).ok) return "ok";
|
|
259
|
+
const status = (res as { status?: number } | undefined)?.status;
|
|
260
|
+
if (typeof status === "number" && DROP_STATUSES.has(status)) return "drop";
|
|
261
|
+
return "retry";
|
|
244
262
|
} catch {
|
|
245
|
-
return
|
|
263
|
+
return "retry";
|
|
246
264
|
} finally {
|
|
247
265
|
clearTimeout(timer);
|
|
248
266
|
}
|
|
@@ -280,12 +298,13 @@ export const createEventQueue = (options: EventQueueOptions): EventQueue => {
|
|
|
280
298
|
try {
|
|
281
299
|
while (pending.length > 0) {
|
|
282
300
|
const batch = pending.slice(0, batchSize);
|
|
283
|
-
const
|
|
284
|
-
if (
|
|
301
|
+
const result = await postBatch(batch.map((item) => item.event));
|
|
302
|
+
if (result === "retry") {
|
|
285
303
|
scheduleRetry();
|
|
286
304
|
return;
|
|
287
305
|
}
|
|
288
|
-
//
|
|
306
|
+
// "ok" (accepted) or "drop" (poison batch discarded so it can't block the rest): remove
|
|
307
|
+
// exactly this batch by id (pending may have grown while in flight) and keep draining.
|
|
289
308
|
const acked = new Set(batch.map((item) => item.id));
|
|
290
309
|
pending = pending.filter((item) => !acked.has(item.id));
|
|
291
310
|
persist();
|
|
@@ -75,7 +75,15 @@ export const fetchQuestionnaireDecision = async (
|
|
|
75
75
|
const res = await fetch(url, { headers });
|
|
76
76
|
if (!res || !res.ok) return null;
|
|
77
77
|
const json = (await res.json()) as QuestionnaireDecisionResponse | null;
|
|
78
|
-
|
|
78
|
+
// A body without a boolean `fire` is not a decision — guard it exactly as the mirror
|
|
79
|
+
// `fetchReviewDecision` does. Without this, a malformed 2xx body flows straight through:
|
|
80
|
+
// `{}` reads as a truthy verdict whose `fire` is undefined (silently "never fire", killing
|
|
81
|
+
// a questionnaire the local rules would have shown), and a stringy `{fire:"yes"}` reads as
|
|
82
|
+
// FIRE while carrying no `questionnaire`, so the host renders a gate with an undefined
|
|
83
|
+
// definition. Only a genuine boolean-`fire` body is a decision; everything else → null (the
|
|
84
|
+
// server has no opinion → the local rules stand).
|
|
85
|
+
if (!json || typeof json.fire !== "boolean") return null;
|
|
86
|
+
return json;
|
|
79
87
|
} catch {
|
|
80
88
|
/* unreachable / non-2xx / bad JSON / missing-fetch - never show */
|
|
81
89
|
return null;
|
|
@@ -141,3 +141,20 @@ export const clearPersistedSession = (storage: WireOnboardingStorage, key: strin
|
|
|
141
141
|
// Best-effort.
|
|
142
142
|
}
|
|
143
143
|
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The opt-in decision for `WireOnboarding.handleComplete`: given the host's
|
|
147
|
+
* `retainSessionOnComplete` opt-in, whether a completion should CLEAR the persisted seed.
|
|
148
|
+
* (The caller still gates on `storage` being present — this only encodes the opt-in axis, so the
|
|
149
|
+
* legacy `if (storage) clearPersistedSession(...)` guard stays byte-for-byte for a host that does
|
|
150
|
+
* not opt in.)
|
|
151
|
+
*
|
|
152
|
+
* - undefined / false (the default) → `true`: clear on completion, so the NEXT onboarding on this
|
|
153
|
+
* device mints a fresh session. This is the legacy single-stage behavior (e.g. Myelino).
|
|
154
|
+
* - true → `false`: LEAVE the seed in place so a same-signup re-entry/remount within the TTL
|
|
155
|
+
* resumes the SAME session (one funnel start) instead of minting a phantom second `started`.
|
|
156
|
+
* Freshness for a genuinely new run is then governed by the TTL (past `sessionTtlMs` → fresh
|
|
157
|
+
* mint) and an explicit new-run signal (a changed `persistKey`).
|
|
158
|
+
*/
|
|
159
|
+
export const shouldClearOnComplete = (retainSessionOnComplete: boolean | undefined): boolean =>
|
|
160
|
+
!retainSessionOnComplete;
|
package/src/types.ts
CHANGED
|
@@ -211,6 +211,22 @@ export type WireOnboardingProps = {
|
|
|
211
211
|
* accounts mid-flow. Only meaningful with `storage`.
|
|
212
212
|
*/
|
|
213
213
|
persistKey?: string;
|
|
214
|
+
/**
|
|
215
|
+
* OPT-IN: keep the persisted session seed alive ACROSS completion, so a multi-stage or
|
|
216
|
+
* replay-after-complete re-entry WITHIN one signup (typically a `key=` remount) resumes the
|
|
217
|
+
* SAME session instead of minting a fresh `metadata.sessionId` — which the backend would adopt
|
|
218
|
+
* as a second `contextId`, double-counting a `session_started` on the funnel.
|
|
219
|
+
*
|
|
220
|
+
* Default (omitted / `false`): a completion CLEARS the seed, so the next onboarding on this
|
|
221
|
+
* device starts fresh — the legacy single-stage behavior. Leave it unset and NOTHING changes.
|
|
222
|
+
*
|
|
223
|
+
* When `true`: the seed survives completion, and freshness for a genuinely new run is governed
|
|
224
|
+
* by the TTL (past `sessionTtlMs` → a fresh seed is minted) and an explicit new-run signal
|
|
225
|
+
* (scope a new `persistKey`, e.g. a per-signup id, to force a fresh seed within the TTL).
|
|
226
|
+
*
|
|
227
|
+
* Only meaningful with `storage`. Dropped/degraded sessions are unaffected (they never clear).
|
|
228
|
+
*/
|
|
229
|
+
retainSessionOnComplete?: boolean;
|
|
214
230
|
};
|
|
215
231
|
|
|
216
232
|
/** Backend-supplied progress, read off `response.props.progress` when present. */
|