@capxul/sdk-react 4.20.0-beta.2 → 4.20.0-beta.20
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 +104 -55
- package/dist/index.mjs +658 -230
- package/dist/testing/index.d.mts +1 -1
- package/dist/testing/index.mjs +1 -1
- package/package.json +5 -4
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 () => {
|
|
@@ -1233,8 +1259,11 @@ function useActivityDetail(reference, actor) {
|
|
|
1233
1259
|
status: detail.data.payment.status,
|
|
1234
1260
|
...detail.data.payment.execution === void 0 ? {} : { execution: detail.data.payment.execution }
|
|
1235
1261
|
} } : {},
|
|
1236
|
-
|
|
1237
|
-
|
|
1262
|
+
documents,
|
|
1263
|
+
selectedDocument,
|
|
1264
|
+
selectDocument,
|
|
1265
|
+
renderDocument,
|
|
1266
|
+
downloadDocument,
|
|
1238
1267
|
isOpeningDocument,
|
|
1239
1268
|
memo: typed ?? detail.data?.annotation?.memo ?? "",
|
|
1240
1269
|
setMemo: setTyped,
|
|
@@ -1247,6 +1276,7 @@ function useActivityDetail(reference, actor) {
|
|
|
1247
1276
|
//#endregion
|
|
1248
1277
|
//#region src/headless/activity/summary.ts
|
|
1249
1278
|
const DAY_MS = 864e5;
|
|
1279
|
+
const display = (money) => money === null ? null : formatMoney(money);
|
|
1250
1280
|
/**
|
|
1251
1281
|
* `"last30days"` is the last 30 whole UTC days including today. Quantizing to
|
|
1252
1282
|
* the UTC day keeps the cache key stable for a whole day instead of changing on
|
|
@@ -1257,57 +1287,6 @@ function resolveWindow(window, now) {
|
|
|
1257
1287
|
if (window === "last30days") return { from: Math.floor(now / DAY_MS) * DAY_MS - 29 * DAY_MS };
|
|
1258
1288
|
return window;
|
|
1259
1289
|
}
|
|
1260
|
-
/** An integer raw-unit string lifted into a major-unit decimal string. */
|
|
1261
|
-
function majorUnits(raw, decimals) {
|
|
1262
|
-
const negative = raw.startsWith("-");
|
|
1263
|
-
const digits = negative ? raw.slice(1) : raw;
|
|
1264
|
-
const sign = negative ? "-" : "";
|
|
1265
|
-
if (decimals === 0) return `${sign}${digits}`;
|
|
1266
|
-
const padded = digits.padStart(decimals + 1, "0");
|
|
1267
|
-
const cut = padded.length - decimals;
|
|
1268
|
-
return `${sign}${padded.slice(0, cut)}.${padded.slice(cut)}`;
|
|
1269
|
-
}
|
|
1270
|
-
function absoluteNet(total) {
|
|
1271
|
-
const net = BigInt(total.net);
|
|
1272
|
-
return net < 0n ? -net : net;
|
|
1273
|
-
}
|
|
1274
|
-
/**
|
|
1275
|
-
* Buckets carry different `decimals`, so their raw integers are not comparable.
|
|
1276
|
-
* Both are scaled to the wider precision before the magnitudes are ordered.
|
|
1277
|
-
*/
|
|
1278
|
-
function compareNet(left, right) {
|
|
1279
|
-
const width = Math.max(left.decimals, right.decimals);
|
|
1280
|
-
const scaledLeft = absoluteNet(left) * 10n ** BigInt(width - left.decimals);
|
|
1281
|
-
const scaledRight = absoluteNet(right) * 10n ** BigInt(width - right.decimals);
|
|
1282
|
-
if (scaledLeft === scaledRight) return 0;
|
|
1283
|
-
return scaledLeft < scaledRight ? -1 : 1;
|
|
1284
|
-
}
|
|
1285
|
-
/**
|
|
1286
|
-
* Which currency `.Summary` shows: the bucket matching the primary holding's
|
|
1287
|
-
* currency, else the bucket with the largest absolute `net`, breaking a tie on
|
|
1288
|
-
* currency code ascending so the choice is stable across reloads.
|
|
1289
|
-
*
|
|
1290
|
-
* The primary holding is matched on `available.currency` — the consumer
|
|
1291
|
-
* `CurrencyCode` — and NOT on `Holding.symbol`, which is the raw token ticker
|
|
1292
|
-
* ("USDX") and can never equal a bucket's currency ("USD"). See the contract
|
|
1293
|
-
* correction on #1449.
|
|
1294
|
-
*/
|
|
1295
|
-
function pickBucket(totals, primaryCurrency) {
|
|
1296
|
-
if (primaryCurrency !== null) {
|
|
1297
|
-
const matched = totals.find((total) => total.currency === primaryCurrency);
|
|
1298
|
-
if (matched !== void 0) return matched;
|
|
1299
|
-
}
|
|
1300
|
-
let best = null;
|
|
1301
|
-
for (const total of totals) {
|
|
1302
|
-
if (best === null) {
|
|
1303
|
-
best = total;
|
|
1304
|
-
continue;
|
|
1305
|
-
}
|
|
1306
|
-
const order = compareNet(total, best);
|
|
1307
|
-
if (order > 0 || order === 0 && total.currency < best.currency) best = total;
|
|
1308
|
-
}
|
|
1309
|
-
return best;
|
|
1310
|
-
}
|
|
1311
1290
|
/**
|
|
1312
1291
|
* The current UTC day number, which advances ON ITS OWN at UTC midnight.
|
|
1313
1292
|
*
|
|
@@ -1351,47 +1330,36 @@ function useActivitySummary(actor, window) {
|
|
|
1351
1330
|
const params = {};
|
|
1352
1331
|
if (actor !== void 0) params.actor = actor;
|
|
1353
1332
|
if (range !== void 0) params.window = range;
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
queryKey: accountKey([...capxulKeys.holdings(actor), "primary"], identity),
|
|
1360
|
-
queryFn: async () => {
|
|
1361
|
-
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
|
+
};
|
|
1362
1338
|
},
|
|
1363
1339
|
enabled
|
|
1364
1340
|
});
|
|
1365
1341
|
const totals = summary.data?.totals;
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
const bucket = useMemo(() => totals === void 0 || isLoading ? null : pickBucket(totals, primaryCurrency), [
|
|
1369
|
-
isLoading,
|
|
1370
|
-
primaryCurrency,
|
|
1371
|
-
totals
|
|
1372
|
-
]);
|
|
1373
|
-
if (bucket === null) return {
|
|
1342
|
+
const isLoading = summary.isLoading;
|
|
1343
|
+
if (summary.data === void 0 || totals === void 0 || totals.length === 0 || isLoading) return {
|
|
1374
1344
|
currency: null,
|
|
1375
1345
|
moneyIn: null,
|
|
1376
1346
|
moneyOut: null,
|
|
1377
1347
|
net: null,
|
|
1378
1348
|
pending: null,
|
|
1349
|
+
unrated: [],
|
|
1379
1350
|
count: summary.data?.count ?? 0,
|
|
1380
1351
|
isLoading,
|
|
1381
1352
|
error: asCapxulError$1(summary.error),
|
|
1382
1353
|
...summary.data?.readWarning === void 0 ? {} : { readWarning: summary.data.readWarning }
|
|
1383
1354
|
};
|
|
1384
|
-
const
|
|
1385
|
-
currency: bucket.currency,
|
|
1386
|
-
value: majorUnits(raw, bucket.decimals),
|
|
1387
|
-
decimals: bucket.decimals
|
|
1388
|
-
});
|
|
1355
|
+
const valuation = summary.data.valuation;
|
|
1389
1356
|
return {
|
|
1390
|
-
currency:
|
|
1391
|
-
moneyIn: display(
|
|
1392
|
-
moneyOut: display(
|
|
1393
|
-
net: display(
|
|
1394
|
-
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,
|
|
1395
1363
|
count: summary.data?.count ?? 0,
|
|
1396
1364
|
isLoading,
|
|
1397
1365
|
error: asCapxulError$1(summary.error),
|
|
@@ -1418,7 +1386,7 @@ const SIGN = Object.freeze({
|
|
|
1418
1386
|
self: ""
|
|
1419
1387
|
});
|
|
1420
1388
|
function toActivityRow(item, actor) {
|
|
1421
|
-
const amountDisplay =
|
|
1389
|
+
const amountDisplay = formatAssetAmount(item.amount);
|
|
1422
1390
|
const signedAmountDisplay = `${SIGN[item.direction]}${amountDisplay}`;
|
|
1423
1391
|
if (item.kind === "movement") return {
|
|
1424
1392
|
kind: "movement",
|
|
@@ -1430,7 +1398,7 @@ function toActivityRow(item, actor) {
|
|
|
1430
1398
|
counterpartyLabel: item.counterpartyLabel,
|
|
1431
1399
|
amountDisplay,
|
|
1432
1400
|
signedAmountDisplay,
|
|
1433
|
-
currency: item.amount.
|
|
1401
|
+
currency: assetSymbolFor(item.amount.asset),
|
|
1434
1402
|
createdAt: item.createdAt,
|
|
1435
1403
|
classification: item.classification,
|
|
1436
1404
|
phase: MOVEMENT_PHASE,
|
|
@@ -1452,7 +1420,7 @@ function toActivityRow(item, actor) {
|
|
|
1452
1420
|
counterpartyLabel: item.counterpartyLabel,
|
|
1453
1421
|
amountDisplay,
|
|
1454
1422
|
signedAmountDisplay,
|
|
1455
|
-
currency: item.amount.
|
|
1423
|
+
currency: assetSymbolFor(item.amount.asset),
|
|
1456
1424
|
createdAt: item.createdAt,
|
|
1457
1425
|
ledgerKind: item.ledgerKind,
|
|
1458
1426
|
paymentType: item.paymentType,
|
|
@@ -1533,7 +1501,7 @@ function buildCsv(items, actor, search) {
|
|
|
1533
1501
|
row.counterpartyLabel,
|
|
1534
1502
|
row.direction,
|
|
1535
1503
|
item.amount.value,
|
|
1536
|
-
item.amount.
|
|
1504
|
+
assetSymbolFor(item.amount.asset),
|
|
1537
1505
|
row.kind === "payment" ? row.execution?.notSent ? "not_sent" : row.execution?.status === "unresolved" ? "verifying" : row.status : "",
|
|
1538
1506
|
row.reference.id,
|
|
1539
1507
|
row.receipt?.transactionHash ?? ""
|
|
@@ -1789,7 +1757,7 @@ function useActivityContext() {
|
|
|
1789
1757
|
if (engine === null) throw new Error("CapxulActivity parts must be used inside <CapxulActivity>");
|
|
1790
1758
|
return engine;
|
|
1791
1759
|
}
|
|
1792
|
-
function Root$
|
|
1760
|
+
function Root$6({ actor, pageSize, children }) {
|
|
1793
1761
|
const engine = useActivity({
|
|
1794
1762
|
...actor === void 0 ? {} : { actor },
|
|
1795
1763
|
...pageSize === void 0 ? {} : { pageSize }
|
|
@@ -1840,7 +1808,7 @@ function Export({ children }) {
|
|
|
1840
1808
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useActivityContext().csv) });
|
|
1841
1809
|
}
|
|
1842
1810
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
1843
|
-
const CapxulActivity = Object.assign(Root$
|
|
1811
|
+
const CapxulActivity = Object.assign(Root$6, {
|
|
1844
1812
|
Summary: Summary$2,
|
|
1845
1813
|
Filters,
|
|
1846
1814
|
Search,
|
|
@@ -1881,6 +1849,26 @@ function useOrgMeReading(orgId) {
|
|
|
1881
1849
|
]);
|
|
1882
1850
|
}
|
|
1883
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
|
|
1884
1872
|
//#region src/headless/send-money/signer-status.ts
|
|
1885
1873
|
const NO_SUBSCRIPTION = () => () => {};
|
|
1886
1874
|
function useSignerStatus(client) {
|
|
@@ -2030,6 +2018,37 @@ function useSelectedOption(options) {
|
|
|
2030
2018
|
}, [options]);
|
|
2031
2019
|
return [options.find((option) => option.id === selectedId) ?? null, select];
|
|
2032
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
|
+
}
|
|
2033
2052
|
function useSendMoney(input) {
|
|
2034
2053
|
const { actor, onSent, onFailed } = input;
|
|
2035
2054
|
const client = useCapxulClientOrNull();
|
|
@@ -2042,15 +2061,12 @@ function useSendMoney(input) {
|
|
|
2042
2061
|
const treasury = useCapxulOrgTreasury(orgId, { enabled: actor.kind === "organization" });
|
|
2043
2062
|
const accountQuery = actor.kind === "personal" ? balance : treasury;
|
|
2044
2063
|
const account = accountQuery.data ?? null;
|
|
2045
|
-
const
|
|
2046
|
-
|
|
2047
|
-
symbol: account.available.currency,
|
|
2048
|
-
availableDisplay: formatMoney(account.available, { grammar: "code" })
|
|
2049
|
-
}], [account]);
|
|
2064
|
+
const assetSelections = useMemo(() => assetSelectionsFromAccount$1(account), [account]);
|
|
2065
|
+
const assetOptions = useMemo(() => assetSelections.map((selection) => selection.option), [assetSelections]);
|
|
2050
2066
|
const [selectedAssetOption, selectAsset] = useSelectedOption(assetOptions);
|
|
2051
|
-
const selectedAssetMoney =
|
|
2067
|
+
const selectedAssetMoney = useMemo(() => assetSelections.find((selection) => selection.option.id === selectedAssetOption?.id)?.money ?? null, [assetSelections, selectedAssetOption]);
|
|
2052
2068
|
const [amountText, setAmountText] = useState("");
|
|
2053
|
-
const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null :
|
|
2069
|
+
const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null : parseAssetAmount(amountText, requireAsset(selectedAssetMoney.asset)), [amountText, selectedAssetMoney]);
|
|
2054
2070
|
const parsedMoney = parsed !== null && !isMoneyParseError(parsed) ? parsed : null;
|
|
2055
2071
|
const amountError = parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null;
|
|
2056
2072
|
const recipient = useRecipientResolution(client, JSON.stringify([
|
|
@@ -2062,7 +2078,7 @@ function useSendMoney(input) {
|
|
|
2062
2078
|
const personalPay = useCapxulPay();
|
|
2063
2079
|
const organizationPay = useCapxulOrganizationPay(orgId);
|
|
2064
2080
|
const pay = actor.kind === "personal" ? personalPay : organizationPay;
|
|
2065
|
-
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";
|
|
2066
2082
|
const submitGate = useRef(false);
|
|
2067
2083
|
const submit = useCallback(() => {
|
|
2068
2084
|
if (blockedReason !== null || submitGate.current) return;
|
|
@@ -2149,7 +2165,7 @@ function useSendMoneyContext() {
|
|
|
2149
2165
|
if (engine === null) throw new Error("CapxulSendMoney parts must be used inside <CapxulSendMoney>");
|
|
2150
2166
|
return engine;
|
|
2151
2167
|
}
|
|
2152
|
-
function Root$
|
|
2168
|
+
function Root$5({ actor, onSent, onFailed, children }) {
|
|
2153
2169
|
const engine = useSendMoney({
|
|
2154
2170
|
actor,
|
|
2155
2171
|
onSent,
|
|
@@ -2176,7 +2192,7 @@ function Actions$1({ children }) {
|
|
|
2176
2192
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useSendMoneyContext().actions) });
|
|
2177
2193
|
}
|
|
2178
2194
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
2179
|
-
const CapxulSendMoney = Object.assign(Root$
|
|
2195
|
+
const CapxulSendMoney = Object.assign(Root$5, {
|
|
2180
2196
|
Source,
|
|
2181
2197
|
Asset: Asset$1,
|
|
2182
2198
|
Amount,
|
|
@@ -2266,7 +2282,7 @@ function useOrgMemberContext() {
|
|
|
2266
2282
|
if (state === null) throw new Error("CapxulOrgMember parts must be used inside <CapxulOrgMember>");
|
|
2267
2283
|
return state;
|
|
2268
2284
|
}
|
|
2269
|
-
function Root$
|
|
2285
|
+
function Root$4({ orgId, children }) {
|
|
2270
2286
|
const state = useOrgMember(orgId);
|
|
2271
2287
|
return /* @__PURE__ */ jsx(OrgMemberContext.Provider, {
|
|
2272
2288
|
value: state,
|
|
@@ -2286,7 +2302,7 @@ function Standing({ children }) {
|
|
|
2286
2302
|
return /* @__PURE__ */ jsx(Fragment, { children: children(useOrgMemberContext().standing) });
|
|
2287
2303
|
}
|
|
2288
2304
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
2289
|
-
const CapxulOrgMember = Object.assign(Root$
|
|
2305
|
+
const CapxulOrgMember = Object.assign(Root$4, {
|
|
2290
2306
|
Can,
|
|
2291
2307
|
Standing
|
|
2292
2308
|
});
|
|
@@ -2326,18 +2342,14 @@ function isOrgLaneSettled(identity) {
|
|
|
2326
2342
|
if (identity.phase !== "authenticated" || identity.account.at !== "claimed") return true;
|
|
2327
2343
|
const org = identity.account.org;
|
|
2328
2344
|
if (org === null) return true;
|
|
2329
|
-
|
|
2330
|
-
case "ready":
|
|
2331
|
-
case "failed": return true;
|
|
2332
|
-
default: return false;
|
|
2333
|
-
}
|
|
2345
|
+
return org.observation === "connected" && org.setup !== null;
|
|
2334
2346
|
}
|
|
2335
2347
|
/** Exactly the Organization asked for, and ready to be used. */
|
|
2336
2348
|
function isExactOrgReady(lifecycle, orgId) {
|
|
2337
2349
|
return lifecycle?.status === "ready" && String(lifecycle.orgId) === orgId;
|
|
2338
2350
|
}
|
|
2339
2351
|
function isLifecycleSettled(lifecycle) {
|
|
2340
|
-
return lifecycle
|
|
2352
|
+
return lifecycle !== void 0;
|
|
2341
2353
|
}
|
|
2342
2354
|
function destinationOrNull(identity) {
|
|
2343
2355
|
return identity === null ? null : resolveIdentityDestination(identity);
|
|
@@ -2365,19 +2377,27 @@ function decidePhase(input) {
|
|
|
2365
2377
|
* Which probe produced the surfaced error — the SAME precedence `error` itself
|
|
2366
2378
|
* is chosen by, written once so the code and its source can never disagree.
|
|
2367
2379
|
*/
|
|
2368
|
-
function probeOf(session, membership, lifecycle) {
|
|
2380
|
+
function probeOf(session, membership, lifecycle, authority) {
|
|
2369
2381
|
if (session !== null) return "session";
|
|
2370
2382
|
if (membership !== null) return "membership";
|
|
2371
2383
|
if (lifecycle !== null) return "lifecycle";
|
|
2384
|
+
if (authority !== null) return "authority";
|
|
2372
2385
|
return null;
|
|
2373
2386
|
}
|
|
2374
2387
|
/** The engine — all the logic. NOT exported from the package barrel. */
|
|
2375
2388
|
function useDashboardAccess(scope) {
|
|
2376
2389
|
const client = useCapxulClientOrNull();
|
|
2390
|
+
const queryClient = useQueryClient();
|
|
2377
2391
|
const identity = useCapxulIdentityOrNull();
|
|
2378
2392
|
const destination = useMemo(() => destinationOrNull(identity), [identity]);
|
|
2379
2393
|
const orgId = scope.kind === "organization" ? scope.orgId : void 0;
|
|
2380
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);
|
|
2381
2401
|
const sessionError = useMemo(() => identity?.phase === "faulted" ? identityError(identity) : null, [identity]);
|
|
2382
2402
|
const wantsMemberships = orgId !== void 0 && claimed;
|
|
2383
2403
|
const memberships = useCapxulOrgs({ enabled: wantsMemberships });
|
|
@@ -2385,33 +2405,125 @@ function useDashboardAccess(scope) {
|
|
|
2385
2405
|
const membershipsSettled = !wantsMemberships || memberships.data !== void 0 || membershipError !== null;
|
|
2386
2406
|
const hasMembership = orgId !== void 0 && (memberships.data ?? []).some((organization) => String(organization.id) === orgId);
|
|
2387
2407
|
const wantsLifecycle = wantsMemberships && membershipsSettled && hasMembership;
|
|
2408
|
+
const setupKey = accountKey(capxulKeys.orgLifecycle(orgId), identity);
|
|
2388
2409
|
const lifecycle = useQuery({
|
|
2389
|
-
queryKey:
|
|
2410
|
+
queryKey: setupKey,
|
|
2390
2411
|
queryFn: async () => {
|
|
2391
|
-
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.
|
|
2412
|
+
return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.organization.setup).org(orgId).setup.get());
|
|
2392
2413
|
},
|
|
2393
2414
|
enabled: wantsLifecycle && client !== null
|
|
2394
2415
|
});
|
|
2395
|
-
const
|
|
2396
|
-
|
|
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);
|
|
2397
2504
|
const retry = useCallback(() => {
|
|
2398
2505
|
if (sessionError !== null && client !== null) client.auth.getSession();
|
|
2399
2506
|
if (membershipError !== null) memberships.refetch();
|
|
2507
|
+
if (observationError !== void 0) setSubscriptionRetry((attempt) => attempt + 1);
|
|
2400
2508
|
if (lifecycleError !== null) lifecycle.refetch();
|
|
2509
|
+
if (authorityError !== null) authority.refetch();
|
|
2401
2510
|
}, [
|
|
2402
2511
|
client,
|
|
2403
2512
|
sessionError,
|
|
2404
2513
|
membershipError,
|
|
2514
|
+
observationError,
|
|
2405
2515
|
lifecycleError,
|
|
2406
2516
|
memberships.refetch,
|
|
2407
|
-
lifecycle.refetch
|
|
2517
|
+
lifecycle.refetch,
|
|
2518
|
+
authorityError,
|
|
2519
|
+
authority.refetch
|
|
2408
2520
|
]);
|
|
2409
|
-
const error = sessionError ?? membershipError ?? lifecycleError;
|
|
2410
|
-
const probe = probeOf(sessionError, membershipError, lifecycleError);
|
|
2521
|
+
const error = sessionError ?? membershipError ?? lifecycleError ?? authorityError;
|
|
2522
|
+
const probe = probeOf(sessionError, membershipError, lifecycleError, authorityError);
|
|
2411
2523
|
const phase = decidePhase({
|
|
2412
2524
|
error,
|
|
2413
2525
|
settled: client !== null && identity !== null && !isRestoring$1(identity) && membershipsSettled && lifecycleSettled,
|
|
2414
|
-
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,
|
|
2415
2527
|
orgLaneSettled: identity !== null && isOrgLaneSettled(identity),
|
|
2416
2528
|
destination
|
|
2417
2529
|
});
|
|
@@ -2446,7 +2558,7 @@ function usePart(phase) {
|
|
|
2446
2558
|
}, [placed, phase]);
|
|
2447
2559
|
return state.phase === phase ? state : null;
|
|
2448
2560
|
}
|
|
2449
|
-
function Root$
|
|
2561
|
+
function Root$3({ scope, children }) {
|
|
2450
2562
|
const state = useDashboardAccess(scope);
|
|
2451
2563
|
const [placed] = useState(() => /* @__PURE__ */ new Set());
|
|
2452
2564
|
const value = useMemo(() => ({
|
|
@@ -2464,7 +2576,7 @@ function Root$2({ scope, children }) {
|
|
|
2464
2576
|
});
|
|
2465
2577
|
}
|
|
2466
2578
|
/** Hands nothing — today's app renders `null` here, exactly as it does now. */
|
|
2467
|
-
function Checking({ children }) {
|
|
2579
|
+
function Checking$1({ children }) {
|
|
2468
2580
|
return usePart("checking") === null ? null : /* @__PURE__ */ jsx(Fragment, { children });
|
|
2469
2581
|
}
|
|
2470
2582
|
function DashboardError({ children }) {
|
|
@@ -2491,19 +2603,14 @@ function Granted({ children }) {
|
|
|
2491
2603
|
return usePart("granted") === null ? null : /* @__PURE__ */ jsx(Fragment, { children });
|
|
2492
2604
|
}
|
|
2493
2605
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
2494
|
-
const CapxulDashboardAccess = Object.assign(Root$
|
|
2495
|
-
Checking,
|
|
2606
|
+
const CapxulDashboardAccess = Object.assign(Root$3, {
|
|
2607
|
+
Checking: Checking$1,
|
|
2496
2608
|
Error: DashboardError,
|
|
2497
2609
|
Redirect,
|
|
2498
2610
|
Granted
|
|
2499
2611
|
});
|
|
2500
2612
|
//#endregion
|
|
2501
2613
|
//#region src/headless/payroll/money.ts
|
|
2502
|
-
/** `{ value: "12.34", decimals: 6 }` → `12340000n`. */
|
|
2503
|
-
function toMinorUnits(money) {
|
|
2504
|
-
const [whole = "0", fraction = ""] = money.value.split(".");
|
|
2505
|
-
return BigInt(`${whole}${fraction.padEnd(money.decimals, "0")}`);
|
|
2506
|
-
}
|
|
2507
2614
|
const TYPED_AMOUNT = /^(-?)(\d+)(?:\.(\d+))?$/;
|
|
2508
2615
|
/**
|
|
2509
2616
|
* Σ of the employer's TYPED amounts on a saved group — a DISPLAY total.
|
|
@@ -2536,6 +2643,17 @@ function fromMinorUnits(raw, asset) {
|
|
|
2536
2643
|
decimals: asset.decimals
|
|
2537
2644
|
};
|
|
2538
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
|
+
}
|
|
2539
2657
|
//#endregion
|
|
2540
2658
|
//#region src/headless/payroll/reads.ts
|
|
2541
2659
|
function toRecipientOption(entry) {
|
|
@@ -2652,7 +2770,7 @@ function scopedClient(client, method, orgId) {
|
|
|
2652
2770
|
//#endregion
|
|
2653
2771
|
//#region src/headless/payroll/use-payroll.ts
|
|
2654
2772
|
/** Money reads the same way everywhere on this screen: amount then code. */
|
|
2655
|
-
const GRAMMAR
|
|
2773
|
+
const GRAMMAR = { grammar: "code" };
|
|
2656
2774
|
/**
|
|
2657
2775
|
* One captured snapshot, or nothing — never a fabricated empty options object,
|
|
2658
2776
|
* and never a DOM event a screen wired straight to `onClick`.
|
|
@@ -2668,7 +2786,7 @@ function toPayrollGroupRow(group) {
|
|
|
2668
2786
|
name: group.name,
|
|
2669
2787
|
tone: group.tone,
|
|
2670
2788
|
memberCount: group.members.length,
|
|
2671
|
-
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),
|
|
2672
2790
|
members: group.members.map((member) => ({
|
|
2673
2791
|
recipientId: member.partyId,
|
|
2674
2792
|
amount: member.amount,
|
|
@@ -2758,8 +2876,8 @@ function usePayroll(orgId) {
|
|
|
2758
2876
|
const started = orgId !== void 0;
|
|
2759
2877
|
return {
|
|
2760
2878
|
summary: {
|
|
2761
|
-
walletBalanceDisplay: treasury.data === void 0 ? null : formatMoney(treasury.data.available, GRAMMAR
|
|
2762
|
-
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(", "),
|
|
2763
2881
|
isLoading: started && (treasury.data === void 0 && treasury.error === null || runs.data === void 0 && runs.error === null),
|
|
2764
2882
|
error: treasury.error ?? runs.error
|
|
2765
2883
|
},
|
|
@@ -2792,7 +2910,7 @@ function usePayrollContext() {
|
|
|
2792
2910
|
if (engine === null) throw new Error("CapxulPayroll parts must be used inside <CapxulPayroll>");
|
|
2793
2911
|
return engine;
|
|
2794
2912
|
}
|
|
2795
|
-
function Root$
|
|
2913
|
+
function Root$2({ orgId, children }) {
|
|
2796
2914
|
const engine = usePayroll(orgId);
|
|
2797
2915
|
return /* @__PURE__ */ jsx(PayrollContext.Provider, {
|
|
2798
2916
|
value: engine,
|
|
@@ -2810,7 +2928,7 @@ function Groups({ children }) {
|
|
|
2810
2928
|
* There is NO `.Runs` part (ruling Q6): no shipped region has the right shape
|
|
2811
2929
|
* for a run list, and a part without a production caller may not ship.
|
|
2812
2930
|
*/
|
|
2813
|
-
const CapxulPayroll = Object.assign(Root$
|
|
2931
|
+
const CapxulPayroll = Object.assign(Root$2, {
|
|
2814
2932
|
Summary: Summary$1,
|
|
2815
2933
|
Groups
|
|
2816
2934
|
});
|
|
@@ -2820,8 +2938,14 @@ function record(value) {
|
|
|
2820
2938
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw Errors.invalidInput("payrollDraft", "Invalid run draft");
|
|
2821
2939
|
return value;
|
|
2822
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
|
+
*/
|
|
2823
2947
|
function identifier(value) {
|
|
2824
|
-
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");
|
|
2825
2949
|
return value;
|
|
2826
2950
|
}
|
|
2827
2951
|
function minorAmount(value) {
|
|
@@ -2837,21 +2961,35 @@ function decodeDraft(value, scope) {
|
|
|
2837
2961
|
const items = input.items.map((raw) => {
|
|
2838
2962
|
const item = record(raw);
|
|
2839
2963
|
const to = record(item.to);
|
|
2964
|
+
const amount = record(item.amount);
|
|
2840
2965
|
const partyId = toPartyId(identifier(item.partyId));
|
|
2841
|
-
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" || "currency" in item || "decimals" in item || !Array.isArray(item.adjustments) || item.adjustments.length !== 0) throw Errors.invalidInput("payrollDraft", "Invalid draft recipient");
|
|
2842
2967
|
const net = minorAmount(item.net);
|
|
2968
|
+
const selectedAsset = requireAsset(identifier(amount.asset));
|
|
2969
|
+
const selectedAmount = {
|
|
2970
|
+
asset: selectedAsset.assetId,
|
|
2971
|
+
value: amount.value
|
|
2972
|
+
};
|
|
2973
|
+
try {
|
|
2974
|
+
if (toRaw(selectedAmount, selectedAsset).toString() !== net) throw Errors.invalidInput("payrollDraft", "Draft payout units differ from net");
|
|
2975
|
+
} catch {
|
|
2976
|
+
throw Errors.invalidInput("payrollDraft", "Invalid draft payout amount");
|
|
2977
|
+
}
|
|
2843
2978
|
return {
|
|
2844
2979
|
to: {
|
|
2845
2980
|
kind: "party",
|
|
2846
2981
|
partyId
|
|
2847
2982
|
},
|
|
2848
2983
|
partyId,
|
|
2984
|
+
amount: selectedAmount,
|
|
2849
2985
|
gross: net,
|
|
2850
2986
|
net,
|
|
2851
2987
|
adjustments: []
|
|
2852
2988
|
};
|
|
2853
2989
|
});
|
|
2854
2990
|
if (new Set(items.map((item) => item.partyId)).size !== items.length) throw Errors.invalidInput("payrollDraft", "Duplicate draft recipient");
|
|
2991
|
+
const exactAsset = requireAsset(identifier(asset.id));
|
|
2992
|
+
if (input.asset !== exactAsset.assetId || items.some((item) => item.amount.asset !== exactAsset.assetId)) throw Errors.invalidInput("payrollDraft", "Draft item asset does not match the run asset");
|
|
2855
2993
|
return {
|
|
2856
2994
|
version: 1,
|
|
2857
2995
|
scope,
|
|
@@ -2862,6 +3000,7 @@ function decodeDraft(value, scope) {
|
|
|
2862
3000
|
},
|
|
2863
3001
|
input: {
|
|
2864
3002
|
permissionId: identifier(input.permissionId),
|
|
3003
|
+
asset: exactAsset.assetId,
|
|
2865
3004
|
period: {
|
|
2866
3005
|
start: period.start,
|
|
2867
3006
|
end: period.start
|
|
@@ -3065,7 +3204,6 @@ function termsSeeds(terms, asset, period) {
|
|
|
3065
3204
|
}
|
|
3066
3205
|
//#endregion
|
|
3067
3206
|
//#region src/headless/payroll/use-payroll-run.ts
|
|
3068
|
-
const GRAMMAR = { grammar: "code" };
|
|
3069
3207
|
/**
|
|
3070
3208
|
* The period this composer composes: an INSTANT run (ruling Q3). `authorizeRun`
|
|
3071
3209
|
* stamps `start === end === runAt` at submit, and the payslip's period renders
|
|
@@ -3089,6 +3227,31 @@ const INSTANT_RUN_PERIOD = {
|
|
|
3089
3227
|
function amountText(amounts, seeds, recipientId) {
|
|
3090
3228
|
return amounts[recipientId]?.value ?? seeds.get(recipientId) ?? "";
|
|
3091
3229
|
}
|
|
3230
|
+
function freshRunItems(input) {
|
|
3231
|
+
const items = [];
|
|
3232
|
+
for (const [index, recipientId] of input.selected.entries()) {
|
|
3233
|
+
const to = input.payableRefs[index];
|
|
3234
|
+
const amount = input.amounts[index];
|
|
3235
|
+
const terms = input.compensationTerms.get(recipientId);
|
|
3236
|
+
if (to === void 0 || amount === void 0 || terms === void 0) throw Errors.unknown();
|
|
3237
|
+
const rawAmount = toRaw(amount, requireAsset(amount.asset)).toString();
|
|
3238
|
+
items.push({
|
|
3239
|
+
to: {
|
|
3240
|
+
kind: "party",
|
|
3241
|
+
partyId: toPartyId(recipientId)
|
|
3242
|
+
},
|
|
3243
|
+
partyId: recipientId,
|
|
3244
|
+
amount,
|
|
3245
|
+
gross: rawAmount,
|
|
3246
|
+
net: rawAmount,
|
|
3247
|
+
adjustments: []
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
return items;
|
|
3251
|
+
}
|
|
3252
|
+
function hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, terms) {
|
|
3253
|
+
return frozen || !writerNamesSelectedAsset || selected.every((recipientId) => terms.has(recipientId));
|
|
3254
|
+
}
|
|
3092
3255
|
/**
|
|
3093
3256
|
* WHICH run the composer is composing, by MEANING rather than by object shape.
|
|
3094
3257
|
*
|
|
@@ -3120,6 +3283,33 @@ function initialAmounts(prefill) {
|
|
|
3120
3283
|
};
|
|
3121
3284
|
return seeded;
|
|
3122
3285
|
}
|
|
3286
|
+
/**
|
|
3287
|
+
* One `.Asset` option per position the treasury Account holds, taken from
|
|
3288
|
+
* `account.balances` — never from `account.available`, which is the Account's
|
|
3289
|
+
* fiat VALUATION of those positions (ADR-0028 R5/R6). The option is keyed on
|
|
3290
|
+
* the ASSET id: several positions share one account id, and a symbol is display
|
|
3291
|
+
* only (ADR-0028 R3). Every row is listed, so a real multi-position treasury
|
|
3292
|
+
* still offers its holdings.
|
|
3293
|
+
*/
|
|
3294
|
+
function assetSelectionsFromAccount(account) {
|
|
3295
|
+
if (account === null) return [];
|
|
3296
|
+
return account.balances.map((position) => {
|
|
3297
|
+
return {
|
|
3298
|
+
option: {
|
|
3299
|
+
id: position.asset,
|
|
3300
|
+
symbol: position.symbol,
|
|
3301
|
+
availableDisplay: formatAssetAmount({
|
|
3302
|
+
asset: position.asset,
|
|
3303
|
+
value: position.available
|
|
3304
|
+
})
|
|
3305
|
+
},
|
|
3306
|
+
money: {
|
|
3307
|
+
asset: position.asset,
|
|
3308
|
+
value: position.available
|
|
3309
|
+
}
|
|
3310
|
+
};
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3123
3313
|
function usePayrollRun(input) {
|
|
3124
3314
|
const { orgId, prefill, onRun, onFailed } = input;
|
|
3125
3315
|
const client = useCapxulClientOrNull();
|
|
@@ -3133,17 +3323,13 @@ function usePayrollRun(input) {
|
|
|
3133
3323
|
const terms = useOrgTerms(orgId);
|
|
3134
3324
|
const [selected, setSelected] = useState(() => prefill?.recipientIds ?? []);
|
|
3135
3325
|
const [amounts, setAmounts] = useState(() => initialAmounts(prefill));
|
|
3136
|
-
const restoreCurrency = treasury.data?.available.currency;
|
|
3137
3326
|
const { draftScope, currentScope, draft, draftError, restorationPending, activateDraft, restoreDraft, setDraft, recovery, draftBlocked } = useRunDraft(client, orgId, useCallback((restored) => {
|
|
3138
3327
|
setSelected(restored.input.items.map((item) => item.partyId));
|
|
3139
|
-
if (
|
|
3140
|
-
value:
|
|
3141
|
-
currency: restoreCurrency,
|
|
3142
|
-
decimals: restored.asset.decimals
|
|
3143
|
-
}).value,
|
|
3328
|
+
if (requireAsset(restored.input.asset).assetId === restored.input.asset) setAmounts(Object.fromEntries(restored.input.items.map((item) => [item.partyId, {
|
|
3329
|
+
value: item.amount.value,
|
|
3144
3330
|
prefilled: false
|
|
3145
3331
|
}])));
|
|
3146
|
-
}, [
|
|
3332
|
+
}, []));
|
|
3147
3333
|
const frozen = draft !== null;
|
|
3148
3334
|
const composerKey = composerIdentity(orgId, prefill, draftScope, frozen);
|
|
3149
3335
|
const [seededKey, setSeededKey] = useState(composerKey);
|
|
@@ -3183,11 +3369,8 @@ function usePayrollRun(input) {
|
|
|
3183
3369
|
draftError
|
|
3184
3370
|
]);
|
|
3185
3371
|
const account = treasury.data ?? null;
|
|
3186
|
-
const
|
|
3187
|
-
|
|
3188
|
-
symbol: account.available.currency,
|
|
3189
|
-
availableDisplay: formatMoney(account.available, GRAMMAR)
|
|
3190
|
-
}], [account]);
|
|
3372
|
+
const assetSelections = useMemo(() => assetSelectionsFromAccount(account), [account]);
|
|
3373
|
+
const assetOptions = useMemo(() => assetSelections.map((selection) => selection.option), [assetSelections]);
|
|
3191
3374
|
const [selectedAssetId, setSelectedAssetId] = useState(null);
|
|
3192
3375
|
useEffect(() => {
|
|
3193
3376
|
if (!assetOptions.some((option) => option.id === selectedAssetId)) setSelectedAssetId(assetOptions[0]?.id ?? null);
|
|
@@ -3196,7 +3379,7 @@ function usePayrollRun(input) {
|
|
|
3196
3379
|
const selectAsset = useCallback((id) => {
|
|
3197
3380
|
if (!submitGate.current && !frozen) setSelectedAssetId((current) => assetOptions.some((option) => option.id === id) ? id : current);
|
|
3198
3381
|
}, [assetOptions, frozen]);
|
|
3199
|
-
const assetMoney =
|
|
3382
|
+
const assetMoney = assetSelections.find((selection) => selection.option.id === selectedAsset?.id)?.money ?? null;
|
|
3200
3383
|
const named = useMemo(() => new Map(roster.options.map((option) => [option.id, option])), [roster.options]);
|
|
3201
3384
|
const refs = roster.refs;
|
|
3202
3385
|
const payableRefs = useMemo(() => {
|
|
@@ -3208,12 +3391,19 @@ function usePayrollRun(input) {
|
|
|
3208
3391
|
}
|
|
3209
3392
|
return resolved;
|
|
3210
3393
|
}, [refs, selected]);
|
|
3211
|
-
const seeds = useMemo(() =>
|
|
3394
|
+
const seeds = useMemo(() => {
|
|
3395
|
+
if (assetMoney === null) return /* @__PURE__ */ new Map();
|
|
3396
|
+
const metadata = requireAsset(assetMoney.asset);
|
|
3397
|
+
return termsSeeds(terms.data ?? [], metadata.peg === null ? null : {
|
|
3398
|
+
currency: metadata.peg,
|
|
3399
|
+
decimals: metadata.decimals
|
|
3400
|
+
}, INSTANT_RUN_PERIOD);
|
|
3401
|
+
}, [assetMoney, terms.data]);
|
|
3212
3402
|
const entries = useMemo(() => {
|
|
3213
3403
|
if (assetMoney === null) return [];
|
|
3214
3404
|
return selected.map((recipientId) => {
|
|
3215
3405
|
const value = amountText(amounts, seeds, recipientId);
|
|
3216
|
-
const parsed = value.trim() === "" ? null :
|
|
3406
|
+
const parsed = value.trim() === "" ? null : parseAssetAmount(value, requireAsset(assetMoney.asset));
|
|
3217
3407
|
return {
|
|
3218
3408
|
recipientId,
|
|
3219
3409
|
name: named.get(recipientId)?.name ?? recipientId,
|
|
@@ -3221,7 +3411,7 @@ function usePayrollRun(input) {
|
|
|
3221
3411
|
change: (text) => change(recipientId, text),
|
|
3222
3412
|
error: parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null,
|
|
3223
3413
|
prefilled: amounts[recipientId]?.prefilled ?? seeds.has(recipientId),
|
|
3224
|
-
currency: assetMoney.
|
|
3414
|
+
currency: requireAsset(assetMoney.asset).symbol
|
|
3225
3415
|
};
|
|
3226
3416
|
});
|
|
3227
3417
|
}, [
|
|
@@ -3236,7 +3426,7 @@ function usePayrollRun(input) {
|
|
|
3236
3426
|
if (assetMoney === null || selected.length === 0) return null;
|
|
3237
3427
|
const money = [];
|
|
3238
3428
|
for (const recipientId of selected) {
|
|
3239
|
-
const parsed =
|
|
3429
|
+
const parsed = parseAssetAmount(amountText(amounts, seeds, recipientId), requireAsset(assetMoney.asset));
|
|
3240
3430
|
if (isMoneyParseError(parsed)) return null;
|
|
3241
3431
|
money.push(parsed);
|
|
3242
3432
|
}
|
|
@@ -3247,16 +3437,25 @@ function usePayrollRun(input) {
|
|
|
3247
3437
|
seeds,
|
|
3248
3438
|
selected
|
|
3249
3439
|
]);
|
|
3250
|
-
const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null :
|
|
3251
|
-
|
|
3252
|
-
|
|
3440
|
+
const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null : {
|
|
3441
|
+
asset: assetMoney.asset,
|
|
3442
|
+
value: assetFromMinorUnits(parsedAmounts.reduce((sum, amount) => sum + toRaw(amount, requireAsset(amount.asset)), 0n), {
|
|
3443
|
+
asset: assetMoney.asset,
|
|
3444
|
+
decimals: requireAsset(assetMoney.asset).decimals
|
|
3445
|
+
}).value
|
|
3446
|
+
}, [assetMoney, parsedAmounts]);
|
|
3447
|
+
const availableRaw = assetMoney === null ? null : toRaw(assetMoney, requireAsset(assetMoney.asset));
|
|
3448
|
+
const totalRaw = total === null ? null : toRaw(total, requireAsset(total.asset));
|
|
3253
3449
|
const funds = availableRaw === null || totalRaw === null ? "unknown" : totalRaw > availableRaw ? "insufficient" : "available";
|
|
3254
3450
|
const budgets = me.data?.budgets ?? [];
|
|
3451
|
+
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]);
|
|
3452
|
+
const writerNamesSelectedAsset = writeNamesAsset();
|
|
3255
3453
|
const blockedReason = firstFalseFact({
|
|
3256
3454
|
signerReady: signerStatus === "ready",
|
|
3257
3455
|
...runAuthorityFacts(draft, budgets, selectedAsset, assetMoney, funds),
|
|
3258
3456
|
recipientCount: payableRefs === null ? 0 : selected.length,
|
|
3259
|
-
amountsParse: total !== null
|
|
3457
|
+
amountsParse: total !== null && hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, compensationTerms),
|
|
3458
|
+
writeNamesAsset: writerNamesSelectedAsset
|
|
3260
3459
|
});
|
|
3261
3460
|
const authorize = useMutation({
|
|
3262
3461
|
mutationFn: async ({ candidate, options }) => {
|
|
@@ -3278,7 +3477,7 @@ function usePayrollRun(input) {
|
|
|
3278
3477
|
if (candidate.scope === currentScope.current) restoreDraft(candidate.scope);
|
|
3279
3478
|
}
|
|
3280
3479
|
});
|
|
3281
|
-
const totalDisplay = total === null ? null :
|
|
3480
|
+
const totalDisplay = total === null ? null : formatAssetAmount(total);
|
|
3282
3481
|
const authorizeMutate = authorize.mutate;
|
|
3283
3482
|
const submit = useCallback((options) => {
|
|
3284
3483
|
const attempt = toInvocationOptions(options);
|
|
@@ -3287,41 +3486,35 @@ function usePayrollRun(input) {
|
|
|
3287
3486
|
if (permissionId === void 0 || totalDisplay === null || parsedAmounts === null) return;
|
|
3288
3487
|
if (payableRefs === null) return;
|
|
3289
3488
|
const recipientCount = selected.length;
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
const
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
kind: "party",
|
|
3299
|
-
partyId: toPartyId(recipientId)
|
|
3300
|
-
},
|
|
3301
|
-
partyId: recipientId,
|
|
3302
|
-
gross: raw,
|
|
3303
|
-
net: raw,
|
|
3304
|
-
adjustments: []
|
|
3489
|
+
let candidate;
|
|
3490
|
+
if (frozen) candidate = draft;
|
|
3491
|
+
else {
|
|
3492
|
+
const items = freshRunItems({
|
|
3493
|
+
selected,
|
|
3494
|
+
payableRefs,
|
|
3495
|
+
amounts: parsedAmounts,
|
|
3496
|
+
compensationTerms
|
|
3305
3497
|
});
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
decimals: assetMoney.decimals
|
|
3315
|
-
},
|
|
3316
|
-
input: {
|
|
3317
|
-
permissionId,
|
|
3318
|
-
period: {
|
|
3319
|
-
start: runAt,
|
|
3320
|
-
end: runAt
|
|
3498
|
+
const runAt = Date.now();
|
|
3499
|
+
candidate = {
|
|
3500
|
+
version: 1,
|
|
3501
|
+
scope: draftScope,
|
|
3502
|
+
asset: {
|
|
3503
|
+
id: selectedAsset.id,
|
|
3504
|
+
currency: requireAsset(assetMoney.asset).symbol,
|
|
3505
|
+
decimals: requireAsset(assetMoney.asset).decimals
|
|
3321
3506
|
},
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3507
|
+
input: {
|
|
3508
|
+
permissionId,
|
|
3509
|
+
asset: selectedAsset.id,
|
|
3510
|
+
period: {
|
|
3511
|
+
start: runAt,
|
|
3512
|
+
end: runAt
|
|
3513
|
+
},
|
|
3514
|
+
items
|
|
3515
|
+
}
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3325
3518
|
submitGate.current = true;
|
|
3326
3519
|
authorizeMutate({
|
|
3327
3520
|
candidate,
|
|
@@ -3353,6 +3546,7 @@ function usePayrollRun(input) {
|
|
|
3353
3546
|
selectedAsset,
|
|
3354
3547
|
blockedReason,
|
|
3355
3548
|
budgets,
|
|
3549
|
+
compensationTerms,
|
|
3356
3550
|
onFailed,
|
|
3357
3551
|
onRun,
|
|
3358
3552
|
parsedAmounts,
|
|
@@ -3401,18 +3595,25 @@ function usePayrollRun(input) {
|
|
|
3401
3595
|
};
|
|
3402
3596
|
}
|
|
3403
3597
|
function remainingBalanceDisplay(available, total, asset) {
|
|
3404
|
-
|
|
3598
|
+
if (available === null || total === null || asset === null) return null;
|
|
3599
|
+
const remaining = available - total;
|
|
3600
|
+
const magnitude = remaining < 0n ? -remaining : remaining;
|
|
3601
|
+
const formatted = formatAssetAmount(assetFromMinorUnits(magnitude, {
|
|
3602
|
+
asset: asset.asset,
|
|
3603
|
+
decimals: requireAsset(asset.asset).decimals
|
|
3604
|
+
}));
|
|
3605
|
+
return remaining < 0n ? `-${formatted}` : formatted;
|
|
3405
3606
|
}
|
|
3406
3607
|
/** Persisted replay keeps its authorized Budget and asset; a prior debit cannot block its own recovery. */
|
|
3407
3608
|
function runAuthorityFacts(draft, budgets, asset, money, funds) {
|
|
3408
3609
|
return {
|
|
3409
3610
|
budgetCount: draft !== null && !budgets.some((budget) => budget.id === draft.input.permissionId) ? 0 : budgets.length,
|
|
3410
|
-
hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.asset
|
|
3611
|
+
hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.input.asset === money?.asset),
|
|
3411
3612
|
funds: draft?.input.requestKey === void 0 ? funds : "available"
|
|
3412
3613
|
};
|
|
3413
3614
|
}
|
|
3414
3615
|
/**
|
|
3415
|
-
* The
|
|
3616
|
+
* The facts, in the ONE order the contract fixes. Exactly one code comes
|
|
3416
3617
|
* out, and it is always the first thing that is false — so two problems never
|
|
3417
3618
|
* race to name the button.
|
|
3418
3619
|
*/
|
|
@@ -3423,7 +3624,8 @@ function firstFalseFact(facts) {
|
|
|
3423
3624
|
if (!facts.hasAsset) return "no-asset";
|
|
3424
3625
|
if (facts.recipientCount === 0) return "no-recipients";
|
|
3425
3626
|
if (!facts.amountsParse) return "amount-invalid";
|
|
3426
|
-
|
|
3627
|
+
if (facts.funds === "insufficient") return "insufficient-funds";
|
|
3628
|
+
return facts.writeNamesAsset ? null : "asset-unnamed";
|
|
3427
3629
|
}
|
|
3428
3630
|
//#endregion
|
|
3429
3631
|
//#region src/headless/payroll/payroll-run.tsx
|
|
@@ -3433,7 +3635,7 @@ function usePayrollRunContext() {
|
|
|
3433
3635
|
if (engine === null) throw new Error("CapxulPayrollRun parts must be used inside <CapxulPayrollRun>");
|
|
3434
3636
|
return engine;
|
|
3435
3637
|
}
|
|
3436
|
-
function Root({ orgId, prefill, onRun, onFailed, children }) {
|
|
3638
|
+
function Root$1({ orgId, prefill, onRun, onFailed, children }) {
|
|
3437
3639
|
const engine = usePayrollRun({
|
|
3438
3640
|
orgId,
|
|
3439
3641
|
prefill,
|
|
@@ -3463,7 +3665,7 @@ function Actions({ children }) {
|
|
|
3463
3665
|
return /* @__PURE__ */ jsx(Fragment, { children: children(engine.actions) });
|
|
3464
3666
|
}
|
|
3465
3667
|
/** The compound shape the app's own design system already uses (`StatusTabs`). */
|
|
3466
|
-
const CapxulPayrollRun = Object.assign(Root, {
|
|
3668
|
+
const CapxulPayrollRun = Object.assign(Root$1, {
|
|
3467
3669
|
Recipients,
|
|
3468
3670
|
Amounts,
|
|
3469
3671
|
Asset,
|
|
@@ -3471,4 +3673,230 @@ const CapxulPayrollRun = Object.assign(Root, {
|
|
|
3471
3673
|
Actions
|
|
3472
3674
|
});
|
|
3473
3675
|
//#endregion
|
|
3474
|
-
|
|
3676
|
+
//#region src/headless/client-connection/use-client-connection.ts
|
|
3677
|
+
function useClientConnection(input) {
|
|
3678
|
+
const [view, setView] = useState(null);
|
|
3679
|
+
const [pending, setPending] = useState(false);
|
|
3680
|
+
const [error, setError] = useState(null);
|
|
3681
|
+
const [resendReady, setResendReady] = useState(false);
|
|
3682
|
+
const identityRef = useRef({
|
|
3683
|
+
client: input.client,
|
|
3684
|
+
requestId: input.requestId
|
|
3685
|
+
});
|
|
3686
|
+
identityRef.current = {
|
|
3687
|
+
client: input.client,
|
|
3688
|
+
requestId: input.requestId
|
|
3689
|
+
};
|
|
3690
|
+
const belongsHere = useCallback((client, requestId) => identityRef.current.client === client && identityRef.current.requestId === requestId, []);
|
|
3691
|
+
const refresh = useCallback(async (signal) => {
|
|
3692
|
+
const { client, requestId } = identityRef.current;
|
|
3693
|
+
const result = await client.auth.clientRequests.read({ requestId }, signal === void 0 ? void 0 : { signal });
|
|
3694
|
+
if (signal?.aborted || !belongsHere(client, requestId)) return;
|
|
3695
|
+
if (result.ok) {
|
|
3696
|
+
setView(result.value);
|
|
3697
|
+
setError(null);
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
setError(result.error);
|
|
3701
|
+
}, [belongsHere]);
|
|
3702
|
+
useEffect(() => {
|
|
3703
|
+
setView(null);
|
|
3704
|
+
setError(null);
|
|
3705
|
+
setPending(false);
|
|
3706
|
+
setResendReady(false);
|
|
3707
|
+
const controller = new AbortController();
|
|
3708
|
+
refresh(controller.signal);
|
|
3709
|
+
return () => {
|
|
3710
|
+
controller.abort();
|
|
3711
|
+
};
|
|
3712
|
+
}, [
|
|
3713
|
+
input.client,
|
|
3714
|
+
input.requestId,
|
|
3715
|
+
refresh
|
|
3716
|
+
]);
|
|
3717
|
+
useEffect(() => {
|
|
3718
|
+
if (view?.phase !== "otp_required") {
|
|
3719
|
+
setResendReady(false);
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
const remaining = view.resendAt - Date.now();
|
|
3723
|
+
if (remaining <= 0) {
|
|
3724
|
+
setResendReady(true);
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
setResendReady(false);
|
|
3728
|
+
const timer = setTimeout(() => setResendReady(true), remaining);
|
|
3729
|
+
return () => clearTimeout(timer);
|
|
3730
|
+
}, [view]);
|
|
3731
|
+
const run = useCallback(async (work) => {
|
|
3732
|
+
const { client, requestId } = identityRef.current;
|
|
3733
|
+
setPending(true);
|
|
3734
|
+
try {
|
|
3735
|
+
const result = await work();
|
|
3736
|
+
if (!belongsHere(client, requestId)) return;
|
|
3737
|
+
if (!result.ok) {
|
|
3738
|
+
setError(result.error);
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
await refresh();
|
|
3742
|
+
} finally {
|
|
3743
|
+
if (belongsHere(client, requestId)) setPending(false);
|
|
3744
|
+
}
|
|
3745
|
+
}, [belongsHere, refresh]);
|
|
3746
|
+
return useMemo(() => {
|
|
3747
|
+
if (error !== null && (view === null || view.phase !== "refused")) return {
|
|
3748
|
+
phase: "failure",
|
|
3749
|
+
otp: null,
|
|
3750
|
+
consent: null,
|
|
3751
|
+
complete: null,
|
|
3752
|
+
failure: {
|
|
3753
|
+
code: error.code,
|
|
3754
|
+
retry: () => refresh(),
|
|
3755
|
+
cancel: async () => {
|
|
3756
|
+
if (view === null) return;
|
|
3757
|
+
const { client, requestId } = identityRef.current;
|
|
3758
|
+
await run(() => client.auth.clientRequests.cancel({
|
|
3759
|
+
requestId,
|
|
3760
|
+
revision: view.revision
|
|
3761
|
+
}));
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
};
|
|
3765
|
+
if (view === null) return {
|
|
3766
|
+
phase: "checking",
|
|
3767
|
+
otp: null,
|
|
3768
|
+
consent: null,
|
|
3769
|
+
complete: null,
|
|
3770
|
+
failure: null
|
|
3771
|
+
};
|
|
3772
|
+
if (view.phase === "otp_required") return {
|
|
3773
|
+
phase: view.phase,
|
|
3774
|
+
otp: {
|
|
3775
|
+
email: view.email,
|
|
3776
|
+
pending,
|
|
3777
|
+
canResend: resendReady,
|
|
3778
|
+
verifyCode: async (code) => {
|
|
3779
|
+
const { client, requestId } = identityRef.current;
|
|
3780
|
+
await run(() => client.auth.clientRequests.verifyOtp({
|
|
3781
|
+
requestId,
|
|
3782
|
+
code
|
|
3783
|
+
}));
|
|
3784
|
+
},
|
|
3785
|
+
resend: async () => {
|
|
3786
|
+
const { client, requestId } = identityRef.current;
|
|
3787
|
+
await run(() => client.auth.clientRequests.sendOtp({ requestId }));
|
|
3788
|
+
}
|
|
3789
|
+
},
|
|
3790
|
+
consent: null,
|
|
3791
|
+
complete: null,
|
|
3792
|
+
failure: null
|
|
3793
|
+
};
|
|
3794
|
+
if (view.phase === "review") return {
|
|
3795
|
+
phase: view.phase,
|
|
3796
|
+
otp: null,
|
|
3797
|
+
consent: {
|
|
3798
|
+
verifiedEmail: view.email,
|
|
3799
|
+
clientLabel: view.clientLabel,
|
|
3800
|
+
scopeCodes: view.scopes,
|
|
3801
|
+
purpose: view.intent.mode,
|
|
3802
|
+
setupState: view.setupState,
|
|
3803
|
+
pending,
|
|
3804
|
+
accept: async () => {
|
|
3805
|
+
const { client, requestId } = identityRef.current;
|
|
3806
|
+
await run(() => client.auth.clientRequests.accept({
|
|
3807
|
+
requestId,
|
|
3808
|
+
revision: view.revision
|
|
3809
|
+
}));
|
|
3810
|
+
},
|
|
3811
|
+
cancel: async () => {
|
|
3812
|
+
const { client, requestId } = identityRef.current;
|
|
3813
|
+
await run(() => client.auth.clientRequests.cancel({
|
|
3814
|
+
requestId,
|
|
3815
|
+
revision: view.revision
|
|
3816
|
+
}));
|
|
3817
|
+
}
|
|
3818
|
+
},
|
|
3819
|
+
complete: null,
|
|
3820
|
+
failure: null
|
|
3821
|
+
};
|
|
3822
|
+
if (view.phase === "connected") return {
|
|
3823
|
+
phase: view.phase,
|
|
3824
|
+
otp: null,
|
|
3825
|
+
consent: null,
|
|
3826
|
+
complete: { outcome: "connected" },
|
|
3827
|
+
failure: null
|
|
3828
|
+
};
|
|
3829
|
+
if (view.phase === "refused") return {
|
|
3830
|
+
phase: view.phase,
|
|
3831
|
+
otp: null,
|
|
3832
|
+
consent: null,
|
|
3833
|
+
complete: null,
|
|
3834
|
+
failure: {
|
|
3835
|
+
code: view.code,
|
|
3836
|
+
...view.field === void 0 ? {} : { field: view.field },
|
|
3837
|
+
retry: () => refresh(),
|
|
3838
|
+
cancel: async () => void 0
|
|
3839
|
+
}
|
|
3840
|
+
};
|
|
3841
|
+
return {
|
|
3842
|
+
phase: view.phase,
|
|
3843
|
+
otp: null,
|
|
3844
|
+
consent: null,
|
|
3845
|
+
complete: null,
|
|
3846
|
+
failure: null
|
|
3847
|
+
};
|
|
3848
|
+
}, [
|
|
3849
|
+
error,
|
|
3850
|
+
pending,
|
|
3851
|
+
refresh,
|
|
3852
|
+
resendReady,
|
|
3853
|
+
run,
|
|
3854
|
+
view
|
|
3855
|
+
]);
|
|
3856
|
+
}
|
|
3857
|
+
//#endregion
|
|
3858
|
+
//#region src/headless/client-connection/client-connection.tsx
|
|
3859
|
+
const ConnectionContext = createContext(null);
|
|
3860
|
+
function useConnection() {
|
|
3861
|
+
const engine = useContext(ConnectionContext);
|
|
3862
|
+
if (engine === null) throw new Error("CapxulClientConnection parts must be used inside <CapxulClientConnection>");
|
|
3863
|
+
return engine;
|
|
3864
|
+
}
|
|
3865
|
+
function Root({ client, requestId, children }) {
|
|
3866
|
+
const engine = useClientConnection({
|
|
3867
|
+
client,
|
|
3868
|
+
requestId
|
|
3869
|
+
});
|
|
3870
|
+
return /* @__PURE__ */ jsx(ConnectionContext.Provider, {
|
|
3871
|
+
value: engine,
|
|
3872
|
+
children
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
function Checking({ children }) {
|
|
3876
|
+
return useConnection().phase === "checking" ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
|
|
3877
|
+
}
|
|
3878
|
+
function Otp({ children }) {
|
|
3879
|
+
const otp = useConnection().otp;
|
|
3880
|
+
return otp === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(otp) });
|
|
3881
|
+
}
|
|
3882
|
+
function Consent({ children }) {
|
|
3883
|
+
const consent = useConnection().consent;
|
|
3884
|
+
return consent === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(consent) });
|
|
3885
|
+
}
|
|
3886
|
+
function Complete({ children }) {
|
|
3887
|
+
const complete = useConnection().complete;
|
|
3888
|
+
return complete === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(complete) });
|
|
3889
|
+
}
|
|
3890
|
+
function Failure({ children }) {
|
|
3891
|
+
const failure = useConnection().failure;
|
|
3892
|
+
return failure === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(failure) });
|
|
3893
|
+
}
|
|
3894
|
+
const CapxulClientConnection = Object.assign(Root, {
|
|
3895
|
+
Checking,
|
|
3896
|
+
Otp,
|
|
3897
|
+
Consent,
|
|
3898
|
+
Complete,
|
|
3899
|
+
Failure
|
|
3900
|
+
});
|
|
3901
|
+
//#endregion
|
|
3902
|
+
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 };
|