@fixback/expo 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.
Files changed (58) hide show
  1. package/README.md +72 -8
  2. package/dist/FeedbackModal.d.ts +10 -2
  3. package/dist/FeedbackModal.js +84 -36
  4. package/dist/FeedbackModal.js.map +1 -0
  5. package/dist/FixbackProvider.d.ts +12 -0
  6. package/dist/FixbackProvider.js +46 -23
  7. package/dist/FixbackProvider.js.map +1 -0
  8. package/dist/adapters.js +73 -23
  9. package/dist/adapters.js.map +1 -0
  10. package/dist/breadcrumbs.d.ts +21 -222
  11. package/dist/breadcrumbs.js +45 -605
  12. package/dist/breadcrumbs.js.map +1 -0
  13. package/dist/client.d.ts +65 -67
  14. package/dist/client.js +288 -151
  15. package/dist/client.js.map +1 -0
  16. package/dist/connect.d.ts +60 -0
  17. package/dist/connect.js +63 -0
  18. package/dist/connect.js.map +1 -0
  19. package/dist/error-capture.d.ts +29 -79
  20. package/dist/error-capture.js +77 -295
  21. package/dist/error-capture.js.map +1 -0
  22. package/dist/http.d.ts +10 -25
  23. package/dist/http.js +15 -12
  24. package/dist/http.js.map +1 -0
  25. package/dist/identity.d.ts +11 -11
  26. package/dist/identity.js +23 -28
  27. package/dist/identity.js.map +1 -0
  28. package/dist/index.d.ts +6 -5
  29. package/dist/index.js +22 -5
  30. package/dist/index.js.map +1 -0
  31. package/dist/origin.d.ts +21 -0
  32. package/dist/origin.js +45 -0
  33. package/dist/origin.js.map +1 -0
  34. package/dist/package.json +3 -0
  35. package/dist/report.d.ts +28 -63
  36. package/dist/report.js +33 -25
  37. package/dist/report.js.map +1 -0
  38. package/dist/reporter-session-store.d.ts +27 -0
  39. package/dist/reporter-session-store.js +53 -0
  40. package/dist/reporter-session-store.js.map +1 -0
  41. package/dist/shake.d.ts +26 -0
  42. package/dist/shake.js +32 -7
  43. package/dist/shake.js.map +1 -0
  44. package/dist/submit.d.ts +24 -15
  45. package/dist/submit.js +30 -47
  46. package/dist/submit.js.map +1 -0
  47. package/dist/tokens.js +5 -1
  48. package/dist/tokens.js.map +1 -0
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +5 -1
  51. package/dist/version.js.map +1 -0
  52. package/package.json +14 -7
  53. package/dist/auto-report-backoff.d.ts +0 -42
  54. package/dist/auto-report-backoff.js +0 -67
  55. package/dist/boot.d.ts +0 -65
  56. package/dist/boot.js +0 -60
  57. package/dist/scrub.d.ts +0 -55
  58. package/dist/scrub.js +0 -184
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * The Expo **Reporter session** store (ADR-0032) — the storage binding only.
4
+ *
5
+ * After a Connect the SDK holds a compact signed token per origin and forwards it on
6
+ * every boot; the boot answer hands back a refreshed one (30-day sliding). On React
7
+ * Native the token is a real credential, so it lives in **Expo SecureStore**
8
+ * (Keychain / Keystore), not AsyncStorage — under a key **scoped by publishable key**
9
+ * (so two Projects in one app keep separate sessions), the key format shared with every
10
+ * SDK (`@fixback/sdk-core`). Persistence is best-effort: an unavailable or failing
11
+ * secure store simply means no stored session — the SDK falls back to anonymous rather
12
+ * than throwing, so it never disturbs the host app.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.readReporterSession = readReporterSession;
16
+ exports.writeReporterSession = writeReporterSession;
17
+ exports.clearReporterSession = clearReporterSession;
18
+ const sdk_core_1 = require("@fixback/sdk-core");
19
+ /** Read the stored Reporter session token for this Project, or `null`. Best-effort. */
20
+ async function readReporterSession(store, publishableKey) {
21
+ if (!store)
22
+ return null;
23
+ try {
24
+ const value = await store.getItemAsync((0, sdk_core_1.reporterSessionStorageKey)(publishableKey));
25
+ return typeof value === "string" && value.length > 0 ? value : null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /** Persist a Reporter session token for this Project. Best-effort. */
32
+ async function writeReporterSession(store, publishableKey, token) {
33
+ if (!store)
34
+ return;
35
+ try {
36
+ await store.setItemAsync((0, sdk_core_1.reporterSessionStorageKey)(publishableKey), token);
37
+ }
38
+ catch {
39
+ // Secure store blocked — the session lives only for this launch, which still works.
40
+ }
41
+ }
42
+ /** Clear the stored Reporter session token for this Project (sign out). Best-effort. */
43
+ async function clearReporterSession(store, publishableKey) {
44
+ if (!store)
45
+ return;
46
+ try {
47
+ await store.deleteItemAsync((0, sdk_core_1.reporterSessionStorageKey)(publishableKey));
48
+ }
49
+ catch {
50
+ // Nothing to do — an unreadable store holds nothing to clear.
51
+ }
52
+ }
53
+ //# sourceMappingURL=reporter-session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter-session-store.js","sourceRoot":"","sources":["../src/reporter-session-store.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AAeH,kDAWC;AAGD,oDAWC;AAGD,oDAUC;AAnDD,gDAA8D;AAY9D,uFAAuF;AAChF,KAAK,UAAU,mBAAmB,CACvC,KAA2C,EAC3C,cAAsB;IAEtB,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,IAAA,oCAAyB,EAAC,cAAc,CAAC,CAAC,CAAC;QAClF,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,sEAAsE;AAC/D,KAAK,UAAU,oBAAoB,CACxC,KAA2C,EAC3C,cAAsB,EACtB,KAAa;IAEb,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,YAAY,CAAC,IAAA,oCAAyB,EAAC,cAAc,CAAC,EAAE,KAAK,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,oFAAoF;IACtF,CAAC;AACH,CAAC;AAED,wFAAwF;AACjF,KAAK,UAAU,oBAAoB,CACxC,KAA2C,EAC3C,cAAsB;IAEtB,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,eAAe,CAAC,IAAA,oCAAyB,EAAC,cAAc,CAAC,CAAC,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;IAChE,CAAC;AACH,CAAC"}
package/dist/shake.d.ts CHANGED
@@ -49,3 +49,29 @@ export interface ShakeDetector {
49
49
  * every sample, so memory stays bounded however long the app runs.
50
50
  */
51
51
  export declare function createShakeDetector(options: ShakeDetectorOptions): ShakeDetector;
52
+ /** How often the accelerometer reports while the gesture is armed. */
53
+ export declare const DEFAULT_SHAKE_SAMPLE_INTERVAL_MS = 80;
54
+ /** The shake gesture's init options: the tuning plus an off switch. */
55
+ export interface ShakeOptions extends Partial<ShakeTuning> {
56
+ /** Arm the shake gesture. Defaults to `true`. */
57
+ readonly enabled?: boolean;
58
+ /**
59
+ * The accelerometer update interval to request, in ms. Defaults to
60
+ * {@link DEFAULT_SHAKE_SAMPLE_INTERVAL_MS}. The interval is global to the
61
+ * sensor in `expo-sensors` — the SDK only sets it when no other listener is
62
+ * registered, and apps that drive the accelerometer themselves should align
63
+ * this with their own setting.
64
+ */
65
+ readonly sampleIntervalMs?: number;
66
+ }
67
+ /** The resolved shake configuration the provider arms the gesture with. */
68
+ export interface ResolvedShakeConfig extends ShakeTuning {
69
+ readonly enabled: boolean;
70
+ readonly sampleIntervalMs: number;
71
+ }
72
+ /**
73
+ * Fill every shake knob from the init options, defaulting each independently —
74
+ * so `shake: { thresholdG: 3 }` retunes only the threshold and keeps the rest of
75
+ * spec 0004 §B's numbers.
76
+ */
77
+ export declare function resolveShakeConfig(shake?: ShakeOptions): ResolvedShakeConfig;
package/dist/shake.js CHANGED
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /**
2
3
  * The pure **shake detector** — the mobile launcher's brain (spec 0004 §B,
3
4
  * ADR-0021). It consumes raw accelerometer samples (in g, gravity included, as
@@ -9,8 +10,12 @@
9
10
  * Kept free of any React Native import so the recogniser is unit-tested
10
11
  * deterministically; the sensor wiring lives in `adapters.ts`.
11
12
  */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.DEFAULT_SHAKE_SAMPLE_INTERVAL_MS = exports.DEFAULT_SHAKE_TUNING = void 0;
15
+ exports.createShakeDetector = createShakeDetector;
16
+ exports.resolveShakeConfig = resolveShakeConfig;
12
17
  /** The default tuning: a deliberate shake, not a bump or a phone put down hard. */
13
- export const DEFAULT_SHAKE_TUNING = {
18
+ exports.DEFAULT_SHAKE_TUNING = {
14
19
  thresholdG: 2.0,
15
20
  minPeaks: 3,
16
21
  windowMs: 900,
@@ -26,12 +31,12 @@ function tuned(value, fallback) {
26
31
  * Create a shake recogniser. The peak list is pruned to the sliding window on
27
32
  * every sample, so memory stays bounded however long the app runs.
28
33
  */
29
- export function createShakeDetector(options) {
30
- const thresholdG = tuned(options.thresholdG, DEFAULT_SHAKE_TUNING.thresholdG);
31
- const minPeaks = Math.max(1, Math.floor(tuned(options.minPeaks, DEFAULT_SHAKE_TUNING.minPeaks)));
32
- const windowMs = tuned(options.windowMs, DEFAULT_SHAKE_TUNING.windowMs);
33
- const minGapMs = tuned(options.minGapMs, DEFAULT_SHAKE_TUNING.minGapMs);
34
- const cooldownMs = tuned(options.cooldownMs, DEFAULT_SHAKE_TUNING.cooldownMs);
34
+ function createShakeDetector(options) {
35
+ const thresholdG = tuned(options.thresholdG, exports.DEFAULT_SHAKE_TUNING.thresholdG);
36
+ const minPeaks = Math.max(1, Math.floor(tuned(options.minPeaks, exports.DEFAULT_SHAKE_TUNING.minPeaks)));
37
+ const windowMs = tuned(options.windowMs, exports.DEFAULT_SHAKE_TUNING.windowMs);
38
+ const minGapMs = tuned(options.minGapMs, exports.DEFAULT_SHAKE_TUNING.minGapMs);
39
+ const cooldownMs = tuned(options.cooldownMs, exports.DEFAULT_SHAKE_TUNING.cooldownMs);
35
40
  /** Epoch-ms times of the peaks inside the current window. */
36
41
  let peaks = [];
37
42
  let lastPeakAt = Number.NEGATIVE_INFINITY;
@@ -77,3 +82,23 @@ export function createShakeDetector(options) {
77
82
  },
78
83
  };
79
84
  }
85
+ // --- The gesture's init options ------------------------------------------------
86
+ /** How often the accelerometer reports while the gesture is armed. */
87
+ exports.DEFAULT_SHAKE_SAMPLE_INTERVAL_MS = 80;
88
+ /**
89
+ * Fill every shake knob from the init options, defaulting each independently —
90
+ * so `shake: { thresholdG: 3 }` retunes only the threshold and keeps the rest of
91
+ * spec 0004 §B's numbers.
92
+ */
93
+ function resolveShakeConfig(shake = {}) {
94
+ return {
95
+ enabled: shake.enabled !== false,
96
+ sampleIntervalMs: shake.sampleIntervalMs ?? exports.DEFAULT_SHAKE_SAMPLE_INTERVAL_MS,
97
+ thresholdG: shake.thresholdG ?? exports.DEFAULT_SHAKE_TUNING.thresholdG,
98
+ minPeaks: shake.minPeaks ?? exports.DEFAULT_SHAKE_TUNING.minPeaks,
99
+ windowMs: shake.windowMs ?? exports.DEFAULT_SHAKE_TUNING.windowMs,
100
+ minGapMs: shake.minGapMs ?? exports.DEFAULT_SHAKE_TUNING.minGapMs,
101
+ cooldownMs: shake.cooldownMs ?? exports.DEFAULT_SHAKE_TUNING.cooldownMs,
102
+ };
103
+ }
104
+ //# sourceMappingURL=shake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shake.js","sourceRoot":"","sources":["../src/shake.ts"],"names":[],"mappings":"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"}
package/dist/submit.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
- * The multipart feedback submission — the transport half of the capture loop,
3
- * kept file-parallel with `packages/sdk/src/submit.ts` (ADR-0021).
2
+ * The multipart feedback submission — the transport half of the capture loop.
3
+ *
4
+ * The endpoint, the identity compaction, the backpressure window, and the `fetch`
5
+ * surface are all shared with the browser SDK through `@fixback/sdk-core`
6
+ * (ADR-0028); what stays here is the mobile envelope.
4
7
  *
5
8
  * `POST /api/ingest/feedback` is a multipart request: a single text part
6
9
  * `payload` carrying the JSON (publishable key + identity evidence + content)
@@ -14,13 +17,12 @@
14
17
  * The content-type header is deliberately not set, so the runtime writes the
15
18
  * `multipart/form-data` boundary itself. Like `requestBoot`, this never throws:
16
19
  * an unreachable Fixback or a refusal is a quiet result, so a Fixback problem
17
- * never breaks the host app. Automatic reports (`source: auto`) honour ingest's
20
+ * never breaks the host app. Automatic reports (`source: error`) honour ingest's
18
21
  * `429` + `Retry-After` backpressure; manual reports are never gated.
19
22
  */
20
- import { AutoReportBackoff } from "./auto-report-backoff";
21
- import type { IdentityInputs, ReporterTier } from "./boot";
22
- import { type FetchLike, type FormDataFactory } from "./http";
23
- import type { ReportContent } from "./report";
23
+ import { AutoReportBackoff, feedbackEndpoint, type FetchLike, type IdentityInputs, type ReportContent, type ReporterTier } from "@fixback/sdk-core";
24
+ import { type FormDataFactory } from "./http";
25
+ export { feedbackEndpoint };
24
26
  /** What ingest returns for an accepted submission (vendored server shape). */
25
27
  export interface RecordedFeedback {
26
28
  readonly feedbackId: string;
@@ -41,8 +43,6 @@ export interface ScreenshotFile {
41
43
  /** Everything a single feedback submission carries. */
42
44
  export interface SubmitInput {
43
45
  readonly key: string;
44
- /** The configured origin, sent as the `Origin` header (spec 0004 §A). */
45
- readonly origin: string;
46
46
  readonly identity?: IdentityInputs;
47
47
  readonly content: ReportContent;
48
48
  readonly screenshot?: ScreenshotFile | null;
@@ -57,7 +57,7 @@ export type SubmitResult = {
57
57
  readonly status?: number;
58
58
  } | {
59
59
  /**
60
- * A `source: auto` report shed under ingest backpressure — either held
60
+ * A `source: error` report shed under ingest backpressure — either held
61
61
  * locally because the window is still open, or answered `429` by ingest,
62
62
  * which opened/extended the window. Manual reports never produce this.
63
63
  */
@@ -72,16 +72,25 @@ export type SubmitResult = {
72
72
  * The abandoned request keeps running in the background harmlessly.
73
73
  */
74
74
  export declare const DEFAULT_SUBMIT_TIMEOUT_MS = 30000;
75
- /** Injectable transport collaborators, defaulted to the real implementations. */
75
+ /**
76
+ * Injectable transport collaborators, defaulted to the real implementations —
77
+ * the same deps-object shape `requestBoot` takes, `origin` included.
78
+ */
76
79
  export interface SubmitDeps {
77
- readonly fetchImpl?: FetchLike;
80
+ /** The `fetch` to call. Defaults to the runtime's global. */
81
+ readonly fetch?: FetchLike;
82
+ /** Creates the multipart form. Defaults to React Native's `FormData`. */
78
83
  readonly formData?: FormDataFactory;
84
+ /**
85
+ * The origin to declare explicitly (spec 0004 §A): React Native attaches no
86
+ * `Origin` header of its own, so the SDK sends the configured one and the server
87
+ * matches it against the Project's allowlist.
88
+ */
89
+ readonly origin?: string;
79
90
  readonly backoff?: AutoReportBackoff;
80
91
  /** Overall request timeout in ms; `0` disables. Defaults to {@link DEFAULT_SUBMIT_TIMEOUT_MS}. */
81
92
  readonly timeoutMs?: number;
82
93
  }
83
- /** Join an API base URL with the feedback path, tolerating a trailing slash. */
84
- export declare function feedbackEndpoint(apiUrl: string): string;
85
94
  /**
86
95
  * Submit a report to ingest. Assembles the `payload` JSON (key + identity +
87
96
  * content, with an explicit `source` stamped) and the optional screenshot file
@@ -89,7 +98,7 @@ export declare function feedbackEndpoint(apiUrl: string): string;
89
98
  * and resolves to the recorded Feedback on success or a named failure
90
99
  * otherwise.
91
100
  *
92
- * A `source: auto` report first consults the shared backpressure window: while
101
+ * A `source: error` report first consults the shared backpressure window: while
93
102
  * it is open the report is dropped without a request. Ingest's `429` +
94
103
  * `Retry-After` opens/extends that window. Manual reports (`source: reporter`,
95
104
  * the default) never consult the window and a non-2xx for them stays a plain
package/dist/submit.js CHANGED
@@ -1,6 +1,10 @@
1
+ "use strict";
1
2
  /**
2
- * The multipart feedback submission — the transport half of the capture loop,
3
- * kept file-parallel with `packages/sdk/src/submit.ts` (ADR-0021).
3
+ * The multipart feedback submission — the transport half of the capture loop.
4
+ *
5
+ * The endpoint, the identity compaction, the backpressure window, and the `fetch`
6
+ * surface are all shared with the browser SDK through `@fixback/sdk-core`
7
+ * (ADR-0028); what stays here is the mobile envelope.
4
8
  *
5
9
  * `POST /api/ingest/feedback` is a multipart request: a single text part
6
10
  * `payload` carrying the JSON (publishable key + identity evidence + content)
@@ -14,11 +18,15 @@
14
18
  * The content-type header is deliberately not set, so the runtime writes the
15
19
  * `multipart/form-data` boundary itself. Like `requestBoot`, this never throws:
16
20
  * an unreachable Fixback or a refusal is a quiet result, so a Fixback problem
17
- * never breaks the host app. Automatic reports (`source: auto`) honour ingest's
21
+ * never breaks the host app. Automatic reports (`source: error`) honour ingest's
18
22
  * `429` + `Retry-After` backpressure; manual reports are never gated.
19
23
  */
20
- import { AutoReportBackoff } from "./auto-report-backoff";
21
- import { globalFetch, globalFormData, } from "./http";
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.DEFAULT_SUBMIT_TIMEOUT_MS = exports.feedbackEndpoint = void 0;
26
+ exports.submitReport = submitReport;
27
+ const sdk_core_1 = require("@fixback/sdk-core");
28
+ Object.defineProperty(exports, "feedbackEndpoint", { enumerable: true, get: function () { return sdk_core_1.feedbackEndpoint; } });
29
+ const http_1 = require("./http");
22
30
  /** The multipart field the screenshot binary is sent under (mirrors the server). */
23
31
  const SCREENSHOT_FIELD = "screenshot";
24
32
  /**
@@ -27,38 +35,12 @@ const SCREENSHOT_FIELD = "screenshot";
27
35
  * timeout, so without this a stalled connection would hang a report forever.
28
36
  * The abandoned request keeps running in the background harmlessly.
29
37
  */
30
- export const DEFAULT_SUBMIT_TIMEOUT_MS = 30_000;
38
+ exports.DEFAULT_SUBMIT_TIMEOUT_MS = 30_000;
31
39
  /**
32
- * The shared `source: auto` backpressure window for the app. One instance so a
40
+ * The shared `source: error` backpressure window for the app. One instance so a
33
41
  * `429` from an auto-report holds the auto-reports that follow it.
34
42
  */
35
- const defaultBackoff = new AutoReportBackoff();
36
- /** Join an API base URL with the feedback path, tolerating a trailing slash. */
37
- export function feedbackEndpoint(apiUrl) {
38
- return `${apiUrl.replace(/\/+$/, "")}/api/ingest/feedback`;
39
- }
40
- /** Drop `undefined` identity fields so the payload carries only what was given. */
41
- function compactIdentity(identity) {
42
- if (!identity)
43
- return {};
44
- const out = {};
45
- if (identity.signedIdentity)
46
- out.signedIdentity = identity.signedIdentity;
47
- if (identity.reporterId)
48
- out.reporterId = identity.reporterId;
49
- if (identity.anonymousId)
50
- out.anonymousId = identity.anonymousId;
51
- return out;
52
- }
53
- /** Read one response header defensively — `null` when unavailable or on any throw. */
54
- function safeHeader(response, name) {
55
- try {
56
- return response.headers?.get?.(name) ?? null;
57
- }
58
- catch {
59
- return null;
60
- }
61
- }
43
+ const defaultBackoff = new sdk_core_1.AutoReportBackoff();
62
44
  /**
63
45
  * Submit a report to ingest. Assembles the `payload` JSON (key + identity +
64
46
  * content, with an explicit `source` stamped) and the optional screenshot file
@@ -66,32 +48,32 @@ function safeHeader(response, name) {
66
48
  * and resolves to the recorded Feedback on success or a named failure
67
49
  * otherwise.
68
50
  *
69
- * A `source: auto` report first consults the shared backpressure window: while
51
+ * A `source: error` report first consults the shared backpressure window: while
70
52
  * it is open the report is dropped without a request. Ingest's `429` +
71
53
  * `Retry-After` opens/extends that window. Manual reports (`source: reporter`,
72
54
  * the default) never consult the window and a non-2xx for them stays a plain
73
55
  * refusal. The `deps` seams exist purely so the call is testable.
74
56
  */
75
- export async function submitReport(apiUrl, input, deps = {}) {
57
+ async function submitReport(apiUrl, input, deps = {}) {
76
58
  const source = input.content.source ?? "reporter";
77
- const isAuto = source === "auto";
59
+ const isError = source === "error";
78
60
  const backoff = deps.backoff ?? defaultBackoff;
79
61
  // Client-side backpressure: shed auto-reports while the hold window is open,
80
62
  // without touching the network. Manual reports are never gated.
81
- if (isAuto && backoff.isPaused()) {
63
+ if (isError && backoff.isPaused()) {
82
64
  return {
83
65
  ok: false,
84
66
  reason: "backpressure",
85
67
  retryAfterSeconds: backoff.retryAfterSeconds(),
86
68
  };
87
69
  }
88
- const doFetch = deps.fetchImpl ?? globalFetch();
89
- const makeForm = deps.formData ?? globalFormData();
70
+ const doFetch = deps.fetch ?? (0, sdk_core_1.resolveFetch)();
71
+ const makeForm = deps.formData ?? (0, http_1.globalFormData)();
90
72
  if (!doFetch || !makeForm)
91
73
  return { ok: false, reason: "unreachable" };
92
74
  const payload = {
93
75
  key: input.key,
94
- ...compactIdentity(input.identity),
76
+ ...(0, sdk_core_1.compactIdentity)(input.identity),
95
77
  ...input.content,
96
78
  source,
97
79
  };
@@ -107,13 +89,13 @@ export async function submitReport(apiUrl, input, deps = {}) {
107
89
  type: input.screenshot.type ?? "image/png",
108
90
  });
109
91
  }
110
- const timeoutMs = deps.timeoutMs ?? DEFAULT_SUBMIT_TIMEOUT_MS;
92
+ const timeoutMs = deps.timeoutMs ?? exports.DEFAULT_SUBMIT_TIMEOUT_MS;
111
93
  let timer;
112
94
  let response;
113
95
  try {
114
- const request = doFetch(feedbackEndpoint(apiUrl), {
96
+ const request = doFetch((0, sdk_core_1.feedbackEndpoint)(apiUrl), {
115
97
  method: "POST",
116
- headers: { origin: input.origin },
98
+ headers: deps.origin ? { origin: deps.origin } : {},
117
99
  body: form,
118
100
  });
119
101
  response =
@@ -137,14 +119,14 @@ export async function submitReport(apiUrl, input, deps = {}) {
137
119
  // A `429` for an auto-report is backpressure, not a refusal: open/extend
138
120
  // the hold window from `Retry-After` and report the wait. Manual reports
139
121
  // never 429 here, so any non-2xx for them stays a plain refusal.
140
- if (isAuto && response.status === 429) {
141
- const retryAfterSeconds = backoff.hold(safeHeader(response, "Retry-After"));
122
+ if (isError && response.status === 429) {
123
+ const retryAfterSeconds = backoff.hold((0, sdk_core_1.safeHeader)(response, "Retry-After"));
142
124
  return { ok: false, reason: "backpressure", retryAfterSeconds };
143
125
  }
144
126
  return { ok: false, reason: "refused", status: response.status };
145
127
  }
146
128
  try {
147
- const feedback = (await response.json());
129
+ const feedback = (await response.json?.());
148
130
  return { ok: true, feedback };
149
131
  }
150
132
  catch {
@@ -152,3 +134,4 @@ export async function submitReport(apiUrl, input, deps = {}) {
152
134
  return { ok: true, feedback: null };
153
135
  }
154
136
  }
137
+ //# sourceMappingURL=submit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submit.js","sourceRoot":"","sources":["../src/submit.ts"],"names":[],"mappings":"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"}
package/dist/tokens.js CHANGED
@@ -1,3 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.signal = void 0;
1
4
  /**
2
5
  * A vendored slice of the **Signal** design tokens for the mobile composer.
3
6
  *
@@ -11,7 +14,7 @@
11
14
  * (the composer passes `Platform.select` for that — this module stays free of
12
15
  * React Native imports so pure code can use it too).
13
16
  */
14
- export const signal = {
17
+ exports.signal = {
15
18
  /* --fb-color-accent / tints */
16
19
  accent: "#2f6fed",
17
20
  accentTint: "#eaf1fe",
@@ -44,3 +47,4 @@ export const signal = {
44
47
  onEmphasis: "#ffffff",
45
48
  scrim: "rgba(15, 23, 32, 0.42)",
46
49
  };
50
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;GAYG;AACU,QAAA,MAAM,GAAG;IACpB,+BAA+B;IAC/B,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,SAAS;IACrB,YAAY,EAAE,SAAS;IAEvB,2BAA2B;IAC3B,IAAI,EAAE,SAAS;IACf,SAAS,EAAE,SAAS;IACpB,SAAS,EAAE,SAAS;IACpB,SAAS,EAAE,SAAS;IACpB,UAAU,EAAE,SAAS;IACrB,SAAS,EAAE,SAAS;IAEpB,wBAAwB;IACxB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;IACjB,aAAa,EAAE,SAAS;IACxB,YAAY,EAAE,SAAS;IAEvB,yFAAyF;IACzF,MAAM,EAAE,SAAS;IACjB,aAAa,EAAE,SAAS;IACxB,YAAY,EAAE,SAAS;IACvB,OAAO,EAAE,SAAS;IAClB,cAAc,EAAE,SAAS;IACzB,aAAa,EAAE,SAAS;IACxB,IAAI,EAAE,SAAS;IACf,WAAW,EAAE,SAAS;IACtB,UAAU,EAAE,SAAS;IAErB,cAAc;IACd,UAAU,EAAE,SAAS;IACrB,KAAK,EAAE,wBAAwB;CACvB,CAAC"}
package/dist/version.d.ts CHANGED
@@ -10,4 +10,4 @@
10
10
  * ever drift. Do not hand-edit this line to a value other than `package.json`'s
11
11
  * version.
12
12
  */
13
- export declare const EXPO_SDK_VERSION = "0.1.0";
13
+ export declare const EXPO_SDK_VERSION = "0.3.0";
package/dist/version.js CHANGED
@@ -1,3 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EXPO_SDK_VERSION = void 0;
1
4
  /**
2
5
  * The Expo SDK's own version string, reported to ingest as
3
6
  * `environment.sdkVersion`. Kept as an inlined constant rather than a runtime
@@ -10,4 +13,5 @@
10
13
  * ever drift. Do not hand-edit this line to a value other than `package.json`'s
11
14
  * version.
12
15
  */
13
- export const EXPO_SDK_VERSION = "0.1.0";
16
+ exports.EXPO_SDK_VERSION = "0.3.0";
17
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACU,QAAA,gBAAgB,GAAG,OAAO,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fixback/expo",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "The Fixback capture SDK for Expo / React Native apps — shake the device to report feedback with trace Evidence.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,13 +11,12 @@
11
11
  "LICENSE"
12
12
  ],
13
13
  "main": "./dist/index.js",
14
- "module": "./dist/index.js",
15
14
  "types": "./dist/index.d.ts",
16
15
  "exports": {
17
16
  ".": {
18
17
  "types": "./dist/index.d.ts",
19
18
  "import": "./dist/index.js",
20
- "default": "./dist/index.js"
19
+ "require": "./dist/index.js"
21
20
  },
22
21
  "./package.json": "./package.json"
23
22
  },
@@ -35,27 +34,35 @@
35
34
  "url": "git+https://github.com/wemuda/fixback.git",
36
35
  "directory": "packages/expo"
37
36
  },
38
- "homepage": "https://github.com/wemuda/fixback/tree/master/packages/expo#readme",
39
- "bugs": "https://github.com/wemuda/fixback/issues",
37
+ "homepage": "https://docs.fixback.dev/sdk/expo",
38
+ "bugs": "https://docs.fixback.dev",
40
39
  "publishConfig": {
41
40
  "access": "public"
42
41
  },
43
42
  "peerDependencies": {
44
43
  "@react-native-async-storage/async-storage": ">=1.23.0",
44
+ "expo-secure-store": ">=13.0.0",
45
45
  "expo-sensors": ">=13.0.0",
46
+ "expo-web-browser": ">=13.0.0",
46
47
  "react": ">=18.0.0",
47
48
  "react-native": ">=0.74.0",
48
49
  "react-native-view-shot": ">=3.8.0"
49
50
  },
50
51
  "devDependencies": {
51
- "@types/react": "^19.0.0",
52
+ "@types/react": "^19.2.18",
52
53
  "react": "19.2.8",
53
54
  "react-native": "^0.81.0",
54
55
  "typescript": "5.9.3",
55
56
  "vitest": "^4.1.10"
56
57
  },
58
+ "dependencies": {
59
+ "@fixback/sdk-core": "0.3.0"
60
+ },
61
+ "engines": {
62
+ "node": ">=20.12"
63
+ },
57
64
  "scripts": {
58
- "build": "tsc -p tsconfig.build.json",
65
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/finalize-cjs.mjs dist",
59
66
  "lint": "eslint .",
60
67
  "typecheck": "tsc --noEmit",
61
68
  "test": "vitest run"
@@ -1,42 +0,0 @@
1
- /**
2
- * Client-side backpressure for **automatic** error reports — a verbatim port of
3
- * `packages/sdk/src/auto-report-backoff.ts` (spec 0003 §E/§H). When ingest
4
- * sheds `source: auto` load it answers `429` with a `Retry-After`; the SDK
5
- * honours it by holding a pause window during which further `source: auto`
6
- * reports are dropped without touching the network. **Manual** reports — a
7
- * human tapping Send — never consult this gate.
8
- */
9
- /** A source of the current time in epoch milliseconds — injectable for tests. */
10
- export type Clock = () => number;
11
- /** The hold window applied when a `429` carries no usable `Retry-After`. */
12
- export declare const DEFAULT_RETRY_AFTER_SECONDS = 60;
13
- /**
14
- * Parse a `Retry-After` header into whole seconds to hold for. Handles both
15
- * HTTP forms — a delta-seconds integer and an HTTP-date (measured from `now`,
16
- * rounded up and clamped at zero) — and falls back to
17
- * {@link DEFAULT_RETRY_AFTER_SECONDS} when the header is absent, blank, or
18
- * unparseable.
19
- */
20
- export declare function parseRetryAfter(header: string | null | undefined, now: number): number;
21
- /**
22
- * A single pause window for `source: auto` reports. `hold` opens (or extends)
23
- * it from a `429`'s `Retry-After`; `isPaused` reports whether it is still open.
24
- * The default instance in `submit.ts` is shared across the app's reports so the
25
- * hold persists across successive auto submissions.
26
- */
27
- export declare class AutoReportBackoff {
28
- private readonly now;
29
- /** Epoch ms until which `source: auto` reports are held; `0` when clear. */
30
- private pausedUntil;
31
- constructor(now?: Clock);
32
- /** Is the `source: auto` pause window currently open? */
33
- isPaused(): boolean;
34
- /** Whole seconds remaining in the pause window (`0` when clear). */
35
- retryAfterSeconds(): number;
36
- /**
37
- * Open (or extend) the window from a `429`'s `Retry-After` value, returning
38
- * the seconds it will hold for. The window only ever grows — a shorter later
39
- * hold never clips a longer one already in effect.
40
- */
41
- hold(retryAfterHeader: string | null | undefined): number;
42
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * Client-side backpressure for **automatic** error reports — a verbatim port of
3
- * `packages/sdk/src/auto-report-backoff.ts` (spec 0003 §E/§H). When ingest
4
- * sheds `source: auto` load it answers `429` with a `Retry-After`; the SDK
5
- * honours it by holding a pause window during which further `source: auto`
6
- * reports are dropped without touching the network. **Manual** reports — a
7
- * human tapping Send — never consult this gate.
8
- */
9
- /** The hold window applied when a `429` carries no usable `Retry-After`. */
10
- export const DEFAULT_RETRY_AFTER_SECONDS = 60;
11
- /**
12
- * Parse a `Retry-After` header into whole seconds to hold for. Handles both
13
- * HTTP forms — a delta-seconds integer and an HTTP-date (measured from `now`,
14
- * rounded up and clamped at zero) — and falls back to
15
- * {@link DEFAULT_RETRY_AFTER_SECONDS} when the header is absent, blank, or
16
- * unparseable.
17
- */
18
- export function parseRetryAfter(header, now) {
19
- if (header == null)
20
- return DEFAULT_RETRY_AFTER_SECONDS;
21
- const value = header.trim();
22
- if (value === "")
23
- return DEFAULT_RETRY_AFTER_SECONDS;
24
- if (/^\d+$/.test(value)) {
25
- return Number(value);
26
- }
27
- const when = Date.parse(value);
28
- if (!Number.isNaN(when)) {
29
- return Math.max(0, Math.ceil((when - now) / 1000));
30
- }
31
- return DEFAULT_RETRY_AFTER_SECONDS;
32
- }
33
- /**
34
- * A single pause window for `source: auto` reports. `hold` opens (or extends)
35
- * it from a `429`'s `Retry-After`; `isPaused` reports whether it is still open.
36
- * The default instance in `submit.ts` is shared across the app's reports so the
37
- * hold persists across successive auto submissions.
38
- */
39
- export class AutoReportBackoff {
40
- now;
41
- /** Epoch ms until which `source: auto` reports are held; `0` when clear. */
42
- pausedUntil = 0;
43
- constructor(now = Date.now) {
44
- this.now = now;
45
- }
46
- /** Is the `source: auto` pause window currently open? */
47
- isPaused() {
48
- return this.now() < this.pausedUntil;
49
- }
50
- /** Whole seconds remaining in the pause window (`0` when clear). */
51
- retryAfterSeconds() {
52
- return Math.max(0, Math.ceil((this.pausedUntil - this.now()) / 1000));
53
- }
54
- /**
55
- * Open (or extend) the window from a `429`'s `Retry-After` value, returning
56
- * the seconds it will hold for. The window only ever grows — a shorter later
57
- * hold never clips a longer one already in effect.
58
- */
59
- hold(retryAfterHeader) {
60
- const now = this.now();
61
- const seconds = parseRetryAfter(retryAfterHeader, now);
62
- const until = now + seconds * 1000;
63
- if (until > this.pausedUntil)
64
- this.pausedUntil = until;
65
- return seconds;
66
- }
67
- }