@mrrocket/sdk 0.4.0 → 0.9.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/CHANGELOG.md +43 -0
- package/README.md +137 -8
- package/dist/api-client.d.ts +213 -0
- package/dist/api-client.js +67 -0
- package/dist/api-client.js.map +1 -0
- package/dist/api.d.ts +9 -109
- package/dist/api.js +7 -62
- package/dist/api.js.map +1 -1
- package/dist/browser/mrrocket-flows.js +64 -0
- package/dist/browser/mrrocket-messenger.js +313 -0
- package/dist/browser/mrrocket-push.js +1 -0
- package/dist/browser/mrrocket-replay.js +3 -3
- package/dist/browser/mrrocket-sw.js +1 -0
- package/dist/browser/mrrocket.js +2 -1
- package/dist/browser.d.ts +3 -2
- package/dist/browser.js +6 -1
- package/dist/browser.js.map +1 -1
- package/dist/capture.d.ts +1 -13
- package/dist/capture.js +1 -22
- package/dist/capture.js.map +1 -1
- package/dist/client.d.ts +36 -1
- package/dist/client.js +161 -6
- package/dist/client.js.map +1 -1
- package/dist/context.d.ts +10 -0
- package/dist/context.js.map +1 -1
- package/dist/core.d.ts +64 -3
- package/dist/core.js +54 -2
- package/dist/core.js.map +1 -1
- package/dist/feedback.d.ts +19 -4
- package/dist/feedback.js +655 -190
- package/dist/feedback.js.map +1 -1
- package/dist/flows-ui.d.ts +23 -0
- package/dist/flows-ui.js +384 -0
- package/dist/flows-ui.js.map +1 -0
- package/dist/flows-web.d.ts +19 -0
- package/dist/flows-web.js +42 -0
- package/dist/flows-web.js.map +1 -0
- package/dist/flows.d.ts +222 -0
- package/dist/flows.js +409 -0
- package/dist/flows.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/messenger-ui.d.ts +47 -0
- package/dist/messenger-ui.js +583 -0
- package/dist/messenger-ui.js.map +1 -0
- package/dist/messenger.d.ts +37 -0
- package/dist/messenger.js +69 -0
- package/dist/messenger.js.map +1 -0
- package/dist/proxy.d.ts +34 -1
- package/dist/proxy.js +14 -1
- package/dist/proxy.js.map +1 -1
- package/dist/push-web.d.ts +43 -0
- package/dist/push-web.js +81 -0
- package/dist/push-web.js.map +1 -0
- package/dist/rage.d.ts +14 -0
- package/dist/rage.js +27 -0
- package/dist/rage.js.map +1 -0
- package/dist/react-native/index.d.cts +640 -3
- package/dist/react-native/index.d.ts +640 -3
- package/dist/react-native/index.js +2015 -34
- package/dist/react-native/index.js.map +1 -1
- package/dist/styles-css.d.ts +6 -0
- package/dist/styles-css.js +327 -0
- package/dist/styles-css.js.map +1 -0
- package/dist/styles.d.ts +1 -2
- package/dist/styles.js +3 -134
- package/dist/styles.js.map +1 -1
- package/dist/ui.d.ts +3 -0
- package/dist/ui.js +12 -0
- package/dist/ui.js.map +1 -1
- package/package.json +20 -4
|
@@ -3,6 +3,7 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
7
|
var __export = (target, all) => {
|
|
7
8
|
for (var name in all)
|
|
8
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -16,27 +17,39 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
17
|
return to;
|
|
17
18
|
};
|
|
18
19
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
19
21
|
|
|
20
22
|
// src/native/index.tsx
|
|
21
23
|
var native_exports = {};
|
|
22
24
|
__export(native_exports, {
|
|
25
|
+
MRRocketFeedback: () => MRRocketFeedback,
|
|
26
|
+
MRRocketFlows: () => MRRocketFlows,
|
|
27
|
+
MRRocketMask: () => MRRocketMask,
|
|
28
|
+
MRRocketMessenger: () => MRRocketMessenger,
|
|
23
29
|
MRRocketProvider: () => MRRocketProvider,
|
|
30
|
+
MRRocketSurveys: () => MRRocketSurveys,
|
|
24
31
|
SDK_VERSION: () => SDK_VERSION,
|
|
25
32
|
activeRouteName: () => activeRouteName,
|
|
26
33
|
createMRRocket: () => createMRRocket,
|
|
27
34
|
getMRRocket: () => getMRRocket,
|
|
35
|
+
useCancelFlow: () => useCancelFlow,
|
|
28
36
|
useExpoRouterTracking: () => useExpoRouterTracking,
|
|
37
|
+
useFeedback: () => useFeedback,
|
|
29
38
|
useFlag: () => useFlag,
|
|
30
39
|
useFlagVariant: () => useFlagVariant,
|
|
31
40
|
useMRRocket: () => useMRRocket,
|
|
41
|
+
useMessenger: () => useMessenger,
|
|
32
42
|
useNavigationTracking: () => useNavigationTracking,
|
|
43
|
+
useOnboarding: () => useOnboarding,
|
|
44
|
+
usePush: () => usePush,
|
|
45
|
+
useText: () => useText,
|
|
33
46
|
useVariant: () => useVariant
|
|
34
47
|
});
|
|
35
48
|
module.exports = __toCommonJS(native_exports);
|
|
36
|
-
var
|
|
49
|
+
var import_react3 = require("react");
|
|
37
50
|
|
|
38
51
|
// src/core.ts
|
|
39
|
-
var SDK_VERSION = "0.
|
|
52
|
+
var SDK_VERSION = "0.9.0";
|
|
40
53
|
var propOf = (person, name) => {
|
|
41
54
|
const want = name.trim().toLowerCase();
|
|
42
55
|
for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
|
|
@@ -109,25 +122,25 @@ var parseQuery = (url) => {
|
|
|
109
122
|
return out;
|
|
110
123
|
};
|
|
111
124
|
var hashUnit = (input) => {
|
|
112
|
-
let
|
|
125
|
+
let h3 = 2166136261;
|
|
113
126
|
for (let i = 0; i < input.length; i++) {
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
h3 ^= input.charCodeAt(i);
|
|
128
|
+
h3 = Math.imul(h3, 16777619);
|
|
116
129
|
}
|
|
117
|
-
return (
|
|
130
|
+
return (h3 >>> 0) / 4294967296;
|
|
118
131
|
};
|
|
119
132
|
var flagUnit = (input) => {
|
|
120
|
-
let
|
|
133
|
+
let h3 = 2166136261;
|
|
121
134
|
for (let i = 0; i < input.length; i++) {
|
|
122
|
-
|
|
123
|
-
|
|
135
|
+
h3 ^= input.charCodeAt(i);
|
|
136
|
+
h3 = Math.imul(h3, 16777619);
|
|
124
137
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return (
|
|
138
|
+
h3 ^= h3 >>> 16;
|
|
139
|
+
h3 = Math.imul(h3, 2246822507);
|
|
140
|
+
h3 ^= h3 >>> 13;
|
|
141
|
+
h3 = Math.imul(h3, 3266489909);
|
|
142
|
+
h3 ^= h3 >>> 16;
|
|
143
|
+
return (h3 >>> 0) / 4294967296;
|
|
131
144
|
};
|
|
132
145
|
var assign = (experiment, distinctId) => {
|
|
133
146
|
const control = experiment.variants[0]?.id ?? "control";
|
|
@@ -141,13 +154,79 @@ var assign = (experiment, distinctId) => {
|
|
|
141
154
|
}
|
|
142
155
|
return experiment.variants[experiment.variants.length - 1]?.id ?? control;
|
|
143
156
|
};
|
|
157
|
+
var experimentsOfText = (config, id) => (config?.experiments ?? []).filter((e) => e.variants.some((v) => v.changes && id in v.changes)).map((e) => e.key);
|
|
144
158
|
var resolveVariant = (key, config, distinctId, preview) => {
|
|
145
159
|
if (preview[key]) return preview[key];
|
|
146
160
|
const experiment = config?.experiments.find((e) => e.key === key);
|
|
147
161
|
return experiment ? assign(experiment, distinctId) : "control";
|
|
148
162
|
};
|
|
163
|
+
var textChanges = (config, distinctId, preview) => {
|
|
164
|
+
const out = { ...config?.texts ?? {} };
|
|
165
|
+
for (const experiment of config?.experiments ?? []) {
|
|
166
|
+
const id = resolveVariant(experiment.key, config, distinctId, preview);
|
|
167
|
+
if (id === (experiment.variants[0]?.id ?? "control")) continue;
|
|
168
|
+
Object.assign(out, experiment.variants.find((v) => v.id === id)?.changes ?? {});
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
};
|
|
149
172
|
var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
150
173
|
|
|
174
|
+
// src/api-client.ts
|
|
175
|
+
var ApiError = class extends Error {
|
|
176
|
+
constructor(message, status) {
|
|
177
|
+
super(message);
|
|
178
|
+
__publicField(this, "status");
|
|
179
|
+
this.status = status;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var qs = (params) => Object.entries(params).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
183
|
+
var call = async (url, init, signal) => {
|
|
184
|
+
let res;
|
|
185
|
+
try {
|
|
186
|
+
res = await fetch(url, {
|
|
187
|
+
...init,
|
|
188
|
+
signal,
|
|
189
|
+
headers: init?.body ? { "content-type": "application/json" } : void 0
|
|
190
|
+
});
|
|
191
|
+
} catch (e) {
|
|
192
|
+
if (e.name === "AbortError") throw e;
|
|
193
|
+
throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
|
|
194
|
+
}
|
|
195
|
+
if (!res.ok) {
|
|
196
|
+
let detail = "";
|
|
197
|
+
try {
|
|
198
|
+
detail = (await res.json()).error ?? "";
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
|
|
202
|
+
}
|
|
203
|
+
return await res.json();
|
|
204
|
+
};
|
|
205
|
+
var createApi = (c) => {
|
|
206
|
+
const base = `${c.host}/api/sdk`;
|
|
207
|
+
const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };
|
|
208
|
+
const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who, ...body }) }, signal);
|
|
209
|
+
return {
|
|
210
|
+
list: (o, signal) => call(`${base}/feedback?${qs({ key: c.apiKey, distinctId: c.distinctId, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
|
|
211
|
+
post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
|
|
212
|
+
roadmap: (signal) => call(`${base}/feedback/roadmap?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
|
|
213
|
+
changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
|
|
214
|
+
submit: (p) => post("/feedback", p),
|
|
215
|
+
vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
|
|
216
|
+
comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
|
|
217
|
+
comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
|
|
218
|
+
survey: (url, signal) => call(`${base}/surveys?${qs({ key: c.apiKey, distinctId: c.distinctId, url })}`, void 0, signal),
|
|
219
|
+
seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
|
|
220
|
+
respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
|
|
221
|
+
chat: (signal) => call(`${base}/messages?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
|
|
222
|
+
sendChat: (m) => post("/messages", m),
|
|
223
|
+
articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
|
|
224
|
+
article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
|
|
225
|
+
messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
|
|
226
|
+
rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
151
230
|
// src/batch.ts
|
|
152
231
|
var MAX_BATCH = 100;
|
|
153
232
|
var MAX_BYTES = 6e4;
|
|
@@ -203,6 +282,568 @@ var describeError = (error) => {
|
|
|
203
282
|
}
|
|
204
283
|
};
|
|
205
284
|
|
|
285
|
+
// src/flows.ts
|
|
286
|
+
var CANCEL_EVENT = "$cancel_flow";
|
|
287
|
+
var ONBOARDING_EVENT = "$onboarding";
|
|
288
|
+
var INAPP_EVENT = "$inapp";
|
|
289
|
+
var PLAIN_CANCEL = {
|
|
290
|
+
version: 0,
|
|
291
|
+
question: "",
|
|
292
|
+
reasons: [],
|
|
293
|
+
offers: {},
|
|
294
|
+
confirm: { title: "Cancel your subscription?", body: "You keep access until the end of the billing period.", button: "Cancel subscription" },
|
|
295
|
+
offersFor: "business"
|
|
296
|
+
};
|
|
297
|
+
var FAILED = "Something went wrong. Please try again.";
|
|
298
|
+
var PROGRESS_KEY = "mrrocket:onboarding";
|
|
299
|
+
var eventSlug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
300
|
+
var SIGNUP = /* @__PURE__ */ new Set(["signed_up", "signup", "sign_up", "registered", "account_created", "user_signed_up"]);
|
|
301
|
+
var matchesTrigger = (trigger, event) => {
|
|
302
|
+
const t = eventSlug(trigger);
|
|
303
|
+
const e = eventSlug(event);
|
|
304
|
+
if (!t || !e) return false;
|
|
305
|
+
return t === e || SIGNUP.has(t) && SIGNUP.has(e);
|
|
306
|
+
};
|
|
307
|
+
var startsOnLoad = (trigger) => /^(first (launch|visit|open|session|login|sign ?in|app open)|app (opened|launched)|on load|every visit|always)?$/i.test(trigger.trim());
|
|
308
|
+
var runId = () => `cf_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
|
|
309
|
+
var discounted = (amount, percent) => Math.round(amount * (1 - percent / 100) * 100) / 100;
|
|
310
|
+
var money = (amount, currency) => {
|
|
311
|
+
try {
|
|
312
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
|
|
313
|
+
} catch {
|
|
314
|
+
return `${amount.toFixed(2)} ${currency}`;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var offerButton = (o) => o.kind === "contact" ? "Send to the team" : o.kind === "pause" ? "Pause instead" : o.kind === "downgrade" ? "Switch plan" : "Accept offer";
|
|
318
|
+
var safeUrl = (url) => {
|
|
319
|
+
const u = (url ?? "").trim();
|
|
320
|
+
return /^https?:\/\//i.test(u) || /^\/(?!\/)/.test(u) ? u : void 0;
|
|
321
|
+
};
|
|
322
|
+
var createFlows = (env) => {
|
|
323
|
+
let config = null;
|
|
324
|
+
let cancel = null;
|
|
325
|
+
let active = null;
|
|
326
|
+
let messages = [];
|
|
327
|
+
let known = false;
|
|
328
|
+
let loading = null;
|
|
329
|
+
let loadedFor;
|
|
330
|
+
let progressFor = null;
|
|
331
|
+
const progress = /* @__PURE__ */ new Map();
|
|
332
|
+
const handled = /* @__PURE__ */ new Set();
|
|
333
|
+
const shown = /* @__PURE__ */ new Set();
|
|
334
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
335
|
+
const auto = env.auto !== false;
|
|
336
|
+
const flows = () => config?.onboarding ?? [];
|
|
337
|
+
const flowById = (id) => id ? flows().find((f) => f.id === id) : void 0;
|
|
338
|
+
const prog = (id) => {
|
|
339
|
+
let p = progress.get(id);
|
|
340
|
+
if (!p) progress.set(id, p = { done: /* @__PURE__ */ new Set(), dismissed: false });
|
|
341
|
+
return p;
|
|
342
|
+
};
|
|
343
|
+
const finished = (f) => {
|
|
344
|
+
const p = progress.get(f.id);
|
|
345
|
+
return !!p && (p.dismissed || f.steps.every((s) => p.done.has(s.id)));
|
|
346
|
+
};
|
|
347
|
+
const keyOf = (pid) => `${PROGRESS_KEY}:${pid ?? "visitor"}`;
|
|
348
|
+
const save = () => {
|
|
349
|
+
const store = env.storage?.();
|
|
350
|
+
if (!store) return;
|
|
351
|
+
const out = {};
|
|
352
|
+
for (const [id, p] of progress) out[id] = { done: [...p.done], ...p.dismissed ? { dismissed: true } : {} };
|
|
353
|
+
try {
|
|
354
|
+
store.set(keyOf(env.personId()), JSON.stringify(out));
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const merge = (state) => {
|
|
359
|
+
for (const [id, s] of Object.entries(state ?? {})) {
|
|
360
|
+
const p = prog(id);
|
|
361
|
+
for (const d of s.done ?? []) p.done.add(d);
|
|
362
|
+
if (s.dismissed) p.dismissed = true;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const onboardingView = () => {
|
|
366
|
+
const flow = flowById(active);
|
|
367
|
+
if (!flow) return null;
|
|
368
|
+
const p = prog(flow.id);
|
|
369
|
+
const at = flow.steps.findIndex((s) => !p.done.has(s.id));
|
|
370
|
+
if (at < 0 || p.dismissed) return null;
|
|
371
|
+
const step = flow.steps[at];
|
|
372
|
+
if (step.kind === "checklist") {
|
|
373
|
+
let from = at;
|
|
374
|
+
while (from > 0 && flow.steps[from - 1].kind === "checklist") from--;
|
|
375
|
+
let to = at;
|
|
376
|
+
while (to + 1 < flow.steps.length && flow.steps[to + 1].kind === "checklist") to++;
|
|
377
|
+
return { type: "checklist", flow, items: flow.steps.slice(from, to + 1).map((s) => ({ step: s, done: p.done.has(s.id) })) };
|
|
378
|
+
}
|
|
379
|
+
return { type: "step", flow, step, index: at, total: flow.steps.length, last: at === flow.steps.length - 1 };
|
|
380
|
+
};
|
|
381
|
+
const view = () => {
|
|
382
|
+
if (cancel) {
|
|
383
|
+
const offer = cancel.reason ? cancel.flow.offers[cancel.reason] : void 0;
|
|
384
|
+
return {
|
|
385
|
+
type: "cancel",
|
|
386
|
+
screen: cancel.screen,
|
|
387
|
+
flow: cancel.flow,
|
|
388
|
+
reason: cancel.reason,
|
|
389
|
+
offer: cancel.screen === "offer" ? offer : void 0,
|
|
390
|
+
price: cancel.options.price,
|
|
391
|
+
busy: cancel.busy,
|
|
392
|
+
error: cancel.error,
|
|
393
|
+
step: cancel.plain ? 1 : cancel.screen === "survey" ? 1 : cancel.screen === "offer" ? 2 : 3,
|
|
394
|
+
steps: cancel.plain ? 1 : 3
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const ob = onboardingView();
|
|
398
|
+
if (ob) return ob;
|
|
399
|
+
if (active) active = null;
|
|
400
|
+
const m = messages.find((x) => !handled.has(x.id));
|
|
401
|
+
return m ? { type: "message", message: m } : null;
|
|
402
|
+
};
|
|
403
|
+
const emit = () => {
|
|
404
|
+
const v = view();
|
|
405
|
+
if (v?.type === "step" && !shown.has(`s:${v.step.id}`)) {
|
|
406
|
+
shown.add(`s:${v.step.id}`);
|
|
407
|
+
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "shown" });
|
|
408
|
+
} else if (v?.type === "checklist") {
|
|
409
|
+
for (const i of v.items)
|
|
410
|
+
if (!i.done && !shown.has(`s:${i.step.id}`)) {
|
|
411
|
+
shown.add(`s:${i.step.id}`);
|
|
412
|
+
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: i.step.id, kind: "checklist", action: "shown" });
|
|
413
|
+
}
|
|
414
|
+
} else if (v?.type === "message" && !shown.has(`m:${v.message.id}`)) {
|
|
415
|
+
shown.add(`m:${v.message.id}`);
|
|
416
|
+
env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "shown" });
|
|
417
|
+
}
|
|
418
|
+
for (const l of [...listeners]) {
|
|
419
|
+
try {
|
|
420
|
+
l();
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const start = (flow) => {
|
|
426
|
+
if (active === flow.id) return true;
|
|
427
|
+
if (active && flowById(active) && !finished(flowById(active))) return false;
|
|
428
|
+
active = flow.id;
|
|
429
|
+
env.track(ONBOARDING_EVENT, { flow: flow.id, action: "started" });
|
|
430
|
+
emit();
|
|
431
|
+
return true;
|
|
432
|
+
};
|
|
433
|
+
const autoStart = () => {
|
|
434
|
+
if (!auto || active || !known) return;
|
|
435
|
+
if (!env.personId() && !env.storage?.()) return;
|
|
436
|
+
const next = flows().find((f) => startsOnLoad(f.trigger) && f.steps.length && !finished(f));
|
|
437
|
+
if (next) start(next);
|
|
438
|
+
};
|
|
439
|
+
const refresh = () => {
|
|
440
|
+
const pid = env.personId();
|
|
441
|
+
if (progressFor !== null && progressFor !== pid) {
|
|
442
|
+
progress.clear();
|
|
443
|
+
handled.clear();
|
|
444
|
+
active = null;
|
|
445
|
+
loadedFor = void 0;
|
|
446
|
+
}
|
|
447
|
+
progressFor = pid;
|
|
448
|
+
const run = (async () => {
|
|
449
|
+
const store = env.storage?.();
|
|
450
|
+
if (store) {
|
|
451
|
+
try {
|
|
452
|
+
const raw = await store.get(keyOf(pid));
|
|
453
|
+
if (raw) merge(JSON.parse(raw));
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const wants = flows().length > 0 || !!config?.messages;
|
|
458
|
+
if (pid && wants && loadedFor !== pid) {
|
|
459
|
+
loadedFor = pid;
|
|
460
|
+
const state = await env.fetchInApp(pid).catch(() => null);
|
|
461
|
+
if (state && env.personId() === pid) {
|
|
462
|
+
merge(state.onboarding);
|
|
463
|
+
messages = (state.messages ?? []).filter((m) => m && m.id && m.title);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (!pid) messages = [];
|
|
467
|
+
known = true;
|
|
468
|
+
autoStart();
|
|
469
|
+
emit();
|
|
470
|
+
})();
|
|
471
|
+
loading = run.finally(() => {
|
|
472
|
+
if (loading === run) loading = null;
|
|
473
|
+
});
|
|
474
|
+
return loading;
|
|
475
|
+
};
|
|
476
|
+
const result = (c, outcome, comment) => {
|
|
477
|
+
const offer = c.reason ? c.flow.offers[c.reason] : void 0;
|
|
478
|
+
return {
|
|
479
|
+
outcome,
|
|
480
|
+
run: c.run,
|
|
481
|
+
...c.reason ? { reason: c.reason, reasonLabel: c.flow.reasons.find((r) => r.id === c.reason)?.label } : {},
|
|
482
|
+
...outcome === "saved" && offer ? { offer } : {},
|
|
483
|
+
...comment?.trim() ? { comment: comment.trim().slice(0, 500) } : {}
|
|
484
|
+
};
|
|
485
|
+
};
|
|
486
|
+
const finish = (c, r) => {
|
|
487
|
+
env.track(CANCEL_EVENT, {
|
|
488
|
+
action: "finished",
|
|
489
|
+
run: c.run,
|
|
490
|
+
version: c.flow.version,
|
|
491
|
+
outcome: r.outcome,
|
|
492
|
+
customer: c.options.customer ?? "consumer",
|
|
493
|
+
...r.reason ? { reason: r.reason } : {},
|
|
494
|
+
...r.offer ? { offer: r.offer.kind } : {},
|
|
495
|
+
...r.comment ? { comment: r.comment } : {}
|
|
496
|
+
});
|
|
497
|
+
if (cancel === c) cancel = null;
|
|
498
|
+
c.resolve(r);
|
|
499
|
+
emit();
|
|
500
|
+
};
|
|
501
|
+
const settle = (c, outcome, comment) => {
|
|
502
|
+
if (c.busy) return;
|
|
503
|
+
const r = result(c, outcome, comment);
|
|
504
|
+
const fn = outcome === "saved" ? c.options.onSave : c.options.onCancel;
|
|
505
|
+
c.busy = true;
|
|
506
|
+
c.error = void 0;
|
|
507
|
+
emit();
|
|
508
|
+
Promise.resolve().then(() => fn?.(r)).then(
|
|
509
|
+
() => finish(c, r),
|
|
510
|
+
() => {
|
|
511
|
+
c.busy = false;
|
|
512
|
+
c.error = FAILED;
|
|
513
|
+
emit();
|
|
514
|
+
}
|
|
515
|
+
);
|
|
516
|
+
};
|
|
517
|
+
const openCancelFlow = (options = {}) => {
|
|
518
|
+
if (cancel) return cancel.promise;
|
|
519
|
+
const flow = config?.cancelFlow ?? PLAIN_CANCEL;
|
|
520
|
+
const plain = options.customer !== "business" || flow.reasons.length < 2;
|
|
521
|
+
let resolve = () => {
|
|
522
|
+
};
|
|
523
|
+
const promise = new Promise((r) => resolve = r);
|
|
524
|
+
const c = { flow, options, plain, screen: plain ? "confirm" : "survey", run: runId(), busy: false, resolve, promise };
|
|
525
|
+
cancel = c;
|
|
526
|
+
env.track(CANCEL_EVENT, { action: "shown", run: c.run, version: flow.version, customer: options.customer ?? "consumer", plain });
|
|
527
|
+
emit();
|
|
528
|
+
return promise;
|
|
529
|
+
};
|
|
530
|
+
const controller = {
|
|
531
|
+
setConfig: (c) => {
|
|
532
|
+
config = c;
|
|
533
|
+
if (active && !flowById(active)) active = null;
|
|
534
|
+
emit();
|
|
535
|
+
},
|
|
536
|
+
refresh,
|
|
537
|
+
onEvent: (name) => {
|
|
538
|
+
if (!name || name.startsWith("$")) return;
|
|
539
|
+
const flow = flowById(active);
|
|
540
|
+
let ticked = false;
|
|
541
|
+
if (flow) {
|
|
542
|
+
const p = prog(flow.id);
|
|
543
|
+
for (const s of flow.steps)
|
|
544
|
+
if (s.kind === "checklist" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {
|
|
545
|
+
p.done.add(s.id);
|
|
546
|
+
ticked = true;
|
|
547
|
+
env.track(ONBOARDING_EVENT, { flow: flow.id, step: s.id, kind: s.kind, action: "completed" });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (ticked) {
|
|
551
|
+
save();
|
|
552
|
+
emit();
|
|
553
|
+
}
|
|
554
|
+
if (!auto || !env.personId() && !env.storage?.()) return;
|
|
555
|
+
const next = flows().find((f) => f.steps.length && matchesTrigger(f.trigger, name) && !finished(f));
|
|
556
|
+
if (next) start(next);
|
|
557
|
+
},
|
|
558
|
+
openCancelFlow,
|
|
559
|
+
startOnboarding: (id) => {
|
|
560
|
+
const f = id ? flowById(id) : flows().find((x) => x.steps.length && !finished(x));
|
|
561
|
+
if (!f || !f.steps.length) return false;
|
|
562
|
+
const p = prog(f.id);
|
|
563
|
+
p.dismissed = false;
|
|
564
|
+
return start(f);
|
|
565
|
+
},
|
|
566
|
+
view,
|
|
567
|
+
subscribe: (l) => {
|
|
568
|
+
listeners.add(l);
|
|
569
|
+
return () => listeners.delete(l);
|
|
570
|
+
},
|
|
571
|
+
cancel: {
|
|
572
|
+
pick: (reason) => {
|
|
573
|
+
if (!cancel || cancel.busy || !cancel.flow.reasons.some((r) => r.id === reason)) return;
|
|
574
|
+
cancel.reason = reason;
|
|
575
|
+
cancel.error = void 0;
|
|
576
|
+
emit();
|
|
577
|
+
},
|
|
578
|
+
next: () => {
|
|
579
|
+
const c = cancel;
|
|
580
|
+
if (!c || c.screen !== "survey" || !c.reason) return;
|
|
581
|
+
env.track(CANCEL_EVENT, { action: "answered", run: c.run, version: c.flow.version, reason: c.reason });
|
|
582
|
+
c.screen = c.flow.offers[c.reason] ? "offer" : "confirm";
|
|
583
|
+
emit();
|
|
584
|
+
},
|
|
585
|
+
accept: (comment) => {
|
|
586
|
+
const c = cancel;
|
|
587
|
+
if (!c || c.screen !== "offer" || !c.reason) return;
|
|
588
|
+
const offer = c.flow.offers[c.reason];
|
|
589
|
+
if (!c.busy)
|
|
590
|
+
env.track(CANCEL_EVENT, {
|
|
591
|
+
action: "offer",
|
|
592
|
+
run: c.run,
|
|
593
|
+
version: c.flow.version,
|
|
594
|
+
reason: c.reason,
|
|
595
|
+
offer: offer?.kind,
|
|
596
|
+
outcome: "accepted"
|
|
597
|
+
});
|
|
598
|
+
settle(c, "saved", comment);
|
|
599
|
+
},
|
|
600
|
+
decline: () => {
|
|
601
|
+
const c = cancel;
|
|
602
|
+
if (!c || c.screen !== "offer" || c.busy) return;
|
|
603
|
+
env.track(CANCEL_EVENT, {
|
|
604
|
+
action: "offer",
|
|
605
|
+
run: c.run,
|
|
606
|
+
version: c.flow.version,
|
|
607
|
+
reason: c.reason,
|
|
608
|
+
offer: c.reason ? c.flow.offers[c.reason]?.kind : void 0,
|
|
609
|
+
outcome: "declined"
|
|
610
|
+
});
|
|
611
|
+
c.screen = "confirm";
|
|
612
|
+
c.error = void 0;
|
|
613
|
+
emit();
|
|
614
|
+
},
|
|
615
|
+
confirm: (comment) => {
|
|
616
|
+
const c = cancel;
|
|
617
|
+
if (!c || c.screen !== "confirm") return;
|
|
618
|
+
settle(c, "cancelled", comment);
|
|
619
|
+
},
|
|
620
|
+
close: () => {
|
|
621
|
+
const c = cancel;
|
|
622
|
+
if (!c || c.busy) return;
|
|
623
|
+
const r = result(c, "kept");
|
|
624
|
+
c.options.onClose?.(r);
|
|
625
|
+
finish(c, r);
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
onboarding: {
|
|
629
|
+
next: () => {
|
|
630
|
+
const v = onboardingView();
|
|
631
|
+
if (v?.type !== "step") return;
|
|
632
|
+
prog(v.flow.id).done.add(v.step.id);
|
|
633
|
+
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: v.step.kind, action: "completed" });
|
|
634
|
+
save();
|
|
635
|
+
emit();
|
|
636
|
+
},
|
|
637
|
+
check: (stepId) => {
|
|
638
|
+
const v = onboardingView();
|
|
639
|
+
if (v?.type !== "checklist") return;
|
|
640
|
+
const item = v.items.find((i) => i.step.id === stepId);
|
|
641
|
+
if (!item || item.done) return;
|
|
642
|
+
prog(v.flow.id).done.add(stepId);
|
|
643
|
+
env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: stepId, kind: "checklist", action: "completed" });
|
|
644
|
+
save();
|
|
645
|
+
emit();
|
|
646
|
+
},
|
|
647
|
+
dismiss: () => {
|
|
648
|
+
const v = onboardingView();
|
|
649
|
+
if (!v || v.type !== "step" && v.type !== "checklist") return;
|
|
650
|
+
prog(v.flow.id).dismissed = true;
|
|
651
|
+
env.track(ONBOARDING_EVENT, { flow: v.flow.id, ...v.type === "step" ? { step: v.step.id } : {}, action: "dismissed" });
|
|
652
|
+
active = null;
|
|
653
|
+
save();
|
|
654
|
+
emit();
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
message: {
|
|
658
|
+
click: () => {
|
|
659
|
+
const v = view();
|
|
660
|
+
if (v?.type !== "message") return;
|
|
661
|
+
handled.add(v.message.id);
|
|
662
|
+
env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "clicked" });
|
|
663
|
+
const url = safeUrl(v.message.url);
|
|
664
|
+
emit();
|
|
665
|
+
if (url) env.openUrl?.(url);
|
|
666
|
+
},
|
|
667
|
+
dismiss: () => {
|
|
668
|
+
const v = view();
|
|
669
|
+
if (v?.type !== "message") return;
|
|
670
|
+
handled.add(v.message.id);
|
|
671
|
+
env.track(INAPP_EVENT, { message: v.message.id, journey: v.message.journey, action: "dismissed" });
|
|
672
|
+
emit();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
return controller;
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// src/rage.ts
|
|
680
|
+
var RageDetector = class {
|
|
681
|
+
constructor(windowMs = 1e3, radius = 30, count = 3) {
|
|
682
|
+
__publicField(this, "windowMs", windowMs);
|
|
683
|
+
__publicField(this, "radius", radius);
|
|
684
|
+
__publicField(this, "count", count);
|
|
685
|
+
__publicField(this, "clicks", []);
|
|
686
|
+
__publicField(this, "fired", false);
|
|
687
|
+
}
|
|
688
|
+
push(x, y, t) {
|
|
689
|
+
const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
|
|
690
|
+
if (near.length !== this.clicks.length) this.fired = false;
|
|
691
|
+
this.clicks = [...near, { x, y, t }];
|
|
692
|
+
if (this.clicks.length >= this.count && !this.fired) {
|
|
693
|
+
this.fired = true;
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// src/native/recorder.ts
|
|
701
|
+
var FRAME_MS = 1e3;
|
|
702
|
+
var WIDTH = 360;
|
|
703
|
+
var BUFFER_MS = 6e4;
|
|
704
|
+
var CHUNK_FRAMES = 12;
|
|
705
|
+
var CHUNK_CHARS = 7e5;
|
|
706
|
+
var MAX_MS = 30 * 6e4;
|
|
707
|
+
var shareOf = (s) => {
|
|
708
|
+
let h3 = 2166136261;
|
|
709
|
+
for (let i = 0; i < s.length; i++) h3 = Math.imul(h3 ^ s.charCodeAt(i), 16777619);
|
|
710
|
+
return (h3 >>> 0) / 4294967296;
|
|
711
|
+
};
|
|
712
|
+
var createRecorder = (deps, st) => {
|
|
713
|
+
let timer = null;
|
|
714
|
+
let busy = false;
|
|
715
|
+
let last = "";
|
|
716
|
+
let rec = null;
|
|
717
|
+
let frames = [];
|
|
718
|
+
let marks = [];
|
|
719
|
+
const ensure = () => {
|
|
720
|
+
const session = st.session();
|
|
721
|
+
const cfg = st.config();
|
|
722
|
+
if (!rec || rec.session !== session) {
|
|
723
|
+
frames = [];
|
|
724
|
+
marks = [];
|
|
725
|
+
last = "";
|
|
726
|
+
const sampled = !!cfg && shareOf(session) < (cfg.sampleRate ?? 0.1);
|
|
727
|
+
rec = { session, start: deps.now(), seq: 0, uploading: sampled, trigger: "sample", stopped: false };
|
|
728
|
+
}
|
|
729
|
+
return rec;
|
|
730
|
+
};
|
|
731
|
+
const send = async (r) => {
|
|
732
|
+
if (!frames.length || r.stopped) return;
|
|
733
|
+
const batch = frames.splice(0, frames.length);
|
|
734
|
+
const ms = marks.splice(0, marks.length);
|
|
735
|
+
const size = deps.screen();
|
|
736
|
+
const { distinctId, identified } = st.who();
|
|
737
|
+
const body = JSON.stringify({
|
|
738
|
+
key: st.apiKey,
|
|
739
|
+
consent: { replay: true },
|
|
740
|
+
sessionId: r.session,
|
|
741
|
+
start: r.start,
|
|
742
|
+
seq: r.seq++,
|
|
743
|
+
enc: "frames",
|
|
744
|
+
data: JSON.stringify(batch),
|
|
745
|
+
meta: { first: batch[0].t, last: batch[batch.length - 1].t, vw: size ? Math.round(size.w) : null, vh: size ? Math.round(size.h) : null, marks: ms },
|
|
746
|
+
distinctId,
|
|
747
|
+
identified,
|
|
748
|
+
trigger: r.trigger,
|
|
749
|
+
app: st.app
|
|
750
|
+
});
|
|
751
|
+
const status = await deps.send(body);
|
|
752
|
+
if (status === 403 || status === 409 || status === 413) r.stopped = true;
|
|
753
|
+
};
|
|
754
|
+
const tick = async () => {
|
|
755
|
+
if (busy || !st.allowed() || !st.config()) return;
|
|
756
|
+
const r = ensure();
|
|
757
|
+
if (r.stopped || deps.now() - r.start > MAX_MS) return;
|
|
758
|
+
if (deps.keyboardOpen()) return;
|
|
759
|
+
busy = true;
|
|
760
|
+
try {
|
|
761
|
+
const shot = await deps.capture(WIDTH);
|
|
762
|
+
if (!shot || shot.img === last) return;
|
|
763
|
+
last = shot.img;
|
|
764
|
+
const size = deps.screen();
|
|
765
|
+
const scale = size && size.w ? shot.w / size.w : 1;
|
|
766
|
+
const measured = await deps.masks();
|
|
767
|
+
if (measured === null) {
|
|
768
|
+
last = "";
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const masks = measured.map(([x, y, w, h3]) => [x * scale, y * scale, w * scale, h3 * scale].map(Math.round));
|
|
772
|
+
frames.push({ t: deps.now(), w: shot.w, h: shot.h, img: shot.img, masks });
|
|
773
|
+
if (!r.uploading) {
|
|
774
|
+
const since = deps.now() - BUFFER_MS;
|
|
775
|
+
frames = frames.filter((f) => f.t >= since);
|
|
776
|
+
marks = marks.filter((m) => m.t >= since);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const chars = frames.reduce((a, f) => a + f.img.length, 0);
|
|
780
|
+
if (frames.length >= CHUNK_FRAMES || chars >= CHUNK_CHARS) await send(r);
|
|
781
|
+
} catch {
|
|
782
|
+
} finally {
|
|
783
|
+
busy = false;
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
return {
|
|
787
|
+
/** Starts or stops the frame loop to match the app state (active) and whether recording is allowed. */
|
|
788
|
+
sync(active) {
|
|
789
|
+
const on = active && st.allowed() && !!st.config();
|
|
790
|
+
if (on && timer === null) timer = deps.setInterval(() => void tick(), FRAME_MS);
|
|
791
|
+
if (!on && timer !== null) {
|
|
792
|
+
deps.clearInterval(timer);
|
|
793
|
+
timer = null;
|
|
794
|
+
if (rec?.uploading) void send(rec);
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
/** A screen, tap, rage tap or error, for the timeline. */
|
|
798
|
+
mark(m) {
|
|
799
|
+
if (!st.allowed() || !st.config()) return;
|
|
800
|
+
ensure();
|
|
801
|
+
marks.push({ t: deps.now(), ...m });
|
|
802
|
+
if (marks.length > 300) marks.splice(0, marks.length - 300);
|
|
803
|
+
},
|
|
804
|
+
/** A rage tap or an error: a buffering recording uploads its last minute and keeps recording. */
|
|
805
|
+
trigger(kind) {
|
|
806
|
+
if (!rec || rec.uploading || rec.stopped || !st.allowed()) return;
|
|
807
|
+
rec.uploading = true;
|
|
808
|
+
rec.trigger = kind;
|
|
809
|
+
void send(rec);
|
|
810
|
+
},
|
|
811
|
+
/** Sends what waits (going to the background). */
|
|
812
|
+
flush() {
|
|
813
|
+
if (rec?.uploading) return send(rec);
|
|
814
|
+
return Promise.resolve();
|
|
815
|
+
},
|
|
816
|
+
/** Consent withdrawn: forget everything not sent. */
|
|
817
|
+
drop() {
|
|
818
|
+
frames = [];
|
|
819
|
+
marks = [];
|
|
820
|
+
rec = null;
|
|
821
|
+
last = "";
|
|
822
|
+
},
|
|
823
|
+
/** For tests: one frame now. */
|
|
824
|
+
tick
|
|
825
|
+
};
|
|
826
|
+
};
|
|
827
|
+
var maskedViews = /* @__PURE__ */ new Set();
|
|
828
|
+
var measureMasks = () => Promise.all(
|
|
829
|
+
[...maskedViews].map(
|
|
830
|
+
(ref) => new Promise((resolve) => {
|
|
831
|
+
const v = ref.current;
|
|
832
|
+
if (!v) return resolve("none");
|
|
833
|
+
const t = setTimeout(() => resolve("failed"), 100);
|
|
834
|
+
try {
|
|
835
|
+
v.measureInWindow((x, y, w, h3) => {
|
|
836
|
+
clearTimeout(t);
|
|
837
|
+
resolve(w > 0 && h3 > 0 ? [x, y, w, h3] : "none");
|
|
838
|
+
});
|
|
839
|
+
} catch {
|
|
840
|
+
clearTimeout(t);
|
|
841
|
+
resolve("failed");
|
|
842
|
+
}
|
|
843
|
+
})
|
|
844
|
+
)
|
|
845
|
+
).then((rs) => rs.includes("failed") ? null : rs.filter((r) => Array.isArray(r)));
|
|
846
|
+
|
|
206
847
|
// src/referral.ts
|
|
207
848
|
var REF_PARAM = "mrr_ref";
|
|
208
849
|
var CLICK_PARAM = "mrr_click";
|
|
@@ -225,7 +866,8 @@ var KEYS = {
|
|
|
225
866
|
queue: "mrrocket:queue",
|
|
226
867
|
config: "mrrocket:config",
|
|
227
868
|
installed: "mrrocket:installed",
|
|
228
|
-
referral: "mrrocket:referral"
|
|
869
|
+
referral: "mrrocket:referral",
|
|
870
|
+
surveys: "mrrocket:surveys"
|
|
229
871
|
};
|
|
230
872
|
var MAX_ERRORS = 20;
|
|
231
873
|
var MAX_CRUMBS = 20;
|
|
@@ -288,13 +930,43 @@ var createClient = (options, deps) => {
|
|
|
288
930
|
let backgroundSince = null;
|
|
289
931
|
const early = [];
|
|
290
932
|
const exposed = /* @__PURE__ */ new Set();
|
|
933
|
+
const surveysDone = /* @__PURE__ */ new Set();
|
|
934
|
+
const rage = new RageDetector();
|
|
935
|
+
let textsFor = null;
|
|
936
|
+
const texts = () => {
|
|
937
|
+
const id = distinctId();
|
|
938
|
+
if (!textsFor || textsFor.config !== config || textsFor.id !== id) textsFor = { config, id, texts: textChanges(config, id, {}) };
|
|
939
|
+
return textsFor.texts;
|
|
940
|
+
};
|
|
291
941
|
const listeners = /* @__PURE__ */ new Set();
|
|
292
942
|
const crumbs = [];
|
|
293
943
|
const errorQueue = [];
|
|
294
944
|
const errorsSeen = /* @__PURE__ */ new Set();
|
|
295
945
|
let errorsSent = 0;
|
|
946
|
+
const flows = createFlows({
|
|
947
|
+
track: (event, props) => record(event, props),
|
|
948
|
+
personId: () => consent !== "off" ? identified : void 0,
|
|
949
|
+
auto: options.onboarding !== false,
|
|
950
|
+
storage: () => storage && consent === "persistent" ? {
|
|
951
|
+
get: (k) => get(k),
|
|
952
|
+
set: (k, v) => void set(k, v)
|
|
953
|
+
} : null,
|
|
954
|
+
openUrl: deps.openUrl,
|
|
955
|
+
fetchInApp: async (id) => {
|
|
956
|
+
if (!apiKey) return null;
|
|
957
|
+
const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}`, { method: "GET" });
|
|
958
|
+
return r.ok ? await r.json() : null;
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
const refreshFlows = () => {
|
|
962
|
+
if (!isReady || closed) return;
|
|
963
|
+
flows.setConfig(config);
|
|
964
|
+
void flows.refresh();
|
|
965
|
+
};
|
|
966
|
+
let recorder = null;
|
|
296
967
|
const changed = () => {
|
|
297
968
|
version++;
|
|
969
|
+
recorder?.sync(appState === "active");
|
|
298
970
|
for (const l of [...listeners]) {
|
|
299
971
|
try {
|
|
300
972
|
l();
|
|
@@ -469,6 +1141,8 @@ var createClient = (options, deps) => {
|
|
|
469
1141
|
...who ? { distinctId: who } : {},
|
|
470
1142
|
...session ? { sessionId: session.id } : {}
|
|
471
1143
|
});
|
|
1144
|
+
recorder?.mark({ kind: "error", path: currentScreen ?? "/", label: clip2(d.message, 120) });
|
|
1145
|
+
recorder?.trigger("error");
|
|
472
1146
|
if (fatal) void sendErrors();
|
|
473
1147
|
else if (errorTimer === null) errorTimer = setTimeoutFn(() => void sendErrors(), 2e3);
|
|
474
1148
|
};
|
|
@@ -486,11 +1160,34 @@ var createClient = (options, deps) => {
|
|
|
486
1160
|
previousHandler?.(error, isFatal);
|
|
487
1161
|
});
|
|
488
1162
|
};
|
|
1163
|
+
let replayOn = options.replay === true;
|
|
1164
|
+
recorder = deps.recorder ? createRecorder(
|
|
1165
|
+
{
|
|
1166
|
+
now,
|
|
1167
|
+
setInterval: setIntervalFn,
|
|
1168
|
+
clearInterval: clearIntervalFn,
|
|
1169
|
+
capture: deps.recorder.capture,
|
|
1170
|
+
keyboardOpen: deps.recorder.keyboardOpen,
|
|
1171
|
+
masks: deps.recorder.masks,
|
|
1172
|
+
screen: () => deps.screen?.(),
|
|
1173
|
+
send: (body) => post("/api/sdk/replay", body)
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
allowed: () => replayOn && loaded && !closed && consent !== "off" && !!apiKey,
|
|
1177
|
+
config: () => config?.replay,
|
|
1178
|
+
session: () => session?.id ?? touchSession(),
|
|
1179
|
+
who: () => ({ distinctId: sendId(), identified: !!identified }),
|
|
1180
|
+
apiKey,
|
|
1181
|
+
app: context
|
|
1182
|
+
}
|
|
1183
|
+
) : null;
|
|
1184
|
+
if (options.replay && !deps.recorder) warn("Session recordings need react-native-view-shot: npx expo install react-native-view-shot");
|
|
489
1185
|
let appStateSub;
|
|
490
1186
|
const onAppState = (next) => {
|
|
491
1187
|
if (closed) return;
|
|
492
1188
|
const prev = appState;
|
|
493
1189
|
appState = next;
|
|
1190
|
+
recorder?.sync(next === "active");
|
|
494
1191
|
if (next === "active" && prev !== "active") {
|
|
495
1192
|
startTimer();
|
|
496
1193
|
if (backgroundSince !== null) {
|
|
@@ -503,6 +1200,7 @@ var createClient = (options, deps) => {
|
|
|
503
1200
|
crumb("lifecycle", "app_background");
|
|
504
1201
|
if (options.lifecycle !== false) record("$app_background");
|
|
505
1202
|
stopTimer();
|
|
1203
|
+
void recorder?.flush();
|
|
506
1204
|
void persistQueue();
|
|
507
1205
|
void flush();
|
|
508
1206
|
if (errorQueue.length) void sendErrors();
|
|
@@ -526,6 +1224,61 @@ var createClient = (options, deps) => {
|
|
|
526
1224
|
debug("Config not reachable; using the cached one.");
|
|
527
1225
|
}
|
|
528
1226
|
};
|
|
1227
|
+
let pushToken = null;
|
|
1228
|
+
let pushFor;
|
|
1229
|
+
let pushOff = null;
|
|
1230
|
+
const opened = /* @__PURE__ */ new Set();
|
|
1231
|
+
const onPushOpen = (data) => {
|
|
1232
|
+
const m = data?.mrrocket;
|
|
1233
|
+
if (!m?.id || opened.has(m.id)) return;
|
|
1234
|
+
opened.add(m.id);
|
|
1235
|
+
record("$push", { action: "opened", message: m.id });
|
|
1236
|
+
if (m.url && /^[a-z][a-z0-9+.-]*:/i.test(m.url)) deps.openUrl?.(m.url);
|
|
1237
|
+
};
|
|
1238
|
+
const listenForOpens = () => {
|
|
1239
|
+
if (pushOff || !deps.push || closed) return;
|
|
1240
|
+
try {
|
|
1241
|
+
pushOff = deps.push.onOpen(onPushOpen);
|
|
1242
|
+
void deps.push.lastOpen?.().then(onPushOpen).catch(() => void 0);
|
|
1243
|
+
} catch {
|
|
1244
|
+
pushOff = null;
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
const loadedNow = () => loaded ? Promise.resolve() : new Promise((resolve) => {
|
|
1248
|
+
early.push(resolve);
|
|
1249
|
+
});
|
|
1250
|
+
const registerForPush = async (o = {}) => {
|
|
1251
|
+
if (!deps.push) return { status: "unavailable", reason: "Install expo-notifications to receive push notifications." };
|
|
1252
|
+
await loadedNow();
|
|
1253
|
+
if (!apiKey || consent === "off") return { status: "error", reason: !apiKey ? "No public key." : "The consent is off." };
|
|
1254
|
+
if (!identified) return { status: "not-signed-in" };
|
|
1255
|
+
const who = identified;
|
|
1256
|
+
try {
|
|
1257
|
+
const permission = await deps.push.permission(o.ask !== false);
|
|
1258
|
+
if (permission !== "granted") return { status: permission === "denied" ? "denied" : "not-asked" };
|
|
1259
|
+
const token = await deps.push.token(o.projectId);
|
|
1260
|
+
if (!token) return { status: "unavailable", reason: "This device has no push token (a simulator, or no EAS project id)." };
|
|
1261
|
+
const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: who, platform: "expo", token, app: context }));
|
|
1262
|
+
if (status < 200 || status >= 300) {
|
|
1263
|
+
warn(`Push registration was rejected with ${status || "no answer"}. Check the public key and the host.`);
|
|
1264
|
+
return { status: "error", reason: `MRRocket answered ${status || "nothing"}.` };
|
|
1265
|
+
}
|
|
1266
|
+
pushToken = token;
|
|
1267
|
+
pushFor = who;
|
|
1268
|
+
listenForOpens();
|
|
1269
|
+
debug("Registered for push.");
|
|
1270
|
+
return { status: "registered", token };
|
|
1271
|
+
} catch (e) {
|
|
1272
|
+
return { status: "error", reason: e instanceof Error ? e.message : String(e) };
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
const unregisterForPush = async () => {
|
|
1276
|
+
const token = pushToken;
|
|
1277
|
+
if (!token || !apiKey) return false;
|
|
1278
|
+
pushToken = null;
|
|
1279
|
+
const status = await post("/api/sdk/push", JSON.stringify({ key: apiKey, distinctId: pushFor, platform: "expo", token, remove: true }));
|
|
1280
|
+
return status >= 200 && status < 300;
|
|
1281
|
+
};
|
|
529
1282
|
let resolveReady = () => {
|
|
530
1283
|
};
|
|
531
1284
|
const ready = new Promise((r) => {
|
|
@@ -540,12 +1293,13 @@ var createClient = (options, deps) => {
|
|
|
540
1293
|
const start = async () => {
|
|
541
1294
|
let firstOpen = false;
|
|
542
1295
|
if (consent !== "off") {
|
|
543
|
-
const [id, stored, cached, installed, ref] = await Promise.all([
|
|
1296
|
+
const [id, stored, cached, installed, ref, surveys] = await Promise.all([
|
|
544
1297
|
get(KEYS.id),
|
|
545
1298
|
get(KEYS.queue),
|
|
546
1299
|
get(KEYS.config),
|
|
547
1300
|
get(KEYS.installed),
|
|
548
|
-
get(KEYS.referral)
|
|
1301
|
+
get(KEYS.referral),
|
|
1302
|
+
get(KEYS.surveys)
|
|
549
1303
|
]);
|
|
550
1304
|
if (consent === "persistent") {
|
|
551
1305
|
installId = id || newDistinctId();
|
|
@@ -568,6 +1322,14 @@ var createClient = (options, deps) => {
|
|
|
568
1322
|
if (r && typeof r.code === "string") referral = r;
|
|
569
1323
|
} catch {
|
|
570
1324
|
}
|
|
1325
|
+
try {
|
|
1326
|
+
const done = surveys && consent === "persistent" ? JSON.parse(surveys) : null;
|
|
1327
|
+
if (Array.isArray(done)) {
|
|
1328
|
+
for (const x of done) if (typeof x === "string") surveysDone.add(x);
|
|
1329
|
+
}
|
|
1330
|
+
} catch {
|
|
1331
|
+
await remove(KEYS.surveys);
|
|
1332
|
+
}
|
|
571
1333
|
firstOpen = !!storage && !installed;
|
|
572
1334
|
if (firstOpen) await set(KEYS.installed, String(now()));
|
|
573
1335
|
} else {
|
|
@@ -601,10 +1363,12 @@ var createClient = (options, deps) => {
|
|
|
601
1363
|
for (const run of early.splice(0)) run();
|
|
602
1364
|
if (deps.appState) appStateSub = deps.appState.addEventListener("change", onAppState);
|
|
603
1365
|
installErrorHandler();
|
|
1366
|
+
listenForOpens();
|
|
604
1367
|
if (appState === "active" || !deps.appState) startTimer();
|
|
605
1368
|
if (queue.length) void flush();
|
|
606
1369
|
await fetchConfig();
|
|
607
1370
|
markReady();
|
|
1371
|
+
refreshFlows();
|
|
608
1372
|
};
|
|
609
1373
|
void start().catch(() => {
|
|
610
1374
|
loaded = true;
|
|
@@ -614,6 +1378,25 @@ var createClient = (options, deps) => {
|
|
|
614
1378
|
track: (event, props) => {
|
|
615
1379
|
if (typeof event !== "string" || !event) return;
|
|
616
1380
|
record(event, props);
|
|
1381
|
+
flows.onEvent(event);
|
|
1382
|
+
},
|
|
1383
|
+
tap: (x, y) => {
|
|
1384
|
+
if (options.heatmaps === false || consent === "off") return;
|
|
1385
|
+
const size = deps.screen?.();
|
|
1386
|
+
if (!size || !Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
1387
|
+
const props = { x: Math.round(x), y: Math.round(y), vw: Math.round(size.w), vh: Math.round(size.h), docH: Math.round(size.h), selector: "", label: "", dead: false };
|
|
1388
|
+
record("$click", props);
|
|
1389
|
+
recorder?.mark({ kind: "click", path: currentScreen ?? "/", x: props.x, y: props.y, vw: props.vw });
|
|
1390
|
+
if (rage.push(x, y, now())) {
|
|
1391
|
+
record("$rageclick", props);
|
|
1392
|
+
recorder?.mark({ kind: "rage", path: currentScreen ?? "/", x: props.x, y: props.y, vw: props.vw });
|
|
1393
|
+
recorder?.trigger("rage");
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1396
|
+
setReplay: (on) => {
|
|
1397
|
+
replayOn = on;
|
|
1398
|
+
if (!on) recorder?.drop();
|
|
1399
|
+
recorder?.sync(appState === "active");
|
|
617
1400
|
},
|
|
618
1401
|
screen: (name, props) => {
|
|
619
1402
|
if (typeof name !== "string" || !name.trim()) return;
|
|
@@ -623,6 +1406,7 @@ var createClient = (options, deps) => {
|
|
|
623
1406
|
currentScreen = path;
|
|
624
1407
|
crumb("navigation", path);
|
|
625
1408
|
record("$screen", { ...props, path, screen: name, ...previous ? { previous } : {} });
|
|
1409
|
+
recorder?.mark({ kind: "pageview", path });
|
|
626
1410
|
},
|
|
627
1411
|
identify: (id, props) => {
|
|
628
1412
|
if (typeof id !== "string" || !id) return;
|
|
@@ -636,12 +1420,14 @@ var createClient = (options, deps) => {
|
|
|
636
1420
|
record("$identify", { ...props, id, ...previous && previous !== id ? { previousId: previous } : {} });
|
|
637
1421
|
if (consent === "persistent") void set(KEYS.id, id);
|
|
638
1422
|
changed();
|
|
1423
|
+
refreshFlows();
|
|
639
1424
|
},
|
|
640
1425
|
reset: () => {
|
|
641
1426
|
if (!loaded) {
|
|
642
1427
|
early.push(() => client.reset());
|
|
643
1428
|
return;
|
|
644
1429
|
}
|
|
1430
|
+
if (pushToken) void unregisterForPush();
|
|
645
1431
|
identified = void 0;
|
|
646
1432
|
person = void 0;
|
|
647
1433
|
session = null;
|
|
@@ -653,6 +1439,7 @@ var createClient = (options, deps) => {
|
|
|
653
1439
|
void set(KEYS.id, installId);
|
|
654
1440
|
}
|
|
655
1441
|
changed();
|
|
1442
|
+
refreshFlows();
|
|
656
1443
|
},
|
|
657
1444
|
setConsent: (next) => {
|
|
658
1445
|
if (next !== "anonymous" && next !== "persistent" && next !== "off") {
|
|
@@ -667,6 +1454,7 @@ var createClient = (options, deps) => {
|
|
|
667
1454
|
const before = consent;
|
|
668
1455
|
consent = next;
|
|
669
1456
|
if (next === "off") {
|
|
1457
|
+
recorder?.drop();
|
|
670
1458
|
queue = [];
|
|
671
1459
|
session = null;
|
|
672
1460
|
errorQueue.length = 0;
|
|
@@ -675,13 +1463,16 @@ var createClient = (options, deps) => {
|
|
|
675
1463
|
installId = null;
|
|
676
1464
|
void remove(KEYS.id);
|
|
677
1465
|
void remove(KEYS.referral);
|
|
1466
|
+
void remove(KEYS.surveys);
|
|
678
1467
|
} else {
|
|
679
1468
|
installId = identified ?? newDistinctId();
|
|
680
1469
|
void set(KEYS.id, installId);
|
|
681
1470
|
if (referral) void set(KEYS.referral, JSON.stringify(referral));
|
|
1471
|
+
if (surveysDone.size) void set(KEYS.surveys, JSON.stringify([...surveysDone]));
|
|
682
1472
|
}
|
|
683
1473
|
if (before === "off" && currentScreen && loaded) record("$screen", { path: currentScreen, screen: currentScreen });
|
|
684
1474
|
changed();
|
|
1475
|
+
refreshFlows();
|
|
685
1476
|
},
|
|
686
1477
|
captureError: (error) => capture(error, "manual"),
|
|
687
1478
|
getFlag: (key) => flagFor(key, config, distinctId(), { ...person }) !== false,
|
|
@@ -702,11 +1493,40 @@ var createClient = (options, deps) => {
|
|
|
702
1493
|
exposed.add(mark);
|
|
703
1494
|
record("$exposure", { experiment: key, variant });
|
|
704
1495
|
},
|
|
1496
|
+
getText: (id, fallback) => {
|
|
1497
|
+
client.exposeText(id);
|
|
1498
|
+
return client.peekText(id) ?? fallback;
|
|
1499
|
+
},
|
|
1500
|
+
peekText: (id) => texts()[id],
|
|
1501
|
+
exposeText: (id) => {
|
|
1502
|
+
for (const key of experimentsOfText(config, id)) client.expose(key, resolveVariant(key, config, distinctId(), {}));
|
|
1503
|
+
},
|
|
1504
|
+
openCancelFlow: (o) => flows.openCancelFlow(o),
|
|
1505
|
+
startOnboarding: (id) => flows.startOnboarding(id),
|
|
1506
|
+
registerForPush,
|
|
1507
|
+
unregisterForPush,
|
|
1508
|
+
flows,
|
|
705
1509
|
flush,
|
|
706
1510
|
ready,
|
|
707
1511
|
isReady: () => isReady,
|
|
708
1512
|
getDistinctId: distinctId,
|
|
709
1513
|
getConsent: () => consent,
|
|
1514
|
+
getUser: () => {
|
|
1515
|
+
if (!identified) return void 0;
|
|
1516
|
+
const p = person;
|
|
1517
|
+
return { id: identified, ...typeof p?.name === "string" ? { name: p.name } : {}, ...typeof p?.email === "string" ? { email: p.email } : {} };
|
|
1518
|
+
},
|
|
1519
|
+
api: (as) => {
|
|
1520
|
+
if (!apiKey || consent === "off") return null;
|
|
1521
|
+
const known = client.getUser();
|
|
1522
|
+
const user = as?.name || as?.email ? { ...known, ...as.name ? { name: as.name } : {}, ...as.email ? { email: as.email } : {} } : known;
|
|
1523
|
+
return createApi({ host, apiKey, distinctId: distinctId(), user });
|
|
1524
|
+
},
|
|
1525
|
+
isSurveyDone: (id) => surveysDone.has(id),
|
|
1526
|
+
markSurveyDone: (id) => {
|
|
1527
|
+
surveysDone.add(id);
|
|
1528
|
+
if (consent === "persistent") void set(KEYS.surveys, JSON.stringify([...surveysDone].slice(-100)));
|
|
1529
|
+
},
|
|
710
1530
|
getConfig: () => config,
|
|
711
1531
|
subscribe: (listener) => {
|
|
712
1532
|
listeners.add(listener);
|
|
@@ -716,7 +1536,10 @@ var createClient = (options, deps) => {
|
|
|
716
1536
|
close: async () => {
|
|
717
1537
|
if (closed) return;
|
|
718
1538
|
stopTimer();
|
|
1539
|
+
recorder?.sync(false);
|
|
719
1540
|
appStateSub?.remove();
|
|
1541
|
+
pushOff?.();
|
|
1542
|
+
pushOff = null;
|
|
720
1543
|
if (handlerInstalled && deps.errorUtils && previousHandler) deps.errorUtils.setGlobalHandler(previousHandler);
|
|
721
1544
|
await flush();
|
|
722
1545
|
await sendErrors();
|
|
@@ -739,8 +1562,252 @@ var activeRouteName = (state) => {
|
|
|
739
1562
|
return name;
|
|
740
1563
|
};
|
|
741
1564
|
|
|
742
|
-
// src/native/
|
|
1565
|
+
// src/native/flows-view.ts
|
|
1566
|
+
var import_react = require("react");
|
|
743
1567
|
var RN = require("react-native");
|
|
1568
|
+
var LIGHT = { bg: "#ffffff", fg: "#1c1d20", muted: "#5d6068", soft: "#f4f5f7", line: "#e4e5e8" };
|
|
1569
|
+
var DARK = { bg: "#17181b", fg: "#f1f2f4", muted: "#a9acb3", soft: "#232428", line: "#2e2f35" };
|
|
1570
|
+
var months = (n) => `${n} ${n === 1 ? "month" : "months"}`;
|
|
1571
|
+
var MRRocketFlows = ({ flows, appearance = {} }) => {
|
|
1572
|
+
const scheme = RN.useColorScheme?.();
|
|
1573
|
+
const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
|
|
1574
|
+
const p = { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
|
|
1575
|
+
const [, setTick] = (0, import_react.useState)(0);
|
|
1576
|
+
(0, import_react.useEffect)(() => flows.subscribe(() => setTick((t) => t + 1)), [flows]);
|
|
1577
|
+
const v = flows.view();
|
|
1578
|
+
const [comment, setComment] = (0, import_react.useState)("");
|
|
1579
|
+
const screen = v?.type === "cancel" ? v.screen : null;
|
|
1580
|
+
(0, import_react.useEffect)(() => {
|
|
1581
|
+
if (!screen) setComment("");
|
|
1582
|
+
}, [screen]);
|
|
1583
|
+
if (!v) return null;
|
|
1584
|
+
const text2 = (children, style = {}) => (0, import_react.createElement)(RN.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
|
|
1585
|
+
const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
|
|
1586
|
+
const title = (children) => (0, import_react.createElement)(RN.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 18, fontWeight: "700" } }, children);
|
|
1587
|
+
const btn = (label, kind, onPress, disabled = false) => (0, import_react.createElement)(
|
|
1588
|
+
RN.Pressable,
|
|
1589
|
+
{
|
|
1590
|
+
key: label,
|
|
1591
|
+
onPress,
|
|
1592
|
+
disabled,
|
|
1593
|
+
accessibilityRole: "button",
|
|
1594
|
+
accessibilityState: { disabled },
|
|
1595
|
+
style: {
|
|
1596
|
+
height: kind === "quiet" ? 36 : 44,
|
|
1597
|
+
borderRadius: 10,
|
|
1598
|
+
alignItems: "center",
|
|
1599
|
+
justifyContent: "center",
|
|
1600
|
+
opacity: disabled ? 0.55 : 1,
|
|
1601
|
+
backgroundColor: kind === "primary" ? p.accent : kind === "danger" ? p.danger : kind === "secondary" ? p.bg : "transparent",
|
|
1602
|
+
borderWidth: kind === "secondary" ? 1 : 0,
|
|
1603
|
+
borderColor: p.line
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1606
|
+
text2(label, {
|
|
1607
|
+
color: kind === "primary" || kind === "danger" ? "#ffffff" : kind === "quiet" ? p.muted : p.fg,
|
|
1608
|
+
fontWeight: kind === "quiet" ? "500" : "600"
|
|
1609
|
+
})
|
|
1610
|
+
);
|
|
1611
|
+
const close = (label, onPress) => (0, import_react.createElement)(
|
|
1612
|
+
RN.Pressable,
|
|
1613
|
+
{ onPress, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 12, style: { marginLeft: "auto", padding: 4 } },
|
|
1614
|
+
text2("\xD7", { fontSize: 22, color: p.muted })
|
|
1615
|
+
);
|
|
1616
|
+
const card = (children, style = {}) => (0, import_react.createElement)(
|
|
1617
|
+
RN.View,
|
|
1618
|
+
{
|
|
1619
|
+
style: {
|
|
1620
|
+
backgroundColor: p.bg,
|
|
1621
|
+
borderRadius: 16,
|
|
1622
|
+
padding: 20,
|
|
1623
|
+
gap: 12,
|
|
1624
|
+
shadowColor: "#000",
|
|
1625
|
+
shadowOpacity: 0.18,
|
|
1626
|
+
shadowRadius: 20,
|
|
1627
|
+
elevation: 8,
|
|
1628
|
+
...style
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
1631
|
+
...children
|
|
1632
|
+
);
|
|
1633
|
+
const sheet = (children, onClose) => (0, import_react.createElement)(
|
|
1634
|
+
RN.Modal,
|
|
1635
|
+
{ visible: true, transparent: true, animationType: "fade", onRequestClose: onClose },
|
|
1636
|
+
(0, import_react.createElement)(
|
|
1637
|
+
RN.View,
|
|
1638
|
+
{ style: { flex: 1, backgroundColor: "rgba(0,0,0,0.4)", justifyContent: "flex-end" } },
|
|
1639
|
+
card(children, { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, paddingBottom: 34 })
|
|
1640
|
+
)
|
|
1641
|
+
);
|
|
1642
|
+
const input = (placeholder) => (0, import_react.createElement)(RN.TextInput, {
|
|
1643
|
+
value: comment,
|
|
1644
|
+
onChangeText: setComment,
|
|
1645
|
+
placeholder,
|
|
1646
|
+
placeholderTextColor: p.muted,
|
|
1647
|
+
multiline: true,
|
|
1648
|
+
maxLength: 500,
|
|
1649
|
+
accessibilityLabel: placeholder,
|
|
1650
|
+
style: { minHeight: 72, borderWidth: 1, borderColor: p.line, borderRadius: 10, padding: 10, color: p.fg, textAlignVertical: "top" }
|
|
1651
|
+
});
|
|
1652
|
+
if (v.type === "cancel") {
|
|
1653
|
+
const top = (0, import_react.createElement)(
|
|
1654
|
+
RN.View,
|
|
1655
|
+
{ style: { flexDirection: "row", alignItems: "center", gap: 4 } },
|
|
1656
|
+
...v.steps > 1 ? Array.from(
|
|
1657
|
+
{ length: v.steps },
|
|
1658
|
+
(_, i) => (0, import_react.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i < v.step ? p.accent : p.line } })
|
|
1659
|
+
) : [],
|
|
1660
|
+
close("Close", flows.cancel.close)
|
|
1661
|
+
);
|
|
1662
|
+
const error = v.error ? text2(v.error, { color: p.danger, fontSize: 13 }) : null;
|
|
1663
|
+
let body = [];
|
|
1664
|
+
if (v.screen === "survey")
|
|
1665
|
+
body = [
|
|
1666
|
+
title(v.flow.question || "Why are you leaving?"),
|
|
1667
|
+
(0, import_react.createElement)(
|
|
1668
|
+
RN.ScrollView,
|
|
1669
|
+
{ style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
|
|
1670
|
+
...v.flow.reasons.map(
|
|
1671
|
+
(r) => (0, import_react.createElement)(
|
|
1672
|
+
RN.Pressable,
|
|
1673
|
+
{
|
|
1674
|
+
key: r.id,
|
|
1675
|
+
onPress: () => flows.cancel.pick(r.id),
|
|
1676
|
+
accessibilityRole: "radio",
|
|
1677
|
+
accessibilityState: { checked: v.reason === r.id },
|
|
1678
|
+
style: {
|
|
1679
|
+
minHeight: 44,
|
|
1680
|
+
paddingHorizontal: 12,
|
|
1681
|
+
borderRadius: 10,
|
|
1682
|
+
borderWidth: 1,
|
|
1683
|
+
borderColor: v.reason === r.id ? p.accent : p.line,
|
|
1684
|
+
backgroundColor: v.reason === r.id ? p.soft : p.bg,
|
|
1685
|
+
justifyContent: "center"
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
text2(r.label, { fontWeight: "500" })
|
|
1689
|
+
)
|
|
1690
|
+
)
|
|
1691
|
+
),
|
|
1692
|
+
btn("Continue", "primary", flows.cancel.next, !v.reason),
|
|
1693
|
+
btn("Never mind, keep my plan", "quiet", flows.cancel.close)
|
|
1694
|
+
];
|
|
1695
|
+
else if (v.screen === "offer" && v.offer) {
|
|
1696
|
+
const o = v.offer;
|
|
1697
|
+
const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
|
|
1698
|
+
const detail = o.kind === "discount" && o.percent ? v.price ? `${money(discounted(v.price.amount, o.percent), v.price.currency)} instead of ${money(v.price.amount, v.price.currency)}${o.months ? ` for ${months(o.months)}` : ""}` : `${o.percent}% off${o.months ? ` for ${months(o.months)}` : ""}` : o.kind === "pause" && o.months ? `Pause for up to ${months(o.months)}. Your data stays.` : o.kind === "downgrade" && o.planName ? o.planName : null;
|
|
1699
|
+
body = [
|
|
1700
|
+
label ? muted(`You said: ${label}`) : null,
|
|
1701
|
+
title(o.headline),
|
|
1702
|
+
detail ? (0, import_react.createElement)(RN.View, { style: { backgroundColor: p.soft, borderRadius: 10, padding: 12 } }, text2(detail, { fontWeight: "600" })) : null,
|
|
1703
|
+
o.kind === "contact" ? input("What would make you stay?") : null,
|
|
1704
|
+
btn(v.busy ? "One moment\u2026" : offerButton(o), "primary", () => flows.cancel.accept(comment), v.busy),
|
|
1705
|
+
btn("No thanks, continue cancelling", "quiet", flows.cancel.decline, v.busy)
|
|
1706
|
+
];
|
|
1707
|
+
} else
|
|
1708
|
+
body = [
|
|
1709
|
+
title(v.flow.confirm.title),
|
|
1710
|
+
v.flow.confirm.body ? muted(v.flow.confirm.body, { fontSize: 15 }) : null,
|
|
1711
|
+
v.steps > 1 ? input("Anything else we should know? (optional)") : null,
|
|
1712
|
+
btn(v.busy ? "One moment\u2026" : v.flow.confirm.button, "danger", () => flows.cancel.confirm(comment), v.busy),
|
|
1713
|
+
btn("Keep my plan", "secondary", flows.cancel.close, v.busy)
|
|
1714
|
+
];
|
|
1715
|
+
return sheet([top, ...body, error], flows.cancel.close);
|
|
1716
|
+
}
|
|
1717
|
+
if (v.type === "step")
|
|
1718
|
+
return sheet(
|
|
1719
|
+
[
|
|
1720
|
+
(0, import_react.createElement)(
|
|
1721
|
+
RN.View,
|
|
1722
|
+
{ style: { flexDirection: "row", alignItems: "center" } },
|
|
1723
|
+
v.total > 1 ? muted(`Step ${v.index + 1} of ${v.total}`) : null,
|
|
1724
|
+
close("Close the tour", flows.onboarding.dismiss)
|
|
1725
|
+
),
|
|
1726
|
+
title(v.step.title),
|
|
1727
|
+
v.step.body ? muted(v.step.body, { fontSize: 15 }) : null,
|
|
1728
|
+
btn(v.step.button || (v.last ? "Done" : "Next"), "primary", flows.onboarding.next)
|
|
1729
|
+
],
|
|
1730
|
+
flows.onboarding.dismiss
|
|
1731
|
+
);
|
|
1732
|
+
if (v.type === "checklist") {
|
|
1733
|
+
const done = v.items.filter((i) => i.done).length;
|
|
1734
|
+
return (0, import_react.createElement)(
|
|
1735
|
+
RN.View,
|
|
1736
|
+
{ pointerEvents: "box-none", style: { position: "absolute", left: 12, right: 12, bottom: 28 } },
|
|
1737
|
+
card([
|
|
1738
|
+
(0, import_react.createElement)(
|
|
1739
|
+
RN.View,
|
|
1740
|
+
{ key: "t", style: { flexDirection: "row", alignItems: "center" } },
|
|
1741
|
+
title(v.flow.name),
|
|
1742
|
+
close("Hide the checklist", flows.onboarding.dismiss)
|
|
1743
|
+
),
|
|
1744
|
+
muted(`${done} of ${v.items.length} done`),
|
|
1745
|
+
...v.items.map(
|
|
1746
|
+
({ step, done: d }) => (0, import_react.createElement)(
|
|
1747
|
+
RN.Pressable,
|
|
1748
|
+
{
|
|
1749
|
+
key: step.id,
|
|
1750
|
+
disabled: d || !!step.event,
|
|
1751
|
+
onPress: () => flows.onboarding.check(step.id),
|
|
1752
|
+
accessibilityRole: "checkbox",
|
|
1753
|
+
accessibilityState: { checked: d },
|
|
1754
|
+
style: { flexDirection: "row", gap: 10, alignItems: "flex-start", paddingVertical: 6 }
|
|
1755
|
+
},
|
|
1756
|
+
(0, import_react.createElement)(
|
|
1757
|
+
RN.View,
|
|
1758
|
+
{
|
|
1759
|
+
style: {
|
|
1760
|
+
width: 20,
|
|
1761
|
+
height: 20,
|
|
1762
|
+
borderRadius: 10,
|
|
1763
|
+
borderWidth: 1.5,
|
|
1764
|
+
borderColor: d ? p.accent : p.line,
|
|
1765
|
+
backgroundColor: d ? p.accent : "transparent",
|
|
1766
|
+
alignItems: "center",
|
|
1767
|
+
justifyContent: "center"
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
d ? text2("\u2713", { color: "#fff", fontSize: 12 }) : null
|
|
1771
|
+
),
|
|
1772
|
+
(0, import_react.createElement)(
|
|
1773
|
+
RN.View,
|
|
1774
|
+
{ style: { flex: 1 } },
|
|
1775
|
+
text2(step.title, { fontWeight: "600", color: d ? p.muted : p.fg, textDecorationLine: d ? "line-through" : "none" }),
|
|
1776
|
+
step.body ? muted(step.body) : null
|
|
1777
|
+
)
|
|
1778
|
+
)
|
|
1779
|
+
)
|
|
1780
|
+
])
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
const m = v.message;
|
|
1784
|
+
if (m.style === "banner")
|
|
1785
|
+
return (0, import_react.createElement)(
|
|
1786
|
+
RN.View,
|
|
1787
|
+
{ pointerEvents: "box-none", style: { position: "absolute", left: 12, right: 12, top: 56 } },
|
|
1788
|
+
card([
|
|
1789
|
+
(0, import_react.createElement)(
|
|
1790
|
+
RN.View,
|
|
1791
|
+
{ key: "t", style: { flexDirection: "row", alignItems: "center", gap: 8 } },
|
|
1792
|
+
(0, import_react.createElement)(RN.View, { style: { flex: 1 } }, text2(m.title, { fontWeight: "700" }), m.body ? muted(m.body) : null),
|
|
1793
|
+
close("Dismiss", flows.message.dismiss)
|
|
1794
|
+
),
|
|
1795
|
+
m.button ? btn(m.button, "primary", flows.message.click) : null
|
|
1796
|
+
])
|
|
1797
|
+
);
|
|
1798
|
+
return sheet(
|
|
1799
|
+
[
|
|
1800
|
+
(0, import_react.createElement)(RN.View, { key: "t", style: { flexDirection: "row" } }, close("Dismiss", flows.message.dismiss)),
|
|
1801
|
+
title(m.title),
|
|
1802
|
+
m.body ? muted(m.body, { fontSize: 15 }) : null,
|
|
1803
|
+
m.button ? btn(m.button, "primary", flows.message.click) : null
|
|
1804
|
+
],
|
|
1805
|
+
flows.message.dismiss
|
|
1806
|
+
);
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
// src/native/platform.ts
|
|
1810
|
+
var RN2 = require("react-native");
|
|
744
1811
|
var asyncStorageModule = null;
|
|
745
1812
|
try {
|
|
746
1813
|
asyncStorageModule = require("@react-native-async-storage/async-storage");
|
|
@@ -765,6 +1832,51 @@ try {
|
|
|
765
1832
|
} catch {
|
|
766
1833
|
expoLocalization = null;
|
|
767
1834
|
}
|
|
1835
|
+
var viewShot = null;
|
|
1836
|
+
try {
|
|
1837
|
+
viewShot = require("react-native-view-shot");
|
|
1838
|
+
} catch {
|
|
1839
|
+
viewShot = null;
|
|
1840
|
+
}
|
|
1841
|
+
var expoNotifications = null;
|
|
1842
|
+
try {
|
|
1843
|
+
expoNotifications = require("expo-notifications");
|
|
1844
|
+
} catch {
|
|
1845
|
+
expoNotifications = null;
|
|
1846
|
+
}
|
|
1847
|
+
var expoConstants = {};
|
|
1848
|
+
try {
|
|
1849
|
+
expoConstants = require("expo-constants");
|
|
1850
|
+
} catch {
|
|
1851
|
+
expoConstants = {};
|
|
1852
|
+
}
|
|
1853
|
+
var dataOf = (r) => r?.notification?.request?.content?.data ?? null;
|
|
1854
|
+
var expoPush = () => {
|
|
1855
|
+
const n = expoNotifications;
|
|
1856
|
+
if (!n || typeof n.getExpoPushTokenAsync !== "function") return null;
|
|
1857
|
+
const constants = expoConstants.default ?? expoConstants;
|
|
1858
|
+
return {
|
|
1859
|
+
permission: async (ask) => {
|
|
1860
|
+
const now = await n.getPermissionsAsync();
|
|
1861
|
+
if (now.granted || now.status === "granted") return "granted";
|
|
1862
|
+
if (now.status === "denied" || !ask) return now.status === "denied" ? "denied" : "undetermined";
|
|
1863
|
+
const asked = await n.requestPermissionsAsync();
|
|
1864
|
+
return asked.granted || asked.status === "granted" ? "granted" : asked.status === "denied" ? "denied" : "undetermined";
|
|
1865
|
+
},
|
|
1866
|
+
token: async (projectId) => {
|
|
1867
|
+
if (RN2.Platform.OS === "android" && n.setNotificationChannelAsync)
|
|
1868
|
+
await n.setNotificationChannelAsync("default", { name: "Default", importance: n.AndroidImportance?.HIGH ?? 4 });
|
|
1869
|
+
const id = projectId ?? constants.expoConfig?.extra?.eas?.projectId ?? constants.easConfig?.projectId;
|
|
1870
|
+
const t = await n.getExpoPushTokenAsync(id ? { projectId: id } : void 0);
|
|
1871
|
+
return typeof t?.data === "string" && t.data ? t.data : null;
|
|
1872
|
+
},
|
|
1873
|
+
onOpen: (listener) => {
|
|
1874
|
+
const sub = n.addNotificationResponseReceivedListener((r) => listener(dataOf(r) ?? {}));
|
|
1875
|
+
return () => sub.remove();
|
|
1876
|
+
},
|
|
1877
|
+
lastOpen: async () => dataOf(await n.getLastNotificationResponseAsync?.())
|
|
1878
|
+
};
|
|
1879
|
+
};
|
|
768
1880
|
var asyncStorage = () => {
|
|
769
1881
|
const m = asyncStorageModule;
|
|
770
1882
|
const s = m?.default ?? m;
|
|
@@ -773,7 +1885,7 @@ var asyncStorage = () => {
|
|
|
773
1885
|
var DEVICE_TYPES = { 1: "phone", 2: "tablet", 3: "desktop", 4: "tv" };
|
|
774
1886
|
var text = (v, max = 80) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : typeof v === "number" ? String(v) : void 0;
|
|
775
1887
|
var readContext = () => {
|
|
776
|
-
const p =
|
|
1888
|
+
const p = RN2.Platform;
|
|
777
1889
|
let locale;
|
|
778
1890
|
try {
|
|
779
1891
|
locale = text(expoLocalization?.getLocales?.()[0]?.languageTag, 35);
|
|
@@ -799,27 +1911,852 @@ var readContext = () => {
|
|
|
799
1911
|
for (const k of Object.keys(ctx)) if (ctx[k] === void 0) delete ctx[k];
|
|
800
1912
|
return ctx;
|
|
801
1913
|
};
|
|
1914
|
+
var recorderDeps = () => {
|
|
1915
|
+
const capture = viewShot?.captureScreen;
|
|
1916
|
+
if (!capture) return null;
|
|
1917
|
+
let keyboard = false;
|
|
1918
|
+
try {
|
|
1919
|
+
RN2.Keyboard.addListener("keyboardWillShow", () => keyboard = true);
|
|
1920
|
+
RN2.Keyboard.addListener("keyboardDidShow", () => keyboard = true);
|
|
1921
|
+
RN2.Keyboard.addListener("keyboardDidHide", () => keyboard = false);
|
|
1922
|
+
} catch {
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
return {
|
|
1926
|
+
capture: async (width) => {
|
|
1927
|
+
const s = RN2.Dimensions.get("screen");
|
|
1928
|
+
if (!s?.width) return null;
|
|
1929
|
+
const w = Math.round(width);
|
|
1930
|
+
const h3 = Math.round(s.height / s.width * width);
|
|
1931
|
+
const img = await capture({ format: "jpg", quality: 0.5, result: "base64", width: w, height: h3 });
|
|
1932
|
+
return img ? { img: img.replace(/^data:image\/\w+;base64,/, ""), w, h: h3 } : null;
|
|
1933
|
+
},
|
|
1934
|
+
keyboardOpen: () => keyboard,
|
|
1935
|
+
masks: measureMasks
|
|
1936
|
+
};
|
|
1937
|
+
};
|
|
802
1938
|
var nativeDeps = (overrides = {}) => ({
|
|
803
1939
|
storage: overrides.storage !== void 0 ? overrides.storage : asyncStorage(),
|
|
804
1940
|
fetch: ((url, init) => fetch(url, init)),
|
|
805
|
-
appState:
|
|
1941
|
+
appState: RN2.AppState,
|
|
806
1942
|
errorUtils: globalThis.ErrorUtils ?? null,
|
|
807
1943
|
context: readContext,
|
|
808
1944
|
appId: text(expoApplication?.applicationId, 150),
|
|
809
1945
|
screen: () => {
|
|
810
|
-
const s =
|
|
1946
|
+
const s = RN2.Dimensions.get("screen");
|
|
811
1947
|
return s ? { w: s.width, h: s.height } : void 0;
|
|
812
1948
|
},
|
|
813
|
-
initialUrl: () =>
|
|
1949
|
+
initialUrl: () => RN2.Linking.getInitialURL(),
|
|
1950
|
+
openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
|
|
1951
|
+
push: expoPush(),
|
|
1952
|
+
recorder: recorderDeps()
|
|
814
1953
|
});
|
|
815
1954
|
var optionalModules = () => ({
|
|
816
1955
|
asyncStorage: !!asyncStorage(),
|
|
817
1956
|
expoDevice: !!expoDevice,
|
|
818
1957
|
expoApplication: !!expoApplication,
|
|
819
|
-
expoLocalization: !!expoLocalization
|
|
1958
|
+
expoLocalization: !!expoLocalization,
|
|
1959
|
+
expoNotifications: !!expoNotifications,
|
|
1960
|
+
viewShot: !!viewShot?.captureScreen
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
// src/native/widgets-view.ts
|
|
1964
|
+
var import_react2 = require("react");
|
|
1965
|
+
var RN3 = require("react-native");
|
|
1966
|
+
var usePalette = (appearance = {}) => {
|
|
1967
|
+
const scheme = RN3.useColorScheme?.();
|
|
1968
|
+
const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
|
|
1969
|
+
return { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
|
|
1970
|
+
};
|
|
1971
|
+
var kit = (p) => {
|
|
1972
|
+
const text2 = (children, style = {}) => (0, import_react2.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
|
|
1973
|
+
const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
|
|
1974
|
+
const title = (children) => (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700", flex: 1 } }, children);
|
|
1975
|
+
const btn = (label, kind, onPress, disabled = false, style = {}) => (0, import_react2.createElement)(
|
|
1976
|
+
RN3.Pressable,
|
|
1977
|
+
{
|
|
1978
|
+
key: label,
|
|
1979
|
+
onPress,
|
|
1980
|
+
disabled,
|
|
1981
|
+
accessibilityRole: "button",
|
|
1982
|
+
accessibilityState: { disabled },
|
|
1983
|
+
style: {
|
|
1984
|
+
height: 44,
|
|
1985
|
+
paddingHorizontal: 16,
|
|
1986
|
+
borderRadius: 10,
|
|
1987
|
+
alignItems: "center",
|
|
1988
|
+
justifyContent: "center",
|
|
1989
|
+
opacity: disabled ? 0.55 : 1,
|
|
1990
|
+
backgroundColor: kind === "primary" ? p.accent : p.bg,
|
|
1991
|
+
borderWidth: kind === "secondary" ? 1 : 0,
|
|
1992
|
+
borderColor: p.line,
|
|
1993
|
+
...style
|
|
1994
|
+
}
|
|
1995
|
+
},
|
|
1996
|
+
text2(label, { color: kind === "primary" ? "#ffffff" : p.fg, fontWeight: "600" })
|
|
1997
|
+
);
|
|
1998
|
+
const closeX = (label, onPress) => (0, import_react2.createElement)(RN3.Pressable, { onPress, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 12, style: { padding: 4 } }, text2("\xD7", { fontSize: 24, color: p.muted }));
|
|
1999
|
+
const field = (o) => (0, import_react2.createElement)(RN3.TextInput, {
|
|
2000
|
+
value: o.value,
|
|
2001
|
+
onChangeText: o.onChange,
|
|
2002
|
+
placeholder: o.placeholder,
|
|
2003
|
+
placeholderTextColor: p.muted,
|
|
2004
|
+
multiline: !!o.multiline,
|
|
2005
|
+
maxLength: o.max ?? 2e3,
|
|
2006
|
+
accessibilityLabel: o.placeholder,
|
|
2007
|
+
...o.email ? { keyboardType: "email-address", autoCapitalize: "none", autoComplete: "email", textContentType: "emailAddress" } : {},
|
|
2008
|
+
...o.onSubmit ? { onSubmitEditing: o.onSubmit, returnKeyType: "send" } : {},
|
|
2009
|
+
style: {
|
|
2010
|
+
minHeight: o.multiline ? 72 : 44,
|
|
2011
|
+
borderWidth: 1,
|
|
2012
|
+
borderColor: p.line,
|
|
2013
|
+
borderRadius: 10,
|
|
2014
|
+
paddingHorizontal: 12,
|
|
2015
|
+
paddingVertical: 10,
|
|
2016
|
+
color: p.fg,
|
|
2017
|
+
backgroundColor: p.bg,
|
|
2018
|
+
textAlignVertical: o.multiline ? "top" : "center",
|
|
2019
|
+
...o.style
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
const chip = (label, on, onPress, key = label) => (0, import_react2.createElement)(
|
|
2023
|
+
RN3.Pressable,
|
|
2024
|
+
{
|
|
2025
|
+
key,
|
|
2026
|
+
onPress,
|
|
2027
|
+
accessibilityRole: "button",
|
|
2028
|
+
accessibilityState: { selected: on },
|
|
2029
|
+
style: { paddingHorizontal: 12, height: 32, borderRadius: 16, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
|
|
2030
|
+
},
|
|
2031
|
+
text2(label, { fontSize: 13, fontWeight: "600", color: on ? p.accent : p.fg })
|
|
2032
|
+
);
|
|
2033
|
+
const page = (heading, onClose, children) => (0, import_react2.createElement)(
|
|
2034
|
+
RN3.Modal,
|
|
2035
|
+
{ visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: onClose },
|
|
2036
|
+
(0, import_react2.createElement)(
|
|
2037
|
+
RN3.KeyboardAvoidingView,
|
|
2038
|
+
{ behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
|
|
2039
|
+
(0, import_react2.createElement)(
|
|
2040
|
+
RN3.View,
|
|
2041
|
+
{ style: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: p.line } },
|
|
2042
|
+
title(heading),
|
|
2043
|
+
closeX("Close", onClose)
|
|
2044
|
+
),
|
|
2045
|
+
...children
|
|
2046
|
+
)
|
|
2047
|
+
);
|
|
2048
|
+
return { text: text2, muted, title, btn, closeX, field, chip, page };
|
|
2049
|
+
};
|
|
2050
|
+
var timeAgo = (iso) => {
|
|
2051
|
+
const t = new Date(iso).getTime();
|
|
2052
|
+
if (Number.isNaN(t)) return "";
|
|
2053
|
+
const d = Math.max(0, Date.now() - t) / 1e3;
|
|
2054
|
+
if (d < 60) return "just now";
|
|
2055
|
+
if (d < 3600) return `${Math.floor(d / 60)}m ago`;
|
|
2056
|
+
if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
|
|
2057
|
+
return `${Math.floor(d / 86400)}d ago`;
|
|
2058
|
+
};
|
|
2059
|
+
var opener = () => {
|
|
2060
|
+
let open = false;
|
|
2061
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2062
|
+
const set = (v) => {
|
|
2063
|
+
open = v;
|
|
2064
|
+
listeners.forEach((l) => l(v));
|
|
2065
|
+
};
|
|
2066
|
+
const useOpen = () => {
|
|
2067
|
+
const [v, setV] = (0, import_react2.useState)(open);
|
|
2068
|
+
(0, import_react2.useEffect)(() => {
|
|
2069
|
+
listeners.add(setV);
|
|
2070
|
+
setV(open);
|
|
2071
|
+
return () => {
|
|
2072
|
+
listeners.delete(setV);
|
|
2073
|
+
};
|
|
2074
|
+
}, []);
|
|
2075
|
+
return v;
|
|
2076
|
+
};
|
|
2077
|
+
return { set, get: () => open, useOpen };
|
|
2078
|
+
};
|
|
2079
|
+
var isAnswered = (a) => Array.isArray(a) ? a.length > 0 : typeof a === "number" ? true : !!a?.trim();
|
|
2080
|
+
var SurveySheet = ({ survey, api, p, onDone }) => {
|
|
2081
|
+
const k = kit(p);
|
|
2082
|
+
const [step, setStep] = (0, import_react2.useState)(0);
|
|
2083
|
+
const [answers, setAnswers] = (0, import_react2.useState)({});
|
|
2084
|
+
const [done, setDone] = (0, import_react2.useState)(false);
|
|
2085
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
2086
|
+
const started = (0, import_react2.useRef)(Date.now());
|
|
2087
|
+
const finished = (0, import_react2.useRef)(false);
|
|
2088
|
+
const q = survey.questions[step];
|
|
2089
|
+
const last = step === survey.questions.length - 1;
|
|
2090
|
+
const value = answers[q.key];
|
|
2091
|
+
(0, import_react2.useEffect)(() => {
|
|
2092
|
+
api.seen(survey.id).catch(() => void 0);
|
|
2093
|
+
}, [api, survey.id]);
|
|
2094
|
+
(0, import_react2.useEffect)(() => {
|
|
2095
|
+
if (!done) return;
|
|
2096
|
+
const t = setTimeout(onDone, 2500);
|
|
2097
|
+
return () => clearTimeout(t);
|
|
2098
|
+
}, [done, onDone]);
|
|
2099
|
+
const finish = (completed, all = answers) => {
|
|
2100
|
+
if (finished.current) return;
|
|
2101
|
+
finished.current = true;
|
|
2102
|
+
api.respond(survey.id, { answers: all, completed, seconds: Math.round((Date.now() - started.current) / 1e3) }).catch(() => void 0);
|
|
2103
|
+
};
|
|
2104
|
+
const dismiss = () => {
|
|
2105
|
+
if (Object.values(answers).some(isAnswered)) finish(false);
|
|
2106
|
+
onDone();
|
|
2107
|
+
};
|
|
2108
|
+
const next = () => {
|
|
2109
|
+
if (q.required && !isAnswered(value)) return setError("Please answer this question.");
|
|
2110
|
+
setError("");
|
|
2111
|
+
if (!last) return setStep(step + 1);
|
|
2112
|
+
finish(true);
|
|
2113
|
+
setDone(true);
|
|
2114
|
+
};
|
|
2115
|
+
const pick = (v) => {
|
|
2116
|
+
setAnswers((a) => ({ ...a, [q.key]: v }));
|
|
2117
|
+
setError("");
|
|
2118
|
+
};
|
|
2119
|
+
let input;
|
|
2120
|
+
if (q.kind === "nps" || q.kind === "rating") {
|
|
2121
|
+
const scale = q.kind === "nps" ? 11 : Math.min(Math.max(q.scale ?? 5, 2), 10);
|
|
2122
|
+
const start = q.kind === "nps" ? 0 : 1;
|
|
2123
|
+
input = (0, import_react2.createElement)(
|
|
2124
|
+
RN3.View,
|
|
2125
|
+
{ style: { gap: 6 } },
|
|
2126
|
+
(0, import_react2.createElement)(
|
|
2127
|
+
RN3.View,
|
|
2128
|
+
{ style: { flexDirection: "row", flexWrap: "wrap", gap: 6 }, accessibilityRole: "radiogroup", accessibilityLabel: q.title },
|
|
2129
|
+
...Array.from({ length: scale }, (_, i) => {
|
|
2130
|
+
const n = i + start;
|
|
2131
|
+
const on = value === n;
|
|
2132
|
+
return (0, import_react2.createElement)(
|
|
2133
|
+
RN3.Pressable,
|
|
2134
|
+
{
|
|
2135
|
+
key: n,
|
|
2136
|
+
onPress: () => pick(n),
|
|
2137
|
+
accessibilityRole: "radio",
|
|
2138
|
+
accessibilityState: { checked: on },
|
|
2139
|
+
accessibilityLabel: `${n} of ${scale - 1 + start}`,
|
|
2140
|
+
style: { width: 40, height: 40, borderRadius: 8, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.accent : p.bg }
|
|
2141
|
+
},
|
|
2142
|
+
k.text(String(n), { color: on ? "#ffffff" : p.fg, fontWeight: "600" })
|
|
2143
|
+
);
|
|
2144
|
+
})
|
|
2145
|
+
),
|
|
2146
|
+
q.low || q.high || q.kind === "nps" ? (0, import_react2.createElement)(RN3.View, { style: { flexDirection: "row", justifyContent: "space-between" } }, k.muted(q.low ?? (q.kind === "nps" ? "Not likely" : "")), k.muted(q.high ?? (q.kind === "nps" ? "Very likely" : ""))) : null
|
|
2147
|
+
);
|
|
2148
|
+
} else if (q.kind === "single" || q.kind === "multi") {
|
|
2149
|
+
const multi = q.kind === "multi";
|
|
2150
|
+
const picked = multi ? value ?? [] : [];
|
|
2151
|
+
input = (0, import_react2.createElement)(
|
|
2152
|
+
RN3.View,
|
|
2153
|
+
{ style: { gap: 6 } },
|
|
2154
|
+
...(q.options ?? []).map((o) => {
|
|
2155
|
+
const on = multi ? picked.includes(o) : value === o;
|
|
2156
|
+
return (0, import_react2.createElement)(
|
|
2157
|
+
RN3.Pressable,
|
|
2158
|
+
{
|
|
2159
|
+
key: o,
|
|
2160
|
+
onPress: () => pick(multi ? on ? picked.filter((x) => x !== o) : [...picked, o] : o),
|
|
2161
|
+
accessibilityRole: multi ? "checkbox" : "radio",
|
|
2162
|
+
accessibilityState: { checked: on },
|
|
2163
|
+
style: { minHeight: 44, paddingHorizontal: 12, borderRadius: 10, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
|
|
2164
|
+
},
|
|
2165
|
+
k.text(o, { fontWeight: "500" })
|
|
2166
|
+
);
|
|
2167
|
+
})
|
|
2168
|
+
);
|
|
2169
|
+
} else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? "Type your answer", multiline: true });
|
|
2170
|
+
const body = done ? [k.text(survey.thanks || "Thanks for your feedback.", { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
|
|
2171
|
+
(0, import_react2.createElement)(
|
|
2172
|
+
RN3.View,
|
|
2173
|
+
{ key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
|
|
2174
|
+
...survey.questions.map((x, i) => (0, import_react2.createElement)(RN3.View, { key: x.key, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= step ? p.accent : p.line } })),
|
|
2175
|
+
(0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
|
|
2176
|
+
k.closeX("Close survey", dismiss)
|
|
2177
|
+
),
|
|
2178
|
+
(0, import_react2.createElement)(RN3.Text, { key: "q", accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700" } }, q.title, q.required ? "" : (0, import_react2.createElement)(RN3.Text, { style: { color: p.muted, fontSize: 13, fontWeight: "400" } }, " optional")),
|
|
2179
|
+
q.description ? k.muted(q.description) : null,
|
|
2180
|
+
input,
|
|
2181
|
+
error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
|
|
2182
|
+
k.btn(last ? "Send" : "Next", "primary", next)
|
|
2183
|
+
];
|
|
2184
|
+
return (0, import_react2.createElement)(
|
|
2185
|
+
RN3.Modal,
|
|
2186
|
+
{ visible: true, transparent: true, animationType: "slide", onRequestClose: dismiss },
|
|
2187
|
+
(0, import_react2.createElement)(
|
|
2188
|
+
RN3.KeyboardAvoidingView,
|
|
2189
|
+
{ behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: "rgba(0,0,0,0.35)", justifyContent: "flex-end" } },
|
|
2190
|
+
(0, import_react2.createElement)(
|
|
2191
|
+
RN3.View,
|
|
2192
|
+
{
|
|
2193
|
+
accessibilityViewIsModal: true,
|
|
2194
|
+
accessibilityLabel: survey.name,
|
|
2195
|
+
style: { backgroundColor: p.bg, borderTopLeftRadius: 16, borderTopRightRadius: 16, padding: 20, paddingBottom: 34, gap: 12 }
|
|
2196
|
+
},
|
|
2197
|
+
...body
|
|
2198
|
+
)
|
|
2199
|
+
)
|
|
2200
|
+
);
|
|
2201
|
+
};
|
|
2202
|
+
var MRRocketSurveys = ({ client, appearance }) => {
|
|
2203
|
+
const p = usePalette(appearance);
|
|
2204
|
+
const [survey, setSurvey] = (0, import_react2.useState)(null);
|
|
2205
|
+
const [who, setWho] = (0, import_react2.useState)(() => client.getDistinctId());
|
|
2206
|
+
(0, import_react2.useEffect)(() => client.subscribe(() => setWho(client.getDistinctId())), [client]);
|
|
2207
|
+
(0, import_react2.useEffect)(() => {
|
|
2208
|
+
let stop = false;
|
|
2209
|
+
const ctrl = new AbortController();
|
|
2210
|
+
const timer = setTimeout(() => {
|
|
2211
|
+
void client.ready.then(() => {
|
|
2212
|
+
const api = client.api();
|
|
2213
|
+
if (stop || !api) return;
|
|
2214
|
+
api.survey("", ctrl.signal).then((r) => {
|
|
2215
|
+
const s = r.survey;
|
|
2216
|
+
if (stop || !s || !s.questions.length || client.isSurveyDone(s.id)) return;
|
|
2217
|
+
setSurvey({ s, api });
|
|
2218
|
+
}).catch(() => void 0);
|
|
2219
|
+
});
|
|
2220
|
+
}, 3e3);
|
|
2221
|
+
return () => {
|
|
2222
|
+
stop = true;
|
|
2223
|
+
clearTimeout(timer);
|
|
2224
|
+
ctrl.abort();
|
|
2225
|
+
};
|
|
2226
|
+
}, [client, who]);
|
|
2227
|
+
const done = (0, import_react2.useCallback)(() => {
|
|
2228
|
+
setSurvey((cur) => {
|
|
2229
|
+
if (cur) client.markSurveyDone(cur.s.id);
|
|
2230
|
+
return null;
|
|
2231
|
+
});
|
|
2232
|
+
}, [client]);
|
|
2233
|
+
if (!survey) return null;
|
|
2234
|
+
return (0, import_react2.createElement)(SurveySheet, { key: survey.s.id, survey: survey.s, api: survey.api, p, onDone: done });
|
|
2235
|
+
};
|
|
2236
|
+
var feedbackOpen = opener();
|
|
2237
|
+
var useFeedback = () => ({ open: () => feedbackOpen.set(true), close: () => feedbackOpen.set(false) });
|
|
2238
|
+
var FeedbackSheet = ({ client, p, title }) => {
|
|
2239
|
+
const k = kit(p);
|
|
2240
|
+
const [tab, setTab] = (0, import_react2.useState)("top");
|
|
2241
|
+
const [posts, setPosts] = (0, import_react2.useState)(null);
|
|
2242
|
+
const [boards, setBoards] = (0, import_react2.useState)([]);
|
|
2243
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
2244
|
+
const [board, setBoard] = (0, import_react2.useState)("feature");
|
|
2245
|
+
const [subject, setSubject] = (0, import_react2.useState)("");
|
|
2246
|
+
const [details, setDetails] = (0, import_react2.useState)("");
|
|
2247
|
+
const [name, setName] = (0, import_react2.useState)("");
|
|
2248
|
+
const [email, setEmail] = (0, import_react2.useState)("");
|
|
2249
|
+
const [sending, setSending] = (0, import_react2.useState)(false);
|
|
2250
|
+
const [sent, setSent] = (0, import_react2.useState)(false);
|
|
2251
|
+
const close = () => feedbackOpen.set(false);
|
|
2252
|
+
const user = client.getUser();
|
|
2253
|
+
(0, import_react2.useEffect)(() => {
|
|
2254
|
+
const api = client.api();
|
|
2255
|
+
if (!api) return setError("Feedback is off while tracking is off.");
|
|
2256
|
+
const ctrl = new AbortController();
|
|
2257
|
+
api.list({ sort: "top" }, ctrl.signal).then((l) => {
|
|
2258
|
+
setPosts(l.posts);
|
|
2259
|
+
setBoards(l.boards);
|
|
2260
|
+
if (l.boards.length && !l.boards.some((b) => b.id === "feature")) setBoard(l.boards[0].id);
|
|
2261
|
+
}).catch((e) => e.name !== "AbortError" && setError(e.message));
|
|
2262
|
+
return () => ctrl.abort();
|
|
2263
|
+
}, [client]);
|
|
2264
|
+
const vote = (post) => {
|
|
2265
|
+
const api = client.api();
|
|
2266
|
+
if (!api) return;
|
|
2267
|
+
setPosts((all) => all?.map((x) => x.id === post.id ? { ...x, voted: !x.voted, votes: x.votes + (x.voted ? -1 : 1) } : x) ?? null);
|
|
2268
|
+
api.vote(post.id).then((r) => setPosts((all) => all?.map((x) => x.id === post.id ? { ...x, voted: r.voted, votes: r.votes } : x) ?? null)).catch((e) => {
|
|
2269
|
+
setError(e.message);
|
|
2270
|
+
setPosts((all) => all?.map((x) => x.id === post.id ? post : x) ?? null);
|
|
2271
|
+
});
|
|
2272
|
+
};
|
|
2273
|
+
const submit = () => {
|
|
2274
|
+
const api = client.api({ name: name.trim(), email: email.trim() });
|
|
2275
|
+
if (!api || subject.trim().length < 3 || sending) return;
|
|
2276
|
+
setSending(true);
|
|
2277
|
+
setError("");
|
|
2278
|
+
api.submit({ title: subject.trim(), body: details.trim() || void 0, board }).then((post) => {
|
|
2279
|
+
setPosts((all) => [post, ...all ?? []]);
|
|
2280
|
+
setSubject("");
|
|
2281
|
+
setDetails("");
|
|
2282
|
+
setSent(true);
|
|
2283
|
+
}).catch((e) => setError(e.message)).finally(() => setSending(false));
|
|
2284
|
+
};
|
|
2285
|
+
const tabs = (0, import_react2.createElement)(
|
|
2286
|
+
RN3.View,
|
|
2287
|
+
{ style: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingTop: 12 } },
|
|
2288
|
+
k.chip("Top requests", tab === "top", () => setTab("top")),
|
|
2289
|
+
k.chip("Suggest", tab === "new", () => {
|
|
2290
|
+
setTab("new");
|
|
2291
|
+
setSent(false);
|
|
2292
|
+
})
|
|
2293
|
+
);
|
|
2294
|
+
const list = (0, import_react2.createElement)(
|
|
2295
|
+
RN3.ScrollView,
|
|
2296
|
+
{ style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
|
|
2297
|
+
error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
|
|
2298
|
+
posts === null && !error ? k.muted("Loading\u2026") : null,
|
|
2299
|
+
posts?.length === 0 ? k.muted("No requests yet. Be the first to suggest one.") : null,
|
|
2300
|
+
...(posts ?? []).map(
|
|
2301
|
+
(post) => (0, import_react2.createElement)(
|
|
2302
|
+
RN3.View,
|
|
2303
|
+
{ key: post.id, style: { flexDirection: "row", gap: 12, alignItems: "flex-start", paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: p.line } },
|
|
2304
|
+
(0, import_react2.createElement)(
|
|
2305
|
+
RN3.Pressable,
|
|
2306
|
+
{
|
|
2307
|
+
onPress: () => vote(post),
|
|
2308
|
+
accessibilityRole: "button",
|
|
2309
|
+
accessibilityState: { selected: post.voted },
|
|
2310
|
+
accessibilityLabel: `${post.voted ? "Remove your vote from" : "Upvote"} ${post.title}. ${post.votes} votes`,
|
|
2311
|
+
style: { width: 44, minHeight: 48, borderRadius: 10, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: post.voted ? p.accent : p.line }
|
|
2312
|
+
},
|
|
2313
|
+
k.text("\u25B2", { fontSize: 11, color: post.voted ? p.accent : p.muted }),
|
|
2314
|
+
k.text(String(post.votes), { fontWeight: "700", fontSize: 14, color: post.voted ? p.accent : p.fg })
|
|
2315
|
+
),
|
|
2316
|
+
(0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 2 } }, k.text(post.title, { fontWeight: "600" }), post.body ? k.muted(post.body.slice(0, 160)) : null)
|
|
2317
|
+
)
|
|
2318
|
+
)
|
|
2319
|
+
);
|
|
2320
|
+
const form = (0, import_react2.createElement)(
|
|
2321
|
+
RN3.ScrollView,
|
|
2322
|
+
{ style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 }, keyboardShouldPersistTaps: "handled" },
|
|
2323
|
+
sent ? k.text("Thanks! Your request is on the board.", { fontWeight: "600" }) : null,
|
|
2324
|
+
boards.length > 1 ? (0, import_react2.createElement)(RN3.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 6 } }, ...boards.map((b) => k.chip(b.label, board === b.id, () => setBoard(b.id), b.id))) : null,
|
|
2325
|
+
k.field({ value: subject, onChange: setSubject, placeholder: "Short title", max: 120 }),
|
|
2326
|
+
k.field({ value: details, onChange: setDetails, placeholder: "Details (optional)", multiline: true }),
|
|
2327
|
+
...user?.email ? [] : [
|
|
2328
|
+
k.muted("Your name and email are optional; they let the team follow up."),
|
|
2329
|
+
k.field({ value: name, onChange: setName, placeholder: "Your name (optional)", max: 100 }),
|
|
2330
|
+
k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional)", max: 200, email: true })
|
|
2331
|
+
],
|
|
2332
|
+
error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
|
|
2333
|
+
k.btn(sending ? "Sending\u2026" : "Send request", "primary", submit, subject.trim().length < 3 || sending)
|
|
2334
|
+
);
|
|
2335
|
+
return k.page(title, close, [tabs, tab === "top" ? list : form]);
|
|
2336
|
+
};
|
|
2337
|
+
var MRRocketFeedback = ({ client, appearance, options = {} }) => {
|
|
2338
|
+
const p = usePalette(appearance);
|
|
2339
|
+
const open = feedbackOpen.useOpen();
|
|
2340
|
+
return open ? (0, import_react2.createElement)(FeedbackSheet, { client, p, title: options.title ?? "Feedback" }) : null;
|
|
2341
|
+
};
|
|
2342
|
+
var POLL_OPEN_MS = 4e3;
|
|
2343
|
+
var POLL_WAITING_MS = 8e3;
|
|
2344
|
+
var POLL_IDLE_MS = 3e4;
|
|
2345
|
+
var WAITING_FOR_MS = 10 * 6e4;
|
|
2346
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
2347
|
+
var whoLabel = { visitor: "You", team: "Team", ai: "AI agent", auto: "Team" };
|
|
2348
|
+
var msgView = "home";
|
|
2349
|
+
var messengerOpen = opener();
|
|
2350
|
+
var openAt = (v = "home") => {
|
|
2351
|
+
msgView = v;
|
|
2352
|
+
messengerOpen.set(true);
|
|
2353
|
+
};
|
|
2354
|
+
var useMessenger = () => ({
|
|
2355
|
+
open: (view) => openAt(view),
|
|
2356
|
+
close: () => messengerOpen.set(false),
|
|
2357
|
+
toggle: () => messengerOpen.get() ? messengerOpen.set(false) : openAt()
|
|
820
2358
|
});
|
|
2359
|
+
var haptics = null;
|
|
2360
|
+
try {
|
|
2361
|
+
haptics = require("expo-haptics");
|
|
2362
|
+
} catch {
|
|
2363
|
+
haptics = null;
|
|
2364
|
+
}
|
|
2365
|
+
var buzz = () => {
|
|
2366
|
+
try {
|
|
2367
|
+
void haptics?.notificationAsync?.(haptics.NotificationFeedbackType?.Success)?.catch(() => void 0);
|
|
2368
|
+
} catch {
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
|
|
2372
|
+
var useChat = (client, o, open, view) => {
|
|
2373
|
+
const [thread, setThread] = (0, import_react2.useState)(null);
|
|
2374
|
+
const [loaded, setLoaded] = (0, import_react2.useState)(false);
|
|
2375
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
2376
|
+
const [unseen, setUnseen] = (0, import_react2.useState)([]);
|
|
2377
|
+
const known = (0, import_react2.useRef)(null);
|
|
2378
|
+
const watching = open && view === "messages";
|
|
2379
|
+
const watch = (0, import_react2.useRef)(watching);
|
|
2380
|
+
watch.current = watching;
|
|
2381
|
+
const [active, setActive] = (0, import_react2.useState)(RN3.AppState.currentState === "active");
|
|
2382
|
+
(0, import_react2.useEffect)(() => {
|
|
2383
|
+
const sub = RN3.AppState.addEventListener("change", (st) => setActive(st === "active"));
|
|
2384
|
+
return () => sub.remove();
|
|
2385
|
+
}, []);
|
|
2386
|
+
const take = (0, import_react2.useCallback)(
|
|
2387
|
+
(t) => {
|
|
2388
|
+
const team = teamOf(t);
|
|
2389
|
+
if (known.current !== null) {
|
|
2390
|
+
const i = team.findIndex((m) => m.id === known.current);
|
|
2391
|
+
const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
|
|
2392
|
+
if (fresh.length) {
|
|
2393
|
+
if (o.haptics !== false) buzz();
|
|
2394
|
+
if (!watch.current) setUnseen((u) => [...u, ...fresh]);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
known.current = team.at(-1)?.id ?? "";
|
|
2398
|
+
setThread(t);
|
|
2399
|
+
},
|
|
2400
|
+
[o.haptics]
|
|
2401
|
+
);
|
|
2402
|
+
(0, import_react2.useEffect)(() => {
|
|
2403
|
+
if (watching) setUnseen([]);
|
|
2404
|
+
}, [watching]);
|
|
2405
|
+
(0, import_react2.useEffect)(() => {
|
|
2406
|
+
if (!active) return;
|
|
2407
|
+
let stop = false;
|
|
2408
|
+
let timer;
|
|
2409
|
+
let ctrl = null;
|
|
2410
|
+
const tick = async () => {
|
|
2411
|
+
const api = client.api();
|
|
2412
|
+
if (!api) {
|
|
2413
|
+
setLoaded(true);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
ctrl = new AbortController();
|
|
2417
|
+
let t = null;
|
|
2418
|
+
try {
|
|
2419
|
+
await client.ready;
|
|
2420
|
+
t = await api.chat(ctrl.signal);
|
|
2421
|
+
if (stop) return;
|
|
2422
|
+
take(t);
|
|
2423
|
+
setError("");
|
|
2424
|
+
} catch (e) {
|
|
2425
|
+
if (e.name === "AbortError" || stop) return;
|
|
2426
|
+
if (watch.current) setError(e.message);
|
|
2427
|
+
}
|
|
2428
|
+
setLoaded(true);
|
|
2429
|
+
const waiting = !!t?.messages.some((m) => m.from === "visitor") && t?.conversation?.status !== "solved";
|
|
2430
|
+
const wrote = (t?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
|
|
2431
|
+
const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
|
|
2432
|
+
const ms = watch.current ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : waiting || open ? POLL_IDLE_MS : 0;
|
|
2433
|
+
if (!stop && ms) timer = setTimeout(tick, ms);
|
|
2434
|
+
};
|
|
2435
|
+
void tick();
|
|
2436
|
+
return () => {
|
|
2437
|
+
stop = true;
|
|
2438
|
+
ctrl?.abort();
|
|
2439
|
+
clearTimeout(timer);
|
|
2440
|
+
};
|
|
2441
|
+
}, [client, take, active, open, watching]);
|
|
2442
|
+
return { thread, loaded, error, setError, unseen, take };
|
|
2443
|
+
};
|
|
2444
|
+
var useArticles = (client, on, query) => {
|
|
2445
|
+
const [list, setList] = (0, import_react2.useState)(null);
|
|
2446
|
+
const [any, setAny] = (0, import_react2.useState)(null);
|
|
2447
|
+
(0, import_react2.useEffect)(() => {
|
|
2448
|
+
if (!on) return;
|
|
2449
|
+
let stop = false;
|
|
2450
|
+
const t = setTimeout(
|
|
2451
|
+
() => void client.ready.then(
|
|
2452
|
+
() => client.api()?.articles(query.trim() || void 0).then((r) => {
|
|
2453
|
+
if (stop) return;
|
|
2454
|
+
setList(r.articles);
|
|
2455
|
+
if (!query.trim()) setAny(r.articles.length > 0);
|
|
2456
|
+
}).catch(() => !stop && setList((l) => l ?? []))
|
|
2457
|
+
),
|
|
2458
|
+
query ? 250 : 0
|
|
2459
|
+
);
|
|
2460
|
+
return () => {
|
|
2461
|
+
stop = true;
|
|
2462
|
+
clearTimeout(t);
|
|
2463
|
+
};
|
|
2464
|
+
}, [client, on, query]);
|
|
2465
|
+
return { list, any };
|
|
2466
|
+
};
|
|
2467
|
+
var spansOf = (p, spans, base) => spans.map(
|
|
2468
|
+
(s, i) => (0, import_react2.createElement)(
|
|
2469
|
+
RN3.Text,
|
|
2470
|
+
{
|
|
2471
|
+
key: i,
|
|
2472
|
+
onPress: s.href ? () => void RN3.Linking.openURL(s.href).catch(() => void 0) : void 0,
|
|
2473
|
+
accessibilityRole: s.href ? "link" : void 0,
|
|
2474
|
+
style: {
|
|
2475
|
+
...base,
|
|
2476
|
+
...s.bold ? { fontWeight: "700" } : {},
|
|
2477
|
+
...s.italic ? { fontStyle: "italic" } : {},
|
|
2478
|
+
...s.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
|
|
2479
|
+
...s.href ? { color: p.accent, textDecorationLine: "underline" } : {}
|
|
2480
|
+
}
|
|
2481
|
+
},
|
|
2482
|
+
s.text
|
|
2483
|
+
)
|
|
2484
|
+
);
|
|
2485
|
+
var ArticleView = ({ client, p, id, onAsk }) => {
|
|
2486
|
+
const k = kit(p);
|
|
2487
|
+
const [a, setA] = (0, import_react2.useState)(null);
|
|
2488
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
2489
|
+
const [rated, setRated] = (0, import_react2.useState)(false);
|
|
2490
|
+
(0, import_react2.useEffect)(() => {
|
|
2491
|
+
const ctrl = new AbortController();
|
|
2492
|
+
client.api()?.article(id, ctrl.signal).then(setA).catch((e) => e.name !== "AbortError" && setError(e.message));
|
|
2493
|
+
return () => ctrl.abort();
|
|
2494
|
+
}, [client, id]);
|
|
2495
|
+
if (error) return k.text(error, { color: p.danger });
|
|
2496
|
+
if (!a) return k.muted("Loading\u2026");
|
|
2497
|
+
const base = { color: p.fg, fontSize: 15, lineHeight: 22 };
|
|
2498
|
+
const blocks = a.blocks.map((b, i) => {
|
|
2499
|
+
if (b.type === "h2" || b.type === "h3") return (0, import_react2.createElement)(RN3.Text, { key: i, accessibilityRole: "header", style: { ...base, fontSize: b.type === "h2" ? 17 : 16, fontWeight: "700", marginTop: 8 } }, ...spansOf(p, b.spans, {}));
|
|
2500
|
+
if (b.type === "p") return (0, import_react2.createElement)(RN3.Text, { key: i, style: base }, ...spansOf(p, b.spans, {}));
|
|
2501
|
+
if (b.type === "quote") return (0, import_react2.createElement)(RN3.View, { key: i, style: { borderLeftWidth: 3, borderLeftColor: p.line, paddingLeft: 10 } }, (0, import_react2.createElement)(RN3.Text, { style: { ...base, color: p.muted } }, ...spansOf(p, b.spans, {})));
|
|
2502
|
+
if (b.type === "ul" || b.type === "ol")
|
|
2503
|
+
return (0, import_react2.createElement)(
|
|
2504
|
+
RN3.View,
|
|
2505
|
+
{ key: i, style: { gap: 4 } },
|
|
2506
|
+
...b.items.map((it, j) => (0, import_react2.createElement)(RN3.View, { key: j, style: { flexDirection: "row", gap: 8 } }, k.text(b.type === "ol" ? `${j + 1}.` : "\u2022", base), (0, import_react2.createElement)(RN3.Text, { style: { ...base, flex: 1 } }, ...spansOf(p, it, {}))))
|
|
2507
|
+
);
|
|
2508
|
+
if (b.type === "code") return (0, import_react2.createElement)(RN3.View, { key: i, style: { backgroundColor: p.soft, borderRadius: 8, padding: 10 } }, k.text(b.text, { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", fontSize: 13 }));
|
|
2509
|
+
return (0, import_react2.createElement)(RN3.View, { key: i, style: { height: 1, backgroundColor: p.line } });
|
|
2510
|
+
});
|
|
2511
|
+
const rate = (helpful) => {
|
|
2512
|
+
setRated(true);
|
|
2513
|
+
client.api()?.rateArticle(id, helpful).catch(() => void 0);
|
|
2514
|
+
if (!helpful) onAsk();
|
|
2515
|
+
};
|
|
2516
|
+
return (0, import_react2.createElement)(
|
|
2517
|
+
RN3.View,
|
|
2518
|
+
{ style: { gap: 10 } },
|
|
2519
|
+
k.muted(a.collection),
|
|
2520
|
+
(0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 20, fontWeight: "700" } }, a.title),
|
|
2521
|
+
...blocks,
|
|
2522
|
+
(0, import_react2.createElement)(
|
|
2523
|
+
RN3.View,
|
|
2524
|
+
{ style: { flexDirection: "row", alignItems: "center", gap: 8, padding: 12, borderRadius: 10, backgroundColor: p.soft, marginTop: 8 } },
|
|
2525
|
+
rated ? k.muted("Thanks for letting us know.") : k.muted("Did this answer your question?", { flex: 1 }),
|
|
2526
|
+
rated ? null : k.btn("Yes", "secondary", () => rate(true), false, { height: 34 }),
|
|
2527
|
+
rated ? null : k.btn("No", "secondary", () => rate(false), false, { height: 34 })
|
|
2528
|
+
),
|
|
2529
|
+
k.btn("Still need help? Send us a message", "secondary", onAsk)
|
|
2530
|
+
);
|
|
2531
|
+
};
|
|
2532
|
+
var MessengerSheet = ({
|
|
2533
|
+
client,
|
|
2534
|
+
p,
|
|
2535
|
+
o,
|
|
2536
|
+
chat,
|
|
2537
|
+
view,
|
|
2538
|
+
setView
|
|
2539
|
+
}) => {
|
|
2540
|
+
const k = kit(p);
|
|
2541
|
+
const [body, setBody] = (0, import_react2.useState)("");
|
|
2542
|
+
const [email, setEmail] = (0, import_react2.useState)("");
|
|
2543
|
+
const [pending, setPending] = (0, import_react2.useState)(null);
|
|
2544
|
+
const [query, setQuery] = (0, import_react2.useState)("");
|
|
2545
|
+
const helpOn = o.help !== false;
|
|
2546
|
+
const arts = useArticles(client, helpOn, query);
|
|
2547
|
+
const scroll = (0, import_react2.useRef)(null);
|
|
2548
|
+
const close = () => messengerOpen.set(false);
|
|
2549
|
+
const greeting = o.greeting ?? "Hi! How can we help?";
|
|
2550
|
+
const { thread } = chat;
|
|
2551
|
+
const count = (thread?.messages.length ?? 0) + (pending ? 1 : 0);
|
|
2552
|
+
(0, import_react2.useEffect)(() => {
|
|
2553
|
+
if (view === "messages") setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 50);
|
|
2554
|
+
}, [count, view]);
|
|
2555
|
+
const needEmail = (o.askEmail ?? true) && !thread?.hasEmail && !client.getUser()?.email;
|
|
2556
|
+
const emailOk = !email.trim() || EMAIL.test(email.trim());
|
|
2557
|
+
const send = async () => {
|
|
2558
|
+
const text2 = body.trim();
|
|
2559
|
+
const api = client.api();
|
|
2560
|
+
if (!text2 || pending || !emailOk || !api) return;
|
|
2561
|
+
setPending({ id: "pending", from: "visitor", body: text2, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2562
|
+
setBody("");
|
|
2563
|
+
chat.setError("");
|
|
2564
|
+
try {
|
|
2565
|
+
chat.take(await api.sendChat({ body: text2, email: needEmail && email.trim() ? email.trim() : void 0 }));
|
|
2566
|
+
} catch (e) {
|
|
2567
|
+
setBody(text2);
|
|
2568
|
+
chat.setError(e.message);
|
|
2569
|
+
} finally {
|
|
2570
|
+
setPending(null);
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
const bubble = (m, isPending = false) => {
|
|
2574
|
+
const mine = m.from === "visitor";
|
|
2575
|
+
return (0, import_react2.createElement)(
|
|
2576
|
+
RN3.View,
|
|
2577
|
+
{ key: m.id, style: { alignSelf: mine ? "flex-end" : "flex-start", maxWidth: "82%", gap: 4 } },
|
|
2578
|
+
(0, import_react2.createElement)(RN3.View, { style: { backgroundColor: mine ? p.accent : p.soft, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8, opacity: isPending ? 0.7 : 1 } }, k.text(m.body, { color: mine ? "#ffffff" : p.fg })),
|
|
2579
|
+
...(m.articles ?? []).map(
|
|
2580
|
+
(a) => (0, import_react2.createElement)(
|
|
2581
|
+
RN3.Pressable,
|
|
2582
|
+
{ key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { borderWidth: 1, borderColor: p.line, borderRadius: 10, paddingHorizontal: 10, paddingVertical: 6 } },
|
|
2583
|
+
k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
|
|
2584
|
+
)
|
|
2585
|
+
),
|
|
2586
|
+
m.at ? k.muted(`${mine ? "" : `${whoLabel[m.from]} \xB7 `}${isPending ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left" }) : null
|
|
2587
|
+
);
|
|
2588
|
+
};
|
|
2589
|
+
const row = (a) => (0, import_react2.createElement)(
|
|
2590
|
+
RN3.Pressable,
|
|
2591
|
+
{ key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "button", style: { flexDirection: "row", alignItems: "center", paddingVertical: 11, paddingHorizontal: 4 } },
|
|
2592
|
+
k.text(a.title, { flex: 1 }),
|
|
2593
|
+
k.muted("\u203A", { fontSize: 18 })
|
|
2594
|
+
);
|
|
2595
|
+
const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.bg, borderRadius: 12, padding: 12, borderWidth: 1, borderColor: p.line, ...style } }, ...children);
|
|
2596
|
+
const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200 });
|
|
2597
|
+
const last = thread?.messages.at(-1);
|
|
2598
|
+
const showHelp = helpOn && arts.any !== false;
|
|
2599
|
+
let content;
|
|
2600
|
+
if (view === "home")
|
|
2601
|
+
content = (0, import_react2.createElement)(
|
|
2602
|
+
RN3.ScrollView,
|
|
2603
|
+
{ key: "home", style: { flex: 1, backgroundColor: p.soft }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
|
|
2604
|
+
(0, import_react2.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 22, fontWeight: "700", paddingVertical: 6 } }, greeting),
|
|
2605
|
+
(0, import_react2.createElement)(
|
|
2606
|
+
RN3.Pressable,
|
|
2607
|
+
{ onPress: () => setView("messages"), accessibilityRole: "button" },
|
|
2608
|
+
card([
|
|
2609
|
+
k.text(last ? "Recent message" : "Send us a message", { fontWeight: "700" }),
|
|
2610
|
+
k.muted(last ? `${last.from === "visitor" ? "You" : whoLabel[last.from]}: ${last.body}` : "We usually answer within a few hours", { marginTop: 2 })
|
|
2611
|
+
])
|
|
2612
|
+
),
|
|
2613
|
+
showHelp ? card([(0, import_react2.createElement)(RN3.Pressable, { key: "s", onPress: () => setView("help") }, (0, import_react2.createElement)(RN3.View, { pointerEvents: "none" }, k.muted("\u{1F50D} Search for help", { paddingVertical: 8 }))), ...(arts.list ?? []).slice(0, 4).map(row)]) : null
|
|
2614
|
+
);
|
|
2615
|
+
else if (view === "messages")
|
|
2616
|
+
content = (0, import_react2.createElement)(
|
|
2617
|
+
RN3.View,
|
|
2618
|
+
{ key: "messages", style: { flex: 1 } },
|
|
2619
|
+
(0, import_react2.createElement)(
|
|
2620
|
+
RN3.ScrollView,
|
|
2621
|
+
{ ref: scroll, style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
|
|
2622
|
+
bubble({ id: "greeting", from: "team", body: greeting, at: "" }),
|
|
2623
|
+
!chat.loaded ? k.muted("Loading\u2026") : null,
|
|
2624
|
+
...(thread?.messages ?? []).map((m) => bubble(m)),
|
|
2625
|
+
pending ? bubble(pending, true) : null,
|
|
2626
|
+
thread?.conversation?.status === "solved" && !pending ? k.muted("This conversation is closed. Write to start a new one.", { textAlign: "center" }) : null
|
|
2627
|
+
),
|
|
2628
|
+
(0, import_react2.createElement)(
|
|
2629
|
+
RN3.View,
|
|
2630
|
+
{ style: { padding: 12, gap: 8, borderTopWidth: 1, borderTopColor: p.line } },
|
|
2631
|
+
chat.error ? k.text(chat.error, { color: p.danger, fontSize: 12 }) : null,
|
|
2632
|
+
needEmail ? k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional), so we can follow up", email: true, max: 200, style: emailOk ? {} : { borderColor: p.danger } }) : null,
|
|
2633
|
+
(0, import_react2.createElement)(
|
|
2634
|
+
RN3.View,
|
|
2635
|
+
{ style: { flexDirection: "row", gap: 8, alignItems: "flex-end" } },
|
|
2636
|
+
k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { flex: 1, minHeight: 44, maxHeight: 120 } }),
|
|
2637
|
+
k.btn("Send", "primary", () => void send(), !body.trim() || !!pending || !emailOk)
|
|
2638
|
+
)
|
|
2639
|
+
)
|
|
2640
|
+
);
|
|
2641
|
+
else if (view === "help") {
|
|
2642
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2643
|
+
for (const a of arts.list ?? []) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
|
|
2644
|
+
content = (0, import_react2.createElement)(
|
|
2645
|
+
RN3.ScrollView,
|
|
2646
|
+
{ key: "help", style: { flex: 1, backgroundColor: p.soft }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
|
|
2647
|
+
search,
|
|
2648
|
+
arts.list === null ? k.muted("Loading\u2026") : null,
|
|
2649
|
+
arts.list?.length === 0 ? (0, import_react2.createElement)(RN3.View, { style: { gap: 10, alignItems: "center", paddingVertical: 24 } }, k.muted(query.trim() ? `Nothing found for \u201C${query.trim()}\u201D.` : "No help articles yet."), k.btn("Send us a message", "primary", () => setView("messages"))) : null,
|
|
2650
|
+
...query.trim() ? arts.list?.length ? [card((arts.list ?? []).map(row))] : [] : [...groups].map(([name, items]) => (0, import_react2.createElement)(RN3.View, { key: name, style: { gap: 6 } }, k.muted(`${name} \xB7 ${items.length}`), card(items.map(row))))
|
|
2651
|
+
);
|
|
2652
|
+
} else
|
|
2653
|
+
content = (0, import_react2.createElement)(
|
|
2654
|
+
RN3.ScrollView,
|
|
2655
|
+
{ key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { padding: 16, paddingBottom: 32 } },
|
|
2656
|
+
(0, import_react2.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
|
|
2657
|
+
);
|
|
2658
|
+
const current = typeof view === "string" ? view : "help";
|
|
2659
|
+
const tab = (v, name, glyph, badge = 0) => (0, import_react2.createElement)(
|
|
2660
|
+
RN3.Pressable,
|
|
2661
|
+
{ key: v, onPress: () => setView(v), accessibilityRole: "tab", accessibilityState: { selected: current === v }, style: { flex: 1, alignItems: "center", paddingTop: 8, paddingBottom: 6, gap: 2 } },
|
|
2662
|
+
k.text(glyph, { fontSize: 18, color: current === v ? p.fg : p.muted }),
|
|
2663
|
+
k.text(name, { fontSize: 12, fontWeight: "600", color: current === v ? p.fg : p.muted }),
|
|
2664
|
+
badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 4, right: "30%", minWidth: 18, height: 18, borderRadius: 9, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(Math.min(badge, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
|
|
2665
|
+
);
|
|
2666
|
+
const titleOf = typeof view !== "string" || view === "help" ? "Help" : view === "messages" ? o.title ?? "Chat with us" : o.title ?? "Chat with us";
|
|
2667
|
+
return k.page(titleOf, close, [
|
|
2668
|
+
typeof view !== "string" ? (0, import_react2.createElement)(RN3.Pressable, { key: "back", onPress: () => setView("help"), accessibilityRole: "button", style: { paddingHorizontal: 16, paddingVertical: 8 } }, k.text("\u2039 All articles", { color: p.accent, fontWeight: "600" })) : null,
|
|
2669
|
+
content,
|
|
2670
|
+
(0, import_react2.createElement)(
|
|
2671
|
+
RN3.View,
|
|
2672
|
+
{ key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", borderTopWidth: 1, borderTopColor: p.line, paddingBottom: 20, backgroundColor: p.bg } },
|
|
2673
|
+
tab("home", "Home", "\u2302"),
|
|
2674
|
+
tab("messages", "Messages", "\u2709\uFE0E", view === "messages" ? 0 : chat.unseen.length),
|
|
2675
|
+
showHelp ? tab("help", "Help", "?") : null
|
|
2676
|
+
)
|
|
2677
|
+
]);
|
|
2678
|
+
};
|
|
2679
|
+
var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
|
|
2680
|
+
const [look, setLook] = (0, import_react2.useState)(null);
|
|
2681
|
+
(0, import_react2.useEffect)(() => {
|
|
2682
|
+
const ctrl = new AbortController();
|
|
2683
|
+
void client.ready.then(
|
|
2684
|
+
() => client.api()?.messengerLook(ctrl.signal).then(setLook).catch(() => void 0)
|
|
2685
|
+
);
|
|
2686
|
+
return () => ctrl.abort();
|
|
2687
|
+
}, [client]);
|
|
2688
|
+
const defined = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== void 0));
|
|
2689
|
+
const options = look ? { title: look.title, greeting: look.greeting, help: look.help, preview: look.preview, askEmail: look.askEmail, ...defined } : given;
|
|
2690
|
+
const p = usePalette({ theme: appearance?.theme ?? look?.theme, accent: appearance?.accent ?? look?.accent });
|
|
2691
|
+
const k = kit(p);
|
|
2692
|
+
const open = messengerOpen.useOpen();
|
|
2693
|
+
const [view, setViewState] = (0, import_react2.useState)(msgView);
|
|
2694
|
+
const [banner, setBanner] = (0, import_react2.useState)(null);
|
|
2695
|
+
(0, import_react2.useEffect)(() => {
|
|
2696
|
+
if (open) setViewState(msgView);
|
|
2697
|
+
}, [open]);
|
|
2698
|
+
const setView = (v) => {
|
|
2699
|
+
msgView = v;
|
|
2700
|
+
setViewState(v);
|
|
2701
|
+
};
|
|
2702
|
+
const chat = useChat(client, options, open, view);
|
|
2703
|
+
const latest = chat.unseen.at(-1);
|
|
2704
|
+
(0, import_react2.useEffect)(() => {
|
|
2705
|
+
if (!latest || open || options.preview === false) return;
|
|
2706
|
+
setBanner(latest);
|
|
2707
|
+
const t = setTimeout(() => setBanner(null), 6e3);
|
|
2708
|
+
return () => clearTimeout(t);
|
|
2709
|
+
}, [latest, open, options.preview]);
|
|
2710
|
+
(0, import_react2.useEffect)(() => {
|
|
2711
|
+
if (open) setBanner(null);
|
|
2712
|
+
}, [open]);
|
|
2713
|
+
const unseen = chat.unseen.length;
|
|
2714
|
+
const launcher = options.launcher ? (0, import_react2.createElement)(
|
|
2715
|
+
RN3.Pressable,
|
|
2716
|
+
{
|
|
2717
|
+
key: "launcher",
|
|
2718
|
+
onPress: () => openAt(unseen ? "messages" : "home"),
|
|
2719
|
+
accessibilityRole: "button",
|
|
2720
|
+
accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
|
|
2721
|
+
style: { position: "absolute", right: 20, bottom: 34, width: 54, height: 54, borderRadius: 27, backgroundColor: p.accent, alignItems: "center", justifyContent: "center", shadowColor: "#000", shadowOpacity: 0.2, shadowRadius: 10, elevation: 6 }
|
|
2722
|
+
},
|
|
2723
|
+
k.text("\u{1F4AC}", { fontSize: 22 }),
|
|
2724
|
+
unseen ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: -2, right: -2, minWidth: 20, height: 20, borderRadius: 10, backgroundColor: p.danger, alignItems: "center", justifyContent: "center", borderWidth: 2, borderColor: p.bg } }, k.text(String(Math.min(unseen, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : null
|
|
2725
|
+
) : null;
|
|
2726
|
+
const toast = banner ? (0, import_react2.createElement)(
|
|
2727
|
+
RN3.Pressable,
|
|
2728
|
+
{
|
|
2729
|
+
key: "banner",
|
|
2730
|
+
onPress: () => openAt("messages"),
|
|
2731
|
+
accessibilityRole: "button",
|
|
2732
|
+
accessibilityLiveRegion: "polite",
|
|
2733
|
+
style: { position: "absolute", top: 54, left: 16, right: 16, backgroundColor: p.bg, borderRadius: 14, padding: 14, gap: 2, borderWidth: 1, borderColor: p.line, shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 16, elevation: 8 }
|
|
2734
|
+
},
|
|
2735
|
+
k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
|
|
2736
|
+
(0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15 } }, banner.body)
|
|
2737
|
+
) : null;
|
|
2738
|
+
if (!launcher && !toast && !open) return null;
|
|
2739
|
+
return (0, import_react2.createElement)(
|
|
2740
|
+
RN3.View,
|
|
2741
|
+
{ pointerEvents: "box-none", style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 } },
|
|
2742
|
+
launcher,
|
|
2743
|
+
toast,
|
|
2744
|
+
open ? (0, import_react2.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView }) : null
|
|
2745
|
+
);
|
|
2746
|
+
};
|
|
821
2747
|
|
|
822
2748
|
// src/native/index.tsx
|
|
2749
|
+
var RN4 = require("react-native");
|
|
2750
|
+
var MRRocketMask = ({ children, style }) => {
|
|
2751
|
+
const ref = (0, import_react3.useRef)(null);
|
|
2752
|
+
(0, import_react3.useEffect)(() => {
|
|
2753
|
+
maskedViews.add(ref);
|
|
2754
|
+
return () => {
|
|
2755
|
+
maskedViews.delete(ref);
|
|
2756
|
+
};
|
|
2757
|
+
}, []);
|
|
2758
|
+
return (0, import_react3.createElement)(RN4.View, { ref, collapsable: false, style }, children);
|
|
2759
|
+
};
|
|
823
2760
|
var shared = null;
|
|
824
2761
|
var sharedKey = "";
|
|
825
2762
|
var createMRRocket = (options) => {
|
|
@@ -835,20 +2772,40 @@ var createMRRocket = (options) => {
|
|
|
835
2772
|
return shared;
|
|
836
2773
|
};
|
|
837
2774
|
var getMRRocket = () => shared;
|
|
838
|
-
var Ctx = (0,
|
|
839
|
-
var MRRocketProvider = ({ client: given, children, consent, ...options }) => {
|
|
840
|
-
const [client] = (0,
|
|
841
|
-
(0,
|
|
2775
|
+
var Ctx = (0, import_react3.createContext)(null);
|
|
2776
|
+
var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, surveys = true, messenger = {}, feedback = {}, heatmaps = true, ...options }) => {
|
|
2777
|
+
const [client] = (0, import_react3.useState)(() => given ?? createMRRocket({ ...options, consent }));
|
|
2778
|
+
(0, import_react3.useEffect)(() => {
|
|
842
2779
|
if (consent) client.setConsent(consent);
|
|
843
2780
|
}, [client, consent]);
|
|
844
|
-
|
|
2781
|
+
(0, import_react3.useEffect)(() => {
|
|
2782
|
+
if (options.replay !== void 0) client.setReplay(options.replay);
|
|
2783
|
+
}, [client, options.replay]);
|
|
2784
|
+
const start = (0, import_react3.useRef)(null);
|
|
2785
|
+
const touch = (e, end) => {
|
|
2786
|
+
const n = e.nativeEvent;
|
|
2787
|
+
const x = n.pageX ?? n.changedTouches?.[0]?.pageX;
|
|
2788
|
+
const y = n.pageY ?? n.changedTouches?.[0]?.pageY;
|
|
2789
|
+
if (x === void 0 || y === void 0) return;
|
|
2790
|
+
if (!end) start.current = { x, y, t: Date.now() };
|
|
2791
|
+
else if (start.current && Math.hypot(x - start.current.x, y - start.current.y) < 10 && Date.now() - start.current.t < 500) client.tap(x, y);
|
|
2792
|
+
};
|
|
2793
|
+
return (0, import_react3.createElement)(
|
|
2794
|
+
Ctx.Provider,
|
|
2795
|
+
{ value: client },
|
|
2796
|
+
heatmaps ? (0, import_react3.createElement)(RN4.View, { style: { flex: 1 }, onTouchStartCapture: (e) => touch(e, false), onTouchEndCapture: (e) => touch(e, true) }, children) : children,
|
|
2797
|
+
flows ? (0, import_react3.createElement)(MRRocketFlows, { flows: client.flows, appearance }) : null,
|
|
2798
|
+
surveys ? (0, import_react3.createElement)(MRRocketSurveys, { client, appearance }) : null,
|
|
2799
|
+
feedback ? (0, import_react3.createElement)(MRRocketFeedback, { client, appearance, options: feedback }) : null,
|
|
2800
|
+
messenger ? (0, import_react3.createElement)(MRRocketMessenger, { client, appearance, options: messenger }) : null
|
|
2801
|
+
);
|
|
845
2802
|
};
|
|
846
2803
|
var useClient = () => {
|
|
847
|
-
const c = (0,
|
|
2804
|
+
const c = (0, import_react3.useContext)(Ctx);
|
|
848
2805
|
if (!c) throw new Error("MRRocket: wrap your app in <MRRocketProvider apiKey=\u2026>.");
|
|
849
2806
|
return c;
|
|
850
2807
|
};
|
|
851
|
-
var useVersion = (client) => (0,
|
|
2808
|
+
var useVersion = (client) => (0, import_react3.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
|
|
852
2809
|
var useMRRocket = () => {
|
|
853
2810
|
const client = useClient();
|
|
854
2811
|
useVersion(client);
|
|
@@ -858,6 +2815,7 @@ var useMRRocket = () => {
|
|
|
858
2815
|
identify: client.identify,
|
|
859
2816
|
reset: client.reset,
|
|
860
2817
|
setConsent: client.setConsent,
|
|
2818
|
+
setReplay: client.setReplay,
|
|
861
2819
|
captureError: client.captureError,
|
|
862
2820
|
flush: client.flush,
|
|
863
2821
|
distinctId: client.getDistinctId(),
|
|
@@ -865,6 +2823,18 @@ var useMRRocket = () => {
|
|
|
865
2823
|
ready: client.isReady()
|
|
866
2824
|
};
|
|
867
2825
|
};
|
|
2826
|
+
var useCancelFlow = () => {
|
|
2827
|
+
const client = useClient();
|
|
2828
|
+
return { open: client.openCancelFlow };
|
|
2829
|
+
};
|
|
2830
|
+
var usePush = () => {
|
|
2831
|
+
const client = useClient();
|
|
2832
|
+
return { register: client.registerForPush, unregister: client.unregisterForPush };
|
|
2833
|
+
};
|
|
2834
|
+
var useOnboarding = () => {
|
|
2835
|
+
const client = useClient();
|
|
2836
|
+
return { start: client.startOnboarding };
|
|
2837
|
+
};
|
|
868
2838
|
var useFlag = (key) => {
|
|
869
2839
|
const client = useClient();
|
|
870
2840
|
useVersion(client);
|
|
@@ -880,11 +2850,22 @@ var useVariant = (key) => {
|
|
|
880
2850
|
useVersion(client);
|
|
881
2851
|
const config = client.getConfig();
|
|
882
2852
|
const variant = client.peekVariant(key);
|
|
883
|
-
(0,
|
|
2853
|
+
(0, import_react3.useEffect)(() => {
|
|
884
2854
|
if (config) client.expose(key, variant);
|
|
885
2855
|
}, [client, config, key, variant]);
|
|
886
2856
|
return variant;
|
|
887
2857
|
};
|
|
2858
|
+
var useText = (id, fallback) => {
|
|
2859
|
+
const client = useClient();
|
|
2860
|
+
useVersion(client);
|
|
2861
|
+
const config = client.getConfig();
|
|
2862
|
+
const text2 = client.peekText(id);
|
|
2863
|
+
const person = client.getDistinctId();
|
|
2864
|
+
(0, import_react3.useEffect)(() => {
|
|
2865
|
+
if (config) client.exposeText(id);
|
|
2866
|
+
}, [client, config, id, person]);
|
|
2867
|
+
return text2 ?? fallback;
|
|
2868
|
+
};
|
|
888
2869
|
var expoRouter;
|
|
889
2870
|
var loadExpoRouter = () => {
|
|
890
2871
|
if (expoRouter !== void 0) return expoRouter;
|
|
@@ -905,13 +2886,13 @@ var useExpoRouterTracking = () => {
|
|
|
905
2886
|
console.warn("[MRRocket] useExpoRouterTracking needs expo-router. With React Navigation use useNavigationTracking.");
|
|
906
2887
|
}
|
|
907
2888
|
const pathname = (router?.usePathname ?? noPathname)();
|
|
908
|
-
(0,
|
|
2889
|
+
(0, import_react3.useEffect)(() => {
|
|
909
2890
|
if (pathname) client.screen(pathname);
|
|
910
2891
|
}, [client, pathname]);
|
|
911
2892
|
};
|
|
912
2893
|
var useNavigationTracking = () => {
|
|
913
2894
|
const client = useClient();
|
|
914
|
-
return (0,
|
|
2895
|
+
return (0, import_react3.useCallback)(
|
|
915
2896
|
(state) => {
|
|
916
2897
|
const name = activeRouteName(state);
|
|
917
2898
|
if (name) client.screen(name);
|