@capxul/sdk-react 1.0.0-alpha.9 → 1.2.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 ADDED
@@ -0,0 +1,60 @@
1
+ # `@capxul/sdk-react`
2
+
3
+ The React projection over `@capxul/sdk`. It owns provider lifecycle, the one
4
+ canonical identity projection, renderer-neutral identity controllers, TanStack
5
+ Query bindings for rich data, and authenticated-cache cleanup.
6
+
7
+ ```tsx
8
+ "use client";
9
+
10
+ import { CapxulProvider } from "@capxul/sdk-react";
11
+
12
+ export function Providers({ children }: { children: React.ReactNode }) {
13
+ return (
14
+ <CapxulProvider publishableKey={process.env.NEXT_PUBLIC_CAPXUL_PUBLISHABLE_KEY!}>
15
+ {children}
16
+ </CapxulProvider>
17
+ );
18
+ }
19
+ ```
20
+
21
+ ## Public shape
22
+
23
+ - `useCapxulIdentity`, `useCapxulDestination`, and `useCapxulTransitions`
24
+ expose the Core SDK identity actor without a parallel React state model.
25
+ - `useCapxulSend` carries caller-owned invocation controls to that actor.
26
+ - `useCapxulAuth` is the stable six-verb application facade.
27
+ - `CapxulAuthenticationController` and `CapxulOnboardingController` select
28
+ app-owned slots and render no SDK-owned DOM.
29
+ - Rich-data hooks such as `useCapxulProfile`, `useCapxulOrgs`, members, roles,
30
+ treasury, and money hooks remain TanStack Query projections.
31
+
32
+ The packed npm package exposes `@capxul/sdk-react` and
33
+ `@capxul/sdk-react/testing`. The workspace-only `@capxul/sdk-react/headless`
34
+ subpath is not published.
35
+
36
+ ```tsx
37
+ import { createCapxulReactTestHarness } from "@capxul/sdk-react/testing";
38
+
39
+ const harness = createCapxulReactTestHarness();
40
+ const tree = harness.provider(harness.authentication(slots));
41
+ await harness.testing.close();
42
+ ```
43
+
44
+ The testing harness uses the real public provider and controllers and imports
45
+ its client factory only through `@capxul/sdk/testing`; it owns no renderer or
46
+ parallel identity state.
47
+
48
+ ## Documentation
49
+
50
+ - [Maintainer context](./CONTEXT.md)
51
+ - [Architecture](./docs/architecture.md)
52
+ - [Proof map](./docs/proof.md)
53
+
54
+ ## Development
55
+
56
+ ```bash
57
+ vp run --filter @capxul/sdk-react check-types
58
+ vp run --filter @capxul/sdk-react test
59
+ (cd packages/sdk-react && vp pack)
60
+ ```
@@ -0,0 +1,647 @@
1
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query";
3
+ import { createCapxulClient, isCapxulError, resolveIdentityDestination, toCountryCode } from "@capxul/sdk";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/internal/capxul-bootstrap-context.tsx
6
+ const CapxulBootstrapContext = createContext(null);
7
+ function CapxulBootstrapProvider({ value, children }) {
8
+ return /* @__PURE__ */ jsx(CapxulBootstrapContext.Provider, {
9
+ value,
10
+ children
11
+ });
12
+ }
13
+ function useCapxul() {
14
+ const state = useContext(CapxulBootstrapContext);
15
+ if (state === null) throw new Error("useCapxul must be used within <CapxulProvider>");
16
+ return state;
17
+ }
18
+ //#endregion
19
+ //#region src/internal/capxul-client-context.tsx
20
+ const MISSING_CAPXUL_CLIENT_PROVIDER = Symbol("MISSING_CAPXUL_CLIENT_PROVIDER");
21
+ const CapxulClientContext = createContext(MISSING_CAPXUL_CLIENT_PROVIDER);
22
+ function CapxulClientProvider({ client, children }) {
23
+ return /* @__PURE__ */ jsx(CapxulClientContext.Provider, {
24
+ value: client,
25
+ children
26
+ });
27
+ }
28
+ /**
29
+ * Returns the client, or `null` while `<CapxulProvider>` is still bootstrapping.
30
+ * Data hooks use this so they can sit in `isPending` (disabled query) until the
31
+ * client resolves, rather than throwing during bootstrap.
32
+ */
33
+ function useCapxulClientOrNull() {
34
+ const client = useContext(CapxulClientContext);
35
+ if (client === MISSING_CAPXUL_CLIENT_PROVIDER) throw new Error("useCapxulClient must be used within <CapxulProvider>");
36
+ return client;
37
+ }
38
+ //#endregion
39
+ //#region src/internal/reactivity-keys.ts
40
+ const capxulKeys = {
41
+ root: ["capxul"],
42
+ profile: ["capxul", "profile"],
43
+ usernameAvailability: (username) => [
44
+ "capxul",
45
+ "profile",
46
+ "username-availability",
47
+ username
48
+ ],
49
+ account: ["capxul", "account"],
50
+ provisioning: ["capxul", "provisioning"],
51
+ binding: ["capxul", "binding"],
52
+ accountBalance: ["capxul", "accountBalance"],
53
+ subAccounts: (accountId) => [
54
+ "capxul",
55
+ "subAccounts",
56
+ accountId ?? "pending"
57
+ ],
58
+ orgs: ["capxul", "orgs"],
59
+ org: (orgId) => [
60
+ "capxul",
61
+ "org",
62
+ orgId ?? "pending"
63
+ ],
64
+ orgMembers: (orgId) => [
65
+ "capxul",
66
+ "org",
67
+ orgId ?? "pending",
68
+ "members"
69
+ ],
70
+ orgRoles: (orgId) => [
71
+ "capxul",
72
+ "org",
73
+ orgId ?? "pending",
74
+ "roles"
75
+ ],
76
+ orgTreasury: (orgId) => [
77
+ "capxul",
78
+ "org",
79
+ orgId ?? "pending",
80
+ "treasury"
81
+ ],
82
+ payments: ["capxul", "payments"],
83
+ payment: (paymentId) => [
84
+ "capxul",
85
+ "payments",
86
+ paymentId ?? "pending"
87
+ ]
88
+ };
89
+ //#endregion
90
+ //#region src/identity.tsx
91
+ const MISSING_IDENTITY_PROVIDER = Symbol("MISSING_IDENTITY_PROVIDER");
92
+ const IdentityContext = createContext(MISSING_IDENTITY_PROVIDER);
93
+ function controls(options) {
94
+ if (options === void 0) return void 0;
95
+ return {
96
+ ...options.signal === void 0 ? {} : { signal: options.signal },
97
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
98
+ ...options.deadlineMs === void 0 ? {} : { deadlineMs: options.deadlineMs },
99
+ ...options.correlationId === void 0 ? {} : { correlation_id: options.correlationId },
100
+ ...options.journeyId === void 0 ? {} : { journey_id: options.journeyId }
101
+ };
102
+ }
103
+ const failure = (result) => result.ok ? null : {
104
+ ok: false,
105
+ reason: result.refused
106
+ };
107
+ async function guarded(runtime, verb, options, run) {
108
+ const invocation = controls(options);
109
+ try {
110
+ return await (runtime.runFacade?.(verb, invocation, run) ?? run(invocation));
111
+ } catch {
112
+ return {
113
+ ok: false,
114
+ reason: "UNKNOWN"
115
+ };
116
+ }
117
+ }
118
+ const ORGANIZATION_HANDLE = /^[a-z0-9-]{3,32}$/;
119
+ function normalizeOrganization(organization) {
120
+ const name = typeof organization.name === "string" ? organization.name.trim() : "";
121
+ const handle = typeof organization.handle === "string" ? organization.handle.trim().toLowerCase() : "";
122
+ if (name.length === 0 || !ORGANIZATION_HANDLE.test(handle)) return null;
123
+ if (organization.bio !== void 0 && typeof organization.bio !== "string") return null;
124
+ if (organization.size !== void 0 && typeof organization.size !== "string") return null;
125
+ try {
126
+ return {
127
+ name,
128
+ handle,
129
+ country: toCountryCode(organization.country),
130
+ ...organization.bio === void 0 ? {} : { bio: organization.bio },
131
+ ...organization.size === void 0 ? {} : { size: organization.size }
132
+ };
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ function createAuth(client, clearAuthenticatedQueries) {
138
+ const runtime = client._internal.identity;
139
+ const read = async (invocation) => {
140
+ return failure(await runtime.send({ _tag: "ReadSession" }, invocation)) ?? { ok: true };
141
+ };
142
+ const ensureAccount = async (invocation) => {
143
+ return failure(await runtime.send({ _tag: "EnsureAccount" }, invocation)) ?? { ok: true };
144
+ };
145
+ const reachClaimed = async (invocation) => {
146
+ let state = runtime.snapshot();
147
+ if (state.phase !== "authenticated" || state.account.at === "unknown") {
148
+ const result = await runtime.send({ _tag: "EnsureAccount" }, invocation);
149
+ const refused = failure(result);
150
+ if (refused !== null) return refused;
151
+ state = result.state;
152
+ }
153
+ if (state.phase !== "authenticated") return {
154
+ ok: false,
155
+ reason: "WRONG_STATE"
156
+ };
157
+ if (state.account.at === "claimed") return { ok: true };
158
+ const event = state.account.at === "failed" ? { _tag: "RetryAccount" } : state.account.at === "counterfactual" ? { _tag: "ClaimAccount" } : { _tag: "EnsureAccount" };
159
+ const result = await runtime.send(event, invocation);
160
+ const refused = failure(result);
161
+ if (refused !== null) return refused;
162
+ const next = result.state;
163
+ return next.phase === "authenticated" && next.account.at === "claimed" ? { ok: true } : {
164
+ ok: false,
165
+ reason: "WRONG_STATE"
166
+ };
167
+ };
168
+ const completeProfile = async (profile, invocation) => {
169
+ const result = await runtime.completeProfile(profile, invocation);
170
+ return result.ok ? { ok: true } : result;
171
+ };
172
+ return {
173
+ requestCode: (email, options) => guarded(runtime, "requestCode", options, async (invocation) => {
174
+ const result = await client.auth.signIn({ email }, invocation);
175
+ if (!result.ok) return {
176
+ ok: false,
177
+ reason: result.error.code
178
+ };
179
+ const state = runtime.snapshot();
180
+ return state.phase === "otp_pending" ? {
181
+ ok: true,
182
+ requestedAt: state.requestedAt
183
+ } : {
184
+ ok: false,
185
+ reason: state.phase === "faulted" ? state.failure.code : "UNKNOWN"
186
+ };
187
+ }),
188
+ verifyCode: (otp, options) => guarded(runtime, "verifyCode", options, async (invocation) => {
189
+ const state = runtime.snapshot();
190
+ const email = state.phase === "otp_pending" ? state.email : state.phase === "faulted" && state.resume !== null ? state.resume.email : "";
191
+ if (!/^\d{6}$/.test(otp)) return failure(await runtime.send({
192
+ _tag: "VerifyOtp",
193
+ email,
194
+ otp,
195
+ now: Date.now()
196
+ }, invocation)) ?? {
197
+ ok: false,
198
+ reason: "UNKNOWN"
199
+ };
200
+ const result = await client.auth.verifyOtp({
201
+ email,
202
+ code: otp
203
+ }, invocation);
204
+ if (!result.ok) return {
205
+ ok: false,
206
+ reason: result.error.code
207
+ };
208
+ const next = runtime.snapshot();
209
+ return next.phase === "authenticated" ? {
210
+ ok: true,
211
+ authUserId: next.session.authUserId,
212
+ profileComplete: next.profileComplete
213
+ } : {
214
+ ok: false,
215
+ reason: next.phase === "faulted" ? next.failure.code : "UNKNOWN"
216
+ };
217
+ }),
218
+ signOut: (options) => guarded(runtime, "signOut", options, async (invocation) => {
219
+ const result = await client.auth.signOut(invocation);
220
+ if (!result.ok) return {
221
+ ok: false,
222
+ reason: result.error.code
223
+ };
224
+ await clearAuthenticatedQueries();
225
+ return { ok: true };
226
+ }),
227
+ completePersonal: (profile, options) => guarded(runtime, "completePersonal", options, async (invocation) => {
228
+ const completed = await completeProfile(profile, invocation);
229
+ if (!completed.ok) return completed;
230
+ const refreshed = await read(invocation);
231
+ if (!refreshed.ok) return refreshed;
232
+ return ensureAccount(invocation);
233
+ }),
234
+ createOrganization: (submission, options) => guarded(runtime, "createOrganization", options, async (invocation) => {
235
+ const organization = normalizeOrganization(submission.organization);
236
+ if (organization === null) return {
237
+ ok: false,
238
+ reason: "INVALID_INPUT"
239
+ };
240
+ const completed = await completeProfile(submission.profileDetails, invocation);
241
+ if (!completed.ok) return completed;
242
+ const refreshed = await read(invocation);
243
+ if (!refreshed.ok) return refreshed;
244
+ const claimed = await reachClaimed(invocation);
245
+ if (!claimed.ok) return claimed;
246
+ const created = await runtime.send({
247
+ _tag: "CreateOrganization",
248
+ draft: organization
249
+ }, invocation);
250
+ const refused = failure(created);
251
+ if (refused !== null) return refused;
252
+ const state = created.state;
253
+ const org = state.phase === "authenticated" && state.account.at === "claimed" ? state.account.org : null;
254
+ return org !== null && org.at !== "creating" && org.orgId !== null ? {
255
+ ok: true,
256
+ orgId: org.orgId
257
+ } : {
258
+ ok: false,
259
+ reason: "UNKNOWN"
260
+ };
261
+ }),
262
+ retry: (options) => guarded(runtime, "retry", options, async (invocation) => {
263
+ const state = runtime.snapshot();
264
+ const event = state.phase === "authenticated" && state.account.at === "claimed" ? { _tag: "RetryOrganization" } : { _tag: "RetryAccount" };
265
+ return failure(await runtime.send(event, invocation)) ?? { ok: true };
266
+ })
267
+ };
268
+ }
269
+ function CapxulIdentityProvider({ client, children }) {
270
+ const queryClient = useQueryClient();
271
+ const runtime = client?._internal.identity ?? null;
272
+ const listeners = useRef(/* @__PURE__ */ new Set());
273
+ useEffect(() => {
274
+ if (client === null) return;
275
+ Promise.resolve().then(() => client.auth.getSession()).catch(() => void 0);
276
+ }, [client]);
277
+ useEffect(() => {
278
+ if (runtime === null) return;
279
+ return runtime.subscribeTransitions((record) => {
280
+ const state = runtime.snapshot();
281
+ for (const listener of listeners.current) try {
282
+ listener(record, state);
283
+ } catch {
284
+ listeners.current.delete(listener);
285
+ }
286
+ });
287
+ }, [runtime]);
288
+ const addTransitionListener = useCallback((listener) => {
289
+ listeners.current.add(listener);
290
+ return () => listeners.current.delete(listener);
291
+ }, []);
292
+ const value = useMemo(() => {
293
+ if (client === null || runtime === null) return null;
294
+ const send = (event, options) => runtime.send(event, controls(options));
295
+ const clearAuthenticatedQueries = async () => {
296
+ await queryClient.cancelQueries({ queryKey: capxulKeys.root });
297
+ await queryClient.resetQueries({ queryKey: capxulKeys.root });
298
+ };
299
+ return {
300
+ runtime,
301
+ send,
302
+ auth: createAuth(client, clearAuthenticatedQueries),
303
+ addTransitionListener
304
+ };
305
+ }, [
306
+ client,
307
+ runtime,
308
+ addTransitionListener,
309
+ queryClient
310
+ ]);
311
+ return /* @__PURE__ */ jsx(IdentityContext.Provider, {
312
+ value,
313
+ children
314
+ });
315
+ }
316
+ function useIdentityContext() {
317
+ const value = useContext(IdentityContext);
318
+ if (value === MISSING_IDENTITY_PROVIDER) throw new Error("identity hooks must be used within <CapxulProvider>");
319
+ if (value === null) throw new Error("identity hooks require a ready <CapxulProvider>");
320
+ return value;
321
+ }
322
+ const noSubscribe = () => () => void 0;
323
+ const noState = () => null;
324
+ function useCapxulIdentityOrNull() {
325
+ const value = useContext(IdentityContext);
326
+ if (value === MISSING_IDENTITY_PROVIDER) throw new Error("identity hooks must be used within <CapxulProvider>");
327
+ return useSyncExternalStore(value?.runtime.subscribe ?? noSubscribe, value?.runtime.snapshot ?? noState, value?.runtime.snapshot ?? noState);
328
+ }
329
+ function useCapxulIdentity() {
330
+ const state = useCapxulIdentityOrNull();
331
+ if (state === null) throw new Error("identity hooks require a ready <CapxulProvider>");
332
+ return state;
333
+ }
334
+ function useCapxulSend() {
335
+ return useIdentityContext().send;
336
+ }
337
+ function useCapxulAuth() {
338
+ return useIdentityContext().auth;
339
+ }
340
+ function useCapxulDestination() {
341
+ const next = resolveIdentityDestination(useCapxulIdentity());
342
+ const held = useRef(null);
343
+ const key = JSON.stringify(next);
344
+ if (held.current?.key !== key) held.current = {
345
+ key,
346
+ value: next
347
+ };
348
+ return held.current.value;
349
+ }
350
+ function useCapxulTransitions(listener) {
351
+ const { addTransitionListener } = useIdentityContext();
352
+ useEffect(() => addTransitionListener(listener), [addTransitionListener, listener]);
353
+ }
354
+ const entered = (record, target) => record.outcome === "applied" && record.from !== target && record.to === target;
355
+ //#endregion
356
+ //#region src/provider.tsx
357
+ /**
358
+ * Transient failure codes worth a retry — network blips, rate limits, and
359
+ * upstream provider/unknown hiccups that a later attempt may clear. Everything
360
+ * else (including any future code) is deterministic and NOT retried: retrying a
361
+ * deterministic failure only multiplies the failed backend actions. A fresh
362
+ * user with no Safe yet hits `SMART_ACCOUNT_MISSING` on every attempt, so the
363
+ * old blanket `retry: 2` tripled that (and every other deterministic) failed
364
+ * action for zero benefit (#1031).
365
+ */
366
+ const RETRYABLE_QUERY_ERROR_CODES = new Set([
367
+ "NETWORK_ERROR",
368
+ "RATE_LIMITED",
369
+ "PROVIDER_ERROR",
370
+ "UNKNOWN"
371
+ ]);
372
+ /** Matches the previous `retry: 2` budget (initial attempt + up to 2 retries). */
373
+ const MAX_CAPXUL_QUERY_RETRIES = 2;
374
+ /**
375
+ * TanStack `retry` predicate: `failureCount` is 0-indexed and checked before
376
+ * increment, so `< MAX` reproduces the old numeric budget for retryable codes.
377
+ * Exported for direct unit coverage of the deterministic-vs-transient split.
378
+ */
379
+ function shouldRetryCapxulQuery(failureCount, error) {
380
+ if (failureCount >= MAX_CAPXUL_QUERY_RETRIES) return false;
381
+ return isCapxulError(error) && RETRYABLE_QUERY_ERROR_CODES.has(error.code);
382
+ }
383
+ /**
384
+ * The default query client used when the host injects none. Exported so a test
385
+ * can pin that `queries.retry` is wired to `shouldRetryCapxulQuery` — reverting
386
+ * it to the old blanket `retry: 2` must fail a test (#1031).
387
+ */
388
+ function makeDefaultQueryClient() {
389
+ return new QueryClient({ defaultOptions: {
390
+ queries: {
391
+ retry: shouldRetryCapxulQuery,
392
+ staleTime: 3e4
393
+ },
394
+ mutations: { retry: 0 }
395
+ } });
396
+ }
397
+ function isCapxulQueryKey(queryKey) {
398
+ return queryKey[0] === "capxul";
399
+ }
400
+ function clearClientScopedQueries(queryClient, ownsQueryClient) {
401
+ if (ownsQueryClient) {
402
+ queryClient.clear();
403
+ return;
404
+ }
405
+ queryClient.removeQueries({ predicate: (query) => isCapxulQueryKey(query.queryKey) });
406
+ }
407
+ function CapxulProvider(props) {
408
+ const { publishableKey, client: injectedClient, requirement, signer, observation, telemetry, queryClient, children } = props;
409
+ const [resolvedQueryClient] = useState(() => queryClient ?? makeDefaultQueryClient());
410
+ const [ownsQueryClient] = useState(() => queryClient === void 0);
411
+ const [attempt, setAttempt] = useState(0);
412
+ const retry = useCallback(() => {
413
+ setAttempt((n) => n + 1);
414
+ }, []);
415
+ const bootstrapInput = useMemo(() => publishableKey === void 0 ? null : {
416
+ publishableKey,
417
+ ...requirement === void 0 ? {} : { requirement },
418
+ ...signer === void 0 ? {} : { signer },
419
+ ...observation === void 0 ? {} : { observation },
420
+ ...telemetry === void 0 ? {} : { telemetry }
421
+ }, [
422
+ publishableKey,
423
+ requirement,
424
+ signer,
425
+ observation,
426
+ telemetry,
427
+ attempt
428
+ ]);
429
+ const [ownedBootstrap, setOwnedBootstrap] = useState(null);
430
+ const activeOwnedBootstrap = bootstrapInput !== null && ownedBootstrap?.input === bootstrapInput ? ownedBootstrap : null;
431
+ const client = injectedClient ?? activeOwnedBootstrap?.client ?? null;
432
+ const previousClientRef = useRef(injectedClient ?? null);
433
+ useEffect(() => {
434
+ if (bootstrapInput === null) return;
435
+ let cancelled = false;
436
+ let created = null;
437
+ setOwnedBootstrap({
438
+ input: bootstrapInput,
439
+ client: null,
440
+ status: "bootstrapping",
441
+ error: null
442
+ });
443
+ (async () => {
444
+ const result = await createCapxulClient(bootstrapInput);
445
+ if (cancelled) {
446
+ if (result.ok) await result.value._internal.close?.();
447
+ return;
448
+ }
449
+ if (result.ok) {
450
+ created = result.value;
451
+ setOwnedBootstrap({
452
+ input: bootstrapInput,
453
+ client: result.value,
454
+ status: "ready",
455
+ error: null
456
+ });
457
+ } else setOwnedBootstrap({
458
+ input: bootstrapInput,
459
+ client: null,
460
+ status: "error",
461
+ error: result.error
462
+ });
463
+ })();
464
+ return () => {
465
+ cancelled = true;
466
+ created?._internal.close?.();
467
+ };
468
+ }, [bootstrapInput]);
469
+ useEffect(() => {
470
+ const previous = previousClientRef.current;
471
+ if (previous !== null && previous !== client) clearClientScopedQueries(resolvedQueryClient, ownsQueryClient);
472
+ previousClientRef.current = client;
473
+ }, [
474
+ client,
475
+ ownsQueryClient,
476
+ resolvedQueryClient
477
+ ]);
478
+ const bootstrapState = useMemo(() => ({
479
+ status: injectedClient === void 0 ? activeOwnedBootstrap?.status ?? "bootstrapping" : "ready",
480
+ error: injectedClient === void 0 ? activeOwnedBootstrap?.error ?? null : null,
481
+ retry
482
+ }), [
483
+ activeOwnedBootstrap,
484
+ injectedClient,
485
+ retry
486
+ ]);
487
+ if (publishableKey === void 0 === (injectedClient === void 0)) throw new Error("CapxulProvider requires exactly one of `publishableKey` or `client`");
488
+ return /* @__PURE__ */ jsx(QueryClientProvider, {
489
+ client: resolvedQueryClient,
490
+ children: /* @__PURE__ */ jsx(CapxulBootstrapProvider, {
491
+ value: bootstrapState,
492
+ children: /* @__PURE__ */ jsx(CapxulClientProvider, {
493
+ client,
494
+ children: /* @__PURE__ */ jsx(CapxulIdentityProvider, {
495
+ client,
496
+ children
497
+ })
498
+ })
499
+ })
500
+ });
501
+ }
502
+ //#endregion
503
+ //#region src/controllers.tsx
504
+ const action = async (send, event) => {
505
+ const result = await send(event);
506
+ return result.ok ? { ok: true } : {
507
+ ok: false,
508
+ reason: result.refused
509
+ };
510
+ };
511
+ function CapxulAuthenticationController({ slots }) {
512
+ const state = useCapxulIdentity();
513
+ const destination = useCapxulDestination();
514
+ const auth = useCapxulAuth();
515
+ const send = useCapxulSend();
516
+ switch (state.phase) {
517
+ case "signed_out": return slots.email({
518
+ state,
519
+ requestCode: auth.requestCode
520
+ });
521
+ case "otp_pending": return slots.otp({
522
+ state,
523
+ verifyCode: auth.verifyCode,
524
+ back: () => action(send, { _tag: "Reset" })
525
+ });
526
+ case "otp_sending":
527
+ case "otp_verifying":
528
+ case "signing_out": return slots.pending({ state });
529
+ case "faulted": return slots.failure({
530
+ state,
531
+ recover: () => action(send, state.resume === null ? { _tag: "Reset" } : {
532
+ _tag: "ResumeOtpEntry",
533
+ now: Date.now()
534
+ }),
535
+ back: () => action(send, { _tag: "Reset" })
536
+ });
537
+ case "authenticated": return slots.success({
538
+ state,
539
+ destination
540
+ });
541
+ }
542
+ }
543
+ function ready(destination) {
544
+ return destination?.to === "dashboardPersonal" || destination?.to === "dashboardOrganization";
545
+ }
546
+ function CapxulOnboardingController(props) {
547
+ const state = useCapxulIdentity();
548
+ const destination = useCapxulDestination();
549
+ const auth = useCapxulAuth();
550
+ if (state.phase !== "authenticated") return null;
551
+ const { slots, navigation } = props;
552
+ if (props.intent === null) return slots.intent({
553
+ state,
554
+ select: props.onIntent,
555
+ back: navigation.selectorBack
556
+ });
557
+ if (props.intent === "personal" && !state.profileComplete) return slots.profile({
558
+ intent: "personal",
559
+ state,
560
+ completePersonal: auth.completePersonal,
561
+ cancel: navigation.profileCancel
562
+ });
563
+ const submission = props.submittedOrganization;
564
+ if (props.intent === "organization" && props.organizationProfile === null && submission === null) return slots.profile({
565
+ intent: "organization",
566
+ state,
567
+ continueOrganization: props.onOrganizationProfile,
568
+ cancel: navigation.profileCancel
569
+ });
570
+ if (props.intent === "organization" && props.organizationProfile !== null && submission === null) return organizationForm(props, state, auth, null);
571
+ if (submission !== null && state.account.at !== "claimed") {
572
+ if (state.account.at === "failed") {
573
+ const retry = state.account.retryable ? (options) => auth.createOrganization(submission, options) : void 0;
574
+ return slots.accountFailure({
575
+ state,
576
+ account: state.account,
577
+ ...retry === void 0 ? {} : { retry }
578
+ });
579
+ }
580
+ return slots.accountProgress({
581
+ state,
582
+ account: state.account
583
+ });
584
+ }
585
+ if (submission !== null && state.account.at === "claimed") {
586
+ const org = state.account.org;
587
+ if (org === null) return organizationForm(props, state, auth, submission);
588
+ if (org.at === "failed") {
589
+ const retry = org.retryable ? org.orgId === null ? (options) => auth.createOrganization(submission, options) : auth.retry : void 0;
590
+ return slots.organizationFailure({
591
+ state,
592
+ org,
593
+ ...retry === void 0 ? {} : { retry }
594
+ });
595
+ }
596
+ if (org.at !== "ready") return slots.organizationProgress({
597
+ state,
598
+ org
599
+ });
600
+ }
601
+ if (state.account.at === "failed") return slots.accountFailure({
602
+ state,
603
+ account: state.account,
604
+ ...state.account.retryable ? { retry: auth.retry } : {}
605
+ });
606
+ if (state.account.at !== "claimed") return slots.accountProgress({
607
+ state,
608
+ account: state.account
609
+ });
610
+ return ready(destination) ? slots.ready({
611
+ state,
612
+ destination
613
+ }) : null;
614
+ }
615
+ function organizationForm(props, state, auth, submitted) {
616
+ const profileDetails = submitted?.profileDetails ?? props.organizationProfile;
617
+ if (profileDetails === null) return null;
618
+ return props.slots.organization({
619
+ state,
620
+ profileDetails,
621
+ submitted,
622
+ pinnedHandle: submitted?.organization.handle ?? null,
623
+ submit: (organization, options) => {
624
+ if (submitted !== null) return auth.createOrganization(submitted, options);
625
+ const normalized = normalizeOrganization(organization);
626
+ if (normalized === null) return Promise.resolve({
627
+ ok: false,
628
+ reason: "INVALID_INPUT"
629
+ });
630
+ const next = {
631
+ profileDetails,
632
+ organization: normalized
633
+ };
634
+ props.onSubmittedOrganization(next);
635
+ return auth.createOrganization(next, options);
636
+ },
637
+ back: () => {
638
+ props.onOrganizationProfile(null);
639
+ props.navigation.organizationBack();
640
+ },
641
+ cancel: props.navigation.organizationCancel
642
+ });
643
+ }
644
+ //#endregion
645
+ export { useCapxulAuth as a, useCapxulIdentityOrNull as c, capxulKeys as d, useCapxulClientOrNull as f, entered as i, useCapxulSend as l, CapxulOnboardingController as n, useCapxulDestination as o, useCapxul as p, CapxulProvider as r, useCapxulIdentity as s, CapxulAuthenticationController as t, useCapxulTransitions as u };
646
+
647
+ //# sourceMappingURL=controllers-BU11km12.mjs.map