@capxul/sdk-react 4.20.0-beta.1 → 4.20.0-beta.11
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 +22 -7
- package/dist/{controllers-BWmKRFy6.d.mts → controllers-BMAU2kKH.d.mts} +2 -8
- package/dist/{controllers-DXmEHrnw.mjs → controllers-BV3e8WD3.mjs} +44 -39
- package/dist/index.d.mts +107 -55
- package/dist/index.mjs +684 -241
- package/dist/testing/index.d.mts +1 -1
- 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 { _ 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-
|
|
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-BV3e8WD3.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
|
/**
|
|
@@ -538,7 +538,7 @@ function useCapxulCreateOrg() {
|
|
|
538
538
|
const queryClient = useQueryClient();
|
|
539
539
|
return useMutation({
|
|
540
540
|
mutationFn: async (input) => {
|
|
541
|
-
return unwrapCapxulResult(await requireBootstrappedClient(client,
|
|
541
|
+
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.onboarding.completeOrganization).onboarding.completeOrganization(input));
|
|
542
542
|
},
|
|
543
543
|
onSuccess: async () => {
|
|
544
544
|
await queryClient.invalidateQueries({ queryKey: capxulKeys.orgs });
|
|
@@ -546,22 +546,6 @@ function useCapxulCreateOrg() {
|
|
|
546
546
|
});
|
|
547
547
|
}
|
|
548
548
|
//#endregion
|
|
549
|
-
//#region src/hooks/use-capxul-invite-member.ts
|
|
550
|
-
function useCapxulInviteMember(orgId) {
|
|
551
|
-
const client = useCapxulClientOrNull();
|
|
552
|
-
const queryClient = useQueryClient();
|
|
553
|
-
return useMutation({
|
|
554
|
-
mutationFn: async ({ options, ...input }) => {
|
|
555
|
-
if (orgId === void 0) throw Errors.invalidInput("orgId", "required for useCapxulInviteMember");
|
|
556
|
-
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.org.invite).org(orgId).invite(input, toInvocationControls(options)));
|
|
557
|
-
},
|
|
558
|
-
onSuccess: async () => {
|
|
559
|
-
if (orgId === void 0) return;
|
|
560
|
-
await queryClient.invalidateQueries({ queryKey: capxulKeys.orgMembers(orgId) });
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
//#endregion
|
|
565
549
|
//#region src/headless/journey/journey-state.ts
|
|
566
550
|
const JOURNEY_KEY = "capxul.onboarding.journey.v1";
|
|
567
551
|
const VALIDATED_OWNER_KEY = "capxul.onboarding.validated-owner.v1";
|
|
@@ -1079,7 +1063,7 @@ function useContactsContext() {
|
|
|
1079
1063
|
if (engine === null) throw new Error("CapxulContacts parts must be used inside <CapxulContacts>");
|
|
1080
1064
|
return engine;
|
|
1081
1065
|
}
|
|
1082
|
-
function Root$
|
|
1066
|
+
function Root$7({ actor, onAdded, onFailed, children }) {
|
|
1083
1067
|
const engine = useContacts({
|
|
1084
1068
|
actor,
|
|
1085
1069
|
onAdded,
|
|
@@ -1103,7 +1087,7 @@ function Add({ children }) {
|
|
|
1103
1087
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useContactsContext().add) });
|
|
1104
1088
|
}
|
|
1105
1089
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
1106
|
-
const CapxulContacts = Object.assign(Root$
|
|
1090
|
+
const CapxulContacts = Object.assign(Root$7, {
|
|
1107
1091
|
Summary: Summary$3,
|
|
1108
1092
|
List,
|
|
1109
1093
|
Add
|
|
@@ -1141,15 +1125,8 @@ function useActivityEnabled() {
|
|
|
1141
1125
|
}
|
|
1142
1126
|
//#endregion
|
|
1143
1127
|
//#region src/headless/activity/detail.ts
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
* the payslip, else the receipt, else nothing. Those two kinds are the ones the
|
|
1147
|
-
* Download receipt button is for. A Movement detail carries no documents.
|
|
1148
|
-
*/
|
|
1149
|
-
function pickReceiptDocument(detail) {
|
|
1150
|
-
if (detail === null || detail.kind !== "payment") return null;
|
|
1151
|
-
const documents = detail.payment.documents;
|
|
1152
|
-
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;
|
|
1153
1130
|
}
|
|
1154
1131
|
/**
|
|
1155
1132
|
* The root actor and the whole reference are included. A payment reference can
|
|
@@ -1184,27 +1161,76 @@ function useActivityDetail(reference, actor) {
|
|
|
1184
1161
|
const enabled = useActivityEnabled() && reference !== void 0;
|
|
1185
1162
|
const detail = useQuery({
|
|
1186
1163
|
queryKey: accountKey(capxulKeys.activityDetail(reference, actor), identity),
|
|
1187
|
-
queryFn: async () => {
|
|
1164
|
+
queryFn: async ({ signal }) => {
|
|
1188
1165
|
if (reference === void 0) throw Errors.invalidInput("reference", "required for activity.get");
|
|
1189
|
-
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
|
+
}));
|
|
1190
1170
|
},
|
|
1191
1171
|
enabled
|
|
1192
1172
|
});
|
|
1193
|
-
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]);
|
|
1194
1197
|
const [isOpeningDocument, setIsOpeningDocument] = useState(false);
|
|
1195
|
-
const
|
|
1196
|
-
|
|
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);
|
|
1197
1204
|
setIsOpeningDocument(true);
|
|
1198
1205
|
try {
|
|
1199
|
-
|
|
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);
|
|
1200
1215
|
} finally {
|
|
1201
|
-
|
|
1216
|
+
pending.current.delete(controller);
|
|
1217
|
+
if (!controller.signal.aborted) setIsOpeningDocument(pending.current.size > 0);
|
|
1202
1218
|
}
|
|
1203
1219
|
}, [
|
|
1204
1220
|
client,
|
|
1205
|
-
|
|
1221
|
+
selectedDocument,
|
|
1206
1222
|
reference
|
|
1207
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]);
|
|
1208
1234
|
const [typed, setTyped] = useState(null);
|
|
1209
1235
|
const annotate = useCapxulAnnotateMovement();
|
|
1210
1236
|
const saveMemo = useCallback(async () => {
|
|
@@ -1229,8 +1255,15 @@ function useActivityDetail(reference, actor) {
|
|
|
1229
1255
|
typed
|
|
1230
1256
|
]);
|
|
1231
1257
|
return {
|
|
1232
|
-
|
|
1233
|
-
|
|
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,
|
|
1234
1267
|
isOpeningDocument,
|
|
1235
1268
|
memo: typed ?? detail.data?.annotation?.memo ?? "",
|
|
1236
1269
|
setMemo: setTyped,
|
|
@@ -1243,6 +1276,7 @@ function useActivityDetail(reference, actor) {
|
|
|
1243
1276
|
//#endregion
|
|
1244
1277
|
//#region src/headless/activity/summary.ts
|
|
1245
1278
|
const DAY_MS = 864e5;
|
|
1279
|
+
const display = (money) => money === null ? null : formatMoney(money);
|
|
1246
1280
|
/**
|
|
1247
1281
|
* `"last30days"` is the last 30 whole UTC days including today. Quantizing to
|
|
1248
1282
|
* the UTC day keeps the cache key stable for a whole day instead of changing on
|
|
@@ -1253,57 +1287,6 @@ function resolveWindow(window, now) {
|
|
|
1253
1287
|
if (window === "last30days") return { from: Math.floor(now / DAY_MS) * DAY_MS - 29 * DAY_MS };
|
|
1254
1288
|
return window;
|
|
1255
1289
|
}
|
|
1256
|
-
/** An integer raw-unit string lifted into a major-unit decimal string. */
|
|
1257
|
-
function majorUnits(raw, decimals) {
|
|
1258
|
-
const negative = raw.startsWith("-");
|
|
1259
|
-
const digits = negative ? raw.slice(1) : raw;
|
|
1260
|
-
const sign = negative ? "-" : "";
|
|
1261
|
-
if (decimals === 0) return `${sign}${digits}`;
|
|
1262
|
-
const padded = digits.padStart(decimals + 1, "0");
|
|
1263
|
-
const cut = padded.length - decimals;
|
|
1264
|
-
return `${sign}${padded.slice(0, cut)}.${padded.slice(cut)}`;
|
|
1265
|
-
}
|
|
1266
|
-
function absoluteNet(total) {
|
|
1267
|
-
const net = BigInt(total.net);
|
|
1268
|
-
return net < 0n ? -net : net;
|
|
1269
|
-
}
|
|
1270
|
-
/**
|
|
1271
|
-
* Buckets carry different `decimals`, so their raw integers are not comparable.
|
|
1272
|
-
* Both are scaled to the wider precision before the magnitudes are ordered.
|
|
1273
|
-
*/
|
|
1274
|
-
function compareNet(left, right) {
|
|
1275
|
-
const width = Math.max(left.decimals, right.decimals);
|
|
1276
|
-
const scaledLeft = absoluteNet(left) * 10n ** BigInt(width - left.decimals);
|
|
1277
|
-
const scaledRight = absoluteNet(right) * 10n ** BigInt(width - right.decimals);
|
|
1278
|
-
if (scaledLeft === scaledRight) return 0;
|
|
1279
|
-
return scaledLeft < scaledRight ? -1 : 1;
|
|
1280
|
-
}
|
|
1281
|
-
/**
|
|
1282
|
-
* Which currency `.Summary` shows: the bucket matching the primary holding's
|
|
1283
|
-
* currency, else the bucket with the largest absolute `net`, breaking a tie on
|
|
1284
|
-
* currency code ascending so the choice is stable across reloads.
|
|
1285
|
-
*
|
|
1286
|
-
* The primary holding is matched on `available.currency` — the consumer
|
|
1287
|
-
* `CurrencyCode` — and NOT on `Holding.symbol`, which is the raw token ticker
|
|
1288
|
-
* ("USDX") and can never equal a bucket's currency ("USD"). See the contract
|
|
1289
|
-
* correction on #1449.
|
|
1290
|
-
*/
|
|
1291
|
-
function pickBucket(totals, primaryCurrency) {
|
|
1292
|
-
if (primaryCurrency !== null) {
|
|
1293
|
-
const matched = totals.find((total) => total.currency === primaryCurrency);
|
|
1294
|
-
if (matched !== void 0) return matched;
|
|
1295
|
-
}
|
|
1296
|
-
let best = null;
|
|
1297
|
-
for (const total of totals) {
|
|
1298
|
-
if (best === null) {
|
|
1299
|
-
best = total;
|
|
1300
|
-
continue;
|
|
1301
|
-
}
|
|
1302
|
-
const order = compareNet(total, best);
|
|
1303
|
-
if (order > 0 || order === 0 && total.currency < best.currency) best = total;
|
|
1304
|
-
}
|
|
1305
|
-
return best;
|
|
1306
|
-
}
|
|
1307
1290
|
/**
|
|
1308
1291
|
* The current UTC day number, which advances ON ITS OWN at UTC midnight.
|
|
1309
1292
|
*
|
|
@@ -1347,47 +1330,36 @@ function useActivitySummary(actor, window) {
|
|
|
1347
1330
|
const params = {};
|
|
1348
1331
|
if (actor !== void 0) params.actor = actor;
|
|
1349
1332
|
if (range !== void 0) params.window = range;
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
queryKey: accountKey([...capxulKeys.holdings(actor), "primary"], identity),
|
|
1356
|
-
queryFn: async () => {
|
|
1357
|
-
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
|
+
};
|
|
1358
1338
|
},
|
|
1359
1339
|
enabled
|
|
1360
1340
|
});
|
|
1361
1341
|
const totals = summary.data?.totals;
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
const bucket = useMemo(() => totals === void 0 || isLoading ? null : pickBucket(totals, primaryCurrency), [
|
|
1365
|
-
isLoading,
|
|
1366
|
-
primaryCurrency,
|
|
1367
|
-
totals
|
|
1368
|
-
]);
|
|
1369
|
-
if (bucket === null) return {
|
|
1342
|
+
const isLoading = summary.isLoading;
|
|
1343
|
+
if (summary.data === void 0 || totals === void 0 || totals.length === 0 || isLoading) return {
|
|
1370
1344
|
currency: null,
|
|
1371
1345
|
moneyIn: null,
|
|
1372
1346
|
moneyOut: null,
|
|
1373
1347
|
net: null,
|
|
1374
1348
|
pending: null,
|
|
1349
|
+
unrated: [],
|
|
1375
1350
|
count: summary.data?.count ?? 0,
|
|
1376
1351
|
isLoading,
|
|
1377
1352
|
error: asCapxulError$1(summary.error),
|
|
1378
1353
|
...summary.data?.readWarning === void 0 ? {} : { readWarning: summary.data.readWarning }
|
|
1379
1354
|
};
|
|
1380
|
-
const
|
|
1381
|
-
currency: bucket.currency,
|
|
1382
|
-
value: majorUnits(raw, bucket.decimals),
|
|
1383
|
-
decimals: bucket.decimals
|
|
1384
|
-
});
|
|
1355
|
+
const valuation = summary.data.valuation;
|
|
1385
1356
|
return {
|
|
1386
|
-
currency:
|
|
1387
|
-
moneyIn: display(
|
|
1388
|
-
moneyOut: display(
|
|
1389
|
-
net: display(
|
|
1390
|
-
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,
|
|
1391
1363
|
count: summary.data?.count ?? 0,
|
|
1392
1364
|
isLoading,
|
|
1393
1365
|
error: asCapxulError$1(summary.error),
|
|
@@ -1414,7 +1386,7 @@ const SIGN = Object.freeze({
|
|
|
1414
1386
|
self: ""
|
|
1415
1387
|
});
|
|
1416
1388
|
function toActivityRow(item, actor) {
|
|
1417
|
-
const amountDisplay =
|
|
1389
|
+
const amountDisplay = formatAssetAmount(item.amount);
|
|
1418
1390
|
const signedAmountDisplay = `${SIGN[item.direction]}${amountDisplay}`;
|
|
1419
1391
|
if (item.kind === "movement") return {
|
|
1420
1392
|
kind: "movement",
|
|
@@ -1426,32 +1398,37 @@ function toActivityRow(item, actor) {
|
|
|
1426
1398
|
counterpartyLabel: item.counterpartyLabel,
|
|
1427
1399
|
amountDisplay,
|
|
1428
1400
|
signedAmountDisplay,
|
|
1429
|
-
currency: item.amount.
|
|
1401
|
+
currency: assetSymbolFor(item.amount.asset),
|
|
1430
1402
|
createdAt: item.createdAt,
|
|
1431
1403
|
classification: item.classification,
|
|
1432
1404
|
phase: MOVEMENT_PHASE,
|
|
1433
1405
|
receipt: item.receipt
|
|
1434
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
|
+
};
|
|
1435
1415
|
return {
|
|
1436
1416
|
kind: "payment",
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
id: item.id
|
|
1440
|
-
} : {
|
|
1441
|
-
kind: "payment",
|
|
1442
|
-
id: item.id,
|
|
1443
|
-
actor
|
|
1444
|
-
},
|
|
1417
|
+
...item.execution === void 0 ? {} : { execution: item.execution },
|
|
1418
|
+
reference,
|
|
1445
1419
|
direction: item.direction,
|
|
1446
1420
|
counterpartyLabel: item.counterpartyLabel,
|
|
1447
1421
|
amountDisplay,
|
|
1448
1422
|
signedAmountDisplay,
|
|
1449
|
-
currency: item.amount.
|
|
1423
|
+
currency: assetSymbolFor(item.amount.asset),
|
|
1450
1424
|
createdAt: item.createdAt,
|
|
1451
1425
|
ledgerKind: item.ledgerKind,
|
|
1452
1426
|
paymentType: item.paymentType,
|
|
1453
1427
|
status: item.status,
|
|
1454
|
-
phase:
|
|
1428
|
+
phase: item.execution?.notSent ? {
|
|
1429
|
+
...paymentPhase(item.status),
|
|
1430
|
+
needsAction: item.direction !== "in"
|
|
1431
|
+
} : paymentPhase(item.status),
|
|
1455
1432
|
receipt: item.receipt
|
|
1456
1433
|
};
|
|
1457
1434
|
}
|
|
@@ -1524,8 +1501,8 @@ function buildCsv(items, actor, search) {
|
|
|
1524
1501
|
row.counterpartyLabel,
|
|
1525
1502
|
row.direction,
|
|
1526
1503
|
item.amount.value,
|
|
1527
|
-
item.amount.
|
|
1528
|
-
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 : "",
|
|
1529
1506
|
row.reference.id,
|
|
1530
1507
|
row.receipt?.transactionHash ?? ""
|
|
1531
1508
|
]));
|
|
@@ -1642,6 +1619,16 @@ function useActivity(input) {
|
|
|
1642
1619
|
searchText
|
|
1643
1620
|
]);
|
|
1644
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
|
+
]);
|
|
1645
1632
|
const pagingRef = useRef({
|
|
1646
1633
|
fetchNextPage,
|
|
1647
1634
|
hasNextPage,
|
|
@@ -1744,7 +1731,7 @@ function useActivity(input) {
|
|
|
1744
1731
|
},
|
|
1745
1732
|
rows: {
|
|
1746
1733
|
rows,
|
|
1747
|
-
isLoading: query.isLoading,
|
|
1734
|
+
isLoading: query.isLoading || rows.length === 0 && emptyContinuation,
|
|
1748
1735
|
error: asCapxulError$1(query.error),
|
|
1749
1736
|
retry,
|
|
1750
1737
|
observedAt: pages === void 0 ? null : pages[pages.length - 1]?.observedAt ?? null,
|
|
@@ -1770,7 +1757,7 @@ function useActivityContext() {
|
|
|
1770
1757
|
if (engine === null) throw new Error("CapxulActivity parts must be used inside <CapxulActivity>");
|
|
1771
1758
|
return engine;
|
|
1772
1759
|
}
|
|
1773
|
-
function Root$
|
|
1760
|
+
function Root$6({ actor, pageSize, children }) {
|
|
1774
1761
|
const engine = useActivity({
|
|
1775
1762
|
...actor === void 0 ? {} : { actor },
|
|
1776
1763
|
...pageSize === void 0 ? {} : { pageSize }
|
|
@@ -1821,7 +1808,7 @@ function Export({ children }) {
|
|
|
1821
1808
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useActivityContext().csv) });
|
|
1822
1809
|
}
|
|
1823
1810
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
1824
|
-
const CapxulActivity = Object.assign(Root$
|
|
1811
|
+
const CapxulActivity = Object.assign(Root$6, {
|
|
1825
1812
|
Summary: Summary$2,
|
|
1826
1813
|
Filters,
|
|
1827
1814
|
Search,
|
|
@@ -1862,6 +1849,26 @@ function useOrgMeReading(orgId) {
|
|
|
1862
1849
|
]);
|
|
1863
1850
|
}
|
|
1864
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
|
|
1865
1872
|
//#region src/headless/send-money/signer-status.ts
|
|
1866
1873
|
const NO_SUBSCRIPTION = () => () => {};
|
|
1867
1874
|
function useSignerStatus(client) {
|
|
@@ -2011,6 +2018,37 @@ function useSelectedOption(options) {
|
|
|
2011
2018
|
}, [options]);
|
|
2012
2019
|
return [options.find((option) => option.id === selectedId) ?? null, select];
|
|
2013
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
|
+
}
|
|
2014
2052
|
function useSendMoney(input) {
|
|
2015
2053
|
const { actor, onSent, onFailed } = input;
|
|
2016
2054
|
const client = useCapxulClientOrNull();
|
|
@@ -2023,15 +2061,12 @@ function useSendMoney(input) {
|
|
|
2023
2061
|
const treasury = useCapxulOrgTreasury(orgId, { enabled: actor.kind === "organization" });
|
|
2024
2062
|
const accountQuery = actor.kind === "personal" ? balance : treasury;
|
|
2025
2063
|
const account = accountQuery.data ?? null;
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2028
|
-
symbol: account.available.currency,
|
|
2029
|
-
availableDisplay: formatMoney(account.available, { grammar: "code" })
|
|
2030
|
-
}], [account]);
|
|
2064
|
+
const assetSelections = useMemo(() => assetSelectionsFromAccount$1(account), [account]);
|
|
2065
|
+
const assetOptions = useMemo(() => assetSelections.map((selection) => selection.option), [assetSelections]);
|
|
2031
2066
|
const [selectedAssetOption, selectAsset] = useSelectedOption(assetOptions);
|
|
2032
|
-
const selectedAssetMoney =
|
|
2067
|
+
const selectedAssetMoney = useMemo(() => assetSelections.find((selection) => selection.option.id === selectedAssetOption?.id)?.money ?? null, [assetSelections, selectedAssetOption]);
|
|
2033
2068
|
const [amountText, setAmountText] = useState("");
|
|
2034
|
-
const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null :
|
|
2069
|
+
const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null : parseAssetAmount(amountText, requireAsset(selectedAssetMoney.asset)), [amountText, selectedAssetMoney]);
|
|
2035
2070
|
const parsedMoney = parsed !== null && !isMoneyParseError(parsed) ? parsed : null;
|
|
2036
2071
|
const amountError = parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null;
|
|
2037
2072
|
const recipient = useRecipientResolution(client, JSON.stringify([
|
|
@@ -2043,7 +2078,7 @@ function useSendMoney(input) {
|
|
|
2043
2078
|
const personalPay = useCapxulPay();
|
|
2044
2079
|
const organizationPay = useCapxulOrganizationPay(orgId);
|
|
2045
2080
|
const pay = actor.kind === "personal" ? personalPay : organizationPay;
|
|
2046
|
-
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";
|
|
2047
2082
|
const submitGate = useRef(false);
|
|
2048
2083
|
const submit = useCallback(() => {
|
|
2049
2084
|
if (blockedReason !== null || submitGate.current) return;
|
|
@@ -2130,7 +2165,7 @@ function useSendMoneyContext() {
|
|
|
2130
2165
|
if (engine === null) throw new Error("CapxulSendMoney parts must be used inside <CapxulSendMoney>");
|
|
2131
2166
|
return engine;
|
|
2132
2167
|
}
|
|
2133
|
-
function Root$
|
|
2168
|
+
function Root$5({ actor, onSent, onFailed, children }) {
|
|
2134
2169
|
const engine = useSendMoney({
|
|
2135
2170
|
actor,
|
|
2136
2171
|
onSent,
|
|
@@ -2157,7 +2192,7 @@ function Actions$1({ children }) {
|
|
|
2157
2192
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useSendMoneyContext().actions) });
|
|
2158
2193
|
}
|
|
2159
2194
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
2160
|
-
const CapxulSendMoney = Object.assign(Root$
|
|
2195
|
+
const CapxulSendMoney = Object.assign(Root$5, {
|
|
2161
2196
|
Source,
|
|
2162
2197
|
Asset: Asset$1,
|
|
2163
2198
|
Amount,
|
|
@@ -2247,7 +2282,7 @@ function useOrgMemberContext() {
|
|
|
2247
2282
|
if (state === null) throw new Error("CapxulOrgMember parts must be used inside <CapxulOrgMember>");
|
|
2248
2283
|
return state;
|
|
2249
2284
|
}
|
|
2250
|
-
function Root$
|
|
2285
|
+
function Root$4({ orgId, children }) {
|
|
2251
2286
|
const state = useOrgMember(orgId);
|
|
2252
2287
|
return /* @__PURE__ */ jsx(OrgMemberContext.Provider, {
|
|
2253
2288
|
value: state,
|
|
@@ -2267,7 +2302,7 @@ function Standing({ children }) {
|
|
|
2267
2302
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useOrgMemberContext().standing) });
|
|
2268
2303
|
}
|
|
2269
2304
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
2270
|
-
const CapxulOrgMember = Object.assign(Root$
|
|
2305
|
+
const CapxulOrgMember = Object.assign(Root$4, {
|
|
2271
2306
|
Can,
|
|
2272
2307
|
Standing
|
|
2273
2308
|
});
|
|
@@ -2307,18 +2342,14 @@ function isOrgLaneSettled(identity) {
|
|
|
2307
2342
|
if (identity.phase !== "authenticated" || identity.account.at !== "claimed") return true;
|
|
2308
2343
|
const org = identity.account.org;
|
|
2309
2344
|
if (org === null) return true;
|
|
2310
|
-
|
|
2311
|
-
case "ready":
|
|
2312
|
-
case "failed": return true;
|
|
2313
|
-
default: return false;
|
|
2314
|
-
}
|
|
2345
|
+
return org.observation === "connected" && org.setup !== null;
|
|
2315
2346
|
}
|
|
2316
2347
|
/** Exactly the Organization asked for, and ready to be used. */
|
|
2317
2348
|
function isExactOrgReady(lifecycle, orgId) {
|
|
2318
2349
|
return lifecycle?.status === "ready" && String(lifecycle.orgId) === orgId;
|
|
2319
2350
|
}
|
|
2320
2351
|
function isLifecycleSettled(lifecycle) {
|
|
2321
|
-
return lifecycle
|
|
2352
|
+
return lifecycle !== void 0;
|
|
2322
2353
|
}
|
|
2323
2354
|
function destinationOrNull(identity) {
|
|
2324
2355
|
return identity === null ? null : resolveIdentityDestination(identity);
|
|
@@ -2346,19 +2377,27 @@ function decidePhase(input) {
|
|
|
2346
2377
|
* Which probe produced the surfaced error — the SAME precedence `error` itself
|
|
2347
2378
|
* is chosen by, written once so the code and its source can never disagree.
|
|
2348
2379
|
*/
|
|
2349
|
-
function probeOf(session, membership, lifecycle) {
|
|
2380
|
+
function probeOf(session, membership, lifecycle, authority) {
|
|
2350
2381
|
if (session !== null) return "session";
|
|
2351
2382
|
if (membership !== null) return "membership";
|
|
2352
2383
|
if (lifecycle !== null) return "lifecycle";
|
|
2384
|
+
if (authority !== null) return "authority";
|
|
2353
2385
|
return null;
|
|
2354
2386
|
}
|
|
2355
2387
|
/** The engine — all the logic. NOT exported from the package barrel. */
|
|
2356
2388
|
function useDashboardAccess(scope) {
|
|
2357
2389
|
const client = useCapxulClientOrNull();
|
|
2390
|
+
const queryClient = useQueryClient();
|
|
2358
2391
|
const identity = useCapxulIdentityOrNull();
|
|
2359
2392
|
const destination = useMemo(() => destinationOrNull(identity), [identity]);
|
|
2360
2393
|
const orgId = scope.kind === "organization" ? scope.orgId : void 0;
|
|
2361
2394
|
const claimed = isClaimedOrFalse(identity);
|
|
2395
|
+
const scopeKey = `${orgId ?? ""}:${identity?.phase === "authenticated" ? identity.session.authUserId : ""}:${actingAccount(identity) ?? ""}`;
|
|
2396
|
+
const [observation, setObservation] = useState({
|
|
2397
|
+
scopeKey,
|
|
2398
|
+
status: "loading"
|
|
2399
|
+
});
|
|
2400
|
+
const [subscriptionRetry, setSubscriptionRetry] = useState(0);
|
|
2362
2401
|
const sessionError = useMemo(() => identity?.phase === "faulted" ? identityError(identity) : null, [identity]);
|
|
2363
2402
|
const wantsMemberships = orgId !== void 0 && claimed;
|
|
2364
2403
|
const memberships = useCapxulOrgs({ enabled: wantsMemberships });
|
|
@@ -2366,33 +2405,125 @@ function useDashboardAccess(scope) {
|
|
|
2366
2405
|
const membershipsSettled = !wantsMemberships || memberships.data !== void 0 || membershipError !== null;
|
|
2367
2406
|
const hasMembership = orgId !== void 0 && (memberships.data ?? []).some((organization) => String(organization.id) === orgId);
|
|
2368
2407
|
const wantsLifecycle = wantsMemberships && membershipsSettled && hasMembership;
|
|
2408
|
+
const setupKey = accountKey(capxulKeys.orgLifecycle(orgId), identity);
|
|
2369
2409
|
const lifecycle = useQuery({
|
|
2370
|
-
queryKey:
|
|
2410
|
+
queryKey: setupKey,
|
|
2371
2411
|
queryFn: async () => {
|
|
2372
|
-
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.
|
|
2412
|
+
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.organization.setup).org(orgId).setup.get());
|
|
2373
2413
|
},
|
|
2374
2414
|
enabled: wantsLifecycle && client !== null
|
|
2375
2415
|
});
|
|
2376
|
-
const
|
|
2377
|
-
|
|
2416
|
+
const meKey = capxulKeys.orgMe(orgId, actingAccount(identity));
|
|
2417
|
+
useEffect(() => {
|
|
2418
|
+
if (!wantsLifecycle || client === null || orgId === void 0) return;
|
|
2419
|
+
const controller = new AbortController();
|
|
2420
|
+
let closed = false;
|
|
2421
|
+
let stop = null;
|
|
2422
|
+
setObservation({
|
|
2423
|
+
scopeKey,
|
|
2424
|
+
status: "loading"
|
|
2425
|
+
});
|
|
2426
|
+
client.org(orgId).setup.subscribe((snapshot) => {
|
|
2427
|
+
if (closed) return;
|
|
2428
|
+
if (snapshot.status === "value") {
|
|
2429
|
+
setObservation({
|
|
2430
|
+
scopeKey,
|
|
2431
|
+
status: "value",
|
|
2432
|
+
value: snapshot.value
|
|
2433
|
+
});
|
|
2434
|
+
queryClient.setQueryData(setupKey, snapshot.value);
|
|
2435
|
+
} else if (snapshot.status === "error") setObservation({
|
|
2436
|
+
scopeKey,
|
|
2437
|
+
status: "error",
|
|
2438
|
+
error: snapshot.error
|
|
2439
|
+
});
|
|
2440
|
+
}, { signal: controller.signal }).then((result) => {
|
|
2441
|
+
if (!result.ok) {
|
|
2442
|
+
if (!closed) setObservation({
|
|
2443
|
+
scopeKey,
|
|
2444
|
+
status: "error",
|
|
2445
|
+
error: result.error
|
|
2446
|
+
});
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
stop = result.value;
|
|
2450
|
+
if (closed) stop();
|
|
2451
|
+
}).catch(() => {
|
|
2452
|
+
if (!closed) setObservation({
|
|
2453
|
+
scopeKey,
|
|
2454
|
+
status: "error",
|
|
2455
|
+
error: new CapxulError("UNKNOWN", "Organization setup subscription failed")
|
|
2456
|
+
});
|
|
2457
|
+
});
|
|
2458
|
+
return () => {
|
|
2459
|
+
closed = true;
|
|
2460
|
+
controller.abort();
|
|
2461
|
+
stop?.();
|
|
2462
|
+
};
|
|
2463
|
+
}, [
|
|
2464
|
+
client,
|
|
2465
|
+
orgId,
|
|
2466
|
+
wantsLifecycle,
|
|
2467
|
+
queryClient,
|
|
2468
|
+
scopeKey,
|
|
2469
|
+
subscriptionRetry
|
|
2470
|
+
]);
|
|
2471
|
+
const currentSetup = observation.scopeKey === scopeKey && observation.status === "value" ? observation.value : void 0;
|
|
2472
|
+
const wantsAuthority = wantsLifecycle && currentSetup?.status === "ready";
|
|
2473
|
+
const authority = useQuery({
|
|
2474
|
+
queryKey: meKey,
|
|
2475
|
+
queryFn: async () => {
|
|
2476
|
+
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.org.me).org(orgId).me());
|
|
2477
|
+
},
|
|
2478
|
+
enabled: wantsAuthority && client !== null
|
|
2479
|
+
});
|
|
2480
|
+
const [checkedAuthority, setCheckedAuthority] = useState(null);
|
|
2481
|
+
useEffect(() => {
|
|
2482
|
+
if (!wantsAuthority || currentSetup === void 0) return;
|
|
2483
|
+
let active = true;
|
|
2484
|
+
authority.refetch().then((result) => {
|
|
2485
|
+
if (active && result.isSuccess) setCheckedAuthority({
|
|
2486
|
+
scopeKey,
|
|
2487
|
+
setup: currentSetup
|
|
2488
|
+
});
|
|
2489
|
+
});
|
|
2490
|
+
return () => {
|
|
2491
|
+
active = false;
|
|
2492
|
+
};
|
|
2493
|
+
}, [
|
|
2494
|
+
scopeKey,
|
|
2495
|
+
currentSetup,
|
|
2496
|
+
wantsAuthority,
|
|
2497
|
+
authority.refetch
|
|
2498
|
+
]);
|
|
2499
|
+
const authorityCurrent = checkedAuthority?.scopeKey === scopeKey && checkedAuthority.setup === currentSetup && authority.data !== void 0;
|
|
2500
|
+
const observationError = observation.scopeKey === scopeKey && observation.status === "error" ? observation.error : void 0;
|
|
2501
|
+
const lifecycleError = wantsLifecycle ? observationError ?? lifecycle.error : null;
|
|
2502
|
+
const authorityError = wantsAuthority ? authority.error : null;
|
|
2503
|
+
const lifecycleSettled = !wantsLifecycle || lifecycleError !== null || authorityError !== null || isLifecycleSettled(currentSetup) && (!wantsAuthority || authorityCurrent);
|
|
2378
2504
|
const retry = useCallback(() => {
|
|
2379
2505
|
if (sessionError !== null && client !== null) client.auth.getSession();
|
|
2380
2506
|
if (membershipError !== null) memberships.refetch();
|
|
2507
|
+
if (observationError !== void 0) setSubscriptionRetry((attempt) => attempt + 1);
|
|
2381
2508
|
if (lifecycleError !== null) lifecycle.refetch();
|
|
2509
|
+
if (authorityError !== null) authority.refetch();
|
|
2382
2510
|
}, [
|
|
2383
2511
|
client,
|
|
2384
2512
|
sessionError,
|
|
2385
2513
|
membershipError,
|
|
2514
|
+
observationError,
|
|
2386
2515
|
lifecycleError,
|
|
2387
2516
|
memberships.refetch,
|
|
2388
|
-
lifecycle.refetch
|
|
2517
|
+
lifecycle.refetch,
|
|
2518
|
+
authorityError,
|
|
2519
|
+
authority.refetch
|
|
2389
2520
|
]);
|
|
2390
|
-
const error = sessionError ?? membershipError ?? lifecycleError;
|
|
2391
|
-
const probe = probeOf(sessionError, membershipError, lifecycleError);
|
|
2521
|
+
const error = sessionError ?? membershipError ?? lifecycleError ?? authorityError;
|
|
2522
|
+
const probe = probeOf(sessionError, membershipError, lifecycleError, authorityError);
|
|
2392
2523
|
const phase = decidePhase({
|
|
2393
2524
|
error,
|
|
2394
2525
|
settled: client !== null && identity !== null && !isRestoring$1(identity) && membershipsSettled && lifecycleSettled,
|
|
2395
|
-
granted: scope.kind === "personal" ? claimed : claimed && orgId !== void 0 && hasMembership && isExactOrgReady(
|
|
2526
|
+
granted: scope.kind === "personal" ? claimed : claimed && orgId !== void 0 && hasMembership && isExactOrgReady(currentSetup, orgId) && authorityCurrent,
|
|
2396
2527
|
orgLaneSettled: identity !== null && isOrgLaneSettled(identity),
|
|
2397
2528
|
destination
|
|
2398
2529
|
});
|
|
@@ -2427,7 +2558,7 @@ function usePart(phase) {
|
|
|
2427
2558
|
}, [placed, phase]);
|
|
2428
2559
|
return state.phase === phase ? state : null;
|
|
2429
2560
|
}
|
|
2430
|
-
function Root$
|
|
2561
|
+
function Root$3({ scope, children }) {
|
|
2431
2562
|
const state = useDashboardAccess(scope);
|
|
2432
2563
|
const [placed] = useState(() => /* @__PURE__ */ new Set());
|
|
2433
2564
|
const value = useMemo(() => ({
|
|
@@ -2445,7 +2576,7 @@ function Root$2({ scope, children }) {
|
|
|
2445
2576
|
});
|
|
2446
2577
|
}
|
|
2447
2578
|
/** Hands nothing — today's app renders `null` here, exactly as it does now. */
|
|
2448
|
-
function Checking({ children }) {
|
|
2579
|
+
function Checking$1({ children }) {
|
|
2449
2580
|
return usePart("checking") === null ? null : /* @__PURE__ */ jsx(Fragment, { children });
|
|
2450
2581
|
}
|
|
2451
2582
|
function DashboardError({ children }) {
|
|
@@ -2472,19 +2603,14 @@ function Granted({ children }) {
|
|
|
2472
2603
|
return usePart("granted") === null ? null : /* @__PURE__ */ jsx(Fragment, { children });
|
|
2473
2604
|
}
|
|
2474
2605
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
2475
|
-
const CapxulDashboardAccess = Object.assign(Root$
|
|
2476
|
-
Checking,
|
|
2606
|
+
const CapxulDashboardAccess = Object.assign(Root$3, {
|
|
2607
|
+
Checking: Checking$1,
|
|
2477
2608
|
Error: DashboardError,
|
|
2478
2609
|
Redirect,
|
|
2479
2610
|
Granted
|
|
2480
2611
|
});
|
|
2481
2612
|
//#endregion
|
|
2482
2613
|
//#region src/headless/payroll/money.ts
|
|
2483
|
-
/** `{ value: "12.34", decimals: 6 }` → `12340000n`. */
|
|
2484
|
-
function toMinorUnits(money) {
|
|
2485
|
-
const [whole = "0", fraction = ""] = money.value.split(".");
|
|
2486
|
-
return BigInt(`${whole}${fraction.padEnd(money.decimals, "0")}`);
|
|
2487
|
-
}
|
|
2488
2614
|
const TYPED_AMOUNT = /^(-?)(\d+)(?:\.(\d+))?$/;
|
|
2489
2615
|
/**
|
|
2490
2616
|
* Σ of the employer's TYPED amounts on a saved group — a DISPLAY total.
|
|
@@ -2517,6 +2643,17 @@ function fromMinorUnits(raw, asset) {
|
|
|
2517
2643
|
decimals: asset.decimals
|
|
2518
2644
|
};
|
|
2519
2645
|
}
|
|
2646
|
+
/** Convert exact raw units to one asset quantity without a fiat denomination. */
|
|
2647
|
+
function assetFromMinorUnits(raw, asset) {
|
|
2648
|
+
const money = fromMinorUnits(raw, {
|
|
2649
|
+
currency: "USD",
|
|
2650
|
+
decimals: asset.decimals
|
|
2651
|
+
});
|
|
2652
|
+
return {
|
|
2653
|
+
asset: asset.asset,
|
|
2654
|
+
value: money.value
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2520
2657
|
//#endregion
|
|
2521
2658
|
//#region src/headless/payroll/reads.ts
|
|
2522
2659
|
function toRecipientOption(entry) {
|
|
@@ -2633,7 +2770,7 @@ function scopedClient(client, method, orgId) {
|
|
|
2633
2770
|
//#endregion
|
|
2634
2771
|
//#region src/headless/payroll/use-payroll.ts
|
|
2635
2772
|
/** Money reads the same way everywhere on this screen: amount then code. */
|
|
2636
|
-
const GRAMMAR
|
|
2773
|
+
const GRAMMAR = { grammar: "code" };
|
|
2637
2774
|
/**
|
|
2638
2775
|
* One captured snapshot, or nothing — never a fabricated empty options object,
|
|
2639
2776
|
* and never a DOM event a screen wired straight to `onClick`.
|
|
@@ -2649,7 +2786,7 @@ function toPayrollGroupRow(group) {
|
|
|
2649
2786
|
name: group.name,
|
|
2650
2787
|
tone: group.tone,
|
|
2651
2788
|
memberCount: group.members.length,
|
|
2652
|
-
totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR
|
|
2789
|
+
totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR),
|
|
2653
2790
|
members: group.members.map((member) => ({
|
|
2654
2791
|
recipientId: member.partyId,
|
|
2655
2792
|
amount: member.amount,
|
|
@@ -2739,8 +2876,8 @@ function usePayroll(orgId) {
|
|
|
2739
2876
|
const started = orgId !== void 0;
|
|
2740
2877
|
return {
|
|
2741
2878
|
summary: {
|
|
2742
|
-
walletBalanceDisplay: treasury.data === void 0 ? null : formatMoney(treasury.data.available, GRAMMAR
|
|
2743
|
-
paidThisMonthDisplay: runs.data === void 0 ? null :
|
|
2879
|
+
walletBalanceDisplay: treasury.data?.available === void 0 || treasury.data.available === null ? null : formatMoney(treasury.data.available, GRAMMAR),
|
|
2880
|
+
paidThisMonthDisplay: runs.data === void 0 ? null : runs.data.settledThisMonth.map(formatAssetAmount).join(", "),
|
|
2744
2881
|
isLoading: started && (treasury.data === void 0 && treasury.error === null || runs.data === void 0 && runs.error === null),
|
|
2745
2882
|
error: treasury.error ?? runs.error
|
|
2746
2883
|
},
|
|
@@ -2773,7 +2910,7 @@ function usePayrollContext() {
|
|
|
2773
2910
|
if (engine === null) throw new Error("CapxulPayroll parts must be used inside <CapxulPayroll>");
|
|
2774
2911
|
return engine;
|
|
2775
2912
|
}
|
|
2776
|
-
function Root$
|
|
2913
|
+
function Root$2({ orgId, children }) {
|
|
2777
2914
|
const engine = usePayroll(orgId);
|
|
2778
2915
|
return /* @__PURE__ */ jsx(PayrollContext.Provider, {
|
|
2779
2916
|
value: engine,
|
|
@@ -2791,7 +2928,7 @@ function Groups({ children }) {
|
|
|
2791
2928
|
* There is NO `.Runs` part (ruling Q6): no shipped region has the right shape
|
|
2792
2929
|
* for a run list, and a part without a production caller may not ship.
|
|
2793
2930
|
*/
|
|
2794
|
-
const CapxulPayroll = Object.assign(Root$
|
|
2931
|
+
const CapxulPayroll = Object.assign(Root$2, {
|
|
2795
2932
|
Summary: Summary$1,
|
|
2796
2933
|
Groups
|
|
2797
2934
|
});
|
|
@@ -2801,8 +2938,14 @@ function record(value) {
|
|
|
2801
2938
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw Errors.invalidInput("payrollDraft", "Invalid run draft");
|
|
2802
2939
|
return value;
|
|
2803
2940
|
}
|
|
2941
|
+
/**
|
|
2942
|
+
* A draft-scoped opaque key. `/` is in the class so `asset.id` can be the
|
|
2943
|
+
* canonical CAIP-19 `AssetId` (`eip155:<chainId>/erc20:<address>`): the draft
|
|
2944
|
+
* records WHICH asset was authorized, and a symbol is not an identity
|
|
2945
|
+
* (ADR-0028 R3). The field never reaches the backend — only `input` is sent.
|
|
2946
|
+
*/
|
|
2804
2947
|
function identifier(value) {
|
|
2805
|
-
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._
|
|
2948
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u.test(value)) throw Errors.invalidInput("payrollDraft", "Invalid draft identifier");
|
|
2806
2949
|
return value;
|
|
2807
2950
|
}
|
|
2808
2951
|
function minorAmount(value) {
|
|
@@ -2818,8 +2961,9 @@ function decodeDraft(value, scope) {
|
|
|
2818
2961
|
const items = input.items.map((raw) => {
|
|
2819
2962
|
const item = record(raw);
|
|
2820
2963
|
const to = record(item.to);
|
|
2964
|
+
const amount = record(item.amount);
|
|
2821
2965
|
const partyId = toPartyId(identifier(item.partyId));
|
|
2822
|
-
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");
|
|
2966
|
+
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");
|
|
2823
2967
|
const net = minorAmount(item.net);
|
|
2824
2968
|
return {
|
|
2825
2969
|
to: {
|
|
@@ -2827,12 +2971,20 @@ function decodeDraft(value, scope) {
|
|
|
2827
2971
|
partyId
|
|
2828
2972
|
},
|
|
2829
2973
|
partyId,
|
|
2974
|
+
amount: {
|
|
2975
|
+
asset: requireAsset(amount.asset).assetId,
|
|
2976
|
+
value: amount.value
|
|
2977
|
+
},
|
|
2830
2978
|
gross: net,
|
|
2831
2979
|
net,
|
|
2980
|
+
currency: item.currency,
|
|
2981
|
+
decimals: item.decimals,
|
|
2832
2982
|
adjustments: []
|
|
2833
2983
|
};
|
|
2834
2984
|
});
|
|
2835
2985
|
if (new Set(items.map((item) => item.partyId)).size !== items.length) throw Errors.invalidInput("payrollDraft", "Duplicate draft recipient");
|
|
2986
|
+
const exactAsset = requireAsset(identifier(asset.id));
|
|
2987
|
+
if (items.some((item) => item.amount.asset !== exactAsset.assetId)) throw Errors.invalidInput("payrollDraft", "Draft item asset does not match the run asset");
|
|
2836
2988
|
return {
|
|
2837
2989
|
version: 1,
|
|
2838
2990
|
scope,
|
|
@@ -2843,6 +2995,7 @@ function decodeDraft(value, scope) {
|
|
|
2843
2995
|
},
|
|
2844
2996
|
input: {
|
|
2845
2997
|
permissionId: identifier(input.permissionId),
|
|
2998
|
+
asset: exactAsset.assetId,
|
|
2846
2999
|
period: {
|
|
2847
3000
|
start: period.start,
|
|
2848
3001
|
end: period.start
|
|
@@ -3046,7 +3199,6 @@ function termsSeeds(terms, asset, period) {
|
|
|
3046
3199
|
}
|
|
3047
3200
|
//#endregion
|
|
3048
3201
|
//#region src/headless/payroll/use-payroll-run.ts
|
|
3049
|
-
const GRAMMAR = { grammar: "code" };
|
|
3050
3202
|
/**
|
|
3051
3203
|
* The period this composer composes: an INSTANT run (ruling Q3). `authorizeRun`
|
|
3052
3204
|
* stamps `start === end === runAt` at submit, and the payslip's period renders
|
|
@@ -3070,6 +3222,32 @@ const INSTANT_RUN_PERIOD = {
|
|
|
3070
3222
|
function amountText(amounts, seeds, recipientId) {
|
|
3071
3223
|
return amounts[recipientId]?.value ?? seeds.get(recipientId) ?? "";
|
|
3072
3224
|
}
|
|
3225
|
+
function freshRunItems(input) {
|
|
3226
|
+
const items = [];
|
|
3227
|
+
for (const [index, recipientId] of input.selected.entries()) {
|
|
3228
|
+
const to = input.payableRefs[index];
|
|
3229
|
+
const amount = input.amounts[index];
|
|
3230
|
+
const terms = input.compensationTerms.get(recipientId);
|
|
3231
|
+
if (to === void 0 || amount === void 0 || terms === void 0) throw Errors.unknown();
|
|
3232
|
+
items.push({
|
|
3233
|
+
to: {
|
|
3234
|
+
kind: "party",
|
|
3235
|
+
partyId: toPartyId(recipientId)
|
|
3236
|
+
},
|
|
3237
|
+
partyId: recipientId,
|
|
3238
|
+
amount,
|
|
3239
|
+
gross: terms.rate,
|
|
3240
|
+
net: terms.rate,
|
|
3241
|
+
currency: terms.currency,
|
|
3242
|
+
decimals: terms.decimals,
|
|
3243
|
+
adjustments: []
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
return items;
|
|
3247
|
+
}
|
|
3248
|
+
function hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, terms) {
|
|
3249
|
+
return frozen || !writerNamesSelectedAsset || selected.every((recipientId) => terms.has(recipientId));
|
|
3250
|
+
}
|
|
3073
3251
|
/**
|
|
3074
3252
|
* WHICH run the composer is composing, by MEANING rather than by object shape.
|
|
3075
3253
|
*
|
|
@@ -3101,6 +3279,33 @@ function initialAmounts(prefill) {
|
|
|
3101
3279
|
};
|
|
3102
3280
|
return seeded;
|
|
3103
3281
|
}
|
|
3282
|
+
/**
|
|
3283
|
+
* One `.Asset` option per position the treasury Account holds, taken from
|
|
3284
|
+
* `account.balances` — never from `account.available`, which is the Account's
|
|
3285
|
+
* fiat VALUATION of those positions (ADR-0028 R5/R6). The option is keyed on
|
|
3286
|
+
* the ASSET id: several positions share one account id, and a symbol is display
|
|
3287
|
+
* only (ADR-0028 R3). Every row is listed, so a real multi-position treasury
|
|
3288
|
+
* still offers its holdings.
|
|
3289
|
+
*/
|
|
3290
|
+
function assetSelectionsFromAccount(account) {
|
|
3291
|
+
if (account === null) return [];
|
|
3292
|
+
return account.balances.map((position) => {
|
|
3293
|
+
return {
|
|
3294
|
+
option: {
|
|
3295
|
+
id: position.asset,
|
|
3296
|
+
symbol: position.symbol,
|
|
3297
|
+
availableDisplay: formatAssetAmount({
|
|
3298
|
+
asset: position.asset,
|
|
3299
|
+
value: position.available
|
|
3300
|
+
})
|
|
3301
|
+
},
|
|
3302
|
+
money: {
|
|
3303
|
+
asset: position.asset,
|
|
3304
|
+
value: position.available
|
|
3305
|
+
}
|
|
3306
|
+
};
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3104
3309
|
function usePayrollRun(input) {
|
|
3105
3310
|
const { orgId, prefill, onRun, onFailed } = input;
|
|
3106
3311
|
const client = useCapxulClientOrNull();
|
|
@@ -3114,17 +3319,13 @@ function usePayrollRun(input) {
|
|
|
3114
3319
|
const terms = useOrgTerms(orgId);
|
|
3115
3320
|
const [selected, setSelected] = useState(() => prefill?.recipientIds ?? []);
|
|
3116
3321
|
const [amounts, setAmounts] = useState(() => initialAmounts(prefill));
|
|
3117
|
-
const restoreCurrency = treasury.data?.available.currency;
|
|
3118
3322
|
const { draftScope, currentScope, draft, draftError, restorationPending, activateDraft, restoreDraft, setDraft, recovery, draftBlocked } = useRunDraft(client, orgId, useCallback((restored) => {
|
|
3119
3323
|
setSelected(restored.input.items.map((item) => item.partyId));
|
|
3120
|
-
if (
|
|
3121
|
-
value:
|
|
3122
|
-
currency: restoreCurrency,
|
|
3123
|
-
decimals: restored.asset.decimals
|
|
3124
|
-
}).value,
|
|
3324
|
+
if (requireAsset(restored.input.asset).assetId === restored.input.asset) setAmounts(Object.fromEntries(restored.input.items.map((item) => [item.partyId, {
|
|
3325
|
+
value: item.amount.value,
|
|
3125
3326
|
prefilled: false
|
|
3126
3327
|
}])));
|
|
3127
|
-
}, [
|
|
3328
|
+
}, []));
|
|
3128
3329
|
const frozen = draft !== null;
|
|
3129
3330
|
const composerKey = composerIdentity(orgId, prefill, draftScope, frozen);
|
|
3130
3331
|
const [seededKey, setSeededKey] = useState(composerKey);
|
|
@@ -3164,11 +3365,8 @@ function usePayrollRun(input) {
|
|
|
3164
3365
|
draftError
|
|
3165
3366
|
]);
|
|
3166
3367
|
const account = treasury.data ?? null;
|
|
3167
|
-
const
|
|
3168
|
-
|
|
3169
|
-
symbol: account.available.currency,
|
|
3170
|
-
availableDisplay: formatMoney(account.available, GRAMMAR)
|
|
3171
|
-
}], [account]);
|
|
3368
|
+
const assetSelections = useMemo(() => assetSelectionsFromAccount(account), [account]);
|
|
3369
|
+
const assetOptions = useMemo(() => assetSelections.map((selection) => selection.option), [assetSelections]);
|
|
3172
3370
|
const [selectedAssetId, setSelectedAssetId] = useState(null);
|
|
3173
3371
|
useEffect(() => {
|
|
3174
3372
|
if (!assetOptions.some((option) => option.id === selectedAssetId)) setSelectedAssetId(assetOptions[0]?.id ?? null);
|
|
@@ -3177,7 +3375,7 @@ function usePayrollRun(input) {
|
|
|
3177
3375
|
const selectAsset = useCallback((id) => {
|
|
3178
3376
|
if (!submitGate.current && !frozen) setSelectedAssetId((current) => assetOptions.some((option) => option.id === id) ? id : current);
|
|
3179
3377
|
}, [assetOptions, frozen]);
|
|
3180
|
-
const assetMoney =
|
|
3378
|
+
const assetMoney = assetSelections.find((selection) => selection.option.id === selectedAsset?.id)?.money ?? null;
|
|
3181
3379
|
const named = useMemo(() => new Map(roster.options.map((option) => [option.id, option])), [roster.options]);
|
|
3182
3380
|
const refs = roster.refs;
|
|
3183
3381
|
const payableRefs = useMemo(() => {
|
|
@@ -3189,12 +3387,19 @@ function usePayrollRun(input) {
|
|
|
3189
3387
|
}
|
|
3190
3388
|
return resolved;
|
|
3191
3389
|
}, [refs, selected]);
|
|
3192
|
-
const seeds = useMemo(() =>
|
|
3390
|
+
const seeds = useMemo(() => {
|
|
3391
|
+
if (assetMoney === null) return /* @__PURE__ */ new Map();
|
|
3392
|
+
const metadata = requireAsset(assetMoney.asset);
|
|
3393
|
+
return termsSeeds(terms.data ?? [], metadata.peg === null ? null : {
|
|
3394
|
+
currency: metadata.peg,
|
|
3395
|
+
decimals: metadata.decimals
|
|
3396
|
+
}, INSTANT_RUN_PERIOD);
|
|
3397
|
+
}, [assetMoney, terms.data]);
|
|
3193
3398
|
const entries = useMemo(() => {
|
|
3194
3399
|
if (assetMoney === null) return [];
|
|
3195
3400
|
return selected.map((recipientId) => {
|
|
3196
3401
|
const value = amountText(amounts, seeds, recipientId);
|
|
3197
|
-
const parsed = value.trim() === "" ? null :
|
|
3402
|
+
const parsed = value.trim() === "" ? null : parseAssetAmount(value, requireAsset(assetMoney.asset));
|
|
3198
3403
|
return {
|
|
3199
3404
|
recipientId,
|
|
3200
3405
|
name: named.get(recipientId)?.name ?? recipientId,
|
|
@@ -3202,7 +3407,7 @@ function usePayrollRun(input) {
|
|
|
3202
3407
|
change: (text) => change(recipientId, text),
|
|
3203
3408
|
error: parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null,
|
|
3204
3409
|
prefilled: amounts[recipientId]?.prefilled ?? seeds.has(recipientId),
|
|
3205
|
-
currency: assetMoney.
|
|
3410
|
+
currency: requireAsset(assetMoney.asset).symbol
|
|
3206
3411
|
};
|
|
3207
3412
|
});
|
|
3208
3413
|
}, [
|
|
@@ -3217,7 +3422,7 @@ function usePayrollRun(input) {
|
|
|
3217
3422
|
if (assetMoney === null || selected.length === 0) return null;
|
|
3218
3423
|
const money = [];
|
|
3219
3424
|
for (const recipientId of selected) {
|
|
3220
|
-
const parsed =
|
|
3425
|
+
const parsed = parseAssetAmount(amountText(amounts, seeds, recipientId), requireAsset(assetMoney.asset));
|
|
3221
3426
|
if (isMoneyParseError(parsed)) return null;
|
|
3222
3427
|
money.push(parsed);
|
|
3223
3428
|
}
|
|
@@ -3228,16 +3433,25 @@ function usePayrollRun(input) {
|
|
|
3228
3433
|
seeds,
|
|
3229
3434
|
selected
|
|
3230
3435
|
]);
|
|
3231
|
-
const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null :
|
|
3232
|
-
|
|
3233
|
-
|
|
3436
|
+
const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null : {
|
|
3437
|
+
asset: assetMoney.asset,
|
|
3438
|
+
value: assetFromMinorUnits(parsedAmounts.reduce((sum, amount) => sum + toRaw(amount, requireAsset(amount.asset)), 0n), {
|
|
3439
|
+
asset: assetMoney.asset,
|
|
3440
|
+
decimals: requireAsset(assetMoney.asset).decimals
|
|
3441
|
+
}).value
|
|
3442
|
+
}, [assetMoney, parsedAmounts]);
|
|
3443
|
+
const availableRaw = assetMoney === null ? null : toRaw(assetMoney, requireAsset(assetMoney.asset));
|
|
3444
|
+
const totalRaw = total === null ? null : toRaw(total, requireAsset(total.asset));
|
|
3234
3445
|
const funds = availableRaw === null || totalRaw === null ? "unknown" : totalRaw > availableRaw ? "insufficient" : "available";
|
|
3235
3446
|
const budgets = me.data?.budgets ?? [];
|
|
3447
|
+
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]);
|
|
3448
|
+
const writerNamesSelectedAsset = writeNamesAsset();
|
|
3236
3449
|
const blockedReason = firstFalseFact({
|
|
3237
3450
|
signerReady: signerStatus === "ready",
|
|
3238
3451
|
...runAuthorityFacts(draft, budgets, selectedAsset, assetMoney, funds),
|
|
3239
3452
|
recipientCount: payableRefs === null ? 0 : selected.length,
|
|
3240
|
-
amountsParse: total !== null
|
|
3453
|
+
amountsParse: total !== null && hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, compensationTerms),
|
|
3454
|
+
writeNamesAsset: writerNamesSelectedAsset
|
|
3241
3455
|
});
|
|
3242
3456
|
const authorize = useMutation({
|
|
3243
3457
|
mutationFn: async ({ candidate, options }) => {
|
|
@@ -3259,7 +3473,7 @@ function usePayrollRun(input) {
|
|
|
3259
3473
|
if (candidate.scope === currentScope.current) restoreDraft(candidate.scope);
|
|
3260
3474
|
}
|
|
3261
3475
|
});
|
|
3262
|
-
const totalDisplay = total === null ? null :
|
|
3476
|
+
const totalDisplay = total === null ? null : formatAssetAmount(total);
|
|
3263
3477
|
const authorizeMutate = authorize.mutate;
|
|
3264
3478
|
const submit = useCallback((options) => {
|
|
3265
3479
|
const attempt = toInvocationOptions(options);
|
|
@@ -3268,41 +3482,35 @@ function usePayrollRun(input) {
|
|
|
3268
3482
|
if (permissionId === void 0 || totalDisplay === null || parsedAmounts === null) return;
|
|
3269
3483
|
if (payableRefs === null) return;
|
|
3270
3484
|
const recipientCount = selected.length;
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
const
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
kind: "party",
|
|
3280
|
-
partyId: toPartyId(recipientId)
|
|
3281
|
-
},
|
|
3282
|
-
partyId: recipientId,
|
|
3283
|
-
gross: raw,
|
|
3284
|
-
net: raw,
|
|
3285
|
-
adjustments: []
|
|
3485
|
+
let candidate;
|
|
3486
|
+
if (frozen) candidate = draft;
|
|
3487
|
+
else {
|
|
3488
|
+
const items = freshRunItems({
|
|
3489
|
+
selected,
|
|
3490
|
+
payableRefs,
|
|
3491
|
+
amounts: parsedAmounts,
|
|
3492
|
+
compensationTerms
|
|
3286
3493
|
});
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
decimals: assetMoney.decimals
|
|
3296
|
-
},
|
|
3297
|
-
input: {
|
|
3298
|
-
permissionId,
|
|
3299
|
-
period: {
|
|
3300
|
-
start: runAt,
|
|
3301
|
-
end: runAt
|
|
3494
|
+
const runAt = Date.now();
|
|
3495
|
+
candidate = {
|
|
3496
|
+
version: 1,
|
|
3497
|
+
scope: draftScope,
|
|
3498
|
+
asset: {
|
|
3499
|
+
id: selectedAsset.id,
|
|
3500
|
+
currency: requireAsset(assetMoney.asset).symbol,
|
|
3501
|
+
decimals: requireAsset(assetMoney.asset).decimals
|
|
3302
3502
|
},
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3503
|
+
input: {
|
|
3504
|
+
permissionId,
|
|
3505
|
+
asset: selectedAsset.id,
|
|
3506
|
+
period: {
|
|
3507
|
+
start: runAt,
|
|
3508
|
+
end: runAt
|
|
3509
|
+
},
|
|
3510
|
+
items
|
|
3511
|
+
}
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3306
3514
|
submitGate.current = true;
|
|
3307
3515
|
authorizeMutate({
|
|
3308
3516
|
candidate,
|
|
@@ -3334,6 +3542,7 @@ function usePayrollRun(input) {
|
|
|
3334
3542
|
selectedAsset,
|
|
3335
3543
|
blockedReason,
|
|
3336
3544
|
budgets,
|
|
3545
|
+
compensationTerms,
|
|
3337
3546
|
onFailed,
|
|
3338
3547
|
onRun,
|
|
3339
3548
|
parsedAmounts,
|
|
@@ -3382,18 +3591,25 @@ function usePayrollRun(input) {
|
|
|
3382
3591
|
};
|
|
3383
3592
|
}
|
|
3384
3593
|
function remainingBalanceDisplay(available, total, asset) {
|
|
3385
|
-
|
|
3594
|
+
if (available === null || total === null || asset === null) return null;
|
|
3595
|
+
const remaining = available - total;
|
|
3596
|
+
const magnitude = remaining < 0n ? -remaining : remaining;
|
|
3597
|
+
const formatted = formatAssetAmount(assetFromMinorUnits(magnitude, {
|
|
3598
|
+
asset: asset.asset,
|
|
3599
|
+
decimals: requireAsset(asset.asset).decimals
|
|
3600
|
+
}));
|
|
3601
|
+
return remaining < 0n ? `-${formatted}` : formatted;
|
|
3386
3602
|
}
|
|
3387
3603
|
/** Persisted replay keeps its authorized Budget and asset; a prior debit cannot block its own recovery. */
|
|
3388
3604
|
function runAuthorityFacts(draft, budgets, asset, money, funds) {
|
|
3389
3605
|
return {
|
|
3390
3606
|
budgetCount: draft !== null && !budgets.some((budget) => budget.id === draft.input.permissionId) ? 0 : budgets.length,
|
|
3391
|
-
hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.asset
|
|
3607
|
+
hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.input.asset === money?.asset),
|
|
3392
3608
|
funds: draft?.input.requestKey === void 0 ? funds : "available"
|
|
3393
3609
|
};
|
|
3394
3610
|
}
|
|
3395
3611
|
/**
|
|
3396
|
-
* The
|
|
3612
|
+
* The facts, in the ONE order the contract fixes. Exactly one code comes
|
|
3397
3613
|
* out, and it is always the first thing that is false — so two problems never
|
|
3398
3614
|
* race to name the button.
|
|
3399
3615
|
*/
|
|
@@ -3404,7 +3620,8 @@ function firstFalseFact(facts) {
|
|
|
3404
3620
|
if (!facts.hasAsset) return "no-asset";
|
|
3405
3621
|
if (facts.recipientCount === 0) return "no-recipients";
|
|
3406
3622
|
if (!facts.amountsParse) return "amount-invalid";
|
|
3407
|
-
|
|
3623
|
+
if (facts.funds === "insufficient") return "insufficient-funds";
|
|
3624
|
+
return facts.writeNamesAsset ? null : "asset-unnamed";
|
|
3408
3625
|
}
|
|
3409
3626
|
//#endregion
|
|
3410
3627
|
//#region src/headless/payroll/payroll-run.tsx
|
|
@@ -3414,7 +3631,7 @@ function usePayrollRunContext() {
|
|
|
3414
3631
|
if (engine === null) throw new Error("CapxulPayrollRun parts must be used inside <CapxulPayrollRun>");
|
|
3415
3632
|
return engine;
|
|
3416
3633
|
}
|
|
3417
|
-
function Root({ orgId, prefill, onRun, onFailed, children }) {
|
|
3634
|
+
function Root$1({ orgId, prefill, onRun, onFailed, children }) {
|
|
3418
3635
|
const engine = usePayrollRun({
|
|
3419
3636
|
orgId,
|
|
3420
3637
|
prefill,
|
|
@@ -3444,7 +3661,7 @@ function Actions({ children }) {
|
|
|
3444
3661
|
return /* @__PURE__ */ jsx(Fragment, { children: children(engine.actions) });
|
|
3445
3662
|
}
|
|
3446
3663
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
3447
|
-
const CapxulPayrollRun = Object.assign(Root, {
|
|
3664
|
+
const CapxulPayrollRun = Object.assign(Root$1, {
|
|
3448
3665
|
Recipients,
|
|
3449
3666
|
Amounts,
|
|
3450
3667
|
Asset,
|
|
@@ -3452,4 +3669,230 @@ const CapxulPayrollRun = Object.assign(Root, {
|
|
|
3452
3669
|
Actions
|
|
3453
3670
|
});
|
|
3454
3671
|
//#endregion
|
|
3455
|
-
|
|
3672
|
+
//#region src/headless/client-connection/use-client-connection.ts
|
|
3673
|
+
function useClientConnection(input) {
|
|
3674
|
+
const [view, setView] = useState(null);
|
|
3675
|
+
const [pending, setPending] = useState(false);
|
|
3676
|
+
const [error, setError] = useState(null);
|
|
3677
|
+
const [resendReady, setResendReady] = useState(false);
|
|
3678
|
+
const identityRef = useRef({
|
|
3679
|
+
client: input.client,
|
|
3680
|
+
requestId: input.requestId
|
|
3681
|
+
});
|
|
3682
|
+
identityRef.current = {
|
|
3683
|
+
client: input.client,
|
|
3684
|
+
requestId: input.requestId
|
|
3685
|
+
};
|
|
3686
|
+
const belongsHere = useCallback((client, requestId) => identityRef.current.client === client && identityRef.current.requestId === requestId, []);
|
|
3687
|
+
const refresh = useCallback(async (signal) => {
|
|
3688
|
+
const { client, requestId } = identityRef.current;
|
|
3689
|
+
const result = await client.auth.clientRequests.read({ requestId }, signal === void 0 ? void 0 : { signal });
|
|
3690
|
+
if (signal?.aborted || !belongsHere(client, requestId)) return;
|
|
3691
|
+
if (result.ok) {
|
|
3692
|
+
setView(result.value);
|
|
3693
|
+
setError(null);
|
|
3694
|
+
return;
|
|
3695
|
+
}
|
|
3696
|
+
setError(result.error);
|
|
3697
|
+
}, [belongsHere]);
|
|
3698
|
+
useEffect(() => {
|
|
3699
|
+
setView(null);
|
|
3700
|
+
setError(null);
|
|
3701
|
+
setPending(false);
|
|
3702
|
+
setResendReady(false);
|
|
3703
|
+
const controller = new AbortController();
|
|
3704
|
+
refresh(controller.signal);
|
|
3705
|
+
return () => {
|
|
3706
|
+
controller.abort();
|
|
3707
|
+
};
|
|
3708
|
+
}, [
|
|
3709
|
+
input.client,
|
|
3710
|
+
input.requestId,
|
|
3711
|
+
refresh
|
|
3712
|
+
]);
|
|
3713
|
+
useEffect(() => {
|
|
3714
|
+
if (view?.phase !== "otp_required") {
|
|
3715
|
+
setResendReady(false);
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
const remaining = view.resendAt - Date.now();
|
|
3719
|
+
if (remaining <= 0) {
|
|
3720
|
+
setResendReady(true);
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
setResendReady(false);
|
|
3724
|
+
const timer = setTimeout(() => setResendReady(true), remaining);
|
|
3725
|
+
return () => clearTimeout(timer);
|
|
3726
|
+
}, [view]);
|
|
3727
|
+
const run = useCallback(async (work) => {
|
|
3728
|
+
const { client, requestId } = identityRef.current;
|
|
3729
|
+
setPending(true);
|
|
3730
|
+
try {
|
|
3731
|
+
const result = await work();
|
|
3732
|
+
if (!belongsHere(client, requestId)) return;
|
|
3733
|
+
if (!result.ok) {
|
|
3734
|
+
setError(result.error);
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3737
|
+
await refresh();
|
|
3738
|
+
} finally {
|
|
3739
|
+
if (belongsHere(client, requestId)) setPending(false);
|
|
3740
|
+
}
|
|
3741
|
+
}, [belongsHere, refresh]);
|
|
3742
|
+
return useMemo(() => {
|
|
3743
|
+
if (error !== null && (view === null || view.phase !== "refused")) return {
|
|
3744
|
+
phase: "failure",
|
|
3745
|
+
otp: null,
|
|
3746
|
+
consent: null,
|
|
3747
|
+
complete: null,
|
|
3748
|
+
failure: {
|
|
3749
|
+
code: error.code,
|
|
3750
|
+
retry: () => refresh(),
|
|
3751
|
+
cancel: async () => {
|
|
3752
|
+
if (view === null) return;
|
|
3753
|
+
const { client, requestId } = identityRef.current;
|
|
3754
|
+
await run(() => client.auth.clientRequests.cancel({
|
|
3755
|
+
requestId,
|
|
3756
|
+
revision: view.revision
|
|
3757
|
+
}));
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
};
|
|
3761
|
+
if (view === null) return {
|
|
3762
|
+
phase: "checking",
|
|
3763
|
+
otp: null,
|
|
3764
|
+
consent: null,
|
|
3765
|
+
complete: null,
|
|
3766
|
+
failure: null
|
|
3767
|
+
};
|
|
3768
|
+
if (view.phase === "otp_required") return {
|
|
3769
|
+
phase: view.phase,
|
|
3770
|
+
otp: {
|
|
3771
|
+
email: view.email,
|
|
3772
|
+
pending,
|
|
3773
|
+
canResend: resendReady,
|
|
3774
|
+
verifyCode: async (code) => {
|
|
3775
|
+
const { client, requestId } = identityRef.current;
|
|
3776
|
+
await run(() => client.auth.clientRequests.verifyOtp({
|
|
3777
|
+
requestId,
|
|
3778
|
+
code
|
|
3779
|
+
}));
|
|
3780
|
+
},
|
|
3781
|
+
resend: async () => {
|
|
3782
|
+
const { client, requestId } = identityRef.current;
|
|
3783
|
+
await run(() => client.auth.clientRequests.sendOtp({ requestId }));
|
|
3784
|
+
}
|
|
3785
|
+
},
|
|
3786
|
+
consent: null,
|
|
3787
|
+
complete: null,
|
|
3788
|
+
failure: null
|
|
3789
|
+
};
|
|
3790
|
+
if (view.phase === "review") return {
|
|
3791
|
+
phase: view.phase,
|
|
3792
|
+
otp: null,
|
|
3793
|
+
consent: {
|
|
3794
|
+
verifiedEmail: view.email,
|
|
3795
|
+
clientLabel: view.clientLabel,
|
|
3796
|
+
scopeCodes: view.scopes,
|
|
3797
|
+
purpose: view.intent.mode,
|
|
3798
|
+
setupState: view.setupState,
|
|
3799
|
+
pending,
|
|
3800
|
+
accept: async () => {
|
|
3801
|
+
const { client, requestId } = identityRef.current;
|
|
3802
|
+
await run(() => client.auth.clientRequests.accept({
|
|
3803
|
+
requestId,
|
|
3804
|
+
revision: view.revision
|
|
3805
|
+
}));
|
|
3806
|
+
},
|
|
3807
|
+
cancel: async () => {
|
|
3808
|
+
const { client, requestId } = identityRef.current;
|
|
3809
|
+
await run(() => client.auth.clientRequests.cancel({
|
|
3810
|
+
requestId,
|
|
3811
|
+
revision: view.revision
|
|
3812
|
+
}));
|
|
3813
|
+
}
|
|
3814
|
+
},
|
|
3815
|
+
complete: null,
|
|
3816
|
+
failure: null
|
|
3817
|
+
};
|
|
3818
|
+
if (view.phase === "connected") return {
|
|
3819
|
+
phase: view.phase,
|
|
3820
|
+
otp: null,
|
|
3821
|
+
consent: null,
|
|
3822
|
+
complete: { outcome: "connected" },
|
|
3823
|
+
failure: null
|
|
3824
|
+
};
|
|
3825
|
+
if (view.phase === "refused") return {
|
|
3826
|
+
phase: view.phase,
|
|
3827
|
+
otp: null,
|
|
3828
|
+
consent: null,
|
|
3829
|
+
complete: null,
|
|
3830
|
+
failure: {
|
|
3831
|
+
code: view.code,
|
|
3832
|
+
...view.field === void 0 ? {} : { field: view.field },
|
|
3833
|
+
retry: () => refresh(),
|
|
3834
|
+
cancel: async () => void 0
|
|
3835
|
+
}
|
|
3836
|
+
};
|
|
3837
|
+
return {
|
|
3838
|
+
phase: view.phase,
|
|
3839
|
+
otp: null,
|
|
3840
|
+
consent: null,
|
|
3841
|
+
complete: null,
|
|
3842
|
+
failure: null
|
|
3843
|
+
};
|
|
3844
|
+
}, [
|
|
3845
|
+
error,
|
|
3846
|
+
pending,
|
|
3847
|
+
refresh,
|
|
3848
|
+
resendReady,
|
|
3849
|
+
run,
|
|
3850
|
+
view
|
|
3851
|
+
]);
|
|
3852
|
+
}
|
|
3853
|
+
//#endregion
|
|
3854
|
+
//#region src/headless/client-connection/client-connection.tsx
|
|
3855
|
+
const ConnectionContext = createContext(null);
|
|
3856
|
+
function useConnection() {
|
|
3857
|
+
const engine = useContext(ConnectionContext);
|
|
3858
|
+
if (engine === null) throw new Error("CapxulClientConnection parts must be used inside <CapxulClientConnection>");
|
|
3859
|
+
return engine;
|
|
3860
|
+
}
|
|
3861
|
+
function Root({ client, requestId, children }) {
|
|
3862
|
+
const engine = useClientConnection({
|
|
3863
|
+
client,
|
|
3864
|
+
requestId
|
|
3865
|
+
});
|
|
3866
|
+
return /* @__PURE__ */ jsx(ConnectionContext.Provider, {
|
|
3867
|
+
value: engine,
|
|
3868
|
+
children
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
function Checking({ children }) {
|
|
3872
|
+
return useConnection().phase === "checking" ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
|
|
3873
|
+
}
|
|
3874
|
+
function Otp({ children }) {
|
|
3875
|
+
const otp = useConnection().otp;
|
|
3876
|
+
return otp === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(otp) });
|
|
3877
|
+
}
|
|
3878
|
+
function Consent({ children }) {
|
|
3879
|
+
const consent = useConnection().consent;
|
|
3880
|
+
return consent === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(consent) });
|
|
3881
|
+
}
|
|
3882
|
+
function Complete({ children }) {
|
|
3883
|
+
const complete = useConnection().complete;
|
|
3884
|
+
return complete === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(complete) });
|
|
3885
|
+
}
|
|
3886
|
+
function Failure({ children }) {
|
|
3887
|
+
const failure = useConnection().failure;
|
|
3888
|
+
return failure === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(failure) });
|
|
3889
|
+
}
|
|
3890
|
+
const CapxulClientConnection = Object.assign(Root, {
|
|
3891
|
+
Checking,
|
|
3892
|
+
Otp,
|
|
3893
|
+
Consent,
|
|
3894
|
+
Complete,
|
|
3895
|
+
Failure
|
|
3896
|
+
});
|
|
3897
|
+
//#endregion
|
|
3898
|
+
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 };
|