@capxul/sdk-react 4.2.0-rc.3 → 4.2.0-rc.5
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 +6 -4
- package/dist/capxul-source.json +3 -3
- package/dist/{controllers-BWmKRFy6.d.mts → controllers-COGAAS-c.d.mts} +13 -6
- package/dist/{controllers-ByS8ZrVm.mjs → controllers-DHXq3gJY.mjs} +129 -81
- package/dist/index.d.mts +26 -9
- package/dist/index.mjs +39 -21
- package/dist/testing/index.d.mts +1 -1
- package/dist/testing/index.mjs +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,10 +28,12 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
28
28
|
session. Account and Organization readiness transitions refresh active
|
|
29
29
|
authenticated queries.
|
|
30
30
|
- `CapxulAuthenticationController` and `CapxulOnboardingController` select
|
|
31
|
-
app-owned slots and render no SDK-owned DOM.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
app-owned slots and render no SDK-owned DOM. The onboarding controller keeps
|
|
32
|
+
no Organization submission: after a reload the Core SDK reads the pending
|
|
33
|
+
Organization from the backend and attaches it, so the app re-submits nothing.
|
|
34
|
+
A claimed identity with no Organization and no app-held form step reports
|
|
35
|
+
`organizationProgress` with `{ at: "loading", orgId: null }` until that
|
|
36
|
+
attach lands.
|
|
35
37
|
- Rich-data hooks such as `useCapxulProfile`, `useCapxulOrgs`, members, roles,
|
|
36
38
|
treasury, and money hooks remain TanStack Query projections. Personal
|
|
37
39
|
holdings and activity reads start only after the Account is claimed.
|
package/dist/capxul-source.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": {
|
|
3
|
-
"commit": "
|
|
4
|
-
"tree": "
|
|
3
|
+
"commit": "72be9c2d5c9b2dcdd27fa5c5acb22163ddec5c44",
|
|
4
|
+
"tree": "1529290d3427ecf12fece6ff70fbea35b077b6a9",
|
|
5
5
|
"branch": "codex/programme-rc",
|
|
6
6
|
"repository": "https://github.com/Xelmar-tech/infrastructure",
|
|
7
7
|
"lockfileSha256": "5870f53b72706d103220c1bcf733cb4fb71196aea32a9cfbdc277d73cc959264"
|
|
8
8
|
},
|
|
9
9
|
"name": "@capxul/sdk-react",
|
|
10
|
-
"version": "4.2.0-rc.
|
|
10
|
+
"version": "4.2.0-rc.5"
|
|
11
11
|
}
|
|
@@ -106,11 +106,23 @@ type AccountProgress = Exclude<Readiness, {
|
|
|
106
106
|
type AccountFailure = Extract<Readiness, {
|
|
107
107
|
at: "failed";
|
|
108
108
|
}>;
|
|
109
|
+
/**
|
|
110
|
+
* The Organization lane while it still owes an answer. `loading` widens the
|
|
111
|
+
* lane's `orgId` to `string | null`: after a reload the SDK asks the backend
|
|
112
|
+
* whether a setup is pending, and until that read lands the claimed identity
|
|
113
|
+
* holds no Organization id at all. The identity model keeps its own `loading`
|
|
114
|
+
* id non-null — only this slot type carries the pre-attach window.
|
|
115
|
+
*/
|
|
109
116
|
type OrgProgress = Exclude<OrgLane, {
|
|
110
117
|
at: "failed";
|
|
111
118
|
} | {
|
|
112
119
|
at: "ready";
|
|
113
|
-
}
|
|
120
|
+
} | {
|
|
121
|
+
at: "loading";
|
|
122
|
+
}> | {
|
|
123
|
+
readonly at: "loading";
|
|
124
|
+
readonly orgId: string | null;
|
|
125
|
+
};
|
|
114
126
|
type OrgFailure = Extract<OrgLane, {
|
|
115
127
|
at: "failed";
|
|
116
128
|
}>;
|
|
@@ -157,11 +169,8 @@ type ProfileSlotProps = {
|
|
|
157
169
|
interface OnboardingControllerProps {
|
|
158
170
|
readonly intent: "personal" | "organization" | null;
|
|
159
171
|
readonly organizationProfile: ProfileDetails | null;
|
|
160
|
-
readonly submittedOrganization: CreateOrganizationSubmission | null;
|
|
161
|
-
readonly recoveryOptions?: () => InvocationOptions;
|
|
162
172
|
readonly onIntent: (intent: "personal" | "organization") => void;
|
|
163
173
|
readonly onOrganizationProfile: (profile: ProfileDetails | null) => void;
|
|
164
|
-
readonly onSubmittedOrganization: (submission: CreateOrganizationSubmission | null) => void;
|
|
165
174
|
readonly navigation: {
|
|
166
175
|
readonly selectorBack: NavigationAction;
|
|
167
176
|
readonly profileCancel: NavigationAction;
|
|
@@ -178,8 +187,6 @@ interface OnboardingControllerProps {
|
|
|
178
187
|
readonly organization: Slot<{
|
|
179
188
|
state: AuthenticatedState;
|
|
180
189
|
profileDetails: ProfileDetails;
|
|
181
|
-
submitted: CreateOrganizationSubmission | null;
|
|
182
|
-
pinnedHandle: string | null;
|
|
183
190
|
submit: (organization: OrganizationDetails, options: InvocationOptions) => ReturnType<CapxulAuth["createOrganization"]>;
|
|
184
191
|
back: NavigationAction;
|
|
185
192
|
cancel: NavigationAction;
|
|
@@ -151,10 +151,35 @@ const capxulKeys = {
|
|
|
151
151
|
]
|
|
152
152
|
};
|
|
153
153
|
//#endregion
|
|
154
|
-
//#region src/
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
//#region src/internal/invocation-controls.ts
|
|
155
|
+
/**
|
|
156
|
+
* Keep only the five public option fields, and only when they hold the right
|
|
157
|
+
* kind of value. A named action verb takes its attempt as an optional FIRST
|
|
158
|
+
* argument, so a screen that wires one straight to a DOM handler
|
|
159
|
+
* (`onClick={row.hide}`) hands us a React SyntheticEvent. That is not an
|
|
160
|
+
* attempt: it must reach neither the SDK call nor the callback echo.
|
|
161
|
+
*/
|
|
162
|
+
function toInvocationOptions(options) {
|
|
163
|
+
if (typeof options !== "object" || options === null) return void 0;
|
|
164
|
+
const attempt = {};
|
|
165
|
+
if (typeof options.correlationId === "string") attempt.correlationId = options.correlationId;
|
|
166
|
+
if (typeof options.journeyId === "string") attempt.journeyId = options.journeyId;
|
|
167
|
+
if (typeof options.timeoutMs === "number") attempt.timeoutMs = options.timeoutMs;
|
|
168
|
+
if (typeof options.deadlineMs === "number") attempt.deadlineMs = options.deadlineMs;
|
|
169
|
+
if (options.signal instanceof AbortSignal) attempt.signal = options.signal;
|
|
170
|
+
return Object.keys(attempt).length === 0 ? void 0 : attempt;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Map the public camel-case invocation options onto the Core SDK's control
|
|
174
|
+
* spelling. ONE definition, because the identity facade and every headless
|
|
175
|
+
* engine must hand the SDK the same shape: a caller that captured one user
|
|
176
|
+
* attempt before it started gets that attempt's `correlation_id`/`journey_id`
|
|
177
|
+
* on whichever operation it starts (#1960 R02).
|
|
178
|
+
*
|
|
179
|
+
* It creates no identifiers. An absent option stays absent, so an automatic
|
|
180
|
+
* read keeps its own invocation context instead of borrowing an attempt's.
|
|
181
|
+
*/
|
|
182
|
+
function toInvocationControls(options) {
|
|
158
183
|
if (options === void 0) return void 0;
|
|
159
184
|
return {
|
|
160
185
|
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
@@ -164,12 +189,16 @@ function controls(options) {
|
|
|
164
189
|
...options.journeyId === void 0 ? {} : { journey_id: options.journeyId }
|
|
165
190
|
};
|
|
166
191
|
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/identity.tsx
|
|
194
|
+
const MISSING_IDENTITY_PROVIDER = Symbol("MISSING_IDENTITY_PROVIDER");
|
|
195
|
+
const IdentityContext = createContext(MISSING_IDENTITY_PROVIDER);
|
|
167
196
|
const failure = (result) => result.ok ? null : {
|
|
168
197
|
ok: false,
|
|
169
198
|
reason: result.refused
|
|
170
199
|
};
|
|
171
200
|
async function guarded(runtime, verb, options, run) {
|
|
172
|
-
const invocation =
|
|
201
|
+
const invocation = toInvocationControls(options);
|
|
173
202
|
try {
|
|
174
203
|
return await (runtime.runFacade?.(verb, invocation, run) ?? run(invocation));
|
|
175
204
|
} catch {
|
|
@@ -215,12 +244,61 @@ function normalizeOrganization(organization) {
|
|
|
215
244
|
return null;
|
|
216
245
|
}
|
|
217
246
|
}
|
|
247
|
+
/** The Organization id when its lane is already running or finished, else null. */
|
|
248
|
+
const runningOrganization = (state) => {
|
|
249
|
+
if (state.phase !== "authenticated" || state.account.at !== "claimed") return null;
|
|
250
|
+
const org = state.account.org;
|
|
251
|
+
return org !== null && (org.at === "loading" || org.at === "settingUp" || org.at === "ready") ? org.orgId : null;
|
|
252
|
+
};
|
|
253
|
+
/** A superseded session read waits at most this long for the winning read. */
|
|
254
|
+
const SESSION_SETTLE_LIMIT_MS = 3e4;
|
|
218
255
|
const accountInFlight = (state) => state.phase === "authenticated" && (state.account.at === "deriving" || state.account.at === "claiming");
|
|
219
256
|
function createAuth(client, clearAuthenticatedQueries) {
|
|
220
257
|
const runtime = client._internal.identity;
|
|
258
|
+
const settledSession = (invocation) => new Promise((resolve) => {
|
|
259
|
+
let done = false;
|
|
260
|
+
let stop = null;
|
|
261
|
+
let timer = null;
|
|
262
|
+
const onAbort = () => finish({
|
|
263
|
+
ok: false,
|
|
264
|
+
reason: "CANCELLED"
|
|
265
|
+
});
|
|
266
|
+
const finish = (result) => {
|
|
267
|
+
if (done) return;
|
|
268
|
+
done = true;
|
|
269
|
+
stop?.();
|
|
270
|
+
if (timer !== null) clearTimeout(timer);
|
|
271
|
+
invocation?.signal?.removeEventListener("abort", onAbort);
|
|
272
|
+
resolve(result);
|
|
273
|
+
};
|
|
274
|
+
if (invocation?.signal?.aborted) {
|
|
275
|
+
onAbort();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
invocation?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
279
|
+
const unsubscribe = runtime.subscribeTransitions((record) => {
|
|
280
|
+
if (record.slot !== "identity:session") return;
|
|
281
|
+
if (record.outcome === "applied" && record.event === "SessionRead") finish({ ok: true });
|
|
282
|
+
else if (record.outcome === "failed" || record.outcome === "cancelled") finish({
|
|
283
|
+
ok: false,
|
|
284
|
+
reason: record.error_code
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
if (done) {
|
|
288
|
+
unsubscribe();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
stop = unsubscribe;
|
|
292
|
+
timer = setTimeout(() => finish({
|
|
293
|
+
ok: false,
|
|
294
|
+
reason: "UNKNOWN"
|
|
295
|
+
}), SESSION_SETTLE_LIMIT_MS);
|
|
296
|
+
});
|
|
221
297
|
const read = async (invocation) => {
|
|
222
298
|
const result = await runtime.send({ _tag: "ReadSession" }, invocation);
|
|
223
|
-
|
|
299
|
+
const refused = failure(result);
|
|
300
|
+
if (refused === null) return { ok: true };
|
|
301
|
+
return refused.reason === "SUPERSEDED" ? settledSession(invocation) : refused;
|
|
224
302
|
};
|
|
225
303
|
const ensureAccount = async (invocation) => {
|
|
226
304
|
const result = await runtime.send({ _tag: "EnsureAccount" }, invocation);
|
|
@@ -303,13 +381,13 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
303
381
|
ok: false,
|
|
304
382
|
reason: "INVALID_INPUT"
|
|
305
383
|
};
|
|
384
|
+
const refreshed = await read(invocation);
|
|
385
|
+
if (!refreshed.ok) return refreshed;
|
|
306
386
|
const current = runtime.snapshot();
|
|
307
387
|
if (current.phase !== "authenticated" || !current.profileComplete) {
|
|
308
388
|
const completed = await runtime.completeProfile(submission.profileDetails, invocation);
|
|
309
389
|
if (!completed.ok) return completed;
|
|
310
390
|
}
|
|
311
|
-
const refreshed = await read(invocation);
|
|
312
|
-
if (!refreshed.ok) return refreshed;
|
|
313
391
|
const claimed = await reachClaimed(invocation);
|
|
314
392
|
return claimed.ok ? {
|
|
315
393
|
ok: true,
|
|
@@ -386,12 +464,13 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
386
464
|
}),
|
|
387
465
|
createOrganization: (submission, options) => guarded(runtime, "createOrganization", options, async (invocation) => {
|
|
388
466
|
const current = runtime.snapshot();
|
|
467
|
+
const running = runningOrganization(current);
|
|
468
|
+
if (running !== null) return {
|
|
469
|
+
ok: true,
|
|
470
|
+
orgId: running
|
|
471
|
+
};
|
|
389
472
|
if (current.phase === "authenticated" && current.account.at === "claimed") {
|
|
390
473
|
const org = current.account.org;
|
|
391
|
-
if (org !== null && (org.at === "loading" || org.at === "settingUp" || org.at === "ready")) return {
|
|
392
|
-
ok: true,
|
|
393
|
-
orgId: org.orgId
|
|
394
|
-
};
|
|
395
474
|
if (org?.at === "failed" && org.orgId !== null) {
|
|
396
475
|
if (!org.retryable) return {
|
|
397
476
|
ok: false,
|
|
@@ -406,6 +485,11 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
406
485
|
}
|
|
407
486
|
const prepared = await prepareOrganization(submission, invocation);
|
|
408
487
|
if (!prepared.ok) return prepared;
|
|
488
|
+
const opened = runningOrganization(runtime.snapshot());
|
|
489
|
+
if (opened !== null) return {
|
|
490
|
+
ok: true,
|
|
491
|
+
orgId: opened
|
|
492
|
+
};
|
|
409
493
|
const created = await runtime.send({
|
|
410
494
|
_tag: "CreateOrganization",
|
|
411
495
|
draft: prepared.organization
|
|
@@ -435,13 +519,6 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
435
519
|
const event = state.phase === "authenticated" && state.account.at === "claimed" ? { _tag: "RetryOrganization" } : { _tag: "RetryAccount" };
|
|
436
520
|
const result = await runtime.send(event, invocation);
|
|
437
521
|
return failure(result) ?? { ok: true };
|
|
438
|
-
}),
|
|
439
|
-
resumeSubmittedOrganization: (submission, options) => guarded(runtime, "createOrganization", options, async (invocation) => {
|
|
440
|
-
const prepared = await prepareOrganization(submission, invocation);
|
|
441
|
-
return prepared.ok ? { ok: true } : {
|
|
442
|
-
ok: false,
|
|
443
|
-
reason: prepared.reason
|
|
444
|
-
};
|
|
445
522
|
})
|
|
446
523
|
};
|
|
447
524
|
}
|
|
@@ -487,17 +564,15 @@ function CapxulIdentityProvider({ client, children }) {
|
|
|
487
564
|
}, []);
|
|
488
565
|
const value = useMemo(() => {
|
|
489
566
|
if (client === null || runtime === null) return null;
|
|
490
|
-
const send = (event, options) => runtime.send(event,
|
|
567
|
+
const send = (event, options) => runtime.send(event, toInvocationControls(options));
|
|
491
568
|
const clearAuthenticatedQueries = async () => {
|
|
492
569
|
await queryClient.cancelQueries({ queryKey: capxulKeys.root });
|
|
493
570
|
await queryClient.resetQueries({ queryKey: capxulKeys.root });
|
|
494
571
|
};
|
|
495
|
-
const auth = createAuth(client, clearAuthenticatedQueries);
|
|
496
572
|
return {
|
|
497
573
|
runtime,
|
|
498
574
|
send,
|
|
499
|
-
auth,
|
|
500
|
-
resumeSubmittedOrganization: auth.resumeSubmittedOrganization,
|
|
575
|
+
auth: createAuth(client, clearAuthenticatedQueries),
|
|
501
576
|
addTransitionListener
|
|
502
577
|
};
|
|
503
578
|
}, [
|
|
@@ -535,9 +610,6 @@ function useCapxulSend() {
|
|
|
535
610
|
function useCapxulAuth() {
|
|
536
611
|
return useIdentityContext().auth;
|
|
537
612
|
}
|
|
538
|
-
function useResumeSubmittedOrganization() {
|
|
539
|
-
return useIdentityContext().resumeSubmittedOrganization;
|
|
540
|
-
}
|
|
541
613
|
function useCapxulDestination() {
|
|
542
614
|
const state = useCapxulIdentity();
|
|
543
615
|
const next = resolveIdentityDestination(state);
|
|
@@ -748,29 +820,24 @@ function CapxulAuthenticationController({ slots }) {
|
|
|
748
820
|
function ready(destination) {
|
|
749
821
|
return destination?.to === "dashboardPersonal" || destination?.to === "dashboardOrganization";
|
|
750
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* The pre-attach window: a claimed identity holding no Organization while the
|
|
825
|
+
* SDK asks the backend whether one is pending. The controller reports progress
|
|
826
|
+
* only when no form step is left to render. An app that still holds an
|
|
827
|
+
* Organization profile and has not submitted through this mount is entering
|
|
828
|
+
* the form, not resuming it — a member who starts an Organization from the
|
|
829
|
+
* dashboard is claimed with a complete Profile before they ever see the form,
|
|
830
|
+
* so reporting progress on the claim alone would hide the form for good.
|
|
831
|
+
*/
|
|
832
|
+
const ATTACHING = {
|
|
833
|
+
at: "loading",
|
|
834
|
+
orgId: null
|
|
835
|
+
};
|
|
751
836
|
function CapxulOnboardingController(props) {
|
|
752
837
|
const state = useCapxulIdentity();
|
|
753
838
|
const destination = useCapxulDestination();
|
|
754
839
|
const auth = useCapxulAuth();
|
|
755
|
-
const
|
|
756
|
-
const resumedSubmission = useRef(null);
|
|
757
|
-
const [failedRecovery, setFailedRecovery] = useState(null);
|
|
758
|
-
const submission = props.submittedOrganization;
|
|
759
|
-
const accountAt = state.phase === "authenticated" ? state.account.at : null;
|
|
760
|
-
useEffect(() => {
|
|
761
|
-
if (props.intent !== "organization" || submission === null || props.recoveryOptions === void 0 || accountAt !== "unknown" && accountAt !== "counterfactual" || resumedSubmission.current === submission) return;
|
|
762
|
-
resumedSubmission.current = submission;
|
|
763
|
-
setFailedRecovery(null);
|
|
764
|
-
resumeSubmittedOrganization(submission, props.recoveryOptions()).then((result) => {
|
|
765
|
-
if (!result.ok) setFailedRecovery(submission);
|
|
766
|
-
});
|
|
767
|
-
}, [
|
|
768
|
-
accountAt,
|
|
769
|
-
props.intent,
|
|
770
|
-
props.recoveryOptions,
|
|
771
|
-
resumeSubmittedOrganization,
|
|
772
|
-
submission
|
|
773
|
-
]);
|
|
840
|
+
const submitted = useRef(false);
|
|
774
841
|
if (state.phase !== "authenticated") return null;
|
|
775
842
|
const { slots, navigation } = props;
|
|
776
843
|
if (props.intent === null) return slots.intent({
|
|
@@ -784,37 +851,24 @@ function CapxulOnboardingController(props) {
|
|
|
784
851
|
completePersonal: auth.completePersonal,
|
|
785
852
|
cancel: navigation.profileCancel
|
|
786
853
|
});
|
|
787
|
-
if (props.intent === "organization" && props.organizationProfile === null
|
|
854
|
+
if (props.intent === "organization" && !state.profileComplete) return props.organizationProfile === null ? slots.profile({
|
|
788
855
|
intent: "organization",
|
|
789
856
|
state,
|
|
790
857
|
continueOrganization: props.onOrganizationProfile,
|
|
791
858
|
cancel: navigation.profileCancel
|
|
792
|
-
});
|
|
793
|
-
if (props.intent === "organization" &&
|
|
794
|
-
if (submission !== null && failedRecovery === submission && state.account.at !== "claimed") return organizationForm(props, state, auth, submission);
|
|
795
|
-
if (submission !== null && state.account.at !== "claimed") {
|
|
796
|
-
if (state.account.at === "failed") {
|
|
797
|
-
const retry = state.account.retryable ? (options) => auth.createOrganization(submission, options) : void 0;
|
|
798
|
-
return slots.accountFailure({
|
|
799
|
-
state,
|
|
800
|
-
account: state.account,
|
|
801
|
-
...retry === void 0 ? {} : { retry }
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
return slots.accountProgress({
|
|
805
|
-
state,
|
|
806
|
-
account: state.account
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
if (submission !== null && state.account.at === "claimed") {
|
|
859
|
+
}) : organizationForm(props, state, auth, submitted);
|
|
860
|
+
if (props.intent === "organization" && state.account.at === "claimed") {
|
|
810
861
|
const org = state.account.org;
|
|
811
|
-
if (org === null) return
|
|
862
|
+
if (org === null) return props.organizationProfile === null || submitted.current ? slots.organizationProgress({
|
|
863
|
+
state,
|
|
864
|
+
org: ATTACHING
|
|
865
|
+
}) : organizationForm(props, state, auth, submitted);
|
|
812
866
|
if (org.at === "failed") {
|
|
813
|
-
const
|
|
867
|
+
const retryable = org.retryable && org.orgId !== null;
|
|
814
868
|
return slots.organizationFailure({
|
|
815
869
|
state,
|
|
816
870
|
org,
|
|
817
|
-
...
|
|
871
|
+
...retryable ? { retry: auth.retry } : {}
|
|
818
872
|
});
|
|
819
873
|
}
|
|
820
874
|
if (org.at !== "ready") return slots.organizationProgress({
|
|
@@ -837,26 +891,20 @@ function CapxulOnboardingController(props) {
|
|
|
837
891
|
}) : null;
|
|
838
892
|
}
|
|
839
893
|
function organizationForm(props, state, auth, submitted) {
|
|
840
|
-
const profileDetails =
|
|
894
|
+
const profileDetails = props.organizationProfile;
|
|
841
895
|
if (profileDetails === null) return null;
|
|
842
896
|
return props.slots.organization({
|
|
843
897
|
state,
|
|
844
898
|
profileDetails,
|
|
845
|
-
submitted,
|
|
846
|
-
pinnedHandle: submitted?.organization.handle ?? null,
|
|
847
899
|
submit: (organization, options) => {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (normalized === null) return auth.createOrganization({
|
|
900
|
+
submitted.current = true;
|
|
901
|
+
return auth.createOrganization({
|
|
851
902
|
profileDetails,
|
|
852
903
|
organization
|
|
853
|
-
}, options)
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
};
|
|
858
|
-
props.onSubmittedOrganization(next);
|
|
859
|
-
return auth.createOrganization(next, options);
|
|
904
|
+
}, options).then((result) => {
|
|
905
|
+
if (!result.ok) submitted.current = false;
|
|
906
|
+
return result;
|
|
907
|
+
});
|
|
860
908
|
},
|
|
861
909
|
back: () => {
|
|
862
910
|
props.onOrganizationProfile(null);
|
|
@@ -866,4 +914,4 @@ function organizationForm(props, state, auth, submitted) {
|
|
|
866
914
|
});
|
|
867
915
|
}
|
|
868
916
|
//#endregion
|
|
869
|
-
export { useCapxulAuth as a, useCapxulIdentityOrNull as c,
|
|
917
|
+
export { useCapxulAuth as a, useCapxulIdentityOrNull as c, toInvocationControls as d, toInvocationOptions as f, useCapxul as h, entered as i, useCapxulSend as l, useCapxulClientOrNull as m, CapxulOnboardingController as n, useCapxulDestination as o, capxulKeys as p, CapxulProvider as r, useCapxulIdentity as s, CapxulAuthenticationController as t, useCapxulTransitions as u };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as entered, C as CreateOrganizationFailure, D as OrganizationDetails, E as InvocationOptions, F as useCapxulTransitions, M as useCapxulDestination, N as useCapxulIdentity, O as ProfileDetails, P as useCapxulSend, S as CapxulSend, T as Destination, _ as ReadyDestination, a as AuthenticationSlots, b as Slot, c as ControllerAction, d as OnboardingControllerProps, f as OrgFailure, g as ProfileSlotProps, h as PendingAuthState, i as AuthenticatedState, j as useCapxulAuth, k as SendResult, l as FaultedState, m as OtpPendingState, n as AccountProgress, o as CapxulAuthenticationController, p as OrgProgress, r as ActionResult, s as CapxulOnboardingController, t as AccountFailure, u as NavigationAction, v as RetryAction, w as CreateOrganizationSubmission, x as CapxulAuth, y as SignedOutState } from "./controllers-
|
|
1
|
+
import { A as entered, C as CreateOrganizationFailure, D as OrganizationDetails, E as InvocationOptions, F as useCapxulTransitions, M as useCapxulDestination, N as useCapxulIdentity, O as ProfileDetails, P as useCapxulSend, S as CapxulSend, T as Destination, _ as ReadyDestination, a as AuthenticationSlots, b as Slot, c as ControllerAction, d as OnboardingControllerProps, f as OrgFailure, g as ProfileSlotProps, h as PendingAuthState, i as AuthenticatedState, j as useCapxulAuth, k as SendResult, l as FaultedState, m as OtpPendingState, n as AccountProgress, o as CapxulAuthenticationController, p as OrgProgress, r as ActionResult, s as CapxulOnboardingController, t as AccountFailure, u as NavigationAction, v as RetryAction, w as CreateOrganizationSubmission, x as CapxulAuth, y as SignedOutState } from "./controllers-COGAAS-c.mjs";
|
|
2
2
|
import { ReactNode, RefCallback } from "react";
|
|
3
3
|
import { QueryClient, UseMutationResult, UseQueryResult } from "@tanstack/react-query";
|
|
4
4
|
import { Account, AccountRequirement, ActivityAnnotation, ActivityAnnotationInput, ActivityDetail, ActivityKind, ActivityListParams, ActivityPage, ActivityPhase, ActivityRange, ActivityReference, ActorReference, AddressBookEntry, BudgetId, CapxulClient, CapxulError, CapxulResult, CapxulSigner, CreateOrgInput, CurrentHoldings, HostObservability, IdentityDestination, InviteMemberInput, MemberView, Money, MoneyParseErrorReason, OrgId, OrgView, OrganizationPaymentBatchInput, OrganizationPaymentInput, PartyId, Payment, PaymentDirection, PaymentDocumentRef, PaymentDocumentRender, PaymentStatus, PaymentTiming, PaymentType, PaymentsPayInput, PayrollGroupId, PayrollGroupInput, PayrollRunId, PayrollRunStatus, Permission, PermissionMethods, PermissionReadResult, Profile, RoleView, SubmittedPermissionExecution, isClaimed, isRestoring } from "@capxul/sdk";
|
|
@@ -383,9 +383,9 @@ interface Contact {
|
|
|
383
383
|
interface ContactRow extends Contact {
|
|
384
384
|
/** Ours: one initials rule, not one copy per screen. */
|
|
385
385
|
readonly initials: string;
|
|
386
|
-
readonly rename: (name: string) => void;
|
|
387
|
-
readonly hide: () => void;
|
|
388
|
-
readonly unhide: () => void;
|
|
386
|
+
readonly rename: (name: string, options?: InvocationOptions) => void;
|
|
387
|
+
readonly hide: (options?: InvocationOptions) => void;
|
|
388
|
+
readonly unhide: (options?: InvocationOptions) => void;
|
|
389
389
|
}
|
|
390
390
|
/** ADR-0023 R1: refusal CODES, never sentences — the app owns the words. */
|
|
391
391
|
type ContactsRefusal = "loading" | "org-unavailable" | "unavailable";
|
|
@@ -421,7 +421,13 @@ interface ContactsListOptions {
|
|
|
421
421
|
interface ContactsAddSlice {
|
|
422
422
|
readonly value: string;
|
|
423
423
|
readonly change: (text: string) => void;
|
|
424
|
-
|
|
424
|
+
/**
|
|
425
|
+
* #1960 R02: hand it the options the screen captured BEFORE the click. The
|
|
426
|
+
* same snapshot reaches the SDK add call and comes back on `onAdded`/
|
|
427
|
+
* `onFailed`, so the UI observation and the operation share one identity even
|
|
428
|
+
* when the reply lands after an account switch. Omitted stays omitted.
|
|
429
|
+
*/
|
|
430
|
+
readonly submit: (options?: InvocationOptions) => void;
|
|
425
431
|
readonly isSubmitting: boolean;
|
|
426
432
|
readonly blocked: boolean;
|
|
427
433
|
readonly blockedReason: ContactsAddBlockedReason | null;
|
|
@@ -431,9 +437,14 @@ interface ContactsAddSlice {
|
|
|
431
437
|
//#region src/headless/contacts/contacts.d.ts
|
|
432
438
|
interface CapxulContactsProps {
|
|
433
439
|
readonly actor: ContactsActor;
|
|
434
|
-
|
|
440
|
+
/**
|
|
441
|
+
* `options` is exactly what the caller handed `.Add.submit` or a row verb, so
|
|
442
|
+
* the screen's own observation of the finished action can reuse the attempt
|
|
443
|
+
* identity the SDK operation ran under (#1960 R02).
|
|
444
|
+
*/
|
|
445
|
+
readonly onAdded: (contact: Contact, options?: InvocationOptions) => void;
|
|
435
446
|
/** ADR-0023 R1: the app maps `error.code` to its own copy; no SDK sentence. */
|
|
436
|
-
readonly onFailed: (error: CapxulError) => void;
|
|
447
|
+
readonly onFailed: (error: CapxulError, options?: InvocationOptions) => void;
|
|
437
448
|
readonly children: ReactNode;
|
|
438
449
|
}
|
|
439
450
|
declare function Root$6({ actor, onAdded, onFailed, children }: CapxulContactsProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -545,7 +556,12 @@ interface ActivityRowsSlice {
|
|
|
545
556
|
readonly rows: readonly ActivityRow[];
|
|
546
557
|
readonly isLoading: boolean;
|
|
547
558
|
readonly error: CapxulError | null;
|
|
548
|
-
|
|
559
|
+
/**
|
|
560
|
+
* #1960 R02: pass the options captured before the retry was selected and the
|
|
561
|
+
* re-read runs under the same attempt identity as the UI observation. Omit
|
|
562
|
+
* them and the read keeps its own invocation context.
|
|
563
|
+
*/
|
|
564
|
+
readonly retry: (options?: InvocationOptions) => void;
|
|
549
565
|
/** Server read time of the newest page, so the app can say which read is older. */
|
|
550
566
|
readonly observedAt: number | null;
|
|
551
567
|
}
|
|
@@ -589,7 +605,8 @@ interface CsvExport {
|
|
|
589
605
|
readonly truncated: boolean;
|
|
590
606
|
}
|
|
591
607
|
interface ActivityExportSlice {
|
|
592
|
-
|
|
608
|
+
/** Same contract as `retry`: the export's pages run under the caller's attempt. */
|
|
609
|
+
readonly toCsv: (options?: InvocationOptions) => Promise<CsvExport>;
|
|
593
610
|
readonly isExporting: boolean;
|
|
594
611
|
}
|
|
595
612
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { a as useCapxulAuth, c as useCapxulIdentityOrNull, d as
|
|
2
|
+
import { a as useCapxulAuth, c as useCapxulIdentityOrNull, d as toInvocationControls, f as toInvocationOptions, h as useCapxul, i as entered, l as useCapxulSend, m as useCapxulClientOrNull, n as CapxulOnboardingController, o as useCapxulDestination, p as capxulKeys, r as CapxulProvider, s as useCapxulIdentity, t as CapxulAuthenticationController, u as useCapxulTransitions } from "./controllers-DHXq3gJY.mjs";
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
5
|
import { CAPXUL_OPERATIONS, CapxulError, EVM_ADDRESS_RE, Errors, HANDLE_RE, PAYMENT_DIRECTIONS, PAYMENT_STATUSES, fingerprintPaymentIntent, formatMoney, isCapxulError, isClaimed, isClaimed as isClaimed$1, isMoneyParseError, isRestoring, isRestoring as isRestoring$1, parseMoney, paymentPhase, resolveIdentityDestination, toEvmAddress, toPartyId } from "@capxul/sdk";
|
|
@@ -879,6 +879,8 @@ function useCapxulUploadImage() {
|
|
|
879
879
|
}
|
|
880
880
|
//#endregion
|
|
881
881
|
//#region src/headless/contacts/use-contacts.ts
|
|
882
|
+
/** One captured snapshot, or nothing — never a fabricated empty options object. */
|
|
883
|
+
const carry = (options) => options === void 0 ? {} : { options };
|
|
882
884
|
function useContacts(input) {
|
|
883
885
|
const { actor, onAdded, onFailed } = input;
|
|
884
886
|
const client = useCapxulClientOrNull();
|
|
@@ -895,35 +897,40 @@ function useContacts(input) {
|
|
|
895
897
|
const writeMutate = useMutation({
|
|
896
898
|
mutationFn: async (command) => {
|
|
897
899
|
const target = requireBook(book, `addressBook.${command.kind}`);
|
|
900
|
+
const controls = toInvocationControls(command.options);
|
|
898
901
|
return unwrapCapxulResult(command.kind === "rename" ? await target.label({
|
|
899
902
|
entryId: command.id,
|
|
900
903
|
label: command.name
|
|
901
|
-
}) : command.kind === "hide" ? await target.hide(command.id) : await target.unhide(command.id));
|
|
904
|
+
}, controls) : command.kind === "hide" ? await target.hide(command.id, controls) : await target.unhide(command.id, controls));
|
|
902
905
|
},
|
|
903
906
|
onSettled: invalidate,
|
|
904
|
-
onError: onFailed
|
|
907
|
+
onError: (error, command) => onFailed(error, command.options)
|
|
905
908
|
}).mutate;
|
|
906
909
|
const [value, setValue] = useState("");
|
|
907
910
|
const [addError, setAddError] = useState(null);
|
|
908
911
|
const addContact = useMutation({
|
|
909
|
-
mutationFn: async (
|
|
912
|
+
mutationFn: async (command) => unwrapCapxulResult(await requireBook(book, CAPXUL_OPERATIONS.addressBook.add).add({ ref: command.ref }, toInvocationControls(command.options))),
|
|
910
913
|
onSettled: invalidate
|
|
911
914
|
});
|
|
912
915
|
const addMutate = addContact.mutate;
|
|
913
916
|
const blockedReason = refusal !== null ? "org-unavailable" : value.trim() === "" ? "empty" : null;
|
|
914
917
|
const submitGate = useRef(false);
|
|
915
|
-
const submit = useCallback(() => {
|
|
918
|
+
const submit = useCallback((options) => {
|
|
916
919
|
if (blockedReason !== null || submitGate.current) return;
|
|
920
|
+
const attempt = toInvocationOptions(options);
|
|
917
921
|
submitGate.current = true;
|
|
918
922
|
setAddError(null);
|
|
919
|
-
addMutate(
|
|
923
|
+
addMutate({
|
|
924
|
+
ref: refFromTypedText(value),
|
|
925
|
+
...carry(attempt)
|
|
926
|
+
}, {
|
|
920
927
|
onSuccess: (entry) => {
|
|
921
928
|
setValue("");
|
|
922
|
-
onAdded(toContact(entry));
|
|
929
|
+
onAdded(toContact(entry), attempt);
|
|
923
930
|
},
|
|
924
931
|
onError: (error) => {
|
|
925
932
|
setAddError(error.code === "INVALID_INPUT" ? "unresolved" : "failed");
|
|
926
|
-
onFailed(error);
|
|
933
|
+
onFailed(error, attempt);
|
|
927
934
|
},
|
|
928
935
|
onSettled: () => {
|
|
929
936
|
submitGate.current = false;
|
|
@@ -944,18 +951,21 @@ function useContacts(input) {
|
|
|
944
951
|
book,
|
|
945
952
|
scope,
|
|
946
953
|
refusal,
|
|
947
|
-
rename: useCallback((id, name) => writeMutate({
|
|
954
|
+
rename: useCallback((id, name, options) => writeMutate({
|
|
948
955
|
kind: "rename",
|
|
949
956
|
id,
|
|
950
|
-
name
|
|
957
|
+
name,
|
|
958
|
+
...carry(toInvocationOptions(options))
|
|
951
959
|
}), [writeMutate]),
|
|
952
|
-
hide: useCallback((id) => writeMutate({
|
|
960
|
+
hide: useCallback((id, options) => writeMutate({
|
|
953
961
|
kind: "hide",
|
|
954
|
-
id
|
|
962
|
+
id,
|
|
963
|
+
...carry(toInvocationOptions(options))
|
|
955
964
|
}), [writeMutate]),
|
|
956
|
-
unhide: useCallback((id) => writeMutate({
|
|
965
|
+
unhide: useCallback((id, options) => writeMutate({
|
|
957
966
|
kind: "unhide",
|
|
958
|
-
id
|
|
967
|
+
id,
|
|
968
|
+
...carry(toInvocationOptions(options))
|
|
959
969
|
}), [writeMutate]),
|
|
960
970
|
add: {
|
|
961
971
|
value,
|
|
@@ -989,9 +999,9 @@ function useContactsList(engine, options) {
|
|
|
989
999
|
if (entries === void 0) return [];
|
|
990
1000
|
return (limit === void 0 ? entries : entries.slice(0, limit)).map((entry) => Object.assign(toContact(entry), {
|
|
991
1001
|
initials: initialsOf(entry.label),
|
|
992
|
-
rename: (name) => rename(entry.id, name),
|
|
993
|
-
hide: () => hide(entry.id),
|
|
994
|
-
unhide: () => unhide(entry.id)
|
|
1002
|
+
rename: (name, attempt) => rename(entry.id, name, attempt),
|
|
1003
|
+
hide: (attempt) => hide(entry.id, attempt),
|
|
1004
|
+
unhide: (attempt) => unhide(entry.id, attempt)
|
|
995
1005
|
}));
|
|
996
1006
|
}, [
|
|
997
1007
|
entries,
|
|
@@ -1567,6 +1577,12 @@ function useActivity(input) {
|
|
|
1567
1577
|
if (filter !== void 0) params.filter = filter;
|
|
1568
1578
|
return params;
|
|
1569
1579
|
}, [actor, filter]);
|
|
1580
|
+
const selectedRetry = useRef(void 0);
|
|
1581
|
+
const takeSelectedRetry = useCallback(() => {
|
|
1582
|
+
const attempt = selectedRetry.current;
|
|
1583
|
+
selectedRetry.current = void 0;
|
|
1584
|
+
return toInvocationControls(attempt);
|
|
1585
|
+
}, []);
|
|
1570
1586
|
const query = useInfiniteQuery({
|
|
1571
1587
|
queryKey: [
|
|
1572
1588
|
...capxulKeys.activity,
|
|
@@ -1576,7 +1592,7 @@ function useActivity(input) {
|
|
|
1576
1592
|
filter ?? "none"
|
|
1577
1593
|
],
|
|
1578
1594
|
queryFn: async ({ pageParam }) => {
|
|
1579
|
-
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.list).activity.list(listParams(pageParam, limit)));
|
|
1595
|
+
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.list).activity.list(listParams(pageParam, limit), takeSelectedRetry()));
|
|
1580
1596
|
},
|
|
1581
1597
|
initialPageParam: void 0,
|
|
1582
1598
|
getNextPageParam: (last) => last.cursor ?? void 0,
|
|
@@ -1624,14 +1640,15 @@ function useActivity(input) {
|
|
|
1624
1640
|
observerRef.current = observer;
|
|
1625
1641
|
}, [loadMore]);
|
|
1626
1642
|
const [isExporting, setIsExporting] = useState(false);
|
|
1627
|
-
const toCsv = useCallback(async () => {
|
|
1643
|
+
const toCsv = useCallback(async (options) => {
|
|
1628
1644
|
setIsExporting(true);
|
|
1645
|
+
const controls = toInvocationControls(toInvocationOptions(options));
|
|
1629
1646
|
try {
|
|
1630
1647
|
const bootstrapped = requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.list);
|
|
1631
1648
|
const items = [];
|
|
1632
1649
|
let cursor;
|
|
1633
1650
|
for (let page = 0; page < EXPORT_PAGE_CEILING; page += 1) {
|
|
1634
|
-
const result = unwrapCapxulResult(await bootstrapped.activity.list(listParams(cursor, EXPORT_PAGE_SIZE)));
|
|
1651
|
+
const result = unwrapCapxulResult(await bootstrapped.activity.list(listParams(cursor, EXPORT_PAGE_SIZE), controls));
|
|
1635
1652
|
items.push(...result.items);
|
|
1636
1653
|
cursor = result.cursor ?? void 0;
|
|
1637
1654
|
if (cursor === void 0) break;
|
|
@@ -1654,7 +1671,8 @@ function useActivity(input) {
|
|
|
1654
1671
|
setDirection(void 0);
|
|
1655
1672
|
setStatus([]);
|
|
1656
1673
|
}, []);
|
|
1657
|
-
const retry = useCallback(() => {
|
|
1674
|
+
const retry = useCallback((options) => {
|
|
1675
|
+
selectedRetry.current = toInvocationOptions(options);
|
|
1658
1676
|
refetch();
|
|
1659
1677
|
}, [refetch]);
|
|
1660
1678
|
return {
|
package/dist/testing/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AuthenticationSlots, d as OnboardingControllerProps } from "../controllers-
|
|
1
|
+
import { a as AuthenticationSlots, d as OnboardingControllerProps } from "../controllers-COGAAS-c.mjs";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
import { CapxulTestClient, CapxulTestClock, CapxulTestObservation, CreateCapxulTestClientOptions, SeedTestIdentityInput, createCapxulTestClient } from "@capxul/sdk/testing";
|
package/dist/testing/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as CapxulOnboardingController, r as CapxulProvider, t as CapxulAuthenticationController } from "../controllers-
|
|
1
|
+
import { n as CapxulOnboardingController, r as CapxulProvider, t as CapxulAuthenticationController } from "../controllers-DHXq3gJY.mjs";
|
|
2
2
|
import "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
import { createCapxulTestClient } from "@capxul/sdk/testing";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capxul/sdk-react",
|
|
3
|
-
"version": "4.2.0-rc.
|
|
3
|
+
"version": "4.2.0-rc.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/Xelmar-tech/infrastructure.git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@capxul/sdk": "4.2.0-rc.
|
|
29
|
+
"@capxul/sdk": "4.2.0-rc.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@tanstack/react-query": "^5.66.9",
|