@capxul/sdk-react 4.2.0 → 4.20.0-beta.10
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 +21 -4
- package/dist/{controllers-BPEhjKX9.mjs → controllers-L1UqwWS1.mjs} +177 -19
- package/dist/index.d.mts +164 -53
- package/dist/index.mjs +742 -284
- package/dist/testing/index.mjs +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { a as useCapxulAuth, c as useCapxulIdentityOrNull, d as
|
|
2
|
+
import { _ as useCapxul, a as useCapxulAuth, c as useCapxulIdentityOrNull, d as toInvocationControls, f as toInvocationOptions, g as useCapxulClientOrNull, h as capxulKeys, i as entered, l as useCapxulSend, m as actingAccount, n as CapxulOnboardingController, o as useCapxulDestination, p as accountKey, r as CapxulProvider, s as useCapxulIdentity, t as CapxulAuthenticationController, u as useCapxulTransitions } from "./controllers-L1UqwWS1.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
|
-
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";
|
|
5
|
+
import { CAPXUL_OPERATIONS, CapxulError, EVM_ADDRESS_RE, Errors, HANDLE_RE, PAYMENT_DIRECTIONS, PAYMENT_STATUSES, assetSymbolFor, fingerprintPaymentIntent, formatAssetAmount, formatMoney, isCapxulError, isClaimed, isClaimed as isClaimed$1, isMoneyParseError, isRestoring, isRestoring as isRestoring$1, parseAssetAmount, parseMoney, paymentPhase, requireAsset, resolveIdentityDestination, toEvmAddress, toPartyId, toRaw, valueActivitySummary } from "@capxul/sdk";
|
|
6
6
|
import { Fragment, jsx } from "react/jsx-runtime";
|
|
7
7
|
//#region src/internal/require-bootstrapped-client.ts
|
|
8
8
|
/**
|
|
@@ -47,8 +47,9 @@ function useCapxulProfile() {
|
|
|
47
47
|
//#region src/hooks/use-capxul-account-balance.ts
|
|
48
48
|
function useCapxulAccountBalance(options) {
|
|
49
49
|
const client = useCapxulClientOrNull();
|
|
50
|
+
const identity = useCapxulIdentityOrNull();
|
|
50
51
|
return useQuery({
|
|
51
|
-
queryKey: capxulKeys.accountBalance,
|
|
52
|
+
queryKey: accountKey(capxulKeys.accountBalance, identity),
|
|
52
53
|
queryFn: async () => {
|
|
53
54
|
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.accounts.read).accounts.read());
|
|
54
55
|
},
|
|
@@ -302,7 +303,7 @@ function useCapxulPayments(options) {
|
|
|
302
303
|
const client = useCapxulClientOrNull();
|
|
303
304
|
const identity = useCapxulIdentityOrNull();
|
|
304
305
|
return useQuery({
|
|
305
|
-
queryKey: capxulKeys.payments,
|
|
306
|
+
queryKey: accountKey(capxulKeys.payments, identity),
|
|
306
307
|
queryFn: async () => {
|
|
307
308
|
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.payments.list).payments.list());
|
|
308
309
|
},
|
|
@@ -311,8 +312,9 @@ function useCapxulPayments(options) {
|
|
|
311
312
|
}
|
|
312
313
|
function useCapxulPayment(paymentId, options) {
|
|
313
314
|
const client = useCapxulClientOrNull();
|
|
315
|
+
const identity = useCapxulIdentityOrNull();
|
|
314
316
|
return useQuery({
|
|
315
|
-
queryKey: capxulKeys.payment(paymentId),
|
|
317
|
+
queryKey: accountKey(capxulKeys.payment(paymentId), identity),
|
|
316
318
|
queryFn: async () => {
|
|
317
319
|
if (paymentId === void 0) throw Errors.invalidInput("paymentId", "required for payments.get");
|
|
318
320
|
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.payments.get).payments.get(paymentId));
|
|
@@ -430,7 +432,7 @@ function useCapxulActivity(params) {
|
|
|
430
432
|
const client = useCapxulClientOrNull();
|
|
431
433
|
const identity = useCapxulIdentityOrNull();
|
|
432
434
|
return useQuery({
|
|
433
|
-
queryKey: [...capxulKeys.activity, params ?? {}],
|
|
435
|
+
queryKey: accountKey([...capxulKeys.activity, params ?? {}], identity),
|
|
434
436
|
queryFn: async () => {
|
|
435
437
|
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.list).activity.list(params));
|
|
436
438
|
},
|
|
@@ -441,7 +443,7 @@ function useCapxulActivityDetail(reference, actor) {
|
|
|
441
443
|
const client = useCapxulClientOrNull();
|
|
442
444
|
const identity = useCapxulIdentityOrNull();
|
|
443
445
|
return useQuery({
|
|
444
|
-
queryKey: capxulKeys.activityDetail(reference, actor),
|
|
446
|
+
queryKey: accountKey(capxulKeys.activityDetail(reference, actor), identity),
|
|
445
447
|
queryFn: async () => {
|
|
446
448
|
if (reference === void 0) throw Errors.invalidInput("reference", "required for activity.get");
|
|
447
449
|
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.get).activity.get(reference, actor === void 0 ? void 0 : { actor }));
|
|
@@ -466,7 +468,7 @@ function useCapxulHoldings(params) {
|
|
|
466
468
|
const identity = useCapxulIdentityOrNull();
|
|
467
469
|
const actor = params?.actor;
|
|
468
470
|
return useQuery({
|
|
469
|
-
queryKey: capxulKeys.holdings(actor),
|
|
471
|
+
queryKey: accountKey(capxulKeys.holdings(actor), identity),
|
|
470
472
|
queryFn: async () => {
|
|
471
473
|
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.holdings.current).holdings.current(actor === void 0 ? void 0 : { actor }));
|
|
472
474
|
},
|
|
@@ -477,8 +479,9 @@ function useCapxulHoldings(params) {
|
|
|
477
479
|
//#region src/hooks/use-capxul-orgs.ts
|
|
478
480
|
function useCapxulOrgs(options) {
|
|
479
481
|
const client = useCapxulClientOrNull();
|
|
482
|
+
const identity = useCapxulIdentityOrNull();
|
|
480
483
|
return useQuery({
|
|
481
|
-
queryKey: capxulKeys.orgs,
|
|
484
|
+
queryKey: accountKey(capxulKeys.orgs, identity),
|
|
482
485
|
queryFn: async () => {
|
|
483
486
|
return unwrapCapxulResult(await requireBootstrappedClient(client, "orgs").orgs());
|
|
484
487
|
},
|
|
@@ -517,9 +520,10 @@ function useCapxulOrgRoles(orgId, options) {
|
|
|
517
520
|
//#region src/hooks/use-capxul-org-treasury.ts
|
|
518
521
|
function useCapxulOrgTreasury(orgId, options) {
|
|
519
522
|
const client = useCapxulClientOrNull();
|
|
523
|
+
const identity = useCapxulIdentityOrNull();
|
|
520
524
|
const enabled = options?.enabled ?? true;
|
|
521
525
|
return useQuery({
|
|
522
|
-
queryKey: capxulKeys.orgTreasury(orgId),
|
|
526
|
+
queryKey: accountKey(capxulKeys.orgTreasury(orgId), identity),
|
|
523
527
|
queryFn: async () => {
|
|
524
528
|
if (orgId === void 0) throw Errors.invalidInput("orgId", "required for useCapxulOrgTreasury");
|
|
525
529
|
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.org.treasury).org(orgId).treasury());
|
|
@@ -542,22 +546,6 @@ function useCapxulCreateOrg() {
|
|
|
542
546
|
});
|
|
543
547
|
}
|
|
544
548
|
//#endregion
|
|
545
|
-
//#region src/hooks/use-capxul-invite-member.ts
|
|
546
|
-
function useCapxulInviteMember(orgId) {
|
|
547
|
-
const client = useCapxulClientOrNull();
|
|
548
|
-
const queryClient = useQueryClient();
|
|
549
|
-
return useMutation({
|
|
550
|
-
mutationFn: async (input) => {
|
|
551
|
-
if (orgId === void 0) throw Errors.invalidInput("orgId", "required for useCapxulInviteMember");
|
|
552
|
-
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.org.invite).org(orgId).invite(input));
|
|
553
|
-
},
|
|
554
|
-
onSuccess: async () => {
|
|
555
|
-
if (orgId === void 0) return;
|
|
556
|
-
await queryClient.invalidateQueries({ queryKey: capxulKeys.orgMembers(orgId) });
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
//#endregion
|
|
561
549
|
//#region src/headless/journey/journey-state.ts
|
|
562
550
|
const JOURNEY_KEY = "capxul.onboarding.journey.v1";
|
|
563
551
|
const VALIDATED_OWNER_KEY = "capxul.onboarding.validated-owner.v1";
|
|
@@ -879,6 +867,8 @@ function useCapxulUploadImage() {
|
|
|
879
867
|
}
|
|
880
868
|
//#endregion
|
|
881
869
|
//#region src/headless/contacts/use-contacts.ts
|
|
870
|
+
/** One captured snapshot, or nothing — never a fabricated empty options object. */
|
|
871
|
+
const carry$1 = (options) => options === void 0 ? {} : { options };
|
|
882
872
|
function useContacts(input) {
|
|
883
873
|
const { actor, onAdded, onFailed } = input;
|
|
884
874
|
const client = useCapxulClientOrNull();
|
|
@@ -895,35 +885,40 @@ function useContacts(input) {
|
|
|
895
885
|
const writeMutate = useMutation({
|
|
896
886
|
mutationFn: async (command) => {
|
|
897
887
|
const target = requireBook(book, `addressBook.${command.kind}`);
|
|
888
|
+
const controls = toInvocationControls(command.options);
|
|
898
889
|
return unwrapCapxulResult(command.kind === "rename" ? await target.label({
|
|
899
890
|
entryId: command.id,
|
|
900
891
|
label: command.name
|
|
901
|
-
}) : command.kind === "hide" ? await target.hide(command.id) : await target.unhide(command.id));
|
|
892
|
+
}, controls) : command.kind === "hide" ? await target.hide(command.id, controls) : await target.unhide(command.id, controls));
|
|
902
893
|
},
|
|
903
894
|
onSettled: invalidate,
|
|
904
|
-
onError: onFailed
|
|
895
|
+
onError: (error, command) => onFailed(error, command.options)
|
|
905
896
|
}).mutate;
|
|
906
897
|
const [value, setValue] = useState("");
|
|
907
898
|
const [addError, setAddError] = useState(null);
|
|
908
899
|
const addContact = useMutation({
|
|
909
|
-
mutationFn: async (
|
|
900
|
+
mutationFn: async (command) => unwrapCapxulResult(await requireBook(book, CAPXUL_OPERATIONS.addressBook.add).add({ ref: command.ref }, toInvocationControls(command.options))),
|
|
910
901
|
onSettled: invalidate
|
|
911
902
|
});
|
|
912
903
|
const addMutate = addContact.mutate;
|
|
913
904
|
const blockedReason = refusal !== null ? "org-unavailable" : value.trim() === "" ? "empty" : null;
|
|
914
905
|
const submitGate = useRef(false);
|
|
915
|
-
const submit = useCallback(() => {
|
|
906
|
+
const submit = useCallback((options) => {
|
|
916
907
|
if (blockedReason !== null || submitGate.current) return;
|
|
908
|
+
const attempt = toInvocationOptions(options);
|
|
917
909
|
submitGate.current = true;
|
|
918
910
|
setAddError(null);
|
|
919
|
-
addMutate(
|
|
911
|
+
addMutate({
|
|
912
|
+
ref: refFromTypedText(value),
|
|
913
|
+
...carry$1(attempt)
|
|
914
|
+
}, {
|
|
920
915
|
onSuccess: (entry) => {
|
|
921
916
|
setValue("");
|
|
922
|
-
onAdded(toContact(entry));
|
|
917
|
+
onAdded(toContact(entry), attempt);
|
|
923
918
|
},
|
|
924
919
|
onError: (error) => {
|
|
925
920
|
setAddError(error.code === "INVALID_INPUT" ? "unresolved" : "failed");
|
|
926
|
-
onFailed(error);
|
|
921
|
+
onFailed(error, attempt);
|
|
927
922
|
},
|
|
928
923
|
onSettled: () => {
|
|
929
924
|
submitGate.current = false;
|
|
@@ -944,18 +939,21 @@ function useContacts(input) {
|
|
|
944
939
|
book,
|
|
945
940
|
scope,
|
|
946
941
|
refusal,
|
|
947
|
-
rename: useCallback((id, name) => writeMutate({
|
|
942
|
+
rename: useCallback((id, name, options) => writeMutate({
|
|
948
943
|
kind: "rename",
|
|
949
944
|
id,
|
|
950
|
-
name
|
|
945
|
+
name,
|
|
946
|
+
...carry$1(toInvocationOptions(options))
|
|
951
947
|
}), [writeMutate]),
|
|
952
|
-
hide: useCallback((id) => writeMutate({
|
|
948
|
+
hide: useCallback((id, options) => writeMutate({
|
|
953
949
|
kind: "hide",
|
|
954
|
-
id
|
|
950
|
+
id,
|
|
951
|
+
...carry$1(toInvocationOptions(options))
|
|
955
952
|
}), [writeMutate]),
|
|
956
|
-
unhide: useCallback((id) => writeMutate({
|
|
953
|
+
unhide: useCallback((id, options) => writeMutate({
|
|
957
954
|
kind: "unhide",
|
|
958
|
-
id
|
|
955
|
+
id,
|
|
956
|
+
...carry$1(toInvocationOptions(options))
|
|
959
957
|
}), [writeMutate]),
|
|
960
958
|
add: {
|
|
961
959
|
value,
|
|
@@ -973,10 +971,11 @@ function useContacts(input) {
|
|
|
973
971
|
* query key, so React Query issues ONE request for them.
|
|
974
972
|
*/
|
|
975
973
|
function useContactsList(engine, options) {
|
|
974
|
+
const identity = useCapxulIdentityOrNull();
|
|
976
975
|
const includeHidden = options.show === "all";
|
|
977
976
|
const { book, refusal } = engine;
|
|
978
977
|
const query = useQuery({
|
|
979
|
-
queryKey: capxulKeys.contactsList(engine.scope, includeHidden),
|
|
978
|
+
queryKey: accountKey(capxulKeys.contactsList(engine.scope, includeHidden), identity),
|
|
980
979
|
queryFn: async () => unwrapCapxulResult(await requireBook(book, CAPXUL_OPERATIONS.addressBook.list).list({ includeHidden })),
|
|
981
980
|
enabled: book !== null
|
|
982
981
|
});
|
|
@@ -989,9 +988,9 @@ function useContactsList(engine, options) {
|
|
|
989
988
|
if (entries === void 0) return [];
|
|
990
989
|
return (limit === void 0 ? entries : entries.slice(0, limit)).map((entry) => Object.assign(toContact(entry), {
|
|
991
990
|
initials: initialsOf(entry.label),
|
|
992
|
-
rename: (name) => rename(entry.id, name),
|
|
993
|
-
hide: () => hide(entry.id),
|
|
994
|
-
unhide: () => unhide(entry.id)
|
|
991
|
+
rename: (name, attempt) => rename(entry.id, name, attempt),
|
|
992
|
+
hide: (attempt) => hide(entry.id, attempt),
|
|
993
|
+
unhide: (attempt) => unhide(entry.id, attempt)
|
|
995
994
|
}));
|
|
996
995
|
}, [
|
|
997
996
|
entries,
|
|
@@ -1064,7 +1063,7 @@ function useContactsContext() {
|
|
|
1064
1063
|
if (engine === null) throw new Error("CapxulContacts parts must be used inside <CapxulContacts>");
|
|
1065
1064
|
return engine;
|
|
1066
1065
|
}
|
|
1067
|
-
function Root$
|
|
1066
|
+
function Root$7({ actor, onAdded, onFailed, children }) {
|
|
1068
1067
|
const engine = useContacts({
|
|
1069
1068
|
actor,
|
|
1070
1069
|
onAdded,
|
|
@@ -1088,7 +1087,7 @@ function Add({ children }) {
|
|
|
1088
1087
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useContactsContext().add) });
|
|
1089
1088
|
}
|
|
1090
1089
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
1091
|
-
const CapxulContacts = Object.assign(Root$
|
|
1090
|
+
const CapxulContacts = Object.assign(Root$7, {
|
|
1092
1091
|
Summary: Summary$3,
|
|
1093
1092
|
List,
|
|
1094
1093
|
Add
|
|
@@ -1126,15 +1125,8 @@ function useActivityEnabled() {
|
|
|
1126
1125
|
}
|
|
1127
1126
|
//#endregion
|
|
1128
1127
|
//#region src/headless/activity/detail.ts
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
* the payslip, else the receipt, else nothing. Those two kinds are the ones the
|
|
1132
|
-
* Download receipt button is for. A Movement detail carries no documents.
|
|
1133
|
-
*/
|
|
1134
|
-
function pickReceiptDocument(detail) {
|
|
1135
|
-
if (detail === null || detail.kind !== "payment") return null;
|
|
1136
|
-
const documents = detail.payment.documents;
|
|
1137
|
-
return documents.find((document) => document.kind === "payslip") ?? documents.find((document) => document.kind === "receipt") ?? null;
|
|
1128
|
+
function detailDocuments(detail) {
|
|
1129
|
+
return detail === null ? [] : detail.kind === "payment" ? detail.payment.documents : detail.documents;
|
|
1138
1130
|
}
|
|
1139
1131
|
/**
|
|
1140
1132
|
* The root actor and the whole reference are included. A payment reference can
|
|
@@ -1165,30 +1157,80 @@ function activityDetailKey(actor, reference) {
|
|
|
1165
1157
|
}
|
|
1166
1158
|
function useActivityDetail(reference, actor) {
|
|
1167
1159
|
const client = useCapxulClientOrNull();
|
|
1160
|
+
const identity = useCapxulIdentityOrNull();
|
|
1168
1161
|
const enabled = useActivityEnabled() && reference !== void 0;
|
|
1169
1162
|
const detail = useQuery({
|
|
1170
|
-
queryKey: capxulKeys.activityDetail(reference, actor),
|
|
1171
|
-
queryFn: async () => {
|
|
1163
|
+
queryKey: accountKey(capxulKeys.activityDetail(reference, actor), identity),
|
|
1164
|
+
queryFn: async ({ signal }) => {
|
|
1172
1165
|
if (reference === void 0) throw Errors.invalidInput("reference", "required for activity.get");
|
|
1173
|
-
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.get).activity.get(reference,
|
|
1166
|
+
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.get).activity.get(reference, {
|
|
1167
|
+
...actor === void 0 ? {} : { actor },
|
|
1168
|
+
signal
|
|
1169
|
+
}));
|
|
1174
1170
|
},
|
|
1175
1171
|
enabled
|
|
1176
1172
|
});
|
|
1177
|
-
const
|
|
1173
|
+
const documents = detailDocuments(detail.data ?? null);
|
|
1174
|
+
const [selected, setSelected] = useState(null);
|
|
1175
|
+
const selectedDocument = (selected === null ? void 0 : documents.find((doc) => doc.documentHash === selected.documentHash && doc.contentHash === selected.contentHash)) ?? documents.find((doc) => doc.role === "instruction") ?? documents.find((doc) => doc.role === "movement") ?? documents[0] ?? null;
|
|
1176
|
+
const pending = useRef(/* @__PURE__ */ new Set());
|
|
1177
|
+
const mounted = useRef(true);
|
|
1178
|
+
useEffect(() => {
|
|
1179
|
+
mounted.current = true;
|
|
1180
|
+
const reads = pending.current;
|
|
1181
|
+
return () => {
|
|
1182
|
+
mounted.current = false;
|
|
1183
|
+
for (const read of reads) read.abort();
|
|
1184
|
+
reads.clear();
|
|
1185
|
+
};
|
|
1186
|
+
}, []);
|
|
1187
|
+
const selectDocument = useCallback((ref) => {
|
|
1188
|
+
if (!documents.some((doc) => doc.documentHash === ref.documentHash && doc.contentHash === ref.contentHash)) throw Errors.invalidInput("documentHash", "document not found");
|
|
1189
|
+
for (const read of pending.current) read.abort();
|
|
1190
|
+
pending.current.clear();
|
|
1191
|
+
setIsOpeningDocument(false);
|
|
1192
|
+
setSelected({
|
|
1193
|
+
documentHash: ref.documentHash,
|
|
1194
|
+
contentHash: ref.contentHash
|
|
1195
|
+
});
|
|
1196
|
+
}, [documents]);
|
|
1178
1197
|
const [isOpeningDocument, setIsOpeningDocument] = useState(false);
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1198
|
+
const readDocument = useCallback(async (download) => {
|
|
1199
|
+
const operation = download ? CAPXUL_OPERATIONS.paymentDocuments.get : CAPXUL_OPERATIONS.paymentDocuments.render;
|
|
1200
|
+
if (!mounted.current) throw Errors.cancelled({ operation });
|
|
1201
|
+
if (reference === void 0 || selectedDocument === null) throw Errors.invalidInput("documentHash", "no document on this row");
|
|
1202
|
+
const controller = new AbortController();
|
|
1203
|
+
pending.current.add(controller);
|
|
1181
1204
|
setIsOpeningDocument(true);
|
|
1182
1205
|
try {
|
|
1183
|
-
|
|
1206
|
+
const bootstrapped = requireBootstrappedClient(client, download ? CAPXUL_OPERATIONS.paymentDocuments.get : CAPXUL_OPERATIONS.paymentDocuments.render);
|
|
1207
|
+
const pair = {
|
|
1208
|
+
documentHash: selectedDocument.documentHash,
|
|
1209
|
+
contentHash: selectedDocument.contentHash
|
|
1210
|
+
};
|
|
1211
|
+
const options = { signal: controller.signal };
|
|
1212
|
+
const result = download ? await bootstrapped.paymentDocuments.get(pair, options) : await bootstrapped.paymentDocuments.render(pair, options);
|
|
1213
|
+
if (controller.signal.aborted) throw Errors.cancelled({ operation });
|
|
1214
|
+
return unwrapCapxulResult(result);
|
|
1184
1215
|
} finally {
|
|
1185
|
-
|
|
1216
|
+
pending.current.delete(controller);
|
|
1217
|
+
if (!controller.signal.aborted) setIsOpeningDocument(pending.current.size > 0);
|
|
1186
1218
|
}
|
|
1187
1219
|
}, [
|
|
1188
1220
|
client,
|
|
1189
|
-
|
|
1221
|
+
selectedDocument,
|
|
1190
1222
|
reference
|
|
1191
1223
|
]);
|
|
1224
|
+
const renderDocument = useCallback(async () => {
|
|
1225
|
+
const result = await readDocument(false);
|
|
1226
|
+
if (!("html" in result)) throw Errors.invalidInput("documentHash", "invalid document render");
|
|
1227
|
+
return result;
|
|
1228
|
+
}, [readDocument]);
|
|
1229
|
+
const downloadDocument = useCallback(async () => {
|
|
1230
|
+
const result = await readDocument(true);
|
|
1231
|
+
if (!("bytesBase64" in result)) throw Errors.invalidInput("documentHash", "invalid document download");
|
|
1232
|
+
return result;
|
|
1233
|
+
}, [readDocument]);
|
|
1192
1234
|
const [typed, setTyped] = useState(null);
|
|
1193
1235
|
const annotate = useCapxulAnnotateMovement();
|
|
1194
1236
|
const saveMemo = useCallback(async () => {
|
|
@@ -1213,8 +1255,15 @@ function useActivityDetail(reference, actor) {
|
|
|
1213
1255
|
typed
|
|
1214
1256
|
]);
|
|
1215
1257
|
return {
|
|
1216
|
-
|
|
1217
|
-
|
|
1258
|
+
...detail.data?.kind === "payment" ? { payment: {
|
|
1259
|
+
status: detail.data.payment.status,
|
|
1260
|
+
...detail.data.payment.execution === void 0 ? {} : { execution: detail.data.payment.execution }
|
|
1261
|
+
} } : {},
|
|
1262
|
+
documents,
|
|
1263
|
+
selectedDocument,
|
|
1264
|
+
selectDocument,
|
|
1265
|
+
renderDocument,
|
|
1266
|
+
downloadDocument,
|
|
1218
1267
|
isOpeningDocument,
|
|
1219
1268
|
memo: typed ?? detail.data?.annotation?.memo ?? "",
|
|
1220
1269
|
setMemo: setTyped,
|
|
@@ -1227,6 +1276,7 @@ function useActivityDetail(reference, actor) {
|
|
|
1227
1276
|
//#endregion
|
|
1228
1277
|
//#region src/headless/activity/summary.ts
|
|
1229
1278
|
const DAY_MS = 864e5;
|
|
1279
|
+
const display = (money) => money === null ? null : formatMoney(money);
|
|
1230
1280
|
/**
|
|
1231
1281
|
* `"last30days"` is the last 30 whole UTC days including today. Quantizing to
|
|
1232
1282
|
* the UTC day keeps the cache key stable for a whole day instead of changing on
|
|
@@ -1237,57 +1287,6 @@ function resolveWindow(window, now) {
|
|
|
1237
1287
|
if (window === "last30days") return { from: Math.floor(now / DAY_MS) * DAY_MS - 29 * DAY_MS };
|
|
1238
1288
|
return window;
|
|
1239
1289
|
}
|
|
1240
|
-
/** An integer raw-unit string lifted into a major-unit decimal string. */
|
|
1241
|
-
function majorUnits(raw, decimals) {
|
|
1242
|
-
const negative = raw.startsWith("-");
|
|
1243
|
-
const digits = negative ? raw.slice(1) : raw;
|
|
1244
|
-
const sign = negative ? "-" : "";
|
|
1245
|
-
if (decimals === 0) return `${sign}${digits}`;
|
|
1246
|
-
const padded = digits.padStart(decimals + 1, "0");
|
|
1247
|
-
const cut = padded.length - decimals;
|
|
1248
|
-
return `${sign}${padded.slice(0, cut)}.${padded.slice(cut)}`;
|
|
1249
|
-
}
|
|
1250
|
-
function absoluteNet(total) {
|
|
1251
|
-
const net = BigInt(total.net);
|
|
1252
|
-
return net < 0n ? -net : net;
|
|
1253
|
-
}
|
|
1254
|
-
/**
|
|
1255
|
-
* Buckets carry different `decimals`, so their raw integers are not comparable.
|
|
1256
|
-
* Both are scaled to the wider precision before the magnitudes are ordered.
|
|
1257
|
-
*/
|
|
1258
|
-
function compareNet(left, right) {
|
|
1259
|
-
const width = Math.max(left.decimals, right.decimals);
|
|
1260
|
-
const scaledLeft = absoluteNet(left) * 10n ** BigInt(width - left.decimals);
|
|
1261
|
-
const scaledRight = absoluteNet(right) * 10n ** BigInt(width - right.decimals);
|
|
1262
|
-
if (scaledLeft === scaledRight) return 0;
|
|
1263
|
-
return scaledLeft < scaledRight ? -1 : 1;
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Which currency `.Summary` shows: the bucket matching the primary holding's
|
|
1267
|
-
* currency, else the bucket with the largest absolute `net`, breaking a tie on
|
|
1268
|
-
* currency code ascending so the choice is stable across reloads.
|
|
1269
|
-
*
|
|
1270
|
-
* The primary holding is matched on `available.currency` — the consumer
|
|
1271
|
-
* `CurrencyCode` — and NOT on `Holding.symbol`, which is the raw token ticker
|
|
1272
|
-
* ("USDX") and can never equal a bucket's currency ("USD"). See the contract
|
|
1273
|
-
* correction on #1449.
|
|
1274
|
-
*/
|
|
1275
|
-
function pickBucket(totals, primaryCurrency) {
|
|
1276
|
-
if (primaryCurrency !== null) {
|
|
1277
|
-
const matched = totals.find((total) => total.currency === primaryCurrency);
|
|
1278
|
-
if (matched !== void 0) return matched;
|
|
1279
|
-
}
|
|
1280
|
-
let best = null;
|
|
1281
|
-
for (const total of totals) {
|
|
1282
|
-
if (best === null) {
|
|
1283
|
-
best = total;
|
|
1284
|
-
continue;
|
|
1285
|
-
}
|
|
1286
|
-
const order = compareNet(total, best);
|
|
1287
|
-
if (order > 0 || order === 0 && total.currency < best.currency) best = total;
|
|
1288
|
-
}
|
|
1289
|
-
return best;
|
|
1290
|
-
}
|
|
1291
1290
|
/**
|
|
1292
1291
|
* The current UTC day number, which advances ON ITS OWN at UTC midnight.
|
|
1293
1292
|
*
|
|
@@ -1322,57 +1321,49 @@ function useUtcDay(active) {
|
|
|
1322
1321
|
}
|
|
1323
1322
|
function useActivitySummary(actor, window) {
|
|
1324
1323
|
const client = useCapxulClientOrNull();
|
|
1324
|
+
const identity = useCapxulIdentityOrNull();
|
|
1325
1325
|
const enabled = useActivityEnabled();
|
|
1326
1326
|
const range = resolveWindow(window, useUtcDay(window === "last30days") * DAY_MS);
|
|
1327
1327
|
const summary = useQuery({
|
|
1328
|
-
queryKey: capxulKeys.activitySummary(actor, range),
|
|
1328
|
+
queryKey: accountKey(capxulKeys.activitySummary(actor, range), identity),
|
|
1329
1329
|
queryFn: async () => {
|
|
1330
1330
|
const params = {};
|
|
1331
1331
|
if (actor !== void 0) params.actor = actor;
|
|
1332
1332
|
if (range !== void 0) params.window = range;
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
queryKey: [...capxulKeys.holdings(actor), "primary"],
|
|
1339
|
-
queryFn: async () => {
|
|
1340
|
-
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.holdings.primary).holdings.primary(actor === void 0 ? void 0 : { actor }));
|
|
1333
|
+
const result = unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.summary).activity.summary(params));
|
|
1334
|
+
return {
|
|
1335
|
+
...result,
|
|
1336
|
+
valuation: valueActivitySummary(result.totals)
|
|
1337
|
+
};
|
|
1341
1338
|
},
|
|
1342
1339
|
enabled
|
|
1343
1340
|
});
|
|
1344
1341
|
const totals = summary.data?.totals;
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
const bucket = useMemo(() => totals === void 0 || isLoading ? null : pickBucket(totals, primaryCurrency), [
|
|
1348
|
-
isLoading,
|
|
1349
|
-
primaryCurrency,
|
|
1350
|
-
totals
|
|
1351
|
-
]);
|
|
1352
|
-
if (bucket === null) return {
|
|
1342
|
+
const isLoading = summary.isLoading;
|
|
1343
|
+
if (summary.data === void 0 || totals === void 0 || totals.length === 0 || isLoading) return {
|
|
1353
1344
|
currency: null,
|
|
1354
1345
|
moneyIn: null,
|
|
1355
1346
|
moneyOut: null,
|
|
1356
1347
|
net: null,
|
|
1357
1348
|
pending: null,
|
|
1349
|
+
unrated: [],
|
|
1358
1350
|
count: summary.data?.count ?? 0,
|
|
1359
1351
|
isLoading,
|
|
1360
|
-
error: asCapxulError$1(summary.error)
|
|
1352
|
+
error: asCapxulError$1(summary.error),
|
|
1353
|
+
...summary.data?.readWarning === void 0 ? {} : { readWarning: summary.data.readWarning }
|
|
1361
1354
|
};
|
|
1362
|
-
const
|
|
1363
|
-
currency: bucket.currency,
|
|
1364
|
-
value: majorUnits(raw, bucket.decimals),
|
|
1365
|
-
decimals: bucket.decimals
|
|
1366
|
-
});
|
|
1355
|
+
const valuation = summary.data.valuation;
|
|
1367
1356
|
return {
|
|
1368
|
-
currency:
|
|
1369
|
-
moneyIn: display(
|
|
1370
|
-
moneyOut: display(
|
|
1371
|
-
net: display(
|
|
1372
|
-
pending: display(
|
|
1357
|
+
currency: valuation.moneyIn?.currency ?? null,
|
|
1358
|
+
moneyIn: display(valuation.moneyIn),
|
|
1359
|
+
moneyOut: display(valuation.moneyOut),
|
|
1360
|
+
net: display(valuation.net),
|
|
1361
|
+
pending: display(valuation.pending),
|
|
1362
|
+
unrated: valuation.unrated,
|
|
1373
1363
|
count: summary.data?.count ?? 0,
|
|
1374
1364
|
isLoading,
|
|
1375
|
-
error: asCapxulError$1(summary.error)
|
|
1365
|
+
error: asCapxulError$1(summary.error),
|
|
1366
|
+
...summary.data?.readWarning === void 0 ? {} : { readWarning: summary.data.readWarning }
|
|
1376
1367
|
};
|
|
1377
1368
|
}
|
|
1378
1369
|
//#endregion
|
|
@@ -1395,7 +1386,7 @@ const SIGN = Object.freeze({
|
|
|
1395
1386
|
self: ""
|
|
1396
1387
|
});
|
|
1397
1388
|
function toActivityRow(item, actor) {
|
|
1398
|
-
const amountDisplay =
|
|
1389
|
+
const amountDisplay = formatAssetAmount(item.amount);
|
|
1399
1390
|
const signedAmountDisplay = `${SIGN[item.direction]}${amountDisplay}`;
|
|
1400
1391
|
if (item.kind === "movement") return {
|
|
1401
1392
|
kind: "movement",
|
|
@@ -1407,32 +1398,37 @@ function toActivityRow(item, actor) {
|
|
|
1407
1398
|
counterpartyLabel: item.counterpartyLabel,
|
|
1408
1399
|
amountDisplay,
|
|
1409
1400
|
signedAmountDisplay,
|
|
1410
|
-
currency: item.amount.
|
|
1401
|
+
currency: assetSymbolFor(item.amount.asset),
|
|
1411
1402
|
createdAt: item.createdAt,
|
|
1412
1403
|
classification: item.classification,
|
|
1413
1404
|
phase: MOVEMENT_PHASE,
|
|
1414
1405
|
receipt: item.receipt
|
|
1415
1406
|
};
|
|
1407
|
+
const reference = actor === void 0 ? {
|
|
1408
|
+
kind: "payment",
|
|
1409
|
+
id: item.id
|
|
1410
|
+
} : {
|
|
1411
|
+
kind: "payment",
|
|
1412
|
+
id: item.id,
|
|
1413
|
+
actor
|
|
1414
|
+
};
|
|
1416
1415
|
return {
|
|
1417
1416
|
kind: "payment",
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
id: item.id
|
|
1421
|
-
} : {
|
|
1422
|
-
kind: "payment",
|
|
1423
|
-
id: item.id,
|
|
1424
|
-
actor
|
|
1425
|
-
},
|
|
1417
|
+
...item.execution === void 0 ? {} : { execution: item.execution },
|
|
1418
|
+
reference,
|
|
1426
1419
|
direction: item.direction,
|
|
1427
1420
|
counterpartyLabel: item.counterpartyLabel,
|
|
1428
1421
|
amountDisplay,
|
|
1429
1422
|
signedAmountDisplay,
|
|
1430
|
-
currency: item.amount.
|
|
1423
|
+
currency: assetSymbolFor(item.amount.asset),
|
|
1431
1424
|
createdAt: item.createdAt,
|
|
1432
1425
|
ledgerKind: item.ledgerKind,
|
|
1433
1426
|
paymentType: item.paymentType,
|
|
1434
1427
|
status: item.status,
|
|
1435
|
-
phase:
|
|
1428
|
+
phase: item.execution?.notSent ? {
|
|
1429
|
+
...paymentPhase(item.status),
|
|
1430
|
+
needsAction: item.direction !== "in"
|
|
1431
|
+
} : paymentPhase(item.status),
|
|
1436
1432
|
receipt: item.receipt
|
|
1437
1433
|
};
|
|
1438
1434
|
}
|
|
@@ -1505,8 +1501,8 @@ function buildCsv(items, actor, search) {
|
|
|
1505
1501
|
row.counterpartyLabel,
|
|
1506
1502
|
row.direction,
|
|
1507
1503
|
item.amount.value,
|
|
1508
|
-
item.amount.
|
|
1509
|
-
row.kind === "payment" ? row.status : "",
|
|
1504
|
+
assetSymbolFor(item.amount.asset),
|
|
1505
|
+
row.kind === "payment" ? row.execution?.notSent ? "not_sent" : row.execution?.status === "unresolved" ? "verifying" : row.status : "",
|
|
1510
1506
|
row.reference.id,
|
|
1511
1507
|
row.receipt?.transactionHash ?? ""
|
|
1512
1508
|
]));
|
|
@@ -1538,6 +1534,8 @@ function clampPageSize(pageSize) {
|
|
|
1538
1534
|
function useActivity(input) {
|
|
1539
1535
|
const { actor, pageSize } = input;
|
|
1540
1536
|
const client = useCapxulClientOrNull();
|
|
1537
|
+
const identity = useCapxulIdentityOrNull();
|
|
1538
|
+
const account = actingAccount(identity);
|
|
1541
1539
|
const enabled = useActivityEnabled();
|
|
1542
1540
|
const limit = clampPageSize(pageSize);
|
|
1543
1541
|
const warned = useRef(false);
|
|
@@ -1567,21 +1565,51 @@ function useActivity(input) {
|
|
|
1567
1565
|
if (filter !== void 0) params.filter = filter;
|
|
1568
1566
|
return params;
|
|
1569
1567
|
}, [actor, filter]);
|
|
1568
|
+
const selectedRetry = useRef(void 0);
|
|
1569
|
+
const takeSelectedRetry = useCallback(() => {
|
|
1570
|
+
const attempt = selectedRetry.current;
|
|
1571
|
+
selectedRetry.current = void 0;
|
|
1572
|
+
return toInvocationControls(attempt);
|
|
1573
|
+
}, []);
|
|
1570
1574
|
const query = useInfiniteQuery({
|
|
1571
|
-
queryKey: [
|
|
1575
|
+
queryKey: accountKey([
|
|
1572
1576
|
...capxulKeys.activity,
|
|
1573
1577
|
"list",
|
|
1574
1578
|
actor ?? "self",
|
|
1575
1579
|
limit,
|
|
1576
1580
|
filter ?? "none"
|
|
1577
|
-
],
|
|
1581
|
+
], identity),
|
|
1578
1582
|
queryFn: async ({ pageParam }) => {
|
|
1579
|
-
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.list).activity.list(listParams(pageParam, limit)));
|
|
1583
|
+
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.list).activity.list(listParams(pageParam, limit), takeSelectedRetry()));
|
|
1580
1584
|
},
|
|
1581
1585
|
initialPageParam: void 0,
|
|
1582
1586
|
getNextPageParam: (last) => last.cursor ?? void 0,
|
|
1583
1587
|
enabled
|
|
1584
1588
|
});
|
|
1589
|
+
const queryClient = useQueryClient();
|
|
1590
|
+
useEffect(() => {
|
|
1591
|
+
if (!enabled || client === null) return;
|
|
1592
|
+
let stop;
|
|
1593
|
+
let cancelled = false;
|
|
1594
|
+
client.activity.subscribe(() => {
|
|
1595
|
+
if (cancelled) return;
|
|
1596
|
+
queryClient.invalidateQueries({ queryKey: capxulKeys.activity });
|
|
1597
|
+
}, actor === void 0 ? void 0 : { actor }).then((result) => {
|
|
1598
|
+
if (!result.ok) return;
|
|
1599
|
+
if (cancelled) result.value();
|
|
1600
|
+
else stop = result.value;
|
|
1601
|
+
});
|
|
1602
|
+
return () => {
|
|
1603
|
+
cancelled = true;
|
|
1604
|
+
stop?.();
|
|
1605
|
+
};
|
|
1606
|
+
}, [
|
|
1607
|
+
account,
|
|
1608
|
+
actor,
|
|
1609
|
+
client,
|
|
1610
|
+
enabled,
|
|
1611
|
+
queryClient
|
|
1612
|
+
]);
|
|
1585
1613
|
const pages = query.data?.pages;
|
|
1586
1614
|
const rows = useMemo(() => {
|
|
1587
1615
|
return (pages?.flatMap((page) => page.items) ?? []).map((item) => toActivityRow(item, actor)).filter((row) => matchesSearch(row, searchText));
|
|
@@ -1591,6 +1619,16 @@ function useActivity(input) {
|
|
|
1591
1619
|
searchText
|
|
1592
1620
|
]);
|
|
1593
1621
|
const { fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = query;
|
|
1622
|
+
const emptyContinuation = pages?.at(-1)?.items.length === 0 && hasNextPage && query.error === null;
|
|
1623
|
+
useEffect(() => {
|
|
1624
|
+
if (enabled && emptyContinuation && !isFetchingNextPage) fetchNextPage();
|
|
1625
|
+
}, [
|
|
1626
|
+
enabled,
|
|
1627
|
+
emptyContinuation,
|
|
1628
|
+
fetchNextPage,
|
|
1629
|
+
isFetchingNextPage,
|
|
1630
|
+
pages
|
|
1631
|
+
]);
|
|
1594
1632
|
const pagingRef = useRef({
|
|
1595
1633
|
fetchNextPage,
|
|
1596
1634
|
hasNextPage,
|
|
@@ -1624,21 +1662,25 @@ function useActivity(input) {
|
|
|
1624
1662
|
observerRef.current = observer;
|
|
1625
1663
|
}, [loadMore]);
|
|
1626
1664
|
const [isExporting, setIsExporting] = useState(false);
|
|
1627
|
-
const toCsv = useCallback(async () => {
|
|
1665
|
+
const toCsv = useCallback(async (options) => {
|
|
1628
1666
|
setIsExporting(true);
|
|
1667
|
+
const controls = toInvocationControls(toInvocationOptions(options));
|
|
1629
1668
|
try {
|
|
1630
1669
|
const bootstrapped = requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.list);
|
|
1631
1670
|
const items = [];
|
|
1671
|
+
let readWarning;
|
|
1632
1672
|
let cursor;
|
|
1633
1673
|
for (let page = 0; page < EXPORT_PAGE_CEILING; page += 1) {
|
|
1634
|
-
const result = unwrapCapxulResult(await bootstrapped.activity.list(listParams(cursor, EXPORT_PAGE_SIZE)));
|
|
1674
|
+
const result = unwrapCapxulResult(await bootstrapped.activity.list(listParams(cursor, EXPORT_PAGE_SIZE), controls));
|
|
1675
|
+
readWarning ??= result.readWarning;
|
|
1635
1676
|
items.push(...result.items);
|
|
1636
1677
|
cursor = result.cursor ?? void 0;
|
|
1637
1678
|
if (cursor === void 0) break;
|
|
1638
1679
|
}
|
|
1639
1680
|
return {
|
|
1640
1681
|
...buildCsv(items, actor, searchText),
|
|
1641
|
-
truncated: cursor !== void 0
|
|
1682
|
+
truncated: cursor !== void 0,
|
|
1683
|
+
...readWarning === void 0 ? {} : { readWarning }
|
|
1642
1684
|
};
|
|
1643
1685
|
} finally {
|
|
1644
1686
|
setIsExporting(false);
|
|
@@ -1654,9 +1696,19 @@ function useActivity(input) {
|
|
|
1654
1696
|
setDirection(void 0);
|
|
1655
1697
|
setStatus([]);
|
|
1656
1698
|
}, []);
|
|
1657
|
-
const retry = useCallback(() => {
|
|
1699
|
+
const retry = useCallback((options) => {
|
|
1700
|
+
selectedRetry.current = toInvocationOptions(options);
|
|
1658
1701
|
refetch();
|
|
1659
|
-
|
|
1702
|
+
queryClient.invalidateQueries({
|
|
1703
|
+
queryKey: capxulKeys.activitySummary(actor).slice(0, -1),
|
|
1704
|
+
predicate: (cached) => cached.queryKey.at(-1) === accountKey([], identity)[0]
|
|
1705
|
+
});
|
|
1706
|
+
}, [
|
|
1707
|
+
actor,
|
|
1708
|
+
identity,
|
|
1709
|
+
queryClient,
|
|
1710
|
+
refetch
|
|
1711
|
+
]);
|
|
1660
1712
|
return {
|
|
1661
1713
|
actor,
|
|
1662
1714
|
filters: {
|
|
@@ -1679,10 +1731,11 @@ function useActivity(input) {
|
|
|
1679
1731
|
},
|
|
1680
1732
|
rows: {
|
|
1681
1733
|
rows,
|
|
1682
|
-
isLoading: query.isLoading,
|
|
1734
|
+
isLoading: query.isLoading || rows.length === 0 && emptyContinuation,
|
|
1683
1735
|
error: asCapxulError$1(query.error),
|
|
1684
1736
|
retry,
|
|
1685
|
-
observedAt: pages === void 0 ? null : pages[pages.length - 1]?.observedAt ?? null
|
|
1737
|
+
observedAt: pages === void 0 ? null : pages[pages.length - 1]?.observedAt ?? null,
|
|
1738
|
+
...pages?.some((page) => page.readWarning === "indexer-unavailable") ? { readWarning: "indexer-unavailable" } : {}
|
|
1686
1739
|
},
|
|
1687
1740
|
loadMore: {
|
|
1688
1741
|
hasMore: hasNextPage,
|
|
@@ -1704,7 +1757,7 @@ function useActivityContext() {
|
|
|
1704
1757
|
if (engine === null) throw new Error("CapxulActivity parts must be used inside <CapxulActivity>");
|
|
1705
1758
|
return engine;
|
|
1706
1759
|
}
|
|
1707
|
-
function Root$
|
|
1760
|
+
function Root$6({ actor, pageSize, children }) {
|
|
1708
1761
|
const engine = useActivity({
|
|
1709
1762
|
...actor === void 0 ? {} : { actor },
|
|
1710
1763
|
...pageSize === void 0 ? {} : { pageSize }
|
|
@@ -1744,17 +1797,18 @@ function DetailInstance({ actor, reference, children }) {
|
|
|
1744
1797
|
}
|
|
1745
1798
|
function Detail({ reference, children }) {
|
|
1746
1799
|
const { actor } = useActivityContext();
|
|
1800
|
+
const identity = useCapxulIdentityOrNull();
|
|
1747
1801
|
return /* @__PURE__ */ jsx(DetailInstance, {
|
|
1748
1802
|
actor,
|
|
1749
1803
|
reference,
|
|
1750
1804
|
children
|
|
1751
|
-
}, activityDetailKey(actor, reference));
|
|
1805
|
+
}, JSON.stringify(accountKey([activityDetailKey(actor, reference)], identity)));
|
|
1752
1806
|
}
|
|
1753
1807
|
function Export({ children }) {
|
|
1754
1808
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useActivityContext().csv) });
|
|
1755
1809
|
}
|
|
1756
1810
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
1757
|
-
const CapxulActivity = Object.assign(Root$
|
|
1811
|
+
const CapxulActivity = Object.assign(Root$6, {
|
|
1758
1812
|
Summary: Summary$2,
|
|
1759
1813
|
Filters,
|
|
1760
1814
|
Search,
|
|
@@ -1771,7 +1825,7 @@ function useOrgMeReading(orgId) {
|
|
|
1771
1825
|
const ready = client !== null && identity !== null && isClaimed$1(identity);
|
|
1772
1826
|
const mayStillOpen = client === null || identity === null || isRestoring$1(identity);
|
|
1773
1827
|
const query = useQuery({
|
|
1774
|
-
queryKey: capxulKeys.orgMe(orgId),
|
|
1828
|
+
queryKey: capxulKeys.orgMe(orgId, actingAccount(identity)),
|
|
1775
1829
|
queryFn: async () => {
|
|
1776
1830
|
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.org.me).org(orgId).me());
|
|
1777
1831
|
},
|
|
@@ -1795,6 +1849,26 @@ function useOrgMeReading(orgId) {
|
|
|
1795
1849
|
]);
|
|
1796
1850
|
}
|
|
1797
1851
|
//#endregion
|
|
1852
|
+
//#region src/internal/write-names-asset.ts
|
|
1853
|
+
/**
|
|
1854
|
+
* Identity law: "A payment names an asset or inherits it from an authorized
|
|
1855
|
+
* fact. Missing asset refuses." The rule was recorded in the retired
|
|
1856
|
+
* treasury-cut handoff (removed from the current tree by the exact-asset
|
|
1857
|
+
* contraction; its record survives in Git history).
|
|
1858
|
+
*
|
|
1859
|
+
* A Capxul payment writer takes `PaymentMoney` — a peg currency, a value and
|
|
1860
|
+
* `decimals` — and settles one fixed legacy token. No `AssetId` crosses that
|
|
1861
|
+
* seam, so a payment composed from a named position cannot name the asset it
|
|
1862
|
+
* moves: a position selected as USDT authorizes a different token.
|
|
1863
|
+
*
|
|
1864
|
+
* `false` until the writer takes the command's asset. The Send and Payroll
|
|
1865
|
+
* composers read this to refuse a submission rather than authorize a token the
|
|
1866
|
+
* person did not pick.
|
|
1867
|
+
*/
|
|
1868
|
+
function writeNamesAsset() {
|
|
1869
|
+
return false;
|
|
1870
|
+
}
|
|
1871
|
+
//#endregion
|
|
1798
1872
|
//#region src/headless/send-money/signer-status.ts
|
|
1799
1873
|
const NO_SUBSCRIPTION = () => () => {};
|
|
1800
1874
|
function useSignerStatus(client) {
|
|
@@ -1944,6 +2018,37 @@ function useSelectedOption(options) {
|
|
|
1944
2018
|
}, [options]);
|
|
1945
2019
|
return [options.find((option) => option.id === selectedId) ?? null, select];
|
|
1946
2020
|
}
|
|
2021
|
+
/**
|
|
2022
|
+
* One Send `.Asset` option per position the Account holds, taken from
|
|
2023
|
+
* `account.balances` — never from `account.available`, which is the Account's
|
|
2024
|
+
* fiat VALUATION of those positions (ADR-0028 R5/R6). Labelling the valuation
|
|
2025
|
+
* was the false identity this cut removes, and it collapsed USDC+USDT into one
|
|
2026
|
+
* "USD" row.
|
|
2027
|
+
*
|
|
2028
|
+
* The option is keyed on the ASSET id, never the symbol and never the account:
|
|
2029
|
+
* several positions share one account id, and a symbol is display only
|
|
2030
|
+
* (ADR-0028 R3). Every row is listed, so a real multi-position Account still
|
|
2031
|
+
* offers its holdings.
|
|
2032
|
+
*/
|
|
2033
|
+
function assetSelectionsFromAccount$1(account) {
|
|
2034
|
+
if (account === null) return [];
|
|
2035
|
+
return account.balances.map((position) => {
|
|
2036
|
+
return {
|
|
2037
|
+
option: {
|
|
2038
|
+
id: position.asset,
|
|
2039
|
+
symbol: position.symbol,
|
|
2040
|
+
availableDisplay: formatAssetAmount({
|
|
2041
|
+
asset: position.asset,
|
|
2042
|
+
value: position.available
|
|
2043
|
+
})
|
|
2044
|
+
},
|
|
2045
|
+
money: {
|
|
2046
|
+
asset: position.asset,
|
|
2047
|
+
value: position.available
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
1947
2052
|
function useSendMoney(input) {
|
|
1948
2053
|
const { actor, onSent, onFailed } = input;
|
|
1949
2054
|
const client = useCapxulClientOrNull();
|
|
@@ -1956,15 +2061,12 @@ function useSendMoney(input) {
|
|
|
1956
2061
|
const treasury = useCapxulOrgTreasury(orgId, { enabled: actor.kind === "organization" });
|
|
1957
2062
|
const accountQuery = actor.kind === "personal" ? balance : treasury;
|
|
1958
2063
|
const account = accountQuery.data ?? null;
|
|
1959
|
-
const
|
|
1960
|
-
|
|
1961
|
-
symbol: account.available.currency,
|
|
1962
|
-
availableDisplay: formatMoney(account.available, { grammar: "code" })
|
|
1963
|
-
}], [account]);
|
|
2064
|
+
const assetSelections = useMemo(() => assetSelectionsFromAccount$1(account), [account]);
|
|
2065
|
+
const assetOptions = useMemo(() => assetSelections.map((selection) => selection.option), [assetSelections]);
|
|
1964
2066
|
const [selectedAssetOption, selectAsset] = useSelectedOption(assetOptions);
|
|
1965
|
-
const selectedAssetMoney =
|
|
2067
|
+
const selectedAssetMoney = useMemo(() => assetSelections.find((selection) => selection.option.id === selectedAssetOption?.id)?.money ?? null, [assetSelections, selectedAssetOption]);
|
|
1966
2068
|
const [amountText, setAmountText] = useState("");
|
|
1967
|
-
const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null :
|
|
2069
|
+
const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null : parseAssetAmount(amountText, requireAsset(selectedAssetMoney.asset)), [amountText, selectedAssetMoney]);
|
|
1968
2070
|
const parsedMoney = parsed !== null && !isMoneyParseError(parsed) ? parsed : null;
|
|
1969
2071
|
const amountError = parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null;
|
|
1970
2072
|
const recipient = useRecipientResolution(client, JSON.stringify([
|
|
@@ -1976,7 +2078,7 @@ function useSendMoney(input) {
|
|
|
1976
2078
|
const personalPay = useCapxulPay();
|
|
1977
2079
|
const organizationPay = useCapxulOrganizationPay(orgId);
|
|
1978
2080
|
const pay = actor.kind === "personal" ? personalPay : organizationPay;
|
|
1979
|
-
const blockedReason = signerStatus !== "ready" ? "signer-not-ready" : actor.kind === "organization" && selectedSource === null ? "no-source" : selectedAssetOption === null ? "no-asset" : parsedMoney === null ? "amount-invalid" : resolvedReference === null ? "recipient-unresolved" : null;
|
|
2081
|
+
const blockedReason = signerStatus !== "ready" ? "signer-not-ready" : actor.kind === "organization" && selectedSource === null ? "no-source" : selectedAssetOption === null ? "no-asset" : parsedMoney === null ? "amount-invalid" : resolvedReference === null ? "recipient-unresolved" : writeNamesAsset() ? null : "asset-unnamed";
|
|
1980
2082
|
const submitGate = useRef(false);
|
|
1981
2083
|
const submit = useCallback(() => {
|
|
1982
2084
|
if (blockedReason !== null || submitGate.current) return;
|
|
@@ -2063,7 +2165,7 @@ function useSendMoneyContext() {
|
|
|
2063
2165
|
if (engine === null) throw new Error("CapxulSendMoney parts must be used inside <CapxulSendMoney>");
|
|
2064
2166
|
return engine;
|
|
2065
2167
|
}
|
|
2066
|
-
function Root$
|
|
2168
|
+
function Root$5({ actor, onSent, onFailed, children }) {
|
|
2067
2169
|
const engine = useSendMoney({
|
|
2068
2170
|
actor,
|
|
2069
2171
|
onSent,
|
|
@@ -2090,7 +2192,7 @@ function Actions$1({ children }) {
|
|
|
2090
2192
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useSendMoneyContext().actions) });
|
|
2091
2193
|
}
|
|
2092
2194
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
2093
|
-
const CapxulSendMoney = Object.assign(Root$
|
|
2195
|
+
const CapxulSendMoney = Object.assign(Root$5, {
|
|
2094
2196
|
Source,
|
|
2095
2197
|
Asset: Asset$1,
|
|
2096
2198
|
Amount,
|
|
@@ -2180,7 +2282,7 @@ function useOrgMemberContext() {
|
|
|
2180
2282
|
if (state === null) throw new Error("CapxulOrgMember parts must be used inside <CapxulOrgMember>");
|
|
2181
2283
|
return state;
|
|
2182
2284
|
}
|
|
2183
|
-
function Root$
|
|
2285
|
+
function Root$4({ orgId, children }) {
|
|
2184
2286
|
const state = useOrgMember(orgId);
|
|
2185
2287
|
return /* @__PURE__ */ jsx(OrgMemberContext.Provider, {
|
|
2186
2288
|
value: state,
|
|
@@ -2200,7 +2302,7 @@ function Standing({ children }) {
|
|
|
2200
2302
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useOrgMemberContext().standing) });
|
|
2201
2303
|
}
|
|
2202
2304
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
2203
|
-
const CapxulOrgMember = Object.assign(Root$
|
|
2305
|
+
const CapxulOrgMember = Object.assign(Root$4, {
|
|
2204
2306
|
Can,
|
|
2205
2307
|
Standing
|
|
2206
2308
|
});
|
|
@@ -2275,6 +2377,16 @@ function decidePhase(input) {
|
|
|
2275
2377
|
if (input.orgLaneSettled && input.destination !== null) return "redirect";
|
|
2276
2378
|
return "checking";
|
|
2277
2379
|
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Which probe produced the surfaced error — the SAME precedence `error` itself
|
|
2382
|
+
* is chosen by, written once so the code and its source can never disagree.
|
|
2383
|
+
*/
|
|
2384
|
+
function probeOf(session, membership, lifecycle) {
|
|
2385
|
+
if (session !== null) return "session";
|
|
2386
|
+
if (membership !== null) return "membership";
|
|
2387
|
+
if (lifecycle !== null) return "lifecycle";
|
|
2388
|
+
return null;
|
|
2389
|
+
}
|
|
2278
2390
|
/** The engine — all the logic. NOT exported from the package barrel. */
|
|
2279
2391
|
function useDashboardAccess(scope) {
|
|
2280
2392
|
const client = useCapxulClientOrNull();
|
|
@@ -2311,6 +2423,7 @@ function useDashboardAccess(scope) {
|
|
|
2311
2423
|
lifecycle.refetch
|
|
2312
2424
|
]);
|
|
2313
2425
|
const error = sessionError ?? membershipError ?? lifecycleError;
|
|
2426
|
+
const probe = probeOf(sessionError, membershipError, lifecycleError);
|
|
2314
2427
|
const phase = decidePhase({
|
|
2315
2428
|
error,
|
|
2316
2429
|
settled: client !== null && identity !== null && !isRestoring$1(identity) && membershipsSettled && lifecycleSettled,
|
|
@@ -2321,11 +2434,13 @@ function useDashboardAccess(scope) {
|
|
|
2321
2434
|
return useMemo(() => ({
|
|
2322
2435
|
phase,
|
|
2323
2436
|
error,
|
|
2437
|
+
probe,
|
|
2324
2438
|
destination,
|
|
2325
2439
|
retry
|
|
2326
2440
|
}), [
|
|
2327
2441
|
phase,
|
|
2328
2442
|
error,
|
|
2443
|
+
probe,
|
|
2329
2444
|
destination,
|
|
2330
2445
|
retry
|
|
2331
2446
|
]);
|
|
@@ -2347,7 +2462,7 @@ function usePart(phase) {
|
|
|
2347
2462
|
}, [placed, phase]);
|
|
2348
2463
|
return state.phase === phase ? state : null;
|
|
2349
2464
|
}
|
|
2350
|
-
function Root$
|
|
2465
|
+
function Root$3({ scope, children }) {
|
|
2351
2466
|
const state = useDashboardAccess(scope);
|
|
2352
2467
|
const [placed] = useState(() => /* @__PURE__ */ new Set());
|
|
2353
2468
|
const value = useMemo(() => ({
|
|
@@ -2365,17 +2480,24 @@ function Root$2({ scope, children }) {
|
|
|
2365
2480
|
});
|
|
2366
2481
|
}
|
|
2367
2482
|
/** Hands nothing — today's app renders `null` here, exactly as it does now. */
|
|
2368
|
-
function Checking({ children }) {
|
|
2483
|
+
function Checking$1({ children }) {
|
|
2369
2484
|
return usePart("checking") === null ? null : /* @__PURE__ */ jsx(Fragment, { children });
|
|
2370
2485
|
}
|
|
2371
2486
|
function DashboardError({ children }) {
|
|
2372
2487
|
const state = usePart("error");
|
|
2373
|
-
if (state === null || state.error === null) return null;
|
|
2488
|
+
if (state === null || state.error === null || state.probe === null) return null;
|
|
2374
2489
|
return /* @__PURE__ */ jsx(Fragment, { children: children({
|
|
2375
2490
|
error: state.error,
|
|
2491
|
+
probe: state.probe,
|
|
2376
2492
|
retry: state.retry
|
|
2377
2493
|
}) });
|
|
2378
2494
|
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Hands the destination and draws nothing. Reaching this part is a REQUEST to
|
|
2497
|
+
* navigate, never an arrival: the app may cancel or supersede it, so nothing
|
|
2498
|
+
* here — and nothing a consumer hangs off it — may claim a completed redirect
|
|
2499
|
+
* (#1963 R05-1). The destination page's own normalized pageview proves arrival.
|
|
2500
|
+
*/
|
|
2379
2501
|
function Redirect({ children }) {
|
|
2380
2502
|
const state = usePart("redirect");
|
|
2381
2503
|
if (state === null || state.destination === null) return null;
|
|
@@ -2385,19 +2507,14 @@ function Granted({ children }) {
|
|
|
2385
2507
|
return usePart("granted") === null ? null : /* @__PURE__ */ jsx(Fragment, { children });
|
|
2386
2508
|
}
|
|
2387
2509
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
2388
|
-
const CapxulDashboardAccess = Object.assign(Root$
|
|
2389
|
-
Checking,
|
|
2510
|
+
const CapxulDashboardAccess = Object.assign(Root$3, {
|
|
2511
|
+
Checking: Checking$1,
|
|
2390
2512
|
Error: DashboardError,
|
|
2391
2513
|
Redirect,
|
|
2392
2514
|
Granted
|
|
2393
2515
|
});
|
|
2394
2516
|
//#endregion
|
|
2395
2517
|
//#region src/headless/payroll/money.ts
|
|
2396
|
-
/** `{ value: "12.34", decimals: 6 }` → `12340000n`. */
|
|
2397
|
-
function toMinorUnits(money) {
|
|
2398
|
-
const [whole = "0", fraction = ""] = money.value.split(".");
|
|
2399
|
-
return BigInt(`${whole}${fraction.padEnd(money.decimals, "0")}`);
|
|
2400
|
-
}
|
|
2401
2518
|
const TYPED_AMOUNT = /^(-?)(\d+)(?:\.(\d+))?$/;
|
|
2402
2519
|
/**
|
|
2403
2520
|
* Σ of the employer's TYPED amounts on a saved group — a DISPLAY total.
|
|
@@ -2430,6 +2547,17 @@ function fromMinorUnits(raw, asset) {
|
|
|
2430
2547
|
decimals: asset.decimals
|
|
2431
2548
|
};
|
|
2432
2549
|
}
|
|
2550
|
+
/** Convert exact raw units to one asset quantity without a fiat denomination. */
|
|
2551
|
+
function assetFromMinorUnits(raw, asset) {
|
|
2552
|
+
const money = fromMinorUnits(raw, {
|
|
2553
|
+
currency: "USD",
|
|
2554
|
+
decimals: asset.decimals
|
|
2555
|
+
});
|
|
2556
|
+
return {
|
|
2557
|
+
asset: asset.asset,
|
|
2558
|
+
value: money.value
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2433
2561
|
//#endregion
|
|
2434
2562
|
//#region src/headless/payroll/reads.ts
|
|
2435
2563
|
function toRecipientOption(entry) {
|
|
@@ -2452,8 +2580,9 @@ function toRecipientOption(entry) {
|
|
|
2452
2580
|
*/
|
|
2453
2581
|
function useOrgRoster(orgId) {
|
|
2454
2582
|
const client = useCapxulClientOrNull();
|
|
2583
|
+
const identity = useCapxulIdentityOrNull();
|
|
2455
2584
|
const query = useQuery({
|
|
2456
|
-
queryKey: capxulKeys.contactsList(orgId ?? "pending", false),
|
|
2585
|
+
queryKey: accountKey(capxulKeys.contactsList(orgId ?? "pending", false), identity),
|
|
2457
2586
|
queryFn: async () => unwrapCapxulResult(await scopedClient(client, CAPXUL_OPERATIONS.org.addressBook.list, orgId).addressBook.list({ includeHidden: false })),
|
|
2458
2587
|
enabled: client !== null && orgId !== void 0
|
|
2459
2588
|
});
|
|
@@ -2474,16 +2603,18 @@ function useOrgRoster(orgId) {
|
|
|
2474
2603
|
}
|
|
2475
2604
|
function usePayrollGroups(orgId) {
|
|
2476
2605
|
const client = useCapxulClientOrNull();
|
|
2606
|
+
const identity = useCapxulIdentityOrNull();
|
|
2477
2607
|
return useQuery({
|
|
2478
|
-
queryKey: capxulKeys.payrollGroups(orgId),
|
|
2608
|
+
queryKey: accountKey(capxulKeys.payrollGroups(orgId), identity),
|
|
2479
2609
|
queryFn: async () => unwrapCapxulResult(await scopedClient(client, CAPXUL_OPERATIONS.org.payroll.groups.list, orgId).payroll.groups.list()),
|
|
2480
2610
|
enabled: client !== null && orgId !== void 0
|
|
2481
2611
|
});
|
|
2482
2612
|
}
|
|
2483
2613
|
function usePayrollRuns(orgId) {
|
|
2484
2614
|
const client = useCapxulClientOrNull();
|
|
2615
|
+
const identity = useCapxulIdentityOrNull();
|
|
2485
2616
|
return useQuery({
|
|
2486
|
-
queryKey: capxulKeys.payrollRuns(orgId),
|
|
2617
|
+
queryKey: accountKey(capxulKeys.payrollRuns(orgId), identity),
|
|
2487
2618
|
queryFn: async () => unwrapCapxulResult(await scopedClient(client, CAPXUL_OPERATIONS.org.payroll.runs, orgId).payroll.runs()),
|
|
2488
2619
|
enabled: client !== null && orgId !== void 0
|
|
2489
2620
|
});
|
|
@@ -2498,29 +2629,34 @@ function usePayrollRuns(orgId) {
|
|
|
2498
2629
|
*/
|
|
2499
2630
|
function useOrgTerms(orgId) {
|
|
2500
2631
|
const client = useCapxulClientOrNull();
|
|
2632
|
+
const identity = useCapxulIdentityOrNull();
|
|
2501
2633
|
return useQuery({
|
|
2502
|
-
queryKey: capxulKeys.payrollTerms(orgId),
|
|
2634
|
+
queryKey: accountKey(capxulKeys.payrollTerms(orgId), identity),
|
|
2503
2635
|
queryFn: async () => unwrapCapxulResult(await scopedClient(client, CAPXUL_OPERATIONS.org.payroll.terms, orgId).payroll.terms()),
|
|
2504
2636
|
enabled: client !== null && orgId !== void 0
|
|
2505
2637
|
});
|
|
2506
2638
|
}
|
|
2507
2639
|
function useOrgTreasury(orgId) {
|
|
2508
2640
|
const client = useCapxulClientOrNull();
|
|
2641
|
+
const identity = useCapxulIdentityOrNull();
|
|
2509
2642
|
return useQuery({
|
|
2510
|
-
queryKey: capxulKeys.orgTreasury(orgId),
|
|
2643
|
+
queryKey: accountKey(capxulKeys.orgTreasury(orgId), identity),
|
|
2511
2644
|
queryFn: async () => unwrapCapxulResult(await scopedClient(client, CAPXUL_OPERATIONS.org.treasury, orgId).treasury()),
|
|
2512
2645
|
enabled: client !== null && orgId !== void 0
|
|
2513
2646
|
});
|
|
2514
2647
|
}
|
|
2515
2648
|
/**
|
|
2516
2649
|
* The caller's own standing, read for its Budgets alone (ruling Q2 grades
|
|
2517
|
-
* them). Keyed as `CapxulOrgMember` keys it
|
|
2518
|
-
* `.Can` wrapper reads
|
|
2650
|
+
* them). Keyed as `CapxulOrgMember` keys it — Organization AND acting account
|
|
2651
|
+
* (#2007 R19) — so a payroll screen inside a `.Can` wrapper still reads
|
|
2652
|
+
* `org(id).me()` once, and an account replacement is a miss for both of them
|
|
2653
|
+
* rather than the previous person's Budgets for both.
|
|
2519
2654
|
*/
|
|
2520
2655
|
function useOrgMe(orgId) {
|
|
2521
2656
|
const client = useCapxulClientOrNull();
|
|
2657
|
+
const identity = useCapxulIdentityOrNull();
|
|
2522
2658
|
return useQuery({
|
|
2523
|
-
queryKey: capxulKeys.orgMe(orgId),
|
|
2659
|
+
queryKey: capxulKeys.orgMe(orgId, actingAccount(identity)),
|
|
2524
2660
|
queryFn: async () => unwrapCapxulResult(await scopedClient(client, CAPXUL_OPERATIONS.org.me, orgId).me()),
|
|
2525
2661
|
enabled: client !== null && orgId !== void 0
|
|
2526
2662
|
});
|
|
@@ -2538,7 +2674,15 @@ function scopedClient(client, method, orgId) {
|
|
|
2538
2674
|
//#endregion
|
|
2539
2675
|
//#region src/headless/payroll/use-payroll.ts
|
|
2540
2676
|
/** Money reads the same way everywhere on this screen: amount then code. */
|
|
2541
|
-
const GRAMMAR
|
|
2677
|
+
const GRAMMAR = { grammar: "code" };
|
|
2678
|
+
/**
|
|
2679
|
+
* One captured snapshot, or nothing — never a fabricated empty options object,
|
|
2680
|
+
* and never a DOM event a screen wired straight to `onClick`.
|
|
2681
|
+
*/
|
|
2682
|
+
function carry(options) {
|
|
2683
|
+
const attempt = toInvocationOptions(options);
|
|
2684
|
+
return attempt === void 0 ? {} : { options: attempt };
|
|
2685
|
+
}
|
|
2542
2686
|
function toPayrollGroupRow(group) {
|
|
2543
2687
|
const currency = new Set(group.members.map((member) => member.currency)).size === 1 ? group.members[0]?.currency : void 0;
|
|
2544
2688
|
return {
|
|
@@ -2546,7 +2690,7 @@ function toPayrollGroupRow(group) {
|
|
|
2546
2690
|
name: group.name,
|
|
2547
2691
|
tone: group.tone,
|
|
2548
2692
|
memberCount: group.members.length,
|
|
2549
|
-
totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR
|
|
2693
|
+
totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR),
|
|
2550
2694
|
members: group.members.map((member) => ({
|
|
2551
2695
|
recipientId: member.partyId,
|
|
2552
2696
|
amount: member.amount,
|
|
@@ -2578,16 +2722,17 @@ function usePayroll(orgId) {
|
|
|
2578
2722
|
const write = useMutation({
|
|
2579
2723
|
mutationFn: async (command) => {
|
|
2580
2724
|
const payroll = scopedClient(client, CAPXUL_OPERATIONS.org.payroll.groups[command.kind], orgId).payroll;
|
|
2725
|
+
const controls = toInvocationControls(command.options);
|
|
2581
2726
|
if (command.kind === "remove") {
|
|
2582
|
-
unwrapCapxulResult(await payroll.groups.remove(command.id));
|
|
2727
|
+
unwrapCapxulResult(await payroll.groups.remove(command.id, controls));
|
|
2583
2728
|
return null;
|
|
2584
2729
|
}
|
|
2585
|
-
return unwrapCapxulResult(await payroll.groups.save(command.input));
|
|
2730
|
+
return unwrapCapxulResult(await payroll.groups.save(command.input, controls));
|
|
2586
2731
|
},
|
|
2587
2732
|
onSettled: invalidate
|
|
2588
2733
|
});
|
|
2589
2734
|
const mutateAsync = write.mutateAsync;
|
|
2590
|
-
const save = useCallback(async (input) => {
|
|
2735
|
+
const save = useCallback(async (input, options) => {
|
|
2591
2736
|
const refusal = invalidGroup(input);
|
|
2592
2737
|
if (refusal !== null) return {
|
|
2593
2738
|
ok: false,
|
|
@@ -2596,7 +2741,8 @@ function usePayroll(orgId) {
|
|
|
2596
2741
|
try {
|
|
2597
2742
|
const saved = await mutateAsync({
|
|
2598
2743
|
kind: "save",
|
|
2599
|
-
input
|
|
2744
|
+
input,
|
|
2745
|
+
...carry(options)
|
|
2600
2746
|
});
|
|
2601
2747
|
return saved === null ? {
|
|
2602
2748
|
ok: false,
|
|
@@ -2612,11 +2758,12 @@ function usePayroll(orgId) {
|
|
|
2612
2758
|
};
|
|
2613
2759
|
}
|
|
2614
2760
|
}, [mutateAsync]);
|
|
2615
|
-
const remove = useCallback(async (id) => {
|
|
2761
|
+
const remove = useCallback(async (id, options) => {
|
|
2616
2762
|
try {
|
|
2617
2763
|
await mutateAsync({
|
|
2618
2764
|
kind: "remove",
|
|
2619
|
-
id
|
|
2765
|
+
id,
|
|
2766
|
+
...carry(options)
|
|
2620
2767
|
});
|
|
2621
2768
|
return {
|
|
2622
2769
|
ok: true,
|
|
@@ -2633,8 +2780,8 @@ function usePayroll(orgId) {
|
|
|
2633
2780
|
const started = orgId !== void 0;
|
|
2634
2781
|
return {
|
|
2635
2782
|
summary: {
|
|
2636
|
-
walletBalanceDisplay: treasury.data === void 0 ? null : formatMoney(treasury.data.available, GRAMMAR
|
|
2637
|
-
paidThisMonthDisplay: runs.data === void 0 ? null :
|
|
2783
|
+
walletBalanceDisplay: treasury.data?.available === void 0 || treasury.data.available === null ? null : formatMoney(treasury.data.available, GRAMMAR),
|
|
2784
|
+
paidThisMonthDisplay: runs.data === void 0 ? null : runs.data.settledThisMonth.map(formatAssetAmount).join(", "),
|
|
2638
2785
|
isLoading: started && (treasury.data === void 0 && treasury.error === null || runs.data === void 0 && runs.error === null),
|
|
2639
2786
|
error: treasury.error ?? runs.error
|
|
2640
2787
|
},
|
|
@@ -2667,7 +2814,7 @@ function usePayrollContext() {
|
|
|
2667
2814
|
if (engine === null) throw new Error("CapxulPayroll parts must be used inside <CapxulPayroll>");
|
|
2668
2815
|
return engine;
|
|
2669
2816
|
}
|
|
2670
|
-
function Root$
|
|
2817
|
+
function Root$2({ orgId, children }) {
|
|
2671
2818
|
const engine = usePayroll(orgId);
|
|
2672
2819
|
return /* @__PURE__ */ jsx(PayrollContext.Provider, {
|
|
2673
2820
|
value: engine,
|
|
@@ -2685,7 +2832,7 @@ function Groups({ children }) {
|
|
|
2685
2832
|
* There is NO `.Runs` part (ruling Q6): no shipped region has the right shape
|
|
2686
2833
|
* for a run list, and a part without a production caller may not ship.
|
|
2687
2834
|
*/
|
|
2688
|
-
const CapxulPayroll = Object.assign(Root$
|
|
2835
|
+
const CapxulPayroll = Object.assign(Root$2, {
|
|
2689
2836
|
Summary: Summary$1,
|
|
2690
2837
|
Groups
|
|
2691
2838
|
});
|
|
@@ -2695,8 +2842,14 @@ function record(value) {
|
|
|
2695
2842
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw Errors.invalidInput("payrollDraft", "Invalid run draft");
|
|
2696
2843
|
return value;
|
|
2697
2844
|
}
|
|
2845
|
+
/**
|
|
2846
|
+
* A draft-scoped opaque key. `/` is in the class so `asset.id` can be the
|
|
2847
|
+
* canonical CAIP-19 `AssetId` (`eip155:<chainId>/erc20:<address>`): the draft
|
|
2848
|
+
* records WHICH asset was authorized, and a symbol is not an identity
|
|
2849
|
+
* (ADR-0028 R3). The field never reaches the backend — only `input` is sent.
|
|
2850
|
+
*/
|
|
2698
2851
|
function identifier(value) {
|
|
2699
|
-
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._
|
|
2852
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u.test(value)) throw Errors.invalidInput("payrollDraft", "Invalid draft identifier");
|
|
2700
2853
|
return value;
|
|
2701
2854
|
}
|
|
2702
2855
|
function minorAmount(value) {
|
|
@@ -2712,8 +2865,9 @@ function decodeDraft(value, scope) {
|
|
|
2712
2865
|
const items = input.items.map((raw) => {
|
|
2713
2866
|
const item = record(raw);
|
|
2714
2867
|
const to = record(item.to);
|
|
2868
|
+
const amount = record(item.amount);
|
|
2715
2869
|
const partyId = toPartyId(identifier(item.partyId));
|
|
2716
|
-
if (to.kind !== "party" || to.partyId !== partyId || item.gross !== item.net || !Array.isArray(item.adjustments) || item.adjustments.length !== 0) throw Errors.invalidInput("payrollDraft", "Invalid draft recipient");
|
|
2870
|
+
if (to.kind !== "party" || to.partyId !== partyId || item.gross !== item.net || typeof amount.asset !== "string" || typeof amount.value !== "string" || typeof item.currency !== "string" || typeof item.decimals !== "number" || !Number.isInteger(item.decimals) || !Array.isArray(item.adjustments) || item.adjustments.length !== 0) throw Errors.invalidInput("payrollDraft", "Invalid draft recipient");
|
|
2717
2871
|
const net = minorAmount(item.net);
|
|
2718
2872
|
return {
|
|
2719
2873
|
to: {
|
|
@@ -2721,12 +2875,20 @@ function decodeDraft(value, scope) {
|
|
|
2721
2875
|
partyId
|
|
2722
2876
|
},
|
|
2723
2877
|
partyId,
|
|
2878
|
+
amount: {
|
|
2879
|
+
asset: requireAsset(amount.asset).assetId,
|
|
2880
|
+
value: amount.value
|
|
2881
|
+
},
|
|
2724
2882
|
gross: net,
|
|
2725
2883
|
net,
|
|
2884
|
+
currency: item.currency,
|
|
2885
|
+
decimals: item.decimals,
|
|
2726
2886
|
adjustments: []
|
|
2727
2887
|
};
|
|
2728
2888
|
});
|
|
2729
2889
|
if (new Set(items.map((item) => item.partyId)).size !== items.length) throw Errors.invalidInput("payrollDraft", "Duplicate draft recipient");
|
|
2890
|
+
const exactAsset = requireAsset(identifier(asset.id));
|
|
2891
|
+
if (items.some((item) => item.amount.asset !== exactAsset.assetId)) throw Errors.invalidInput("payrollDraft", "Draft item asset does not match the run asset");
|
|
2730
2892
|
return {
|
|
2731
2893
|
version: 1,
|
|
2732
2894
|
scope,
|
|
@@ -2737,6 +2899,7 @@ function decodeDraft(value, scope) {
|
|
|
2737
2899
|
},
|
|
2738
2900
|
input: {
|
|
2739
2901
|
permissionId: identifier(input.permissionId),
|
|
2902
|
+
asset: exactAsset.assetId,
|
|
2740
2903
|
period: {
|
|
2741
2904
|
start: period.start,
|
|
2742
2905
|
end: period.start
|
|
@@ -2940,7 +3103,6 @@ function termsSeeds(terms, asset, period) {
|
|
|
2940
3103
|
}
|
|
2941
3104
|
//#endregion
|
|
2942
3105
|
//#region src/headless/payroll/use-payroll-run.ts
|
|
2943
|
-
const GRAMMAR = { grammar: "code" };
|
|
2944
3106
|
/**
|
|
2945
3107
|
* The period this composer composes: an INSTANT run (ruling Q3). `authorizeRun`
|
|
2946
3108
|
* stamps `start === end === runAt` at submit, and the payslip's period renders
|
|
@@ -2964,6 +3126,32 @@ const INSTANT_RUN_PERIOD = {
|
|
|
2964
3126
|
function amountText(amounts, seeds, recipientId) {
|
|
2965
3127
|
return amounts[recipientId]?.value ?? seeds.get(recipientId) ?? "";
|
|
2966
3128
|
}
|
|
3129
|
+
function freshRunItems(input) {
|
|
3130
|
+
const items = [];
|
|
3131
|
+
for (const [index, recipientId] of input.selected.entries()) {
|
|
3132
|
+
const to = input.payableRefs[index];
|
|
3133
|
+
const amount = input.amounts[index];
|
|
3134
|
+
const terms = input.compensationTerms.get(recipientId);
|
|
3135
|
+
if (to === void 0 || amount === void 0 || terms === void 0) throw Errors.unknown();
|
|
3136
|
+
items.push({
|
|
3137
|
+
to: {
|
|
3138
|
+
kind: "party",
|
|
3139
|
+
partyId: toPartyId(recipientId)
|
|
3140
|
+
},
|
|
3141
|
+
partyId: recipientId,
|
|
3142
|
+
amount,
|
|
3143
|
+
gross: terms.rate,
|
|
3144
|
+
net: terms.rate,
|
|
3145
|
+
currency: terms.currency,
|
|
3146
|
+
decimals: terms.decimals,
|
|
3147
|
+
adjustments: []
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
return items;
|
|
3151
|
+
}
|
|
3152
|
+
function hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, terms) {
|
|
3153
|
+
return frozen || !writerNamesSelectedAsset || selected.every((recipientId) => terms.has(recipientId));
|
|
3154
|
+
}
|
|
2967
3155
|
/**
|
|
2968
3156
|
* WHICH run the composer is composing, by MEANING rather than by object shape.
|
|
2969
3157
|
*
|
|
@@ -2995,6 +3183,33 @@ function initialAmounts(prefill) {
|
|
|
2995
3183
|
};
|
|
2996
3184
|
return seeded;
|
|
2997
3185
|
}
|
|
3186
|
+
/**
|
|
3187
|
+
* One `.Asset` option per position the treasury Account holds, taken from
|
|
3188
|
+
* `account.balances` — never from `account.available`, which is the Account's
|
|
3189
|
+
* fiat VALUATION of those positions (ADR-0028 R5/R6). The option is keyed on
|
|
3190
|
+
* the ASSET id: several positions share one account id, and a symbol is display
|
|
3191
|
+
* only (ADR-0028 R3). Every row is listed, so a real multi-position treasury
|
|
3192
|
+
* still offers its holdings.
|
|
3193
|
+
*/
|
|
3194
|
+
function assetSelectionsFromAccount(account) {
|
|
3195
|
+
if (account === null) return [];
|
|
3196
|
+
return account.balances.map((position) => {
|
|
3197
|
+
return {
|
|
3198
|
+
option: {
|
|
3199
|
+
id: position.asset,
|
|
3200
|
+
symbol: position.symbol,
|
|
3201
|
+
availableDisplay: formatAssetAmount({
|
|
3202
|
+
asset: position.asset,
|
|
3203
|
+
value: position.available
|
|
3204
|
+
})
|
|
3205
|
+
},
|
|
3206
|
+
money: {
|
|
3207
|
+
asset: position.asset,
|
|
3208
|
+
value: position.available
|
|
3209
|
+
}
|
|
3210
|
+
};
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
2998
3213
|
function usePayrollRun(input) {
|
|
2999
3214
|
const { orgId, prefill, onRun, onFailed } = input;
|
|
3000
3215
|
const client = useCapxulClientOrNull();
|
|
@@ -3008,17 +3223,13 @@ function usePayrollRun(input) {
|
|
|
3008
3223
|
const terms = useOrgTerms(orgId);
|
|
3009
3224
|
const [selected, setSelected] = useState(() => prefill?.recipientIds ?? []);
|
|
3010
3225
|
const [amounts, setAmounts] = useState(() => initialAmounts(prefill));
|
|
3011
|
-
const restoreCurrency = treasury.data?.available.currency;
|
|
3012
3226
|
const { draftScope, currentScope, draft, draftError, restorationPending, activateDraft, restoreDraft, setDraft, recovery, draftBlocked } = useRunDraft(client, orgId, useCallback((restored) => {
|
|
3013
3227
|
setSelected(restored.input.items.map((item) => item.partyId));
|
|
3014
|
-
if (
|
|
3015
|
-
value:
|
|
3016
|
-
currency: restoreCurrency,
|
|
3017
|
-
decimals: restored.asset.decimals
|
|
3018
|
-
}).value,
|
|
3228
|
+
if (requireAsset(restored.input.asset).assetId === restored.input.asset) setAmounts(Object.fromEntries(restored.input.items.map((item) => [item.partyId, {
|
|
3229
|
+
value: item.amount.value,
|
|
3019
3230
|
prefilled: false
|
|
3020
3231
|
}])));
|
|
3021
|
-
}, [
|
|
3232
|
+
}, []));
|
|
3022
3233
|
const frozen = draft !== null;
|
|
3023
3234
|
const composerKey = composerIdentity(orgId, prefill, draftScope, frozen);
|
|
3024
3235
|
const [seededKey, setSeededKey] = useState(composerKey);
|
|
@@ -3058,11 +3269,8 @@ function usePayrollRun(input) {
|
|
|
3058
3269
|
draftError
|
|
3059
3270
|
]);
|
|
3060
3271
|
const account = treasury.data ?? null;
|
|
3061
|
-
const
|
|
3062
|
-
|
|
3063
|
-
symbol: account.available.currency,
|
|
3064
|
-
availableDisplay: formatMoney(account.available, GRAMMAR)
|
|
3065
|
-
}], [account]);
|
|
3272
|
+
const assetSelections = useMemo(() => assetSelectionsFromAccount(account), [account]);
|
|
3273
|
+
const assetOptions = useMemo(() => assetSelections.map((selection) => selection.option), [assetSelections]);
|
|
3066
3274
|
const [selectedAssetId, setSelectedAssetId] = useState(null);
|
|
3067
3275
|
useEffect(() => {
|
|
3068
3276
|
if (!assetOptions.some((option) => option.id === selectedAssetId)) setSelectedAssetId(assetOptions[0]?.id ?? null);
|
|
@@ -3071,7 +3279,7 @@ function usePayrollRun(input) {
|
|
|
3071
3279
|
const selectAsset = useCallback((id) => {
|
|
3072
3280
|
if (!submitGate.current && !frozen) setSelectedAssetId((current) => assetOptions.some((option) => option.id === id) ? id : current);
|
|
3073
3281
|
}, [assetOptions, frozen]);
|
|
3074
|
-
const assetMoney =
|
|
3282
|
+
const assetMoney = assetSelections.find((selection) => selection.option.id === selectedAsset?.id)?.money ?? null;
|
|
3075
3283
|
const named = useMemo(() => new Map(roster.options.map((option) => [option.id, option])), [roster.options]);
|
|
3076
3284
|
const refs = roster.refs;
|
|
3077
3285
|
const payableRefs = useMemo(() => {
|
|
@@ -3083,12 +3291,19 @@ function usePayrollRun(input) {
|
|
|
3083
3291
|
}
|
|
3084
3292
|
return resolved;
|
|
3085
3293
|
}, [refs, selected]);
|
|
3086
|
-
const seeds = useMemo(() =>
|
|
3294
|
+
const seeds = useMemo(() => {
|
|
3295
|
+
if (assetMoney === null) return /* @__PURE__ */ new Map();
|
|
3296
|
+
const metadata = requireAsset(assetMoney.asset);
|
|
3297
|
+
return termsSeeds(terms.data ?? [], metadata.peg === null ? null : {
|
|
3298
|
+
currency: metadata.peg,
|
|
3299
|
+
decimals: metadata.decimals
|
|
3300
|
+
}, INSTANT_RUN_PERIOD);
|
|
3301
|
+
}, [assetMoney, terms.data]);
|
|
3087
3302
|
const entries = useMemo(() => {
|
|
3088
3303
|
if (assetMoney === null) return [];
|
|
3089
3304
|
return selected.map((recipientId) => {
|
|
3090
3305
|
const value = amountText(amounts, seeds, recipientId);
|
|
3091
|
-
const parsed = value.trim() === "" ? null :
|
|
3306
|
+
const parsed = value.trim() === "" ? null : parseAssetAmount(value, requireAsset(assetMoney.asset));
|
|
3092
3307
|
return {
|
|
3093
3308
|
recipientId,
|
|
3094
3309
|
name: named.get(recipientId)?.name ?? recipientId,
|
|
@@ -3096,7 +3311,7 @@ function usePayrollRun(input) {
|
|
|
3096
3311
|
change: (text) => change(recipientId, text),
|
|
3097
3312
|
error: parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null,
|
|
3098
3313
|
prefilled: amounts[recipientId]?.prefilled ?? seeds.has(recipientId),
|
|
3099
|
-
currency: assetMoney.
|
|
3314
|
+
currency: requireAsset(assetMoney.asset).symbol
|
|
3100
3315
|
};
|
|
3101
3316
|
});
|
|
3102
3317
|
}, [
|
|
@@ -3111,7 +3326,7 @@ function usePayrollRun(input) {
|
|
|
3111
3326
|
if (assetMoney === null || selected.length === 0) return null;
|
|
3112
3327
|
const money = [];
|
|
3113
3328
|
for (const recipientId of selected) {
|
|
3114
|
-
const parsed =
|
|
3329
|
+
const parsed = parseAssetAmount(amountText(amounts, seeds, recipientId), requireAsset(assetMoney.asset));
|
|
3115
3330
|
if (isMoneyParseError(parsed)) return null;
|
|
3116
3331
|
money.push(parsed);
|
|
3117
3332
|
}
|
|
@@ -3122,23 +3337,33 @@ function usePayrollRun(input) {
|
|
|
3122
3337
|
seeds,
|
|
3123
3338
|
selected
|
|
3124
3339
|
]);
|
|
3125
|
-
const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null :
|
|
3126
|
-
|
|
3127
|
-
|
|
3340
|
+
const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null : {
|
|
3341
|
+
asset: assetMoney.asset,
|
|
3342
|
+
value: assetFromMinorUnits(parsedAmounts.reduce((sum, amount) => sum + toRaw(amount, requireAsset(amount.asset)), 0n), {
|
|
3343
|
+
asset: assetMoney.asset,
|
|
3344
|
+
decimals: requireAsset(assetMoney.asset).decimals
|
|
3345
|
+
}).value
|
|
3346
|
+
}, [assetMoney, parsedAmounts]);
|
|
3347
|
+
const availableRaw = assetMoney === null ? null : toRaw(assetMoney, requireAsset(assetMoney.asset));
|
|
3348
|
+
const totalRaw = total === null ? null : toRaw(total, requireAsset(total.asset));
|
|
3128
3349
|
const funds = availableRaw === null || totalRaw === null ? "unknown" : totalRaw > availableRaw ? "insufficient" : "available";
|
|
3129
3350
|
const budgets = me.data?.budgets ?? [];
|
|
3351
|
+
const compensationTerms = useMemo(() => new Map((terms.data ?? []).filter((row) => row.unit === "paycheck" && /^[1-9][0-9]*$/u.test(row.rate)).map((row) => [row.partyId, row])), [terms.data]);
|
|
3352
|
+
const writerNamesSelectedAsset = writeNamesAsset();
|
|
3130
3353
|
const blockedReason = firstFalseFact({
|
|
3131
3354
|
signerReady: signerStatus === "ready",
|
|
3132
3355
|
...runAuthorityFacts(draft, budgets, selectedAsset, assetMoney, funds),
|
|
3133
3356
|
recipientCount: payableRefs === null ? 0 : selected.length,
|
|
3134
|
-
amountsParse: total !== null
|
|
3357
|
+
amountsParse: total !== null && hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, compensationTerms),
|
|
3358
|
+
writeNamesAsset: writerNamesSelectedAsset
|
|
3135
3359
|
});
|
|
3136
3360
|
const authorize = useMutation({
|
|
3137
|
-
mutationFn: async (candidate) => {
|
|
3361
|
+
mutationFn: async ({ candidate, options }) => {
|
|
3138
3362
|
if (candidate.scope !== currentScope.current) throw Errors.notAuthenticated();
|
|
3139
3363
|
return executeRunDraft(candidate, async (persisted, onRequestKey, onResolved) => {
|
|
3140
3364
|
if (candidate.scope === currentScope.current) setDraft(persisted);
|
|
3141
3365
|
return unwrapCapxulResult(await scopedClient(client, CAPXUL_OPERATIONS.org.payroll.authorizeRun, orgId).payroll.authorizeRun(persisted.input, {
|
|
3366
|
+
...toInvocationControls(options),
|
|
3142
3367
|
onResolved,
|
|
3143
3368
|
onRequestKey: (key) => {
|
|
3144
3369
|
if (candidate.scope !== currentScope.current) throw Errors.notAuthenticated();
|
|
@@ -3147,56 +3372,54 @@ function usePayrollRun(input) {
|
|
|
3147
3372
|
}));
|
|
3148
3373
|
});
|
|
3149
3374
|
},
|
|
3150
|
-
onSettled: (_run, _error, candidate) => {
|
|
3375
|
+
onSettled: (_run, _error, { candidate }) => {
|
|
3151
3376
|
queryClient.invalidateQueries({ queryKey: capxulKeys.payrollRuns(orgId) });
|
|
3152
3377
|
if (candidate.scope === currentScope.current) restoreDraft(candidate.scope);
|
|
3153
3378
|
}
|
|
3154
3379
|
});
|
|
3155
|
-
const totalDisplay = total === null ? null :
|
|
3380
|
+
const totalDisplay = total === null ? null : formatAssetAmount(total);
|
|
3156
3381
|
const authorizeMutate = authorize.mutate;
|
|
3157
|
-
const submit = useCallback(() => {
|
|
3382
|
+
const submit = useCallback((options) => {
|
|
3383
|
+
const attempt = toInvocationOptions(options);
|
|
3158
3384
|
const permissionId = budgets[0]?.id;
|
|
3159
3385
|
if (blockedReason !== null || submitGate.current || draftBlocked || draftScope === null || assetMoney === null || selectedAsset === null) return;
|
|
3160
3386
|
if (permissionId === void 0 || totalDisplay === null || parsedAmounts === null) return;
|
|
3161
3387
|
if (payableRefs === null) return;
|
|
3162
3388
|
const recipientCount = selected.length;
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
const
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
kind: "party",
|
|
3172
|
-
partyId: toPartyId(recipientId)
|
|
3173
|
-
},
|
|
3174
|
-
partyId: recipientId,
|
|
3175
|
-
gross: raw,
|
|
3176
|
-
net: raw,
|
|
3177
|
-
adjustments: []
|
|
3389
|
+
let candidate;
|
|
3390
|
+
if (frozen) candidate = draft;
|
|
3391
|
+
else {
|
|
3392
|
+
const items = freshRunItems({
|
|
3393
|
+
selected,
|
|
3394
|
+
payableRefs,
|
|
3395
|
+
amounts: parsedAmounts,
|
|
3396
|
+
compensationTerms
|
|
3178
3397
|
});
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
decimals: assetMoney.decimals
|
|
3188
|
-
},
|
|
3189
|
-
input: {
|
|
3190
|
-
permissionId,
|
|
3191
|
-
period: {
|
|
3192
|
-
start: runAt,
|
|
3193
|
-
end: runAt
|
|
3398
|
+
const runAt = Date.now();
|
|
3399
|
+
candidate = {
|
|
3400
|
+
version: 1,
|
|
3401
|
+
scope: draftScope,
|
|
3402
|
+
asset: {
|
|
3403
|
+
id: selectedAsset.id,
|
|
3404
|
+
currency: requireAsset(assetMoney.asset).symbol,
|
|
3405
|
+
decimals: requireAsset(assetMoney.asset).decimals
|
|
3194
3406
|
},
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3407
|
+
input: {
|
|
3408
|
+
permissionId,
|
|
3409
|
+
asset: selectedAsset.id,
|
|
3410
|
+
period: {
|
|
3411
|
+
start: runAt,
|
|
3412
|
+
end: runAt
|
|
3413
|
+
},
|
|
3414
|
+
items
|
|
3415
|
+
}
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3198
3418
|
submitGate.current = true;
|
|
3199
|
-
authorizeMutate(
|
|
3419
|
+
authorizeMutate({
|
|
3420
|
+
candidate,
|
|
3421
|
+
...attempt === void 0 ? {} : { options: attempt }
|
|
3422
|
+
}, {
|
|
3200
3423
|
onSuccess: (run) => {
|
|
3201
3424
|
if (candidate.scope === currentScope.current) onRun({
|
|
3202
3425
|
id: run.id,
|
|
@@ -3223,6 +3446,7 @@ function usePayrollRun(input) {
|
|
|
3223
3446
|
selectedAsset,
|
|
3224
3447
|
blockedReason,
|
|
3225
3448
|
budgets,
|
|
3449
|
+
compensationTerms,
|
|
3226
3450
|
onFailed,
|
|
3227
3451
|
onRun,
|
|
3228
3452
|
parsedAmounts,
|
|
@@ -3271,18 +3495,25 @@ function usePayrollRun(input) {
|
|
|
3271
3495
|
};
|
|
3272
3496
|
}
|
|
3273
3497
|
function remainingBalanceDisplay(available, total, asset) {
|
|
3274
|
-
|
|
3498
|
+
if (available === null || total === null || asset === null) return null;
|
|
3499
|
+
const remaining = available - total;
|
|
3500
|
+
const magnitude = remaining < 0n ? -remaining : remaining;
|
|
3501
|
+
const formatted = formatAssetAmount(assetFromMinorUnits(magnitude, {
|
|
3502
|
+
asset: asset.asset,
|
|
3503
|
+
decimals: requireAsset(asset.asset).decimals
|
|
3504
|
+
}));
|
|
3505
|
+
return remaining < 0n ? `-${formatted}` : formatted;
|
|
3275
3506
|
}
|
|
3276
3507
|
/** Persisted replay keeps its authorized Budget and asset; a prior debit cannot block its own recovery. */
|
|
3277
3508
|
function runAuthorityFacts(draft, budgets, asset, money, funds) {
|
|
3278
3509
|
return {
|
|
3279
3510
|
budgetCount: draft !== null && !budgets.some((budget) => budget.id === draft.input.permissionId) ? 0 : budgets.length,
|
|
3280
|
-
hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.asset
|
|
3511
|
+
hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.input.asset === money?.asset),
|
|
3281
3512
|
funds: draft?.input.requestKey === void 0 ? funds : "available"
|
|
3282
3513
|
};
|
|
3283
3514
|
}
|
|
3284
3515
|
/**
|
|
3285
|
-
* The
|
|
3516
|
+
* The facts, in the ONE order the contract fixes. Exactly one code comes
|
|
3286
3517
|
* out, and it is always the first thing that is false — so two problems never
|
|
3287
3518
|
* race to name the button.
|
|
3288
3519
|
*/
|
|
@@ -3293,7 +3524,8 @@ function firstFalseFact(facts) {
|
|
|
3293
3524
|
if (!facts.hasAsset) return "no-asset";
|
|
3294
3525
|
if (facts.recipientCount === 0) return "no-recipients";
|
|
3295
3526
|
if (!facts.amountsParse) return "amount-invalid";
|
|
3296
|
-
|
|
3527
|
+
if (facts.funds === "insufficient") return "insufficient-funds";
|
|
3528
|
+
return facts.writeNamesAsset ? null : "asset-unnamed";
|
|
3297
3529
|
}
|
|
3298
3530
|
//#endregion
|
|
3299
3531
|
//#region src/headless/payroll/payroll-run.tsx
|
|
@@ -3303,7 +3535,7 @@ function usePayrollRunContext() {
|
|
|
3303
3535
|
if (engine === null) throw new Error("CapxulPayrollRun parts must be used inside <CapxulPayrollRun>");
|
|
3304
3536
|
return engine;
|
|
3305
3537
|
}
|
|
3306
|
-
function Root({ orgId, prefill, onRun, onFailed, children }) {
|
|
3538
|
+
function Root$1({ orgId, prefill, onRun, onFailed, children }) {
|
|
3307
3539
|
const engine = usePayrollRun({
|
|
3308
3540
|
orgId,
|
|
3309
3541
|
prefill,
|
|
@@ -3333,7 +3565,7 @@ function Actions({ children }) {
|
|
|
3333
3565
|
return /* @__PURE__ */ jsx(Fragment, { children: children(engine.actions) });
|
|
3334
3566
|
}
|
|
3335
3567
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
3336
|
-
const CapxulPayrollRun = Object.assign(Root, {
|
|
3568
|
+
const CapxulPayrollRun = Object.assign(Root$1, {
|
|
3337
3569
|
Recipients,
|
|
3338
3570
|
Amounts,
|
|
3339
3571
|
Asset,
|
|
@@ -3341,4 +3573,230 @@ const CapxulPayrollRun = Object.assign(Root, {
|
|
|
3341
3573
|
Actions
|
|
3342
3574
|
});
|
|
3343
3575
|
//#endregion
|
|
3344
|
-
|
|
3576
|
+
//#region src/headless/client-connection/use-client-connection.ts
|
|
3577
|
+
function useClientConnection(input) {
|
|
3578
|
+
const [view, setView] = useState(null);
|
|
3579
|
+
const [pending, setPending] = useState(false);
|
|
3580
|
+
const [error, setError] = useState(null);
|
|
3581
|
+
const [resendReady, setResendReady] = useState(false);
|
|
3582
|
+
const identityRef = useRef({
|
|
3583
|
+
client: input.client,
|
|
3584
|
+
requestId: input.requestId
|
|
3585
|
+
});
|
|
3586
|
+
identityRef.current = {
|
|
3587
|
+
client: input.client,
|
|
3588
|
+
requestId: input.requestId
|
|
3589
|
+
};
|
|
3590
|
+
const belongsHere = useCallback((client, requestId) => identityRef.current.client === client && identityRef.current.requestId === requestId, []);
|
|
3591
|
+
const refresh = useCallback(async (signal) => {
|
|
3592
|
+
const { client, requestId } = identityRef.current;
|
|
3593
|
+
const result = await client.auth.clientRequests.read({ requestId }, signal === void 0 ? void 0 : { signal });
|
|
3594
|
+
if (signal?.aborted || !belongsHere(client, requestId)) return;
|
|
3595
|
+
if (result.ok) {
|
|
3596
|
+
setView(result.value);
|
|
3597
|
+
setError(null);
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
setError(result.error);
|
|
3601
|
+
}, [belongsHere]);
|
|
3602
|
+
useEffect(() => {
|
|
3603
|
+
setView(null);
|
|
3604
|
+
setError(null);
|
|
3605
|
+
setPending(false);
|
|
3606
|
+
setResendReady(false);
|
|
3607
|
+
const controller = new AbortController();
|
|
3608
|
+
refresh(controller.signal);
|
|
3609
|
+
return () => {
|
|
3610
|
+
controller.abort();
|
|
3611
|
+
};
|
|
3612
|
+
}, [
|
|
3613
|
+
input.client,
|
|
3614
|
+
input.requestId,
|
|
3615
|
+
refresh
|
|
3616
|
+
]);
|
|
3617
|
+
useEffect(() => {
|
|
3618
|
+
if (view?.phase !== "otp_required") {
|
|
3619
|
+
setResendReady(false);
|
|
3620
|
+
return;
|
|
3621
|
+
}
|
|
3622
|
+
const remaining = view.resendAt - Date.now();
|
|
3623
|
+
if (remaining <= 0) {
|
|
3624
|
+
setResendReady(true);
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
setResendReady(false);
|
|
3628
|
+
const timer = setTimeout(() => setResendReady(true), remaining);
|
|
3629
|
+
return () => clearTimeout(timer);
|
|
3630
|
+
}, [view]);
|
|
3631
|
+
const run = useCallback(async (work) => {
|
|
3632
|
+
const { client, requestId } = identityRef.current;
|
|
3633
|
+
setPending(true);
|
|
3634
|
+
try {
|
|
3635
|
+
const result = await work();
|
|
3636
|
+
if (!belongsHere(client, requestId)) return;
|
|
3637
|
+
if (!result.ok) {
|
|
3638
|
+
setError(result.error);
|
|
3639
|
+
return;
|
|
3640
|
+
}
|
|
3641
|
+
await refresh();
|
|
3642
|
+
} finally {
|
|
3643
|
+
if (belongsHere(client, requestId)) setPending(false);
|
|
3644
|
+
}
|
|
3645
|
+
}, [belongsHere, refresh]);
|
|
3646
|
+
return useMemo(() => {
|
|
3647
|
+
if (error !== null && (view === null || view.phase !== "refused")) return {
|
|
3648
|
+
phase: "failure",
|
|
3649
|
+
otp: null,
|
|
3650
|
+
consent: null,
|
|
3651
|
+
complete: null,
|
|
3652
|
+
failure: {
|
|
3653
|
+
code: error.code,
|
|
3654
|
+
retry: () => refresh(),
|
|
3655
|
+
cancel: async () => {
|
|
3656
|
+
if (view === null) return;
|
|
3657
|
+
const { client, requestId } = identityRef.current;
|
|
3658
|
+
await run(() => client.auth.clientRequests.cancel({
|
|
3659
|
+
requestId,
|
|
3660
|
+
revision: view.revision
|
|
3661
|
+
}));
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
if (view === null) return {
|
|
3666
|
+
phase: "checking",
|
|
3667
|
+
otp: null,
|
|
3668
|
+
consent: null,
|
|
3669
|
+
complete: null,
|
|
3670
|
+
failure: null
|
|
3671
|
+
};
|
|
3672
|
+
if (view.phase === "otp_required") return {
|
|
3673
|
+
phase: view.phase,
|
|
3674
|
+
otp: {
|
|
3675
|
+
email: view.email,
|
|
3676
|
+
pending,
|
|
3677
|
+
canResend: resendReady,
|
|
3678
|
+
verifyCode: async (code) => {
|
|
3679
|
+
const { client, requestId } = identityRef.current;
|
|
3680
|
+
await run(() => client.auth.clientRequests.verifyOtp({
|
|
3681
|
+
requestId,
|
|
3682
|
+
code
|
|
3683
|
+
}));
|
|
3684
|
+
},
|
|
3685
|
+
resend: async () => {
|
|
3686
|
+
const { client, requestId } = identityRef.current;
|
|
3687
|
+
await run(() => client.auth.clientRequests.sendOtp({ requestId }));
|
|
3688
|
+
}
|
|
3689
|
+
},
|
|
3690
|
+
consent: null,
|
|
3691
|
+
complete: null,
|
|
3692
|
+
failure: null
|
|
3693
|
+
};
|
|
3694
|
+
if (view.phase === "review") return {
|
|
3695
|
+
phase: view.phase,
|
|
3696
|
+
otp: null,
|
|
3697
|
+
consent: {
|
|
3698
|
+
verifiedEmail: view.email,
|
|
3699
|
+
clientLabel: view.clientLabel,
|
|
3700
|
+
scopeCodes: view.scopes,
|
|
3701
|
+
purpose: view.intent.mode,
|
|
3702
|
+
setupState: view.setupState,
|
|
3703
|
+
pending,
|
|
3704
|
+
accept: async () => {
|
|
3705
|
+
const { client, requestId } = identityRef.current;
|
|
3706
|
+
await run(() => client.auth.clientRequests.accept({
|
|
3707
|
+
requestId,
|
|
3708
|
+
revision: view.revision
|
|
3709
|
+
}));
|
|
3710
|
+
},
|
|
3711
|
+
cancel: async () => {
|
|
3712
|
+
const { client, requestId } = identityRef.current;
|
|
3713
|
+
await run(() => client.auth.clientRequests.cancel({
|
|
3714
|
+
requestId,
|
|
3715
|
+
revision: view.revision
|
|
3716
|
+
}));
|
|
3717
|
+
}
|
|
3718
|
+
},
|
|
3719
|
+
complete: null,
|
|
3720
|
+
failure: null
|
|
3721
|
+
};
|
|
3722
|
+
if (view.phase === "connected") return {
|
|
3723
|
+
phase: view.phase,
|
|
3724
|
+
otp: null,
|
|
3725
|
+
consent: null,
|
|
3726
|
+
complete: { outcome: "connected" },
|
|
3727
|
+
failure: null
|
|
3728
|
+
};
|
|
3729
|
+
if (view.phase === "refused") return {
|
|
3730
|
+
phase: view.phase,
|
|
3731
|
+
otp: null,
|
|
3732
|
+
consent: null,
|
|
3733
|
+
complete: null,
|
|
3734
|
+
failure: {
|
|
3735
|
+
code: view.code,
|
|
3736
|
+
...view.field === void 0 ? {} : { field: view.field },
|
|
3737
|
+
retry: () => refresh(),
|
|
3738
|
+
cancel: async () => void 0
|
|
3739
|
+
}
|
|
3740
|
+
};
|
|
3741
|
+
return {
|
|
3742
|
+
phase: view.phase,
|
|
3743
|
+
otp: null,
|
|
3744
|
+
consent: null,
|
|
3745
|
+
complete: null,
|
|
3746
|
+
failure: null
|
|
3747
|
+
};
|
|
3748
|
+
}, [
|
|
3749
|
+
error,
|
|
3750
|
+
pending,
|
|
3751
|
+
refresh,
|
|
3752
|
+
resendReady,
|
|
3753
|
+
run,
|
|
3754
|
+
view
|
|
3755
|
+
]);
|
|
3756
|
+
}
|
|
3757
|
+
//#endregion
|
|
3758
|
+
//#region src/headless/client-connection/client-connection.tsx
|
|
3759
|
+
const ConnectionContext = createContext(null);
|
|
3760
|
+
function useConnection() {
|
|
3761
|
+
const engine = useContext(ConnectionContext);
|
|
3762
|
+
if (engine === null) throw new Error("CapxulClientConnection parts must be used inside <CapxulClientConnection>");
|
|
3763
|
+
return engine;
|
|
3764
|
+
}
|
|
3765
|
+
function Root({ client, requestId, children }) {
|
|
3766
|
+
const engine = useClientConnection({
|
|
3767
|
+
client,
|
|
3768
|
+
requestId
|
|
3769
|
+
});
|
|
3770
|
+
return /* @__PURE__ */ jsx(ConnectionContext.Provider, {
|
|
3771
|
+
value: engine,
|
|
3772
|
+
children
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
function Checking({ children }) {
|
|
3776
|
+
return useConnection().phase === "checking" ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
|
|
3777
|
+
}
|
|
3778
|
+
function Otp({ children }) {
|
|
3779
|
+
const otp = useConnection().otp;
|
|
3780
|
+
return otp === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(otp) });
|
|
3781
|
+
}
|
|
3782
|
+
function Consent({ children }) {
|
|
3783
|
+
const consent = useConnection().consent;
|
|
3784
|
+
return consent === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(consent) });
|
|
3785
|
+
}
|
|
3786
|
+
function Complete({ children }) {
|
|
3787
|
+
const complete = useConnection().complete;
|
|
3788
|
+
return complete === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(complete) });
|
|
3789
|
+
}
|
|
3790
|
+
function Failure({ children }) {
|
|
3791
|
+
const failure = useConnection().failure;
|
|
3792
|
+
return failure === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(failure) });
|
|
3793
|
+
}
|
|
3794
|
+
const CapxulClientConnection = Object.assign(Root, {
|
|
3795
|
+
Checking,
|
|
3796
|
+
Otp,
|
|
3797
|
+
Consent,
|
|
3798
|
+
Complete,
|
|
3799
|
+
Failure
|
|
3800
|
+
});
|
|
3801
|
+
//#endregion
|
|
3802
|
+
export { CapxulActivity, CapxulAuthenticationController, CapxulClientConnection, CapxulContacts, CapxulDashboardAccess, CapxulOnboardingController, CapxulOrgMember, CapxulPayroll, CapxulPayrollRun, CapxulProvider, CapxulSendMoney, acknowledgeOnboardingDestination, activeOnboardingRecovery, capxulKeys, clearOnboardingJourney, currentOnboardingJourneyId, entered, invalidateOnboardingJourneyObservation, isClaimed, isRestoring, loadOnboardingJourney, onboardingJourneyPosition, saveOnboardingJourney, startOnboardingJourney, useCapxul, useCapxulAccountBalance, useCapxulAccountFund, useCapxulActivity, useCapxulActivityDetail, useCapxulAnnotateMovement, useCapxulAuth, useCapxulCancelPayment, useCapxulClaimPayment, useCapxulClientOrNull, useCapxulCreateCommitment, useCapxulCreateOrg, useCapxulDestination, useCapxulHandleAvailability, useCapxulHoldings, useCapxulIdentity, useCapxulImageUpload, useCapxulOrgMembers, useCapxulOrgRoles, useCapxulOrgTreasury, useCapxulOrganizationPay, useCapxulOrganizationPayBatch, useCapxulOrgs, useCapxulPay, useCapxulPayment, useCapxulPayments, useCapxulPermission, useCapxulPermissionAssign, useCapxulPermissionChange, useCapxulPermissionCreate, useCapxulPermissionReplace, useCapxulPermissionRevoke, useCapxulPermissions, useCapxulProfile, useCapxulRedirectPayment, useCapxulSend, useCapxulTransitions, useCapxulUploadImage };
|