@getdevteam/analytics-react-native 0.1.0 → 0.3.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/README.md +71 -0
- package/dist/index.cjs +564 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -3
- package/dist/index.d.ts +41 -3
- package/dist/index.js +587 -3
- package/dist/index.js.map +1 -1
- package/package.json +23 -4
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @getdevteam/analytics-react-native
|
|
2
|
+
|
|
3
|
+
React Native / Expo SDK for the [DevTeam Analytics & Logging Platform](https://github.com/getdevteam-ai/analytics).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @getdevteam/analytics-react-native @react-native-async-storage/async-storage
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Use
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createAnalytics } from "@getdevteam/analytics-react-native";
|
|
15
|
+
|
|
16
|
+
const analytics = createAnalytics({
|
|
17
|
+
key: "dtp_your_public_key",
|
|
18
|
+
host: "https://ingest.analytics.getdevteam.ai",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
analytics.screen("Home", { tab: "featured" }); // screen view
|
|
22
|
+
analytics.track("add_to_cart", { sku: "A-1" });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`@react-native-async-storage/async-storage` lets identity survive app restarts.
|
|
26
|
+
The SDK auto-flushes when the app goes to the background.
|
|
27
|
+
On a physical device use your machine's LAN IP instead of `localhost` when pointing at a local backend.
|
|
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
|
+
|
|
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.
|
|
70
|
+
|
|
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
|
|
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.
|
|
628
|
+
var REACT_NATIVE_SDK_VERSION = "0.3.0";
|
|
112
629
|
function createAnalytics(config) {
|
|
113
|
-
const client = (0,
|
|
630
|
+
const client = (0, import_analytics_core2.createClient)({
|
|
114
631
|
...config,
|
|
115
632
|
storage: config.storage ?? createAsyncStorageAdapter()
|
|
116
633
|
});
|
|
117
|
-
|
|
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,
|