@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,6 +1,6 @@
1
1
  import React__default from 'react';
2
2
  import { ImageSourcePropType } from 'react-native';
3
- import { G as GestureKind, C as CoachmarkStorage } from '../types-BcmagF6K.mjs';
3
+ import { G as GestureKind, C as CoachmarkStorage } from '../types-D_0B0yay.mjs';
4
4
  import { O as OnboardingTheme } from '../types-BKfpdZzX.mjs';
5
5
  import { a as WireFeatures, b as WireFeaturesConfig } from '../types-BpwiRpA8.mjs';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import React__default from 'react';
2
2
  import { ImageSourcePropType } from 'react-native';
3
- import { G as GestureKind, C as CoachmarkStorage } from '../types-BcmagF6K.js';
3
+ import { G as GestureKind, C as CoachmarkStorage } from '../types-D_0B0yay.js';
4
4
  import { O as OnboardingTheme } from '../types-BKfpdZzX.js';
5
5
  import { a as WireFeatures, b as WireFeaturesConfig } from '../types-BpwiRpA8.js';
6
6
 
@@ -389,9 +389,54 @@ var getCoachmarkStorage = () => coachmarkRuntime().storage;
389
389
  var isCoachmarkTesting = () => coachmarkRuntime().testing;
390
390
  var showcaseGateKey = (showcaseId) => `wire_showcase_${showcaseId}_seen`;
391
391
  var SEEN_VALUE = "1";
