@stripe/link-cli 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/cli.js +350 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,6 +124,8 @@ The `--request-approval` flag triggers a push notification to the user for appro
|
|
|
124
124
|
|
|
125
125
|
Easily approve requests with the [Link app](https://link.com/download).
|
|
126
126
|
|
|
127
|
+
If the created spend request comes back with `status: "requires_action"`, no approval is needed yet — the payment method or account needs attention first. Check `status_details.requires_action.next_action` for `type`, `display_message`, `action_url`, and `resolution`. For 3D Secure (`resolution: "auto_resume"`), keep polling `spend-request retrieve` — the request resolves on its own once the challenge is completed. For any other resolution, complete the indicated action and create a new spend request.
|
|
128
|
+
|
|
127
129
|
#### Line items and totals
|
|
128
130
|
|
|
129
131
|
`--line-item` and `--total` use repeatable `key:value` format.
|
|
@@ -223,7 +225,7 @@ For agent polling, pass `--interval` and optionally `--max-attempts`:
|
|
|
223
225
|
link-cli spend-request retrieve lsrq_001 --interval 2 --max-attempts 300
|
|
224
226
|
```
|
|
225
227
|
|
|
226
|
-
Polling exits successfully only after the request reaches a terminal status such as `approved`, `denied`, `expired`, or `canceled`. If polling
|
|
228
|
+
Polling exits successfully only after the request reaches a terminal status such as `approved`, `denied`, `expired`, or `canceled`. If the status becomes `requires_action`, behavior depends on `next_action.resolution`: `auto_resume` (used for 3D Secure) means polling continues automatically — the request resolves on its own once the user completes the challenge. Any other resolution stops polling immediately and the command exits with the `next_action` details instead of waiting for a terminal status; the caller must have the user act, then create a new spend request. If `--timeout` is reached or `--max-attempts` is exhausted while the request is still non-terminal, the command exits non-zero with `code: "POLLING_TIMEOUT"` so callers do not treat a still-pending request as complete.
|
|
227
229
|
|
|
228
230
|
If the merchant supports MPP, use `link-cli mpp pay` instead:
|
|
229
231
|
|
|
@@ -285,7 +287,7 @@ Link Pay Token requests require `execution_method=link_pay_token` and the
|
|
|
285
287
|
DOM-derived `merchant_account_id`, and Link supplies their canonical merchant
|
|
286
288
|
identity.
|
|
287
289
|
|
|
288
|
-
**Constraints:** `context` must be at least 100 characters; `amount` must not exceed
|
|
290
|
+
**Constraints:** `context` must be at least 100 characters; `amount` must not exceed 50000 (cents); `currency` must be a 3-letter ISO code. The user has 10 minutes from when approval is requested to approve. Approved credentials (card or SPT) are valid for 12 hours from spend request creation.
|
|
289
291
|
**Test mode:** Pass `--test` to create a testmode SpendRequest. A testmode SpendRequest will return test payment credentials (e.g test card `4000009990001984`) rather than a real payment credential. Testmode SpendRequests will not charge the underlying payment method of the SpendRequest. This is useful for development and integration testing without real payment methods.
|
|
290
292
|
|
|
291
293
|
```bash
|
|
@@ -307,10 +309,10 @@ link-cli spend-request cancel lsrq_001
|
|
|
307
309
|
|
|
308
310
|
| Limit | Value |
|
|
309
311
|
|-------|-------|
|
|
310
|
-
| Max amount per spend request | $
|
|
312
|
+
| Max amount per spend request | $500 (50,000 cents) |
|
|
311
313
|
| Approval window | 10 minutes — user must approve within 10 min of `request-approval` |
|
|
312
314
|
| Card / SPT validity | 12 hours from spend request creation |
|
|
313
|
-
| Daily spend | $
|
|
315
|
+
| Daily spend | $500 |
|
|
314
316
|
| Monthly spend (30 days) | $20,000 |
|
|
315
317
|
| Concurrent active requests (created + approved) | 30 |
|
|
316
318
|
| Concurrent approved requests | 10 |
|
package/dist/cli.js
CHANGED
|
@@ -11885,6 +11885,13 @@ function normalizeSpendRequest(data) {
|
|
|
11885
11885
|
}
|
|
11886
11886
|
return sr;
|
|
11887
11887
|
}
|
|
11888
|
+
function getDuplicateSpendRequest(error) {
|
|
11889
|
+
if (!(error instanceof LinkApiError)) return null;
|
|
11890
|
+
const details = error.details;
|
|
11891
|
+
const duplicate = details?.error?.duplicate_spend_request;
|
|
11892
|
+
if (!duplicate || typeof duplicate !== "object") return null;
|
|
11893
|
+
return normalizeSpendRequest(duplicate);
|
|
11894
|
+
}
|
|
11888
11895
|
function extractApiError(data, rawBody) {
|
|
11889
11896
|
if (data && typeof data === "object") {
|
|
11890
11897
|
const body = data;
|
|
@@ -12771,6 +12778,8 @@ import { useEffect, useState } from "react";
|
|
|
12771
12778
|
|
|
12772
12779
|
// src/utils/constants.ts
|
|
12773
12780
|
var DISPLAY_DELAY_MS = 1500;
|
|
12781
|
+
var RESUME_POLL_INTERVAL_MS = 2e3;
|
|
12782
|
+
var RESUME_TIMEOUT_MS = 6e5;
|
|
12774
12783
|
|
|
12775
12784
|
// src/utils/open-url.ts
|
|
12776
12785
|
import { spawn } from "child_process";
|
|
@@ -16488,6 +16497,21 @@ import { Box as Box18, Text as Text20, useApp as useApp4, useInput as useInput8
|
|
|
16488
16497
|
import Spinner10 from "ink-spinner";
|
|
16489
16498
|
import { useCallback as useCallback8, useEffect as useEffect9, useState as useState9 } from "react";
|
|
16490
16499
|
|
|
16500
|
+
// src/utils/format-amount.ts
|
|
16501
|
+
function formatAmount(amount, currency) {
|
|
16502
|
+
const currencyCode = currency.toUpperCase();
|
|
16503
|
+
try {
|
|
16504
|
+
const formatter = new Intl.NumberFormat("en-US", {
|
|
16505
|
+
style: "currency",
|
|
16506
|
+
currency: currencyCode
|
|
16507
|
+
});
|
|
16508
|
+
const fractionDigits = formatter.resolvedOptions().maximumFractionDigits ?? 2;
|
|
16509
|
+
return formatter.format(amount / 10 ** fractionDigits);
|
|
16510
|
+
} catch {
|
|
16511
|
+
return `${amount} ${currency}`;
|
|
16512
|
+
}
|
|
16513
|
+
}
|
|
16514
|
+
|
|
16491
16515
|
// src/commands/spend-request/approval-waiting-view.tsx
|
|
16492
16516
|
import { Box as Box17, Text as Text19 } from "ink";
|
|
16493
16517
|
import Spinner9 from "ink-spinner";
|
|
@@ -16532,7 +16556,8 @@ function useApprovalPolling({
|
|
|
16532
16556
|
requestId,
|
|
16533
16557
|
onComplete,
|
|
16534
16558
|
onSuccess,
|
|
16535
|
-
onError
|
|
16559
|
+
onError,
|
|
16560
|
+
onRequiresAction
|
|
16536
16561
|
}) {
|
|
16537
16562
|
const isWaiting = status === "waiting" || status === "polling";
|
|
16538
16563
|
useInput7(
|
|
@@ -16553,6 +16578,12 @@ function useApprovalPolling({
|
|
|
16553
16578
|
try {
|
|
16554
16579
|
const final = await pollUntilApproved(repository, requestId);
|
|
16555
16580
|
if (cancelled) return;
|
|
16581
|
+
if (final.status === "requires_action") {
|
|
16582
|
+
onRequiresAction(final);
|
|
16583
|
+
setStatus("requires_action");
|
|
16584
|
+
setTimeout(() => onComplete(final), DISPLAY_DELAY_MS);
|
|
16585
|
+
return;
|
|
16586
|
+
}
|
|
16556
16587
|
if (final.status !== "approved") {
|
|
16557
16588
|
onError(
|
|
16558
16589
|
`Spend request did not reach approved (status: ${final.status})`
|
|
@@ -16582,6 +16613,7 @@ function useApprovalPolling({
|
|
|
16582
16613
|
onComplete,
|
|
16583
16614
|
onSuccess,
|
|
16584
16615
|
onError,
|
|
16616
|
+
onRequiresAction,
|
|
16585
16617
|
setStatus
|
|
16586
16618
|
]);
|
|
16587
16619
|
}
|
|
@@ -16599,12 +16631,16 @@ var CreateSpendRequest = ({
|
|
|
16599
16631
|
const { exit } = useApp4();
|
|
16600
16632
|
const [status, setStatus] = useState9("creating");
|
|
16601
16633
|
const [request, setRequest] = useState9(null);
|
|
16634
|
+
const [duplicateRequest, setDuplicateRequest] = useState9(
|
|
16635
|
+
null
|
|
16636
|
+
);
|
|
16602
16637
|
const [error, setError] = useState9("");
|
|
16603
16638
|
const [verificationUrl, setVerificationUrl] = useState9("");
|
|
16604
16639
|
const [supportUrl, setSupportUrl] = useState9("");
|
|
16605
16640
|
const [countdown, setCountdown] = useState9(30);
|
|
16606
16641
|
const [outputFilePath, setOutputFilePath] = useState9(null);
|
|
16607
16642
|
const [fileError, setFileError] = useState9("");
|
|
16643
|
+
const [nextAction, setNextAction] = useState9(null);
|
|
16608
16644
|
const approvalUrl = request?.approval_url ?? "";
|
|
16609
16645
|
const completeAndExit = useCallback8(
|
|
16610
16646
|
(result) => {
|
|
@@ -16618,6 +16654,10 @@ var CreateSpendRequest = ({
|
|
|
16618
16654
|
[]
|
|
16619
16655
|
);
|
|
16620
16656
|
const onError = useCallback8((msg) => setError(msg), []);
|
|
16657
|
+
const onRequiresAction = useCallback8((result) => {
|
|
16658
|
+
setRequest(result);
|
|
16659
|
+
setNextAction(result.status_details?.requires_action?.next_action ?? null);
|
|
16660
|
+
}, []);
|
|
16621
16661
|
useApprovalPolling({
|
|
16622
16662
|
status,
|
|
16623
16663
|
setStatus,
|
|
@@ -16626,8 +16666,66 @@ var CreateSpendRequest = ({
|
|
|
16626
16666
|
requestId: request?.id ?? null,
|
|
16627
16667
|
onComplete: completeAndExit,
|
|
16628
16668
|
onSuccess,
|
|
16629
|
-
onError
|
|
16669
|
+
onError,
|
|
16670
|
+
onRequiresAction
|
|
16630
16671
|
});
|
|
16672
|
+
useInput8(
|
|
16673
|
+
(_input, key) => {
|
|
16674
|
+
if (key.return && nextAction?.action_url) {
|
|
16675
|
+
openUrl(nextAction.action_url);
|
|
16676
|
+
completeAndExit(request);
|
|
16677
|
+
}
|
|
16678
|
+
},
|
|
16679
|
+
{
|
|
16680
|
+
isActive: status === "requires_action" && nextAction?.resolution !== "auto_resume"
|
|
16681
|
+
}
|
|
16682
|
+
);
|
|
16683
|
+
useEffect9(() => {
|
|
16684
|
+
if (status !== "requires_action") return;
|
|
16685
|
+
if (nextAction?.resolution === "auto_resume") {
|
|
16686
|
+
setStatus("resuming");
|
|
16687
|
+
}
|
|
16688
|
+
}, [status, nextAction]);
|
|
16689
|
+
useEffect9(() => {
|
|
16690
|
+
if (status !== "resuming" || !request?.id) return;
|
|
16691
|
+
let cancelled = false;
|
|
16692
|
+
const requestId = request.id;
|
|
16693
|
+
const deadline = Date.now() + RESUME_TIMEOUT_MS;
|
|
16694
|
+
const poll = async () => {
|
|
16695
|
+
while (!cancelled) {
|
|
16696
|
+
if (Date.now() > deadline) {
|
|
16697
|
+
setStatus("resume_timeout");
|
|
16698
|
+
setTimeout(() => completeAndExit(request), DISPLAY_DELAY_MS);
|
|
16699
|
+
return;
|
|
16700
|
+
}
|
|
16701
|
+
await new Promise((r) => setTimeout(r, RESUME_POLL_INTERVAL_MS));
|
|
16702
|
+
if (cancelled) return;
|
|
16703
|
+
let latest;
|
|
16704
|
+
try {
|
|
16705
|
+
latest = await repository.getSpendRequest(requestId);
|
|
16706
|
+
} catch {
|
|
16707
|
+
continue;
|
|
16708
|
+
}
|
|
16709
|
+
if (cancelled || !latest) continue;
|
|
16710
|
+
setRequest(latest);
|
|
16711
|
+
if (latest.status === "requires_action") continue;
|
|
16712
|
+
if (latest.status === "approved" || latest.status === "succeeded") {
|
|
16713
|
+
setStatus("success");
|
|
16714
|
+
} else {
|
|
16715
|
+
setError(
|
|
16716
|
+
`Spend request did not resolve after 3D Secure (status: ${latest.status})`
|
|
16717
|
+
);
|
|
16718
|
+
setStatus("error");
|
|
16719
|
+
}
|
|
16720
|
+
setTimeout(() => completeAndExit(latest), DISPLAY_DELAY_MS);
|
|
16721
|
+
return;
|
|
16722
|
+
}
|
|
16723
|
+
};
|
|
16724
|
+
poll();
|
|
16725
|
+
return () => {
|
|
16726
|
+
cancelled = true;
|
|
16727
|
+
};
|
|
16728
|
+
}, [status, request, repository, completeAndExit]);
|
|
16631
16729
|
useInput8((_, key) => {
|
|
16632
16730
|
if (key.return && (verificationUrl || supportUrl) && status === "verification_required") {
|
|
16633
16731
|
openUrl(verificationUrl || supportUrl);
|
|
@@ -16644,12 +16742,27 @@ var CreateSpendRequest = ({
|
|
|
16644
16742
|
const timer = setTimeout(() => setCountdown((c) => c - 1), 1e3);
|
|
16645
16743
|
return () => clearTimeout(timer);
|
|
16646
16744
|
}, [status, countdown, completeAndExit]);
|
|
16745
|
+
useEffect9(() => {
|
|
16746
|
+
if (status !== "requires_action") return;
|
|
16747
|
+
if (nextAction?.resolution === "auto_resume") return;
|
|
16748
|
+
if (countdown <= 0) {
|
|
16749
|
+
completeAndExit(request);
|
|
16750
|
+
return;
|
|
16751
|
+
}
|
|
16752
|
+
const timer = setTimeout(() => setCountdown((c) => c - 1), 1e3);
|
|
16753
|
+
return () => clearTimeout(timer);
|
|
16754
|
+
}, [status, nextAction, countdown, completeAndExit, request]);
|
|
16647
16755
|
useEffect9(() => {
|
|
16648
16756
|
const create = async () => {
|
|
16649
16757
|
try {
|
|
16650
16758
|
const result = await repository.createSpendRequest(params);
|
|
16651
16759
|
setRequest(result);
|
|
16652
|
-
if (
|
|
16760
|
+
if (result.status === "requires_action") {
|
|
16761
|
+
setNextAction(
|
|
16762
|
+
result.status_details?.requires_action?.next_action ?? null
|
|
16763
|
+
);
|
|
16764
|
+
setStatus("requires_action");
|
|
16765
|
+
} else if (requestApproval) {
|
|
16653
16766
|
setStatus("waiting");
|
|
16654
16767
|
} else {
|
|
16655
16768
|
setStatus("success");
|
|
@@ -16667,6 +16780,8 @@ var CreateSpendRequest = ({
|
|
|
16667
16780
|
setStatus("verification_required");
|
|
16668
16781
|
return;
|
|
16669
16782
|
}
|
|
16783
|
+
const duplicate = getDuplicateSpendRequest(err);
|
|
16784
|
+
if (duplicate) setDuplicateRequest(sanitizeDeep(duplicate));
|
|
16670
16785
|
}
|
|
16671
16786
|
setStatus("error");
|
|
16672
16787
|
setTimeout(() => completeAndExit(null), DISPLAY_DELAY_MS);
|
|
@@ -16724,6 +16839,72 @@ var CreateSpendRequest = ({
|
|
|
16724
16839
|
/* @__PURE__ */ jsx27(Text20, { color: "green", children: "\u2713 Opened verification URL in browser" })
|
|
16725
16840
|
] });
|
|
16726
16841
|
}
|
|
16842
|
+
if (status === "requires_action") {
|
|
16843
|
+
return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
|
|
16844
|
+
/* @__PURE__ */ jsx27(Text20, { color: "yellow", children: "\u26A0 Action required before payment can proceed" }),
|
|
16845
|
+
/* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [
|
|
16846
|
+
/* @__PURE__ */ jsxs18(Text20, { children: [
|
|
16847
|
+
"ID: ",
|
|
16848
|
+
/* @__PURE__ */ jsx27(Text20, { bold: true, children: request?.id })
|
|
16849
|
+
] }),
|
|
16850
|
+
/* @__PURE__ */ jsxs18(Text20, { children: [
|
|
16851
|
+
"Type: ",
|
|
16852
|
+
/* @__PURE__ */ jsx27(Text20, { bold: true, children: nextAction?.type })
|
|
16853
|
+
] }),
|
|
16854
|
+
/* @__PURE__ */ jsx27(Text20, { children: nextAction?.display_message })
|
|
16855
|
+
] }),
|
|
16856
|
+
nextAction?.action_url && /* @__PURE__ */ jsxs18(
|
|
16857
|
+
Box18,
|
|
16858
|
+
{
|
|
16859
|
+
flexDirection: "column",
|
|
16860
|
+
borderStyle: "round",
|
|
16861
|
+
borderColor: "cyan",
|
|
16862
|
+
paddingX: 2,
|
|
16863
|
+
paddingY: 1,
|
|
16864
|
+
marginTop: 1,
|
|
16865
|
+
children: [
|
|
16866
|
+
/* @__PURE__ */ jsxs18(Text20, { children: [
|
|
16867
|
+
"Open:",
|
|
16868
|
+
" ",
|
|
16869
|
+
/* @__PURE__ */ jsx27(Text20, { bold: true, color: "cyan", children: nextAction.action_url })
|
|
16870
|
+
] }),
|
|
16871
|
+
/* @__PURE__ */ jsx27(Text20, { dimColor: true, children: "Press Enter to open in browser" }),
|
|
16872
|
+
/* @__PURE__ */ jsxs18(Text20, { dimColor: true, children: [
|
|
16873
|
+
"Exiting in ",
|
|
16874
|
+
countdown,
|
|
16875
|
+
"s..."
|
|
16876
|
+
] })
|
|
16877
|
+
]
|
|
16878
|
+
}
|
|
16879
|
+
),
|
|
16880
|
+
/* @__PURE__ */ jsx27(Box18, { marginTop: 1, children: /* @__PURE__ */ jsx27(Text20, { dimColor: true, children: "Complete this step, then create a new spend request." }) })
|
|
16881
|
+
] });
|
|
16882
|
+
}
|
|
16883
|
+
if (status === "resuming") {
|
|
16884
|
+
return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
|
|
16885
|
+
/* @__PURE__ */ jsxs18(Text20, { color: "cyan", children: [
|
|
16886
|
+
/* @__PURE__ */ jsx27(Spinner10, { type: "dots" }),
|
|
16887
|
+
" Waiting for 3D Secure verification to complete..."
|
|
16888
|
+
] }),
|
|
16889
|
+
/* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [
|
|
16890
|
+
/* @__PURE__ */ jsx27(Text20, { children: nextAction?.display_message }),
|
|
16891
|
+
nextAction?.action_url && /* @__PURE__ */ jsxs18(Text20, { dimColor: true, children: [
|
|
16892
|
+
"URL: ",
|
|
16893
|
+
/* @__PURE__ */ jsx27(Text20, { color: "cyan", children: nextAction.action_url })
|
|
16894
|
+
] })
|
|
16895
|
+
] })
|
|
16896
|
+
] });
|
|
16897
|
+
}
|
|
16898
|
+
if (status === "resume_timeout") {
|
|
16899
|
+
return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
|
|
16900
|
+
/* @__PURE__ */ jsx27(Text20, { color: "yellow", children: "\u2717 Timed out waiting for 3D Secure verification to resolve" }),
|
|
16901
|
+
/* @__PURE__ */ jsxs18(Text20, { dimColor: true, children: [
|
|
16902
|
+
"Run `spend-request retrieve ",
|
|
16903
|
+
request?.id,
|
|
16904
|
+
"` to check the current status."
|
|
16905
|
+
] })
|
|
16906
|
+
] });
|
|
16907
|
+
}
|
|
16727
16908
|
if (status === "creating") {
|
|
16728
16909
|
return /* @__PURE__ */ jsx27(Box18, { children: /* @__PURE__ */ jsxs18(Text20, { color: "cyan", children: [
|
|
16729
16910
|
/* @__PURE__ */ jsx27(Spinner10, { type: "dots" }),
|
|
@@ -16733,7 +16914,53 @@ var CreateSpendRequest = ({
|
|
|
16733
16914
|
if (status === "error") {
|
|
16734
16915
|
return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", children: [
|
|
16735
16916
|
/* @__PURE__ */ jsx27(Text20, { color: "red", children: "\u2717 Failed to create spend request" }),
|
|
16736
|
-
/* @__PURE__ */ jsx27(Text20, { color: "red", children: error })
|
|
16917
|
+
/* @__PURE__ */ jsx27(Text20, { color: "red", children: error }),
|
|
16918
|
+
duplicateRequest && /* @__PURE__ */ jsxs18(
|
|
16919
|
+
Box18,
|
|
16920
|
+
{
|
|
16921
|
+
flexDirection: "column",
|
|
16922
|
+
borderStyle: "round",
|
|
16923
|
+
borderColor: "yellow",
|
|
16924
|
+
paddingX: 2,
|
|
16925
|
+
paddingY: 1,
|
|
16926
|
+
marginTop: 1,
|
|
16927
|
+
children: [
|
|
16928
|
+
/* @__PURE__ */ jsx27(Text20, { bold: true, color: "yellow", children: "A matching spend request already exists" }),
|
|
16929
|
+
/* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", marginTop: 1, children: [
|
|
16930
|
+
/* @__PURE__ */ jsxs18(Text20, { children: [
|
|
16931
|
+
"ID: ",
|
|
16932
|
+
/* @__PURE__ */ jsx27(Text20, { bold: true, children: duplicateRequest.id })
|
|
16933
|
+
] }),
|
|
16934
|
+
/* @__PURE__ */ jsxs18(Text20, { children: [
|
|
16935
|
+
"Status: ",
|
|
16936
|
+
/* @__PURE__ */ jsx27(Text20, { bold: true, children: duplicateRequest.status })
|
|
16937
|
+
] }),
|
|
16938
|
+
/* @__PURE__ */ jsxs18(Text20, { children: [
|
|
16939
|
+
"Amount:",
|
|
16940
|
+
" ",
|
|
16941
|
+
/* @__PURE__ */ jsx27(Text20, { bold: true, children: duplicateRequest.amount != null ? formatAmount(
|
|
16942
|
+
duplicateRequest.amount,
|
|
16943
|
+
duplicateRequest.currency ?? ""
|
|
16944
|
+
) : "N/A" })
|
|
16945
|
+
] }),
|
|
16946
|
+
/* @__PURE__ */ jsxs18(Text20, { children: [
|
|
16947
|
+
"Merchant: ",
|
|
16948
|
+
/* @__PURE__ */ jsx27(Text20, { bold: true, children: duplicateRequest.merchant_name })
|
|
16949
|
+
] })
|
|
16950
|
+
] }),
|
|
16951
|
+
duplicateRequest.status !== "expired" && duplicateRequest.status !== "canceled" && duplicateRequest.status !== "failed" && /* @__PURE__ */ jsxs18(Fragment4, { children: [
|
|
16952
|
+
/* @__PURE__ */ jsxs18(Text20, { dimColor: true, children: [
|
|
16953
|
+
"\n",
|
|
16954
|
+
"Retrieve it to resume instead of creating a new one:"
|
|
16955
|
+
] }),
|
|
16956
|
+
/* @__PURE__ */ jsxs18(Text20, { color: "cyan", children: [
|
|
16957
|
+
"spend-request retrieve ",
|
|
16958
|
+
duplicateRequest.id
|
|
16959
|
+
] })
|
|
16960
|
+
] })
|
|
16961
|
+
]
|
|
16962
|
+
}
|
|
16963
|
+
)
|
|
16737
16964
|
] });
|
|
16738
16965
|
}
|
|
16739
16966
|
if (status === "success") {
|
|
@@ -16751,7 +16978,7 @@ var CreateSpendRequest = ({
|
|
|
16751
16978
|
/* @__PURE__ */ jsxs18(Text20, { children: [
|
|
16752
16979
|
"Amount:",
|
|
16753
16980
|
" ",
|
|
16754
|
-
/* @__PURE__ */ jsx27(Text20, { bold: true, children: request?.amount != null ?
|
|
16981
|
+
/* @__PURE__ */ jsx27(Text20, { bold: true, children: request?.amount != null ? formatAmount(request.amount, request.currency ?? "") : "N/A" })
|
|
16755
16982
|
] }),
|
|
16756
16983
|
/* @__PURE__ */ jsxs18(Text20, { children: [
|
|
16757
16984
|
"Merchant: ",
|
|
@@ -16874,7 +17101,7 @@ var SpendRequestList = ({
|
|
|
16874
17101
|
return /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", children: [
|
|
16875
17102
|
/* @__PURE__ */ jsx28(Text21, { bold: true, children: includeHistory ? "All Spend Requests" : "Active Spend Requests" }),
|
|
16876
17103
|
/* @__PURE__ */ jsx28(Box19, { flexDirection: "column", marginTop: 1, children: requests.map((sr) => {
|
|
16877
|
-
const statusColor = sr.status === "approved" ? "green" : sr.status === "pending_approval" ? "yellow" : "white";
|
|
17104
|
+
const statusColor = sr.status === "approved" ? "green" : sr.status === "pending_approval" || sr.status === "requires_action" ? "yellow" : "white";
|
|
16878
17105
|
const amount = sr.amount != null ? `$${(sr.amount / 100).toFixed(2)} ${(sr.currency ?? "usd").toUpperCase()}` : "";
|
|
16879
17106
|
return /* @__PURE__ */ jsx28(Box19, { paddingX: 2, children: /* @__PURE__ */ jsxs19(Text21, { children: [
|
|
16880
17107
|
/* @__PURE__ */ jsx28(Text21, { dimColor: true, children: sr.id }),
|
|
@@ -16912,8 +17139,13 @@ var RequestApproval = ({
|
|
|
16912
17139
|
const [verificationUrl, setVerificationUrl] = useState10("");
|
|
16913
17140
|
const [supportUrl, setSupportUrl] = useState10("");
|
|
16914
17141
|
const [countdown, setCountdown] = useState10(30);
|
|
17142
|
+
const [nextAction, setNextAction] = useState10(null);
|
|
16915
17143
|
const onSuccess = useCallback10((r) => setResult(r), []);
|
|
16916
17144
|
const onError = useCallback10((msg) => setError(msg), []);
|
|
17145
|
+
const onRequiresAction = useCallback10((r) => {
|
|
17146
|
+
setResult(r);
|
|
17147
|
+
setNextAction(r.status_details?.requires_action?.next_action ?? null);
|
|
17148
|
+
}, []);
|
|
16917
17149
|
useApprovalPolling({
|
|
16918
17150
|
status,
|
|
16919
17151
|
setStatus,
|
|
@@ -16922,7 +17154,8 @@ var RequestApproval = ({
|
|
|
16922
17154
|
requestId: id,
|
|
16923
17155
|
onComplete: completeAndExit,
|
|
16924
17156
|
onSuccess,
|
|
16925
|
-
onError
|
|
17157
|
+
onError,
|
|
17158
|
+
onRequiresAction
|
|
16926
17159
|
});
|
|
16927
17160
|
useInput9((_, key) => {
|
|
16928
17161
|
if (key.return && (verificationUrl || supportUrl) && status === "verification_required") {
|
|
@@ -17006,6 +17239,22 @@ var RequestApproval = ({
|
|
|
17006
17239
|
/* @__PURE__ */ jsx29(Text22, { color: "green", children: "\u2713 Opened verification URL in browser" })
|
|
17007
17240
|
] });
|
|
17008
17241
|
}
|
|
17242
|
+
if (status === "requires_action") {
|
|
17243
|
+
return /* @__PURE__ */ jsxs20(Box20, { flexDirection: "column", children: [
|
|
17244
|
+
/* @__PURE__ */ jsx29(Text22, { color: "yellow", children: "\u26A0 Action required before payment can proceed" }),
|
|
17245
|
+
/* @__PURE__ */ jsxs20(Box20, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [
|
|
17246
|
+
/* @__PURE__ */ jsxs20(Text22, { children: [
|
|
17247
|
+
"ID: ",
|
|
17248
|
+
/* @__PURE__ */ jsx29(Text22, { bold: true, children: result?.id })
|
|
17249
|
+
] }),
|
|
17250
|
+
/* @__PURE__ */ jsx29(Text22, { children: nextAction?.display_message }),
|
|
17251
|
+
nextAction?.action_url && /* @__PURE__ */ jsxs20(Text22, { children: [
|
|
17252
|
+
"URL: ",
|
|
17253
|
+
/* @__PURE__ */ jsx29(Text22, { color: "cyan", children: nextAction.action_url })
|
|
17254
|
+
] })
|
|
17255
|
+
] })
|
|
17256
|
+
] });
|
|
17257
|
+
}
|
|
17009
17258
|
if (status === "requesting") {
|
|
17010
17259
|
return /* @__PURE__ */ jsx29(Box20, { children: /* @__PURE__ */ jsxs20(Text22, { color: "cyan", children: [
|
|
17011
17260
|
/* @__PURE__ */ jsx29(Spinner12, { type: "dots" }),
|
|
@@ -17068,6 +17317,9 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
|
17068
17317
|
"failed",
|
|
17069
17318
|
"canceled"
|
|
17070
17319
|
]);
|
|
17320
|
+
function isAutoResume(request) {
|
|
17321
|
+
return request.status_details?.requires_action?.next_action?.resolution === "auto_resume";
|
|
17322
|
+
}
|
|
17071
17323
|
var RetrieveSpendRequest = ({
|
|
17072
17324
|
repository,
|
|
17073
17325
|
id,
|
|
@@ -17123,6 +17375,9 @@ var RetrieveSpendRequest = ({
|
|
|
17123
17375
|
} else if (result.status === "denied") {
|
|
17124
17376
|
setPhase("declined");
|
|
17125
17377
|
setTimeout(() => onComplete(result), DISPLAY_DELAY_MS);
|
|
17378
|
+
} else if (result.status === "requires_action" && !isAutoResume(result)) {
|
|
17379
|
+
setPhase("requires_action");
|
|
17380
|
+
setTimeout(() => onComplete(result), DISPLAY_DELAY_MS);
|
|
17126
17381
|
} else if (TERMINAL_STATUSES.has(result.status)) {
|
|
17127
17382
|
setPhase("finalized");
|
|
17128
17383
|
setTimeout(() => onComplete(result), DISPLAY_DELAY_MS);
|
|
@@ -17168,6 +17423,11 @@ var RetrieveSpendRequest = ({
|
|
|
17168
17423
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
17169
17424
|
setPhase("declined");
|
|
17170
17425
|
setTimeout(() => onComplete(result), DISPLAY_DELAY_MS);
|
|
17426
|
+
} else if (result.status === "requires_action" && !isAutoResume(result)) {
|
|
17427
|
+
if (pollRef.current) clearInterval(pollRef.current);
|
|
17428
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
17429
|
+
setPhase("requires_action");
|
|
17430
|
+
setTimeout(() => onComplete(result), DISPLAY_DELAY_MS);
|
|
17171
17431
|
} else if (TERMINAL_STATUSES.has(result.status)) {
|
|
17172
17432
|
if (pollRef.current) clearInterval(pollRef.current);
|
|
17173
17433
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
@@ -17216,14 +17476,24 @@ var RetrieveSpendRequest = ({
|
|
|
17216
17476
|
] });
|
|
17217
17477
|
}
|
|
17218
17478
|
if (phase === "polling") {
|
|
17479
|
+
const resumingNextAction = request?.status === "requires_action" ? request.status_details?.requires_action?.next_action : void 0;
|
|
17219
17480
|
return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", children: [
|
|
17220
17481
|
/* @__PURE__ */ jsx30(Box21, { children: /* @__PURE__ */ jsxs21(Text23, { color: "cyan", children: [
|
|
17221
17482
|
/* @__PURE__ */ jsx30(Spinner13, { type: "dots" }),
|
|
17222
|
-
"
|
|
17483
|
+
" ",
|
|
17484
|
+
resumingNextAction ? "Waiting for 3D Secure verification to complete..." : "Awaiting approval...",
|
|
17485
|
+
" ",
|
|
17486
|
+
"(",
|
|
17223
17487
|
elapsed,
|
|
17224
17488
|
"s elapsed)"
|
|
17225
17489
|
] }) }),
|
|
17226
|
-
|
|
17490
|
+
resumingNextAction ? /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [
|
|
17491
|
+
/* @__PURE__ */ jsx30(Text23, { dimColor: true, children: resumingNextAction.display_message }),
|
|
17492
|
+
resumingNextAction.action_url && /* @__PURE__ */ jsxs21(Text23, { dimColor: true, children: [
|
|
17493
|
+
"URL: ",
|
|
17494
|
+
/* @__PURE__ */ jsx30(Text23, { color: "cyan", children: resumingNextAction.action_url })
|
|
17495
|
+
] })
|
|
17496
|
+
] }) : request?.approval_url && /* @__PURE__ */ jsx30(Box21, { marginTop: 1, paddingX: 2, children: /* @__PURE__ */ jsxs21(Text23, { dimColor: true, children: [
|
|
17227
17497
|
"Approval URL: ",
|
|
17228
17498
|
/* @__PURE__ */ jsx30(Text23, { color: "cyan", children: request.approval_url })
|
|
17229
17499
|
] }) })
|
|
@@ -17313,6 +17583,28 @@ var RetrieveSpendRequest = ({
|
|
|
17313
17583
|
] })
|
|
17314
17584
|
] });
|
|
17315
17585
|
}
|
|
17586
|
+
if (phase === "requires_action") {
|
|
17587
|
+
const nextAction = request?.status_details?.requires_action?.next_action;
|
|
17588
|
+
return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", children: [
|
|
17589
|
+
/* @__PURE__ */ jsx30(Text23, { color: "yellow", children: "\u26A0 Action required before payment can proceed" }),
|
|
17590
|
+
/* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", marginTop: 1, paddingX: 2, children: [
|
|
17591
|
+
/* @__PURE__ */ jsxs21(Text23, { children: [
|
|
17592
|
+
"ID: ",
|
|
17593
|
+
/* @__PURE__ */ jsx30(Text23, { bold: true, children: request?.id })
|
|
17594
|
+
] }),
|
|
17595
|
+
/* @__PURE__ */ jsxs21(Text23, { children: [
|
|
17596
|
+
"Type: ",
|
|
17597
|
+
/* @__PURE__ */ jsx30(Text23, { bold: true, children: nextAction?.type })
|
|
17598
|
+
] }),
|
|
17599
|
+
/* @__PURE__ */ jsx30(Text23, { children: nextAction?.display_message }),
|
|
17600
|
+
nextAction?.action_url && /* @__PURE__ */ jsxs21(Text23, { children: [
|
|
17601
|
+
"URL: ",
|
|
17602
|
+
/* @__PURE__ */ jsx30(Text23, { color: "cyan", children: nextAction.action_url })
|
|
17603
|
+
] })
|
|
17604
|
+
] }),
|
|
17605
|
+
/* @__PURE__ */ jsx30(Box21, { marginTop: 1, children: /* @__PURE__ */ jsx30(Text23, { dimColor: true, children: "Complete this step, then create a new spend request." }) })
|
|
17606
|
+
] });
|
|
17607
|
+
}
|
|
17316
17608
|
if (phase === "declined") {
|
|
17317
17609
|
return /* @__PURE__ */ jsxs21(Box21, { flexDirection: "column", children: [
|
|
17318
17610
|
/* @__PURE__ */ jsx30(Text23, { color: "red", children: "\u2717 Spend request declined" }),
|
|
@@ -17516,7 +17808,7 @@ var createOptions = z10.object({
|
|
|
17516
17808
|
merchantAccountId: z10.string().optional().describe(
|
|
17517
17809
|
"Stripe account ID from data-stripe-merchant-account; required with execution_method link_pay_token"
|
|
17518
17810
|
),
|
|
17519
|
-
amount: z10.coerce.number().int().positive().max(5e5).describe("Amount in cents
|
|
17811
|
+
amount: z10.coerce.number().int().positive().max(5e5).describe("Amount in cents"),
|
|
17520
17812
|
currency: z10.string().length(3).default("usd").describe("Currency code"),
|
|
17521
17813
|
merchantName: z10.string().optional().describe(
|
|
17522
17814
|
"Merchant name (required for regular card requests; omit for link_pay_token and shared_payment_token)"
|
|
@@ -17533,7 +17825,9 @@ var createOptions = z10.object({
|
|
|
17533
17825
|
total: z10.array(z10.union([z10.string(), z10.record(z10.string(), z10.unknown())])).default([]).describe(
|
|
17534
17826
|
'Total (repeatable, key:value format). Keys: type (required; one of: subtotal, tax, total, items_base_amount, items_discount, discount, fulfillment, shipping, fee, gift_wrap, tip, store_credit), display_text (required), amount (required). Example: "type:total,display_text:Total,amount:5000"'
|
|
17535
17827
|
),
|
|
17536
|
-
requestApproval: z10.boolean().default(true).describe(
|
|
17828
|
+
requestApproval: z10.boolean().default(true).describe(
|
|
17829
|
+
"Request approval and poll until approved/denied/expired, or until requires_action with a non-auto_resume resolution"
|
|
17830
|
+
),
|
|
17537
17831
|
test: z10.boolean().default(false).describe(
|
|
17538
17832
|
"Use test mode (creates testmode credentials from test card data)"
|
|
17539
17833
|
),
|
|
@@ -17647,6 +17941,18 @@ var UpdateSpendRequest = ({
|
|
|
17647
17941
|
|
|
17648
17942
|
// src/commands/spend-request/index.tsx
|
|
17649
17943
|
import { jsx as jsx32 } from "react/jsx-runtime";
|
|
17944
|
+
function buildRequiresActionResult(request) {
|
|
17945
|
+
const nextAction = request.status_details?.requires_action?.next_action;
|
|
17946
|
+
const isAutoResume2 = nextAction?.resolution === "auto_resume";
|
|
17947
|
+
return {
|
|
17948
|
+
...request,
|
|
17949
|
+
instruction: isAutoResume2 ? `The spend request requires 3D Secure verification. Present action_url (${nextAction?.action_url}) to the user, then call \`spend-request retrieve ${request.id} --interval 2 --max-attempts 300\` to poll until it resolves. Do not create a new spend request \u2014 this one resumes automatically once the challenge is completed.` : `The spend request requires action (${nextAction?.type}): ${nextAction?.display_message}${nextAction?.action_url ? ` URL: ${nextAction.action_url}` : ""} Have the user complete this, then create a new spend request.`,
|
|
17950
|
+
_next: isAutoResume2 ? {
|
|
17951
|
+
command: `spend-request retrieve ${request.id} --interval 2 --max-attempts 300`,
|
|
17952
|
+
until: "status changes from requires_action"
|
|
17953
|
+
} : void 0
|
|
17954
|
+
};
|
|
17955
|
+
}
|
|
17650
17956
|
async function applyOutputFile(request, outputFile, force) {
|
|
17651
17957
|
if (!outputFile || !request.card) return request;
|
|
17652
17958
|
const fileData = {
|
|
@@ -17862,9 +18168,29 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
|
|
|
17862
18168
|
message: `${err.message} Support URL: ${apiErr.error.support_url}`
|
|
17863
18169
|
});
|
|
17864
18170
|
}
|
|
18171
|
+
const duplicate = getDuplicateSpendRequest(err);
|
|
18172
|
+
if (duplicate) {
|
|
18173
|
+
return c.error({
|
|
18174
|
+
code: apiErr?.error?.code ?? "spend_request_rate_limited",
|
|
18175
|
+
message: `${err.message} A matching spend request already exists: ${duplicate.id} (status: ${duplicate.status}). Retrieve it to resume instead of creating a new one.`,
|
|
18176
|
+
cta: {
|
|
18177
|
+
description: "Retrieve the conflicting spend request to inspect its status and resume it if valid.",
|
|
18178
|
+
commands: [
|
|
18179
|
+
{
|
|
18180
|
+
command: `spend-request retrieve ${duplicate.id}`,
|
|
18181
|
+
description: "Retrieve the conflicting spend request to resume it"
|
|
18182
|
+
}
|
|
18183
|
+
]
|
|
18184
|
+
}
|
|
18185
|
+
});
|
|
18186
|
+
}
|
|
17865
18187
|
}
|
|
17866
18188
|
throw err;
|
|
17867
18189
|
}
|
|
18190
|
+
if (created.status === "requires_action") {
|
|
18191
|
+
yield buildRequiresActionResult(created);
|
|
18192
|
+
return;
|
|
18193
|
+
}
|
|
17868
18194
|
if (!requestApproval) {
|
|
17869
18195
|
try {
|
|
17870
18196
|
yield await applyOutputFile(created, outputFile, forceOverwrite);
|
|
@@ -18050,9 +18376,16 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
|
|
|
18050
18376
|
"failed",
|
|
18051
18377
|
"canceled"
|
|
18052
18378
|
]);
|
|
18379
|
+
const isPollTerminal = (req) => {
|
|
18380
|
+
if (terminalStatuses.has(req.status)) return true;
|
|
18381
|
+
if (req.status === "requires_action") {
|
|
18382
|
+
return req.status_details?.requires_action?.next_action?.resolution !== "auto_resume";
|
|
18383
|
+
}
|
|
18384
|
+
return false;
|
|
18385
|
+
};
|
|
18053
18386
|
for await (const result of pollUntil({
|
|
18054
18387
|
fn: () => repository.getSpendRequest(id, { include }),
|
|
18055
|
-
isTerminal: (req) => req === null ||
|
|
18388
|
+
isTerminal: (req) => req === null || isPollTerminal(req),
|
|
18056
18389
|
interval,
|
|
18057
18390
|
maxAttempts,
|
|
18058
18391
|
timeout
|
|
@@ -18064,6 +18397,10 @@ function createSpendRequestCli(repository, authStorage2, envAccessToken2) {
|
|
|
18064
18397
|
});
|
|
18065
18398
|
}
|
|
18066
18399
|
if (result.terminal) {
|
|
18400
|
+
if (result.value.status === "requires_action" && !result.reason) {
|
|
18401
|
+
yield buildRequiresActionResult(result.value);
|
|
18402
|
+
return;
|
|
18403
|
+
}
|
|
18067
18404
|
if (terminalStatuses.has(result.value.status) || !result.reason) {
|
|
18068
18405
|
try {
|
|
18069
18406
|
yield await applyOutputFile(
|
|
@@ -18141,19 +18478,6 @@ var STATUS_WIDTH = 10;
|
|
|
18141
18478
|
var CATEGORY_WIDTH = 16;
|
|
18142
18479
|
var MIN_DESCRIPTION_WIDTH = 16;
|
|
18143
18480
|
var HORIZONTAL_PADDING2 = 4;
|
|
18144
|
-
function formatAmount(amount, currency) {
|
|
18145
|
-
const currencyCode = currency.toUpperCase();
|
|
18146
|
-
try {
|
|
18147
|
-
const formatter = new Intl.NumberFormat("en-US", {
|
|
18148
|
-
style: "currency",
|
|
18149
|
-
currency: currencyCode
|
|
18150
|
-
});
|
|
18151
|
-
const fractionDigits = formatter.resolvedOptions().maximumFractionDigits ?? 2;
|
|
18152
|
-
return formatter.format(amount / 10 ** fractionDigits);
|
|
18153
|
-
} catch {
|
|
18154
|
-
return `${amount} ${currency}`;
|
|
18155
|
-
}
|
|
18156
|
-
}
|
|
18157
18481
|
function truncateCell3(value, width) {
|
|
18158
18482
|
if (value.length <= width) {
|
|
18159
18483
|
return value;
|
|
@@ -18965,7 +19289,7 @@ function cacheUpdateInfo(value, ttlMs = UPDATE_CACHE_TTL_MS) {
|
|
|
18965
19289
|
}
|
|
18966
19290
|
|
|
18967
19291
|
// src/cli.tsx
|
|
18968
|
-
var cliVersion = "0.
|
|
19292
|
+
var cliVersion = "0.13.1";
|
|
18969
19293
|
var cliName = "@stripe/link-cli";
|
|
18970
19294
|
var defaultHeaders = {
|
|
18971
19295
|
"User-Agent": `link-cli/${cliVersion}`
|