@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.d.mts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React__default from 'react';
|
|
2
|
-
import { R as ReviewGateProps, a as ReviewPresentation, U as UseReviewGateOptions, b as ReviewGateController, c as ReviewDecision, d as ReviewStoreConfig } from '../types-
|
|
3
|
-
export { e as RatingRoute, f as ReviewConfig, g as ReviewDecisionResponse, h as ReviewGateEvent, i as ReviewGateEventName, j as ReviewSubmission, k as ReviewTarget, S as SubmitResult } from '../types-
|
|
2
|
+
import { R as ReviewGateProps, a as ReviewPresentation, U as UseReviewGateOptions, b as ReviewGateController, c as ReviewDecision, d as ReviewStoreConfig } from '../types-Byx306Kv.mjs';
|
|
3
|
+
export { e as RatingRoute, f as ReviewConfig, g as ReviewDecisionResponse, h as ReviewGateEvent, i as ReviewGateEventName, j as ReviewSubmission, k as ReviewTarget, S as SubmitResult } from '../types-Byx306Kv.mjs';
|
|
4
4
|
export { C as ReviewModal, a as ReviewModalHandle, b as ReviewModalProps } from '../CenteredModal-C3qQBHsA.mjs';
|
|
5
|
-
export { G as GateRules, a as GateSignals, b as buildReviewSubmission, d as decideReview, e as evaluateGate, r as resolveRules, c as routeRating } from '../decision-
|
|
5
|
+
export { G as GateRules, a as GateSignals, b as buildReviewSubmission, d as decideReview, e as evaluateGate, r as resolveRules, c as routeRating } from '../decision-yBj2AyPW.mjs';
|
|
6
6
|
import { c as OnboardingFonts } from '../types-BKfpdZzX.mjs';
|
|
7
|
-
export { F as FetchReviewDecisionOptions, R as ReportAppEventOptions, a as ReviewSubmitResult, f as fetchReviewDecision, r as reportAppEvent, s as submitReview } from '../transport-
|
|
8
|
-
import '../types-
|
|
7
|
+
export { F as FetchReviewDecisionOptions, R as ReportAppEventOptions, a as ReviewSubmitResult, f as fetchReviewDecision, r as reportAppEvent, s as submitReview } from '../transport-s5QxA-ci.mjs';
|
|
8
|
+
import '../types-D_0B0yay.mjs';
|
|
9
9
|
import '../types-BpwiRpA8.mjs';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -41,7 +41,11 @@ declare const reviewSessionsKey: (id: string) => string;
|
|
|
41
41
|
*
|
|
42
42
|
* The form NEVER asks for identity (no name / email / contact field): the host already
|
|
43
43
|
* identifies the user via `sessionId` + `meta` (e.g. user_id, device_key), which is all
|
|
44
|
-
* the POST body carries alongside the rating and the free text.
|
|
44
|
+
* the POST body carries alongside the rating and the free text. Since 2026-08-19 the gate also
|
|
45
|
+
* fills in `meta.device_key` from the kit's own per-install id when the host passed none — not for
|
|
46
|
+
* identification, but because it is the unit the server scopes the idempotency key to, and without
|
|
47
|
+
* it the key is discarded. Any non-blank string the host passes still wins verbatim. See
|
|
48
|
+
* `idempotency.ts`.
|
|
45
49
|
*
|
|
46
50
|
* Motion (2026-07-16): the three phases used to swap IN PLACE with no transition. They now
|
|
47
51
|
* ride `ModalPaneHost`, the same pane-slide primitive the questionnaire steps use, and the
|
package/dist/reviews/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React__default from 'react';
|
|
2
|
-
import { R as ReviewGateProps, a as ReviewPresentation, U as UseReviewGateOptions, b as ReviewGateController, c as ReviewDecision, d as ReviewStoreConfig } from '../types-
|
|
3
|
-
export { e as RatingRoute, f as ReviewConfig, g as ReviewDecisionResponse, h as ReviewGateEvent, i as ReviewGateEventName, j as ReviewSubmission, k as ReviewTarget, S as SubmitResult } from '../types-
|
|
2
|
+
import { R as ReviewGateProps, a as ReviewPresentation, U as UseReviewGateOptions, b as ReviewGateController, c as ReviewDecision, d as ReviewStoreConfig } from '../types-tdATL5z0.js';
|
|
3
|
+
export { e as RatingRoute, f as ReviewConfig, g as ReviewDecisionResponse, h as ReviewGateEvent, i as ReviewGateEventName, j as ReviewSubmission, k as ReviewTarget, S as SubmitResult } from '../types-tdATL5z0.js';
|
|
4
4
|
export { C as ReviewModal, a as ReviewModalHandle, b as ReviewModalProps } from '../CenteredModal-Cdgns6--.js';
|
|
5
|
-
export { G as GateRules, a as GateSignals, b as buildReviewSubmission, d as decideReview, e as evaluateGate, r as resolveRules, c as routeRating } from '../decision-
|
|
5
|
+
export { G as GateRules, a as GateSignals, b as buildReviewSubmission, d as decideReview, e as evaluateGate, r as resolveRules, c as routeRating } from '../decision-3vWLuBlO.js';
|
|
6
6
|
import { c as OnboardingFonts } from '../types-BKfpdZzX.js';
|
|
7
|
-
export { F as FetchReviewDecisionOptions, R as ReportAppEventOptions, a as ReviewSubmitResult, f as fetchReviewDecision, r as reportAppEvent, s as submitReview } from '../transport-
|
|
8
|
-
import '../types-
|
|
7
|
+
export { F as FetchReviewDecisionOptions, R as ReportAppEventOptions, a as ReviewSubmitResult, f as fetchReviewDecision, r as reportAppEvent, s as submitReview } from '../transport-xqqSFqxs.js';
|
|
8
|
+
import '../types-D_0B0yay.js';
|
|
9
9
|
import '../types-BpwiRpA8.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -41,7 +41,11 @@ declare const reviewSessionsKey: (id: string) => string;
|
|
|
41
41
|
*
|
|
42
42
|
* The form NEVER asks for identity (no name / email / contact field): the host already
|
|
43
43
|
* identifies the user via `sessionId` + `meta` (e.g. user_id, device_key), which is all
|
|
44
|
-
* the POST body carries alongside the rating and the free text.
|
|
44
|
+
* the POST body carries alongside the rating and the free text. Since 2026-08-19 the gate also
|
|
45
|
+
* fills in `meta.device_key` from the kit's own per-install id when the host passed none — not for
|
|
46
|
+
* identification, but because it is the unit the server scopes the idempotency key to, and without
|
|
47
|
+
* it the key is discarded. Any non-blank string the host passes still wins verbatim. See
|
|
48
|
+
* `idempotency.ts`.
|
|
45
49
|
*
|
|
46
50
|
* Motion (2026-07-16): the three phases used to swap IN PLACE with no transition. They now
|
|
47
51
|
* ride `ModalPaneHost`, the same pane-slide primitive the questionnaire steps use, and the
|
package/dist/reviews/index.js
CHANGED
|
@@ -546,6 +546,7 @@ var buildReviewSubmission = (input) => {
|
|
|
546
546
|
if (contact) body.contact = contact;
|
|
547
547
|
if (input.sessionId) body.session_id = input.sessionId;
|
|
548
548
|
if (input.source) body.source = input.source;
|
|
549
|
+
if (input.idempotencyKey) body.idempotency_key = input.idempotencyKey;
|
|
549
550
|
if (input.meta && Object.keys(input.meta).length > 0) body.meta = input.meta;
|
|
550
551
|
return body;
|
|
551
552
|
};
|
|
@@ -563,6 +564,43 @@ var feedbackInputHeights = (fonts, verticalPadding = 0) => {
|
|
|
563
564
|
};
|
|
564
565
|
};
|
|
565
566
|
|
|
567
|
+
// src/context/deviceId.ts
|
|
568
|
+
var AUTO_DEVICE_ID_PREFIX = "wdev_";
|
|
569
|
+
var randomChunk = () => Math.floor(Math.random() * 4294967296).toString(36).padStart(6, "0");
|
|
570
|
+
var mintDeviceId = () => {
|
|
571
|
+
const time = Date.now().toString(36);
|
|
572
|
+
return `${AUTO_DEVICE_ID_PREFIX}${time}_${randomChunk()}${randomChunk()}`;
|
|
573
|
+
};
|
|
574
|
+
var AUTO_DEVICE_KEY_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:autoDeviceKeys");
|
|
575
|
+
var deviceKeyGlobal = globalThis;
|
|
576
|
+
var autoDeviceKeyRegistry = () => {
|
|
577
|
+
const existing = deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT];
|
|
578
|
+
if (existing) return existing;
|
|
579
|
+
const created = { keys: /* @__PURE__ */ new Map(), hydrating: /* @__PURE__ */ new Set() };
|
|
580
|
+
deviceKeyGlobal[AUTO_DEVICE_KEY_SLOT] = created;
|
|
581
|
+
return created;
|
|
582
|
+
};
|
|
583
|
+
var ambientAutoDeviceKey = () => {
|
|
584
|
+
const registry = autoDeviceKeyRegistry();
|
|
585
|
+
if (registry.keys.size > 1) return void 0;
|
|
586
|
+
const sole = [...registry.keys.values()][0];
|
|
587
|
+
if (sole) return sole;
|
|
588
|
+
if (!registry.ambient) registry.ambient = mintDeviceId();
|
|
589
|
+
return registry.ambient;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/reviews/idempotency.ts
|
|
593
|
+
var REVIEW_IDEMPOTENCY_PREFIX = "rvw_";
|
|
594
|
+
var mintIdempotencyKey = () => `${REVIEW_IDEMPOTENCY_PREFIX}${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
595
|
+
var hasDeviceKey = (meta) => {
|
|
596
|
+
const value = meta == null ? void 0 : meta.device_key;
|
|
597
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
598
|
+
};
|
|
599
|
+
var createImpression = () => ({
|
|
600
|
+
idempotencyKey: mintIdempotencyKey(),
|
|
601
|
+
deviceKey: ambientAutoDeviceKey()
|
|
602
|
+
});
|
|
603
|
+
|
|
566
604
|
// src/motion/centeredModalMotion.ts
|
|
567
605
|
var CENTERED_MODAL_ANIM = {
|
|
568
606
|
/** Spring config for the card scale/opacity entrance (mirrors CompletionView's pop). */
|
|
@@ -740,6 +778,28 @@ var requestStoreReview = async (store) => {
|
|
|
740
778
|
return opened ? "store_url" : "none";
|
|
741
779
|
};
|
|
742
780
|
|
|
781
|
+
// src/utils/withDeadline.ts
|
|
782
|
+
var DEADLINE_EXPIRED = /* @__PURE__ */ Symbol("wire-deadline-expired");
|
|
783
|
+
var DEFAULT_DEADLINE_MS = 15e3;
|
|
784
|
+
var withDeadline = async (exchange, timeoutMs = DEFAULT_DEADLINE_MS) => {
|
|
785
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
786
|
+
let timer;
|
|
787
|
+
const expired = new Promise((resolve) => {
|
|
788
|
+
timer = setTimeout(() => {
|
|
789
|
+
controller == null ? void 0 : controller.abort();
|
|
790
|
+
resolve(DEADLINE_EXPIRED);
|
|
791
|
+
}, timeoutMs);
|
|
792
|
+
});
|
|
793
|
+
const running = exchange(controller == null ? void 0 : controller.signal);
|
|
794
|
+
try {
|
|
795
|
+
return await Promise.race([running, expired]);
|
|
796
|
+
} finally {
|
|
797
|
+
clearTimeout(timer);
|
|
798
|
+
void running.catch(() => {
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
|
|
743
803
|
// src/analytics/reportClientEvent.ts
|
|
744
804
|
var makeSessionId = () => `wire_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
745
805
|
var toWireEvents = (events) => events.map((event) => {
|
|
@@ -781,17 +841,51 @@ var CURRENT_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
|
|
|
781
841
|
"@wireai/activation:currentSessionId"
|
|
782
842
|
);
|
|
783
843
|
var globalSlot = globalThis;
|
|
844
|
+
var SESSION_ID_LISTENERS_SLOT = /* @__PURE__ */ Symbol.for(
|
|
845
|
+
"@wireai/activation:currentSessionIdListeners"
|
|
846
|
+
);
|
|
847
|
+
var listenerSlot = globalThis;
|
|
848
|
+
var listeners = () => {
|
|
849
|
+
const existing = listenerSlot[SESSION_ID_LISTENERS_SLOT];
|
|
850
|
+
if (existing) return existing;
|
|
851
|
+
const created = /* @__PURE__ */ new Set();
|
|
852
|
+
listenerSlot[SESSION_ID_LISTENERS_SLOT] = created;
|
|
853
|
+
return created;
|
|
854
|
+
};
|
|
855
|
+
var writeCurrentSessionId = (id) => {
|
|
856
|
+
if (globalSlot[CURRENT_SESSION_ID_SLOT] === id) return;
|
|
857
|
+
globalSlot[CURRENT_SESSION_ID_SLOT] = id;
|
|
858
|
+
for (const listener of Array.from(listeners())) {
|
|
859
|
+
try {
|
|
860
|
+
listener();
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
var subscribeCurrentSessionId = (listener) => {
|
|
866
|
+
const set = listeners();
|
|
867
|
+
set.add(listener);
|
|
868
|
+
return () => {
|
|
869
|
+
set.delete(listener);
|
|
870
|
+
};
|
|
871
|
+
};
|
|
784
872
|
var getCurrentSessionId = () => globalSlot[CURRENT_SESSION_ID_SLOT];
|
|
785
873
|
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.";
|
|
786
874
|
var MINT_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
|
|
787
875
|
"@wireai/activation:currentSessionIdMintWarned"
|
|
788
876
|
);
|
|
789
877
|
var warnSlot = globalThis;
|
|
878
|
+
var MINTED_SESSION_ID_SLOT = /* @__PURE__ */ Symbol.for(
|
|
879
|
+
"@wireai/activation:mintedSessionId"
|
|
880
|
+
);
|
|
881
|
+
var mintedSlot = globalThis;
|
|
882
|
+
var isMintedSessionId = (id) => id !== void 0 && id === mintedSlot[MINTED_SESSION_ID_SLOT];
|
|
790
883
|
var ensureCurrentSessionId = () => {
|
|
791
884
|
const existing = globalSlot[CURRENT_SESSION_ID_SLOT];
|
|
792
885
|
if (typeof existing === "string" && existing.length > 0) return existing;
|
|
793
886
|
const minted = makeSessionId();
|
|
794
|
-
|
|
887
|
+
writeCurrentSessionId(minted);
|
|
888
|
+
mintedSlot[MINTED_SESSION_ID_SLOT] = minted;
|
|
795
889
|
if (!warnSlot[MINT_WARNED_SLOT] && warnInDev(MINT_WARNING)) {
|
|
796
890
|
warnSlot[MINT_WARNED_SLOT] = true;
|
|
797
891
|
}
|
|
@@ -805,11 +899,15 @@ var submitReview = async (target, review) => {
|
|
|
805
899
|
const url = `${target.serverUrl.replace(/\/$/, "")}/v1/reviews`;
|
|
806
900
|
const headers = { "Content-Type": "application/json" };
|
|
807
901
|
if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
|
|
808
|
-
const res = await
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
902
|
+
const res = await withDeadline(
|
|
903
|
+
(signal) => fetch(url, {
|
|
904
|
+
method: "POST",
|
|
905
|
+
headers,
|
|
906
|
+
body: JSON.stringify(review),
|
|
907
|
+
signal
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
if (res === DEADLINE_EXPIRED) return "unsent";
|
|
813
911
|
if (!res) return "unsent";
|
|
814
912
|
return res.ok ? "accepted" : "rejected";
|
|
815
913
|
} catch {
|
|
@@ -827,9 +925,12 @@ var fetchReviewDecision = async (target, options = {}) => {
|
|
|
827
925
|
const url = `${base}/v1/reviews/decision${qs ? `?${qs}` : ""}`;
|
|
828
926
|
const headers = {};
|
|
829
927
|
if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
928
|
+
const json = await withDeadline(async (signal) => {
|
|
929
|
+
const res = await fetch(url, { headers, signal });
|
|
930
|
+
if (!res || !res.ok) return null;
|
|
931
|
+
return await res.json();
|
|
932
|
+
});
|
|
933
|
+
if (json === DEADLINE_EXPIRED) return null;
|
|
833
934
|
if (!json || typeof json.fire !== "boolean") return null;
|
|
834
935
|
return json;
|
|
835
936
|
} catch {
|
|
@@ -901,8 +1002,33 @@ var _ReviewGate = ({
|
|
|
901
1002
|
const resolve = React7.useCallback(() => {
|
|
902
1003
|
onResolved == null ? void 0 : onResolved();
|
|
903
1004
|
}, [onResolved]);
|
|
1005
|
+
const impressionRef = React7__default.default.useRef(null);
|
|
1006
|
+
const buildBody = React7.useCallback(
|
|
1007
|
+
(input) => {
|
|
1008
|
+
let impression = impressionRef.current;
|
|
1009
|
+
if (!impression) {
|
|
1010
|
+
impression = createImpression();
|
|
1011
|
+
impressionRef.current = impression;
|
|
1012
|
+
}
|
|
1013
|
+
const merged = { ...meta, ...input.extraMeta };
|
|
1014
|
+
if (!hasDeviceKey(merged) && impression.deviceKey) merged.device_key = impression.deviceKey;
|
|
1015
|
+
return buildReviewSubmission({
|
|
1016
|
+
stars: input.stars,
|
|
1017
|
+
feedbackText: input.feedbackText,
|
|
1018
|
+
suggestion: input.suggestion,
|
|
1019
|
+
sessionId,
|
|
1020
|
+
meta: merged,
|
|
1021
|
+
idempotencyKey: impression.idempotencyKey
|
|
1022
|
+
});
|
|
1023
|
+
},
|
|
1024
|
+
[meta, sessionId]
|
|
1025
|
+
);
|
|
904
1026
|
const postedRef = React7__default.default.useRef(false);
|
|
905
1027
|
const unackedRef = React7__default.default.useRef(null);
|
|
1028
|
+
const recoverOnSettleRef = React7__default.default.useRef(false);
|
|
1029
|
+
const recoveredRef = React7__default.default.useRef(false);
|
|
1030
|
+
const postOnceRef = React7__default.default.useRef(() => {
|
|
1031
|
+
});
|
|
906
1032
|
const postOnce = React7.useCallback(
|
|
907
1033
|
(body) => {
|
|
908
1034
|
if (postedRef.current) return;
|
|
@@ -911,6 +1037,10 @@ var _ReviewGate = ({
|
|
|
911
1037
|
void submitReview(target, body).then((result) => {
|
|
912
1038
|
if (result === "unsent") {
|
|
913
1039
|
postedRef.current = false;
|
|
1040
|
+
if (recoverOnSettleRef.current && !recoveredRef.current) {
|
|
1041
|
+
recoveredRef.current = true;
|
|
1042
|
+
postOnceRef.current(body);
|
|
1043
|
+
}
|
|
914
1044
|
return;
|
|
915
1045
|
}
|
|
916
1046
|
unackedRef.current = null;
|
|
@@ -918,6 +1048,7 @@ var _ReviewGate = ({
|
|
|
918
1048
|
},
|
|
919
1049
|
[target]
|
|
920
1050
|
);
|
|
1051
|
+
postOnceRef.current = postOnce;
|
|
921
1052
|
const abandonRef = React7__default.default.useRef(() => {
|
|
922
1053
|
});
|
|
923
1054
|
abandonRef.current = () => {
|
|
@@ -926,14 +1057,17 @@ var _ReviewGate = ({
|
|
|
926
1057
|
postOnce(undelivered);
|
|
927
1058
|
return;
|
|
928
1059
|
}
|
|
1060
|
+
if (postedRef.current && undelivered) {
|
|
1061
|
+
recoverOnSettleRef.current = true;
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
929
1064
|
if (postedRef.current || stars < 1) return;
|
|
930
1065
|
postOnce(
|
|
931
|
-
|
|
1066
|
+
buildBody({
|
|
932
1067
|
stars,
|
|
933
1068
|
feedbackText: feedback,
|
|
934
1069
|
suggestion,
|
|
935
|
-
|
|
936
|
-
meta: { ...meta, abandoned: true }
|
|
1070
|
+
extraMeta: { abandoned: true }
|
|
937
1071
|
})
|
|
938
1072
|
);
|
|
939
1073
|
};
|
|
@@ -958,10 +1092,9 @@ var _ReviewGate = ({
|
|
|
958
1092
|
onEvent == null ? void 0 : onEvent({ name: "store_review_requested", id, stars: value });
|
|
959
1093
|
const route = await requestStoreReview(store);
|
|
960
1094
|
postOnce(
|
|
961
|
-
|
|
1095
|
+
buildBody({
|
|
962
1096
|
stars: value,
|
|
963
|
-
|
|
964
|
-
meta: { ...meta, store_route: route }
|
|
1097
|
+
extraMeta: { store_route: route }
|
|
965
1098
|
})
|
|
966
1099
|
);
|
|
967
1100
|
setPhase("thanks");
|
|
@@ -970,24 +1103,18 @@ var _ReviewGate = ({
|
|
|
970
1103
|
setPhase("feedback");
|
|
971
1104
|
}
|
|
972
1105
|
},
|
|
973
|
-
[onEvent, id, store, postOnce,
|
|
1106
|
+
[onEvent, id, store, postOnce, buildBody, resolveWithDelay]
|
|
974
1107
|
);
|
|
975
1108
|
const feedbackReady = feedback.trim().length > 0;
|
|
976
1109
|
const sendFeedback = React7.useCallback(() => {
|
|
977
1110
|
if (!feedbackReady) return;
|
|
978
|
-
const body =
|
|
979
|
-
stars,
|
|
980
|
-
feedbackText: feedback,
|
|
981
|
-
suggestion,
|
|
982
|
-
sessionId,
|
|
983
|
-
meta
|
|
984
|
-
});
|
|
1111
|
+
const body = buildBody({ stars, feedbackText: feedback, suggestion });
|
|
985
1112
|
postOnce(body);
|
|
986
1113
|
onEvent == null ? void 0 : onEvent({ name: "review_feedback_submitted", id, stars });
|
|
987
1114
|
playHaptic("success");
|
|
988
1115
|
setPhase("thanks");
|
|
989
1116
|
resolveWithDelay();
|
|
990
|
-
}, [feedbackReady, stars, feedback, suggestion,
|
|
1117
|
+
}, [feedbackReady, stars, feedback, suggestion, buildBody, postOnce, onEvent, id, resolveWithDelay]);
|
|
991
1118
|
const dismiss = React7.useCallback(() => {
|
|
992
1119
|
setPhase("thanks");
|
|
993
1120
|
resolveWithDelay();
|
|
@@ -1134,9 +1261,54 @@ var coachmarkRuntime = () => {
|
|
|
1134
1261
|
var getCoachmarkStorage = () => coachmarkRuntime().storage;
|
|
1135
1262
|
var isCoachmarkTesting = () => coachmarkRuntime().testing;
|
|
1136
1263
|
var SEEN_VALUE = "1";
|
|
1264
|
+
var GATE_PROBE_KEY = "wire_gate_probe";
|
|
1265
|
+
var ASYNC_GATE_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
|
|
1266
|
+
"@wireai/activation:asyncGateStorageWarned"
|
|
1267
|
+
);
|
|
1268
|
+
var asyncGateWarnGlobal = globalThis;
|
|
1269
|
+
var closedGateStorage = Object.freeze({
|
|
1270
|
+
getItem: () => SEEN_VALUE,
|
|
1271
|
+
setItem: () => {
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
var syncGateAdapters = /* @__PURE__ */ new WeakSet();
|
|
1275
|
+
var asyncGateAdapters = /* @__PURE__ */ new WeakSet();
|
|
1276
|
+
var warnAsyncGateStorage = () => {
|
|
1277
|
+
if (asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT]) return;
|
|
1278
|
+
if (typeof __DEV__ === "undefined" || !__DEV__) return;
|
|
1279
|
+
if (typeof console === "undefined" || !console.warn) return;
|
|
1280
|
+
asyncGateWarnGlobal[ASYNC_GATE_STORAGE_WARNED_SLOT] = true;
|
|
1281
|
+
console.warn(
|
|
1282
|
+
"[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."
|
|
1283
|
+
);
|
|
1284
|
+
};
|
|
1285
|
+
var validateGateStorage = (storage) => {
|
|
1286
|
+
if (!storage) return null;
|
|
1287
|
+
if (syncGateAdapters.has(storage)) return storage;
|
|
1288
|
+
if (asyncGateAdapters.has(storage)) return closedGateStorage;
|
|
1289
|
+
let probed;
|
|
1290
|
+
try {
|
|
1291
|
+
probed = storage.getItem(GATE_PROBE_KEY);
|
|
1292
|
+
} catch {
|
|
1293
|
+
syncGateAdapters.add(storage);
|
|
1294
|
+
return storage;
|
|
1295
|
+
}
|
|
1296
|
+
if (probed != null && typeof probed.then === "function") {
|
|
1297
|
+
asyncGateAdapters.add(storage);
|
|
1298
|
+
try {
|
|
1299
|
+
probed.then(void 0, () => {
|
|
1300
|
+
});
|
|
1301
|
+
} catch {
|
|
1302
|
+
}
|
|
1303
|
+
warnAsyncGateStorage();
|
|
1304
|
+
return closedGateStorage;
|
|
1305
|
+
}
|
|
1306
|
+
syncGateAdapters.add(storage);
|
|
1307
|
+
return storage;
|
|
1308
|
+
};
|
|
1137
1309
|
var hasSeenGate = (key, storageOverride, testingOverride) => {
|
|
1138
1310
|
if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return false;
|
|
1139
|
-
const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
|
|
1311
|
+
const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
|
|
1140
1312
|
if (!storage) return false;
|
|
1141
1313
|
try {
|
|
1142
1314
|
return storage.getItem(key) === SEEN_VALUE;
|
|
@@ -1146,7 +1318,7 @@ var hasSeenGate = (key, storageOverride, testingOverride) => {
|
|
|
1146
1318
|
};
|
|
1147
1319
|
var markSeenGate = (key, storageOverride, testingOverride) => {
|
|
1148
1320
|
if (testingOverride != null ? testingOverride : isCoachmarkTesting()) return;
|
|
1149
|
-
const storage = storageOverride != null ? storageOverride : getCoachmarkStorage();
|
|
1321
|
+
const storage = validateGateStorage(storageOverride != null ? storageOverride : getCoachmarkStorage());
|
|
1150
1322
|
if (!storage) return;
|
|
1151
1323
|
try {
|
|
1152
1324
|
storage.setItem(key, SEEN_VALUE);
|
|
@@ -1234,17 +1406,31 @@ var failOpen = async (storage, key) => {
|
|
|
1234
1406
|
const cached = await readCachedFeatures(storage, key);
|
|
1235
1407
|
return (_a = cached == null ? void 0 : cached.features) != null ? _a : defaultWireFeatures;
|
|
1236
1408
|
};
|
|
1237
|
-
var
|
|
1409
|
+
var NO_ANSWER = /* @__PURE__ */ Symbol("wire-features-no-answer");
|
|
1410
|
+
var fetchFeaturesJson = async (url, apiKey, timeoutMs) => {
|
|
1238
1411
|
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1412
|
+
let timer;
|
|
1413
|
+
const expired = new Promise((resolve) => {
|
|
1414
|
+
timer = setTimeout(() => {
|
|
1415
|
+
controller == null ? void 0 : controller.abort();
|
|
1416
|
+
resolve(NO_ANSWER);
|
|
1417
|
+
}, timeoutMs);
|
|
1418
|
+
});
|
|
1419
|
+
const exchange = (async () => {
|
|
1420
|
+
const res = await fetch(url, {
|
|
1242
1421
|
method: "GET",
|
|
1243
1422
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
1244
1423
|
signal: controller == null ? void 0 : controller.signal
|
|
1245
1424
|
});
|
|
1425
|
+
if (!res || !res.ok) return NO_ANSWER;
|
|
1426
|
+
return await res.json();
|
|
1427
|
+
})();
|
|
1428
|
+
try {
|
|
1429
|
+
return await Promise.race([exchange, expired]);
|
|
1246
1430
|
} finally {
|
|
1247
1431
|
clearTimeout(timer);
|
|
1432
|
+
void exchange.catch(() => {
|
|
1433
|
+
});
|
|
1248
1434
|
}
|
|
1249
1435
|
};
|
|
1250
1436
|
var fetchWireFeatures = async (config) => {
|
|
@@ -1254,9 +1440,8 @@ var fetchWireFeatures = async (config) => {
|
|
|
1254
1440
|
const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
|
|
1255
1441
|
const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
1256
1442
|
try {
|
|
1257
|
-
const
|
|
1258
|
-
if (
|
|
1259
|
-
const json = await res.json();
|
|
1443
|
+
const json = await fetchFeaturesJson(url, config.apiKey, timeoutMs);
|
|
1444
|
+
if (json === NO_ANSWER) return failOpen(storage, key);
|
|
1260
1445
|
const features = parseWireFeatures(json);
|
|
1261
1446
|
if (storage) writeCachedFeatures(storage, key, features);
|
|
1262
1447
|
return features;
|
|
@@ -1272,39 +1457,51 @@ var useStableValue = (value, isEqual) => {
|
|
|
1272
1457
|
return ref.current;
|
|
1273
1458
|
};
|
|
1274
1459
|
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);
|
|
1275
|
-
var
|
|
1460
|
+
var useWireFeaturesState = (config) => {
|
|
1276
1461
|
const stableConfig = useStableValue(config, sameFetchKey);
|
|
1277
1462
|
const [flags, setFlags] = React7.useState(defaultWireFeatures);
|
|
1278
1463
|
const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
|
|
1464
|
+
const [settled, setSettled] = React7.useState(!canFetch);
|
|
1279
1465
|
React7.useEffect(() => {
|
|
1280
1466
|
if (!canFetch) {
|
|
1281
1467
|
setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
|
|
1468
|
+
setSettled(true);
|
|
1282
1469
|
return;
|
|
1283
1470
|
}
|
|
1284
1471
|
let alive = true;
|
|
1285
1472
|
void fetchWireFeatures(stableConfig).then((next) => {
|
|
1286
1473
|
if (!alive) return;
|
|
1287
1474
|
setFlags((prev) => featuresEqual(prev, next) ? prev : next);
|
|
1475
|
+
setSettled(true);
|
|
1288
1476
|
});
|
|
1289
1477
|
return () => {
|
|
1290
1478
|
alive = false;
|
|
1291
1479
|
};
|
|
1292
1480
|
}, [canFetch, stableConfig]);
|
|
1293
|
-
return flags;
|
|
1481
|
+
return React7.useMemo(() => ({ flags, settled }), [flags, settled]);
|
|
1294
1482
|
};
|
|
1295
1483
|
var CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.context");
|
|
1296
|
-
var
|
|
1297
|
-
if (!
|
|
1298
|
-
|
|
1484
|
+
var featuresContextGlobal = globalThis;
|
|
1485
|
+
if (!featuresContextGlobal[CONTEXT_SYMBOL]) {
|
|
1486
|
+
featuresContextGlobal[CONTEXT_SYMBOL] = React7.createContext(null);
|
|
1299
1487
|
}
|
|
1300
|
-
var WireFeaturesContext =
|
|
1488
|
+
var WireFeaturesContext = featuresContextGlobal[CONTEXT_SYMBOL];
|
|
1489
|
+
var SETTLED_SYMBOL = /* @__PURE__ */ Symbol.for("wireai.features.settled.context");
|
|
1490
|
+
var settledContextGlobal = globalThis;
|
|
1491
|
+
if (!settledContextGlobal[SETTLED_SYMBOL]) {
|
|
1492
|
+
settledContextGlobal[SETTLED_SYMBOL] = React7.createContext(true);
|
|
1493
|
+
}
|
|
1494
|
+
var WireFeaturesSettledContext = settledContextGlobal[SETTLED_SYMBOL];
|
|
1301
1495
|
var useWireFeaturesContext = () => React7.useContext(WireFeaturesContext);
|
|
1302
|
-
var
|
|
1496
|
+
var useResolvedFeaturesState = (options) => {
|
|
1303
1497
|
var _a, _b, _c;
|
|
1304
1498
|
const ctx = useWireFeaturesContext();
|
|
1499
|
+
const ctxSettled = React7.useContext(WireFeaturesSettledContext);
|
|
1305
1500
|
const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
|
|
1306
|
-
const fetched =
|
|
1307
|
-
|
|
1501
|
+
const fetched = useWireFeaturesState(shouldFetch ? options == null ? void 0 : options.config : void 0);
|
|
1502
|
+
const flags = (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched.flags) != null ? _c : defaultWireFeatures;
|
|
1503
|
+
const settled = (options == null ? void 0 : options.flags) ? true : ctx != null ? ctxSettled : fetched.settled;
|
|
1504
|
+
return React7.useMemo(() => ({ flags, settled }), [flags, settled]);
|
|
1308
1505
|
};
|
|
1309
1506
|
|
|
1310
1507
|
// src/reviews/equality.ts
|
|
@@ -1333,7 +1530,7 @@ var reviewSeenKey = (id, version) => version ? `wire_review_${id}_${version}_see
|
|
|
1333
1530
|
var reviewLastShownKey = (id) => `wire_review_${id}_last`;
|
|
1334
1531
|
var reviewSessionsKey = (id) => `wire_review_${id}_sessions`;
|
|
1335
1532
|
var reviewSessionOpenKey = (id) => `wire_review_${id}_open`;
|
|
1336
|
-
var resolveStorage = (override) => override != null ? override : getCoachmarkStorage();
|
|
1533
|
+
var resolveStorage = (override) => validateGateStorage(override != null ? override : getCoachmarkStorage());
|
|
1337
1534
|
var NO_STORAGE_WARNED_SLOT = /* @__PURE__ */ Symbol.for(
|
|
1338
1535
|
"@wireai/activation:gateStorageWarned"
|
|
1339
1536
|
);
|
|
@@ -1378,18 +1575,23 @@ var PROCESS_OPEN_ID_SLOT = /* @__PURE__ */ Symbol.for("@wireai/activation:proces
|
|
|
1378
1575
|
var openIdGlobal = globalThis;
|
|
1379
1576
|
var currentOpenId = () => {
|
|
1380
1577
|
const live = getCurrentSessionId();
|
|
1578
|
+
const liveMinted = isMintedSessionId(live);
|
|
1381
1579
|
const pin = openIdGlobal[PROCESS_OPEN_ID_SLOT];
|
|
1382
1580
|
if (!pin) {
|
|
1383
1581
|
const openId = live != null ? live : makeSessionId();
|
|
1384
|
-
openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live };
|
|
1582
|
+
openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId, observed: live, observedMinted: liveMinted };
|
|
1385
1583
|
return openId;
|
|
1386
1584
|
}
|
|
1387
1585
|
if (!live || live === pin.observed) return pin.openId;
|
|
1388
|
-
if (pin.observed === void 0) {
|
|
1389
|
-
openIdGlobal[PROCESS_OPEN_ID_SLOT] = {
|
|
1586
|
+
if (pin.observed === void 0 || pin.observedMinted) {
|
|
1587
|
+
openIdGlobal[PROCESS_OPEN_ID_SLOT] = {
|
|
1588
|
+
openId: pin.openId,
|
|
1589
|
+
observed: live,
|
|
1590
|
+
observedMinted: liveMinted
|
|
1591
|
+
};
|
|
1390
1592
|
return pin.openId;
|
|
1391
1593
|
}
|
|
1392
|
-
openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live };
|
|
1594
|
+
openIdGlobal[PROCESS_OPEN_ID_SLOT] = { openId: live, observed: live, observedMinted: liveMinted };
|
|
1393
1595
|
return live;
|
|
1394
1596
|
};
|
|
1395
1597
|
var bumpSessionCount = (storage, sessionsKey, openKey, openId = currentOpenId()) => {
|
|
@@ -1427,17 +1629,32 @@ var useReviewGate = ({
|
|
|
1427
1629
|
decisionProp,
|
|
1428
1630
|
(a, b) => sameDecision(a, b)
|
|
1429
1631
|
);
|
|
1430
|
-
const
|
|
1632
|
+
const { flags: resolvedFeatures, settled: featuresSettled } = useResolvedFeaturesState({
|
|
1633
|
+
flags: features,
|
|
1634
|
+
config: featuresConfig
|
|
1635
|
+
});
|
|
1636
|
+
const featureEnabled = resolvedFeatures.review.enabled;
|
|
1431
1637
|
const testing = isTesting != null ? isTesting : isCoachmarkTesting();
|
|
1432
1638
|
const seenKey = reviewSeenKey(config.id, config.oncePerVersion === false ? void 0 : config.appVersion);
|
|
1433
1639
|
const lastKey = reviewLastShownKey(config.id);
|
|
1434
1640
|
const sessionsKey = reviewSessionsKey(config.id);
|
|
1435
1641
|
const sessionOpenKey = reviewSessionOpenKey(config.id);
|
|
1436
|
-
const
|
|
1642
|
+
const openSessionId = React7.useSyncExternalStore(
|
|
1643
|
+
subscribeCurrentSessionId,
|
|
1644
|
+
getCurrentSessionId,
|
|
1645
|
+
getCurrentSessionId
|
|
1646
|
+
);
|
|
1647
|
+
const storageRef = React7.useRef(storage);
|
|
1648
|
+
storageRef.current = storage;
|
|
1649
|
+
const [sessions, setSessions] = React7.useState(() => {
|
|
1437
1650
|
const store = resolveStorage(storage);
|
|
1438
1651
|
warnMissingGateStorage(store, "review");
|
|
1439
1652
|
return bumpSessionCount(store, sessionsKey, sessionOpenKey);
|
|
1440
|
-
})
|
|
1653
|
+
});
|
|
1654
|
+
React7.useEffect(() => {
|
|
1655
|
+
const next = bumpSessionCount(resolveStorage(storageRef.current), sessionsKey, sessionOpenKey);
|
|
1656
|
+
setSessions((prev) => prev === next ? prev : next);
|
|
1657
|
+
}, [openSessionId, sessionsKey, sessionOpenKey]);
|
|
1441
1658
|
const hasServerDecision = decision != null;
|
|
1442
1659
|
const timeoutMs = config.timeoutFallbackMs;
|
|
1443
1660
|
const waits = !hasServerDecision && timeoutMs != null && timeoutMs > 0;
|
|
@@ -1463,9 +1680,9 @@ var useReviewGate = ({
|
|
|
1463
1680
|
});
|
|
1464
1681
|
const resolved = decideReview(local, decision);
|
|
1465
1682
|
const alreadySeen = hasSeenGate(seenKey, storage, isTesting);
|
|
1466
|
-
const ready = hasServerDecision || elapsed;
|
|
1683
|
+
const ready = (hasServerDecision || elapsed) && featuresSettled;
|
|
1467
1684
|
return { visible: ready && resolved.fire && !alreadySeen, verdict: resolved };
|
|
1468
|
-
}, [featureEnabled, testing, config, decision, events, sessions, elapsed, hasServerDecision, seenKey, lastKey, storage, isTesting]);
|
|
1685
|
+
}, [featureEnabled, featuresSettled, testing, config, decision, events, sessions, elapsed, hasServerDecision, seenKey, lastKey, storage, isTesting]);
|
|
1469
1686
|
const shownRef = React7.useRef(false);
|
|
1470
1687
|
const markShown = React7.useCallback(() => {
|
|
1471
1688
|
if (shownRef.current) return;
|