@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,4 +1,4 @@
1
- import React, { forwardRef, createElement, createContext, useEffect, useMemo, useRef, useCallback, useState, useContext, useReducer } from 'react';
1
+ import React, { forwardRef, createElement, createContext, useEffect, useMemo, useState, useRef, useCallback, useContext, useReducer } from 'react';
2
2
  import { View, StyleSheet, AccessibilityInfo } from 'react-native';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
@@ -383,9 +383,54 @@ var getCoachmarkStorage = () => coachmarkRuntime().storage;
383
383
  var isCoachmarkTesting = () => coachmarkRuntime().testing;
384
384
  var showcaseGateKey = (showcaseId) => `wire_showcase_${showcaseId}_seen`;
385
385
  var SEEN_VALUE = "1";
386
+ var GATE_PROBE_KEY = "wire_gate_probe";
387
+ var ASYNC_GATE_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
388
+ "@wireai/activation:asyncGateStorageWarned"
389
+ );
390
+ var asyncGateWarnGlobal = globalThis;
391
+ var closedGateStorage = Object.freeze({
392
+ getItem: () => SEEN_VALUE,
393
+ setItem: () => {
394
+ }
395
+ });
396
+ var syncGateAdapters = /* @__PURE__ */ new WeakSet();
397
+ var asyncGateAdapters = /* @__PURE__ */ new WeakSet();
398
+ var warnAsyncGateStorage = () => {
399
+ if (asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT]) return;
400
+ if (typeof __DEV__ === "undefined" || !__DEV__) return;
401
+ if (typeof console === "undefined" || !console.warn) return;
402
+ asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT] = true;
403
+ console.warn(
404
+ "[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."
405
+ );
406
+ };
407
+ var validateGateStorage = (storage) => {
408
+ if (!storage) return null;
409
+ if (syncGateAdapters.has(storage)) return storage;
410
+ if (asyncGateAdapters.has(storage)) return closedGateStorage;
411
+ let probed;
412
+ try {
413
+ probed = storage.getItem(GATE_PROBE_KEY);
414
+ } catch {
415
+ syncGateAdapters.add(storage);
416
+ return storage;
417
+ }
418
+ if (probed != null && typeof probed.then === "function") {
419
+ asyncGateAdapters.add(storage);
420
+ try {
421
+ probed.then(void 0, () => {
422
+ });
423
+ } catch {
424
+ }
425
+ warnAsyncGateStorage();
426
+ return closedGateStorage;
427
+ }
428
+ syncGateAdapters.add(storage);
429
+ return storage;
430
+ };
386
431
  var hasSeenGate = (key, storageOverride, testingOverride) => {
387
432
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return false;
388
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
433
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
389
434
  if (!storage) return false;
390
435
  try {
391
436
  return storage.getItem(key) === SEEN_VALUE;
@@ -395,7 +440,7 @@ var hasSeenGate = (key, storageOverride, testingOverride) => {
395
440
  };
396
441
  var markSeenGate = (key, storageOverride, testingOverride) => {
397
442
  if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return;
398
- const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
443
+ const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
399
444
  if (!storage) return;
400
445
  try {
401
446
  storage.setItem(key, SEEN_VALUE);
@@ -483,17 +528,31 @@ var failOpen = async (storage, key) => {
483
528
  const cached3 = await readCachedFeatures(storage, key);
484
529
  return (_a = cached3 == null ? void 0 : cached3.features) != null ? _a : defaultWireFeatures;
485
530
  };
486
- var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
531
+ var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
532
+ var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
487
533
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
488
- const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
489
- try {
490
- return await fetch(url, {
534
+ let timer;
535
+ const expired = new Promise((resolve) => {
536
+ timer = setTimeout(() => {
537
+ controller == null ? void 0 : controller.abort();
538
+ resolve(NO_ANSWER);
539
+ }, timeoutMs);
540
+ });
541
+ const exchange = (async () => {
542
+ const res = await fetch(url, {
491
543
  method: "GET",
492
544
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
493
545
  signal: controller == null ? void 0 : controller.signal
494
546
  });
547
+ if (!res || !res.ok) return NO_ANSWER;
548
+ return await res.json();
549
+ })();
550
+ try {
551
+ return await Promise.race([exchange, expired]);
495
552
  } finally {
496
553
  clearTimeout(timer);
554
+ void exchange.catch(() => {
555
+ });
497
556
  }
498
557
  };
499
558
  var fetchWireFeatures = async (config) => {
@@ -503,9 +562,8 @@ var fetchWireFeatures = async (config) => {
503
562
  const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
504
563
  const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
505
564
  try {
506
- const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
507
- if (!res || !res.ok) return failOpen(storage, key);
508
- const json = await res.json();
565
+ const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
566
+ if (json === NO_ANSWER) return failOpen(storage, key);
509
567
  const features = parseWireFeatures(json);
510
568
  if (storage) writeCachedFeatures(storage, key, features);
511
569
  return features;
@@ -521,41 +579,56 @@ var useStableValue = (value, isEqual) => {
521
579
  return ref.current;
522
580
  };
523
581
  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);
524
- var useWireFeatures = (config) => {
582
+ var useWireFeaturesState = (config) => {
525
583
  const stableConfig = useStableValue(config, sameFetchKey);
526
584
  const [flags, setFlags] = useState(defaultWireFeatures);
527
585
  const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
586
+ const [settled, setSettled] = useState(!canFetch);
528
587
  useEffect(() => {
529
588
  if (!canFetch) {
530
589
  setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
590
+ setSettled(true);
531
591
  return;
532
592
  }
533
593
  let alive = true;
534
594
  void fetchWireFeatures(stableConfig).then((next) => {
535
595
  if (!alive) return;
536
596
  setFlags((prev) => featuresEqual(prev, next) ? prev : next);
597
+ setSettled(true);
537
598
  });
538
599
  return () => {
539
600
  alive = false;
540
601
  };
541
602
  }, [canFetch, stableConfig]);
542
- return flags;
603
+ return useMemo(() => ({ flags, settled }), [flags, settled]);
543
604
  };
544
605
  var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
545
- var globalObj = globalThis;
546
- if (!globalObj[CONTEXT_SYMBOL]) {
547
- globalObj[CONTEXT_SYMBOL] = createContext(null);
606
+ var featuresContextGlobal = globalThis;
607
+ if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
608
+ featuresContextGlobal[CONTEXT_SYMBOL] = createContext(null);
548
609
  }
549
- var WireFeaturesContext = globalObj[CONTEXT_SYMBOL];
610
+ var WireFeaturesContext = featuresContextGlobal[CONTEXT_SYMBOL];
611
+ var SETTLED_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.settled.context");
612
+ var settledContextGlobal = globalThis;
613
+ if (!settledContextGlobal[SETTLED_SYMBOL]) {
614
+ settledContextGlobal[SETTLED_SYMBOL] = createContext(true);
615
+ }
616
+ var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
550
617
  var useWireFeaturesContext = () => useContext(WireFeaturesContext);
551
- var useResolvedFeatures = (options) => {
618
+ var useResolvedFeaturesState = (options) => {
552
619
  var _a, _b, _c;
553
620
  const ctx = useWireFeaturesContext();
621
+ const ctxSettled = useContext(WireFeaturesSettledContext);
554
622
  const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
555
- const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
556
- return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
623
+ const fetched = useWireFeaturesState(shouldFetch ? options == null ? void 0 : options.config : void 0);
624
+ const flags = (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched.flags) != null ? _c : defaultWireFeatures;
625
+ const settled = (options == null ? void 0 : options.flags) ? true : ctx != null ? ctxSettled : fetched.settled;
626
+ return useMemo(() => ({ flags, settled }), [flags, settled]);
557
627
  };
558
628
 
629
+ // src/session/persistedSession.ts
630
+ var READ_TIMEOUT_MS2 = 1500;
631
+
559
632
  // src/theme/defaultTheme.ts
560
633
  var defaultOnboardingTheme = {
561
634
  colors: {
@@ -715,7 +788,10 @@ var _FeatureShowcase = ({
715
788
  [active, themeOverride]
716
789
  );
717
790
  const accent = accentColor != null ? accentColor : t.colors.primary;
718
- const flags = useResolvedFeatures({ flags: features, config: featuresConfig });
791
+ const { flags, settled } = useResolvedFeaturesState({
792
+ flags: features,
793
+ config: featuresConfig
794
+ });
719
795
  const disabled = !flags.showcase.enabled;
720
796
  const Onboarding = useMemo(() => resolveShowcaseOnboarding(), []);
721
797
  const gateKey = showcaseGateKey(config.id);
@@ -724,6 +800,14 @@ var _FeatureShowcase = ({
724
800
  [gateKey, storage, isTesting]
725
801
  );
726
802
  const skip = seen || disabled || !Onboarding;
803
+ const flagsCouldMatter = !seen && !!Onboarding;
804
+ const [holdExpired, setHoldExpired] = useState(false);
805
+ useEffect(() => {
806
+ if (settled || holdExpired || !flagsCouldMatter) return void 0;
807
+ const timer = setTimeout(() => setHoldExpired(true), READ_TIMEOUT_MS2);
808
+ return () => clearTimeout(timer);
809
+ }, [settled, holdExpired, flagsCouldMatter]);
810
+ const holding = flagsCouldMatter && !settled && !holdExpired;
727
811
  const doneRef = useRef(false);
728
812
  const finish = useCallback(() => {
729
813
  if (doneRef.current) return;
@@ -732,11 +816,12 @@ var _FeatureShowcase = ({
732
816
  onDone();
733
817
  }, [gateKey, storage, isTesting, onDone]);
734
818
  useEffect(() => {
819
+ if (holding) return;
735
820
  if (skip && !doneRef.current) {
736
821
  doneRef.current = true;
737
822
  onDone();
738
823
  }
739
- }, [skip, onDone]);
824
+ }, [holding, skip, onDone]);
740
825
  const [activeIndex, setActiveIndex] = useState(0);
741
826
  const steps = useMemo(
742
827
  () => config.slides.map((slide) => {
@@ -803,6 +888,7 @@ var _FeatureShowcase = ({
803
888
  }),
804
889
  [t]
805
890
  );
891
+ if (holding) return null;
806
892
  if (skip || !Onboarding) return null;
807
893
  const activeGesture = (_a = config.slides[activeIndex]) == null ? void 0 : _a.gesture;
808
894
  const showGesture = !!activeGesture && activeGesture !== "none" && activeGesture !== "tap" && activeGesture !== "double_tap";