@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.
- package/AGENTS.md +96 -21
- package/CHANGELOG.md +780 -0
- package/INTEGRATION_PROMPT.md +61 -23
- package/README.md +110 -31
- package/dist/analytics/index.d.mts +35 -13
- package/dist/analytics/index.d.ts +35 -13
- 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 +15 -1
- package/dist/coachmarks/index.d.ts +15 -1
- package/dist/coachmarks/index.js +120 -22
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +120 -22
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{currentSession-CUvTOchb.d.mts → currentSession-Bz7G6lno.d.mts} +35 -39
- package/dist/{currentSession-CW_5Mq4O.d.ts → currentSession-z-CZ55ad.d.ts} +35 -39
- package/dist/{decision-Bgo17oH7.d.mts → decision-3vWLuBlO.d.ts} +11 -2
- package/dist/{decision-Bkh_LigV.d.ts → decision-yBj2AyPW.d.mts} +11 -2
- 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 +3 -16
- package/dist/questionnaire/index.d.ts +3 -16
- package/dist/questionnaire/index.js +202 -46
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +203 -47
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +10 -6
- package/dist/reviews/index.d.ts +10 -6
- package/dist/reviews/index.js +269 -52
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +270 -53
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.d.mts +1 -1
- package/dist/showcase/index.d.ts +1 -1
- package/dist/showcase/index.js +106 -20
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs +107 -21
- package/dist/showcase/index.mjs.map +1 -1
- package/dist/{transport-j5gFfJhK.d.mts → transport-s5QxA-ci.d.mts} +18 -13
- package/dist/{transport-B_0SgCBe.d.ts → transport-xqqSFqxs.d.ts} +18 -13
- package/dist/{types-Cju-1_jT.d.mts → types-Byx306Kv.d.mts} +25 -10
- package/dist/{types-BcmagF6K.d.mts → types-D_0B0yay.d.mts} +7 -2
- package/dist/{types-BcmagF6K.d.ts → types-D_0B0yay.d.ts} +7 -2
- package/dist/{types-h2BZvl1t.d.ts → types-tdATL5z0.d.ts} +25 -10
- package/llms.txt +9 -9
- package/package.json +6 -9
- package/src/WireOnboarding.tsx +4 -1
- 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 +156 -2
- package/src/coachmarks/types.ts +7 -2
- package/src/coachmarks/useCoachmarkTour.ts +51 -1
- package/src/context/deviceId.ts +72 -6
- 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 +125 -37
- package/src/reviews/decision.ts +11 -1
- package/src/reviews/idempotency.ts +109 -0
- package/src/reviews/index.ts +4 -2
- package/src/reviews/runtime.ts +44 -13
- package/src/reviews/transport.ts +39 -20
- package/src/reviews/types.ts +7 -0
- 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 +15 -8
- package/src/utils/submitResult.ts +17 -9
- package/src/utils/withDeadline.ts +70 -0
package/dist/reviews/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React7, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useContext } from 'react';
|
|
1
|
+
import React7, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useContext, useSyncExternalStore } from 'react';
|
|
2
2
|
import { StyleSheet, Easing, Animated, BackHandler, Modal, Pressable, Text, View, AccessibilityInfo, TextInput, Linking, Platform } from 'react-native';
|
|
3
3
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
@@ -540,6 +540,7 @@ var buildReviewSubmission = (input) => {
|
|
|
540
540
|
if (contact) body.contact = contact;
|
|
541
541
|
if (input.sessionId) body.session_id = input.sessionId;
|
|
542
542
|
if (input.source) body.source = input.source;
|
|
543
|
+
if (input.idempotencyKey) body.idempotency_key = input.idempotencyKey;
|
|
543
544
|
if (input.meta && Object.keys(input.meta).length > 0) body.meta = input.meta;
|
|
544
545
|
return body;
|
|
545
546
|
};
|
|
@@ -557,6 +558,43 @@ var feedbackInputHeights = (fonts, verticalPadding = 0) => {
|
|
|
557
558
|
};
|
|
558
559
|
};
|
|
559
560
|
|
|
561
|
+
// src/context/deviceId.ts
|
|
562
|
+
var AUTO_DEVICE_ID_PREFIX = "wdev_";
|
|
563
|
+
var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
|
|
564
|
+
var mintDeviceId = () => {
|
|
565
|
+
const time = Date.now().toString(36);
|
|
566
|
+
return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
|
|
567
|
+
};
|
|
568
|
+
var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
|
|
569
|
+
var deviceKeyGlobal = globalThis;
|
|
570
|
+
var autoDeviceKeyRegistry = () => {
|
|
571
|
+
const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
|
|
572
|
+
if (existing) return existing;
|
|
573
|
+
const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
|
|
574
|
+
deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
|
|
575
|
+
return created;
|
|
576
|
+
};
|
|
577
|
+
var ambientAutoDeviceKey = () => {
|
|
578
|
+
const registry = autoDeviceKeyRegistry();
|
|
579
|
+
if (registry.keys.size > 1) return void 0;
|
|
580
|
+
const sole = [...registry.keys.values()][0];
|
|
581
|
+
if (sole) return sole;
|
|
582
|
+
if (!registry.ambient) registry.ambient = mintDeviceId();
|
|
583
|
+
return registry.ambient;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// src/reviews/idempotency.ts
|
|
587
|
+
var REVIEW_IDEMPOTENCY_PREFIX = "rvw_";
|
|
588
|
+
var mintIdempotencyKey = () => `${REVIEW_IDEMPOTENCY_PREFIX}${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
589
|
+
var hasDeviceKey = (meta) => {
|
|
590
|
+
const value = meta == null ? void 0 : meta.device_key;
|
|
591
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
592
|
+
};
|
|
593
|
+
var createImpression = () => ({
|
|
594
|
+
idempotencyKey: mintIdempotencyKey(),
|
|
595
|
+
deviceKey: ambientAutoDeviceKey()
|
|
596
|
+
});
|
|
597
|
+
|
|
560
598
|
// src/motion/centeredModalMotion.ts
|
|
561
599
|
var CENTERED_MODAL_ANIM = {
|
|
562
600
|
/** Spring config for the card scale/opacity entrance (mirrors CompletionView's pop). */
|
|
@@ -734,6 +772,28 @@ var requestStoreReview = async (store) => {
|
|
|
734
772
|
return opened ? "store_url" : "none";
|
|
735
773
|
};
|
|
736
774
|
|
|
775
|
+
// src/utils/withDeadline.ts
|
|
776
|
+
var DEADLINE_EXPIRED = /* @__PURE__ */ Symbol("wire-deadline-expired");
|
|
777
|
+
var DEFAULT_DEADLINE_MS = 15e3;
|
|
778
|
+
var withDeadline = async (exchange, timeoutMs = DEFAULT_DEADLINE_MS) => {
|
|
779
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
780
|
+
let timer;
|
|
781
|
+
const expired = new Promise((resolve) => {
|
|
782
|
+
timer = setTimeout(() => {
|
|
783
|
+
controller == null ? void 0 : controller.abort();
|
|
784
|
+
resolve(DEADLINE_EXPIRED);
|
|
785
|
+
}, timeoutMs);
|
|
786
|
+
});
|
|
787
|
+
const running = exchange(controller == null ? void 0 : controller.signal);
|
|
788
|
+
try {
|
|
789
|
+
return await Promise.race([running, expired]);
|
|
790
|
+
} finally {
|
|
791
|
+
clearTimeout(timer);
|
|
792
|
+
void running.catch(() => {
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
|
|
737
797
|
// src/analytics/reportClientEvent.ts
|
|
738
798
|
var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
739
799
|
var toWireEvents = (events) => events.map((event) => {
|
|
@@ -775,17 +835,51 @@ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
|
|
|
775
835
|
"@wireai/activation:currentSessionId"
|
|
776
836
|
);
|
|
777
837
|
var globalSlot = globalThis;
|
|
838
|
+
var SESSION_ID_LISTENERS_SLOT = /* @__PURE__ */ Symbol.for(
|
|
839
|
+
"@wireai/activation:currentSessionIdListeners"
|
|
840
|
+
);
|
|
841
|
+
var listenerSlot = globalThis;
|
|
842
|
+
var listeners = () => {
|
|
843
|
+
const existing = listenerSlot[SESSION_ID_LISTENERS_SLOT];
|
|
844
|
+
if (existing) return existing;
|
|
845
|
+
const created = /* @__PURE__ */ new Set();
|
|
846
|
+
listenerSlot[SESSION_ID_LISTENERS_SLOT] = created;
|
|
847
|
+
return created;
|
|
848
|
+
};
|
|
849
|
+
var writeCurrentSessionId = (id) => {
|
|
850
|
+
if (globalSlot[CURRENT_SESSION_ID_SLOT] === id) return;
|
|
851
|
+
globalSlot[CURRENT_SESSION_ID_SLOT] = id;
|
|
852
|
+
for (const listener of Array.from(listeners())) {
|
|
853
|
+
try {
|
|
854
|
+
listener();
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
var subscribeCurrentSessionId = (listener) => {
|
|
860
|
+
const set = listeners();
|
|
861
|
+
set.add(listener);
|
|
862
|
+
return () => {
|
|
863
|
+
set.delete(listener);
|
|
864
|
+
};
|
|
865
|
+
};
|
|
778
866
|
var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
|
|
779
867
|
var MINT_WARNING = "[wireai] No app-open session was registered, so a session id was minted for this event (the server drops an event that has no session_id, and still answers 200). Mount useLifecycleEvents at your app root so events correlate to a real app-open.";
|
|
780
868
|
var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
|
|
781
869
|
"@wireai/activation:currentSessionIdMintWarned"
|
|
782
870
|
);
|
|
783
871
|
var warnSlot = globalThis;
|
|
872
|
+
var MINTED_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
|
|
873
|
+
"@wireai/activation:mintedSessionId"
|
|
874
|
+
);
|
|
875
|
+
var mintedSlot = globalThis;
|
|
876
|
+
var isMintedSessionId = (id) => id !== void 0 && id === mintedSlot[MINTED_SESSION_ID_SLOT];
|
|
784
877
|
var ensureCurrentSessionId = () => {
|
|
785
878
|
const existing = globalSlot[CURRENT_SESSION_ID_SLOT];
|
|
786
879
|
if (typeof existing === "string" && existing.length > 0) return existing;
|
|
787
880
|
const minted = makeSessionId();
|
|
788
|
-
|
|
881
|
+
writeCurrentSessionId(minted);
|
|
882
|
+
mintedSlot[MINTED_SESSION_ID_SLOT] = minted;
|
|
789
883
|
if (!warnSlot[MINT_WARNED_SLOT] && warnInDev(MINT_WARNING)) {
|
|
790
884
|
warnSlot[MINT_WARNED_SLOT] = true;
|
|
791
885
|
}
|
|
@@ -799,11 +893,15 @@ var submitReview = async (target, review) => {
|
|
|
799
893
|
const url = `${target.serverUrl.replace(/\/$/, "")}/v1/reviews`;
|
|
800
894
|
const headers = { "Content-Type": "application/json" };
|
|
801
895
|
if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
|
|
802
|
-
const res = await
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
896
|
+
const res = await withDeadline(
|
|
897
|
+
(signal) => fetch(url, {
|
|
898
|
+
method: "POST",
|
|
899
|
+
headers,
|
|
900
|
+
body: JSON.stringify(review),
|
|
901
|
+
signal
|
|
902
|
+
})
|
|
903
|
+
);
|
|
904
|
+
if (res === DEADLINE_EXPIRED) return "unsent";
|
|
807
905
|
if (!res) return "unsent";
|
|
808
906
|
return res.ok ? "accepted" : "rejected";
|
|
809
907
|
} catch {
|
|
@@ -821,9 +919,12 @@ var fetchReviewDecision = async (target, options = {}) => {
|
|
|
821
919
|
const url = `${base}/v1/reviews/decision${qs ? `?${qs}` : ""}`;
|
|
822
920
|
const headers = {};
|
|
823
921
|
if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
922
|
+
const json = await withDeadline(async (signal) => {
|
|
923
|
+
const res = await fetch(url, { headers, signal });
|
|
924
|
+
if (!res || !res.ok) return null;
|
|
925
|
+
return await res.json();
|
|
926
|
+
});
|
|
927
|
+
if (json === DEADLINE_EXPIRED) return null;
|
|
827
928
|
if (!json || typeof json.fire !== "boolean") return null;
|
|
828
929
|
return json;
|
|
829
930
|
} catch {
|
|
@@ -895,8 +996,33 @@ var _ReviewGate = ({
|
|
|
895
996
|
const resolve = useCallback(() => {
|
|
896
997
|
onResolved == null ? void 0 : onResolved();
|
|
897
998
|
}, [onResolved]);
|
|
999
|
+
const impressionRef = React7.useRef(null);
|
|
1000
|
+
const buildBody = useCallback(
|
|
1001
|
+
(input) => {
|
|
1002
|
+
let impression = impressionRef.current;
|
|
1003
|
+
if (!impression) {
|
|
1004
|
+
impression = createImpression();
|
|
1005
|
+
impressionRef.current = impression;
|
|
1006
|
+
}
|
|
1007
|
+
const merged = { ...meta, ...input.extraMeta };
|
|
1008
|
+
if (!hasDeviceKey(merged) && impression.deviceKey) merged.device_key = impression.deviceKey;
|
|
1009
|
+
return buildReviewSubmission({
|
|
1010
|
+
stars: input.stars,
|
|
1011
|
+
feedbackText: input.feedbackText,
|
|
1012
|
+
suggestion: input.suggestion,
|
|
1013
|
+
sessionId,
|
|
1014
|
+
meta: merged,
|
|
1015
|
+
idempotencyKey: impression.idempotencyKey
|
|
1016
|
+
});
|
|
1017
|
+
},
|
|
1018
|
+
[meta, sessionId]
|
|
1019
|
+
);
|
|
898
1020
|
const postedRef = React7.useRef(false);
|
|
899
1021
|
const unackedRef = React7.useRef(null);
|
|
1022
|
+
const recoverOnSettleRef = React7.useRef(false);
|
|
1023
|
+
const recoveredRef = React7.useRef(false);
|
|
1024
|
+
const postOnceRef = React7.useRef(() => {
|
|
1025
|
+
});
|
|
900
1026
|
const postOnce = useCallback(
|
|
901
1027
|
(body) => {
|
|
902
1028
|
if (postedRef.current) return;
|
|
@@ -905,6 +1031,10 @@ var _ReviewGate = ({
|
|
|
905
1031
|
void submitReview(target, body).then((result) => {
|
|
906
1032
|
if (result === "unsent") {
|
|
907
1033
|
postedRef.current = false;
|
|
1034
|
+
if (recoverOnSettleRef.current && !recoveredRef.current) {
|
|
1035
|
+
recoveredRef.current = true;
|
|
1036
|
+
postOnceRef.current(body);
|
|
1037
|
+
}
|
|
908
1038
|
return;
|
|
909
1039
|
}
|
|
910
1040
|
unackedRef.current = null;
|
|
@@ -912,6 +1042,7 @@ var _ReviewGate = ({
|
|
|
912
1042
|
},
|
|
913
1043
|
[target]
|
|
914
1044
|
);
|
|
1045
|
+
postOnceRef.current = postOnce;
|
|
915
1046
|
const abandonRef = React7.useRef(() => {
|
|
916
1047
|
});
|
|
917
1048
|
abandonRef.current = () => {
|
|
@@ -920,14 +1051,17 @@ var _ReviewGate = ({
|
|
|
920
1051
|
postOnce(undelivered);
|
|
921
1052
|
return;
|
|
922
1053
|
}
|
|
1054
|
+
if (postedRef.current && undelivered) {
|
|
1055
|
+
recoverOnSettleRef.current = true;
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
923
1058
|
if (postedRef.current || stars < 1) return;
|
|
924
1059
|
postOnce(
|
|
925
|
-
|
|
1060
|
+
buildBody({
|
|
926
1061
|
stars,
|
|
927
1062
|
feedbackText: feedback,
|
|
928
1063
|
suggestion,
|
|
929
|
-
|
|
930
|
-
meta: { ...meta, abandoned: true }
|
|
1064
|
+
extraMeta: { abandoned: true }
|
|
931
1065
|
})
|
|
932
1066
|
);
|
|
933
1067
|
};
|
|
@@ -952,10 +1086,9 @@ var _ReviewGate = ({
|
|
|
952
1086
|
onEvent == null ? void 0 : onEvent({ name: "store_review_requested", id, stars: value });
|
|
953
1087
|
const route = await requestStoreReview(store);
|
|
954
1088
|
postOnce(
|
|
955
|
-
|
|
1089
|
+
buildBody({
|
|
956
1090
|
stars: value,
|
|
957
|
-
|
|
958
|
-
meta: { ...meta, store_route: route }
|
|
1091
|
+
extraMeta: { store_route: route }
|
|
959
1092
|
})
|
|
960
1093
|
);
|
|
961
1094
|
setPhase("thanks");
|
|
@@ -964,24 +1097,18 @@ var _ReviewGate = ({
|
|
|
964
1097
|
setPhase("feedback");
|
|
965
1098
|
}
|
|
966
1099
|
},
|
|
967
|
-
[onEvent, id, store, postOnce,
|
|
1100
|
+
[onEvent, id, store, postOnce, buildBody, resolveWithDelay]
|
|
968
1101
|
);
|
|
969
1102
|
const feedbackReady = feedback.trim().length > 0;
|
|
970
1103
|
const sendFeedback = useCallback(() => {
|
|
971
1104
|
if (!feedbackReady) return;
|
|
972
|
-
const body =
|
|
973
|
-
stars,
|
|
974
|
-
feedbackText: feedback,
|
|
975
|
-
suggestion,
|
|
976
|
-
sessionId,
|
|
977
|
-
meta
|
|
978
|
-
});
|
|
1105
|
+
const body = buildBody({ stars, feedbackText: feedback, suggestion });
|
|
979
1106
|
postOnce(body);
|
|
980
1107
|
onEvent == null ? void 0 : onEvent({ name: "review_feedback_submitted", id, stars });
|
|
981
1108
|
playHaptic("success");
|
|
982
1109
|
setPhase("thanks");
|
|
983
1110
|
resolveWithDelay();
|
|
984
|
-
}, [feedbackReady, stars, feedback, suggestion,
|
|
1111
|
+
}, [feedbackReady, stars, feedback, suggestion, buildBody, postOnce, onEvent, id, resolveWithDelay]);
|
|
985
1112
|
const dismiss = useCallback(() => {
|
|
986
1113
|
setPhase("thanks");
|
|
987
1114
|
resolveWithDelay();
|
|
@@ -1128,9 +1255,54 @@ var coachmarkRuntime = () => {
|
|
|
1128
1255
|
var getCoachmarkStorage = () => coachmarkRuntime().storage;
|
|
1129
1256
|
var isCoachmarkTesting = () => coachmarkRuntime().testing;
|
|
1130
1257
|
var SEEN_VALUE = "1";
|
|
1258
|
+
var GATE_PROBE_KEY = "wire_gate_probe";
|
|
1259
|
+
var ASYNC_GATE_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
|
|
1260
|
+
"@wireai/activation:asyncGateStorageWarned"
|
|
1261
|
+
);
|
|
1262
|
+
var asyncGateWarnGlobal = globalThis;
|
|
1263
|
+
var closedGateStorage = Object.freeze({
|
|
1264
|
+
getItem: () => SEEN_VALUE,
|
|
1265
|
+
setItem: () => {
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
var syncGateAdapters = /* @__PURE__ */ new WeakSet();
|
|
1269
|
+
var asyncGateAdapters = /* @__PURE__ */ new WeakSet();
|
|
1270
|
+
var warnAsyncGateStorage = () => {
|
|
1271
|
+
if (asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT]) return;
|
|
1272
|
+
if (typeof __DEV__ === "undefined" || !__DEV__) return;
|
|
1273
|
+
if (typeof console === "undefined" || !console.warn) return;
|
|
1274
|
+
asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT] = true;
|
|
1275
|
+
console.warn(
|
|
1276
|
+
"[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."
|
|
1277
|
+
);
|
|
1278
|
+
};
|
|
1279
|
+
var validateGateStorage = (storage) => {
|
|
1280
|
+
if (!storage) return null;
|
|
1281
|
+
if (syncGateAdapters.has(storage)) return storage;
|
|
1282
|
+
if (asyncGateAdapters.has(storage)) return closedGateStorage;
|
|
1283
|
+
let probed;
|
|
1284
|
+
try {
|
|
1285
|
+
probed = storage.getItem(GATE_PROBE_KEY);
|
|
1286
|
+
} catch {
|
|
1287
|
+
syncGateAdapters.add(storage);
|
|
1288
|
+
return storage;
|
|
1289
|
+
}
|
|
1290
|
+
if (probed != null && typeof probed.then === "function") {
|
|
1291
|
+
asyncGateAdapters.add(storage);
|
|
1292
|
+
try {
|
|
1293
|
+
probed.then(void 0, () => {
|
|
1294
|
+
});
|
|
1295
|
+
} catch {
|
|
1296
|
+
}
|
|
1297
|
+
warnAsyncGateStorage();
|
|
1298
|
+
return closedGateStorage;
|
|
1299
|
+
}
|
|
1300
|
+
syncGateAdapters.add(storage);
|
|
1301
|
+
return storage;
|
|
1302
|
+
};
|
|
1131
1303
|
var hasSeenGate = (key, storageOverride, testingOverride) => {
|
|
1132
1304
|
if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return false;
|
|
1133
|
-
const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
|
|
1305
|
+
const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
|
|
1134
1306
|
if (!storage) return false;
|
|
1135
1307
|
try {
|
|
1136
1308
|
return storage.getItem(key) === SEEN_VALUE;
|
|
@@ -1140,7 +1312,7 @@ var hasSeenGate = (key, storageOverride, testingOverride) => {
|
|
|
1140
1312
|
};
|
|
1141
1313
|
var markSeenGate = (key, storageOverride, testingOverride) => {
|
|
1142
1314
|
if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return;
|
|
1143
|
-
const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
|
|
1315
|
+
const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
|
|
1144
1316
|
if (!storage) return;
|
|
1145
1317
|
try {
|
|
1146
1318
|
storage.setItem(key, SEEN_VALUE);
|
|
@@ -1228,17 +1400,31 @@ var failOpen = async (storage, key) => {
|
|
|
1228
1400
|
const cached = await readCachedFeatures(storage, key);
|
|
1229
1401
|
return (_a = cached == null ? void 0 : cached.features) != null ? _a : defaultWireFeatures;
|
|
1230
1402
|
};
|
|
1231
|
-
var
|
|
1403
|
+
var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
|
|
1404
|
+
var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
|
|
1232
1405
|
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1406
|
+
let timer;
|
|
1407
|
+
const expired = new Promise((resolve) => {
|
|
1408
|
+
timer = setTimeout(() => {
|
|
1409
|
+
controller == null ? void 0 : controller.abort();
|
|
1410
|
+
resolve(NO_ANSWER);
|
|
1411
|
+
}, timeoutMs);
|
|
1412
|
+
});
|
|
1413
|
+
const exchange = (async () => {
|
|
1414
|
+
const res = await fetch(url, {
|
|
1236
1415
|
method: "GET",
|
|
1237
1416
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
1238
1417
|
signal: controller == null ? void 0 : controller.signal
|
|
1239
1418
|
});
|
|
1419
|
+
if (!res || !res.ok) return NO_ANSWER;
|
|
1420
|
+
return await res.json();
|
|
1421
|
+
})();
|
|
1422
|
+
try {
|
|
1423
|
+
return await Promise.race([exchange, expired]);
|
|
1240
1424
|
} finally {
|
|
1241
1425
|
clearTimeout(timer);
|
|
1426
|
+
void exchange.catch(() => {
|
|
1427
|
+
});
|
|
1242
1428
|
}
|
|
1243
1429
|
};
|
|
1244
1430
|
var fetchWireFeatures = async (config) => {
|
|
@@ -1248,9 +1434,8 @@ var fetchWireFeatures = async (config) => {
|
|
|
1248
1434
|
const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
|
|
1249
1435
|
const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
1250
1436
|
try {
|
|
1251
|
-
const
|
|
1252
|
-
if (
|
|
1253
|
-
const json = await res.json();
|
|
1437
|
+
const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
|
|
1438
|
+
if (json === NO_ANSWER) return failOpen(storage, key);
|
|
1254
1439
|
const features = parseWireFeatures(json);
|
|
1255
1440
|
if (storage) writeCachedFeatures(storage, key, features);
|
|
1256
1441
|
return features;
|
|
@@ -1266,39 +1451,51 @@ var useStableValue = (value, isEqual) => {
|
|
|
1266
1451
|
return ref.current;
|
|
1267
1452
|
};
|
|
1268
1453
|
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);
|
|
1269
|
-
var
|
|
1454
|
+
var useWireFeaturesState = (config) => {
|
|
1270
1455
|
const stableConfig = useStableValue(config, sameFetchKey);
|
|
1271
1456
|
const [flags, setFlags] = useState(defaultWireFeatures);
|
|
1272
1457
|
const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
|
|
1458
|
+
const [settled, setSettled] = useState(!canFetch);
|
|
1273
1459
|
useEffect(() => {
|
|
1274
1460
|
if (!canFetch) {
|
|
1275
1461
|
setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
|
|
1462
|
+
setSettled(true);
|
|
1276
1463
|
return;
|
|
1277
1464
|
}
|
|
1278
1465
|
let alive = true;
|
|
1279
1466
|
void fetchWireFeatures(stableConfig).then((next) => {
|
|
1280
1467
|
if (!alive) return;
|
|
1281
1468
|
setFlags((prev) => featuresEqual(prev, next) ? prev : next);
|
|
1469
|
+
setSettled(true);
|
|
1282
1470
|
});
|
|
1283
1471
|
return () => {
|
|
1284
1472
|
alive = false;
|
|
1285
1473
|
};
|
|
1286
1474
|
}, [canFetch, stableConfig]);
|
|
1287
|
-
return flags;
|
|
1475
|
+
return useMemo(() => ({ flags, settled }), [flags, settled]);
|
|
1288
1476
|
};
|
|
1289
1477
|
var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
|
|
1290
|
-
var
|
|
1291
|
-
if (!
|
|
1292
|
-
|
|
1478
|
+
var featuresContextGlobal = globalThis;
|
|
1479
|
+
if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
|
|
1480
|
+
featuresContextGlobal[CONTEXT_SYMBOL] = createContext(null);
|
|
1293
1481
|
}
|
|
1294
|
-
var WireFeaturesContext =
|
|
1482
|
+
var WireFeaturesContext = featuresContextGlobal[CONTEXT_SYMBOL];
|
|
1483
|
+
var SETTLED_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.settled.context");
|
|
1484
|
+
var settledContextGlobal = globalThis;
|
|
1485
|
+
if (!settledContextGlobal[SETTLED_SYMBOL]) {
|
|
1486
|
+
settledContextGlobal[SETTLED_SYMBOL] = createContext(true);
|
|
1487
|
+
}
|
|
1488
|
+
var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
|
|
1295
1489
|
var useWireFeaturesContext = () => useContext(WireFeaturesContext);
|
|
1296
|
-
var
|
|
1490
|
+
var useResolvedFeaturesState = (options) => {
|
|
1297
1491
|
var _a, _b, _c;
|
|
1298
1492
|
const ctx = useWireFeaturesContext();
|
|
1493
|
+
const ctxSettled = useContext(WireFeaturesSettledContext);
|
|
1299
1494
|
const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
|
|
1300
|
-
const fetched =
|
|
1301
|
-
|
|
1495
|
+
const fetched = useWireFeaturesState(shouldFetch ? options == null ? void 0 : options.config : void 0);
|
|
1496
|
+
const flags = (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched.flags) != null ? _c : defaultWireFeatures;
|
|
1497
|
+
const settled = (options == null ? void 0 : options.flags) ? true : ctx != null ? ctxSettled : fetched.settled;
|
|
1498
|
+
return useMemo(() => ({ flags, settled }), [flags, settled]);
|
|
1302
1499
|
};
|
|
1303
1500
|
|
|
1304
1501
|
// src/reviews/equality.ts
|
|
@@ -1327,7 +1524,7 @@ var reviewSeenKey = (id, version) => version ? `wire_review_${id}_${version}_see
|
|
|
1327
1524
|
var reviewLastShownKey = (id) => `wire_review_${id}_last`;
|
|
1328
1525
|
var reviewSessionsKey = (id) => `wire_review_${id}_sessions`;
|
|
1329
1526
|
var reviewSessionOpenKey = (id) => `wire_review_${id}_open`;
|
|
1330
|
-
var resolveStorage = (override) => override != null ? override : getCoachmarkStorage();
|
|
1527
|
+
var resolveStorage = (override) => validateGateStorage(override != null ? override : getCoachmarkStorage());
|
|
1331
1528
|
var NO_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
|
|
1332
1529
|
"@wireai/activation:gateStorageWarned"
|
|
1333
1530
|
);
|
|
@@ -1372,18 +1569,23 @@ var PROCESS_OPEN_ID_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:proces
|
|
|
1372
1569
|
var openIdGlobal = globalThis;
|
|
1373
1570
|
var currentOpenId = () => {
|
|
1374
1571
|
const live = getCurrentSessionId();
|
|
1572
|
+
const liveMinted = isMintedSessionId(live);
|
|
1375
1573
|
const pin = openIdGlobal[PROCESS_OPEN_ID_SLOT];
|
|
1376
1574
|
if (!pin) {
|
|
1377
1575
|
const openId = live != null ? live : makeSessionId();
|
|
1378
|
-
openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live };
|
|
1576
|
+
openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live, observedMinted: liveMinted };
|
|
1379
1577
|
return openId;
|
|
1380
1578
|
}
|
|
1381
1579
|
if (!live || live === pin.observed) return pin.openId;
|
|
1382
|
-
if (pin.observed === void 0) {
|
|
1383
|
-
openIdGlobal[PROCESS_OPEN_ID_SLOT] = {
|
|
1580
|
+
if (pin.observed === void 0 || pin.observedMinted) {
|
|
1581
|
+
openIdGlobal[PROCESS_OPEN_ID_SLOT] = {
|
|
1582
|
+
openId: pin.openId,
|
|
1583
|
+
observed: live,
|
|
1584
|
+
observedMinted: liveMinted
|
|
1585
|
+
};
|
|
1384
1586
|
return pin.openId;
|
|
1385
1587
|
}
|
|
1386
|
-
openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live };
|
|
1588
|
+
openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live, observedMinted: liveMinted };
|
|
1387
1589
|
return live;
|
|
1388
1590
|
};
|
|
1389
1591
|
var bumpSessionCount = (storage, sessionsKey, openKey, openId = currentOpenId()) => {
|
|
@@ -1421,17 +1623,32 @@ var useReviewGate = ({
|
|
|
1421
1623
|
decisionProp,
|
|
1422
1624
|
(a, b) => sameDecision(a, b)
|
|
1423
1625
|
);
|
|
1424
|
-
const
|
|
1626
|
+
const { flags: resolvedFeatures, settled: featuresSettled } = useResolvedFeaturesState({
|
|
1627
|
+
flags: features,
|
|
1628
|
+
config: featuresConfig
|
|
1629
|
+
});
|
|
1630
|
+
const featureEnabled = resolvedFeatures.review.enabled;
|
|
1425
1631
|
const testing = isTesting != null ? isTesting : isCoachmarkTesting();
|
|
1426
1632
|
const seenKey = reviewSeenKey(config.id, config.oncePerVersion === false ? void 0 : config.appVersion);
|
|
1427
1633
|
const lastKey = reviewLastShownKey(config.id);
|
|
1428
1634
|
const sessionsKey = reviewSessionsKey(config.id);
|
|
1429
1635
|
const sessionOpenKey = reviewSessionOpenKey(config.id);
|
|
1430
|
-
const
|
|
1636
|
+
const openSessionId = useSyncExternalStore(
|
|
1637
|
+
subscribeCurrentSessionId,
|
|
1638
|
+
getCurrentSessionId,
|
|
1639
|
+
getCurrentSessionId
|
|
1640
|
+
);
|
|
1641
|
+
const storageRef = useRef(storage);
|
|
1642
|
+
storageRef.current = storage;
|
|
1643
|
+
const [sessions, setSessions] = useState(() => {
|
|
1431
1644
|
const store = resolveStorage(storage);
|
|
1432
1645
|
warnMissingGateStorage(store, "review");
|
|
1433
1646
|
return bumpSessionCount(store, sessionsKey, sessionOpenKey);
|
|
1434
|
-
})
|
|
1647
|
+
});
|
|
1648
|
+
useEffect(() => {
|
|
1649
|
+
const next = bumpSessionCount(resolveStorage(storageRef.current), sessionsKey, sessionOpenKey);
|
|
1650
|
+
setSessions((prev) => prev === next ? prev : next);
|
|
1651
|
+
}, [openSessionId, sessionsKey, sessionOpenKey]);
|
|
1435
1652
|
const hasServerDecision = decision != null;
|
|
1436
1653
|
const timeoutMs = config.timeoutFallbackMs;
|
|
1437
1654
|
const waits = !hasServerDecision && timeoutMs != null && timeoutMs > 0;
|
|
@@ -1457,9 +1674,9 @@ var useReviewGate = ({
|
|
|
1457
1674
|
});
|
|
1458
1675
|
const resolved = decideReview(local, decision);
|
|
1459
1676
|
const alreadySeen = hasSeenGate(seenKey, storage, isTesting);
|
|
1460
|
-
const ready = hasServerDecision || elapsed;
|
|
1677
|
+
const ready = (hasServerDecision || elapsed) && featuresSettled;
|
|
1461
1678
|
return { visible: ready && resolved.fire && !alreadySeen, verdict: resolved };
|
|
1462
|
-
}, [featureEnabled, testing, config, decision, events, sessions, elapsed, hasServerDecision, seenKey, lastKey, storage, isTesting]);
|
|
1679
|
+
}, [featureEnabled, featuresSettled, testing, config, decision, events, sessions, elapsed, hasServerDecision, seenKey, lastKey, storage, isTesting]);
|
|
1463
1680
|
const shownRef = useRef(false);
|
|
1464
1681
|
const markShown = useCallback(() => {
|
|
1465
1682
|
if (shownRef.current) return;
|