@djangocfg/payments 2.1.547 → 2.1.549

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/payments",
3
- "version": "2.1.547",
3
+ "version": "2.1.549",
4
4
  "description": "Provider-agnostic React payments module (Stripe-first): checkout state machine, Payment Element wrapper, close-guard context, mock adapter for keyless development",
5
5
  "keywords": [
6
6
  "payments",
@@ -54,15 +54,15 @@
54
54
  "@stripe/stripe-js": "^9.9.0"
55
55
  },
56
56
  "peerDependencies": {
57
- "@djangocfg/ui-core": "^2.1.547",
57
+ "@djangocfg/ui-core": "^2.1.549",
58
58
  "lucide-react": "^0.545.0",
59
59
  "react": "^19.0.0",
60
60
  "react-dom": "^19.0.0"
61
61
  },
62
62
  "devDependencies": {
63
- "@djangocfg/eslint-config": "^2.1.547",
64
- "@djangocfg/typescript-config": "^2.1.547",
65
- "@djangocfg/ui-core": "^2.1.547",
63
+ "@djangocfg/eslint-config": "^2.1.549",
64
+ "@djangocfg/typescript-config": "^2.1.549",
65
+ "@djangocfg/ui-core": "^2.1.549",
66
66
  "@storybook/react-vite": "^10.5.0",
67
67
  "@types/node": "^25.9.5",
68
68
  "@types/react": "19.2.15",
@@ -100,17 +100,34 @@ export function CheckoutForm({
100
100
  const isDone = status === 'succeeded';
101
101
  const needsAction = status === 'requires_action';
102
102
 
103
+ // A ref, because the disabled button is not a lock. `start` leaves status at
104
+ // `requires_payment` before `confirm` sets `processing`, so between the two
105
+ // awaits `isBusy` is false and the button is live again — and a second Enter
106
+ // there would skip `start` (intent now set) and confirm the SAME intent
107
+ // twice. A ref flips synchronously, with no render in between.
108
+ const submittingRef = React.useRef(false);
109
+
103
110
  const handlePay = useCallback(
104
111
  async (e: React.FormEvent) => {
105
112
  e.preventDefault();
106
- // Self-driving mode only: lazily create the one-time intent on first
107
- // submit. In shared mode the parent already created the intent/subscription
108
- // (start / startSubscription) — creating another here would duplicate it,
109
- // so we go straight to confirm.
110
- if (!isShared && !intent) {
111
- await start({ amount, currency, reference: _reference });
113
+ if (submittingRef.current) return;
114
+ submittingRef.current = true;
115
+ try {
116
+ // Self-driving mode only: lazily create the one-time intent on first
117
+ // submit. In shared mode the parent already created the intent/subscription
118
+ // (start / startSubscription) — creating another here would duplicate it,
119
+ // so we go straight to confirm.
120
+ if (!isShared && !intent) {
121
+ // `start` reports its own failure through status/error. Confirming
122
+ // anyway would replace that reason with a generic "not ready to
123
+ // confirm", hiding why the payment could not be created.
124
+ const created = await start({ amount, currency, reference: _reference });
125
+ if (!created) return;
126
+ }
127
+ await confirm(elementsContext, returnUrl);
128
+ } finally {
129
+ submittingRef.current = false;
112
130
  }
113
- await confirm(elementsContext, returnUrl);
114
131
  },
115
132
  [isShared, intent, start, amount, currency, _reference, confirm, elementsContext, returnUrl],
116
133
  );
@@ -25,7 +25,8 @@ export interface UseCheckoutResult {
25
25
  /** Set when the provider needs a redirect / 3DS step. */
26
26
  nextActionUrl: string | null;
27
27
  /** Step 1 (one-time): create the PaymentIntent → client_secret ready. */
28
- start: (input: CreatePaymentInput) => Promise<void>;
28
+ /** Resolves with the created intent, or undefined when creation failed. */
29
+ start: (input: CreatePaymentInput) => Promise<PaymentIntent | undefined>;
29
30
  /**
30
31
  * Step 1 (subscription, deferred flow): create an `incomplete` Stripe
31
32
  * subscription on the backend → client_secret + confirmType ready. `confirm`
@@ -76,9 +77,13 @@ export function useCheckout(): UseCheckoutResult {
76
77
  intentRef.current = created;
77
78
  setIntent(created);
78
79
  setStatus(created.status === 'idle' ? 'requires_payment' : created.status);
80
+ // Returned so a caller can tell success from failure without reading
81
+ // state that has not re-rendered yet.
82
+ return created;
79
83
  } catch (e) {
80
84
  setStatus('failed');
81
85
  setError(e instanceof Error ? e.message : 'Failed to start payment.');
86
+ return undefined;
82
87
  }
83
88
  },
84
89
  [adapter],