Skip to content

stripe setup#27

Merged
benhalverson merged 3 commits intomainfrom
stripe-setup
Dec 10, 2025
Merged

stripe setup#27
benhalverson merged 3 commits intomainfrom
stripe-setup

Conversation

@benhalverson
Copy link
Owner

No description provided.

Copilot AI review requested due to automatic review settings December 10, 2025 06:46
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR integrates Stripe payment processing into the e-commerce application by adding a dedicated payment flow with two new pages and connecting them to the existing checkout process. The implementation adds Stripe's React components for secure payment handling and establishes the necessary routing and configuration infrastructure.

Key changes:

  • Added Payment page with Stripe Elements integration for processing payments
  • Added OrderComplete page to confirm successful transactions and clear cart state
  • Extended Checkout page to create payment intents and navigate to the payment flow

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/pages/Payment.tsx New page implementing Stripe payment form with Elements and payment confirmation logic
src/pages/OrderComplete.tsx New order confirmation page that displays success message and clears cart from localStorage
src/pages/Checkout.tsx Added payment intent creation logic, response parsing utility, and navigation to payment page
src/context/CartContext.tsx Added debug logging for cart ID tracking
src/App.tsx Registered new /payment and /order/complete routes with lazy loading
pnpm-lock.yaml Added @stripe/stripe-js and @stripe/react-stripe-js dependencies
package.json Declared Stripe SDK dependencies (v8.5.3 and v5.4.1)
.env.sample Added VITE_STRIPE_PUBLISHABLE_KEY environment variable
.vscode/settings.json Modified editor settings (formatOnSave, fontSize, colorTheme)
pnpm-workspace.yaml New workspace configuration for dependency build settings
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any type defeats the purpose of TypeScript's type safety. The Stripe library provides proper types for the confirmPayment result. Use the PaymentIntentResult type from @stripe/stripe-js instead of any.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

