@wireai/activation 0.1.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 +73 -0
- package/CHANGELOG.md +572 -0
- package/INTEGRATION_PROMPT.md +55 -0
- package/LICENSE +21 -0
- package/README.md +923 -0
- package/dist/CenteredModal-C3qQBHsA.d.mts +38 -0
- package/dist/CenteredModal-Cdgns6--.d.ts +38 -0
- package/dist/coachmarks/index.d.mts +352 -0
- package/dist/coachmarks/index.d.ts +352 -0
- package/dist/coachmarks/index.js +944 -0
- package/dist/coachmarks/index.js.map +1 -0
- package/dist/coachmarks/index.mjs +918 -0
- package/dist/coachmarks/index.mjs.map +1 -0
- package/dist/decision-CFvGY6nP.d.ts +228 -0
- package/dist/decision-Cl8OFYzu.d.mts +228 -0
- package/dist/index.d.mts +1390 -0
- package/dist/index.d.ts +1390 -0
- package/dist/index.js +3260 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3186 -0
- package/dist/index.mjs.map +1 -0
- package/dist/questionnaire/index.d.mts +249 -0
- package/dist/questionnaire/index.d.ts +249 -0
- package/dist/questionnaire/index.js +922 -0
- package/dist/questionnaire/index.js.map +1 -0
- package/dist/questionnaire/index.mjs +906 -0
- package/dist/questionnaire/index.mjs.map +1 -0
- package/dist/reviews/index.d.mts +213 -0
- package/dist/reviews/index.d.ts +213 -0
- package/dist/reviews/index.js +1001 -0
- package/dist/reviews/index.js.map +1 -0
- package/dist/reviews/index.mjs +971 -0
- package/dist/reviews/index.mjs.map +1 -0
- package/dist/showcase/index.d.mts +119 -0
- package/dist/showcase/index.d.ts +119 -0
- package/dist/showcase/index.js +660 -0
- package/dist/showcase/index.js.map +1 -0
- package/dist/showcase/index.mjs +651 -0
- package/dist/showcase/index.mjs.map +1 -0
- package/dist/types-BKfpdZzX.d.mts +84 -0
- package/dist/types-BKfpdZzX.d.ts +84 -0
- package/dist/types-CMuOexw0.d.mts +116 -0
- package/dist/types-CMuOexw0.d.ts +116 -0
- package/dist/types-GL_hQ0TN.d.mts +46 -0
- package/dist/types-GL_hQ0TN.d.ts +46 -0
- package/llms.txt +36 -0
- package/metro/index.d.ts +32 -0
- package/metro/index.js +83 -0
- package/package.json +171 -0
- package/src/DemoOnboarding.tsx +133 -0
- package/src/OnboardingFlow.tsx +504 -0
- package/src/WireOnboarding.tsx +248 -0
- package/src/analytics/analyticsEvent.ts +69 -0
- package/src/analytics/reportClientEvent.ts +119 -0
- package/src/analytics/sendPreview.ts +73 -0
- package/src/attribution/attribution.ts +46 -0
- package/src/cards/ChipSelectCard.tsx +265 -0
- package/src/cards/InterstitialCard.tsx +299 -0
- package/src/cards/NumberStepperCard.tsx +121 -0
- package/src/cards/SelectionCard.tsx +270 -0
- package/src/cards/StatusCard.tsx +170 -0
- package/src/cards/TextInputCard.tsx +147 -0
- package/src/cards/index.ts +23 -0
- package/src/coachmarks/CoachmarkOverlayHost.tsx +44 -0
- package/src/coachmarks/CoachmarkProvider.tsx +108 -0
- package/src/coachmarks/GestureHint.tsx +331 -0
- package/src/coachmarks/SpotlightOverlay.tsx +254 -0
- package/src/coachmarks/coachmarkAnchorRegistry.ts +70 -0
- package/src/coachmarks/coachmarkOverlayStore.ts +63 -0
- package/src/coachmarks/index.ts +62 -0
- package/src/coachmarks/runtime.ts +103 -0
- package/src/coachmarks/selectTourSteps.ts +45 -0
- package/src/coachmarks/types.ts +58 -0
- package/src/coachmarks/useCoachmarkAnchor.ts +49 -0
- package/src/coachmarks/useCoachmarkTour.ts +200 -0
- package/src/components/AnimatedSparkle.tsx +101 -0
- package/src/components/Button.tsx +129 -0
- package/src/components/CardHandoff.tsx +212 -0
- package/src/components/CardLayout.tsx +103 -0
- package/src/components/CenteredModal.tsx +184 -0
- package/src/components/CompletionView.tsx +186 -0
- package/src/components/DoneBlock.tsx +37 -0
- package/src/components/ErrorBlock.tsx +42 -0
- package/src/components/Illustration.tsx +36 -0
- package/src/components/LoadingBlock.tsx +182 -0
- package/src/components/LoadingScreen.tsx +35 -0
- package/src/components/OnboardingScaffold.tsx +105 -0
- package/src/components/StepProgress.tsx +85 -0
- package/src/components/loaderChrome.ts +28 -0
- package/src/config/onboardingFlag.ts +39 -0
- package/src/config/wireConfigFromEnv.ts +52 -0
- package/src/device/deviceContext.ts +158 -0
- package/src/features/WireFeaturesProvider.tsx +81 -0
- package/src/features/cache.ts +92 -0
- package/src/features/defaults.ts +67 -0
- package/src/features/fetchWireFeatures.ts +86 -0
- package/src/features/index.ts +39 -0
- package/src/features/types.ts +61 -0
- package/src/features/useWireFeatures.ts +57 -0
- package/src/identity/userIdentity.ts +100 -0
- package/src/illustrations/defaultIllustrations.tsx +99 -0
- package/src/index.ts +159 -0
- package/src/motion/centeredModalMotion.ts +45 -0
- package/src/motion/motionSpec.ts +133 -0
- package/src/motion/useReducedMotion.ts +43 -0
- package/src/questionnaire/QuestionnaireGate.tsx +235 -0
- package/src/questionnaire/decision.ts +63 -0
- package/src/questionnaire/index.ts +54 -0
- package/src/questionnaire/runtime.ts +20 -0
- package/src/questionnaire/transport.ts +83 -0
- package/src/questionnaire/types.ts +182 -0
- package/src/questionnaire/useQuestionnaireGate.ts +159 -0
- package/src/reviews/ReviewGate.tsx +316 -0
- package/src/reviews/ReviewModal.tsx +13 -0
- package/src/reviews/decision.ts +120 -0
- package/src/reviews/equality.ts +54 -0
- package/src/reviews/expo-store-review.d.ts +16 -0
- package/src/reviews/feedbackForm.ts +41 -0
- package/src/reviews/index.ts +78 -0
- package/src/reviews/presentation.ts +29 -0
- package/src/reviews/runtime.ts +45 -0
- package/src/reviews/storeReview.ts +62 -0
- package/src/reviews/transport.ts +89 -0
- package/src/reviews/types.ts +172 -0
- package/src/reviews/useReviewGate.ts +137 -0
- package/src/session/persistedSession.ts +143 -0
- package/src/session-analytics/index.ts +18 -0
- package/src/session-analytics/reportSessionStart.ts +144 -0
- package/src/session-analytics/useSessionStart.ts +114 -0
- package/src/showcase/FeatureShowcase.tsx +233 -0
- package/src/showcase/index.ts +19 -0
- package/src/showcase/selectShowcaseSlides.ts +45 -0
- package/src/showcase/showcaseColors.ts +91 -0
- package/src/showcase/types.ts +74 -0
- package/src/theme/ThemeContext.tsx +38 -0
- package/src/theme/defaultTheme.ts +70 -0
- package/src/theme/themeFromBrand.ts +93 -0
- package/src/theme/types.ts +88 -0
- package/src/theme/typography.ts +46 -0
- package/src/types.ts +215 -0
- package/src/utils/deriveAnswers.ts +43 -0
- package/src/utils/readProgress.ts +33 -0
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React4 = require('react');
|
|
4
|
+
var reactNative = require('react-native');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var reactNativeSafeAreaContext = require('react-native-safe-area-context');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var React4__default = /*#__PURE__*/_interopDefault(React4);
|
|
11
|
+
|
|
12
|
+
// src/questionnaire/QuestionnaireGate.tsx
|
|
13
|
+
|
|
14
|
+
// src/theme/defaultTheme.ts
|
|
15
|
+
var defaultOnboardingTheme = {
|
|
16
|
+
colors: {
|
|
17
|
+
background: "#FFFFFF",
|
|
18
|
+
surface: "#F7F7F8",
|
|
19
|
+
text: "#101114",
|
|
20
|
+
textMuted: "#6B7280",
|
|
21
|
+
primary: "#111111",
|
|
22
|
+
onPrimary: "#FFFFFF",
|
|
23
|
+
primarySoft: "rgba(17,17,17,0.06)",
|
|
24
|
+
border: "#E4E4E7",
|
|
25
|
+
borderStrong: "#C7C7CC",
|
|
26
|
+
disabled: "#EBEBEB",
|
|
27
|
+
success: "#16A34A",
|
|
28
|
+
warning: "#D97706",
|
|
29
|
+
error: "#DC2626",
|
|
30
|
+
info: "#2563EB"
|
|
31
|
+
},
|
|
32
|
+
fonts: {
|
|
33
|
+
regular: void 0,
|
|
34
|
+
medium: void 0,
|
|
35
|
+
bold: void 0,
|
|
36
|
+
baseSize: 16,
|
|
37
|
+
scale: 1
|
|
38
|
+
},
|
|
39
|
+
radius: {
|
|
40
|
+
sm: 8,
|
|
41
|
+
md: 12,
|
|
42
|
+
lg: 16,
|
|
43
|
+
full: 9999
|
|
44
|
+
},
|
|
45
|
+
spacing: {
|
|
46
|
+
xs: 4,
|
|
47
|
+
sm: 8,
|
|
48
|
+
md: 16,
|
|
49
|
+
lg: 24,
|
|
50
|
+
xl: 32
|
|
51
|
+
},
|
|
52
|
+
// Circled (pill) CTA by default; an app can pass `button: { radius: 12 }` for
|
|
53
|
+
// a rounded rectangle, or override height/paddingX to match its design system.
|
|
54
|
+
button: {
|
|
55
|
+
radius: 9999,
|
|
56
|
+
height: 52,
|
|
57
|
+
paddingX: 24
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var mergeTheme = (override) => {
|
|
61
|
+
return defaultOnboardingTheme;
|
|
62
|
+
};
|
|
63
|
+
var OnboardingThemeContext = React4.createContext(null);
|
|
64
|
+
var useOnboardingTheme = () => {
|
|
65
|
+
const ctx = React4.useContext(OnboardingThemeContext);
|
|
66
|
+
return ctx != null ? ctx : mergeTheme();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/theme/typography.ts
|
|
70
|
+
var round = (n) => Math.round(n);
|
|
71
|
+
var bodyStyle = (f) => ({
|
|
72
|
+
fontFamily: f.regular,
|
|
73
|
+
fontSize: round(f.baseSize * f.scale),
|
|
74
|
+
lineHeight: round(f.baseSize * f.scale * 1.5)
|
|
75
|
+
});
|
|
76
|
+
var captionStyle = (f) => ({
|
|
77
|
+
fontFamily: f.regular,
|
|
78
|
+
fontSize: round(f.baseSize * 0.8125 * f.scale),
|
|
79
|
+
lineHeight: round(f.baseSize * 0.8125 * f.scale * 1.5)
|
|
80
|
+
});
|
|
81
|
+
var headingStyle = (f) => ({
|
|
82
|
+
fontFamily: f.bold,
|
|
83
|
+
fontSize: round(f.baseSize * 1.25 * f.scale),
|
|
84
|
+
lineHeight: round(f.baseSize * 1.25 * f.scale * 1.3),
|
|
85
|
+
fontWeight: f.bold ? void 0 : "700"
|
|
86
|
+
});
|
|
87
|
+
var buttonLabelStyle = (f) => {
|
|
88
|
+
var _a;
|
|
89
|
+
return {
|
|
90
|
+
fontFamily: (_a = f.bold) != null ? _a : f.medium,
|
|
91
|
+
fontSize: round(f.baseSize * f.scale),
|
|
92
|
+
fontWeight: f.bold ? void 0 : "600"
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/motion/motionSpec.ts
|
|
97
|
+
var REDUCED_FADE_MS = 120;
|
|
98
|
+
var BUTTON_PRESS_SCALE = 0.975;
|
|
99
|
+
var REDUCED_PRESS_OPACITY = 0.85;
|
|
100
|
+
var BUTTON_PRESS_SPRING = { friction: 8, tension: 240 };
|
|
101
|
+
var durationScale = 1;
|
|
102
|
+
var scaledMs = (ms) => Math.round(ms * durationScale);
|
|
103
|
+
var lastKnown = false;
|
|
104
|
+
var useReducedMotion = () => {
|
|
105
|
+
const [reduced, setReduced] = React4.useState(lastKnown);
|
|
106
|
+
React4.useEffect(() => {
|
|
107
|
+
let mounted = true;
|
|
108
|
+
reactNative.AccessibilityInfo.isReduceMotionEnabled().then((value) => {
|
|
109
|
+
lastKnown = value;
|
|
110
|
+
if (mounted) setReduced(value);
|
|
111
|
+
}).catch(() => {
|
|
112
|
+
});
|
|
113
|
+
const sub = reactNative.AccessibilityInfo.addEventListener("reduceMotionChanged", (value) => {
|
|
114
|
+
lastKnown = value;
|
|
115
|
+
setReduced(value);
|
|
116
|
+
});
|
|
117
|
+
return () => {
|
|
118
|
+
mounted = false;
|
|
119
|
+
sub == null ? void 0 : sub.remove();
|
|
120
|
+
};
|
|
121
|
+
}, []);
|
|
122
|
+
return reduced;
|
|
123
|
+
};
|
|
124
|
+
var _Button = ({
|
|
125
|
+
title,
|
|
126
|
+
onPress,
|
|
127
|
+
variant = "primary",
|
|
128
|
+
disabled = false,
|
|
129
|
+
fullWidth = false
|
|
130
|
+
}) => {
|
|
131
|
+
const t = useOnboardingTheme();
|
|
132
|
+
const reduced = useReducedMotion();
|
|
133
|
+
const isPrimary = variant === "primary";
|
|
134
|
+
const pressT = React4.useRef(new reactNative.Animated.Value(0)).current;
|
|
135
|
+
const pressTo = React4.useCallback(
|
|
136
|
+
(to) => {
|
|
137
|
+
if (reduced) {
|
|
138
|
+
reactNative.Animated.timing(pressT, {
|
|
139
|
+
toValue: to,
|
|
140
|
+
duration: scaledMs(REDUCED_FADE_MS),
|
|
141
|
+
useNativeDriver: true
|
|
142
|
+
}).start();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
reactNative.Animated.spring(pressT, {
|
|
146
|
+
toValue: to,
|
|
147
|
+
friction: BUTTON_PRESS_SPRING.friction,
|
|
148
|
+
tension: BUTTON_PRESS_SPRING.tension,
|
|
149
|
+
useNativeDriver: true
|
|
150
|
+
}).start();
|
|
151
|
+
},
|
|
152
|
+
[pressT, reduced]
|
|
153
|
+
);
|
|
154
|
+
const handlePressIn = React4.useCallback(() => pressTo(1), [pressTo]);
|
|
155
|
+
const handlePressOut = React4.useCallback(() => pressTo(0), [pressTo]);
|
|
156
|
+
const scale = pressT.interpolate({
|
|
157
|
+
inputRange: [0, 1],
|
|
158
|
+
outputRange: [1, reduced ? 1 : BUTTON_PRESS_SCALE]
|
|
159
|
+
});
|
|
160
|
+
const opacity = pressT.interpolate({
|
|
161
|
+
inputRange: [0, 1],
|
|
162
|
+
outputRange: [1, reduced ? REDUCED_PRESS_OPACITY : 1]
|
|
163
|
+
});
|
|
164
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
165
|
+
reactNative.Pressable,
|
|
166
|
+
{
|
|
167
|
+
onPress: disabled ? void 0 : onPress,
|
|
168
|
+
onPressIn: disabled ? void 0 : handlePressIn,
|
|
169
|
+
onPressOut: disabled ? void 0 : handlePressOut,
|
|
170
|
+
disabled,
|
|
171
|
+
accessibilityRole: "button",
|
|
172
|
+
accessibilityState: { disabled },
|
|
173
|
+
style: fullWidth ? styles.fill : void 0,
|
|
174
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
175
|
+
reactNative.Animated.View,
|
|
176
|
+
{
|
|
177
|
+
style: [
|
|
178
|
+
styles.base,
|
|
179
|
+
fullWidth && styles.fill,
|
|
180
|
+
{
|
|
181
|
+
borderRadius: t.button.radius,
|
|
182
|
+
minHeight: t.button.height,
|
|
183
|
+
paddingHorizontal: t.button.paddingX,
|
|
184
|
+
opacity,
|
|
185
|
+
transform: [{ scale }]
|
|
186
|
+
},
|
|
187
|
+
isPrimary ? { backgroundColor: disabled ? t.colors.disabled : t.colors.primary } : { borderWidth: 1.5, borderColor: disabled ? t.colors.border : t.colors.primary }
|
|
188
|
+
],
|
|
189
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
190
|
+
reactNative.Text,
|
|
191
|
+
{
|
|
192
|
+
style: [
|
|
193
|
+
buttonLabelStyle(t.fonts),
|
|
194
|
+
{
|
|
195
|
+
color: disabled ? t.colors.textMuted : isPrimary ? t.colors.onPrimary : t.colors.primary
|
|
196
|
+
}
|
|
197
|
+
],
|
|
198
|
+
children: title
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
var Button = React4__default.default.memo(_Button);
|
|
207
|
+
var styles = reactNative.StyleSheet.create({
|
|
208
|
+
base: { alignItems: "center", justifyContent: "center" },
|
|
209
|
+
fill: { width: "100%" }
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// src/motion/centeredModalMotion.ts
|
|
213
|
+
var CENTERED_MODAL_ANIM = {
|
|
214
|
+
/** Spring config for the card scale/opacity entrance (mirrors CompletionView's pop). */
|
|
215
|
+
enterSpring: { friction: 7, tension: 80 },
|
|
216
|
+
/** Exit timing in ms (fade + shrink back out before unmount). */
|
|
217
|
+
exitMs: 160,
|
|
218
|
+
/** Card scale at rest (progress 0); grows to 1 at progress 1. */
|
|
219
|
+
scaleFrom: 0.9,
|
|
220
|
+
/** Peak dim of the backdrop at progress 1. */
|
|
221
|
+
backdropOpacity: 0.55
|
|
222
|
+
};
|
|
223
|
+
var _CenteredModal = React4.forwardRef(
|
|
224
|
+
({ theme, dismissible = true, onDismiss, children }, ref) => {
|
|
225
|
+
const progress = React4.useRef(new reactNative.Animated.Value(0)).current;
|
|
226
|
+
const [open, setOpen] = React4.useState(true);
|
|
227
|
+
const closingRef = React4.useRef(false);
|
|
228
|
+
const reduced = useReducedMotion();
|
|
229
|
+
React4.useEffect(() => {
|
|
230
|
+
const anim = reduced ? reactNative.Animated.timing(progress, {
|
|
231
|
+
toValue: 1,
|
|
232
|
+
duration: REDUCED_FADE_MS,
|
|
233
|
+
useNativeDriver: true
|
|
234
|
+
}) : reactNative.Animated.spring(progress, {
|
|
235
|
+
toValue: 1,
|
|
236
|
+
useNativeDriver: true,
|
|
237
|
+
friction: CENTERED_MODAL_ANIM.enterSpring.friction,
|
|
238
|
+
tension: CENTERED_MODAL_ANIM.enterSpring.tension
|
|
239
|
+
});
|
|
240
|
+
anim.start();
|
|
241
|
+
return () => anim.stop();
|
|
242
|
+
}, [progress, reduced]);
|
|
243
|
+
const close = React4.useCallback(() => {
|
|
244
|
+
if (closingRef.current) return;
|
|
245
|
+
closingRef.current = true;
|
|
246
|
+
reactNative.Animated.timing(progress, {
|
|
247
|
+
toValue: 0,
|
|
248
|
+
duration: reduced ? REDUCED_FADE_MS : CENTERED_MODAL_ANIM.exitMs,
|
|
249
|
+
easing: reactNative.Easing.in(reactNative.Easing.cubic),
|
|
250
|
+
useNativeDriver: true
|
|
251
|
+
}).start(() => {
|
|
252
|
+
setOpen(false);
|
|
253
|
+
onDismiss();
|
|
254
|
+
});
|
|
255
|
+
}, [progress, onDismiss, reduced]);
|
|
256
|
+
React4.useImperativeHandle(ref, () => ({ close }), [close]);
|
|
257
|
+
React4.useEffect(() => {
|
|
258
|
+
const sub = reactNative.BackHandler.addEventListener("hardwareBackPress", () => {
|
|
259
|
+
if (dismissible) close();
|
|
260
|
+
return true;
|
|
261
|
+
});
|
|
262
|
+
return () => sub.remove();
|
|
263
|
+
}, [dismissible, close]);
|
|
264
|
+
const onBackdrop = React4.useCallback(() => {
|
|
265
|
+
if (dismissible) close();
|
|
266
|
+
}, [dismissible, close]);
|
|
267
|
+
const backdropOpacity = progress.interpolate({
|
|
268
|
+
inputRange: [0, 1],
|
|
269
|
+
outputRange: [0, CENTERED_MODAL_ANIM.backdropOpacity]
|
|
270
|
+
});
|
|
271
|
+
const cardScale = reduced ? 1 : progress.interpolate({
|
|
272
|
+
inputRange: [0, 1],
|
|
273
|
+
outputRange: [CENTERED_MODAL_ANIM.scaleFrom, 1]
|
|
274
|
+
});
|
|
275
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
276
|
+
reactNative.Modal,
|
|
277
|
+
{
|
|
278
|
+
visible: open,
|
|
279
|
+
transparent: true,
|
|
280
|
+
animationType: "none",
|
|
281
|
+
statusBarTranslucent: true,
|
|
282
|
+
onRequestClose: dismissible ? close : void 0,
|
|
283
|
+
children: [
|
|
284
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
285
|
+
reactNative.Animated.View,
|
|
286
|
+
{
|
|
287
|
+
style: [styles2.backdrop, { opacity: backdropOpacity }],
|
|
288
|
+
pointerEvents: "none"
|
|
289
|
+
}
|
|
290
|
+
),
|
|
291
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
292
|
+
reactNative.Pressable,
|
|
293
|
+
{
|
|
294
|
+
style: styles2.fill,
|
|
295
|
+
onPress: onBackdrop,
|
|
296
|
+
accessibilityRole: "button",
|
|
297
|
+
accessibilityLabel: "Dismiss",
|
|
298
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNativeSafeAreaContext.SafeAreaView, { style: styles2.center, pointerEvents: "box-none", children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => {
|
|
299
|
+
}, style: styles2.cardWrap, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
300
|
+
reactNative.Animated.View,
|
|
301
|
+
{
|
|
302
|
+
accessibilityViewIsModal: true,
|
|
303
|
+
accessibilityRole: "alert",
|
|
304
|
+
style: [
|
|
305
|
+
styles2.card,
|
|
306
|
+
{
|
|
307
|
+
backgroundColor: theme.colors.surface,
|
|
308
|
+
borderRadius: theme.radius.lg,
|
|
309
|
+
padding: theme.spacing.lg,
|
|
310
|
+
gap: theme.spacing.md,
|
|
311
|
+
opacity: progress,
|
|
312
|
+
transform: [{ scale: cardScale }]
|
|
313
|
+
}
|
|
314
|
+
],
|
|
315
|
+
children
|
|
316
|
+
}
|
|
317
|
+
) }) })
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
_CenteredModal.displayName = "CenteredModal";
|
|
326
|
+
var CenteredModal = React4__default.default.memo(_CenteredModal);
|
|
327
|
+
var styles2 = reactNative.StyleSheet.create({
|
|
328
|
+
fill: { flex: 1 },
|
|
329
|
+
backdrop: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#000" },
|
|
330
|
+
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
|
|
331
|
+
cardWrap: { width: "100%", maxWidth: 420 },
|
|
332
|
+
card: {
|
|
333
|
+
width: "100%",
|
|
334
|
+
// Soft elevation so the popup reads as lifted above the app in both themes.
|
|
335
|
+
shadowColor: "#000",
|
|
336
|
+
shadowOffset: { width: 0, height: 8 },
|
|
337
|
+
shadowOpacity: 0.28,
|
|
338
|
+
shadowRadius: 24,
|
|
339
|
+
elevation: 12
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// src/reviews/feedbackForm.ts
|
|
344
|
+
var FEEDBACK_MIN_LINES = 4;
|
|
345
|
+
var FEEDBACK_MAX_LINES = 5;
|
|
346
|
+
var feedbackInputHeights = (fonts, verticalPadding = 0) => {
|
|
347
|
+
var _a;
|
|
348
|
+
const lineHeight = (_a = bodyStyle(fonts).lineHeight) != null ? _a : Math.round(fonts.baseSize * fonts.scale * 1.5);
|
|
349
|
+
const pad = verticalPadding * 2;
|
|
350
|
+
return {
|
|
351
|
+
minHeight: lineHeight * FEEDBACK_MIN_LINES + pad,
|
|
352
|
+
maxHeight: lineHeight * FEEDBACK_MAX_LINES + pad
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/reviews/decision.ts
|
|
357
|
+
var decideReview = (local, decision) => decision != null ? decision : local;
|
|
358
|
+
var resolveRules = (config) => {
|
|
359
|
+
var _a, _b, _c, _d, _e;
|
|
360
|
+
return {
|
|
361
|
+
enabled: (_a = config.enabled) != null ? _a : true,
|
|
362
|
+
minSessions: (_b = config.minSessions) != null ? _b : 0,
|
|
363
|
+
minEvents: (_c = config.minEvents) != null ? _c : 0,
|
|
364
|
+
cooldownDays: (_d = config.cooldownDays) != null ? _d : 0,
|
|
365
|
+
oncePerVersion: (_e = config.oncePerVersion) != null ? _e : true
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
var MS_PER_DAY = 864e5;
|
|
369
|
+
var evaluateGate = (rules, s) => {
|
|
370
|
+
if (!rules.enabled) return { fire: false, reason: "disabled" };
|
|
371
|
+
if (rules.oncePerVersion && s.seen) return { fire: false, reason: "already_shown" };
|
|
372
|
+
if (s.sessions < rules.minSessions) return { fire: false, reason: "min_sessions" };
|
|
373
|
+
if (s.events < rules.minEvents) return { fire: false, reason: "min_events" };
|
|
374
|
+
if (rules.cooldownDays > 0 && s.lastShownAt != null) {
|
|
375
|
+
const elapsedDays = (s.now - s.lastShownAt) / MS_PER_DAY;
|
|
376
|
+
if (elapsedDays < rules.cooldownDays) return { fire: false, reason: "cooldown" };
|
|
377
|
+
}
|
|
378
|
+
return { fire: true, reason: "local_rules_met" };
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/questionnaire/decision.ts
|
|
382
|
+
var decideQuestionnaire = (local, decision) => decideReview(local, decision);
|
|
383
|
+
var buildQuestionnaireSubmission = (input) => {
|
|
384
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
385
|
+
const opinion = input.answers.opinion.trim();
|
|
386
|
+
const improve = (_a = input.answers.improve) == null ? void 0 : _a.trim();
|
|
387
|
+
const suggestions = (_b = input.answers.suggestions) == null ? void 0 : _b.trim();
|
|
388
|
+
return {
|
|
389
|
+
session_id: (_c = input.sessionId) != null ? _c : null,
|
|
390
|
+
answers: {
|
|
391
|
+
opinion,
|
|
392
|
+
improve: improve ? improve : null,
|
|
393
|
+
suggestions: suggestions ? suggestions : null
|
|
394
|
+
},
|
|
395
|
+
meta: {
|
|
396
|
+
user_id: (_e = (_d = input.meta) == null ? void 0 : _d.user_id) != null ? _e : null,
|
|
397
|
+
device_key: (_g = (_f = input.meta) == null ? void 0 : _f.device_key) != null ? _g : null,
|
|
398
|
+
session_count: (_i = (_h = input.meta) == null ? void 0 : _h.session_count) != null ? _i : null
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// src/questionnaire/transport.ts
|
|
404
|
+
var submitQuestionnaireResponse = (target, id, submission) => {
|
|
405
|
+
if (!(target == null ? void 0 : target.serverUrl) || !id) return;
|
|
406
|
+
try {
|
|
407
|
+
const base = target.serverUrl.replace(/\/$/, "");
|
|
408
|
+
const url = `${base}/v1/questionnaires/${encodeURIComponent(id)}/responses`;
|
|
409
|
+
const headers = { "Content-Type": "application/json" };
|
|
410
|
+
if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
|
|
411
|
+
void fetch(url, {
|
|
412
|
+
method: "POST",
|
|
413
|
+
headers,
|
|
414
|
+
body: JSON.stringify(submission)
|
|
415
|
+
}).catch(() => {
|
|
416
|
+
});
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
var fetchQuestionnaireDecision = async (target, options = {}) => {
|
|
421
|
+
if (!(target == null ? void 0 : target.serverUrl)) return null;
|
|
422
|
+
try {
|
|
423
|
+
const base = target.serverUrl.replace(/\/$/, "");
|
|
424
|
+
const params = new URLSearchParams();
|
|
425
|
+
if (options.sessionId) params.set("session_id", options.sessionId);
|
|
426
|
+
if (options.deviceKey) params.set("device_key", options.deviceKey);
|
|
427
|
+
const qs = params.toString();
|
|
428
|
+
const url = `${base}/v1/questionnaires/decision${qs ? `?${qs}` : ""}`;
|
|
429
|
+
const headers = {};
|
|
430
|
+
if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
|
|
431
|
+
const res = await fetch(url, { headers });
|
|
432
|
+
if (!res || !res.ok) return null;
|
|
433
|
+
const json = await res.json();
|
|
434
|
+
return json != null ? json : null;
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
var mergeThemeOver = (base, override) => {
|
|
440
|
+
if (!override) return base;
|
|
441
|
+
return {
|
|
442
|
+
colors: { ...base.colors, ...override.colors },
|
|
443
|
+
fonts: { ...base.fonts, ...override.fonts },
|
|
444
|
+
radius: { ...base.radius, ...override.radius },
|
|
445
|
+
spacing: { ...base.spacing, ...override.spacing },
|
|
446
|
+
button: { ...base.button, ...override.button }
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
var QuestionnaireGate = ({
|
|
450
|
+
questionnaire,
|
|
451
|
+
target,
|
|
452
|
+
sessionId,
|
|
453
|
+
meta,
|
|
454
|
+
onEvent,
|
|
455
|
+
onResolved,
|
|
456
|
+
onShown,
|
|
457
|
+
theme: themeOverride,
|
|
458
|
+
dismissible = true
|
|
459
|
+
}) => {
|
|
460
|
+
var _a, _b, _c, _d;
|
|
461
|
+
const active = useOnboardingTheme();
|
|
462
|
+
const t = React4.useMemo(() => mergeThemeOver(active, themeOverride), [active, themeOverride]);
|
|
463
|
+
const modalRef = React4__default.default.useRef(null);
|
|
464
|
+
const id = questionnaire.id;
|
|
465
|
+
const [phase, setPhase] = React4.useState("form");
|
|
466
|
+
const [opinion, setOpinion] = React4.useState("");
|
|
467
|
+
const [improve, setImprove] = React4.useState("");
|
|
468
|
+
const [suggestions, setSuggestions] = React4.useState("");
|
|
469
|
+
const shownRef = React4__default.default.useRef(false);
|
|
470
|
+
const onShownRef = React4__default.default.useRef(onShown);
|
|
471
|
+
const onEventRef = React4__default.default.useRef(onEvent);
|
|
472
|
+
onShownRef.current = onShown;
|
|
473
|
+
onEventRef.current = onEvent;
|
|
474
|
+
React4__default.default.useEffect(() => {
|
|
475
|
+
var _a2, _b2;
|
|
476
|
+
if (shownRef.current) return;
|
|
477
|
+
shownRef.current = true;
|
|
478
|
+
(_a2 = onShownRef.current) == null ? void 0 : _a2.call(onShownRef);
|
|
479
|
+
(_b2 = onEventRef.current) == null ? void 0 : _b2.call(onEventRef, { name: "questionnaire_prompt_shown", id });
|
|
480
|
+
}, []);
|
|
481
|
+
const resolve = React4.useCallback(() => {
|
|
482
|
+
onResolved == null ? void 0 : onResolved();
|
|
483
|
+
}, [onResolved]);
|
|
484
|
+
const canSend = opinion.trim().length > 0;
|
|
485
|
+
const send = React4.useCallback(() => {
|
|
486
|
+
if (!canSend) return;
|
|
487
|
+
const submission = buildQuestionnaireSubmission({
|
|
488
|
+
answers: { opinion, improve, suggestions },
|
|
489
|
+
sessionId,
|
|
490
|
+
meta
|
|
491
|
+
});
|
|
492
|
+
submitQuestionnaireResponse(target, id, submission);
|
|
493
|
+
onEvent == null ? void 0 : onEvent({ name: "questionnaire_submitted", id });
|
|
494
|
+
setPhase("thanks");
|
|
495
|
+
resolve();
|
|
496
|
+
}, [canSend, opinion, improve, suggestions, sessionId, meta, target, id, onEvent, resolve]);
|
|
497
|
+
const handleDismissTap = React4.useCallback(() => {
|
|
498
|
+
var _a2;
|
|
499
|
+
onEvent == null ? void 0 : onEvent({ name: "questionnaire_dismissed", id });
|
|
500
|
+
(_a2 = modalRef.current) == null ? void 0 : _a2.close();
|
|
501
|
+
}, [onEvent, id]);
|
|
502
|
+
const title = (_a = questionnaire.title) != null ? _a : "How's it going so far?";
|
|
503
|
+
const opinionLabel = (_b = questionnaire.opinion_label) != null ? _b : "What do you think about the app?";
|
|
504
|
+
const improveLabel = (_c = questionnaire.improve_label) != null ? _c : "How can we improve? (optional)";
|
|
505
|
+
const suggestionsLabel = (_d = questionnaire.suggestions_label) != null ? _d : "Any suggestions? (optional)";
|
|
506
|
+
const opinionBox = feedbackInputHeights(t.fonts, t.spacing.md);
|
|
507
|
+
const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
508
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [headingStyle(t.fonts), { color: t.colors.text }], children: title }),
|
|
509
|
+
phase === "form" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
510
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "A few words is plenty. Only the first box is required." }),
|
|
511
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: opinionLabel }),
|
|
512
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
513
|
+
reactNative.TextInput,
|
|
514
|
+
{
|
|
515
|
+
value: opinion,
|
|
516
|
+
onChangeText: setOpinion,
|
|
517
|
+
placeholder: "e.g. it feels fast and clear",
|
|
518
|
+
placeholderTextColor: t.colors.textMuted,
|
|
519
|
+
multiline: true,
|
|
520
|
+
textAlignVertical: "top",
|
|
521
|
+
style: [
|
|
522
|
+
bodyStyle(t.fonts),
|
|
523
|
+
styles3.input,
|
|
524
|
+
{
|
|
525
|
+
color: t.colors.text,
|
|
526
|
+
backgroundColor: t.colors.background,
|
|
527
|
+
borderRadius: t.radius.md,
|
|
528
|
+
borderColor: t.colors.border,
|
|
529
|
+
padding: t.spacing.md,
|
|
530
|
+
minHeight: opinionBox.minHeight,
|
|
531
|
+
maxHeight: opinionBox.maxHeight
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
}
|
|
535
|
+
),
|
|
536
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: improveLabel }),
|
|
537
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
538
|
+
reactNative.TextInput,
|
|
539
|
+
{
|
|
540
|
+
value: improve,
|
|
541
|
+
onChangeText: setImprove,
|
|
542
|
+
placeholder: "e.g. fewer steps to start",
|
|
543
|
+
placeholderTextColor: t.colors.textMuted,
|
|
544
|
+
style: [
|
|
545
|
+
bodyStyle(t.fonts),
|
|
546
|
+
styles3.input,
|
|
547
|
+
{
|
|
548
|
+
color: t.colors.text,
|
|
549
|
+
backgroundColor: t.colors.background,
|
|
550
|
+
borderRadius: t.radius.md,
|
|
551
|
+
borderColor: t.colors.border,
|
|
552
|
+
paddingHorizontal: t.spacing.md,
|
|
553
|
+
paddingVertical: t.spacing.sm,
|
|
554
|
+
minHeight: t.button.height
|
|
555
|
+
}
|
|
556
|
+
]
|
|
557
|
+
}
|
|
558
|
+
),
|
|
559
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: suggestionsLabel }),
|
|
560
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
561
|
+
reactNative.TextInput,
|
|
562
|
+
{
|
|
563
|
+
value: suggestions,
|
|
564
|
+
onChangeText: setSuggestions,
|
|
565
|
+
placeholder: "e.g. a dark theme",
|
|
566
|
+
placeholderTextColor: t.colors.textMuted,
|
|
567
|
+
style: [
|
|
568
|
+
bodyStyle(t.fonts),
|
|
569
|
+
styles3.input,
|
|
570
|
+
{
|
|
571
|
+
color: t.colors.text,
|
|
572
|
+
backgroundColor: t.colors.background,
|
|
573
|
+
borderRadius: t.radius.md,
|
|
574
|
+
borderColor: t.colors.border,
|
|
575
|
+
paddingHorizontal: t.spacing.md,
|
|
576
|
+
paddingVertical: t.spacing.sm,
|
|
577
|
+
minHeight: t.button.height
|
|
578
|
+
}
|
|
579
|
+
]
|
|
580
|
+
}
|
|
581
|
+
),
|
|
582
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { title: "Send", onPress: send, variant: "primary", fullWidth: true, disabled: !canSend }),
|
|
583
|
+
dismissible && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: handleDismissTap, hitSlop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [captionStyle(t.fonts), styles3.link, { color: t.colors.textMuted }], children: "Not now" }) })
|
|
584
|
+
] }),
|
|
585
|
+
phase === "thanks" && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted }], children: "Thank you. This helps us make it better." })
|
|
586
|
+
] });
|
|
587
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CenteredModal, { ref: modalRef, theme: t, dismissible, onDismiss: resolve, children: content });
|
|
588
|
+
};
|
|
589
|
+
QuestionnaireGate.displayName = "QuestionnaireGate";
|
|
590
|
+
var styles3 = reactNative.StyleSheet.create({
|
|
591
|
+
input: { borderWidth: 1 },
|
|
592
|
+
link: { textAlign: "center", textDecorationLine: "underline" }
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// src/coachmarks/runtime.ts
|
|
596
|
+
var storageSingleton = null;
|
|
597
|
+
var testing = false;
|
|
598
|
+
var getCoachmarkStorage = () => storageSingleton;
|
|
599
|
+
var isCoachmarkTesting = () => testing;
|
|
600
|
+
var SEEN_VALUE = "1";
|
|
601
|
+
var hasSeenGate = (key, storageOverride, testingOverride) => {
|
|
602
|
+
if (testingOverride != null ? testingOverride : testing) return false;
|
|
603
|
+
const storage = storageOverride != null ? storageOverride : storageSingleton;
|
|
604
|
+
if (!storage) return false;
|
|
605
|
+
try {
|
|
606
|
+
return storage.getItem(key) === SEEN_VALUE;
|
|
607
|
+
} catch {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
var markSeenGate = (key, storageOverride, testingOverride) => {
|
|
612
|
+
if (testingOverride != null ? testingOverride : testing) return;
|
|
613
|
+
const storage = storageOverride != null ? storageOverride : storageSingleton;
|
|
614
|
+
if (!storage) return;
|
|
615
|
+
try {
|
|
616
|
+
storage.setItem(key, SEEN_VALUE);
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// src/features/defaults.ts
|
|
622
|
+
var defaultWireFeatures = Object.freeze({
|
|
623
|
+
coachmarks: Object.freeze({ enabled: true }),
|
|
624
|
+
showcase: Object.freeze({ enabled: true }),
|
|
625
|
+
onboarding: Object.freeze({ mode: "ai" }),
|
|
626
|
+
review: Object.freeze({ enabled: true }),
|
|
627
|
+
questionnaire: Object.freeze({ enabled: true }),
|
|
628
|
+
announcements: Object.freeze({ enabled: true })
|
|
629
|
+
});
|
|
630
|
+
var VALID_MODES = ["ai", "static", "off"];
|
|
631
|
+
var readEnabled = (raw) => {
|
|
632
|
+
if (raw && typeof raw === "object" && typeof raw.enabled === "boolean") {
|
|
633
|
+
return { enabled: raw.enabled };
|
|
634
|
+
}
|
|
635
|
+
return { enabled: true };
|
|
636
|
+
};
|
|
637
|
+
var parseWireFeatures = (raw) => {
|
|
638
|
+
var _a;
|
|
639
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return defaultWireFeatures;
|
|
640
|
+
const src = raw;
|
|
641
|
+
const modeRaw = (_a = src.onboarding) == null ? void 0 : _a.mode;
|
|
642
|
+
const mode = typeof modeRaw === "string" && VALID_MODES.includes(modeRaw) ? modeRaw : "ai";
|
|
643
|
+
return {
|
|
644
|
+
coachmarks: readEnabled(src.coachmarks),
|
|
645
|
+
showcase: readEnabled(src.showcase),
|
|
646
|
+
onboarding: { mode },
|
|
647
|
+
review: readEnabled(src.review),
|
|
648
|
+
questionnaire: readEnabled(src.questionnaire),
|
|
649
|
+
announcements: readEnabled(src.announcements)
|
|
650
|
+
};
|
|
651
|
+
};
|
|
652
|
+
var featuresEqual = (a, b) => a === b || a.coachmarks.enabled === b.coachmarks.enabled && a.showcase.enabled === b.showcase.enabled && a.onboarding.mode === b.onboarding.mode && a.review.enabled === b.review.enabled && a.questionnaire.enabled === b.questionnaire.enabled && a.announcements.enabled === b.announcements.enabled;
|
|
653
|
+
|
|
654
|
+
// src/features/cache.ts
|
|
655
|
+
var READ_TIMEOUT_MS = 1500;
|
|
656
|
+
var featuresCacheKey = (appId) => `wireai:features:${appId && appId.length > 0 ? appId : "default"}`;
|
|
657
|
+
var withTimeout = (p, ms) => {
|
|
658
|
+
let timer;
|
|
659
|
+
const timeout = new Promise((resolve) => {
|
|
660
|
+
timer = setTimeout(() => resolve(void 0), ms);
|
|
661
|
+
});
|
|
662
|
+
return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
|
|
663
|
+
};
|
|
664
|
+
var readCachedFeatures = async (storage, key) => {
|
|
665
|
+
let raw;
|
|
666
|
+
try {
|
|
667
|
+
raw = await withTimeout(storage.getItem(key), READ_TIMEOUT_MS);
|
|
668
|
+
} catch {
|
|
669
|
+
return void 0;
|
|
670
|
+
}
|
|
671
|
+
if (!raw) return void 0;
|
|
672
|
+
try {
|
|
673
|
+
const parsed = JSON.parse(raw);
|
|
674
|
+
if (parsed && typeof parsed === "object" && typeof parsed.ts === "number") {
|
|
675
|
+
const ts = parsed.ts;
|
|
676
|
+
return {
|
|
677
|
+
features: parseWireFeatures(parsed.features),
|
|
678
|
+
ageMs: Date.now() - ts
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
return void 0;
|
|
684
|
+
};
|
|
685
|
+
var writeCachedFeatures = (storage, key, features) => {
|
|
686
|
+
try {
|
|
687
|
+
void storage.setItem(key, JSON.stringify({ features, ts: Date.now() })).catch(() => {
|
|
688
|
+
});
|
|
689
|
+
} catch {
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/features/fetchWireFeatures.ts
|
|
694
|
+
var DEFAULT_TIMEOUT_MS = 4e3;
|
|
695
|
+
var failOpen = async (storage, key) => {
|
|
696
|
+
var _a;
|
|
697
|
+
if (!storage) return defaultWireFeatures;
|
|
698
|
+
const cached = await readCachedFeatures(storage, key);
|
|
699
|
+
return (_a = cached == null ? void 0 : cached.features) != null ? _a : defaultWireFeatures;
|
|
700
|
+
};
|
|
701
|
+
var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
|
|
702
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
703
|
+
const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
|
|
704
|
+
try {
|
|
705
|
+
return await fetch(url, {
|
|
706
|
+
method: "GET",
|
|
707
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
708
|
+
signal: controller == null ? void 0 : controller.signal
|
|
709
|
+
});
|
|
710
|
+
} finally {
|
|
711
|
+
clearTimeout(timer);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
var fetchWireFeatures = async (config) => {
|
|
715
|
+
const key = featuresCacheKey(config == null ? void 0 : config.appId);
|
|
716
|
+
const storage = config == null ? void 0 : config.storage;
|
|
717
|
+
if (!(config == null ? void 0 : config.serverUrl) || !(config == null ? void 0 : config.apiKey)) return failOpen(storage, key);
|
|
718
|
+
const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
|
|
719
|
+
const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
720
|
+
try {
|
|
721
|
+
const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
|
|
722
|
+
if (!res || !res.ok) return failOpen(storage, key);
|
|
723
|
+
const json = await res.json();
|
|
724
|
+
const features = parseWireFeatures(json);
|
|
725
|
+
if (storage) writeCachedFeatures(storage, key, features);
|
|
726
|
+
return features;
|
|
727
|
+
} catch {
|
|
728
|
+
return failOpen(storage, key);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// src/features/useWireFeatures.ts
|
|
733
|
+
var useStableValue = (value, isEqual) => {
|
|
734
|
+
const ref = React4.useRef(value);
|
|
735
|
+
if (!isEqual(ref.current, value)) ref.current = value;
|
|
736
|
+
return ref.current;
|
|
737
|
+
};
|
|
738
|
+
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);
|
|
739
|
+
var useWireFeatures = (config) => {
|
|
740
|
+
const stableConfig = useStableValue(config, sameFetchKey);
|
|
741
|
+
const [flags, setFlags] = React4.useState(defaultWireFeatures);
|
|
742
|
+
const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
|
|
743
|
+
React4.useEffect(() => {
|
|
744
|
+
if (!canFetch) {
|
|
745
|
+
setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
let alive = true;
|
|
749
|
+
void fetchWireFeatures(stableConfig).then((next) => {
|
|
750
|
+
if (!alive) return;
|
|
751
|
+
setFlags((prev) => featuresEqual(prev, next) ? prev : next);
|
|
752
|
+
});
|
|
753
|
+
return () => {
|
|
754
|
+
alive = false;
|
|
755
|
+
};
|
|
756
|
+
}, [canFetch, stableConfig]);
|
|
757
|
+
return flags;
|
|
758
|
+
};
|
|
759
|
+
var WireFeaturesContext = React4.createContext(null);
|
|
760
|
+
var useWireFeaturesContext = () => React4.useContext(WireFeaturesContext);
|
|
761
|
+
var useResolvedFeatures = (options) => {
|
|
762
|
+
var _a, _b, _c;
|
|
763
|
+
const ctx = useWireFeaturesContext();
|
|
764
|
+
const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
|
|
765
|
+
const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
|
|
766
|
+
return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
// src/reviews/equality.ts
|
|
770
|
+
var shallowEqual = (a, b) => {
|
|
771
|
+
if (Object.is(a, b)) return true;
|
|
772
|
+
if (a == null || b == null || typeof a !== "object" || typeof b !== "object") {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
const ak = Object.keys(a);
|
|
776
|
+
const bk = Object.keys(b);
|
|
777
|
+
if (ak.length !== bk.length) return false;
|
|
778
|
+
for (const k of ak) {
|
|
779
|
+
if (!Object.is(a[k], b[k])) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return true;
|
|
784
|
+
};
|
|
785
|
+
var sameDecision = (a, b) => {
|
|
786
|
+
if (a == null || b == null) return a == b;
|
|
787
|
+
return a.fire === b.fire && a.reason === b.reason;
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// src/reviews/runtime.ts
|
|
791
|
+
var resolveStorage = (override) => override != null ? override : getCoachmarkStorage();
|
|
792
|
+
var readInt = (storage, key) => {
|
|
793
|
+
if (!storage) return 0;
|
|
794
|
+
try {
|
|
795
|
+
const raw = storage.getItem(key);
|
|
796
|
+
const n = raw == null ? 0 : parseInt(raw, 10);
|
|
797
|
+
return Number.isFinite(n) ? n : 0;
|
|
798
|
+
} catch {
|
|
799
|
+
return 0;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
var writeInt = (storage, key, value) => {
|
|
803
|
+
if (!storage) return;
|
|
804
|
+
try {
|
|
805
|
+
storage.setItem(key, String(value));
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// src/questionnaire/runtime.ts
|
|
811
|
+
var questionnaireSeenKey = (id, version) => version ? `wire_questionnaire_${id}_${version}_seen` : `wire_questionnaire_${id}_seen`;
|
|
812
|
+
var questionnaireLastShownKey = (id) => `wire_questionnaire_${id}_last`;
|
|
813
|
+
var questionnaireSessionsKey = (id) => `wire_questionnaire_${id}_sessions`;
|
|
814
|
+
|
|
815
|
+
// src/questionnaire/useQuestionnaireGate.ts
|
|
816
|
+
var useStableValue2 = (value, isEqual) => {
|
|
817
|
+
const ref = React4.useRef(value);
|
|
818
|
+
if (!isEqual(ref.current, value)) ref.current = value;
|
|
819
|
+
return ref.current;
|
|
820
|
+
};
|
|
821
|
+
var useQuestionnaireGate = ({
|
|
822
|
+
config: configProp,
|
|
823
|
+
decision: decisionProp,
|
|
824
|
+
events = 0,
|
|
825
|
+
storage,
|
|
826
|
+
isTesting,
|
|
827
|
+
features,
|
|
828
|
+
featuresConfig
|
|
829
|
+
}) => {
|
|
830
|
+
const config = useStableValue2(configProp, shallowEqual);
|
|
831
|
+
const decision = useStableValue2(decisionProp, (a, b) => sameDecision(a, b));
|
|
832
|
+
const featureEnabled = useResolvedFeatures({ flags: features, config: featuresConfig }).questionnaire.enabled;
|
|
833
|
+
const testing2 = isTesting != null ? isTesting : isCoachmarkTesting();
|
|
834
|
+
const seenKey = questionnaireSeenKey(
|
|
835
|
+
config.id,
|
|
836
|
+
config.oncePerVersion === false ? void 0 : config.appVersion
|
|
837
|
+
);
|
|
838
|
+
const lastKey = questionnaireLastShownKey(config.id);
|
|
839
|
+
const sessionsKey = questionnaireSessionsKey(config.id);
|
|
840
|
+
const sessions = React4.useState(() => {
|
|
841
|
+
const store = resolveStorage(storage);
|
|
842
|
+
const next = readInt(store, sessionsKey) + 1;
|
|
843
|
+
writeInt(store, sessionsKey, next);
|
|
844
|
+
return next;
|
|
845
|
+
})[0];
|
|
846
|
+
const hasServerDecision = decision != null;
|
|
847
|
+
const timeoutMs = config.timeoutFallbackMs;
|
|
848
|
+
const waits = !hasServerDecision && timeoutMs != null && timeoutMs > 0;
|
|
849
|
+
const [elapsed, setElapsed] = React4.useState(!waits);
|
|
850
|
+
React4.useEffect(() => {
|
|
851
|
+
if (!waits) {
|
|
852
|
+
setElapsed(true);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const timer = setTimeout(() => setElapsed(true), timeoutMs);
|
|
856
|
+
return () => clearTimeout(timer);
|
|
857
|
+
}, [waits, timeoutMs]);
|
|
858
|
+
const { visible, verdict } = React4.useMemo(() => {
|
|
859
|
+
if (!featureEnabled)
|
|
860
|
+
return {
|
|
861
|
+
visible: false,
|
|
862
|
+
verdict: { fire: false, reason: "feature_disabled" }
|
|
863
|
+
};
|
|
864
|
+
if (testing2)
|
|
865
|
+
return {
|
|
866
|
+
visible: true,
|
|
867
|
+
verdict: { fire: true, reason: "testing" }
|
|
868
|
+
};
|
|
869
|
+
const store = resolveStorage(storage);
|
|
870
|
+
const local = evaluateGate(resolveRules(config), {
|
|
871
|
+
sessions,
|
|
872
|
+
events,
|
|
873
|
+
lastShownAt: readInt(store, lastKey) || null,
|
|
874
|
+
seen: hasSeenGate(seenKey, storage, isTesting),
|
|
875
|
+
now: Date.now()
|
|
876
|
+
});
|
|
877
|
+
const resolved = decideQuestionnaire(local, decision);
|
|
878
|
+
const alreadySeen = hasSeenGate(seenKey, storage, isTesting);
|
|
879
|
+
const ready = hasServerDecision || elapsed;
|
|
880
|
+
return { visible: ready && resolved.fire && !alreadySeen, verdict: resolved };
|
|
881
|
+
}, [
|
|
882
|
+
featureEnabled,
|
|
883
|
+
testing2,
|
|
884
|
+
config,
|
|
885
|
+
decision,
|
|
886
|
+
events,
|
|
887
|
+
sessions,
|
|
888
|
+
elapsed,
|
|
889
|
+
hasServerDecision,
|
|
890
|
+
seenKey,
|
|
891
|
+
lastKey,
|
|
892
|
+
storage,
|
|
893
|
+
isTesting
|
|
894
|
+
]);
|
|
895
|
+
const shownRef = React4.useRef(false);
|
|
896
|
+
const markShown = React4.useCallback(() => {
|
|
897
|
+
if (shownRef.current) return;
|
|
898
|
+
shownRef.current = true;
|
|
899
|
+
writeInt(resolveStorage(storage), lastKey, Date.now());
|
|
900
|
+
}, [storage, lastKey]);
|
|
901
|
+
const markResolved = React4.useCallback(() => {
|
|
902
|
+
markSeenGate(seenKey, storage, isTesting);
|
|
903
|
+
}, [seenKey, storage, isTesting]);
|
|
904
|
+
return React4.useMemo(
|
|
905
|
+
() => ({ visible, decision: verdict, markShown, markResolved }),
|
|
906
|
+
[visible, verdict, markShown, markResolved]
|
|
907
|
+
);
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
exports.QuestionnaireGate = QuestionnaireGate;
|
|
911
|
+
exports.buildQuestionnaireSubmission = buildQuestionnaireSubmission;
|
|
912
|
+
exports.decideQuestionnaire = decideQuestionnaire;
|
|
913
|
+
exports.evaluateGate = evaluateGate;
|
|
914
|
+
exports.fetchQuestionnaireDecision = fetchQuestionnaireDecision;
|
|
915
|
+
exports.questionnaireLastShownKey = questionnaireLastShownKey;
|
|
916
|
+
exports.questionnaireSeenKey = questionnaireSeenKey;
|
|
917
|
+
exports.questionnaireSessionsKey = questionnaireSessionsKey;
|
|
918
|
+
exports.resolveRules = resolveRules;
|
|
919
|
+
exports.submitQuestionnaireResponse = submitQuestionnaireResponse;
|
|
920
|
+
exports.useQuestionnaireGate = useQuestionnaireGate;
|
|
921
|
+
//# sourceMappingURL=index.js.map
|
|
922
|
+
//# sourceMappingURL=index.js.map
|