@mrrocket/sdk 0.8.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 +13 -0
- package/README.md +35 -1
- 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 +2 -122
- package/dist/api.js +2 -61
- package/dist/api.js.map +1 -1
- package/dist/browser/mrrocket-messenger.js +313 -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 +5 -2
- 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 +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/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 +339 -4
- package/dist/react-native/index.d.ts +339 -4
- package/dist/react-native/index.js +1191 -31
- 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 -237
- package/dist/styles.js.map +1 -1
- package/package.json +13 -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.9.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,62 @@ 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 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
|
+
|
|
166
230
|
// src/batch.ts
|
|
167
231
|
var MAX_BATCH = 100;
|
|
168
232
|
var MAX_BYTES = 6e4;
|
|
@@ -612,6 +676,174 @@ var createFlows = (env) => {
|
|
|
612
676
|
return controller;
|
|
613
677
|
};
|
|
614
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
|
+
|
|
615
847
|
// src/referral.ts
|
|
616
848
|
var REF_PARAM = "mrr_ref";
|
|
617
849
|
var CLICK_PARAM = "mrr_click";
|
|
@@ -634,7 +866,8 @@ var KEYS = {
|
|
|
634
866
|
queue: "mrrocket:queue",
|
|
635
867
|
config: "mrrocket:config",
|
|
636
868
|
installed: "mrrocket:installed",
|
|
637
|
-
referral: "mrrocket:referral"
|
|
869
|
+
referral: "mrrocket:referral",
|
|
870
|
+
surveys: "mrrocket:surveys"
|
|
638
871
|
};
|
|
639
872
|
var MAX_ERRORS = 20;
|
|
640
873
|
var MAX_CRUMBS = 20;
|
|
@@ -697,6 +930,8 @@ var createClient = (options, deps) => {
|
|
|
697
930
|
let backgroundSince = null;
|
|
698
931
|
const early = [];
|
|
699
932
|
const exposed = /* @__PURE__ */ new Set();
|
|
933
|
+
const surveysDone = /* @__PURE__ */ new Set();
|
|
934
|
+
const rage = new RageDetector();
|
|
700
935
|
let textsFor = null;
|
|
701
936
|
const texts = () => {
|
|
702
937
|
const id = distinctId();
|
|
@@ -728,8 +963,10 @@ var createClient = (options, deps) => {
|
|
|
728
963
|
flows.setConfig(config);
|
|
729
964
|
void flows.refresh();
|
|
730
965
|
};
|
|
966
|
+
let recorder = null;
|
|
731
967
|
const changed = () => {
|
|
732
968
|
version++;
|
|
969
|
+
recorder?.sync(appState === "active");
|
|
733
970
|
for (const l of [...listeners]) {
|
|
734
971
|
try {
|
|
735
972
|
l();
|
|
@@ -904,6 +1141,8 @@ var createClient = (options, deps) => {
|
|
|
904
1141
|
...who ? { distinctId: who } : {},
|
|
905
1142
|
...session ? { sessionId: session.id } : {}
|
|
906
1143
|
});
|
|
1144
|
+
recorder?.mark({ kind: "error", path: currentScreen ?? "/", label: clip2(d.message, 120) });
|
|
1145
|
+
recorder?.trigger("error");
|
|
907
1146
|
if (fatal) void sendErrors();
|
|
908
1147
|
else if (errorTimer === null) errorTimer = setTimeoutFn(() => void sendErrors(), 2e3);
|
|
909
1148
|
};
|
|
@@ -921,11 +1160,34 @@ var createClient = (options, deps) => {
|
|
|
921
1160
|
previousHandler?.(error, isFatal);
|
|
922
1161
|
});
|
|
923
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");
|
|
924
1185
|
let appStateSub;
|
|
925
1186
|
const onAppState = (next) => {
|
|
926
1187
|
if (closed) return;
|
|
927
1188
|
const prev = appState;
|
|
928
1189
|
appState = next;
|
|
1190
|
+
recorder?.sync(next === "active");
|
|
929
1191
|
if (next === "active" && prev !== "active") {
|
|
930
1192
|
startTimer();
|
|
931
1193
|
if (backgroundSince !== null) {
|
|
@@ -938,6 +1200,7 @@ var createClient = (options, deps) => {
|
|
|
938
1200
|
crumb("lifecycle", "app_background");
|
|
939
1201
|
if (options.lifecycle !== false) record("$app_background");
|
|
940
1202
|
stopTimer();
|
|
1203
|
+
void recorder?.flush();
|
|
941
1204
|
void persistQueue();
|
|
942
1205
|
void flush();
|
|
943
1206
|
if (errorQueue.length) void sendErrors();
|
|
@@ -1030,12 +1293,13 @@ var createClient = (options, deps) => {
|
|
|
1030
1293
|
const start = async () => {
|
|
1031
1294
|
let firstOpen = false;
|
|
1032
1295
|
if (consent !== "off") {
|
|
1033
|
-
const [id, stored, cached, installed, ref] = await Promise.all([
|
|
1296
|
+
const [id, stored, cached, installed, ref, surveys] = await Promise.all([
|
|
1034
1297
|
get(KEYS.id),
|
|
1035
1298
|
get(KEYS.queue),
|
|
1036
1299
|
get(KEYS.config),
|
|
1037
1300
|
get(KEYS.installed),
|
|
1038
|
-
get(KEYS.referral)
|
|
1301
|
+
get(KEYS.referral),
|
|
1302
|
+
get(KEYS.surveys)
|
|
1039
1303
|
]);
|
|
1040
1304
|
if (consent === "persistent") {
|
|
1041
1305
|
installId = id || newDistinctId();
|
|
@@ -1058,6 +1322,14 @@ var createClient = (options, deps) => {
|
|
|
1058
1322
|
if (r && typeof r.code === "string") referral = r;
|
|
1059
1323
|
} catch {
|
|
1060
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
|
+
}
|
|
1061
1333
|
firstOpen = !!storage && !installed;
|
|
1062
1334
|
if (firstOpen) await set(KEYS.installed, String(now()));
|
|
1063
1335
|
} else {
|
|
@@ -1108,6 +1380,24 @@ var createClient = (options, deps) => {
|
|
|
1108
1380
|
record(event, props);
|
|
1109
1381
|
flows.onEvent(event);
|
|
1110
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");
|
|
1400
|
+
},
|
|
1111
1401
|
screen: (name, props) => {
|
|
1112
1402
|
if (typeof name !== "string" || !name.trim()) return;
|
|
1113
1403
|
const path = (name.startsWith("/") ? name : `/${name}`).slice(0, 300);
|
|
@@ -1116,6 +1406,7 @@ var createClient = (options, deps) => {
|
|
|
1116
1406
|
currentScreen = path;
|
|
1117
1407
|
crumb("navigation", path);
|
|
1118
1408
|
record("$screen", { ...props, path, screen: name, ...previous ? { previous } : {} });
|
|
1409
|
+
recorder?.mark({ kind: "pageview", path });
|
|
1119
1410
|
},
|
|
1120
1411
|
identify: (id, props) => {
|
|
1121
1412
|
if (typeof id !== "string" || !id) return;
|
|
@@ -1163,6 +1454,7 @@ var createClient = (options, deps) => {
|
|
|
1163
1454
|
const before = consent;
|
|
1164
1455
|
consent = next;
|
|
1165
1456
|
if (next === "off") {
|
|
1457
|
+
recorder?.drop();
|
|
1166
1458
|
queue = [];
|
|
1167
1459
|
session = null;
|
|
1168
1460
|
errorQueue.length = 0;
|
|
@@ -1171,10 +1463,12 @@ var createClient = (options, deps) => {
|
|
|
1171
1463
|
installId = null;
|
|
1172
1464
|
void remove(KEYS.id);
|
|
1173
1465
|
void remove(KEYS.referral);
|
|
1466
|
+
void remove(KEYS.surveys);
|
|
1174
1467
|
} else {
|
|
1175
1468
|
installId = identified ?? newDistinctId();
|
|
1176
1469
|
void set(KEYS.id, installId);
|
|
1177
1470
|
if (referral) void set(KEYS.referral, JSON.stringify(referral));
|
|
1471
|
+
if (surveysDone.size) void set(KEYS.surveys, JSON.stringify([...surveysDone]));
|
|
1178
1472
|
}
|
|
1179
1473
|
if (before === "off" && currentScreen && loaded) record("$screen", { path: currentScreen, screen: currentScreen });
|
|
1180
1474
|
changed();
|
|
@@ -1217,6 +1511,22 @@ var createClient = (options, deps) => {
|
|
|
1217
1511
|
isReady: () => isReady,
|
|
1218
1512
|
getDistinctId: distinctId,
|
|
1219
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
|
+
},
|
|
1220
1530
|
getConfig: () => config,
|
|
1221
1531
|
subscribe: (listener) => {
|
|
1222
1532
|
listeners.add(listener);
|
|
@@ -1226,6 +1536,7 @@ var createClient = (options, deps) => {
|
|
|
1226
1536
|
close: async () => {
|
|
1227
1537
|
if (closed) return;
|
|
1228
1538
|
stopTimer();
|
|
1539
|
+
recorder?.sync(false);
|
|
1229
1540
|
appStateSub?.remove();
|
|
1230
1541
|
pushOff?.();
|
|
1231
1542
|
pushOff = null;
|
|
@@ -1521,6 +1832,12 @@ try {
|
|
|
1521
1832
|
} catch {
|
|
1522
1833
|
expoLocalization = null;
|
|
1523
1834
|
}
|
|
1835
|
+
var viewShot = null;
|
|
1836
|
+
try {
|
|
1837
|
+
viewShot = require("react-native-view-shot");
|
|
1838
|
+
} catch {
|
|
1839
|
+
viewShot = null;
|
|
1840
|
+
}
|
|
1524
1841
|
var expoNotifications = null;
|
|
1525
1842
|
try {
|
|
1526
1843
|
expoNotifications = require("expo-notifications");
|
|
@@ -1594,6 +1911,30 @@ var readContext = () => {
|
|
|
1594
1911
|
for (const k of Object.keys(ctx)) if (ctx[k] === void 0) delete ctx[k];
|
|
1595
1912
|
return ctx;
|
|
1596
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
|
+
};
|
|
1597
1938
|
var nativeDeps = (overrides = {}) => ({
|
|
1598
1939
|
storage: overrides.storage !== void 0 ? overrides.storage : asyncStorage(),
|
|
1599
1940
|
fetch: ((url, init) => fetch(url, init)),
|
|
@@ -1607,17 +1948,815 @@ var nativeDeps = (overrides = {}) => ({
|
|
|
1607
1948
|
},
|
|
1608
1949
|
initialUrl: () => RN2.Linking.getInitialURL(),
|
|
1609
1950
|
openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
|
|
1610
|
-
push: expoPush()
|
|
1951
|
+
push: expoPush(),
|
|
1952
|
+
recorder: recorderDeps()
|
|
1611
1953
|
});
|
|
1612
1954
|
var optionalModules = () => ({
|
|
1613
1955
|
asyncStorage: !!asyncStorage(),
|
|
1614
1956
|
expoDevice: !!expoDevice,
|
|
1615
1957
|
expoApplication: !!expoApplication,
|
|
1616
1958
|
expoLocalization: !!expoLocalization,
|
|
1617
|
-
expoNotifications: !!expoNotifications
|
|
1959
|
+
expoNotifications: !!expoNotifications,
|
|
1960
|
+
viewShot: !!viewShot?.captureScreen
|
|
1618
1961
|
});
|
|
1619
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()
|
|
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
|
+
};
|
|
2747
|
+
|
|
1620
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
|
+
};
|
|
1621
2760
|
var shared = null;
|
|
1622
2761
|
var sharedKey = "";
|
|
1623
2762
|
var createMRRocket = (options) => {
|
|
@@ -1633,20 +2772,40 @@ var createMRRocket = (options) => {
|
|
|
1633
2772
|
return shared;
|
|
1634
2773
|
};
|
|
1635
2774
|
var getMRRocket = () => shared;
|
|
1636
|
-
var Ctx = (0,
|
|
1637
|
-
var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, ...options }) => {
|
|
1638
|
-
const [client] = (0,
|
|
1639
|
-
(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)(() => {
|
|
1640
2779
|
if (consent) client.setConsent(consent);
|
|
1641
2780
|
}, [client, consent]);
|
|
1642
|
-
|
|
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
|
+
);
|
|
1643
2802
|
};
|
|
1644
2803
|
var useClient = () => {
|
|
1645
|
-
const c = (0,
|
|
2804
|
+
const c = (0, import_react3.useContext)(Ctx);
|
|
1646
2805
|
if (!c) throw new Error("MRRocket: wrap your app in <MRRocketProvider apiKey=\u2026>.");
|
|
1647
2806
|
return c;
|
|
1648
2807
|
};
|
|
1649
|
-
var useVersion = (client) => (0,
|
|
2808
|
+
var useVersion = (client) => (0, import_react3.useSyncExternalStore)(client.subscribe, client.getVersion, client.getVersion);
|
|
1650
2809
|
var useMRRocket = () => {
|
|
1651
2810
|
const client = useClient();
|
|
1652
2811
|
useVersion(client);
|
|
@@ -1656,6 +2815,7 @@ var useMRRocket = () => {
|
|
|
1656
2815
|
identify: client.identify,
|
|
1657
2816
|
reset: client.reset,
|
|
1658
2817
|
setConsent: client.setConsent,
|
|
2818
|
+
setReplay: client.setReplay,
|
|
1659
2819
|
captureError: client.captureError,
|
|
1660
2820
|
flush: client.flush,
|
|
1661
2821
|
distinctId: client.getDistinctId(),
|
|
@@ -1690,7 +2850,7 @@ var useVariant = (key) => {
|
|
|
1690
2850
|
useVersion(client);
|
|
1691
2851
|
const config = client.getConfig();
|
|
1692
2852
|
const variant = client.peekVariant(key);
|
|
1693
|
-
(0,
|
|
2853
|
+
(0, import_react3.useEffect)(() => {
|
|
1694
2854
|
if (config) client.expose(key, variant);
|
|
1695
2855
|
}, [client, config, key, variant]);
|
|
1696
2856
|
return variant;
|
|
@@ -1701,7 +2861,7 @@ var useText = (id, fallback) => {
|
|
|
1701
2861
|
const config = client.getConfig();
|
|
1702
2862
|
const text2 = client.peekText(id);
|
|
1703
2863
|
const person = client.getDistinctId();
|
|
1704
|
-
(0,
|
|
2864
|
+
(0, import_react3.useEffect)(() => {
|
|
1705
2865
|
if (config) client.exposeText(id);
|
|
1706
2866
|
}, [client, config, id, person]);
|
|
1707
2867
|
return text2 ?? fallback;
|
|
@@ -1726,13 +2886,13 @@ var useExpoRouterTracking = () => {
|
|
|
1726
2886
|
console.warn("[MRRocket] useExpoRouterTracking needs expo-router. With React Navigation use useNavigationTracking.");
|
|
1727
2887
|
}
|
|
1728
2888
|
const pathname = (router?.usePathname ?? noPathname)();
|
|
1729
|
-
(0,
|
|
2889
|
+
(0, import_react3.useEffect)(() => {
|
|
1730
2890
|
if (pathname) client.screen(pathname);
|
|
1731
2891
|
}, [client, pathname]);
|
|
1732
2892
|
};
|
|
1733
2893
|
var useNavigationTracking = () => {
|
|
1734
2894
|
const client = useClient();
|
|
1735
|
-
return (0,
|
|
2895
|
+
return (0, import_react3.useCallback)(
|
|
1736
2896
|
(state) => {
|
|
1737
2897
|
const name = activeRouteName(state);
|
|
1738
2898
|
if (name) client.screen(name);
|