const clientSecret = data.clientSecret ?? data.client_secret;
if (clientSecret) {
// Navigate to the payment page which loads Stripe Elements and completes confirmation
navigate(`/payment?client_secret=${encodeURIComponent(clientSecret)}`);
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing the Stripe client_secret as a URL query parameter exposes it in browser history, server logs, and referrer headers. While Stripe client secrets are designed to be somewhat safe to expose, best practice is to pass them via state or session storage instead. Consider using navigate('/payment', { state: { clientSecret } }) and retrieving it with useLocation().state in the Payment component.

Suggested change
navigate(`/payment?client_secret=${encodeURIComponent(clientSecret)}`);
navigate('/payment', { state: { clientSecret } });

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +11

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY as string;
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VITE_STRIPE_PUBLISHABLE_KEY environment variable should be validated using the existing Zod schema in src/config.ts, consistent with how VITE_BASE_URL and VITE_DOMAIN are validated. This ensures the application fails fast at startup if the required configuration is missing, rather than at runtime when users try to make payments.

Suggested change
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY as string;
import { stripePublishableKey } from "../config";
const publishableKey = stripePublishableKey;

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +12
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY as string;
const stripePromise = loadStripe(publishableKey);
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loadStripe function is called with publishableKey at the module level, which will attempt to load Stripe even when publishableKey is undefined. This should be moved inside the component or wrapped in a conditional to prevent initialization with an invalid key. Consider using lazy initialization or conditionally creating the promise.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI commented Dec 10, 2025

@benhalverson I've opened a new pull request, #28, to work on those changes. Once the pull request is ready, I'll request review from you.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings December 10, 2025 06:57
* Initial plan

* Replace any type with PaymentIntentResult for type safety

Co-authored-by: benhalverson <7907232+benhalverson@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: benhalverson <7907232+benhalverson@users.noreply.github.com>
@benhalverson benhalverson merged commit 9c29ac0 into main Dec 10, 2025
1 check passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 8 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +66 to +97
export default function PaymentPage() {
const loc = useLocation();
const q = useMemo(() => new URLSearchParams(loc.search), [loc.search]);
const clientSecret = q.get("client_secret");

if (!publishableKey) {
return (
<div className="p-8 text-center text-red-600">
Missing Stripe publishable key. Set `VITE_STRIPE_PUBLISHABLE_KEY`.
</div>
);
}

if (!clientSecret) {
return (
<div className="p-8 text-center">No payment session available.</div>
);
}

const options = useMemo(() => ({ clientSecret }), [clientSecret]);

return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="mx-auto max-w-2xl px-4">
<h2 className="text-lg font-medium mb-6">Complete payment</h2>
<Elements stripe={stripePromise} options={options}>
<PaymentForm />
</Elements>
</div>
</div>
);
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PaymentPage component lacks test coverage. Consider adding tests to verify the component handles missing publishable keys, missing client secrets, and successful payment element rendering.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +32
export default function OrderComplete() {
useEffect(() => {
try {
localStorage.removeItem("cartId");
} catch (e) {
// ignore localStorage errors in some environments
console.log('Could not clear cartId from localStorage', e);
}
}, []);

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12">
<div className="max-w-xl w-full bg-white shadow rounded-lg p-8 text-center">
<h1 className="text-2xl font-semibold mb-4">Thank you — your order is complete</h1>
<p className="text-gray-600 mb-6">
We received your payment. You will receive an email confirmation shortly.
</p>
<div className="flex justify-center gap-4">
<Link to="/" className="rounded-md bg-indigo-600 text-white px-4 py-2">
Back to shop
</Link>
<Link to="/profile" className="rounded-md border px-4 py-2">
View account
</Link>
</div>
</div>
</div>
);
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OrderComplete component lacks test coverage. Given that other page components have tests, consider adding tests to verify localStorage clearing behavior and UI rendering.

Copilot uses AI. Check for mistakes.
Comment on lines +230 to +231
navigate(data.checkout_url);
return;
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkout_url from the API response is used directly with navigate() without validation. If this URL could be external or untrusted, consider validating that it's a relative path or matches expected patterns to prevent open redirect vulnerabilities.

Suggested change
navigate(data.checkout_url);
return;
// Only allow relative URLs starting with a single slash, and not containing "//"
if (
typeof data.checkout_url === "string" &&
/^\/(?!\/)/.test(data.checkout_url)
) {
navigate(data.checkout_url);
return;
} else {
console.warn("Unsafe checkout_url received, not navigating:", data.checkout_url);
setCartError("Received unsafe checkout URL from server.");
return;
}

Copilot uses AI. Check for mistakes.
typeof record.orderId === "string" || typeof record.orderId === "number"
? (record.orderId as string | number)
: undefined;
if (checkout_url || clientSecret || client_secret || amount || currency || orderId)
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition on line 72 returns a PaymentIntentResponse object if ANY field is present. This could lead to accepting incomplete responses. Consider validating that at least one meaningful field (checkout_url, clientSecret/client_secret, or orderId) is present, rather than accepting responses with only optional fields like amount or currency.

Suggested change
if (checkout_url || clientSecret || client_secret || amount || currency || orderId)
if (checkout_url || clientSecret || client_secret || orderId)

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +77
if (!publishableKey) {
return (
<div className="p-8 text-center text-red-600">
Missing Stripe publishable key. Set `VITE_STRIPE_PUBLISHABLE_KEY`.
</div>
);
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for publishableKey happens inside the component render, meaning the Stripe promise (stripePromise) is already created at module level (line 12) even if the key is missing. Consider moving the validation to the module level or handling the missing key case before creating the Stripe promise.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +42
} else if (result?.paymentIntent) {
// If payment succeeded or requires action handled by Stripe, navigate to a success page
navigate("/order/complete");
} else {
// Unknown result — navigate to a generic page or reload
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 38-43 both navigate to "/order/complete" in different conditions. Consider consolidating this logic or adding a comment explaining why the distinction is necessary for code clarity.

Suggested change
} else if (result?.paymentIntent) {
// If payment succeeded or requires action handled by Stripe, navigate to a success page
navigate("/order/complete");
} else {
// Unknown result — navigate to a generic page or reload
} else {
// Payment succeeded or unknown result — navigate to a success page

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +64
function PaymentForm() {
const stripe = useStripe();
const elements = useElements();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
setLoading(true);
setError(null);
try {
const result: PaymentIntentResult = await stripe.confirmPayment({
elements,
confirmParams: {
// You can change return_url to an order confirmation route
return_url: window.location.origin + "/order/complete",
},
redirect: "if_required",
});

if (result?.error) {
setError(result.error.message || "Payment confirmation failed");
} else if (result?.paymentIntent) {
// If payment succeeded or requires action handled by Stripe, navigate to a success page
navigate("/order/complete");
} else {
// Unknown result — navigate to a generic page or reload
navigate("/order/complete");
}
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Payment failed");
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleSubmit} className="max-w-lg mx-auto p-4">
<PaymentElement />
{error && <div className="text-red-600 mt-2">{error}</div>}
<button
type="submit"
disabled={!stripe || loading}
className="mt-4 w-full rounded bg-indigo-600 text-white py-2">
{loading ? "Processing…" : "Pay"}
</button>
</form>
);
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PaymentForm component lacks test coverage. Given that other page components like Product.tsx have comprehensive tests, consider adding tests for this component to cover payment submission, error handling, and loading states.

Copilot uses AI. Check for mistakes.
Comment on lines 201 to 249
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("handleSubmit called", profile);
const cartId = localStorage.getItem("cartId");
if (!cartId) {
setCartError("No cartId found");
return;
}
setCartLoading(true);
setCartError(null);
try {
const res = await fetch(`${BASE_URL}/cart/${cartId}/payment-intent`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ shippingInfo, profile }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Payment intent request failed (${res.status}): ${text}`);
}
const dataJson: unknown = await res.json();
const data = parsePaymentIntentResponse(dataJson);

if (!data) {
console.warn("Unexpected payment intent response:", dataJson);
throw new Error("Invalid payment intent response");
}
// If backend provides a checkout URL, redirect there
if (data.checkout_url) {
navigate(data.checkout_url);
return;
}
// If backend returned a Stripe client secret, navigate to a route that can handle it
const clientSecret = data.clientSecret ?? data.client_secret;
if (clientSecret) {
// Navigate to the payment page which loads Stripe Elements and completes confirmation
navigate(`/payment?client_secret=${encodeURIComponent(clientSecret)}`);
return;
}
// Fallback: optionally navigate if order id provided
if (data.orderId) {
navigate(`/order/${data.orderId}`);
}
} catch (err: unknown) {
setCartError(err instanceof Error ? err.message : "Payment intent failed");
} finally {
setCartLoading(false);
}
};
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new payment intent creation logic in handleSubmit lacks test coverage. Consider adding tests to verify proper handling of successful payment intent responses, error cases, and the various navigation paths (checkout_url, clientSecret, orderId).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments