@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 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. Supplying the onboarding
32
- controller's `recoveryOptions` factory lets a fresh actor resume account
33
- prerequisites before the stored Organization replay; a failed prerequisite
34
- returns the same manual replay instead of looping.
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.
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "source": {
3
- "commit": "4547e113caca6cf936b489dda3c22c68ea2f30d8",
4
- "tree": "6ccaeaf77b6d8fef28eacd312d0b884a383c502f",
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.3"
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/identity.tsx
155
- const MISSING_IDENTITY_PROVIDER = Symbol("MISSING_IDENTITY_PROVIDER");
156
- const IdentityContext = createContext(MISSING_IDENTITY_PROVIDER);
157
- function controls(options) {
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 = controls(options);
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
- return failure(result) ?? { ok: true };
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, controls(options));
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 resumeSubmittedOrganization = useResumeSubmittedOrganization();
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 && submission === null) return slots.profile({
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" && props.organizationProfile !== null && submission === null) return organizationForm(props, state, auth, null);
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 organizationForm(props, state, auth, submission);
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 retry = org.retryable ? org.orgId === null ? (options) => auth.createOrganization(submission, options) : auth.retry : void 0;
867
+ const retryable = org.retryable && org.orgId !== null;
814
868
  return slots.organizationFailure({
815
869
  state,
816
870
  org,
817
- ...retry === void 0 ? {} : { retry }
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 = submitted?.profileDetails ?? props.organizationProfile;
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
- if (submitted !== null) return auth.createOrganization(submitted, options);
849
- const normalized = normalizeOrganization(organization);
850
- if (normalized === null) return auth.createOrganization({
900
+ submitted.current = true;
901
+ return auth.createOrganization({
851
902
  profileDetails,
852
903
  organization
853
- }, options);
854
- const next = {
855
- profileDetails,
856
- organization: normalized
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, 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 };
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-BWmKRFy6.mjs";
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
- readonly submit: () => void;
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
- readonly onAdded: (contact: Contact) => void;
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
- readonly retry: () => void;
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
- readonly toCsv: () => Promise<CsvExport>;
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 capxulKeys, f as useCapxulClientOrNull, i as entered, l as useCapxulSend, n as CapxulOnboardingController, o as useCapxulDestination, p as useCapxul, r as CapxulProvider, s as useCapxulIdentity, t as CapxulAuthenticationController, u as useCapxulTransitions } from "./controllers-ByS8ZrVm.mjs";
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 (ref) => unwrapCapxulResult(await requireBook(book, CAPXUL_OPERATIONS.addressBook.add).add({ ref })),
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(refFromTypedText(value), {
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 {
@@ -1,4 +1,4 @@
1
- import { a as AuthenticationSlots, d as OnboardingControllerProps } from "../controllers-BWmKRFy6.mjs";
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";
@@ -1,4 +1,4 @@
1
- import { n as CapxulOnboardingController, r as CapxulProvider, t as CapxulAuthenticationController } from "../controllers-ByS8ZrVm.mjs";
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",
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.3"
29
+ "@capxul/sdk": "4.2.0-rc.5"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@tanstack/react-query": "^5.66.9",