@stripe/link-cli 0.12.0 → 0.13.0

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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +349 -25
  3. package/package.json +3 -3
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 reaches `--timeout` or exhausts `--max-attempts` 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.
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
 
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 (requestApproval) {
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 ? `${request.amount} ${request.currency?.toUpperCase() ?? ""}`.trim() : "N/A" })
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
- " Awaiting approval... (",
17483
+ " ",
17484
+ resumingNextAction ? "Waiting for 3D Secure verification to complete..." : "Awaiting approval...",
17485
+ " ",
17486
+ "(",
17223
17487
  elapsed,
17224
17488
  "s elapsed)"
17225
17489
  ] }) }),
17226
- request?.approval_url && /* @__PURE__ */ jsx30(Box21, { marginTop: 1, paddingX: 2, children: /* @__PURE__ */ jsxs21(Text23, { dimColor: true, children: [
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" }),
@@ -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("Request approval and poll until approved/denied/expired"),
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 || terminalStatuses.has(req.status),
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.12.0";
19292
+ var cliVersion = "0.13.0";
18969
19293
  var cliName = "@stripe/link-cli";
18970
19294
  var defaultHeaders = {
18971
19295
  "User-Agent": `link-cli/${cliVersion}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stripe/link-cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "link-cli": "./dist/cli.js"
@@ -44,8 +44,8 @@
44
44
  "tsx": "^4.23.5",
45
45
  "typescript": "^5.9.3",
46
46
  "vitest": "^4.1.10",
47
- "@stripe/link-typescript-config": "0.0.0",
48
- "@stripe/link-sdk": "1.0.0"
47
+ "@stripe/link-sdk": "1.0.0",
48
+ "@stripe/link-typescript-config": "0.0.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsup",