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