@mrrocket/sdk 0.8.0 → 0.10.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 +20 -0
- package/README.md +35 -1
- package/dist/api-client.d.ts +221 -0
- package/dist/api-client.js +79 -0
- package/dist/api-client.js.map +1 -0
- package/dist/api.d.ts +2 -122
- package/dist/api.js +2 -61
- package/dist/api.js.map +1 -1
- package/dist/browser/mrrocket-messenger.js +348 -0
- package/dist/browser/mrrocket-replay.js +3 -3
- package/dist/browser/mrrocket.js +1 -1
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +3 -0
- 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/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/dist/feedback.js +132 -24
- package/dist/feedback.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/messenger-ui.d.ts +49 -0
- package/dist/messenger-ui.js +619 -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/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 +349 -4
- package/dist/react-native/index.d.ts +349 -4
- package/dist/react-native/index.js +1345 -31
- package/dist/react-native/index.js.map +1 -1
- package/dist/styles-css.d.ts +6 -0
- package/dist/styles-css.js +362 -0
- package/dist/styles-css.js.map +1 -0
- package/dist/styles.d.ts +1 -2
- package/dist/styles.js +3 -237
- package/dist/styles.js.map +1 -1
- package/package.json +17 -5
|
@@ -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,21 +17,28 @@ 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,
|
|
23
26
|
MRRocketFlows: () => MRRocketFlows,
|
|
27
|
+
MRRocketMask: () => MRRocketMask,
|
|
28
|
+
MRRocketMessenger: () => MRRocketMessenger,
|
|
24
29
|
MRRocketProvider: () => MRRocketProvider,
|
|
30
|
+
MRRocketSurveys: () => MRRocketSurveys,
|
|
25
31
|
SDK_VERSION: () => SDK_VERSION,
|
|
26
32
|
activeRouteName: () => activeRouteName,
|
|
27
33
|
createMRRocket: () => createMRRocket,
|
|
28
34
|
getMRRocket: () => getMRRocket,
|
|
29
35
|
useCancelFlow: () => useCancelFlow,
|
|
30
36
|
useExpoRouterTracking: () => useExpoRouterTracking,
|
|
37
|
+
useFeedback: () => useFeedback,
|
|
31
38
|
useFlag: () => useFlag,
|
|
32
39
|
useFlagVariant: () => useFlagVariant,
|
|
33
40
|
useMRRocket: () => useMRRocket,
|
|
41
|
+
useMessenger: () => useMessenger,
|
|
34
42
|
useNavigationTracking: () => useNavigationTracking,
|
|
35
43
|
useOnboarding: () => useOnboarding,
|
|
36
44
|
usePush: () => usePush,
|
|
@@ -38,10 +46,10 @@ __export(native_exports, {
|
|
|
38
46
|
useVariant: () => useVariant
|
|
39
47
|
});
|
|
40
48
|
module.exports = __toCommonJS(native_exports);
|
|
41
|
-
var
|
|
49
|
+
var import_react3 = require("react");
|
|
42
50
|
|
|
43
51
|
// src/core.ts
|
|
44
|
-
var SDK_VERSION = "0.
|
|
52
|
+
var SDK_VERSION = "0.10.0";
|
|
45
53
|
var propOf = (person, name) => {
|
|
46
54
|
const want = name.trim().toLowerCase();
|
|
47
55
|
for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
|
|
@@ -114,25 +122,25 @@ var parseQuery = (url) => {
|
|
|
114
122
|
return out;
|
|
115
123
|
};
|
|
116
124
|
var hashUnit = (input) => {
|
|
117
|
-
let
|
|
125
|
+
let h3 = 2166136261;
|
|
118
126
|
for (let i = 0; i < input.length; i++) {
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
h3 ^= input.charCodeAt(i);
|
|
128
|
+
h3 = Math.imul(h3, 16777619);
|
|
121
129
|
}
|
|
122
|
-
return (
|
|
130
|
+
return (h3 >>> 0) / 4294967296;
|
|
123
131
|
};
|
|
124
132
|
var flagUnit = (input) => {
|
|
125
|
-
let
|
|
133
|
+
let h3 = 2166136261;
|
|
126
134
|
for (let i = 0; i < input.length; i++) {
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
h3 ^= input.charCodeAt(i);
|
|
136
|
+
h3 = Math.imul(h3, 16777619);
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
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;
|
|
136
144
|
};
|
|
137
145
|
var assign = (experiment, distinctId) => {
|
|
138
146
|
const control = experiment.variants[0]?.id ?? "control";
|
|
@@ -163,6 +171,72 @@ var textChanges = (config, distinctId, preview) => {
|
|
|
163
171
|
};
|
|
164
172
|
var newDistinctId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `ck_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
165
173
|
|
|
174
|
+
// src/api-client.ts
|
|
175
|
+
var replyLine = (t) => t === "minutes" ? "We usually reply in a few minutes" : t === "day" ? "We usually reply within a day" : "We usually reply in a few hours";
|
|
176
|
+
var inkOn = (hex) => {
|
|
177
|
+
const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
|
|
178
|
+
if (!m) return "#ffffff";
|
|
179
|
+
const [r, g, b] = [m[1], m[2], m[3]].map((x) => {
|
|
180
|
+
const c = parseInt(x, 16) / 255;
|
|
181
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
182
|
+
});
|
|
183
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
|
|
184
|
+
};
|
|
185
|
+
var ApiError = class extends Error {
|
|
186
|
+
constructor(message, status) {
|
|
187
|
+
super(message);
|
|
188
|
+
__publicField(this, "status");
|
|
189
|
+
this.status = status;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var qs = (params) => Object.entries(params).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
193
|
+
var call = async (url, init, signal) => {
|
|
194
|
+
let res;
|
|
195
|
+
try {
|
|
196
|
+
res = await fetch(url, {
|
|
197
|
+
...init,
|
|
198
|
+
signal,
|
|
199
|
+
headers: init?.body ? { "content-type": "application/json" } : void 0
|
|
200
|
+
});
|
|
201
|
+
} catch (e) {
|
|
202
|
+
if (e.name === "AbortError") throw e;
|
|
203
|
+
throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
|
|
204
|
+
}
|
|
205
|
+
if (!res.ok) {
|
|
206
|
+
let detail = "";
|
|
207
|
+
try {
|
|
208
|
+
detail = (await res.json()).error ?? "";
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
|
|
212
|
+
}
|
|
213
|
+
return await res.json();
|
|
214
|
+
};
|
|
215
|
+
var createApi = (c) => {
|
|
216
|
+
const base = `${c.host}/api/sdk`;
|
|
217
|
+
const who = { key: c.apiKey, distinctId: c.distinctId, user: c.user };
|
|
218
|
+
const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who, ...body }) }, signal);
|
|
219
|
+
return {
|
|
220
|
+
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),
|
|
221
|
+
post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
|
|
222
|
+
roadmap: (signal) => call(`${base}/feedback/roadmap?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
|
|
223
|
+
changelog: (signal) => call(`${base}/feedback/changelog?${qs({ key: c.apiKey })}`, void 0, signal),
|
|
224
|
+
submit: (p) => post("/feedback", p),
|
|
225
|
+
vote: (id) => post(`/feedback/${encodeURIComponent(id)}/vote`, {}),
|
|
226
|
+
comments: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}/comments?${qs({ key: c.apiKey })}`, void 0, signal),
|
|
227
|
+
comment: (id, body) => post(`/feedback/${encodeURIComponent(id)}/comments`, { body }),
|
|
228
|
+
survey: (url, signal) => call(`${base}/surveys?${qs({ key: c.apiKey, distinctId: c.distinctId, url })}`, void 0, signal),
|
|
229
|
+
seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
|
|
230
|
+
respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
|
|
231
|
+
chat: (signal) => call(`${base}/messages?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
|
|
232
|
+
sendChat: (m) => post("/messages", m),
|
|
233
|
+
articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
|
|
234
|
+
article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs({ key: c.apiKey, distinctId: c.distinctId })}`, void 0, signal),
|
|
235
|
+
messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
|
|
236
|
+
rateArticle: (id, helpful) => post(`/articles/${encodeURIComponent(id)}/rate`, { helpful })
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
166
240
|
// src/batch.ts
|
|
167
241
|
var MAX_BATCH = 100;
|
|
168
242
|
var MAX_BYTES = 6e4;
|
|
@@ -612,6 +686,174 @@ var createFlows = (env) => {
|
|
|
612
686
|
return controller;
|
|
613
687
|
};
|
|
614
688
|
|
|
689
|
+
// src/rage.ts
|
|
690
|
+
var RageDetector = class {
|
|
691
|
+
constructor(windowMs = 1e3, radius = 30, count = 3) {
|
|
692
|
+
__publicField(this, "windowMs", windowMs);
|
|
693
|
+
__publicField(this, "radius", radius);
|
|
694
|
+
__publicField(this, "count", count);
|
|
695
|
+
__publicField(this, "clicks", []);
|
|
696
|
+
__publicField(this, "fired", false);
|
|
697
|
+
}
|
|
698
|
+
push(x, y, t) {
|
|
699
|
+
const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
|
|
700
|
+
if (near.length !== this.clicks.length) this.fired = false;
|
|
701
|
+
this.clicks = [...near, { x, y, t }];
|
|
702
|
+
if (this.clicks.length >= this.count && !this.fired) {
|
|
703
|
+
this.fired = true;
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// src/native/recorder.ts
|
|
711
|
+
var FRAME_MS = 1e3;
|
|
712
|
+
var WIDTH = 360;
|
|
713
|
+
var BUFFER_MS = 6e4;
|
|
714
|
+
var CHUNK_FRAMES = 12;
|
|
715
|
+
var CHUNK_CHARS = 7e5;
|
|
716
|
+
var MAX_MS = 30 * 6e4;
|
|
717
|
+
var shareOf = (s) => {
|
|
718
|
+
let h3 = 2166136261;
|
|
719
|
+
for (let i = 0; i < s.length; i++) h3 = Math.imul(h3 ^ s.charCodeAt(i), 16777619);
|
|
720
|
+
return (h3 >>> 0) / 4294967296;
|
|
721
|
+
};
|
|
722
|
+
var createRecorder = (deps, st) => {
|
|
723
|
+
let timer = null;
|
|
724
|
+
let busy = false;
|
|
725
|
+
let last = "";
|
|
726
|
+
let rec = null;
|
|
727
|
+
let frames = [];
|
|
728
|
+
let marks = [];
|
|
729
|
+
const ensure = () => {
|
|
730
|
+
const session = st.session();
|
|
731
|
+
const cfg = st.config();
|
|
732
|
+
if (!rec || rec.session !== session) {
|
|
733
|
+
frames = [];
|
|
734
|
+
marks = [];
|
|
735
|
+
last = "";
|
|
736
|
+
const sampled = !!cfg && shareOf(session) < (cfg.sampleRate ?? 0.1);
|
|
737
|
+
rec = { session, start: deps.now(), seq: 0, uploading: sampled, trigger: "sample", stopped: false };
|
|
738
|
+
}
|
|
739
|
+
return rec;
|
|
740
|
+
};
|
|
741
|
+
const send = async (r) => {
|
|
742
|
+
if (!frames.length || r.stopped) return;
|
|
743
|
+
const batch = frames.splice(0, frames.length);
|
|
744
|
+
const ms = marks.splice(0, marks.length);
|
|
745
|
+
const size = deps.screen();
|
|
746
|
+
const { distinctId, identified } = st.who();
|
|
747
|
+
const body = JSON.stringify({
|
|
748
|
+
key: st.apiKey,
|
|
749
|
+
consent: { replay: true },
|
|
750
|
+
sessionId: r.session,
|
|
751
|
+
start: r.start,
|
|
752
|
+
seq: r.seq++,
|
|
753
|
+
enc: "frames",
|
|
754
|
+
data: JSON.stringify(batch),
|
|
755
|
+
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 },
|
|
756
|
+
distinctId,
|
|
757
|
+
identified,
|
|
758
|
+
trigger: r.trigger,
|
|
759
|
+
app: st.app
|
|
760
|
+
});
|
|
761
|
+
const status = await deps.send(body);
|
|
762
|
+
if (status === 403 || status === 409 || status === 413) r.stopped = true;
|
|
763
|
+
};
|
|
764
|
+
const tick = async () => {
|
|
765
|
+
if (busy || !st.allowed() || !st.config()) return;
|
|
766
|
+
const r = ensure();
|
|
767
|
+
if (r.stopped || deps.now() - r.start > MAX_MS) return;
|
|
768
|
+
if (deps.keyboardOpen()) return;
|
|
769
|
+
busy = true;
|
|
770
|
+
try {
|
|
771
|
+
const shot = await deps.capture(WIDTH);
|
|
772
|
+
if (!shot || shot.img === last) return;
|
|
773
|
+
last = shot.img;
|
|
774
|
+
const size = deps.screen();
|
|
775
|
+
const scale = size && size.w ? shot.w / size.w : 1;
|
|
776
|
+
const measured = await deps.masks();
|
|
777
|
+
if (measured === null) {
|
|
778
|
+
last = "";
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const masks = measured.map(([x, y, w, h3]) => [x * scale, y * scale, w * scale, h3 * scale].map(Math.round));
|
|
782
|
+
frames.push({ t: deps.now(), w: shot.w, h: shot.h, img: shot.img, masks });
|
|
783
|
+
if (!r.uploading) {
|
|
784
|
+
const since = deps.now() - BUFFER_MS;
|
|
785
|
+
frames = frames.filter((f) => f.t >= since);
|
|
786
|
+
marks = marks.filter((m) => m.t >= since);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const chars = frames.reduce((a, f) => a + f.img.length, 0);
|
|
790
|
+
if (frames.length >= CHUNK_FRAMES || chars >= CHUNK_CHARS) await send(r);
|
|
791
|
+
} catch {
|
|
792
|
+
} finally {
|
|
793
|
+
busy = false;
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
return {
|
|
797
|
+
/** Starts or stops the frame loop to match the app state (active) and whether recording is allowed. */
|
|
798
|
+
sync(active) {
|
|
799
|
+
const on = active && st.allowed() && !!st.config();
|
|
800
|
+
if (on && timer === null) timer = deps.setInterval(() => void tick(), FRAME_MS);
|
|
801
|
+
if (!on && timer !== null) {
|
|
802
|
+
deps.clearInterval(timer);
|
|
803
|
+
timer = null;
|
|
804
|
+
if (rec?.uploading) void send(rec);
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
/** A screen, tap, rage tap or error, for the timeline. */
|
|
808
|
+
mark(m) {
|
|
809
|
+
if (!st.allowed() || !st.config()) return;
|
|
810
|
+
ensure();
|
|
811
|
+
marks.push({ t: deps.now(), ...m });
|
|
812
|
+
if (marks.length > 300) marks.splice(0, marks.length - 300);
|
|
813
|
+
},
|
|
814
|
+
/** A rage tap or an error: a buffering recording uploads its last minute and keeps recording. */
|
|
815
|
+
trigger(kind) {
|
|
816
|
+
if (!rec || rec.uploading || rec.stopped || !st.allowed()) return;
|
|
817
|
+
rec.uploading = true;
|
|
818
|
+
rec.trigger = kind;
|
|
819
|
+
void send(rec);
|
|
820
|
+
},
|
|
821
|
+
/** Sends what waits (going to the background). */
|
|
822
|
+
flush() {
|
|
823
|
+
if (rec?.uploading) return send(rec);
|
|
824
|
+
return Promise.resolve();
|
|
825
|
+
},
|
|
826
|
+
/** Consent withdrawn: forget everything not sent. */
|
|
827
|
+
drop() {
|
|
828
|
+
frames = [];
|
|
829
|
+
marks = [];
|
|
830
|
+
rec = null;
|
|
831
|
+
last = "";
|
|
832
|
+
},
|
|
833
|
+
/** For tests: one frame now. */
|
|
834
|
+
tick
|
|
835
|
+
};
|
|
836
|
+
};
|
|
837
|
+
var maskedViews = /* @__PURE__ */ new Set();
|
|
838
|
+
var measureMasks = () => Promise.all(
|
|
839
|
+
[...maskedViews].map(
|
|
840
|
+
(ref) => new Promise((resolve) => {
|
|
841
|
+
const v = ref.current;
|
|
842
|
+
if (!v) return resolve("none");
|
|
843
|
+
const t = setTimeout(() => resolve("failed"), 100);
|
|
844
|
+
try {
|
|
845
|
+
v.measureInWindow((x, y, w, h3) => {
|
|
846
|
+
clearTimeout(t);
|
|
847
|
+
resolve(w > 0 && h3 > 0 ? [x, y, w, h3] : "none");
|
|
848
|
+
});
|
|
849
|
+
} catch {
|
|
850
|
+
clearTimeout(t);
|
|
851
|
+
resolve("failed");
|
|
852
|
+
}
|
|
853
|
+
})
|
|
854
|
+
)
|
|
855
|
+
).then((rs) => rs.includes("failed") ? null : rs.filter((r) => Array.isArray(r)));
|
|
856
|
+
|
|
615
857
|
// src/referral.ts
|
|
616
858
|
var REF_PARAM = "mrr_ref";
|
|
617
859
|
var CLICK_PARAM = "mrr_click";
|
|
@@ -634,7 +876,8 @@ var KEYS = {
|
|
|
634
876
|
queue: "mrrocket:queue",
|
|
635
877
|
config: "mrrocket:config",
|
|
636
878
|
installed: "mrrocket:installed",
|
|
637
|
-
referral: "mrrocket:referral"
|
|
879
|
+
referral: "mrrocket:referral",
|
|
880
|
+
surveys: "mrrocket:surveys"
|
|
638
881
|
};
|
|
639
882
|
var MAX_ERRORS = 20;
|
|
640
883
|
var MAX_CRUMBS = 20;
|
|
@@ -697,6 +940,8 @@ var createClient = (options, deps) => {
|
|
|
697
940
|
let backgroundSince = null;
|
|
698
941
|
const early = [];
|
|
699
942
|
const exposed = /* @__PURE__ */ new Set();
|
|
943
|
+
const surveysDone = /* @__PURE__ */ new Set();
|
|
944
|
+
const rage = new RageDetector();
|
|
700
945
|
let textsFor = null;
|
|
701
946
|
const texts = () => {
|
|
702
947
|
const id = distinctId();
|
|
@@ -728,8 +973,10 @@ var createClient = (options, deps) => {
|
|
|
728
973
|
flows.setConfig(config);
|
|
729
974
|
void flows.refresh();
|
|
730
975
|
};
|
|
976
|
+
let recorder = null;
|
|
731
977
|
const changed = () => {
|
|
732
978
|
version++;
|
|
979
|
+
recorder?.sync(appState === "active");
|
|
733
980
|
for (const l of [...listeners]) {
|
|
734
981
|
try {
|
|
735
982
|
l();
|
|
@@ -904,6 +1151,8 @@ var createClient = (options, deps) => {
|
|
|
904
1151
|
...who ? { distinctId: who } : {},
|
|
905
1152
|
...session ? { sessionId: session.id } : {}
|
|
906
1153
|
});
|
|
1154
|
+
recorder?.mark({ kind: "error", path: currentScreen ?? "/", label: clip2(d.message, 120) });
|
|
1155
|
+
recorder?.trigger("error");
|
|
907
1156
|
if (fatal) void sendErrors();
|
|
908
1157
|
else if (errorTimer === null) errorTimer = setTimeoutFn(() => void sendErrors(), 2e3);
|
|
909
1158
|
};
|
|
@@ -921,11 +1170,34 @@ var createClient = (options, deps) => {
|
|
|
921
1170
|
previousHandler?.(error, isFatal);
|
|
922
1171
|
});
|
|
923
1172
|
};
|
|
1173
|
+
let replayOn = options.replay === true;
|
|
1174
|
+
recorder = deps.recorder ? createRecorder(
|
|
1175
|
+
{
|
|
1176
|
+
now,
|
|
1177
|
+
setInterval: setIntervalFn,
|
|
1178
|
+
clearInterval: clearIntervalFn,
|
|
1179
|
+
capture: deps.recorder.capture,
|
|
1180
|
+
keyboardOpen: deps.recorder.keyboardOpen,
|
|
1181
|
+
masks: deps.recorder.masks,
|
|
1182
|
+
screen: () => deps.screen?.(),
|
|
1183
|
+
send: (body) => post("/api/sdk/replay", body)
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
allowed: () => replayOn && loaded && !closed && consent !== "off" && !!apiKey,
|
|
1187
|
+
config: () => config?.replay,
|
|
1188
|
+
session: () => session?.id ?? touchSession(),
|
|
1189
|
+
who: () => ({ distinctId: sendId(), identified: !!identified }),
|
|
1190
|
+
apiKey,
|
|
1191
|
+
app: context
|
|
1192
|
+
}
|
|
1193
|
+
) : null;
|
|
1194
|
+
if (options.replay && !deps.recorder) warn("Session recordings need react-native-view-shot: npx expo install react-native-view-shot");
|
|
924
1195
|
let appStateSub;
|
|
925
1196
|
const onAppState = (next) => {
|
|
926
1197
|
if (closed) return;
|
|
927
1198
|
const prev = appState;
|
|
928
1199
|
appState = next;
|
|
1200
|
+
recorder?.sync(next === "active");
|
|
929
1201
|
if (next === "active" && prev !== "active") {
|
|
930
1202
|
startTimer();
|
|
931
1203
|
if (backgroundSince !== null) {
|
|
@@ -938,6 +1210,7 @@ var createClient = (options, deps) => {
|
|
|
938
1210
|
crumb("lifecycle", "app_background");
|
|
939
1211
|
if (options.lifecycle !== false) record("$app_background");
|
|
940
1212
|
stopTimer();
|
|
1213
|
+
void recorder?.flush();
|
|
941
1214
|
void persistQueue();
|
|
942
1215
|
void flush();
|
|
943
1216
|
if (errorQueue.length) void sendErrors();
|
|
@@ -1030,12 +1303,13 @@ var createClient = (options, deps) => {
|
|
|
1030
1303
|
const start = async () => {
|
|
1031
1304
|
let firstOpen = false;
|
|
1032
1305
|
if (consent !== "off") {
|
|
1033
|
-
const [id, stored, cached, installed, ref] = await Promise.all([
|
|
1306
|
+
const [id, stored, cached, installed, ref, surveys] = await Promise.all([
|
|
1034
1307
|
get(KEYS.id),
|
|
1035
1308
|
get(KEYS.queue),
|
|
1036
1309
|
get(KEYS.config),
|
|
1037
1310
|
get(KEYS.installed),
|
|
1038
|
-
get(KEYS.referral)
|
|
1311
|
+
get(KEYS.referral),
|
|
1312
|
+
get(KEYS.surveys)
|
|
1039
1313
|
]);
|
|
1040
1314
|
if (consent === "persistent") {
|
|
1041
1315
|
installId = id || newDistinctId();
|
|
@@ -1058,6 +1332,14 @@ var createClient = (options, deps) => {
|
|
|
1058
1332
|
if (r && typeof r.code === "string") referral = r;
|
|
1059
1333
|
} catch {
|
|
1060
1334
|
}
|
|
1335
|
+
try {
|
|
1336
|
+
const done = surveys && consent === "persistent" ? JSON.parse(surveys) : null;
|
|
1337
|
+
if (Array.isArray(done)) {
|
|
1338
|
+
for (const x of done) if (typeof x === "string") surveysDone.add(x);
|
|
1339
|
+
}
|
|
1340
|
+
} catch {
|
|
1341
|
+
await remove(KEYS.surveys);
|
|
1342
|
+
}
|
|
1061
1343
|
firstOpen = !!storage && !installed;
|
|
1062
1344
|
if (firstOpen) await set(KEYS.installed, String(now()));
|
|
1063
1345
|
} else {
|
|
@@ -1108,6 +1390,24 @@ var createClient = (options, deps) => {
|
|
|
1108
1390
|
record(event, props);
|
|
1109
1391
|
flows.onEvent(event);
|
|
1110
1392
|
},
|
|
1393
|
+
tap: (x, y) => {
|
|
1394
|
+
if (options.heatmaps === false || consent === "off") return;
|
|
1395
|
+
const size = deps.screen?.();
|
|
1396
|
+
if (!size || !Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
1397
|
+
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 };
|
|
1398
|
+
record("$click", props);
|
|
1399
|
+
recorder?.mark({ kind: "click", path: currentScreen ?? "/", x: props.x, y: props.y, vw: props.vw });
|
|
1400
|
+
if (rage.push(x, y, now())) {
|
|
1401
|
+
record("$rageclick", props);
|
|
1402
|
+
recorder?.mark({ kind: "rage", path: currentScreen ?? "/", x: props.x, y: props.y, vw: props.vw });
|
|
1403
|
+
recorder?.trigger("rage");
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
setReplay: (on) => {
|
|
1407
|
+
replayOn = on;
|
|
1408
|
+
if (!on) recorder?.drop();
|
|
1409
|
+
recorder?.sync(appState === "active");
|
|
1410
|
+
},
|
|
1111
1411
|
screen: (name, props) => {
|
|
1112
1412
|
if (typeof name !== "string" || !name.trim()) return;
|
|
1113
1413
|
const path = (name.startsWith("/") ? name : `/${name}`).slice(0, 300);
|
|
@@ -1116,6 +1416,7 @@ var createClient = (options, deps) => {
|
|
|
1116
1416
|
currentScreen = path;
|
|
1117
1417
|
crumb("navigation", path);
|
|
1118
1418
|
record("$screen", { ...props, path, screen: name, ...previous ? { previous } : {} });
|
|
1419
|
+
recorder?.mark({ kind: "pageview", path });
|
|
1119
1420
|
},
|
|
1120
1421
|
identify: (id, props) => {
|
|
1121
1422
|
if (typeof id !== "string" || !id) return;
|
|
@@ -1163,6 +1464,7 @@ var createClient = (options, deps) => {
|
|
|
1163
1464
|
const before = consent;
|
|
1164
1465
|
consent = next;
|
|
1165
1466
|
if (next === "off") {
|
|
1467
|
+
recorder?.drop();
|
|
1166
1468
|
queue = [];
|
|
1167
1469
|
session = null;
|
|
1168
1470
|
errorQueue.length = 0;
|
|
@@ -1171,10 +1473,12 @@ var createClient = (options, deps) => {
|
|
|
1171
1473
|
installId = null;
|
|
1172
1474
|
void remove(KEYS.id);
|
|
1173
1475
|
void remove(KEYS.referral);
|
|
1476
|
+
void remove(KEYS.surveys);
|
|
1174
1477
|
} else {
|
|
1175
1478
|
installId = identified ?? newDistinctId();
|
|
1176
1479
|
void set(KEYS.id, installId);
|
|
1177
1480
|
if (referral) void set(KEYS.referral, JSON.stringify(referral));
|
|
1481
|
+
if (surveysDone.size) void set(KEYS.surveys, JSON.stringify([...surveysDone]));
|
|
1178
1482
|
}
|
|
1179
1483
|
if (before === "off" && currentScreen && loaded) record("$screen", { path: currentScreen, screen: currentScreen });
|
|
1180
1484
|
changed();
|
|
@@ -1217,6 +1521,22 @@ var createClient = (options, deps) => {
|
|
|
1217
1521
|
isReady: () => isReady,
|
|
1218
1522
|
getDistinctId: distinctId,
|
|
1219
1523
|
getConsent: () => consent,
|
|
1524
|
+
getUser: () => {
|
|
1525
|
+
if (!identified) return void 0;
|
|
1526
|
+
const p = person;
|
|
1527
|
+
return { id: identified, ...typeof p?.name === "string" ? { name: p.name } : {}, ...typeof p?.email === "string" ? { email: p.email } : {} };
|
|
1528
|
+
},
|
|
1529
|
+
api: (as) => {
|
|
1530
|
+
if (!apiKey || consent === "off") return null;
|
|
1531
|
+
const known = client.getUser();
|
|
1532
|
+
const user = as?.name || as?.email ? { ...known, ...as.name ? { name: as.name } : {}, ...as.email ? { email: as.email } : {} } : known;
|
|
1533
|
+
return createApi({ host, apiKey, distinctId: distinctId(), user });
|
|
1534
|
+
},
|
|
1535
|
+
isSurveyDone: (id) => surveysDone.has(id),
|
|
1536
|
+
markSurveyDone: (id) => {
|
|
1537
|
+
surveysDone.add(id);
|
|
1538
|
+
if (consent === "persistent") void set(KEYS.surveys, JSON.stringify([...surveysDone].slice(-100)));
|
|
1539
|
+
},
|
|
1220
1540
|
getConfig: () => config,
|
|
1221
1541
|
subscribe: (listener) => {
|
|
1222
1542
|
listeners.add(listener);
|
|
@@ -1226,6 +1546,7 @@ var createClient = (options, deps) => {
|
|
|
1226
1546
|
close: async () => {
|
|
1227
1547
|
if (closed) return;
|
|
1228
1548
|
stopTimer();
|
|
1549
|
+
recorder?.sync(false);
|
|
1229
1550
|
appStateSub?.remove();
|
|
1230
1551
|
pushOff?.();
|
|
1231
1552
|
pushOff = null;
|
|
@@ -1521,6 +1842,12 @@ try {
|
|
|
1521
1842
|
} catch {
|
|
1522
1843
|
expoLocalization = null;
|
|
1523
1844
|
}
|
|
1845
|
+
var viewShot = null;
|
|
1846
|
+
try {
|
|
1847
|
+
viewShot = require("react-native-view-shot");
|
|
1848
|
+
} catch {
|
|
1849
|
+
viewShot = null;
|
|
1850
|
+
}
|
|
1524
1851
|
var expoNotifications = null;
|
|
1525
1852
|
try {
|
|
1526
1853
|
expoNotifications = require("expo-notifications");
|
|
@@ -1594,6 +1921,30 @@ var readContext = () => {
|
|
|
1594
1921
|
for (const k of Object.keys(ctx)) if (ctx[k] === void 0) delete ctx[k];
|
|
1595
1922
|
return ctx;
|
|
1596
1923
|
};
|
|
1924
|
+
var recorderDeps = () => {
|
|
1925
|
+
const capture = viewShot?.captureScreen;
|
|
1926
|
+
if (!capture) return null;
|
|
1927
|
+
let keyboard = false;
|
|
1928
|
+
try {
|
|
1929
|
+
RN2.Keyboard.addListener("keyboardWillShow", () => keyboard = true);
|
|
1930
|
+
RN2.Keyboard.addListener("keyboardDidShow", () => keyboard = true);
|
|
1931
|
+
RN2.Keyboard.addListener("keyboardDidHide", () => keyboard = false);
|
|
1932
|
+
} catch {
|
|
1933
|
+
return null;
|
|
1934
|
+
}
|
|
1935
|
+
return {
|
|
1936
|
+
capture: async (width) => {
|
|
1937
|
+
const s = RN2.Dimensions.get("screen");
|
|
1938
|
+
if (!s?.width) return null;
|
|
1939
|
+
const w = Math.round(width);
|
|
1940
|
+
const h3 = Math.round(s.height / s.width * width);
|
|
1941
|
+
const img = await capture({ format: "jpg", quality: 0.5, result: "base64", width: w, height: h3 });
|
|
1942
|
+
return img ? { img: img.replace(/^data:image\/\w+;base64,/, ""), w, h: h3 } : null;
|
|
1943
|
+
},
|
|
1944
|
+
keyboardOpen: () => keyboard,
|
|
1945
|
+
masks: measureMasks
|
|
1946
|
+
};
|
|
1947
|
+
};
|
|
1597
1948
|
var nativeDeps = (overrides = {}) => ({
|
|
1598
1949
|
storage: overrides.storage !== void 0 ? overrides.storage : asyncStorage(),
|
|
1599
1950
|
fetch: ((url, init) => fetch(url, init)),
|
|
@@ -1607,17 +1958,959 @@ var nativeDeps = (overrides = {}) => ({
|
|
|
1607
1958
|
},
|
|
1608
1959
|
initialUrl: () => RN2.Linking.getInitialURL(),
|
|
1609
1960
|
openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
|
|
1610
|
-
push: expoPush()
|
|
1961
|
+
push: expoPush(),
|
|
1962
|
+
recorder: recorderDeps()
|
|
1611
1963
|
});
|
|
1612
1964
|
var optionalModules = () => ({
|
|
1613
1965
|
asyncStorage: !!asyncStorage(),
|
|
1614
1966
|
expoDevice: !!expoDevice,
|
|
1615
1967
|
expoApplication: !!expoApplication,
|
|
1616
1968
|
expoLocalization: !!expoLocalization,
|
|
1617
|
-
expoNotifications: !!expoNotifications
|
|
1969
|
+
expoNotifications: !!expoNotifications,
|
|
1970
|
+
viewShot: !!viewShot?.captureScreen
|
|
1618
1971
|
});
|
|
1619
1972
|
|
|
1973
|
+
// src/native/widgets-view.ts
|
|
1974
|
+
var import_react2 = require("react");
|
|
1975
|
+
var RN3 = require("react-native");
|
|
1976
|
+
var usePalette = (appearance = {}) => {
|
|
1977
|
+
const scheme = RN3.useColorScheme?.();
|
|
1978
|
+
const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
|
|
1979
|
+
return { ...dark ? DARK : LIGHT, accent: appearance.accent ?? "#3f76ff", danger: "#d92d20" };
|
|
1980
|
+
};
|
|
1981
|
+
var kit = (p) => {
|
|
1982
|
+
const text2 = (children, style = {}) => (0, import_react2.createElement)(RN3.Text, { style: { color: p.fg, fontSize: 15, ...style } }, children);
|
|
1983
|
+
const muted = (children, style = {}) => text2(children, { color: p.muted, fontSize: 13, ...style });
|
|
1984
|
+
const title = (children) => (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700", flex: 1 } }, children);
|
|
1985
|
+
const btn = (label, kind, onPress, disabled = false, style = {}) => (0, import_react2.createElement)(
|
|
1986
|
+
RN3.Pressable,
|
|
1987
|
+
{
|
|
1988
|
+
key: label,
|
|
1989
|
+
onPress,
|
|
1990
|
+
disabled,
|
|
1991
|
+
accessibilityRole: "button",
|
|
1992
|
+
accessibilityState: { disabled },
|
|
1993
|
+
style: {
|
|
1994
|
+
height: 44,
|
|
1995
|
+
paddingHorizontal: 16,
|
|
1996
|
+
borderRadius: 10,
|
|
1997
|
+
alignItems: "center",
|
|
1998
|
+
justifyContent: "center",
|
|
1999
|
+
opacity: disabled ? 0.55 : 1,
|
|
2000
|
+
backgroundColor: kind === "primary" ? p.accent : p.bg,
|
|
2001
|
+
borderWidth: kind === "secondary" ? 1 : 0,
|
|
2002
|
+
borderColor: p.line,
|
|
2003
|
+
...style
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
text2(label, { color: kind === "primary" ? "#ffffff" : p.fg, fontWeight: "600" })
|
|
2007
|
+
);
|
|
2008
|
+
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 }));
|
|
2009
|
+
const field = (o) => (0, import_react2.createElement)(RN3.TextInput, {
|
|
2010
|
+
value: o.value,
|
|
2011
|
+
onChangeText: o.onChange,
|
|
2012
|
+
placeholder: o.placeholder,
|
|
2013
|
+
placeholderTextColor: p.muted,
|
|
2014
|
+
multiline: !!o.multiline,
|
|
2015
|
+
maxLength: o.max ?? 2e3,
|
|
2016
|
+
accessibilityLabel: o.placeholder,
|
|
2017
|
+
...o.email ? { keyboardType: "email-address", autoCapitalize: "none", autoComplete: "email", textContentType: "emailAddress" } : {},
|
|
2018
|
+
...o.onSubmit ? { onSubmitEditing: o.onSubmit, returnKeyType: "send" } : {},
|
|
2019
|
+
style: {
|
|
2020
|
+
minHeight: o.multiline ? 72 : 44,
|
|
2021
|
+
borderWidth: 1,
|
|
2022
|
+
borderColor: p.line,
|
|
2023
|
+
borderRadius: 10,
|
|
2024
|
+
paddingHorizontal: 12,
|
|
2025
|
+
paddingVertical: 10,
|
|
2026
|
+
color: p.fg,
|
|
2027
|
+
backgroundColor: p.bg,
|
|
2028
|
+
textAlignVertical: o.multiline ? "top" : "center",
|
|
2029
|
+
...o.style
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
const chip = (label, on, onPress, key = label) => (0, import_react2.createElement)(
|
|
2033
|
+
RN3.Pressable,
|
|
2034
|
+
{
|
|
2035
|
+
key,
|
|
2036
|
+
onPress,
|
|
2037
|
+
accessibilityRole: "button",
|
|
2038
|
+
accessibilityState: { selected: on },
|
|
2039
|
+
style: { paddingHorizontal: 12, height: 32, borderRadius: 16, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
|
|
2040
|
+
},
|
|
2041
|
+
text2(label, { fontSize: 13, fontWeight: "600", color: on ? p.accent : p.fg })
|
|
2042
|
+
);
|
|
2043
|
+
const page = (heading, onClose, children) => (0, import_react2.createElement)(
|
|
2044
|
+
RN3.Modal,
|
|
2045
|
+
{ visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: onClose },
|
|
2046
|
+
(0, import_react2.createElement)(
|
|
2047
|
+
RN3.KeyboardAvoidingView,
|
|
2048
|
+
{ behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
|
|
2049
|
+
(0, import_react2.createElement)(
|
|
2050
|
+
RN3.View,
|
|
2051
|
+
{ style: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: p.line } },
|
|
2052
|
+
title(heading),
|
|
2053
|
+
closeX("Close", onClose)
|
|
2054
|
+
),
|
|
2055
|
+
...children
|
|
2056
|
+
)
|
|
2057
|
+
);
|
|
2058
|
+
return { text: text2, muted, title, btn, closeX, field, chip, page };
|
|
2059
|
+
};
|
|
2060
|
+
var timeAgo = (iso) => {
|
|
2061
|
+
const t = new Date(iso).getTime();
|
|
2062
|
+
if (Number.isNaN(t)) return "";
|
|
2063
|
+
const d = Math.max(0, Date.now() - t) / 1e3;
|
|
2064
|
+
if (d < 60) return "just now";
|
|
2065
|
+
if (d < 3600) return `${Math.floor(d / 60)}m ago`;
|
|
2066
|
+
if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
|
|
2067
|
+
return `${Math.floor(d / 86400)}d ago`;
|
|
2068
|
+
};
|
|
2069
|
+
var opener = () => {
|
|
2070
|
+
let open = false;
|
|
2071
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2072
|
+
const set = (v) => {
|
|
2073
|
+
open = v;
|
|
2074
|
+
listeners.forEach((l) => l(v));
|
|
2075
|
+
};
|
|
2076
|
+
const useOpen = () => {
|
|
2077
|
+
const [v, setV] = (0, import_react2.useState)(open);
|
|
2078
|
+
(0, import_react2.useEffect)(() => {
|
|
2079
|
+
listeners.add(setV);
|
|
2080
|
+
setV(open);
|
|
2081
|
+
return () => {
|
|
2082
|
+
listeners.delete(setV);
|
|
2083
|
+
};
|
|
2084
|
+
}, []);
|
|
2085
|
+
return v;
|
|
2086
|
+
};
|
|
2087
|
+
return { set, get: () => open, useOpen };
|
|
2088
|
+
};
|
|
2089
|
+
var isAnswered = (a) => Array.isArray(a) ? a.length > 0 : typeof a === "number" ? true : !!a?.trim();
|
|
2090
|
+
var SurveySheet = ({ survey, api, p, onDone }) => {
|
|
2091
|
+
const k = kit(p);
|
|
2092
|
+
const [step, setStep] = (0, import_react2.useState)(0);
|
|
2093
|
+
const [answers, setAnswers] = (0, import_react2.useState)({});
|
|
2094
|
+
const [done, setDone] = (0, import_react2.useState)(false);
|
|
2095
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
2096
|
+
const started = (0, import_react2.useRef)(Date.now());
|
|
2097
|
+
const finished = (0, import_react2.useRef)(false);
|
|
2098
|
+
const q = survey.questions[step];
|
|
2099
|
+
const last = step === survey.questions.length - 1;
|
|
2100
|
+
const value = answers[q.key];
|
|
2101
|
+
(0, import_react2.useEffect)(() => {
|
|
2102
|
+
api.seen(survey.id).catch(() => void 0);
|
|
2103
|
+
}, [api, survey.id]);
|
|
2104
|
+
(0, import_react2.useEffect)(() => {
|
|
2105
|
+
if (!done) return;
|
|
2106
|
+
const t = setTimeout(onDone, 2500);
|
|
2107
|
+
return () => clearTimeout(t);
|
|
2108
|
+
}, [done, onDone]);
|
|
2109
|
+
const finish = (completed, all = answers) => {
|
|
2110
|
+
if (finished.current) return;
|
|
2111
|
+
finished.current = true;
|
|
2112
|
+
api.respond(survey.id, { answers: all, completed, seconds: Math.round((Date.now() - started.current) / 1e3) }).catch(() => void 0);
|
|
2113
|
+
};
|
|
2114
|
+
const dismiss = () => {
|
|
2115
|
+
if (Object.values(answers).some(isAnswered)) finish(false);
|
|
2116
|
+
onDone();
|
|
2117
|
+
};
|
|
2118
|
+
const next = () => {
|
|
2119
|
+
if (q.required && !isAnswered(value)) return setError("Please answer this question.");
|
|
2120
|
+
setError("");
|
|
2121
|
+
if (!last) return setStep(step + 1);
|
|
2122
|
+
finish(true);
|
|
2123
|
+
setDone(true);
|
|
2124
|
+
};
|
|
2125
|
+
const pick = (v) => {
|
|
2126
|
+
setAnswers((a) => ({ ...a, [q.key]: v }));
|
|
2127
|
+
setError("");
|
|
2128
|
+
};
|
|
2129
|
+
let input;
|
|
2130
|
+
if (q.kind === "nps" || q.kind === "rating") {
|
|
2131
|
+
const scale = q.kind === "nps" ? 11 : Math.min(Math.max(q.scale ?? 5, 2), 10);
|
|
2132
|
+
const start = q.kind === "nps" ? 0 : 1;
|
|
2133
|
+
input = (0, import_react2.createElement)(
|
|
2134
|
+
RN3.View,
|
|
2135
|
+
{ style: { gap: 6 } },
|
|
2136
|
+
(0, import_react2.createElement)(
|
|
2137
|
+
RN3.View,
|
|
2138
|
+
{ style: { flexDirection: "row", flexWrap: "wrap", gap: 6 }, accessibilityRole: "radiogroup", accessibilityLabel: q.title },
|
|
2139
|
+
...Array.from({ length: scale }, (_, i) => {
|
|
2140
|
+
const n = i + start;
|
|
2141
|
+
const on = value === n;
|
|
2142
|
+
return (0, import_react2.createElement)(
|
|
2143
|
+
RN3.Pressable,
|
|
2144
|
+
{
|
|
2145
|
+
key: n,
|
|
2146
|
+
onPress: () => pick(n),
|
|
2147
|
+
accessibilityRole: "radio",
|
|
2148
|
+
accessibilityState: { checked: on },
|
|
2149
|
+
accessibilityLabel: `${n} of ${scale - 1 + start}`,
|
|
2150
|
+
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 }
|
|
2151
|
+
},
|
|
2152
|
+
k.text(String(n), { color: on ? "#ffffff" : p.fg, fontWeight: "600" })
|
|
2153
|
+
);
|
|
2154
|
+
})
|
|
2155
|
+
),
|
|
2156
|
+
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
|
|
2157
|
+
);
|
|
2158
|
+
} else if (q.kind === "single" || q.kind === "multi") {
|
|
2159
|
+
const multi = q.kind === "multi";
|
|
2160
|
+
const picked = multi ? value ?? [] : [];
|
|
2161
|
+
input = (0, import_react2.createElement)(
|
|
2162
|
+
RN3.View,
|
|
2163
|
+
{ style: { gap: 6 } },
|
|
2164
|
+
...(q.options ?? []).map((o) => {
|
|
2165
|
+
const on = multi ? picked.includes(o) : value === o;
|
|
2166
|
+
return (0, import_react2.createElement)(
|
|
2167
|
+
RN3.Pressable,
|
|
2168
|
+
{
|
|
2169
|
+
key: o,
|
|
2170
|
+
onPress: () => pick(multi ? on ? picked.filter((x) => x !== o) : [...picked, o] : o),
|
|
2171
|
+
accessibilityRole: multi ? "checkbox" : "radio",
|
|
2172
|
+
accessibilityState: { checked: on },
|
|
2173
|
+
style: { minHeight: 44, paddingHorizontal: 12, borderRadius: 10, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
|
|
2174
|
+
},
|
|
2175
|
+
k.text(o, { fontWeight: "500" })
|
|
2176
|
+
);
|
|
2177
|
+
})
|
|
2178
|
+
);
|
|
2179
|
+
} else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? "Type your answer", multiline: true });
|
|
2180
|
+
const body = done ? [k.text(survey.thanks || "Thanks for your feedback.", { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
|
|
2181
|
+
(0, import_react2.createElement)(
|
|
2182
|
+
RN3.View,
|
|
2183
|
+
{ key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
|
|
2184
|
+
...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 } })),
|
|
2185
|
+
(0, import_react2.createElement)(RN3.View, { style: { flex: 1 } }),
|
|
2186
|
+
k.closeX("Close survey", dismiss)
|
|
2187
|
+
),
|
|
2188
|
+
(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")),
|
|
2189
|
+
q.description ? k.muted(q.description) : null,
|
|
2190
|
+
input,
|
|
2191
|
+
error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
|
|
2192
|
+
k.btn(last ? "Send" : "Next", "primary", next)
|
|
2193
|
+
];
|
|
2194
|
+
return (0, import_react2.createElement)(
|
|
2195
|
+
RN3.Modal,
|
|
2196
|
+
{ visible: true, transparent: true, animationType: "slide", onRequestClose: dismiss },
|
|
2197
|
+
(0, import_react2.createElement)(
|
|
2198
|
+
RN3.KeyboardAvoidingView,
|
|
2199
|
+
{ behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: "rgba(0,0,0,0.35)", justifyContent: "flex-end" } },
|
|
2200
|
+
(0, import_react2.createElement)(
|
|
2201
|
+
RN3.View,
|
|
2202
|
+
{
|
|
2203
|
+
accessibilityViewIsModal: true,
|
|
2204
|
+
accessibilityLabel: survey.name,
|
|
2205
|
+
style: { backgroundColor: p.bg, borderTopLeftRadius: 16, borderTopRightRadius: 16, padding: 20, paddingBottom: 34, gap: 12 }
|
|
2206
|
+
},
|
|
2207
|
+
...body
|
|
2208
|
+
)
|
|
2209
|
+
)
|
|
2210
|
+
);
|
|
2211
|
+
};
|
|
2212
|
+
var MRRocketSurveys = ({ client, appearance }) => {
|
|
2213
|
+
const p = usePalette(appearance);
|
|
2214
|
+
const [survey, setSurvey] = (0, import_react2.useState)(null);
|
|
2215
|
+
const [who, setWho] = (0, import_react2.useState)(() => client.getDistinctId());
|
|
2216
|
+
(0, import_react2.useEffect)(() => client.subscribe(() => setWho(client.getDistinctId())), [client]);
|
|
2217
|
+
(0, import_react2.useEffect)(() => {
|
|
2218
|
+
let stop = false;
|
|
2219
|
+
const ctrl = new AbortController();
|
|
2220
|
+
const timer = setTimeout(() => {
|
|
2221
|
+
void client.ready.then(() => {
|
|
2222
|
+
const api = client.api();
|
|
2223
|
+
if (stop || !api) return;
|
|
2224
|
+
api.survey("", ctrl.signal).then((r) => {
|
|
2225
|
+
const s = r.survey;
|
|
2226
|
+
if (stop || !s || !s.questions.length || client.isSurveyDone(s.id)) return;
|
|
2227
|
+
setSurvey({ s, api });
|
|
2228
|
+
}).catch(() => void 0);
|
|
2229
|
+
});
|
|
2230
|
+
}, 3e3);
|
|
2231
|
+
return () => {
|
|
2232
|
+
stop = true;
|
|
2233
|
+
clearTimeout(timer);
|
|
2234
|
+
ctrl.abort();
|
|
2235
|
+
};
|
|
2236
|
+
}, [client, who]);
|
|
2237
|
+
const done = (0, import_react2.useCallback)(() => {
|
|
2238
|
+
setSurvey((cur) => {
|
|
2239
|
+
if (cur) client.markSurveyDone(cur.s.id);
|
|
2240
|
+
return null;
|
|
2241
|
+
});
|
|
2242
|
+
}, [client]);
|
|
2243
|
+
if (!survey) return null;
|
|
2244
|
+
return (0, import_react2.createElement)(SurveySheet, { key: survey.s.id, survey: survey.s, api: survey.api, p, onDone: done });
|
|
2245
|
+
};
|
|
2246
|
+
var feedbackOpen = opener();
|
|
2247
|
+
var useFeedback = () => ({ open: () => feedbackOpen.set(true), close: () => feedbackOpen.set(false) });
|
|
2248
|
+
var FeedbackSheet = ({ client, p, title }) => {
|
|
2249
|
+
const k = kit(p);
|
|
2250
|
+
const [tab, setTab] = (0, import_react2.useState)("top");
|
|
2251
|
+
const [posts, setPosts] = (0, import_react2.useState)(null);
|
|
2252
|
+
const [boards, setBoards] = (0, import_react2.useState)([]);
|
|
2253
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
2254
|
+
const [board, setBoard] = (0, import_react2.useState)("feature");
|
|
2255
|
+
const [subject, setSubject] = (0, import_react2.useState)("");
|
|
2256
|
+
const [details, setDetails] = (0, import_react2.useState)("");
|
|
2257
|
+
const [name, setName] = (0, import_react2.useState)("");
|
|
2258
|
+
const [email, setEmail] = (0, import_react2.useState)("");
|
|
2259
|
+
const [sending, setSending] = (0, import_react2.useState)(false);
|
|
2260
|
+
const [sent, setSent] = (0, import_react2.useState)(false);
|
|
2261
|
+
const close = () => feedbackOpen.set(false);
|
|
2262
|
+
const user = client.getUser();
|
|
2263
|
+
(0, import_react2.useEffect)(() => {
|
|
2264
|
+
const api = client.api();
|
|
2265
|
+
if (!api) return setError("Feedback is off while tracking is off.");
|
|
2266
|
+
const ctrl = new AbortController();
|
|
2267
|
+
api.list({ sort: "top" }, ctrl.signal).then((l) => {
|
|
2268
|
+
setPosts(l.posts);
|
|
2269
|
+
setBoards(l.boards);
|
|
2270
|
+
if (l.boards.length && !l.boards.some((b) => b.id === "feature")) setBoard(l.boards[0].id);
|
|
2271
|
+
}).catch((e) => e.name !== "AbortError" && setError(e.message));
|
|
2272
|
+
return () => ctrl.abort();
|
|
2273
|
+
}, [client]);
|
|
2274
|
+
const vote = (post) => {
|
|
2275
|
+
const api = client.api();
|
|
2276
|
+
if (!api) return;
|
|
2277
|
+
setPosts((all) => all?.map((x) => x.id === post.id ? { ...x, voted: !x.voted, votes: x.votes + (x.voted ? -1 : 1) } : x) ?? null);
|
|
2278
|
+
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) => {
|
|
2279
|
+
setError(e.message);
|
|
2280
|
+
setPosts((all) => all?.map((x) => x.id === post.id ? post : x) ?? null);
|
|
2281
|
+
});
|
|
2282
|
+
};
|
|
2283
|
+
const submit = () => {
|
|
2284
|
+
const api = client.api({ name: name.trim(), email: email.trim() });
|
|
2285
|
+
if (!api || subject.trim().length < 3 || sending) return;
|
|
2286
|
+
setSending(true);
|
|
2287
|
+
setError("");
|
|
2288
|
+
api.submit({ title: subject.trim(), body: details.trim() || void 0, board }).then((post) => {
|
|
2289
|
+
setPosts((all) => [post, ...all ?? []]);
|
|
2290
|
+
setSubject("");
|
|
2291
|
+
setDetails("");
|
|
2292
|
+
setSent(true);
|
|
2293
|
+
}).catch((e) => setError(e.message)).finally(() => setSending(false));
|
|
2294
|
+
};
|
|
2295
|
+
const tabs = (0, import_react2.createElement)(
|
|
2296
|
+
RN3.View,
|
|
2297
|
+
{ style: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingTop: 12 } },
|
|
2298
|
+
k.chip("Top requests", tab === "top", () => setTab("top")),
|
|
2299
|
+
k.chip("Suggest", tab === "new", () => {
|
|
2300
|
+
setTab("new");
|
|
2301
|
+
setSent(false);
|
|
2302
|
+
})
|
|
2303
|
+
);
|
|
2304
|
+
const list = (0, import_react2.createElement)(
|
|
2305
|
+
RN3.ScrollView,
|
|
2306
|
+
{ style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
|
|
2307
|
+
error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
|
|
2308
|
+
posts === null && !error ? k.muted("Loading\u2026") : null,
|
|
2309
|
+
posts?.length === 0 ? k.muted("No requests yet. Be the first to suggest one.") : null,
|
|
2310
|
+
...(posts ?? []).map(
|
|
2311
|
+
(post) => (0, import_react2.createElement)(
|
|
2312
|
+
RN3.View,
|
|
2313
|
+
{ key: post.id, style: { flexDirection: "row", gap: 12, alignItems: "flex-start", paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: p.line } },
|
|
2314
|
+
(0, import_react2.createElement)(
|
|
2315
|
+
RN3.Pressable,
|
|
2316
|
+
{
|
|
2317
|
+
onPress: () => vote(post),
|
|
2318
|
+
accessibilityRole: "button",
|
|
2319
|
+
accessibilityState: { selected: post.voted },
|
|
2320
|
+
accessibilityLabel: `${post.voted ? "Remove your vote from" : "Upvote"} ${post.title}. ${post.votes} votes`,
|
|
2321
|
+
style: { width: 44, minHeight: 48, borderRadius: 10, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: post.voted ? p.accent : p.line }
|
|
2322
|
+
},
|
|
2323
|
+
k.text("\u25B2", { fontSize: 11, color: post.voted ? p.accent : p.muted }),
|
|
2324
|
+
k.text(String(post.votes), { fontWeight: "700", fontSize: 14, color: post.voted ? p.accent : p.fg })
|
|
2325
|
+
),
|
|
2326
|
+
(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)
|
|
2327
|
+
)
|
|
2328
|
+
)
|
|
2329
|
+
);
|
|
2330
|
+
const form = (0, import_react2.createElement)(
|
|
2331
|
+
RN3.ScrollView,
|
|
2332
|
+
{ style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 }, keyboardShouldPersistTaps: "handled" },
|
|
2333
|
+
sent ? k.text("Thanks! Your request is on the board.", { fontWeight: "600" }) : null,
|
|
2334
|
+
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,
|
|
2335
|
+
k.field({ value: subject, onChange: setSubject, placeholder: "Short title", max: 120 }),
|
|
2336
|
+
k.field({ value: details, onChange: setDetails, placeholder: "Details (optional)", multiline: true }),
|
|
2337
|
+
...user?.email ? [] : [
|
|
2338
|
+
k.muted("Your name and email are optional; they let the team follow up."),
|
|
2339
|
+
k.field({ value: name, onChange: setName, placeholder: "Your name (optional)", max: 100 }),
|
|
2340
|
+
k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional)", max: 200, email: true })
|
|
2341
|
+
],
|
|
2342
|
+
error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
|
|
2343
|
+
k.btn(sending ? "Sending\u2026" : "Send request", "primary", submit, subject.trim().length < 3 || sending)
|
|
2344
|
+
);
|
|
2345
|
+
return k.page(title, close, [tabs, tab === "top" ? list : form]);
|
|
2346
|
+
};
|
|
2347
|
+
var MRRocketFeedback = ({ client, appearance, options = {} }) => {
|
|
2348
|
+
const p = usePalette(appearance);
|
|
2349
|
+
const open = feedbackOpen.useOpen();
|
|
2350
|
+
return open ? (0, import_react2.createElement)(FeedbackSheet, { client, p, title: options.title ?? "Feedback" }) : null;
|
|
2351
|
+
};
|
|
2352
|
+
var POLL_OPEN_MS = 4e3;
|
|
2353
|
+
var POLL_WAITING_MS = 8e3;
|
|
2354
|
+
var POLL_IDLE_MS = 3e4;
|
|
2355
|
+
var WAITING_FOR_MS = 10 * 6e4;
|
|
2356
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
2357
|
+
var whoLabel = { visitor: "You", team: "Team", ai: "AI agent", auto: "Team" };
|
|
2358
|
+
var msgView = "home";
|
|
2359
|
+
var messengerOpen = opener();
|
|
2360
|
+
var openAt = (v = "home") => {
|
|
2361
|
+
msgView = v;
|
|
2362
|
+
messengerOpen.set(true);
|
|
2363
|
+
};
|
|
2364
|
+
var useMessenger = () => ({
|
|
2365
|
+
open: (view) => openAt(view),
|
|
2366
|
+
close: () => messengerOpen.set(false),
|
|
2367
|
+
toggle: () => messengerOpen.get() ? messengerOpen.set(false) : openAt()
|
|
2368
|
+
});
|
|
2369
|
+
var haptics = null;
|
|
2370
|
+
try {
|
|
2371
|
+
haptics = require("expo-haptics");
|
|
2372
|
+
} catch {
|
|
2373
|
+
haptics = null;
|
|
2374
|
+
}
|
|
2375
|
+
var buzz = () => {
|
|
2376
|
+
try {
|
|
2377
|
+
void haptics?.notificationAsync?.(haptics.NotificationFeedbackType?.Success)?.catch(() => void 0);
|
|
2378
|
+
} catch {
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
|
|
2382
|
+
var svg = null;
|
|
2383
|
+
try {
|
|
2384
|
+
svg = require("react-native-svg");
|
|
2385
|
+
} catch {
|
|
2386
|
+
svg = null;
|
|
2387
|
+
}
|
|
2388
|
+
var GLYPH = {
|
|
2389
|
+
chat: "M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z",
|
|
2390
|
+
home: "M2.5 7 8 2.5 13.5 7v6a1 1 0 0 1-1 1h-3v-4h-3v4h-3a1 1 0 0 1-1-1z",
|
|
2391
|
+
help: "M8 14.5A6.5 6.5 0 1 0 8 1.5a6.5 6.5 0 0 0 0 13zM6.2 6.2a1.9 1.9 0 1 1 2.6 1.8c-.5.2-.8.6-.8 1.1v.4M8 11.6v.1",
|
|
2392
|
+
send: "M2.5 8h10M8.5 3.5 13 8l-4.5 4.5",
|
|
2393
|
+
up: "M8 13V3M3.5 7.5 8 3l4.5 4.5",
|
|
2394
|
+
next: "M6 3.5 10.5 8 6 12.5",
|
|
2395
|
+
search: "M7 11.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9zM10.5 10.5l3 3",
|
|
2396
|
+
cross: "m4 4 8 8M12 4l-8 8"
|
|
2397
|
+
};
|
|
2398
|
+
var FALLBACK = { chat: "\u{1F4AC}", home: "\u2302", help: "?", send: "\u2192", up: "\u2191", next: "\u203A", search: "\u2315", cross: "\xD7" };
|
|
2399
|
+
var glyph = (name, color, size = 20) => {
|
|
2400
|
+
const Svg = svg?.Svg ?? svg?.default;
|
|
2401
|
+
if (!svg || !Svg) return (0, import_react2.createElement)(RN3.Text, { style: { color, fontSize: Math.round(size * 0.85), lineHeight: size + 2, textAlign: "center", minWidth: size } }, FALLBACK[name]);
|
|
2402
|
+
return (0, import_react2.createElement)(Svg, { width: size, height: size, viewBox: "0 0 16 16", fill: "none" }, (0, import_react2.createElement)(svg.Path, { d: GLYPH[name], stroke: color, strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }));
|
|
2403
|
+
};
|
|
2404
|
+
var alpha = (hex, a) => /^#[0-9a-f]{6}$/i.test(hex) ? `${hex}${Math.round(a * 255).toString(16).padStart(2, "0")}` : hex;
|
|
2405
|
+
var teamMark = (title, bg, fg, size = 36) => (0, import_react2.createElement)(
|
|
2406
|
+
RN3.View,
|
|
2407
|
+
{ style: { width: size, height: size, borderRadius: size / 2, backgroundColor: bg, alignItems: "center", justifyContent: "center" } },
|
|
2408
|
+
(0, import_react2.createElement)(RN3.Text, { style: { color: fg, fontSize: Math.round(size * 0.42), fontWeight: "700" } }, (title.trim()[0] ?? "?").toUpperCase())
|
|
2409
|
+
);
|
|
2410
|
+
var lift = { shadowColor: "#101218", shadowOpacity: 0.07, shadowRadius: 14, shadowOffset: { width: 0, height: 4 }, elevation: 2 };
|
|
2411
|
+
var useChat = (client, o, open, view) => {
|
|
2412
|
+
const [thread, setThread] = (0, import_react2.useState)(null);
|
|
2413
|
+
const [loaded, setLoaded] = (0, import_react2.useState)(false);
|
|
2414
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
2415
|
+
const [unseen, setUnseen] = (0, import_react2.useState)([]);
|
|
2416
|
+
const known = (0, import_react2.useRef)(null);
|
|
2417
|
+
const watching = open && view === "messages";
|
|
2418
|
+
const watch = (0, import_react2.useRef)(watching);
|
|
2419
|
+
watch.current = watching;
|
|
2420
|
+
const [active, setActive] = (0, import_react2.useState)(RN3.AppState.currentState === "active");
|
|
2421
|
+
(0, import_react2.useEffect)(() => {
|
|
2422
|
+
const sub = RN3.AppState.addEventListener("change", (st) => setActive(st === "active"));
|
|
2423
|
+
return () => sub.remove();
|
|
2424
|
+
}, []);
|
|
2425
|
+
const take = (0, import_react2.useCallback)(
|
|
2426
|
+
(t) => {
|
|
2427
|
+
const team = teamOf(t);
|
|
2428
|
+
if (known.current !== null) {
|
|
2429
|
+
const i = team.findIndex((m) => m.id === known.current);
|
|
2430
|
+
const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
|
|
2431
|
+
if (fresh.length) {
|
|
2432
|
+
if (o.haptics !== false) buzz();
|
|
2433
|
+
if (!watch.current) setUnseen((u) => [...u, ...fresh]);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
known.current = team.at(-1)?.id ?? "";
|
|
2437
|
+
setThread(t);
|
|
2438
|
+
},
|
|
2439
|
+
[o.haptics]
|
|
2440
|
+
);
|
|
2441
|
+
(0, import_react2.useEffect)(() => {
|
|
2442
|
+
if (watching) setUnseen([]);
|
|
2443
|
+
}, [watching]);
|
|
2444
|
+
(0, import_react2.useEffect)(() => {
|
|
2445
|
+
if (!active) return;
|
|
2446
|
+
let stop = false;
|
|
2447
|
+
let timer;
|
|
2448
|
+
let ctrl = null;
|
|
2449
|
+
const tick = async () => {
|
|
2450
|
+
const api = client.api();
|
|
2451
|
+
if (!api) {
|
|
2452
|
+
setLoaded(true);
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
ctrl = new AbortController();
|
|
2456
|
+
let t = null;
|
|
2457
|
+
try {
|
|
2458
|
+
await client.ready;
|
|
2459
|
+
t = await api.chat(ctrl.signal);
|
|
2460
|
+
if (stop) return;
|
|
2461
|
+
take(t);
|
|
2462
|
+
setError("");
|
|
2463
|
+
} catch (e) {
|
|
2464
|
+
if (e.name === "AbortError" || stop) return;
|
|
2465
|
+
if (watch.current) setError(e.message);
|
|
2466
|
+
}
|
|
2467
|
+
setLoaded(true);
|
|
2468
|
+
const waiting = !!t?.messages.some((m) => m.from === "visitor") && t?.conversation?.status !== "solved";
|
|
2469
|
+
const wrote = (t?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
|
|
2470
|
+
const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
|
|
2471
|
+
const ms = watch.current ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : waiting || open ? POLL_IDLE_MS : 0;
|
|
2472
|
+
if (!stop && ms) timer = setTimeout(tick, ms);
|
|
2473
|
+
};
|
|
2474
|
+
void tick();
|
|
2475
|
+
return () => {
|
|
2476
|
+
stop = true;
|
|
2477
|
+
ctrl?.abort();
|
|
2478
|
+
clearTimeout(timer);
|
|
2479
|
+
};
|
|
2480
|
+
}, [client, take, active, open, watching]);
|
|
2481
|
+
return { thread, loaded, error, setError, unseen, take };
|
|
2482
|
+
};
|
|
2483
|
+
var useArticles = (client, on, query) => {
|
|
2484
|
+
const [list, setList] = (0, import_react2.useState)(null);
|
|
2485
|
+
const [any, setAny] = (0, import_react2.useState)(null);
|
|
2486
|
+
(0, import_react2.useEffect)(() => {
|
|
2487
|
+
if (!on) return;
|
|
2488
|
+
let stop = false;
|
|
2489
|
+
const t = setTimeout(
|
|
2490
|
+
() => void client.ready.then(
|
|
2491
|
+
() => client.api()?.articles(query.trim() || void 0).then((r) => {
|
|
2492
|
+
if (stop) return;
|
|
2493
|
+
setList(r.articles);
|
|
2494
|
+
if (!query.trim()) setAny(r.articles.length > 0);
|
|
2495
|
+
}).catch(() => !stop && setList((l) => l ?? []))
|
|
2496
|
+
),
|
|
2497
|
+
query ? 250 : 0
|
|
2498
|
+
);
|
|
2499
|
+
return () => {
|
|
2500
|
+
stop = true;
|
|
2501
|
+
clearTimeout(t);
|
|
2502
|
+
};
|
|
2503
|
+
}, [client, on, query]);
|
|
2504
|
+
return { list, any };
|
|
2505
|
+
};
|
|
2506
|
+
var spansOf = (p, spans, base) => spans.map(
|
|
2507
|
+
(s, i) => (0, import_react2.createElement)(
|
|
2508
|
+
RN3.Text,
|
|
2509
|
+
{
|
|
2510
|
+
key: i,
|
|
2511
|
+
onPress: s.href ? () => void RN3.Linking.openURL(s.href).catch(() => void 0) : void 0,
|
|
2512
|
+
accessibilityRole: s.href ? "link" : void 0,
|
|
2513
|
+
style: {
|
|
2514
|
+
...base,
|
|
2515
|
+
...s.bold ? { fontWeight: "700" } : {},
|
|
2516
|
+
...s.italic ? { fontStyle: "italic" } : {},
|
|
2517
|
+
...s.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
|
|
2518
|
+
...s.href ? { color: p.accent, textDecorationLine: "underline" } : {}
|
|
2519
|
+
}
|
|
2520
|
+
},
|
|
2521
|
+
s.text
|
|
2522
|
+
)
|
|
2523
|
+
);
|
|
2524
|
+
var ArticleView = ({ client, p, id, onAsk }) => {
|
|
2525
|
+
const k = kit(p);
|
|
2526
|
+
const [a, setA] = (0, import_react2.useState)(null);
|
|
2527
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
2528
|
+
const [rated, setRated] = (0, import_react2.useState)(false);
|
|
2529
|
+
(0, import_react2.useEffect)(() => {
|
|
2530
|
+
const ctrl = new AbortController();
|
|
2531
|
+
client.api()?.article(id, ctrl.signal).then(setA).catch((e) => e.name !== "AbortError" && setError(e.message));
|
|
2532
|
+
return () => ctrl.abort();
|
|
2533
|
+
}, [client, id]);
|
|
2534
|
+
if (error) return k.text(error, { color: p.danger });
|
|
2535
|
+
if (!a) return k.muted("Loading\u2026");
|
|
2536
|
+
const base = { color: p.fg, fontSize: 15, lineHeight: 22 };
|
|
2537
|
+
const blocks = a.blocks.map((b, i) => {
|
|
2538
|
+
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, {}));
|
|
2539
|
+
if (b.type === "p") return (0, import_react2.createElement)(RN3.Text, { key: i, style: base }, ...spansOf(p, b.spans, {}));
|
|
2540
|
+
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, {})));
|
|
2541
|
+
if (b.type === "ul" || b.type === "ol")
|
|
2542
|
+
return (0, import_react2.createElement)(
|
|
2543
|
+
RN3.View,
|
|
2544
|
+
{ key: i, style: { gap: 4 } },
|
|
2545
|
+
...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, {}))))
|
|
2546
|
+
);
|
|
2547
|
+
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 }));
|
|
2548
|
+
return (0, import_react2.createElement)(RN3.View, { key: i, style: { height: 1, backgroundColor: p.line } });
|
|
2549
|
+
});
|
|
2550
|
+
const rate = (helpful) => {
|
|
2551
|
+
setRated(true);
|
|
2552
|
+
client.api()?.rateArticle(id, helpful).catch(() => void 0);
|
|
2553
|
+
if (!helpful) onAsk();
|
|
2554
|
+
};
|
|
2555
|
+
return (0, import_react2.createElement)(
|
|
2556
|
+
RN3.View,
|
|
2557
|
+
{ style: { gap: 10 } },
|
|
2558
|
+
k.muted(a.collection),
|
|
2559
|
+
(0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", style: { color: p.fg, fontSize: 20, fontWeight: "700" } }, a.title),
|
|
2560
|
+
...blocks,
|
|
2561
|
+
(0, import_react2.createElement)(
|
|
2562
|
+
RN3.View,
|
|
2563
|
+
{ style: { flexDirection: "row", alignItems: "center", gap: 8, padding: 12, borderRadius: 10, backgroundColor: p.soft, marginTop: 8 } },
|
|
2564
|
+
rated ? k.muted("Thanks for letting us know.") : k.muted("Did this answer your question?", { flex: 1 }),
|
|
2565
|
+
rated ? null : k.btn("Yes", "secondary", () => rate(true), false, { height: 34 }),
|
|
2566
|
+
rated ? null : k.btn("No", "secondary", () => rate(false), false, { height: 34 })
|
|
2567
|
+
),
|
|
2568
|
+
k.btn("Still need help? Send us a message", "secondary", onAsk)
|
|
2569
|
+
);
|
|
2570
|
+
};
|
|
2571
|
+
var MessengerSheet = ({
|
|
2572
|
+
client,
|
|
2573
|
+
p,
|
|
2574
|
+
o,
|
|
2575
|
+
chat,
|
|
2576
|
+
view,
|
|
2577
|
+
setView
|
|
2578
|
+
}) => {
|
|
2579
|
+
const k = kit(p);
|
|
2580
|
+
const [body, setBody] = (0, import_react2.useState)("");
|
|
2581
|
+
const [email, setEmail] = (0, import_react2.useState)("");
|
|
2582
|
+
const [pending, setPending] = (0, import_react2.useState)(null);
|
|
2583
|
+
const [query, setQuery] = (0, import_react2.useState)("");
|
|
2584
|
+
const helpOn = o.help !== false;
|
|
2585
|
+
const arts = useArticles(client, helpOn, query);
|
|
2586
|
+
const scroll = (0, import_react2.useRef)(null);
|
|
2587
|
+
const close = () => messengerOpen.set(false);
|
|
2588
|
+
const greeting = o.greeting ?? "Hi! How can we help?";
|
|
2589
|
+
const { thread } = chat;
|
|
2590
|
+
const count = (thread?.messages.length ?? 0) + (pending ? 1 : 0);
|
|
2591
|
+
(0, import_react2.useEffect)(() => {
|
|
2592
|
+
if (view === "messages") setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 50);
|
|
2593
|
+
}, [count, view]);
|
|
2594
|
+
const needEmail = (o.askEmail ?? true) && !thread?.hasEmail && !client.getUser()?.email;
|
|
2595
|
+
const emailOk = !email.trim() || EMAIL.test(email.trim());
|
|
2596
|
+
const send = async () => {
|
|
2597
|
+
const text2 = body.trim();
|
|
2598
|
+
const api = client.api();
|
|
2599
|
+
if (!text2 || pending || !emailOk || !api) return;
|
|
2600
|
+
setPending({ id: "pending", from: "visitor", body: text2, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2601
|
+
setBody("");
|
|
2602
|
+
chat.setError("");
|
|
2603
|
+
try {
|
|
2604
|
+
chat.take(await api.sendChat({ body: text2, email: needEmail && email.trim() ? email.trim() : void 0 }));
|
|
2605
|
+
} catch (e) {
|
|
2606
|
+
setBody(text2);
|
|
2607
|
+
chat.setError(e.message);
|
|
2608
|
+
} finally {
|
|
2609
|
+
setPending(null);
|
|
2610
|
+
}
|
|
2611
|
+
};
|
|
2612
|
+
const ink = inkOn(p.accent);
|
|
2613
|
+
const dark = p.bg === DARK.bg;
|
|
2614
|
+
const canvas = dark ? p.bg : p.soft;
|
|
2615
|
+
const raised = dark ? p.soft : p.bg;
|
|
2616
|
+
const title = o.title ?? "Chat with us";
|
|
2617
|
+
const reply = replyLine(o.replyTime);
|
|
2618
|
+
const bubble = (m, isPending = false) => {
|
|
2619
|
+
const mine = m.from === "visitor";
|
|
2620
|
+
return (0, import_react2.createElement)(
|
|
2621
|
+
RN3.View,
|
|
2622
|
+
{ key: m.id, style: { alignSelf: mine ? "flex-end" : "flex-start", maxWidth: "82%", gap: 4 } },
|
|
2623
|
+
(0, import_react2.createElement)(
|
|
2624
|
+
RN3.View,
|
|
2625
|
+
{
|
|
2626
|
+
style: {
|
|
2627
|
+
backgroundColor: mine ? p.accent : dark ? p.soft : "#eeeff1",
|
|
2628
|
+
borderRadius: 18,
|
|
2629
|
+
borderBottomRightRadius: mine ? 6 : 18,
|
|
2630
|
+
borderBottomLeftRadius: mine ? 18 : 6,
|
|
2631
|
+
paddingHorizontal: 14,
|
|
2632
|
+
paddingVertical: 9,
|
|
2633
|
+
opacity: isPending ? 0.7 : 1
|
|
2634
|
+
}
|
|
2635
|
+
},
|
|
2636
|
+
k.text(m.body, { color: mine ? ink : p.fg })
|
|
2637
|
+
),
|
|
2638
|
+
...(m.articles ?? []).map(
|
|
2639
|
+
(a) => (0, import_react2.createElement)(
|
|
2640
|
+
RN3.Pressable,
|
|
2641
|
+
{ key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "link", style: { borderWidth: 1, borderColor: p.line, borderRadius: 12, paddingHorizontal: 10, paddingVertical: 7 } },
|
|
2642
|
+
k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
|
|
2643
|
+
)
|
|
2644
|
+
),
|
|
2645
|
+
m.at ? k.muted(`${mine ? "" : `${whoLabel[m.from]} \xB7 `}${isPending ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left", paddingHorizontal: 6 }) : null
|
|
2646
|
+
);
|
|
2647
|
+
};
|
|
2648
|
+
const row = (a) => (0, import_react2.createElement)(
|
|
2649
|
+
RN3.Pressable,
|
|
2650
|
+
{ key: a.id, onPress: () => setView({ article: a.id }), accessibilityRole: "button", style: { flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 11, paddingHorizontal: 8 } },
|
|
2651
|
+
k.text(a.title, { flex: 1 }),
|
|
2652
|
+
glyph("next", p.accent, 14)
|
|
2653
|
+
);
|
|
2654
|
+
const card = (children, style = {}) => (0, import_react2.createElement)(RN3.View, { style: { backgroundColor: raised, borderRadius: 16, padding: 16, ...dark ? { borderWidth: 1, borderColor: p.line } : lift, ...style } }, ...children);
|
|
2655
|
+
const searchRow = (onPress) => (0, import_react2.createElement)(
|
|
2656
|
+
RN3.Pressable,
|
|
2657
|
+
{ key: "s", onPress, accessibilityRole: "search", style: { flexDirection: "row", alignItems: "center", height: 44, borderRadius: 12, paddingHorizontal: 14, backgroundColor: canvas } },
|
|
2658
|
+
k.muted("Search for help", { flex: 1, fontSize: 15 }),
|
|
2659
|
+
glyph("search", p.accent, 16)
|
|
2660
|
+
);
|
|
2661
|
+
const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200, style: { borderWidth: 0, borderRadius: 12, backgroundColor: raised, ...dark ? {} : lift } });
|
|
2662
|
+
const last = thread?.messages.at(-1);
|
|
2663
|
+
const showHelp = helpOn && arts.any !== false;
|
|
2664
|
+
let content;
|
|
2665
|
+
if (view === "home")
|
|
2666
|
+
content = (0, import_react2.createElement)(
|
|
2667
|
+
RN3.ScrollView,
|
|
2668
|
+
{ key: "home", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingBottom: 24 }, keyboardShouldPersistTaps: "handled" },
|
|
2669
|
+
(0, import_react2.createElement)(RN3.View, { style: { backgroundColor: p.accent, paddingHorizontal: 22, paddingTop: 4, paddingBottom: 64 } }, (0, import_react2.createElement)(RN3.Text, { style: { color: ink, fontSize: 28, fontWeight: "700", letterSpacing: -0.4, lineHeight: 34 } }, greeting)),
|
|
2670
|
+
(0, import_react2.createElement)(
|
|
2671
|
+
RN3.View,
|
|
2672
|
+
{ style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } },
|
|
2673
|
+
last ? (0, import_react2.createElement)(
|
|
2674
|
+
RN3.Pressable,
|
|
2675
|
+
{ key: "recent", onPress: () => setView("messages"), accessibilityRole: "button" },
|
|
2676
|
+
card([
|
|
2677
|
+
k.muted("Recent message", { fontSize: 12, fontWeight: "600", marginBottom: 10 }),
|
|
2678
|
+
(0, import_react2.createElement)(
|
|
2679
|
+
RN3.View,
|
|
2680
|
+
{ key: "r", style: { flexDirection: "row", alignItems: "center", gap: 12 } },
|
|
2681
|
+
teamMark(title, p.accent, ink),
|
|
2682
|
+
(0, import_react2.createElement)(RN3.View, { style: { flex: 1, gap: 2 } }, (0, import_react2.createElement)(RN3.Text, { numberOfLines: 1, style: { color: p.fg, fontSize: 15 } }, last.body), k.muted(`${last.from === "visitor" ? "You" : whoLabel[last.from]} \xB7 ${timeAgo(last.at)}`, { fontSize: 12 })),
|
|
2683
|
+
chat.unseen.length ? (0, import_react2.createElement)(RN3.View, { style: { minWidth: 20, height: 20, borderRadius: 10, paddingHorizontal: 6, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(chat.unseen.length), { color: "#fff", fontSize: 12, fontWeight: "700" })) : glyph("next", p.muted, 14)
|
|
2684
|
+
)
|
|
2685
|
+
])
|
|
2686
|
+
) : null,
|
|
2687
|
+
(0, import_react2.createElement)(
|
|
2688
|
+
RN3.Pressable,
|
|
2689
|
+
{ key: "send", onPress: () => setView("messages"), accessibilityRole: "button" },
|
|
2690
|
+
card(
|
|
2691
|
+
[
|
|
2692
|
+
(0, import_react2.createElement)(RN3.View, { key: "t", style: { flex: 1, gap: 2 } }, k.text("Send us a message", { fontWeight: "700", fontSize: 16 }), k.muted(reply)),
|
|
2693
|
+
(0, import_react2.createElement)(RN3.View, { key: "g", style: { width: 36, height: 36, borderRadius: 18, backgroundColor: alpha(p.accent, 0.12), alignItems: "center", justifyContent: "center" } }, glyph("send", p.accent, 18))
|
|
2694
|
+
],
|
|
2695
|
+
{ flexDirection: "row", alignItems: "center", gap: 12 }
|
|
2696
|
+
)
|
|
2697
|
+
),
|
|
2698
|
+
showHelp ? card([searchRow(() => setView("help")), ...(arts.list ?? []).slice(0, 4).map(row)], { padding: 8, gap: 2 }) : null
|
|
2699
|
+
)
|
|
2700
|
+
);
|
|
2701
|
+
else if (view === "messages")
|
|
2702
|
+
content = (0, import_react2.createElement)(
|
|
2703
|
+
RN3.View,
|
|
2704
|
+
{ key: "messages", style: { flex: 1, backgroundColor: p.bg } },
|
|
2705
|
+
(0, import_react2.createElement)(
|
|
2706
|
+
RN3.ScrollView,
|
|
2707
|
+
{ ref: scroll, style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 12 } },
|
|
2708
|
+
bubble({ id: "greeting", from: "team", body: greeting, at: "" }),
|
|
2709
|
+
!chat.loaded ? k.muted("Loading\u2026") : null,
|
|
2710
|
+
...(thread?.messages ?? []).map((m) => bubble(m)),
|
|
2711
|
+
pending ? bubble(pending, true) : null,
|
|
2712
|
+
thread?.conversation?.status === "solved" && !pending ? k.muted("This conversation is closed. Write to start a new one.", { textAlign: "center" }) : null
|
|
2713
|
+
),
|
|
2714
|
+
(0, import_react2.createElement)(
|
|
2715
|
+
RN3.View,
|
|
2716
|
+
{ style: { paddingHorizontal: 12, paddingTop: 8, paddingBottom: 10, gap: 8 } },
|
|
2717
|
+
chat.error ? k.text(chat.error, { color: p.danger, fontSize: 12 }) : null,
|
|
2718
|
+
needEmail ? k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional), so we can follow up", email: true, max: 200, style: { borderWidth: emailOk ? 0 : 1, borderColor: p.danger, borderRadius: 12, backgroundColor: canvas } }) : null,
|
|
2719
|
+
(0, import_react2.createElement)(
|
|
2720
|
+
RN3.View,
|
|
2721
|
+
{ style: { flexDirection: "row", alignItems: "flex-end", gap: 6, borderWidth: 1, borderColor: p.line, borderRadius: 22, paddingVertical: 5, paddingRight: 5, paddingLeft: 16, backgroundColor: p.bg } },
|
|
2722
|
+
k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { flex: 1, minHeight: 34, maxHeight: 120, borderWidth: 0, paddingHorizontal: 0, paddingVertical: 7, backgroundColor: "transparent" } }),
|
|
2723
|
+
(0, import_react2.createElement)(
|
|
2724
|
+
RN3.Pressable,
|
|
2725
|
+
{
|
|
2726
|
+
onPress: () => void send(),
|
|
2727
|
+
disabled: !body.trim() || !!pending || !emailOk,
|
|
2728
|
+
accessibilityRole: "button",
|
|
2729
|
+
accessibilityLabel: "Send",
|
|
2730
|
+
style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center", backgroundColor: !body.trim() || !!pending || !emailOk ? p.line : p.accent }
|
|
2731
|
+
},
|
|
2732
|
+
glyph("up", !body.trim() || !!pending || !emailOk ? p.muted : ink, 18)
|
|
2733
|
+
)
|
|
2734
|
+
)
|
|
2735
|
+
)
|
|
2736
|
+
);
|
|
2737
|
+
else if (view === "help") {
|
|
2738
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2739
|
+
for (const a of arts.list ?? []) groups.set(a.collection, [...groups.get(a.collection) ?? [], a]);
|
|
2740
|
+
content = (0, import_react2.createElement)(
|
|
2741
|
+
RN3.ScrollView,
|
|
2742
|
+
{ key: "help", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { padding: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
|
|
2743
|
+
search,
|
|
2744
|
+
arts.list === null ? k.muted("Loading\u2026") : null,
|
|
2745
|
+
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,
|
|
2746
|
+
...query.trim() ? arts.list?.length ? [card((arts.list ?? []).map(row), { padding: 8 })] : [] : [...groups].map(([name, items]) => (0, import_react2.createElement)(RN3.View, { key: name, style: { gap: 6 } }, k.muted(`${name} \xB7 ${items.length}`, { fontSize: 12, fontWeight: "600", paddingHorizontal: 2 }), card(items.map(row), { padding: 8 })))
|
|
2747
|
+
);
|
|
2748
|
+
} else
|
|
2749
|
+
content = (0, import_react2.createElement)(
|
|
2750
|
+
RN3.ScrollView,
|
|
2751
|
+
{ key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { padding: 16, paddingBottom: 32 } },
|
|
2752
|
+
(0, import_react2.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
|
|
2753
|
+
);
|
|
2754
|
+
const current = typeof view === "string" ? view : "help";
|
|
2755
|
+
const tab = (v, name, icon, badge = 0) => (0, import_react2.createElement)(
|
|
2756
|
+
RN3.Pressable,
|
|
2757
|
+
{ key: v, onPress: () => setView(v), accessibilityRole: "tab", accessibilityState: { selected: current === v }, style: { flex: 1, alignItems: "center", paddingTop: 8, paddingBottom: 6, gap: 3 } },
|
|
2758
|
+
glyph(icon, current === v ? p.accent : p.muted, 22),
|
|
2759
|
+
k.text(name, { fontSize: 12, fontWeight: "600", color: current === v ? p.accent : p.muted }),
|
|
2760
|
+
badge ? (0, import_react2.createElement)(RN3.View, { style: { position: "absolute", top: 4, right: "28%", 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
|
|
2761
|
+
);
|
|
2762
|
+
const onHome = view === "home";
|
|
2763
|
+
const headFg = onHome ? ink : p.fg;
|
|
2764
|
+
const head = (0, import_react2.createElement)(
|
|
2765
|
+
RN3.View,
|
|
2766
|
+
{ key: "head", style: { flexDirection: "row", alignItems: "center", gap: 10, paddingLeft: 16, paddingRight: 12, paddingTop: 16, paddingBottom: 12, backgroundColor: onHome ? p.accent : p.bg, borderBottomWidth: onHome ? 0 : 1, borderBottomColor: p.line } },
|
|
2767
|
+
typeof view !== "string" ? (0, import_react2.createElement)(RN3.Pressable, { key: "back", onPress: () => setView("help"), accessibilityRole: "button", hitSlop: 10 }, k.text("\u2039 All articles", { color: p.accent, fontWeight: "600" })) : view === "help" ? null : teamMark(title, onHome ? ink : p.accent, onHome ? p.accent : ink, onHome ? 28 : 36),
|
|
2768
|
+
(0, import_react2.createElement)(
|
|
2769
|
+
RN3.View,
|
|
2770
|
+
{ style: { flex: 1, minWidth: 0 } },
|
|
2771
|
+
typeof view !== "string" ? null : (0, import_react2.createElement)(RN3.Text, { accessibilityRole: "header", numberOfLines: 1, style: { color: headFg, fontSize: 17, fontWeight: "700" } }, view === "help" ? "Help" : title),
|
|
2772
|
+
view === "messages" ? k.muted(reply, { fontSize: 12 }) : null
|
|
2773
|
+
),
|
|
2774
|
+
(0, import_react2.createElement)(RN3.Pressable, { onPress: close, accessibilityRole: "button", accessibilityLabel: "Close", hitSlop: 12, style: { width: 32, height: 32, borderRadius: 10, alignItems: "center", justifyContent: "center" } }, glyph("cross", onHome ? ink : p.muted, 18))
|
|
2775
|
+
);
|
|
2776
|
+
return (0, import_react2.createElement)(
|
|
2777
|
+
RN3.Modal,
|
|
2778
|
+
{ visible: true, animationType: "slide", presentationStyle: "pageSheet", onRequestClose: close },
|
|
2779
|
+
(0, import_react2.createElement)(
|
|
2780
|
+
RN3.KeyboardAvoidingView,
|
|
2781
|
+
{ behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: p.bg } },
|
|
2782
|
+
head,
|
|
2783
|
+
content,
|
|
2784
|
+
(0, import_react2.createElement)(
|
|
2785
|
+
RN3.View,
|
|
2786
|
+
{ key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", borderTopWidth: 1, borderTopColor: p.line, paddingHorizontal: 8, paddingBottom: 22, backgroundColor: p.bg } },
|
|
2787
|
+
tab("home", "Home", "home"),
|
|
2788
|
+
tab("messages", "Messages", "chat", view === "messages" ? 0 : chat.unseen.length),
|
|
2789
|
+
showHelp ? tab("help", "Help", "help") : null
|
|
2790
|
+
)
|
|
2791
|
+
)
|
|
2792
|
+
);
|
|
2793
|
+
};
|
|
2794
|
+
var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
|
|
2795
|
+
const [look, setLook] = (0, import_react2.useState)(null);
|
|
2796
|
+
(0, import_react2.useEffect)(() => {
|
|
2797
|
+
const ctrl = new AbortController();
|
|
2798
|
+
void client.ready.then(
|
|
2799
|
+
() => client.api()?.messengerLook(ctrl.signal).then(setLook).catch(() => void 0)
|
|
2800
|
+
);
|
|
2801
|
+
return () => ctrl.abort();
|
|
2802
|
+
}, [client]);
|
|
2803
|
+
const defined = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== void 0));
|
|
2804
|
+
const options = look ? {
|
|
2805
|
+
title: look.title,
|
|
2806
|
+
greeting: look.greeting,
|
|
2807
|
+
replyTime: look.replyTime,
|
|
2808
|
+
launcher: look.appLauncher ?? false,
|
|
2809
|
+
position: look.position,
|
|
2810
|
+
label: look.label,
|
|
2811
|
+
help: look.help,
|
|
2812
|
+
preview: look.preview,
|
|
2813
|
+
askEmail: look.askEmail,
|
|
2814
|
+
haptics: look.sound,
|
|
2815
|
+
...defined
|
|
2816
|
+
} : given;
|
|
2817
|
+
const p = usePalette({ theme: appearance?.theme ?? look?.theme, accent: appearance?.accent ?? look?.accent });
|
|
2818
|
+
const k = kit(p);
|
|
2819
|
+
const open = messengerOpen.useOpen();
|
|
2820
|
+
const [view, setViewState] = (0, import_react2.useState)(msgView);
|
|
2821
|
+
const [banner, setBanner] = (0, import_react2.useState)(null);
|
|
2822
|
+
(0, import_react2.useEffect)(() => {
|
|
2823
|
+
if (open) setViewState(msgView);
|
|
2824
|
+
}, [open]);
|
|
2825
|
+
const setView = (v) => {
|
|
2826
|
+
msgView = v;
|
|
2827
|
+
setViewState(v);
|
|
2828
|
+
};
|
|
2829
|
+
const chat = useChat(client, options, open, view);
|
|
2830
|
+
const latest = chat.unseen.at(-1);
|
|
2831
|
+
(0, import_react2.useEffect)(() => {
|
|
2832
|
+
if (!latest || open || options.preview === false) return;
|
|
2833
|
+
setBanner(latest);
|
|
2834
|
+
const t = setTimeout(() => setBanner(null), 6e3);
|
|
2835
|
+
return () => clearTimeout(t);
|
|
2836
|
+
}, [latest, open, options.preview]);
|
|
2837
|
+
(0, import_react2.useEffect)(() => {
|
|
2838
|
+
if (open) setBanner(null);
|
|
2839
|
+
}, [open]);
|
|
2840
|
+
const unseen = chat.unseen.length;
|
|
2841
|
+
const ink = inkOn(p.accent);
|
|
2842
|
+
const label = options.label?.trim() ?? "";
|
|
2843
|
+
const side = options.position === "bottom-left" ? { left: 20 } : { right: 20 };
|
|
2844
|
+
const launcher = options.launcher ? (0, import_react2.createElement)(
|
|
2845
|
+
RN3.Pressable,
|
|
2846
|
+
{
|
|
2847
|
+
key: "launcher",
|
|
2848
|
+
onPress: () => openAt(unseen ? "messages" : "home"),
|
|
2849
|
+
accessibilityRole: "button",
|
|
2850
|
+
accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
|
|
2851
|
+
style: {
|
|
2852
|
+
position: "absolute",
|
|
2853
|
+
...side,
|
|
2854
|
+
bottom: 34,
|
|
2855
|
+
height: label ? 50 : 56,
|
|
2856
|
+
minWidth: label ? 0 : 56,
|
|
2857
|
+
paddingHorizontal: label ? 18 : 0,
|
|
2858
|
+
borderRadius: 28,
|
|
2859
|
+
flexDirection: "row",
|
|
2860
|
+
gap: 8,
|
|
2861
|
+
backgroundColor: p.accent,
|
|
2862
|
+
alignItems: "center",
|
|
2863
|
+
justifyContent: "center",
|
|
2864
|
+
shadowColor: p.accent,
|
|
2865
|
+
shadowOpacity: 0.4,
|
|
2866
|
+
shadowRadius: 12,
|
|
2867
|
+
shadowOffset: { width: 0, height: 6 },
|
|
2868
|
+
elevation: 6
|
|
2869
|
+
}
|
|
2870
|
+
},
|
|
2871
|
+
glyph("chat", ink, label ? 20 : 26),
|
|
2872
|
+
label ? k.text(label, { color: ink, fontWeight: "700" }) : null,
|
|
2873
|
+
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
|
|
2874
|
+
) : null;
|
|
2875
|
+
const toast = banner ? (0, import_react2.createElement)(
|
|
2876
|
+
RN3.Pressable,
|
|
2877
|
+
{
|
|
2878
|
+
key: "banner",
|
|
2879
|
+
onPress: () => openAt("messages"),
|
|
2880
|
+
accessibilityRole: "button",
|
|
2881
|
+
accessibilityLiveRegion: "polite",
|
|
2882
|
+
style: { position: "absolute", top: 54, left: 16, right: 16, flexDirection: "row", alignItems: "flex-start", gap: 12, backgroundColor: p.bg, borderRadius: 18, padding: 14, borderWidth: 1, borderColor: p.line, shadowColor: "#000", shadowOpacity: 0.16, shadowRadius: 18, shadowOffset: { width: 0, height: 8 }, elevation: 8 }
|
|
2883
|
+
},
|
|
2884
|
+
teamMark(options.title ?? "Chat with us", p.accent, ink),
|
|
2885
|
+
(0, import_react2.createElement)(
|
|
2886
|
+
RN3.View,
|
|
2887
|
+
{ style: { flex: 1, gap: 2 } },
|
|
2888
|
+
k.muted(`${whoLabel[banner.from]}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
|
|
2889
|
+
(0, import_react2.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15 } }, banner.body)
|
|
2890
|
+
)
|
|
2891
|
+
) : null;
|
|
2892
|
+
if (!launcher && !toast && !open) return null;
|
|
2893
|
+
return (0, import_react2.createElement)(
|
|
2894
|
+
RN3.View,
|
|
2895
|
+
{ pointerEvents: "box-none", style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 } },
|
|
2896
|
+
launcher,
|
|
2897
|
+
toast,
|
|
2898
|
+
open ? (0, import_react2.createElement)(MessengerSheet, { client, p, o: options, chat, view, setView }) : null
|
|
2899
|
+
);
|
|
2900
|
+
};
|
|
2901
|
+
|
|
1620
2902
|
// src/native/index.tsx
|
|
2903
|
+
var RN4 = require("react-native");
|
|
2904
|
+
var MRRocketMask = ({ children, style }) => {
|
|
2905
|
+
const ref = (0, import_react3.useRef)(null);
|
|
2906
|
+
(0, import_react3.useEffect)(() => {
|
|
2907
|
+
maskedViews.add(ref);
|
|
2908
|
+
return () => {
|
|
2909
|
+
maskedViews.delete(ref);
|
|
2910
|
+
};
|
|
2911
|
+
}, []);
|
|
2912
|
+
return (0, import_react3.createElement)(RN4.View, { ref, collapsable: false, style }, children);
|
|
2913
|
+
};
|
|
1621
2914
|
var shared = null;
|
|
1622
2915
|
var sharedKey = "";
|
|
1623
2916
|
var createMRRocket = (options) => {
|
|
@@ -1633,20 +2926,40 @@ var createMRRocket = (options) => {
|
|
|
1633
2926
|
return shared;
|
|
1634
2927
|
};
|
|
1635
2928
|
var getMRRocket = () => shared;
|
|
1636
|
-
var Ctx = (0,
|
|
1637
|
-
var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, ...options }) => {
|
|
1638
|
-
const [client] = (0,
|
|
1639
|
-
(0,
|
|
2929
|
+
var Ctx = (0, import_react3.createContext)(null);
|
|
2930
|
+
var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, surveys = true, messenger = {}, feedback = {}, heatmaps = true, ...options }) => {
|
|
2931
|
+
const [client] = (0, import_react3.useState)(() => given ?? createMRRocket({ ...options, consent }));
|
|
2932
|
+
(0, import_react3.useEffect)(() => {
|
|
1640
2933
|
if (consent) client.setConsent(consent);
|
|
1641
2934
|
}, [client, consent]);
|
|
1642
|
-
|
|
2935
|
+
(0, import_react3.useEffect)(() => {
|
|
2936
|
+
if (options.replay !== void 0) client.setReplay(options.replay);
|
|
2937
|
+
}, [client, options.replay]);
|
|
2938
|
+
const start = (0, import_react3.useRef)(null);
|
|
2939
|
+
const touch = (e, end) => {
|
|
2940
|
+
const n = e.nativeEvent;
|
|
2941
|
+
const x = n.pageX ?? n.changedTouches?.[0]?.pageX;
|
|
2942
|
+
const y = n.pageY ?? n.changedTouches?.[0]?.pageY;
|
|
2943
|
+
if (x === void 0 || y === void 0) return;
|
|
2944
|
+
if (!end) start.current = { x, y, t: Date.now() };
|
|
2945
|
+
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);
|
|
2946
|
+
};
|
|
2947
|
+
return (0, import_react3.createElement)(
|
|
2948
|
+
Ctx.Provider,
|
|
2949
|
+
{ value: client },
|
|
2950
|
+
heatmaps ? (0, import_react3.createElement)(RN4.View, { style: { flex: 1 }, onTouchStartCapture: (e) => touch(e, false), onTouchEndCapture: (e) => touch(e, true) }, children) : children,
|
|
2951
|
+
flows ? (0, import_react3.createElement)(MRRocketFlows, { flows: client.flows, appearance }) : null,
|
|
2952
|
+
surveys ? (0, import_react3.createElement)(MRRocketSurveys, { client, appearance }) : null,
|
|
2953
|
+
feedback ? (0, import_react3.createElement)(MRRocketFeedback, { client, appearance, options: feedback }) : null,
|
|
2954
|
+
messenger ? (0, import_react3.createElement)(MRRocketMessenger, { client, appearance, options: messenger }) : null
|
|
2955
|
+
);
|
|
1643
2956
|
};
|
|
1644
2957
|
var useClient = () => {
|
|
1645
|
-
const c = (0,
|
|
2958
|
+
const c = (0, import_react3.useContext)(Ctx);
|
|
1646
2959
|
if (!c) throw new Error("MRRocket: wrap your app in <MRRocketProvider apiKey=\u2026>.");
|
|
1647
2960
|
return c;
|
|
1648
2961
|
};
|
|
1649
|
-
var useVersion = (client) => (0,
|
|
2962
|
+
var useVersion = (client) => (0, import_react3.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
|
|
1650
2963
|
var useMRRocket = () => {
|
|
1651
2964
|
const client = useClient();
|
|
1652
2965
|
useVersion(client);
|
|
@@ -1656,6 +2969,7 @@ var useMRRocket = () => {
|
|
|
1656
2969
|
identify: client.identify,
|
|
1657
2970
|
reset: client.reset,
|
|
1658
2971
|
setConsent: client.setConsent,
|
|
2972
|
+
setReplay: client.setReplay,
|
|
1659
2973
|
captureError: client.captureError,
|
|
1660
2974
|
flush: client.flush,
|
|
1661
2975
|
distinctId: client.getDistinctId(),
|
|
@@ -1690,7 +3004,7 @@ var useVariant = (key) => {
|
|
|
1690
3004
|
useVersion(client);
|
|
1691
3005
|
const config = client.getConfig();
|
|
1692
3006
|
const variant = client.peekVariant(key);
|
|
1693
|
-
(0,
|
|
3007
|
+
(0, import_react3.useEffect)(() => {
|
|
1694
3008
|
if (config) client.expose(key, variant);
|
|
1695
3009
|
}, [client, config, key, variant]);
|
|
1696
3010
|
return variant;
|
|
@@ -1701,7 +3015,7 @@ var useText = (id, fallback) => {
|
|
|
1701
3015
|
const config = client.getConfig();
|
|
1702
3016
|
const text2 = client.peekText(id);
|
|
1703
3017
|
const person = client.getDistinctId();
|
|
1704
|
-
(0,
|
|
3018
|
+
(0, import_react3.useEffect)(() => {
|
|
1705
3019
|
if (config) client.exposeText(id);
|
|
1706
3020
|
}, [client, config, id, person]);
|
|
1707
3021
|
return text2 ?? fallback;
|
|
@@ -1726,13 +3040,13 @@ var useExpoRouterTracking = () => {
|
|
|
1726
3040
|
console.warn("[MRRocket] useExpoRouterTracking needs expo-router. With React Navigation use useNavigationTracking.");
|
|
1727
3041
|
}
|
|
1728
3042
|
const pathname = (router?.usePathname ?? noPathname)();
|
|
1729
|
-
(0,
|
|
3043
|
+
(0, import_react3.useEffect)(() => {
|
|
1730
3044
|
if (pathname) client.screen(pathname);
|
|
1731
3045
|
}, [client, pathname]);
|
|
1732
3046
|
};
|
|
1733
3047
|
var useNavigationTracking = () => {
|
|
1734
3048
|
const client = useClient();
|
|
1735
|
-
return (0,
|
|
3049
|
+
return (0, import_react3.useCallback)(
|
|
1736
3050
|
(state) => {
|
|
1737
3051
|
const name = activeRouteName(state);
|
|
1738
3052
|
if (name) client.screen(name);
|