@getdevteam/analytics-react-native 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -26,6 +26,46 @@ analytics.track("add_to_cart", { sku: "A-1" });
26
26
  The SDK auto-flushes when the app goes to the background.
27
27
  On a physical device use your machine's LAN IP instead of `localhost` when pointing at a local backend.
28
28
 
29
+ ## User feedback
30
+
31
+ Create the client with `allowUserFeedback: true` and wrap your app in the exported provider:
32
+
33
+ ```tsx
34
+ import { createAnalytics, FeedbackProvider } from "@getdevteam/analytics-react-native";
35
+
36
+ const analytics = createAnalytics({
37
+ key: "dtp_your_public_key",
38
+ host: "https://ingest.analytics.getdevteam.ai",
39
+ allowUserFeedback: true,
40
+ });
41
+
42
+ export default function App() {
43
+ return (
44
+ <FeedbackProvider client={analytics}>
45
+ <RootNavigator />
46
+ </FeedbackProvider>
47
+ );
48
+ }
49
+ ```
50
+
51
+ A floating button opens a modal with a screenshot of the app, drawing tools, and a comment box; submit sends it to the platform's feedback inbox.
52
+ `analytics.submitFeedback({ text, screenshot?, screenshotContentType?, pageUrl? })` sends feedback programmatically and resolves `false` instead of throwing on any failure.
53
+ Customize with `feedback: { colors, accentColor, buttonLabel, title, submitLabel, commentPlaceholder, successMessage, position }`.
54
+
55
+ Screenshot and drawing come from two optional peers, and each degrades instead of breaking:
56
+
57
+ | Installed | Behavior |
58
+ |---|---|
59
+ | `react-native-view-shot` + `react-native-svg` | Screenshot, drawing tools, comment. |
60
+ | `react-native-view-shot` only | Screenshot + comment, no drawing tools. |
61
+ | neither | Comment-only feedback. |
62
+ | app not wrapped in `FeedbackProvider`, or flag off | Nothing renders; the app is untouched. |
63
+
64
+ The optional peers are loaded with literal `require` calls, each inside a try/catch, in the CJS build.
65
+ That assumes Metro resolves this package's `require` export condition (the default) and keeps `transformer.allowOptionalDependencies` enabled (the React Native and Expo template default, and it only covers string-literal requires inside a try/catch), so a missing optional peer never fails the bundle.
66
+ Import-condition consumers (react-native-web, Node ESM) cannot load the optional peers and degrade to comment-only feedback.
67
+ Native views (maps, video, web views) may come out blank in captures; a failed capture falls back to comment-only.
68
+
29
69
  You need a public ingest key (`dtp_...`) and the ingest host URL for your environment - see [Getting a key](https://github.com/getdevteam-ai/analytics/tree/main/src/sdk#getting-a-key) in the full SDK docs.
30
70
 
31
71
  Full client API, config options, and release process: [src/sdk README](https://github.com/getdevteam-ai/analytics/tree/main/src/sdk).
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ FeedbackProvider: () => FeedbackProvider,
33
34
  REACT_NATIVE_SDK_NAME: () => REACT_NATIVE_SDK_NAME,
34
35
  REACT_NATIVE_SDK_VERSION: () => REACT_NATIVE_SDK_VERSION,
35
36
  collectReactNativeContext: () => collectReactNativeContext,
@@ -39,7 +40,7 @@ __export(index_exports, {
39
40
  osNameFromPlatform: () => osNameFromPlatform
40
41
  });
41
42
  module.exports = __toCommonJS(index_exports);
42
- var import_analytics_core = require("@getdevteam/analytics-core");
43
+ var import_analytics_core2 = require("@getdevteam/analytics-core");
43
44
 
44
45
  // src/appState.ts
45
46
  var import_react_native = require("react-native");
@@ -80,6 +81,55 @@ function collectReactNativeContext() {
80
81
  return context;
81
82
  }
82
83
 
84
+ // src/feedback.ts
85
+ var import_analytics_core = require("@getdevteam/analytics-core");
86
+ var FEEDBACK_DEFAULT_COLORS = ["#f44336", "#4caf50", "#2196f3", "#ffeb3b"];
87
+ var FEEDBACK_DEFAULT_ACCENT = "#5b9cf5";
88
+ var MAX_TEXT_LENGTH = 1e4;
89
+ var MAX_PAGE_URL_LENGTH = 2048;
90
+ function createFeedbackSender(config) {
91
+ const url = `${String(config.host ?? "").replace(/\/+$/, "")}/v1/ingest/feedback`;
92
+ return async (input) => {
93
+ try {
94
+ const text = typeof input?.text === "string" ? input.text.trim() : "";
95
+ if (text.length === 0) {
96
+ config.onError(new Error("feedback text must be a non-empty string"));
97
+ return false;
98
+ }
99
+ const body = {
100
+ feedback_id: (0, import_analytics_core.uuidV7)(),
101
+ text: text.slice(0, MAX_TEXT_LENGTH)
102
+ };
103
+ if (typeof input.screenshot === "string" && input.screenshot.length > 0) {
104
+ body.screenshot = input.screenshot;
105
+ body.screenshot_content_type = input.screenshotContentType ?? "image/png";
106
+ }
107
+ const distinctId = config.getDistinctId();
108
+ if (distinctId) body.distinct_id = distinctId;
109
+ const sessionId = config.getSessionId();
110
+ if (sessionId) body.session_id = sessionId;
111
+ if (typeof input.pageUrl === "string" && input.pageUrl.length > 0) {
112
+ body.page_url = input.pageUrl.slice(0, MAX_PAGE_URL_LENGTH);
113
+ }
114
+ body.context = config.getContext();
115
+ if (!config.fetch) throw new Error("fetch is not available in this environment");
116
+ const response = await config.fetch(url, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json", "X-DevTeam-Key": config.key },
119
+ body: JSON.stringify(body)
120
+ });
121
+ if (!response.ok) {
122
+ config.onError(new Error(`feedback ingest responded ${response.status}`));
123
+ return false;
124
+ }
125
+ return true;
126
+ } catch (error) {
127
+ config.onError(error);
128
+ return false;
129
+ }
130
+ };
131
+ }
132
+
83
133
  // src/storage.ts
84
134
  var import_async_storage = __toESM(require("@react-native-async-storage/async-storage"), 1);
85
135
  function createAsyncStorageAdapter() {
@@ -106,31 +156,540 @@ function createAsyncStorageAdapter() {
106
156
  };
107
157
  }
108
158
 
159
+ // src/feedbackProvider.ts
160
+ var import_react = require("react");
161
+ var import_react_native3 = require("react-native");
162
+
163
+ // src/optionalPeers.ts
164
+ var testLoaderOverride;
165
+ function requireViewShot() {
166
+ if (typeof require !== "function") return void 0;
167
+ try {
168
+ return require("react-native-view-shot");
169
+ } catch {
170
+ return void 0;
171
+ }
172
+ }
173
+ function requireSvg() {
174
+ if (typeof require !== "function") return void 0;
175
+ try {
176
+ return require("react-native-svg");
177
+ } catch {
178
+ return void 0;
179
+ }
180
+ }
181
+ function loadOptionalModule(name, requirePeer) {
182
+ if (testLoaderOverride) {
183
+ try {
184
+ return testLoaderOverride(name);
185
+ } catch {
186
+ return void 0;
187
+ }
188
+ }
189
+ return requirePeer();
190
+ }
191
+ function loadViewShot() {
192
+ const loaded = loadOptionalModule("react-native-view-shot", requireViewShot);
193
+ const captureRef = loaded?.captureRef ?? loaded?.default?.captureRef;
194
+ if (typeof captureRef !== "function") return void 0;
195
+ return { captureRef };
196
+ }
197
+ function loadSvg() {
198
+ const loaded = loadOptionalModule("react-native-svg", requireSvg);
199
+ const svg = loaded?.Svg ?? loaded?.default;
200
+ const polyline = loaded?.Polyline;
201
+ if (svg == null || polyline == null) return void 0;
202
+ return {
203
+ Svg: svg,
204
+ Polyline: polyline
205
+ };
206
+ }
207
+
208
+ // src/feedbackProvider.ts
209
+ var PEN_WIDTH = 5;
210
+ var SUCCESS_CLOSE_DELAY_MS = 1600;
211
+ function FeedbackProvider(props) {
212
+ const options = props.client.feedbackOptions ?? {};
213
+ const colors = options.colors && options.colors.length > 0 ? options.colors : FEEDBACK_DEFAULT_COLORS;
214
+ const firstColor = colors[0] ?? FEEDBACK_DEFAULT_COLORS[0];
215
+ const accent = options.accentColor ?? FEEDBACK_DEFAULT_ACCENT;
216
+ const [visible, setVisible] = (0, import_react.useState)(false);
217
+ const [screenshot, setScreenshot] = (0, import_react.useState)(void 0);
218
+ const [screenshotSize, setScreenshotSize] = (0, import_react.useState)(void 0);
219
+ const [comment, setComment] = (0, import_react.useState)("");
220
+ const [sending, setSending] = (0, import_react.useState)(false);
221
+ const [sent, setSent] = (0, import_react.useState)(false);
222
+ const [errorMessage, setErrorMessage] = (0, import_react.useState)(void 0);
223
+ const [activeColor, setActiveColor] = (0, import_react.useState)(firstColor);
224
+ const [, setStrokeVersion] = (0, import_react.useState)(0);
225
+ const contentRef = (0, import_react.useRef)(null);
226
+ const previewRef = (0, import_react.useRef)(null);
227
+ const strokesRef = (0, import_react.useRef)([]);
228
+ const activeStrokeRef = (0, import_react.useRef)(void 0);
229
+ const activeColorRef = (0, import_react.useRef)(firstColor);
230
+ const closeTimerRef = (0, import_react.useRef)(void 0);
231
+ const sessionRef = (0, import_react.useRef)(0);
232
+ (0, import_react.useEffect)(
233
+ () => () => {
234
+ if (closeTimerRef.current !== void 0) clearTimeout(closeTimerRef.current);
235
+ },
236
+ []
237
+ );
238
+ const bumpStrokes = (0, import_react.useCallback)(() => setStrokeVersion((version) => version + 1), []);
239
+ const panResponderRef = (0, import_react.useRef)(
240
+ import_react_native3.PanResponder.create({
241
+ onStartShouldSetPanResponder: () => true,
242
+ onMoveShouldSetPanResponder: () => true,
243
+ onPanResponderGrant: (event) => {
244
+ const { locationX, locationY } = event.nativeEvent;
245
+ const stroke = {
246
+ points: [{ x: locationX, y: locationY }],
247
+ color: activeColorRef.current,
248
+ width: PEN_WIDTH
249
+ };
250
+ activeStrokeRef.current = stroke;
251
+ strokesRef.current.push(stroke);
252
+ bumpStrokes();
253
+ },
254
+ onPanResponderMove: (event) => {
255
+ const stroke = activeStrokeRef.current;
256
+ if (!stroke) return;
257
+ stroke.points.push({ x: event.nativeEvent.locationX, y: event.nativeEvent.locationY });
258
+ bumpStrokes();
259
+ },
260
+ onPanResponderRelease: () => {
261
+ activeStrokeRef.current = void 0;
262
+ },
263
+ onPanResponderTerminate: () => {
264
+ activeStrokeRef.current = void 0;
265
+ }
266
+ })
267
+ );
268
+ const close = (0, import_react.useCallback)(() => {
269
+ sessionRef.current += 1;
270
+ if (closeTimerRef.current !== void 0) {
271
+ clearTimeout(closeTimerRef.current);
272
+ closeTimerRef.current = void 0;
273
+ }
274
+ setVisible(false);
275
+ setScreenshot(void 0);
276
+ setScreenshotSize(void 0);
277
+ setComment("");
278
+ setSending(false);
279
+ setSent(false);
280
+ setErrorMessage(void 0);
281
+ strokesRef.current = [];
282
+ activeStrokeRef.current = void 0;
283
+ }, []);
284
+ const open = (0, import_react.useCallback)(async () => {
285
+ sessionRef.current += 1;
286
+ try {
287
+ strokesRef.current = [];
288
+ activeStrokeRef.current = void 0;
289
+ activeColorRef.current = firstColor;
290
+ setActiveColor(firstColor);
291
+ setComment("");
292
+ setSent(false);
293
+ setSending(false);
294
+ setErrorMessage(void 0);
295
+ let shot;
296
+ const viewShot = loadViewShot();
297
+ if (viewShot && contentRef.current) {
298
+ try {
299
+ shot = await viewShot.captureRef(contentRef.current, {
300
+ format: "png",
301
+ quality: 0.9,
302
+ result: "base64"
303
+ });
304
+ } catch (captureError) {
305
+ props.client.reportError(captureError);
306
+ shot = void 0;
307
+ }
308
+ }
309
+ setScreenshotSize(shot ? await capturedImageSize(shot) : void 0);
310
+ setScreenshot(shot);
311
+ setVisible(true);
312
+ } catch (openError) {
313
+ props.client.reportError(openError);
314
+ setScreenshotSize(void 0);
315
+ setScreenshot(void 0);
316
+ setVisible(true);
317
+ }
318
+ }, [firstColor, props.client]);
319
+ const submit = (0, import_react.useCallback)(async () => {
320
+ if (sending || sent) return;
321
+ const text = comment.trim();
322
+ if (text.length === 0) {
323
+ setErrorMessage("Please add a short comment before sending.");
324
+ return;
325
+ }
326
+ const session = sessionRef.current;
327
+ setSending(true);
328
+ setErrorMessage(void 0);
329
+ try {
330
+ let finalScreenshot = screenshot;
331
+ const viewShot = loadViewShot();
332
+ if (screenshot && strokesRef.current.length > 0 && viewShot && previewRef.current) {
333
+ try {
334
+ finalScreenshot = await viewShot.captureRef(previewRef.current, {
335
+ format: "png",
336
+ quality: 0.9,
337
+ result: "base64"
338
+ });
339
+ } catch (flattenError) {
340
+ props.client.reportError(flattenError);
341
+ }
342
+ }
343
+ const submission = { text };
344
+ if (finalScreenshot) {
345
+ submission.screenshot = finalScreenshot;
346
+ submission.screenshotContentType = "image/png";
347
+ }
348
+ const ok = await props.client.submitFeedback(submission);
349
+ if (sessionRef.current !== session) return;
350
+ if (ok) {
351
+ setSent(true);
352
+ closeTimerRef.current = setTimeout(close, SUCCESS_CLOSE_DELAY_MS);
353
+ } else {
354
+ setErrorMessage("Could not send feedback. Please try again.");
355
+ }
356
+ } catch {
357
+ if (sessionRef.current === session) {
358
+ setErrorMessage("Could not send feedback. Please try again.");
359
+ }
360
+ } finally {
361
+ if (sessionRef.current === session) {
362
+ setSending(false);
363
+ }
364
+ }
365
+ }, [close, comment, props.client, screenshot, sending, sent]);
366
+ const undo = (0, import_react.useCallback)(() => {
367
+ if (activeStrokeRef.current) return;
368
+ strokesRef.current.pop();
369
+ bumpStrokes();
370
+ }, [bumpStrokes]);
371
+ const clear = (0, import_react.useCallback)(() => {
372
+ if (activeStrokeRef.current) return;
373
+ strokesRef.current = [];
374
+ bumpStrokes();
375
+ }, [bumpStrokes]);
376
+ if (props.client.userFeedbackAllowed !== true || typeof props.client.submitFeedback !== "function") {
377
+ return (0, import_react.createElement)(import_react.Fragment, null, props.children);
378
+ }
379
+ const svg = loadSvg();
380
+ const window = import_react_native3.Dimensions.get("window");
381
+ const previewAspectRatio = screenshotSize && screenshotSize.height > 0 ? screenshotSize.width / screenshotSize.height : window.height > 0 ? window.width / window.height : 1;
382
+ const positionStyle = options.position === "bottom-left" ? styles.fabLeft : styles.fabRight;
383
+ const preview = screenshot ? (0, import_react.createElement)(
384
+ import_react_native3.View,
385
+ {
386
+ style: [styles.preview, { aspectRatio: previewAspectRatio }],
387
+ testID: "devteam-feedback-preview-frame"
388
+ },
389
+ (0, import_react.createElement)(
390
+ import_react_native3.View,
391
+ { ref: previewRef, collapsable: false, style: styles.previewSurface },
392
+ (0, import_react.createElement)(import_react_native3.Image, {
393
+ source: { uri: `data:image/png;base64,${screenshot}` },
394
+ style: styles.previewImage,
395
+ resizeMode: "stretch",
396
+ testID: "devteam-feedback-preview"
397
+ }),
398
+ svg ? (0, import_react.createElement)(
399
+ import_react_native3.View,
400
+ { style: styles.drawLayer, ...panResponderRef.current.panHandlers },
401
+ (0, import_react.createElement)(
402
+ svg.Svg,
403
+ { width: "100%", height: "100%" },
404
+ ...strokesRef.current.map(
405
+ (stroke, index) => (0, import_react.createElement)(svg.Polyline, {
406
+ key: String(index),
407
+ points: stroke.points.map((point) => `${point.x},${point.y}`).join(" "),
408
+ fill: "none",
409
+ stroke: stroke.color,
410
+ strokeWidth: stroke.width,
411
+ strokeLinecap: "round",
412
+ strokeLinejoin: "round"
413
+ })
414
+ )
415
+ )
416
+ ) : null
417
+ )
418
+ ) : null;
419
+ const tools = screenshot && svg ? (0, import_react.createElement)(
420
+ import_react_native3.View,
421
+ { style: styles.tools },
422
+ ...colors.map(
423
+ (color) => (0, import_react.createElement)(import_react_native3.Pressable, {
424
+ key: color,
425
+ accessibilityRole: "button",
426
+ testID: `devteam-feedback-color-${color}`,
427
+ onPress: () => {
428
+ activeColorRef.current = color;
429
+ setActiveColor(color);
430
+ },
431
+ style: [
432
+ styles.swatch,
433
+ { backgroundColor: color },
434
+ activeColor === color ? styles.swatchActive : null
435
+ ]
436
+ })
437
+ ),
438
+ (0, import_react.createElement)(
439
+ import_react_native3.Pressable,
440
+ {
441
+ accessibilityRole: "button",
442
+ testID: "devteam-feedback-undo",
443
+ onPress: undo,
444
+ style: styles.toolButton
445
+ },
446
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.toolLabel }, "Undo")
447
+ ),
448
+ (0, import_react.createElement)(
449
+ import_react_native3.Pressable,
450
+ {
451
+ accessibilityRole: "button",
452
+ testID: "devteam-feedback-clear",
453
+ onPress: clear,
454
+ style: styles.toolButton
455
+ },
456
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.toolLabel }, "Clear")
457
+ )
458
+ ) : null;
459
+ const modal = (0, import_react.createElement)(
460
+ import_react_native3.Modal,
461
+ { visible, transparent: true, animationType: "fade", onRequestClose: close },
462
+ (0, import_react.createElement)(
463
+ import_react_native3.View,
464
+ { style: styles.backdrop },
465
+ (0, import_react.createElement)(
466
+ import_react_native3.View,
467
+ { style: styles.panel },
468
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.title }, options.title ?? "Send feedback"),
469
+ preview,
470
+ tools,
471
+ (0, import_react.createElement)(import_react_native3.TextInput, {
472
+ multiline: true,
473
+ value: comment,
474
+ onChangeText: setComment,
475
+ placeholder: options.commentPlaceholder ?? "What went wrong, or what could be better?",
476
+ style: styles.input,
477
+ testID: "devteam-feedback-comment"
478
+ }),
479
+ errorMessage ? (0, import_react.createElement)(import_react_native3.Text, { style: styles.error, testID: "devteam-feedback-error" }, errorMessage) : null,
480
+ sent ? (0, import_react.createElement)(
481
+ import_react_native3.Text,
482
+ { style: styles.success, testID: "devteam-feedback-success" },
483
+ options.successMessage ?? "Thanks for the feedback!"
484
+ ) : null,
485
+ (0, import_react.createElement)(
486
+ import_react_native3.View,
487
+ { style: styles.actions },
488
+ (0, import_react.createElement)(
489
+ import_react_native3.Pressable,
490
+ {
491
+ accessibilityRole: "button",
492
+ testID: "devteam-feedback-cancel",
493
+ onPress: close,
494
+ style: styles.cancelButton
495
+ },
496
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.cancelLabel }, "Cancel")
497
+ ),
498
+ (0, import_react.createElement)(
499
+ import_react_native3.Pressable,
500
+ {
501
+ accessibilityRole: "button",
502
+ testID: "devteam-feedback-submit",
503
+ disabled: sending || sent,
504
+ onPress: () => {
505
+ void submit();
506
+ },
507
+ style: [styles.submitButton, { backgroundColor: accent }]
508
+ },
509
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.submitLabel }, options.submitLabel ?? "Send")
510
+ )
511
+ )
512
+ )
513
+ )
514
+ );
515
+ return (0, import_react.createElement)(
516
+ import_react_native3.View,
517
+ { style: styles.root },
518
+ (0, import_react.createElement)(
519
+ import_react_native3.View,
520
+ { ref: contentRef, collapsable: false, style: styles.content },
521
+ props.children
522
+ ),
523
+ (0, import_react.createElement)(
524
+ import_react_native3.Pressable,
525
+ {
526
+ accessibilityRole: "button",
527
+ testID: "devteam-feedback-button",
528
+ onPress: () => {
529
+ void open();
530
+ },
531
+ style: [styles.fab, positionStyle, { backgroundColor: accent }]
532
+ },
533
+ (0, import_react.createElement)(import_react_native3.Text, { style: styles.fabLabel }, options.buttonLabel ?? "Feedback")
534
+ ),
535
+ modal
536
+ );
537
+ }
538
+ function capturedImageSize(base64) {
539
+ return new Promise((resolve) => {
540
+ try {
541
+ import_react_native3.Image.getSize(
542
+ `data:image/png;base64,${base64}`,
543
+ (width, height) => resolve(width > 0 && height > 0 ? { width, height } : void 0),
544
+ () => resolve(void 0)
545
+ );
546
+ } catch {
547
+ resolve(void 0);
548
+ }
549
+ });
550
+ }
551
+ var styles = import_react_native3.StyleSheet.create({
552
+ root: { flex: 1 },
553
+ content: { flex: 1 },
554
+ fab: {
555
+ position: "absolute",
556
+ bottom: 24,
557
+ borderRadius: 999,
558
+ paddingVertical: 10,
559
+ paddingHorizontal: 18,
560
+ elevation: 4
561
+ },
562
+ fabRight: { right: 20 },
563
+ fabLeft: { left: 20 },
564
+ fabLabel: { color: "#ffffff", fontSize: 14, fontWeight: "600" },
565
+ backdrop: {
566
+ flex: 1,
567
+ backgroundColor: "rgba(15, 23, 42, 0.55)",
568
+ alignItems: "center",
569
+ justifyContent: "center",
570
+ padding: 16
571
+ },
572
+ panel: {
573
+ width: "100%",
574
+ maxWidth: 560,
575
+ backgroundColor: "#ffffff",
576
+ borderRadius: 12,
577
+ padding: 16,
578
+ gap: 10
579
+ },
580
+ title: { fontSize: 16, fontWeight: "600", color: "#1f2933" },
581
+ preview: {
582
+ width: "100%",
583
+ borderRadius: 8,
584
+ borderWidth: 1,
585
+ borderColor: "#d3dce6",
586
+ overflow: "hidden"
587
+ },
588
+ previewSurface: { width: "100%", height: "100%" },
589
+ previewImage: { width: "100%", height: "100%" },
590
+ drawLayer: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0 },
591
+ tools: { flexDirection: "row", alignItems: "center", gap: 8 },
592
+ swatch: { width: 24, height: 24, borderRadius: 12, borderWidth: 2, borderColor: "transparent" },
593
+ swatchActive: { borderColor: "#1f2933" },
594
+ toolButton: {
595
+ borderWidth: 1,
596
+ borderColor: "#d3dce6",
597
+ borderRadius: 6,
598
+ paddingVertical: 4,
599
+ paddingHorizontal: 10
600
+ },
601
+ toolLabel: { fontSize: 13, color: "#1f2933" },
602
+ input: {
603
+ minHeight: 72,
604
+ borderWidth: 1,
605
+ borderColor: "#d3dce6",
606
+ borderRadius: 8,
607
+ padding: 10,
608
+ color: "#1f2933",
609
+ textAlignVertical: "top"
610
+ },
611
+ error: { color: "#b91c1c", fontSize: 13 },
612
+ success: { color: "#15803d", fontSize: 14, fontWeight: "600" },
613
+ actions: { flexDirection: "row", justifyContent: "flex-end", gap: 8 },
614
+ cancelButton: {
615
+ borderWidth: 1,
616
+ borderColor: "#d3dce6",
617
+ borderRadius: 8,
618
+ paddingVertical: 8,
619
+ paddingHorizontal: 14
620
+ },
621
+ cancelLabel: { fontSize: 14, color: "#1f2933" },
622
+ submitButton: { borderRadius: 8, paddingVertical: 8, paddingHorizontal: 16 },
623
+ submitLabel: { fontSize: 14, fontWeight: "600", color: "#ffffff" }
624
+ });
625
+
109
626
  // src/index.ts
