feat: integrate Stripe payment module with API + UI components
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Checkout service — the only place the frontend talks to Stripe / the payment
|
||||
* API. The UI calls these functions; it never handles card data.
|
||||
*
|
||||
* `startCheckout` follows Stripe's recommended hosted-checkout flow:
|
||||
* 1. Ask our backend to create a Checkout Session (amount resolved server-side).
|
||||
* 2. Redirect the browser to the Stripe-hosted `session.url`.
|
||||
* 3. Stripe handles payment, then redirects back to success_url / cancel_url.
|
||||
*
|
||||
* A Stripe.js `redirectToCheckout({ sessionId })` fallback (which uses the
|
||||
* publishable key) is kept for older API behavior, but `session.url` is the
|
||||
* current default and avoids loading Stripe.js for the redirect.
|
||||
*/
|
||||
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { stripeConfig } from '../config/stripeConfig';
|
||||
|
||||
let stripePromise;
|
||||
const getStripe = () => {
|
||||
if (!stripePromise) stripePromise = loadStripe(stripeConfig.publishableKey);
|
||||
return stripePromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a session for an invoice and redirect to Stripe's hosted page.
|
||||
* Throws (with a friendly message) if the backend isn't configured/reachable.
|
||||
*/
|
||||
export async function startCheckout({ invoiceId, customerEmail } = {}) {
|
||||
if (!stripeConfig.isConfigured) {
|
||||
const err = new Error(
|
||||
'Stripe is not configured yet. Add your publishable key to enable payments.'
|
||||
);
|
||||
err.code = 'stripe_not_configured';
|
||||
throw err;
|
||||
}
|
||||
|
||||
const res = await fetch(`${stripeConfig.apiBase}/create-checkout-session`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ invoiceId, customerEmail }),
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Could not start checkout. Please try again.');
|
||||
}
|
||||
|
||||
// Preferred: redirect straight to the Stripe-hosted Checkout URL.
|
||||
if (data.url) {
|
||||
window.location.assign(data.url);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: Stripe.js redirect using the session id + publishable key.
|
||||
const stripe = await getStripe();
|
||||
if (!stripe) throw new Error('Unable to initialize Stripe.');
|
||||
const { error } = await stripe.redirectToCheckout({ sessionId: data.id });
|
||||
if (error) throw new Error(error.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the verified status of a completed Checkout Session (used by the
|
||||
* success return page). Returns the trimmed status object from the backend.
|
||||
*/
|
||||
export async function getSessionStatus(sessionId) {
|
||||
const res = await fetch(
|
||||
`${stripeConfig.apiBase}/session-status?session_id=${encodeURIComponent(sessionId)}`
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Could not verify payment status.');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user