392
+ var GATE_PROBE_KEY = "wire_gate_probe";
393
+ var ASYNC_GATE_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
394
+ "@wireai/activation:asyncGateStorageWarned"
395
+ );
396
+ var asyncGateWarnGlobal = globalThis;
397
+ var closedGateStorage = Object.freeze({
398
+ getItem: () => SEEN_VALUE,
399
+ setItem: () => {
400
+ }
401
+ });
402
+ var syncGateAdapters = /* @__PURE__ */ new WeakSet();
403
+ var asyncGateAdapters = /* @__PURE__ */ new WeakSet();
404
+ var warnAsyncGateStorage = () => {
405
+ if (asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT]) return;
406
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
407
+ if (typeof console === "undefined" || !console.warn) return;
408
+ asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT] = true;
409
+ console.warn(
410
+ "[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."
411
+ );
412
+ };
413
+ var validateGateStorage = (storage) => {
414
+ if (!storage) return null;
415
+ if (syncGateAdapters.has(storage)) return storage;
416
+ if (asyncGateAdapters.has(storage)) return closedGateStorage;
417
+ let probed;
418
+ try {
419
+ probed = storage.getItem(GATE_PROBE_KEY);
420
+ } catch {
421
+ syncGateAdapters.add(storage);
422
+ return storage;
423
+ }
424
+ if (probed != null && typeof probed.then === "function") {
425
+ asyncGateAdapters.add(storage);
426
+ try {
427
+ probed.then(void 0, () => {
428
+ });
429
+ } catch {
430
+ }
431
+ warnAsyncGateStorage();
432
+ return closedGateStorage;
433
+ }
434
+ syncGateAdapters.add(storage);
435
+ return storage;
436
+ };
392
437
  var hasSeenGate = (key, storageOverride, testingOverride) => {
393
438
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return false;
394
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
439
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
395
440
  if (!storage) return false;
396
441
  try {
397
442
  return storage.getItem(key) === SEEN_VALUE;
@@ -401,7 +446,7 @@ var hasSeenGate = (key, storageOverride, testingOverride) => {
401
446
  };
402
447
  var markSeenGate = (key, storageOverride, testingOverride) => {
403
448
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return;
404
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
449
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
405
450
  if (!storage) return;
406
451
  try {
407
452
  storage.setItem(key, SEEN_VALUE);
@@ -489,17 +534,31 @@ var failOpen = async (storage, key) => {
489
534
  const cached3 = await readCachedFeatures(storage, key);
490
535
  return (_a = cached3 == null ? void 0 : cached3.features) != null ? _a : defaultWireFeatures;
491
536
  };
492
- var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
537
+ var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
538
+ var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
493
539
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
494
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
495
- try {
496
- return await fetch(url, {
540
+ let timer;
541
+ const expired = new Promise((resolve) => {
542
+ timer = setTimeout(() => {
543
+ controller == null ? void 0 : controller.abort();
544
+ resolve(NO_ANSWER);
545
+ }, timeoutMs);
546
+ });
547
+ const exchange = (async () => {
548
+ const res = await fetch(url, {
497
549
  method: "GET",
498
550
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
499
551
  signal: controller == null ? void 0 : controller.signal
500
552
  });
553
+ if (!res || !res.ok) return NO_ANSWER;
554
+ return await res.json();
555
+ })();
556
+ try {
557
+ return await Promise.race([exchange, expired]);
501
558
  } finally {
502
559
  clearTimeout(timer);
560
+ void exchange.catch(() => {
561
+ });
503
562
  }
504
563
  };
505
564
  var fetchWireFeatures = async (config) => {
@@ -509,9 +568,8 @@ var fetchWireFeatures = async (config) => {
509
568
  const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
510
569
  const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
511
570
  try {
512
- const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
513
- if (!res || !res.ok) return failOpen(storage, key);
514
- const json = await res.json();
571
+ const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
572
+ if (json === NO_ANSWER) return failOpen(storage, key);
515
573
  const features = parseWireFeatures(json);
516
574
  if (storage) writeCachedFeatures(storage, key, features);
517
575
  return features;
@@ -527,41 +585,56 @@ var useStableValue = (value, isEqual) => {
527
585
  return ref.current;
528
586
  };
529
587
  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);
530
- var useWireFeatures = (config) => {
588
+ var useWireFeaturesState = (config) => {
531
589
  const stableConfig = useStableValue(config, sameFetchKey);
532
590
  const [flags, setFlags] = React.useState(defaultWireFeatures);
533
591
  const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
592
+ const [settled, setSettled] = React.useState(!canFetch);
534
593
  React.useEffect(() => {
535
594
  if (!canFetch) {
536
595
  setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
596
+ setSettled(true);
537
597
  return;
538
598
  }
539
599
  let alive = true;
540
600
  void fetchWireFeatures(stableConfig).then((next) => {
541
601
  if (!alive) return;
542
602
  setFlags((prev) => featuresEqual(prev, next) ? prev : next);
603
+ setSettled(true);
543
604
  });
544
605
  return () => {
545
606
  alive = false;
546
607
  };
547
608
  }, [canFetch, stableConfig]);
548
- return flags;
609
+ return React.useMemo(() => ({ flags, settled }), [flags, settled]);
549
610
  };
550
611
  var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
551
- var globalObj = globalThis;
552
- if (!globalObj[CONTEXT_SYMBOL]) {
553
- globalObj[CONTEXT_SYMBOL] = React.createContext(null);
612
+ var featuresContextGlobal = globalThis;
613
+ if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
614
+ featuresContextGlobal[CONTEXT_SYMBOL] = React.createContext(null);
554
615
  }
555
- var WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
616
+ var WireFeaturesContext = featuresContextGlobal[CONTEXT_SYMBOL];
617
+ var SETTLED_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.settled.context");
618
+ var settledContextGlobal = globalThis;
619
+ if (!settledContextGlobal[SETTLED_SYMBOL]) {
620
+ settledContextGlobal[SETTLED_SYMBOL] = React.createContext(true);
621
+ }
622
+ var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
556
623
  var useWireFeaturesContext = () => React.useContext(WireFeaturesContext);
557
- var useResolvedFeatures = (options) => {
624
+ var useResolvedFeaturesState = (options) => {
558
625
  var _a, _b, _c;
559
626
  const ctx = useWireFeaturesContext();
627
+ const ctxSettled = React.useContext(WireFeaturesSettledContext);
560
628
  const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
561
- const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
562
- return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
629
+ const fetched = useWireFeaturesState(shouldFetch ? options == null ? void 0 : options.config : void 0);
630
+ const flags = (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched.flags) != null ? _c : defaultWireFeatures;
631
+ const settled = (options == null ? void 0 : options.flags) ? true : ctx != null ? ctxSettled : fetched.settled;
632
+ return React.useMemo(() => ({ flags, settled }), [flags, settled]);
563
633
  };
564
634
 
635
+ // src/session/persistedSession.ts
636
+ var READ_TIMEOUT_MS2 = 1500;
637
+
565
638
  // src/theme/defaultTheme.ts
566
639
  var defaultOnboardingTheme = {
567
640
  colors: {
@@ -721,7 +794,10 @@ var _FeatureShowcase = ({
721
794
  [active, themeOverride]
722
795
  );
723
796
  const accent = accentColor != null ? accentColor : t.colors.primary;
724
- const flags = useResolvedFeatures({ flags: features, config: featuresConfig });
797
+ const { flags, settled } = useResolvedFeaturesState({
798
+ flags: features,
799
+ config: featuresConfig
800
+ });
725
801
  const disabled = !flags.showcase.enabled;
726
802
  const Onboarding = React.useMemo(() => resolveShowcaseOnboarding(), []);
727
803
  const gateKey = showcaseGateKey(config.id);
@@ -730,6 +806,14 @@ var _FeatureShowcase = ({
730
806
  [gateKey, storage, isTesting]
731
807
  );
732
808
  const skip = seen || disabled || !Onboarding;
809
+ const flagsCouldMatter = !seen && !!Onboarding;
810
+ const [holdExpired, setHoldExpired] = React.useState(false);
811
+ React.useEffect(() => {
812
+ if (settled || holdExpired || !flagsCouldMatter) return void 0;
813
+ const timer = setTimeout(() => setHoldExpired(true), READ_TIMEOUT_MS2);
814
+ return () => clearTimeout(timer);
815
+ }, [settled, holdExpired, flagsCouldMatter]);
816
+ const holding = flagsCouldMatter && !settled && !holdExpired;
733
817
  const doneRef = React.useRef(false);
734
818
  const finish = React.useCallback(() => {
735
819
  if (doneRef.current) return;
@@ -738,11 +822,12 @@ var _FeatureShowcase = ({
738
822
  onDone();
739
823
  }, [gateKey, storage, isTesting, onDone]);
740
824
  React.useEffect(() => {
825
+ if (holding) return;
741
826
  if (skip && !doneRef.current) {
742
827
  doneRef.current = true;
743
828
  onDone();
744
829
  }
745
- }, [skip, onDone]);
830
+ }, [holding, skip, onDone]);
746
831
  const [activeIndex, setActiveIndex] = React.useState(0);
747
832
  const steps = React.useMemo(
748
833
  () => config.slides.map((slide) => {
@@ -809,6 +894,7 @@ var _FeatureShowcase = ({
809
894
  }),
810
895
  [t]
811
896
  );
897
+ if (holding) return null;
812
898
  if (skip || !Onboarding) return null;
813
899
  const activeGesture = (_a = config.slides[activeIndex]) == null ? void 0 : _a.gesture;
814
900
  const showGesture = !!activeGesture && activeGesture !== "none" && activeGesture !== "tap" && activeGesture !== "double_tap";