110
627
  var REACT_NATIVE_SDK_NAME = "@getdevteam/analytics-react-native";
111
- var REACT_NATIVE_SDK_VERSION = "0.2.0";
628
+ var REACT_NATIVE_SDK_VERSION = "0.3.1";
112
629
  function createAnalytics(config) {
113
- const client = (0, import_analytics_core.createClient)({
630
+ const client = (0, import_analytics_core2.createClient)({
114
631
  ...config,
115
632
  storage: config.storage ?? createAsyncStorageAdapter()
116
633
  });
117
- client.setContext({
634
+ const reactNativeContext = {
118
635
  sdk_name: REACT_NATIVE_SDK_NAME,
119
636
  sdk_version: REACT_NATIVE_SDK_VERSION,
120
637
  ...collectReactNativeContext(),
121
638
  ...config.context
122
- });
639
+ };
640
+ client.setContext(reactNativeContext);
641
+ let currentContext = reactNativeContext;
642
+ const setContext = (partial) => {
643
+ client.setContext(partial);
644
+ try {
645
+ const next = { ...currentContext };
646
+ for (const [key, value] of Object.entries(partial)) {
647
+ if (value === void 0) {
648
+ delete next[key];
649
+ } else {
650
+ next[key] = value;
651
+ }
652
+ }
653
+ currentContext = next;
654
+ } catch {
655
+ }
656
+ };
123
657
  const removeAppStateFlush = installAppStateFlush(client);
658
+ const reportError = (error) => {
659
+ try {
660
+ config.onError?.(error);
661
+ } catch {
662
+ }
663
+ };
664
+ const submitFeedback = createFeedbackSender({
665
+ host: config.host,
666
+ key: config.key,
667
+ fetch: config.fetch ?? defaultReactNativeFetch(),
668
+ getDistinctId: () => client.getDistinctId(),
669
+ getSessionId: () => client.getSessionId(),
670
+ getContext: () => currentContext,
671
+ onError: reportError
672
+ });
124
673
  return {
125
674
  ...client,
675
+ setContext,
676
+ submitFeedback,
677
+ reportError,
678
+ userFeedbackAllowed: config.allowUserFeedback === true,
679
+ feedbackOptions: config.feedback ?? {},
126
680
  shutdown: async () => {
127
681
  removeAppStateFlush();
128
682
  await client.shutdown();
129
683
  }
130
684
  };
131
685
  }
686
+ function defaultReactNativeFetch() {
687
+ if (typeof fetch !== "function") return void 0;
688
+ return (url, init) => fetch(url, init);
689
+ }
132
690
  // Annotate the CommonJS export names for ESM import in node:
133
691
  0 && (module.exports = {
692
+ FeedbackProvider,
134
693
  REACT_NATIVE_SDK_NAME,
135
694
  REACT_NATIVE_SDK_VERSION,
136
695
  collectReactNativeContext,