Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ SPREE_PUBLISHABLE_KEY=your_publishable_api_key
NEXT_PUBLIC_DEFAULT_COUNTRY=us
NEXT_PUBLIC_DEFAULT_LOCALE=en

# Stripe (optional - required for Stripe payment method)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key

# Sentry Error Tracking (optional - set DSN to enable)
SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
SENTRY_ORG=your-org
Expand Down
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.1/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down
554 changes: 328 additions & 226 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
},
"dependencies": {
"@sentry/nextjs": "^10.38.0",
"@spree/next": "^0.2.0",
"@spree/sdk": "^0.2.0",
"@spree/next": "^0.2.3",
"@spree/sdk": "^0.2.3",
"@stripe/react-stripe-js": "^5.6.0",
"@stripe/stripe-js": "^8.7.0",
"next": "^16",
"react": "^19",
"react-dom": "^19"
"react-dom": "^19",
"react-svg-credit-card-payment-icons": "^5.1.2"
},
"devDependencies": {
"@biomejs/biome": "^2.3.14",
Expand Down
97 changes: 65 additions & 32 deletions src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ import { getAddresses, updateAddress } from "@/lib/data/addresses";
import {
advanceCheckout,
applyCouponCode,
completeOrder,
getCheckoutOrder,
removeCouponCode,
selectShippingRate,
updateOrderAddresses,
} from "@/lib/data/checkout";
import { isAuthenticated as checkAuth } from "@/lib/data/cookies";
import { getCountries, getCountry } from "@/lib/data/countries";
import {
completeCheckoutOrder,
completeCheckoutPaymentSession,
} from "@/lib/data/payment";
import { extractBasePath } from "@/lib/utils/path";

// Checkout steps
Expand Down Expand Up @@ -187,7 +190,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {

// Check if order is already complete
if (orderData.state === "complete") {
router.push(`${basePath}/account/orders/${orderId}`);
router.push(`${basePath}/order-placed/${orderId}`);
return;
}

Expand Down Expand Up @@ -276,12 +279,9 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {
const result = await selectShippingRate(order.id, shipmentId, rateId);
if (!result.success) {
setError(result.error || "Failed to select shipping rate");
}
// Reload order to get updated totals (includes shipments)
const orderData = await getCheckoutOrder(order.id);
if (orderData) {
setOrder(orderData);
setShipments(orderData.shipments || []);
} else if (result.order) {
setOrder(result.order);
setShipments(result.order.shipments || []);
}
} catch {
setError("An error occurred. Please try again.");
Expand Down Expand Up @@ -315,43 +315,73 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {
}
};

// Handle payment submission (billing address + complete order)
const handlePaymentSubmit = async (paymentData: {
// Handle billing address update (called by PaymentStep before gateway confirmation)
const handleUpdateBillingAddress = async (data: {
bill_address: AddressParams;
use_shipping_for_billing: boolean;
}) => {
if (!order) return;
}): Promise<boolean> => {
if (!order) return false;

setProcessing(true);
setError(null);

try {
// Update billing address
const updateResult = await updateOrderAddresses(order.id, {
bill_address: paymentData.bill_address,
bill_address: data.bill_address,
});

if (!updateResult.success) {
setError(updateResult.error || "Failed to save billing address");
return false;
}

return true;
} catch {
setError("Failed to save billing address. Please try again.");
return false;
}
};

// Handle payment completion (called by PaymentStep after Stripe confirms)
const handlePaymentComplete = async (paymentSessionId: string) => {
if (!order) return;

setError(null);

try {
// Complete the payment session on the backend
const sessionResult = await completeCheckoutPaymentSession(
order.id,
paymentSessionId,
);

if (!sessionResult.success) {
setError(sessionResult.error || "Failed to complete payment session");
setProcessing(false);
return;
}

// Complete the order (skip actual payment for now)
const completeResult = await completeOrder(order.id);
if (!completeResult.success) {
setError(completeResult.error || "Failed to complete order");
// Check if the order was already completed by the payment session completion.
// If not, explicitly complete it.
const updatedOrder = await getCheckoutOrder(order.id);

if (!updatedOrder) {
setError("Order not found after payment. Please contact support.");
setProcessing(false);
return;
}

// Redirect to order confirmation
if (completeResult.order) {
router.push(`${basePath}/account/orders/${completeResult.order.id}`);
if (updatedOrder.state !== "complete") {
const completeResult = await completeCheckoutOrder(order.id);
if (!completeResult.success) {
setError(completeResult.error || "Failed to complete order");
setProcessing(false);
return;
}
}

// Redirect to order placed page (cart cookie is cleared there)
router.push(`${basePath}/order-placed/${order.id}`);
} catch {
setError("An error occurred. Please try again.");
} finally {
setProcessing(false);
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -419,7 +449,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {
<p className="text-gray-600 mb-6">{error}</p>
<Link
href={`${basePath}/cart`}
className="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
className="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-xl hover:bg-primary-700"
>
Return to Cart
</Link>
Expand All @@ -441,7 +471,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {
</p>
<Link
href={`${basePath}/products`}
className="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
className="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-xl hover:bg-primary-700"
>
Continue Shopping
</Link>
Expand Down Expand Up @@ -478,9 +508,9 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
index < currentStepIndex
? "bg-indigo-600 text-white"
? "bg-primary-600 text-white"
: index === currentStepIndex
? "bg-indigo-600 text-white"
? "bg-primary-600 text-white"
: "bg-gray-200 text-gray-500"
}`}
>
Expand All @@ -489,7 +519,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {
<span
className={`ml-2 text-sm font-medium ${
index === currentStepIndex
? "text-indigo-600"
? "text-primary-600"
: index < currentStepIndex
? "text-gray-900"
: "text-gray-500"
Expand All @@ -501,7 +531,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {
{index < steps.length - 1 && (
<div className="w-full h-0.5 bg-gray-200">
<div
className={`h-full ${index < currentStepIndex ? "bg-indigo-600" : "bg-gray-200"}`}
className={`h-full ${index < currentStepIndex ? "bg-primary-600" : "bg-gray-200"}`}
style={{
width: index < currentStepIndex ? "100%" : "0%",
}}
Expand All @@ -516,7 +546,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {

{/* Error banner */}
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-700">
{error}
</div>
)}
Expand Down Expand Up @@ -553,10 +583,13 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {
<PaymentStep
order={order}
countries={countries}
isAuthenticated={isAuthenticated}
fetchStates={fetchStates}
onSubmit={handlePaymentSubmit}
onUpdateBillingAddress={handleUpdateBillingAddress}
onPaymentComplete={handlePaymentComplete}
onBack={() => goToStep("delivery")}
processing={processing}
setProcessing={setProcessing}
/>
)}
</>
Expand Down
Loading