@wireai/activation 0.15.0 → 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.
- package/AGENTS.md +95 -20
- package/CHANGELOG.md +707 -0
- package/INTEGRATION_PROMPT.md +61 -23
- package/README.md +100 -25
- package/dist/analytics/index.d.mts +32 -10
- package/dist/analytics/index.d.ts +32 -10
- package/dist/analytics/index.js +288 -127
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +288 -127
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.d.mts +14 -0
- package/dist/coachmarks/index.d.ts +14 -0
- package/dist/coachmarks/index.js +73 -20
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +73 -20
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{currentSession-orZy5p1e.d.mts → currentSession-Bz7G6lno.d.mts} +25 -35
- package/dist/{currentSession-CFSRZ2wg.d.ts → currentSession-z-CZ55ad.d.ts} +25 -35
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +125 -36
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +125 -36
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +0 -13
- package/dist/questionnaire/index.d.ts +0 -13
- package/dist/questionnaire/index.js +154 -43
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +155 -44
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.js +159 -91
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +160 -92
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.js +59 -18
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs +60 -19
- package/dist/showcase/index.mjs.map +1 -1
- package/llms.txt +9 -9
- package/package.json +6 -9
- package/src/analytics/currentSession.ts +141 -4
- package/src/analytics/index.ts +6 -1
- package/src/analytics/reportClientEvent.ts +19 -10
- package/src/analytics/useAnalytics.ts +74 -15
- package/src/analytics/wireDoctor.ts +152 -7
- package/src/coachmarks/CoachmarkProvider.tsx +26 -5
- package/src/coachmarks/runtime.ts +53 -0
- package/src/coachmarks/useCoachmarkTour.ts +51 -1
- package/src/context/deviceId.ts +49 -15
- package/src/features/WireFeaturesProvider.tsx +72 -12
- package/src/features/fetchWireFeatures.ts +49 -11
- package/src/features/useWireFeatures.ts +39 -3
- package/src/identity/identityRecord.ts +15 -2
- package/src/questionnaire/QuestionnaireGate.tsx +40 -1
- package/src/questionnaire/transport.ts +22 -8
- package/src/questionnaire/useQuestionnaireGate.ts +58 -7
- package/src/reviews/ReviewGate.tsx +39 -0
- package/src/reviews/idempotency.ts +38 -0
- package/src/reviews/runtime.ts +39 -10
- package/src/reviews/transport.ts +22 -8
- package/src/reviews/useReviewGate.ts +57 -7
- package/src/session-analytics/lifecycle.ts +16 -0
- package/src/session-analytics/useLifecycleEvents.ts +30 -2
- package/src/session-analytics/useSessionStart.ts +22 -2
- package/src/showcase/FeatureShowcase.tsx +50 -3
- package/src/types.ts +5 -4
- package/src/utils/withDeadline.ts +70 -0
|
@@ -29,6 +29,11 @@ interface CoachmarkProviderProps {
|
|
|
29
29
|
* pre-resolved `features`, OR a `featuresConfig` (serverUrl + apiKey) to lazily fetch once, OR
|
|
30
30
|
* mount a `WireFeaturesProvider` above (context is read automatically). Omit all three and
|
|
31
31
|
* coachmarks stay on (fail-open) — zero behavior change for hosts that never adopt flags.
|
|
32
|
+
*
|
|
33
|
+
* While a fetch is still in flight, a NEW tour does not arm (so a tenant who switched the module
|
|
34
|
+
* off gets no first impression), and a tour already in flight is never torn down. Nothing is
|
|
35
|
+
* spent by that wait: no once-gate is consumed, so an ON answer plays the tour in full. A failed
|
|
36
|
+
* fetch is an answer — fail-open is unchanged.
|
|
32
37
|
*/
|
|
33
38
|
features?: WireFeatures;
|
|
34
39
|
/** Lazy-fetch config for the flags, used when `features` is absent and no provider is above. */
|
|
@@ -130,6 +135,15 @@ interface UseCoachmarkTourOptions {
|
|
|
130
135
|
* `show()` is a no-op while the flag is off, so a tour that carried on would paint nothing, could
|
|
131
136
|
* not be tapped to advance, and would never reach a terminal state.
|
|
132
137
|
*
|
|
138
|
+
* ARM vs TERMINAL — the third gate, and why it is not the second one. Until the tenant's flags
|
|
139
|
+
* land, that kill switch reads ON because nobody has asked yet, so a tenant who turned coachmarks
|
|
140
|
+
* OFF still got the first tour of every cold start. The wait CANNOT be expressed by writing the
|
|
141
|
+
* kill switch false, precisely because that flag is terminal: it would end the tour of every
|
|
142
|
+
* ENABLED tenant on every cold start, a worse defect than the one being fixed. So an UNANSWERED
|
|
143
|
+
* verdict (`areCoachmarksResolved`, written by CoachmarkProvider from the features' `settled`)
|
|
144
|
+
* blocks only the ARM below — a tour already in flight is never touched by it — and costs nothing
|
|
145
|
+
* while it waits: no timer, no overlay, no once-gate spent.
|
|
146
|
+
*
|
|
133
147
|
* IMPORTANT: `steps` MUST be a stable (memoized) array. If a new array identity
|
|
134
148
|
* is passed on every render the drive effect re-runs and re-shows the current
|
|
135
149
|
* step (wasteful anchor re-resolves / overlay churn), and analytics can
|
|
@@ -29,6 +29,11 @@ interface CoachmarkProviderProps {
|
|
|
29
29
|
* pre-resolved `features`, OR a `featuresConfig` (serverUrl + apiKey) to lazily fetch once, OR
|
|
30
30
|
* mount a `WireFeaturesProvider` above (context is read automatically). Omit all three and
|
|
31
31
|
* coachmarks stay on (fail-open) — zero behavior change for hosts that never adopt flags.
|
|
32
|
+
*
|
|
33
|
+
* While a fetch is still in flight, a NEW tour does not arm (so a tenant who switched the module
|
|
34
|
+
* off gets no first impression), and a tour already in flight is never torn down. Nothing is
|
|
35
|
+
* spent by that wait: no once-gate is consumed, so an ON answer plays the tour in full. A failed
|
|
36
|
+
* fetch is an answer — fail-open is unchanged.
|
|
32
37
|
*/
|
|
33
38
|
features?: WireFeatures;
|
|
34
39
|
/** Lazy-fetch config for the flags, used when `features` is absent and no provider is above. */
|
|
@@ -130,6 +135,15 @@ interface UseCoachmarkTourOptions {
|
|
|
130
135
|
* `show()` is a no-op while the flag is off, so a tour that carried on would paint nothing, could
|
|
131
136
|
* not be tapped to advance, and would never reach a terminal state.
|
|
132
137
|
*
|
|
138
|
+
* ARM vs TERMINAL — the third gate, and why it is not the second one. Until the tenant's flags
|
|
139
|
+
* land, that kill switch reads ON because nobody has asked yet, so a tenant who turned coachmarks
|
|
140
|
+
* OFF still got the first tour of every cold start. The wait CANNOT be expressed by writing the
|
|
141
|
+
* kill switch false, precisely because that flag is terminal: it would end the tour of every
|
|
142
|
+
* ENABLED tenant on every cold start, a worse defect than the one being fixed. So an UNANSWERED
|
|
143
|
+
* verdict (`areCoachmarksResolved`, written by CoachmarkProvider from the features' `settled`)
|
|
144
|
+
* blocks only the ARM below — a tour already in flight is never touched by it — and costs nothing
|
|
145
|
+
* while it waits: no timer, no overlay, no once-gate spent.
|
|
146
|
+
*
|
|
133
147
|
* IMPORTANT: `steps` MUST be a stable (memoized) array. If a new array identity
|
|
134
148
|
* is passed on every render the drive effect re-runs and re-shows the current
|
|
135
149
|
* step (wasteful anchor re-resolves / overlay churn), and analytics can
|
package/dist/coachmarks/index.js
CHANGED
|
@@ -95,17 +95,31 @@ var failOpen = async (storage, key) => {
|
|
|
95
95
|
const cached3 = await readCachedFeatures(storage, key);
|
|
96
96
|
return (_a = cached3 == null ? void 0 : cached3.features) != null ? _a : defaultWireFeatures;
|
|
97
97
|
};
|
|
98
|
-
var
|
|
98
|
+
var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
|
|
99
|
+
var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
|
|
99
100
|
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
let timer;
|
|
102
|
+
const expired = new Promise((resolve) => {
|
|
103
|
+
timer = setTimeout(() => {
|
|
104
|
+
controller == null ? void 0 : controller.abort();
|
|
105
|
+
resolve(NO_ANSWER);
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
});
|
|
108
|
+
const exchange = (async () => {
|
|
109
|
+
const res = await fetch(url, {
|
|
103
110
|
method: "GET",
|
|
104
111
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
105
112
|
signal: controller == null ? void 0 : controller.signal
|
|
106
113
|
});
|
|
114
|
+
if (!res || !res.ok) return NO_ANSWER;
|
|
115
|
+
return await res.json();
|
|
116
|
+
})();
|
|
117
|
+
try {
|
|
118
|
+
return await Promise.race([exchange, expired]);
|
|
107
119
|
} finally {
|
|
108
120
|
clearTimeout(timer);
|
|
121
|
+
void exchange.catch(() => {
|
|
122
|
+
});
|
|
109
123
|
}
|
|
110
124
|
};
|
|
111
125
|
var fetchWireFeatures = async (config) => {
|
|
@@ -115,9 +129,8 @@ var fetchWireFeatures = async (config) => {
|
|
|
115
129
|
const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
|
|
116
130
|
const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
117
131
|
try {
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
const json = await res.json();
|
|
132
|
+
const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
|
|
133
|
+
if (json === NO_ANSWER) return failOpen(storage, key);
|
|
121
134
|
const features = parseWireFeatures(json);
|
|
122
135
|
if (storage) writeCachedFeatures(storage, key, features);
|
|
123
136
|
return features;
|
|
@@ -133,39 +146,51 @@ var useStableValue = (value, isEqual) => {
|
|
|
133
146
|
return ref.current;
|
|
134
147
|
};
|
|
135
148
|
var sameFetchKey = (a, b) => (a == null ? void 0 : a.serverUrl) === (b == null ? void 0 : b.serverUrl) && (a == null ? void 0 : a.apiKey) === (b == null ? void 0 : b.apiKey) && (a == null ? void 0 : a.appId) === (b == null ? void 0 : b.appId);
|
|
136
|
-
var
|
|
149
|
+
var useWireFeaturesState = (config) => {
|
|
137
150
|
const stableConfig = useStableValue(config, sameFetchKey);
|
|
138
151
|
const [flags, setFlags] = React3.useState(defaultWireFeatures);
|
|
139
152
|
const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
|
|
153
|
+
const [settled, setSettled] = React3.useState(!canFetch);
|
|
140
154
|
React3.useEffect(() => {
|
|
141
155
|
if (!canFetch) {
|
|
142
156
|
setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
|
|
157
|
+
setSettled(true);
|
|
143
158
|
return;
|
|
144
159
|
}
|
|
145
160
|
let alive = true;
|
|
146
161
|
void fetchWireFeatures(stableConfig).then((next) => {
|
|
147
162
|
if (!alive) return;
|
|
148
163
|
setFlags((prev) => featuresEqual(prev, next) ? prev : next);
|
|
164
|
+
setSettled(true);
|
|
149
165
|
});
|
|
150
166
|
return () => {
|
|
151
167
|
alive = false;
|
|
152
168
|
};
|
|
153
169
|
}, [canFetch, stableConfig]);
|
|
154
|
-
return flags;
|
|
170
|
+
return React3.useMemo(() => ({ flags, settled }), [flags, settled]);
|
|
155
171
|
};
|
|
156
172
|
var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
|
|
157
|
-
var
|
|
158
|
-
if (!
|
|
159
|
-
|
|
173
|
+
var featuresContextGlobal = globalThis;
|
|
174
|
+
if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
|
|
175
|
+
featuresContextGlobal[CONTEXT_SYMBOL] = React3.createContext(null);
|
|
160
176
|
}
|
|
161
|
-
var WireFeaturesContext =
|
|
177
|
+
var WireFeaturesContext = featuresContextGlobal[CONTEXT_SYMBOL];
|
|
178
|
+
var SETTLED_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.settled.context");
|
|
179
|
+
var settledContextGlobal = globalThis;
|
|
180
|
+
if (!settledContextGlobal[SETTLED_SYMBOL]) {
|
|
181
|
+
settledContextGlobal[SETTLED_SYMBOL] = React3.createContext(true);
|
|
182
|
+
}
|
|
183
|
+
var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
|
|
162
184
|
var useWireFeaturesContext = () => React3.useContext(WireFeaturesContext);
|
|
163
|
-
var
|
|
185
|
+
var useResolvedFeaturesState = (options) => {
|
|
164
186
|
var _a, _b, _c;
|
|
165
187
|
const ctx = useWireFeaturesContext();
|
|
188
|
+
const ctxSettled = React3.useContext(WireFeaturesSettledContext);
|
|
166
189
|
const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
|
|
167
|
-
const fetched =
|
|
168
|
-
|
|
190
|
+
const fetched = useWireFeaturesState(shouldFetch ? options == null ? void 0 : options.config : void 0);
|
|
191
|
+
const flags = (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched.flags) != null ? _c : defaultWireFeatures;
|
|
192
|
+
const settled = (options == null ? void 0 : options.flags) ? true : ctx != null ? ctxSettled : fetched.settled;
|
|
193
|
+
return React3.useMemo(() => ({ flags, settled }), [flags, settled]);
|
|
169
194
|
};
|
|
170
195
|
|
|
171
196
|
// src/coachmarks/runtime.ts
|
|
@@ -201,6 +226,17 @@ var setCoachmarksEnabled = (value) => {
|
|
|
201
226
|
notifyCoachmarksEnabled();
|
|
202
227
|
};
|
|
203
228
|
var areCoachmarksEnabled = () => coachmarkRuntime().coachmarksEnabled;
|
|
229
|
+
var setCoachmarksResolved = (value) => {
|
|
230
|
+
var _a;
|
|
231
|
+
const runtime = coachmarkRuntime();
|
|
232
|
+
if (((_a = runtime.coachmarksResolved) != null ? _a : true) === value) return;
|
|
233
|
+
runtime.coachmarksResolved = value;
|
|
234
|
+
notifyCoachmarksEnabled();
|
|
235
|
+
};
|
|
236
|
+
var areCoachmarksResolved = () => {
|
|
237
|
+
var _a;
|
|
238
|
+
return (_a = coachmarkRuntime().coachmarksResolved) != null ? _a : true;
|
|
239
|
+
};
|
|
204
240
|
var subscribeCoachmarksEnabled = (listener) => {
|
|
205
241
|
const listeners2 = enabledListeners();
|
|
206
242
|
listeners2.add(listener);
|
|
@@ -1013,22 +1049,28 @@ var CoachmarkProvider = ({
|
|
|
1013
1049
|
featuresConfig,
|
|
1014
1050
|
children
|
|
1015
1051
|
}) => {
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1052
|
+
const { flags, settled } = useResolvedFeaturesState({
|
|
1053
|
+
flags: features,
|
|
1054
|
+
config: featuresConfig
|
|
1055
|
+
});
|
|
1056
|
+
const coachmarksOn = settled ? flags.coachmarks.enabled : true;
|
|
1018
1057
|
setCoachmarkStorage(storage);
|
|
1019
1058
|
setCoachmarkTesting(isTestingCoachmark);
|
|
1059
|
+
setCoachmarksResolved(settled);
|
|
1020
1060
|
setCoachmarksEnabled(coachmarksOn);
|
|
1021
1061
|
React3.useEffect(() => {
|
|
1022
1062
|
setCoachmarkStorage(storage);
|
|
1023
1063
|
setCoachmarkTesting(isTestingCoachmark);
|
|
1064
|
+
setCoachmarksResolved(settled);
|
|
1024
1065
|
setCoachmarksEnabled(coachmarksOn);
|
|
1025
1066
|
return () => {
|
|
1026
1067
|
if (getCoachmarkStorage() === storage) {
|
|
1027
1068
|
setCoachmarkStorage(null);
|
|
1028
1069
|
}
|
|
1029
1070
|
setCoachmarksEnabled(true);
|
|
1071
|
+
setCoachmarksResolved(true);
|
|
1030
1072
|
};
|
|
1031
|
-
}, [storage, isTestingCoachmark, coachmarksOn]);
|
|
1073
|
+
}, [storage, isTestingCoachmark, coachmarksOn, settled]);
|
|
1032
1074
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1033
1075
|
children,
|
|
1034
1076
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1131,6 +1173,11 @@ var useCoachmarkTour = (steps, {
|
|
|
1131
1173
|
areCoachmarksEnabled,
|
|
1132
1174
|
areCoachmarksEnabled
|
|
1133
1175
|
);
|
|
1176
|
+
const coachmarksResolved = React3.useSyncExternalStore(
|
|
1177
|
+
subscribeCoachmarksEnabled,
|
|
1178
|
+
areCoachmarksResolved,
|
|
1179
|
+
areCoachmarksResolved
|
|
1180
|
+
);
|
|
1134
1181
|
const exit = React3.useCallback(
|
|
1135
1182
|
(writeGate) => {
|
|
1136
1183
|
if (finishedRef.current) return;
|
|
@@ -1145,14 +1192,16 @@ var useCoachmarkTour = (steps, {
|
|
|
1145
1192
|
const finish = React3.useCallback(() => exit(true), [exit]);
|
|
1146
1193
|
React3.useEffect(() => {
|
|
1147
1194
|
if (!enabled || startedRef.current) return void 0;
|
|
1195
|
+
if (steps.length === 0) return void 0;
|
|
1148
1196
|
if (!coachmarksOn) return void 0;
|
|
1197
|
+
if (!coachmarksResolved) return void 0;
|
|
1149
1198
|
if (showOnce && hasSeenGate(coachmarkGateKey(tourId))) return void 0;
|
|
1150
1199
|
const timer = setTimeout(() => {
|
|
1151
1200
|
startedRef.current = true;
|
|
1152
1201
|
setActiveIndex(0);
|
|
1153
1202
|
}, startDelayMs);
|
|
1154
1203
|
return () => clearTimeout(timer);
|
|
1155
|
-
}, [enabled, coachmarksOn, startDelayMs, showOnce, tourId]);
|
|
1204
|
+
}, [enabled, coachmarksOn, coachmarksResolved, startDelayMs, showOnce, tourId, steps.length]);
|
|
1156
1205
|
React3.useEffect(() => {
|
|
1157
1206
|
if (activeIndex === null) return void 0;
|
|
1158
1207
|
if (!coachmarksOn) {
|
|
@@ -1163,6 +1212,10 @@ var useCoachmarkTour = (steps, {
|
|
|
1163
1212
|
if (startedRef.current && !finishedRef.current) coachmarkOverlay.hide();
|
|
1164
1213
|
return void 0;
|
|
1165
1214
|
}
|
|
1215
|
+
if (steps.length === 0) {
|
|
1216
|
+
exit(false);
|
|
1217
|
+
return void 0;
|
|
1218
|
+
}
|
|
1166
1219
|
if (activeIndex >= steps.length) {
|
|
1167
1220
|
finish();
|
|
1168
1221
|
return void 0;
|