-
Notifications
You must be signed in to change notification settings - Fork 0
stripe setup #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
stripe setup #27
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| VITE_BASE_URL="" | ||
| VITE_DOMAIN="" | ||
| VITE_DOMAIN="" | ||
| VITE_STRIPE_PUBLISHABLE_KEY="" |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| onlyBuiltDependencies: | ||
| - '@swc/core' | ||
| - esbuild | ||
| - msw | ||
| - sharp | ||
| - workerd |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -43,6 +43,37 @@ interface ShippingCostResponse { | |||||||||||||||||||||||||||||
| shippingCost: number; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| interface PaymentIntentResponse { | ||||||||||||||||||||||||||||||
| checkout_url?: string; | ||||||||||||||||||||||||||||||
| // API returns `clientSecret` (camelCase); accept `client_secret` as well for compatibility | ||||||||||||||||||||||||||||||
| clientSecret?: string; | ||||||||||||||||||||||||||||||
| client_secret?: string; | ||||||||||||||||||||||||||||||
| amount?: number; | ||||||||||||||||||||||||||||||
| currency?: string; | ||||||||||||||||||||||||||||||
| orderId?: string | number; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function isObject(val: unknown): val is Record<string, unknown> { | ||||||||||||||||||||||||||||||
| return typeof val === "object" && val !== null; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function parsePaymentIntentResponse(obj: unknown): PaymentIntentResponse | null { | ||||||||||||||||||||||||||||||
| if (!isObject(obj)) return null; | ||||||||||||||||||||||||||||||
| const record = obj as Record<string, unknown>; | ||||||||||||||||||||||||||||||
| const checkout_url = typeof record.checkout_url === "string" ? record.checkout_url : undefined; | ||||||||||||||||||||||||||||||
| const clientSecret = typeof record.clientSecret === "string" ? record.clientSecret : undefined; | ||||||||||||||||||||||||||||||
| const client_secret = typeof record.client_secret === "string" ? record.client_secret : undefined; | ||||||||||||||||||||||||||||||
| const amount = typeof record.amount === "number" ? record.amount : undefined; | ||||||||||||||||||||||||||||||
| const currency = typeof record.currency === "string" ? record.currency : undefined; | ||||||||||||||||||||||||||||||
| const orderId = | ||||||||||||||||||||||||||||||
| typeof record.orderId === "string" || typeof record.orderId === "number" | ||||||||||||||||||||||||||||||
| ? (record.orderId as string | number) | ||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||
| if (checkout_url || clientSecret || client_secret || amount || currency || orderId) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| if (checkout_url || clientSecret || client_secret || amount || currency || orderId) | |
| if (checkout_url || clientSecret || client_secret || orderId) |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
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.
| 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
AI
Dec 10, 2025
There was a problem hiding this comment.
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.
| navigate(`/payment?client_secret=${encodeURIComponent(clientSecret)}`); | |
| navigate('/payment', { state: { clientSecret } }); |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
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).
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { Link } from "react-router-dom"; | ||
| import { useEffect } from "react"; | ||
|
|
||
| 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> | ||
| ); | ||
| } | ||
|
Comment on lines
+4
to
+32
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,97 @@ | ||||||||||||||||
| import { useMemo, useState } from "react"; | ||||||||||||||||
| import { useLocation, useNavigate } from "react-router-dom"; | ||||||||||||||||
| import { loadStripe, type PaymentIntentResult } from "@stripe/stripe-js"; | ||||||||||||||||
| import { | ||||||||||||||||
| Elements, | ||||||||||||||||
| PaymentElement, | ||||||||||||||||
| useStripe, | ||||||||||||||||
| useElements, | ||||||||||||||||
| } from "@stripe/react-stripe-js"; | ||||||||||||||||
|
|
||||||||||||||||
| const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY as string; | ||||||||||||||||
|
Comment on lines
+10
to
+11
|
||||||||||||||||
| const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY as string; | |
| import { stripePublishableKey } from "../config"; | |
| const publishableKey = stripePublishableKey; |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
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
AI
Dec 10, 2025
There was a problem hiding this comment.
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.
| } 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
AI
Dec 10, 2025
There was a problem hiding this comment.
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
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Retrieving the Stripe client_secret from URL query parameters 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 receive it via navigation state instead. Consider using useLocation().state to retrieve the client secret passed from the Checkout page.
| const q = useMemo(() => new URLSearchParams(loc.search), [loc.search]); | |
| const clientSecret = q.get("client_secret"); | |
| const clientSecret = loc.state?.clientSecret; |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
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
AI
Dec 10, 2025
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.