@fixback/expo 0.9.0 → 0.9.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.
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ /**
3
+ * Boot, and the capture its answer arms (spec 0003 §G ported to mobile, spec 0004).
4
+ *
5
+ * {@link Boot.bootAndArm} is the round trip `start`, `signIn` and `signOut` all end in:
6
+ * it swaps in the refreshed Reporter session, records the Account and tier for the
7
+ * identity row, raises or lowers maintenance mode's notice (and schedules its
8
+ * recheck), and arms capture only while a submission would be accepted — the trace
9
+ * hooks, mobile replay (`./client-replay`), and automatic error capture — or says why
10
+ * the SDK is dormant.
11
+ *
12
+ * One piece of the headless client (`./client`), created over the state every piece
13
+ * shares (`./client-state`).
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.createBoot = createBoot;
17
+ const sdk_core_1 = require("@fixback/sdk-core");
18
+ const breadcrumbs_1 = require("./breadcrumbs");
19
+ const client_connect_1 = require("./client-connect");
20
+ const client_state_1 = require("./client-state");
21
+ const error_capture_1 = require("./error-capture");
22
+ const reporter_session_store_1 = require("./reporter-session-store");
23
+ const submit_1 = require("./submit");
24
+ const version_1 = require("./version");
25
+ /** Boot and capture arming over the client's shared state, with replay's arming. */
26
+ function createBoot(s, replay) {
27
+ const { options, adapters, warn, now, readEnvironment } = s;
28
+ const { key, origin, apiUrl, release, propagateTo, buffer } = s;
29
+ const { armReplay, disarmReplay, freezeAndEncodeReplay } = replay;
30
+ function setMaintenance(next) {
31
+ const changed = (s.maintenance === null) !== (next === null) ||
32
+ s.maintenance?.message !== next?.message ||
33
+ s.maintenance?.until !== next?.until;
34
+ s.maintenance = next;
35
+ if (next !== null)
36
+ scheduleMaintenanceRecheck();
37
+ if (!changed)
38
+ return;
39
+ for (const listener of s.maintenanceListeners) {
40
+ try {
41
+ listener(next);
42
+ }
43
+ catch {
44
+ /* a listener must never break the client */
45
+ }
46
+ }
47
+ }
48
+ /** Ask boot again in a minute, once, so the notice comes down on its own (the maintenance-mode ADR). */
49
+ function scheduleMaintenanceRecheck() {
50
+ if (!s.running)
51
+ return;
52
+ const gen = s.generation;
53
+ s.maintenanceRecheck.schedule(() => {
54
+ if (gen === s.generation)
55
+ void bootAndArm(gen, { recheck: true }).catch(() => undefined);
56
+ });
57
+ }
58
+ function clearMaintenanceRecheck() {
59
+ s.maintenanceRecheck.cancel();
60
+ }
61
+ /** Tear down the trace hooks (best-effort), so they can be re-armed. */
62
+ function disarmTrace() {
63
+ try {
64
+ s.teardownTrace?.();
65
+ }
66
+ catch {
67
+ /* teardown is best-effort */
68
+ }
69
+ s.teardownTrace = null;
70
+ }
71
+ /** Tear down the trace + auto-capture hooks (best-effort), so they can be re-armed. */
72
+ function disarmCapture() {
73
+ disarmTrace();
74
+ disarmReplay();
75
+ try {
76
+ s.autoCapture?.destroy();
77
+ }
78
+ catch {
79
+ /* teardown is best-effort */
80
+ }
81
+ s.autoCapture = null;
82
+ }
83
+ /**
84
+ * The effective per-stream capture flags: an init option overrides the served
85
+ * config, and an absent (or malformed) served value means **on** — the default-on
86
+ * rule (spec 0003 §C, ported). Called with no answer before boot has one, which is
87
+ * how the pre-boot arming lands on "both streams on".
88
+ */
89
+ function resolveStreams(served) {
90
+ return {
91
+ console: (0, sdk_core_1.streamEnabled)(options.capture?.console, served?.console),
92
+ network: (0, sdk_core_1.streamEnabled)(options.capture?.network, served?.network),
93
+ };
94
+ }
95
+ /**
96
+ * Install the trace instrumentation for the given per-stream flags, replacing
97
+ * whatever is installed — the teardown runs first, so a re-arm can never double-wrap
98
+ * `console` or `XMLHttpRequest`.
99
+ */
100
+ function armTrace(streams) {
101
+ disarmTrace();
102
+ s.teardownTrace = (0, breadcrumbs_1.instrumentBreadcrumbs)(buffer, {
103
+ consoleObj: adapters.consoleObj,
104
+ xhr: adapters.xhr,
105
+ ignoreUrl: (url) => (0, sdk_core_1.isFixbackApiRequest)(apiUrl, url),
106
+ now,
107
+ captureConsole: streams.console,
108
+ captureNetwork: streams.network,
109
+ propagateTo,
110
+ });
111
+ }
112
+ /**
113
+ * Boot with the current identity and reconcile capture + the identity row. Shared by
114
+ * `start`, `signIn`, and `signOut`: it swaps in the refreshed Reporter session token
115
+ * (30-day sliding), records the Account / tier / connect URL the boot answer carries,
116
+ * and arms capture only while a submission would be accepted — so a gated Project's
117
+ * shake gesture arms exactly when the Account becomes eligible, and drops when it signs
118
+ * out. `gen` is the caller's lifecycle generation; every await re-checks it so a
119
+ * destroyed (or restarted) client abandons this in-flight boot.
120
+ */
121
+ async function bootAndArm(gen, { recheck = false } = {}) {
122
+ // Boots are last-one-wins. Two can be in flight at once — a deep link redeemed
123
+ // while `start`'s boot is still out, a `signOut` racing a Connect — and they carry
124
+ // *different* identities, so an older answer landing last would undo a newer one:
125
+ // the shake gesture would stay withheld from a Reporter the server had just
126
+ // accepted, with the session sitting unused in SecureStore until the next launch.
127
+ const seq = ++s.bootSequence;
128
+ let failure = null;
129
+ const answer = await (0, sdk_core_1.requestBoot)(apiUrl, { key, ...(0, sdk_core_1.compactIdentity)(s.identity) }, {
130
+ fetch: adapters.fetch,
131
+ origin,
132
+ onFailure: (reason) => {
133
+ failure = reason;
134
+ },
135
+ });
136
+ if (gen !== s.generation || seq !== s.bootSequence)
137
+ return;
138
+ // Mid-maintenance, an unanswered boot is Fixback restarting, not a reason to disarm:
139
+ // keep the notice up and ask again (the maintenance-mode ADR).
140
+ if (!answer && s.maintenance !== null && s.status === "ready") {
141
+ scheduleMaintenanceRecheck();
142
+ return;
143
+ }
144
+ s.boot = answer;
145
+ // What the answer settles, read the way every capture SDK reads it (`reconcileBoot`).
146
+ const settled = (0, sdk_core_1.reconcileBoot)(answer);
147
+ // Swap in the refreshed session token (30-day sliding) when boot returned one.
148
+ if (settled.reporterSession) {
149
+ s.reporterSessionToken = settled.reporterSession;
150
+ s.identity = { ...s.identity, reporterSession: s.reporterSessionToken };
151
+ await (0, reporter_session_store_1.writeReporterSession)(adapters.secureStore, key, s.reporterSessionToken);
152
+ if (gen !== s.generation || seq !== s.bootSequence)
153
+ return;
154
+ }
155
+ s.currentTier = settled.tier;
156
+ s.account = settled.account;
157
+ s.connectUrl = settled.connectUrl;
158
+ (0, client_state_1.notifyIdentity)(s);
159
+ // Maintenance mode's notice (the maintenance-mode ADR) — sent only to a caller who could
160
+ // otherwise report, so it arms the composer as a yes does: it opens onto the notice.
161
+ const notice = settled.maintenance;
162
+ // A maintenance recheck on a client already armed changes only the notice: re-arming would
163
+ // restart the replay recorder every minute and drop the lead-up the next report should carry.
164
+ if (recheck && s.status === "ready" && settled.arm) {
165
+ setMaintenance(notice);
166
+ return;
167
+ }
168
+ if (answer && settled.arm) {
169
+ // Re-arm from a clean slate (the shared trace buffer persists across a re-arm, so
170
+ // no lead-up is lost) — a Connect that just made the Account eligible arms here.
171
+ disarmCapture();
172
+ armCapture(answer);
173
+ s.lastDormancyWarning = null;
174
+ setMaintenance(notice);
175
+ (0, client_state_1.setStatus)(s, "ready");
176
+ }
177
+ else {
178
+ setMaintenance(null);
179
+ // Say why, once per dormant boot, in a development build (`warn` is silent in
180
+ // production). A dormant SDK is otherwise indistinguishable on the device from
181
+ // one that was never installed — and the reason is right here in the answer the
182
+ // client already holds, or in the failure that stopped one arriving.
183
+ warnDormant(answer, failure);
184
+ // Not eligible (a gated Project the Account cannot yet report on, or a signed-out
185
+ // visitor): nothing armed, but `connectUrl` is retained so `signIn` still works.
186
+ // What the pre-boot arming optimistically recorded is dropped with it — this
187
+ // Account cannot submit, so holding its console and network lead-up in memory
188
+ // serves nothing. A later `signIn` re-arms and starts a fresh lead-up.
189
+ disarmCapture();
190
+ buffer.clear();
191
+ (0, client_state_1.setStatus)(s, "disabled");
192
+ }
193
+ }
194
+ /** Explain a dormant boot to the developer, at most once per distinct reason. */
195
+ function warnDormant(answer, failure) {
196
+ const message = (0, sdk_core_1.explainDormancy)({
197
+ answer,
198
+ failure,
199
+ apiUrl,
200
+ origin: origin || undefined,
201
+ // Connect needs a door this app can actually open — an auth session it can
202
+ // return through, or an in-app browser plus the seam the redirect comes back
203
+ // on. With neither, naming `signIn()` would send the developer down a dead end.
204
+ signInCall: (0, client_connect_1.canSignIn)(s) ? "Fixback.signIn()" : undefined,
205
+ });
206
+ if (!message || message === s.lastDormancyWarning)
207
+ return;
208
+ s.lastDormancyWarning = message;
209
+ warn(message);
210
+ }
211
+ /**
212
+ * Step two of `start`: with a boot answer that says a submission would be
213
+ * accepted, reconcile the trace instrumentation against the Project's served
214
+ * capture config and install (unless turned off) automatic error capture.
215
+ * Reaching here means the Gate is already satisfied, so an auto-error inherits
216
+ * the very tier boot derived.
217
+ *
218
+ * The trace was already armed optimistically at `start()`, so this is a
219
+ * **reconciliation**, not the first arming: a stream the Project turns off is both
220
+ * uninstrumented and **purged**, so what the optimistic window recorded for it can
221
+ * never ship.
222
+ */
223
+ function armCapture(answer) {
224
+ const streams = resolveStreams(answer.capture);
225
+ armTrace(streams);
226
+ if (!streams.console)
227
+ buffer.clearStream("console");
228
+ if (!streams.network)
229
+ buffer.clearStream("network");
230
+ // Replay arms **here and only here** — never optimistically before boot, the way
231
+ // the trace does. A trace that turns out to be unwanted is purged; frames already
232
+ // written to the cache are a recording that should not have started (ADR-0044).
233
+ armReplay(answer.capture);
234
+ if (options.autoCapture === false)
235
+ return;
236
+ s.autoCapture = (0, error_capture_1.installErrorCapture)({
237
+ apiUrl,
238
+ key,
239
+ origin,
240
+ identity: s.identity,
241
+ sdkVersion: version_1.EXPO_SDK_VERSION,
242
+ release,
243
+ buffer,
244
+ beforeSend: options.beforeSend,
245
+ scrub: options.scrub,
246
+ environment: readEnvironment,
247
+ currentUrl: () => (0, client_state_1.currentUrl)(s),
248
+ // The replay window as it stood at the throw (ADR-0044): frozen and encoded at
249
+ // filing time, on the rich first report only — a flush is a count update, and
250
+ // re-encoding the same window for it would be pure cost.
251
+ captureReplay: () => freezeAndEncodeReplay(),
252
+ errorUtils: adapters.errorUtils,
253
+ appState: adapters.appState,
254
+ deps: {
255
+ submitReport: (url, input) => (0, submit_1.submitReport)(url, input, (0, client_state_1.submitDeps)(s)),
256
+ },
257
+ now,
258
+ });
259
+ }
260
+ return {
261
+ bootAndArm,
262
+ resolveStreams,
263
+ armTrace,
264
+ disarmCapture,
265
+ setMaintenance,
266
+ clearMaintenanceRecheck,
267
+ };
268
+ }
269
+ //# sourceMappingURL=client-boot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-boot.js","sourceRoot":"","sources":["../src/client-boot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * **Connect** on the headless client (ADR-0066): the two doors `signIn()` opens
3
+ * (ADR-0052), the inbound deep links that finish a Connect started outside the app
4
+ * (ADR-0046), and `signOut()`.
5
+ *
6
+ * One piece of the headless client (`./client`), created over the state every piece
7
+ * shares (`./client-state`): the Reporter session token, the connect URL boot
8
+ * returned, the last inbound code, and the callers waiting on one. A Connect that lands
9
+ * re-boots as the Account through the boot it is handed (`./client-boot`), so a gated
10
+ * Project's shake gesture arms once the Account is eligible.
11
+ */
12
+ import { type ReporterIdentity } from "@fixback/sdk-core";
13
+ import { type ClientState } from "./client-state";
14
+ /** Whether `signIn()` can open a door at all — what the dormancy hint is gated on. */
15
+ export declare function canSignIn(s: ClientState): boolean;
16
+ /** Connect on the client, as {@link createConnect} builds it. */
17
+ export interface Connect {
18
+ /** Start a Connect through whichever door this app can open. Never throws. */
19
+ signIn(): Promise<ReporterIdentity>;
20
+ /** Revoke the Reporter session, clear it from SecureStore, and re-boot as anonymous. */
21
+ signOut(): Promise<void>;
22
+ /** Listen for Connect deep links that arrive while the app runs. */
23
+ armConnectLinking(gen: number): void;
24
+ /** Redeem the code on the URL the app was launched with, before the first boot. */
25
+ redeemLaunchUrl(gen: number): Promise<void>;
26
+ /** Resolve everyone waiting on an inbound Connect, and stop waiting. */
27
+ settleInboundWaiters(landed: boolean): void;
28
+ }
29
+ /**
30
+ * Connect over the client's shared state. `bootAndArm` is the client's boot: a Connect
31
+ * that lands re-boots through it, as the Account.
32
+ */
33
+ export declare function createConnect(s: ClientState, bootAndArm: (gen: number) => Promise<void>): Connect;
@@ -0,0 +1,306 @@
1
+ "use strict";
2
+ /**
3
+ * **Connect** on the headless client (ADR-0066): the two doors `signIn()` opens
4
+ * (ADR-0052), the inbound deep links that finish a Connect started outside the app
5
+ * (ADR-0046), and `signOut()`.
6
+ *
7
+ * One piece of the headless client (`./client`), created over the state every piece
8
+ * shares (`./client-state`): the Reporter session token, the connect URL boot
9
+ * returned, the last inbound code, and the callers waiting on one. A Connect that lands
10
+ * re-boots as the Account through the boot it is handed (`./client-boot`), so a gated
11
+ * Project's shake gesture arms once the Account is eligible.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.canSignIn = canSignIn;
15
+ exports.createConnect = createConnect;
16
+ const sdk_core_1 = require("@fixback/sdk-core");
17
+ const client_state_1 = require("./client-state");
18
+ const connect_1 = require("./connect");
19
+ const reporter_session_store_1 = require("./reporter-session-store");
20
+ /**
21
+ * How long the browser door's `signIn()` waits for the connect page's redirect to come
22
+ * back as a deep link (ADR-0052). Generous, because the round trip contains a real
23
+ * sign-in — the doors, an email link, a password manager — and the only cost of
24
+ * waiting is one pending promise. It exists so a person who abandons the page settles
25
+ * that promise eventually rather than never; the Connect itself is not cancelled by it
26
+ * and still lands through the inbound listener if they come back later.
27
+ */
28
+ const CONNECT_BROWSER_WAIT_MS = 5 * 60_000;
29
+ /**
30
+ * Whether the browser door is available: something to open the page with, and the
31
+ * `linking` seam the code comes back on. Without the seam the page would land the
32
+ * person back in the app with a code nothing reads, which is worse than not opening
33
+ * it — so both are required, not either.
34
+ */
35
+ function canConnectThroughBrowser({ adapters }) {
36
+ return Boolean(adapters.openBrowser && adapters.linking);
37
+ }
38
+ /** Whether `signIn()` can open a door at all — what the dormancy hint is gated on. */
39
+ function canSignIn(s) {
40
+ const { options, adapters } = s;
41
+ return Boolean((options.returnUrl && adapters.openAuthSession) || canConnectThroughBrowser(s));
42
+ }
43
+ /** Why `signIn()` could not open a door, named by what this app is actually missing. */
44
+ function explainSignInDoor({ adapters }) {
45
+ if (!adapters.openBrowser && !adapters.openAuthSession) {
46
+ return `${sdk_core_1.WARN_PREFIX} signIn needs a browser to open the connect page — install expo-web-browser.`;
47
+ }
48
+ if (!adapters.linking) {
49
+ return `${sdk_core_1.WARN_PREFIX} ` + "signIn needs the Linking seam to receive the connect page's redirect — or declare a `returnUrl` in init to return through an auth session instead.";
50
+ }
51
+ return `${sdk_core_1.WARN_PREFIX} ` + "signIn needs either an auth-session opener plus a `returnUrl` in init, or an in-app browser (install expo-web-browser).";
52
+ }
53
+ /**
54
+ * Connect over the client's shared state. `bootAndArm` is the client's boot: a Connect
55
+ * that lands re-boots through it, as the Account.
56
+ */
57
+ function createConnect(s, bootAndArm) {
58
+ const { options, adapters, warn, key, apiUrl } = s;
59
+ /**
60
+ * Start a **Connect** (ADR-0066): open the platform's connect page, exchange the
61
+ * one-time code it hands back for a Reporter session, store it in SecureStore, and
62
+ * re-boot as the Account — so a gated Project's shake gesture arms once the Account
63
+ * is eligible. Exposed so a host app can put it on its own settings screen (the way
64
+ * a gated Project, which shows nothing to a signed-out visitor, is entered), and
65
+ * behind the composer's identity row. Never throws: any failure leaves the current
66
+ * identity untouched.
67
+ *
68
+ * There are two doors, and which one opens depends only on what this app can offer
69
+ * (ADR-0052):
70
+ *
71
+ * - **The auth session**, when `init` declared a `returnUrl` and an opener is
72
+ * installed. The page returns *in place*, so the person never leaves the app.
73
+ * - **The in-app browser**, otherwise. The page is opened with no return URL and
74
+ * resolves where to land from the app links the *Project* lists, then redirects
75
+ * to one — which re-opens this app, where the inbound listener redeems the code
76
+ * (the journey a claimed Invite already travels). This is what keeps `signIn()`
77
+ * from being inert on the default install, which declares no `returnUrl` at all.
78
+ */
79
+ async function signIn() {
80
+ const gen = s.generation;
81
+ if (!s.connectUrl)
82
+ return (0, client_state_1.reporterIdentity)(s); // not booted, or an older server
83
+ try {
84
+ if (options.returnUrl && adapters.openAuthSession) {
85
+ const outcome = await (0, connect_1.startExpoConnect)(adapters.openAuthSession, {
86
+ connectUrl: s.connectUrl,
87
+ key,
88
+ returnUrl: options.returnUrl,
89
+ });
90
+ if (gen !== s.generation)
91
+ return (0, client_state_1.reporterIdentity)(s);
92
+ if (outcome.kind === "code")
93
+ await redeemConnectCode(outcome.code, gen);
94
+ }
95
+ else if (canConnectThroughBrowser(s)) {
96
+ await connectThroughBrowser(gen);
97
+ }
98
+ else {
99
+ warn(explainSignInDoor(s));
100
+ }
101
+ }
102
+ catch {
103
+ // A Connect problem must never disturb the host app.
104
+ }
105
+ return (0, client_state_1.reporterIdentity)(s);
106
+ }
107
+ /**
108
+ * The browser door (ADR-0052): open the connect page with no return URL and wait for
109
+ * the app link it redirects to, which the OS delivers back here as a deep link.
110
+ *
111
+ * The wait matters because the two halves are separated by the operating system: the
112
+ * page redirects, the app is foregrounded, and {@link handleInboundUrl} redeems —
113
+ * all after `openBrowser` has resolved (on Android it resolves as soon as the tab is
114
+ * shown). Without it, an awaited `signIn()` would report the anonymous identity it
115
+ * started from while the Connect it started was still completing. It is bounded, so
116
+ * a person who wandered off never leaves a promise pending for the life of the app.
117
+ */
118
+ async function connectThroughBrowser(gen) {
119
+ const landed = awaitInboundConnect();
120
+ try {
121
+ await adapters.openBrowser((0, connect_1.buildAppConnectUrl)(s.connectUrl, key));
122
+ }
123
+ catch {
124
+ settleInboundWaiters(false);
125
+ return;
126
+ }
127
+ if (gen !== s.generation)
128
+ return settleInboundWaiters(false);
129
+ // The redirect re-opens this app; dismiss the page it came from so the person is
130
+ // returned to where they tapped Sign in rather than to a finished connect page.
131
+ if (await landed)
132
+ dismissBrowser();
133
+ }
134
+ /**
135
+ * A promise for the next inbound Connect — `true` when one is redeemed, `false` when
136
+ * the wait expires or the client is destroyed. The timer is cleared on either, so a
137
+ * completed sign-in leaves nothing running.
138
+ */
139
+ function awaitInboundConnect() {
140
+ return new Promise((resolve) => {
141
+ const timer = setTimeout(() => settleInboundWaiters(false), CONNECT_BROWSER_WAIT_MS);
142
+ s.inboundWaiters.push((landed) => {
143
+ clearTimeout(timer);
144
+ resolve(landed);
145
+ });
146
+ });
147
+ }
148
+ /** Resolve everyone waiting on an inbound Connect, and stop waiting. */
149
+ function settleInboundWaiters(landed) {
150
+ const waiting = s.inboundWaiters;
151
+ s.inboundWaiters = [];
152
+ for (const resolve of waiting) {
153
+ try {
154
+ resolve(landed);
155
+ }
156
+ catch {
157
+ /* a waiter must never break the client */
158
+ }
159
+ }
160
+ }
161
+ /** Close the in-app browser this app opened — best-effort, like every seam here. */
162
+ function dismissBrowser() {
163
+ try {
164
+ adapters.dismissBrowser?.();
165
+ }
166
+ catch {
167
+ /* dismissing is a courtesy, never a failure */
168
+ }
169
+ }
170
+ /**
171
+ * Exchange a one-time Connect code for a Reporter session and persist it, leaving the
172
+ * client identified as the Account. The tail every half of Connect on mobile shares:
173
+ * {@link signIn}, where the app opened the connect page itself, and
174
+ * {@link handleInboundUrl}, where the code arrived on a deep link from an invite the
175
+ * person claimed elsewhere (ADR-0046). `gen` is the caller's lifecycle generation,
176
+ * re-checked after every await so a destroyed client abandons the redemption.
177
+ *
178
+ * Booting on the new identity is the **caller's** step, because the two callers
179
+ * differ: a redemption mid-run must re-boot to arm, while the one on the launch URL
180
+ * is followed by `start`'s own boot and would only duplicate it.
181
+ */
182
+ async function exchangeAndStore(code, gen) {
183
+ const result = await (0, sdk_core_1.exchangeConnectCode)(apiUrl, {
184
+ key,
185
+ code,
186
+ anonymousId: s.identity.anonymousId,
187
+ platform: "expo",
188
+ }, { fetch: adapters.fetch });
189
+ if (gen !== s.generation || !result)
190
+ return false;
191
+ s.reporterSessionToken = result.reporterSession;
192
+ s.identity = { ...s.identity, reporterSession: s.reporterSessionToken };
193
+ await (0, reporter_session_store_1.writeReporterSession)(adapters.secureStore, key, s.reporterSessionToken);
194
+ return gen === s.generation;
195
+ }
196
+ /**
197
+ * {@link exchangeAndStore} plus the re-boot that arms the client as the Account, and
198
+ * whether the Connect landed. A refused exchange changes no identity, so it is not
199
+ * re-booted on: the answer would be the one already in hand.
200
+ */
201
+ async function redeemConnectCode(code, gen) {
202
+ if (!(await exchangeAndStore(code, gen)))
203
+ return false;
204
+ await bootAndArm(gen);
205
+ return true;
206
+ }
207
+ /**
208
+ * Finish a Connect that started outside the app: read the one-time code off an
209
+ * inbound deep link and redeem it. Ignores a code it has already seen — iOS and
210
+ * Android both replay the launch URL in ordinary situations (a cold start whose
211
+ * `getInitialURL` is then re-delivered as a `url` event), and a code is single-use,
212
+ * so a second exchange would fail and report a Connect that in fact succeeded as one
213
+ * that did not. A declared `returnUrl` additionally narrows this to links on the
214
+ * app's own return origin; an app that declared none still redeems, because the
215
+ * claim page mints its code against the **Project's** app link, not against `init`.
216
+ */
217
+ async function handleInboundUrl(url, gen, { reboot = true } = {}) {
218
+ try {
219
+ const code = (0, connect_1.parseInboundConnectCode)(url, options.returnUrl);
220
+ if (!code || code === s.lastInboundCode)
221
+ return;
222
+ s.lastInboundCode = code;
223
+ const landed = reboot
224
+ ? await redeemConnectCode(code, gen)
225
+ : await exchangeAndStore(code, gen);
226
+ // A `signIn()` that went through the browser door is waiting on exactly this —
227
+ // the code it sent the person out for, arriving back on the app's own link.
228
+ if (landed)
229
+ settleInboundWaiters(true);
230
+ }
231
+ catch {
232
+ // A Connect problem must never disturb the host app.
233
+ }
234
+ }
235
+ /**
236
+ * Listen for Connect deep links that arrive **while the app runs** — the person was
237
+ * already in the app (or it was backgrounded) when they tapped the claim page's
238
+ * button. Armed on the `linking` seam alone: a Project's app link is allowlisted on
239
+ * the Project, so an app that never declared a `returnUrl` in `init` is still where
240
+ * a claim lands (ADR-0046). Silent on an app that provides no seam, which can never
241
+ * be a Connect target.
242
+ */
243
+ function armConnectLinking(gen) {
244
+ const linking = adapters.linking;
245
+ if (!linking)
246
+ return;
247
+ try {
248
+ const subscription = linking.addEventListener("url", (event) => {
249
+ void handleInboundUrl(event?.url ?? "", gen);
250
+ });
251
+ s.teardownLinking = () => subscription.remove();
252
+ }
253
+ catch {
254
+ // A Linking seam that refuses to subscribe is not a reason to fail a start.
255
+ }
256
+ }
257
+ /**
258
+ * Redeem the code on the URL the app was **launched** with, before the first boot —
259
+ * the invite journey's cold start (the person tapped the claim page's redirect and
260
+ * the app opened). Awaited rather than left in flight, so the one boot that follows
261
+ * carries the Reporter session: two concurrent boots race, and an anonymous answer
262
+ * landing last leaves a Connect that succeeded looking like one that never happened.
263
+ * The browser SDK exchanges a URL code before booting for the same reason.
264
+ *
265
+ * Best-effort throughout: a seam that throws, a launch URL that carries nothing, or
266
+ * an exchange the server refuses all fall through to an ordinary anonymous boot.
267
+ */
268
+ async function redeemLaunchUrl(gen) {
269
+ const linking = adapters.linking;
270
+ if (!linking)
271
+ return;
272
+ try {
273
+ const initial = await linking.getInitialURL();
274
+ if (gen !== s.generation || !initial)
275
+ return;
276
+ await handleInboundUrl(initial, gen, { reboot: false });
277
+ }
278
+ catch {
279
+ // Never throw into the host app.
280
+ }
281
+ }
282
+ /**
283
+ * Revoke the Reporter session, clear it from SecureStore, and re-boot as anonymous —
284
+ * so a gated Project's launcher (shake gesture) drops at once. Never throws.
285
+ */
286
+ async function signOut() {
287
+ const gen = s.generation;
288
+ try {
289
+ if (s.reporterSessionToken) {
290
+ await (0, sdk_core_1.revokeReporterSession)(apiUrl, s.reporterSessionToken, {
291
+ fetch: adapters.fetch,
292
+ });
293
+ }
294
+ await (0, reporter_session_store_1.clearReporterSession)(adapters.secureStore, key);
295
+ s.reporterSessionToken = null;
296
+ s.identity = { ...s.identity, reporterSession: undefined };
297
+ if (gen === s.generation)
298
+ await bootAndArm(gen);
299
+ }
300
+ catch {
301
+ // A Fixback problem must never disturb the host app.
302
+ }
303
+ }
304
+ return { signIn, signOut, armConnectLinking, redeemLaunchUrl, settleInboundWaiters };
305
+ }
306
+ //# sourceMappingURL=client-connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-connect.js","sourceRoot":"","sources":["../src/client-connect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Mobile replay's arming (ADR-0044): whether the client records at all, the frame
3
+ * recorder it starts and stops on every boot, and turning a frozen window into the
4
+ * report's video.
5
+ *
6
+ * One piece of the headless client (`./client`), created over the state every piece
7
+ * shares (`./client-state`). The recorder and the window the composer froze live
8
+ * there, because the composer (`present`, `submit`) and boot (`./client-boot`) both
9
+ * reach them.
10
+ */
11
+ import { type CaptureConfig } from "@fixback/sdk-core";
12
+ import type { ClientState } from "./client-state";
13
+ import { type MobileReplayWindow } from "./replay";
14
+ import { type ReplayAttachment } from "./replay-encode";
15
+ /** The replay half of capture arming, as {@link createReplayArming} builds it. */
16
+ export interface ReplayArming {
17
+ /** Start (or stop) the frame recorder for the Project's served config. */
18
+ armReplay(served: CaptureConfig | undefined): void;
19
+ /** Stop the recorder and drop every frame it owns (best-effort). */
20
+ disarmReplay(): void;
21
+ /** Delete a replay frame or encoded window file (best-effort, repeat-safe). */
22
+ releaseReplayFile(uri: string): void;
23
+ /** Give a frozen window back to the recorder, so its frames can be reclaimed. */
24
+ releaseReplayWindow(window: MobileReplayWindow | null): void;
25
+ /** Freeze the window as it stands now and encode it — the auto-capture path's replay. */
26
+ freezeAndEncodeReplay(): Promise<ReplayAttachment | null>;
27
+ /** Encode a frozen window for transport; `null` whenever it cannot become a video. */
28
+ encodeReplay(window: MobileReplayWindow | null): Promise<ReplayAttachment | null>;
29
+ }
30
+ /** Mobile replay's arming over the client's shared state. */
31
+ export declare function createReplayArming(s: ClientState): ReplayArming;