@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
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
3
  import { a as WireFeatures, b as WireFeaturesConfig } from '../types-BpwiRpA8.mjs';
4
- import { C as CoachmarkStorage, a as CoachmarkStep, G as GestureKind, T as TargetRect, P as Placement } from '../types-BcmagF6K.mjs';
4
+ import { C as CoachmarkStorage, a as CoachmarkStep, G as GestureKind, T as TargetRect, P as Placement } from '../types-D_0B0yay.mjs';
5
5
  import { View } from 'react-native';
6
6
 
7
7
  interface CoachmarkProviderProps {
@@ -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
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
3
  import { a as WireFeatures, b as WireFeaturesConfig } from '../types-BpwiRpA8.js';
4
- import { C as CoachmarkStorage, a as CoachmarkStep, G as GestureKind, T as TargetRect, P as Placement } from '../types-BcmagF6K.js';
4
+ import { C as CoachmarkStorage, a as CoachmarkStep, G as GestureKind, T as TargetRect, P as Placement } from '../types-D_0B0yay.js';
5
5
  import { View } from 'react-native';
6
6
 
7
7
  interface CoachmarkProviderProps {
@@ -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
@@ -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 fetchWithTimeout = async (url, apiKey, timeoutMs) => {
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
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
101
- try {
102
- return await fetch(url, {
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 res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
119
- if (!res || !res.ok) return failOpen(storage, key);
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 useWireFeatures = (config) => {
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 globalObj = globalThis;
158
- if (!globalObj[CONTEXT_SYMBOL]) {
159
- globalObj[CONTEXT_SYMBOL] = React3.createContext(null);
173
+ var featuresContextGlobal = globalThis;
174
+ if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
175
+ featuresContextGlobal[CONTEXT_SYMBOL] = React3.createContext(null);
176
+ }
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);
160
182
  }
161
- var WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
183
+ var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
162
184
  var useWireFeaturesContext = () => React3.useContext(WireFeaturesContext);
163
- var useResolvedFeatures = (options) => {
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 = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
168
- return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
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);
@@ -219,9 +255,54 @@ var isCoachmarkTesting = () => coachmarkRuntime().testing;
219
255
  var coachmarkGateKey = (tourId) => `wire_coachmark_${tourId}_seen`;
220
256
  var showcaseGateKey = (showcaseId) => `wire_showcase_${showcaseId}_seen`;
221
257
  var SEEN_VALUE = "1";
258
+ var GATE_PROBE_KEY = "wire_gate_probe";
259
+ var ASYNC_GATE_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
260
+ "@wireai/activation:asyncGateStorageWarned"
261
+ );
262
+ var asyncGateWarnGlobal = globalThis;
263
+ var closedGateStorage = Object.freeze({
264
+ getItem: () => SEEN_VALUE,
265
+ setItem: () => {
266
+ }
267
+ });
268
+ var syncGateAdapters = /* @__PURE__ */ new WeakSet();
269
+ var asyncGateAdapters = /* @__PURE__ */ new WeakSet();
270
+ var warnAsyncGateStorage = () => {
271
+ if (asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT]) return;
272
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
273
+ if (typeof console === "undefined" || !console.warn) return;
274
+ asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT] = true;
275
+ console.warn(
276
+ "[wireai] the gate storage's getItem returned a Promise \u2014 an async adapter (e.g. AsyncStorage) cannot drive the once-per-user gates, so coachmark tours, showcases and the review/questionnaire gates are all DISABLED (fail closed: nothing shows, nothing fires) until a synchronous adapter is provided. Pass a sync adapter (e.g. MMKV: `{ getItem: (k) => mmkv.getString(k) ?? null, setItem: (k, v) => mmkv.set(k, v) }`) to CoachmarkProvider or the gate's `storage` prop."
277
+ );
278
+ };
279
+ var validateGateStorage = (storage) => {
280
+ if (!storage) return null;
281
+ if (syncGateAdapters.has(storage)) return storage;
282
+ if (asyncGateAdapters.has(storage)) return closedGateStorage;
283
+ let probed;
284
+ try {
285
+ probed = storage.getItem(GATE_PROBE_KEY);
286
+ } catch {
287
+ syncGateAdapters.add(storage);
288
+ return storage;
289
+ }
290
+ if (probed != null && typeof probed.then === "function") {
291
+ asyncGateAdapters.add(storage);
292
+ try {
293
+ probed.then(void 0, () => {
294
+ });
295
+ } catch {
296
+ }
297
+ warnAsyncGateStorage();
298
+ return closedGateStorage;
299
+ }
300
+ syncGateAdapters.add(storage);
301
+ return storage;
302
+ };
222
303
  var hasSeenGate = (key, storageOverride, testingOverride) => {
223
304
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return false;
224
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
305
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
225
306
  if (!storage) return false;
226
307
  try {
227
308
  return storage.getItem(key) === SEEN_VALUE;
@@ -231,7 +312,7 @@ var hasSeenGate = (key, storageOverride, testingOverride) => {
231
312
  };
232
313
  var markSeenGate = (key, storageOverride, testingOverride) => {
233
314
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return;
234
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
315
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
235
316
  if (!storage) return;
236
317
  try {
237
318
  storage.setItem(key, SEEN_VALUE);
@@ -968,22 +1049,28 @@ var CoachmarkProvider = ({
968
1049
  featuresConfig,
969
1050
  children
970
1051
  }) => {
971
- const flags = useResolvedFeatures({ flags: features, config: featuresConfig });
972
- const coachmarksOn = flags.coachmarks.enabled;
1052
+ const { flags, settled } = useResolvedFeaturesState({
1053
+ flags: features,
1054
+ config: featuresConfig
1055
+ });
1056
+ const coachmarksOn = settled ? flags.coachmarks.enabled : true;
973
1057
  setCoachmarkStorage(storage);
974
1058
  setCoachmarkTesting(isTestingCoachmark);
1059
+ setCoachmarksResolved(settled);
975
1060
  setCoachmarksEnabled(coachmarksOn);
976
1061
  React3.useEffect(() => {
977
1062
  setCoachmarkStorage(storage);
978
1063
  setCoachmarkTesting(isTestingCoachmark);
1064
+ setCoachmarksResolved(settled);
979
1065
  setCoachmarksEnabled(coachmarksOn);
980
1066
  return () => {
981
1067
  if (getCoachmarkStorage() === storage) {
982
1068
  setCoachmarkStorage(null);
983
1069
  }
984
1070
  setCoachmarksEnabled(true);
1071
+ setCoachmarksResolved(true);
985
1072
  };
986
- }, [storage, isTestingCoachmark, coachmarksOn]);
1073
+ }, [storage, isTestingCoachmark, coachmarksOn, settled]);
987
1074
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
988
1075
  children,
989
1076
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1086,6 +1173,11 @@ var useCoachmarkTour = (steps, {
1086
1173
  areCoachmarksEnabled,
1087
1174
  areCoachmarksEnabled
1088
1175
  );
1176
+ const coachmarksResolved = React3.useSyncExternalStore(
1177
+ subscribeCoachmarksEnabled,
1178
+ areCoachmarksResolved,
1179
+ areCoachmarksResolved
1180
+ );
1089
1181
  const exit = React3.useCallback(
1090
1182
  (writeGate) => {
1091
1183
  if (finishedRef.current) return;
@@ -1100,14 +1192,16 @@ var useCoachmarkTour = (steps, {
1100
1192
  const finish = React3.useCallback(() => exit(true), [exit]);
1101
1193
  React3.useEffect(() => {
1102
1194
  if (!enabled || startedRef.current) return void 0;
1195
+ if (steps.length === 0) return void 0;
1103
1196
  if (!coachmarksOn) return void 0;
1197
+ if (!coachmarksResolved) return void 0;
1104
1198
  if (showOnce && hasSeenGate(coachmarkGateKey(tourId))) return void 0;
1105
1199
  const timer = setTimeout(() => {
1106
1200
  startedRef.current = true;
1107
1201
  setActiveIndex(0);
1108
1202
  }, startDelayMs);
1109
1203
  return () => clearTimeout(timer);
1110
- }, [enabled, coachmarksOn, startDelayMs, showOnce, tourId]);
1204
+ }, [enabled, coachmarksOn, coachmarksResolved, startDelayMs, showOnce, tourId, steps.length]);
1111
1205
  React3.useEffect(() => {
1112
1206
  if (activeIndex === null) return void 0;
1113
1207
  if (!coachmarksOn) {
@@ -1118,6 +1212,10 @@ var useCoachmarkTour = (steps, {
1118
1212
  if (startedRef.current && !finishedRef.current) coachmarkOverlay.hide();
1119
1213
  return void 0;
1120
1214
  }
1215
+ if (steps.length === 0) {
1216
+ exit(false);
1217
+ return void 0;
1218
+ }
1121
1219
  if (activeIndex >= steps.length) {
1122
1220
  finish();
1123
1221
  return void 0;