-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: e2e stripe checkout functionality
- Loading branch information
1 parent
5294447
commit bc9babe
Showing
17 changed files
with
654 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,4 @@ | ||
MY_SECRET_VALUE="foobar" | ||
|
||
# Auth0 - https://auth0.com/docs/quickstart/webapp/nextjs/interactive | ||
# Allowed callback URLs: https://my-app.vercel.app/api/auth/callback, http://localhost:3000/api/auth/callback | ||
# Allowed logout URLs: https://my-app.vercel.app, http://localhost:3000 | ||
AUTH0_SECRET='use [openssl rand -hex 32] to generate a 32 bytes value' | ||
AUTH0_BASE_URL='http://localhost:3000' | ||
AUTH0_ISSUER_BASE_URL='https://{yourDomain}' | ||
AUTH0_CLIENT_ID='{yourClientId}' | ||
AUTH0_CLIENT_SECRET='{yourClientSecret}' | ||
NEXT_PUBLIC_URL="http://localhost:3000" | ||
NEXT_PUBLIC_STRIPE_KEY="pk_asdfasdfasdfasdfasdfasdfasdfasdfasdfasdf" | ||
STRIPE_SECRET_KEY="sk_asdfasdfasdfasdfasdfasdfasdfasdfasdfasdf" | ||
STRIPE_WEBHOOK_SECRET="whsec_asdfasdfasdfasdfasdfa" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { memo } from 'react'; | ||
import OrderConfirmation from '@/components/OrderConfirmation'; | ||
import { validateStripeSession } from '@/lib/stripe'; | ||
import { notFound } from 'next/navigation'; | ||
|
||
// Redirect page after successful checkout | ||
// <baseUrl>/order-confirmation?session_id=cs_test_b1FKoQomBOaQFgMqW1lU6oYXRwIruD6AbGV804gMZRrptJr1bF91sDmK5T | ||
const OrderConfirmationPage: React.FC<{ searchParams: { [key: string]: string | undefined } }> = async ({ searchParams }) => { | ||
const sessionId = searchParams['session_id']; | ||
const { validSession } = await validateStripeSession(sessionId); | ||
if (!validSession) return notFound(); | ||
|
||
return ( | ||
<div className="my-8 mx-20"> | ||
<OrderConfirmation /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default memo(OrderConfirmationPage); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { createCheckoutSession, formatCartItemsForStripe } from '@/lib/stripe'; | ||
import validateCartItems from '@/utils/validateCartItems'; | ||
import { NextRequest, NextResponse } from 'next/server'; | ||
|
||
/** | ||
* @openapi | ||
* /v1/checkout: | ||
* post: | ||
* summary: Creates a Stripe checkout session. | ||
* tags: | ||
* - Checkout | ||
* requestBody: | ||
* required: true | ||
* content: | ||
* application/json: | ||
* schema: | ||
* type: object | ||
* properties: | ||
* cartItems: | ||
* type: array | ||
* items: | ||
* type: object | ||
* properties: | ||
* id: | ||
* type: string | ||
* name: | ||
* type: string | ||
* description: | ||
* type: string | ||
* quantity: | ||
* type: integer | ||
* price: | ||
* type: number | ||
* image: | ||
* type: string | ||
* currency: | ||
* type: string | ||
* price_data: | ||
* type: object | ||
* product_data: | ||
* type: object | ||
* properties: | ||
* size: | ||
* type: string | ||
* productId: | ||
* type: string | ||
* category: | ||
* type: string | ||
* type: string | ||
* responses: | ||
* 200: | ||
* description: Checkout session created successfully. | ||
* content: | ||
* application/json: | ||
* schema: | ||
* type: object | ||
* properties: | ||
* checkoutUrl: | ||
* type: string | ||
* 400: | ||
* description: Error creating checkout session. | ||
* content: | ||
* application/json: | ||
* schema: | ||
* type: object | ||
* properties: | ||
* message: | ||
* type: string | ||
*/ | ||
export const POST = async (request: NextRequest) => { | ||
try { | ||
const body = await request.json(); | ||
const { cartItems: clientCartItems } = body; | ||
|
||
const cartItems = validateCartItems(clientCartItems); | ||
if (!cartItems) { | ||
console.error('[Checkout] invalid client cart', clientCartItems); | ||
return NextResponse.json({ message: 'Unable to checkout cart items' }, { status: 400 }); | ||
} | ||
|
||
// TODO_PRINTIFY | ||
const printifyOrderId = 'asdf1234'; | ||
const stripeLineItems = formatCartItemsForStripe(cartItems); | ||
const session = await createCheckoutSession(stripeLineItems, { printifyOrderId }); | ||
|
||
if (!session || !session.url) { | ||
console.error('[Stripe] error creating checkout session:', session); | ||
return NextResponse.json({ message: 'Unable to create Stripe checkout session' }, { status: 400 }); | ||
} | ||
|
||
return NextResponse.json({ checkoutUrl: session.url }); | ||
} catch (error) { | ||
console.error('Error processing request:', error); | ||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import type { Stripe as StripeType } from 'stripe'; | ||
import { stripe } from '@/lib/stripe'; | ||
import { NextRequest, NextResponse } from 'next/server'; | ||
|
||
async function fulfillCheckout(printifyOrderId: string) { | ||
console.log('Fulfilling Checkout Session - printifyOrderId:' + printifyOrderId); | ||
// TODO_PRINTIFY | ||
return; | ||
|
||
// // TODO: Make this function safe to run multiple times, | ||
// // even concurrently, with the same session ID | ||
|
||
// // TODO: Make sure fulfillment hasn't already been | ||
// // peformed for this Checkout Session | ||
|
||
// const checkoutSession = await retrieveCheckoutSession(sessionId) | ||
// const { shipping_details, line_items, metadata } = checkoutSession | ||
// const { printifyOrderId } = metadata | ||
|
||
// // Check the Checkout Session's payment_status property | ||
// // to determine if fulfillment should be peformed | ||
// if (checkoutSession.payment_status !== 'unpaid') { | ||
// // TODO: Perform fulfillment of the line items | ||
|
||
// // TODO: Record/save fulfillment status for this | ||
// // Checkout Session | ||
// await sendOrderToProduction(printifyOrderId) | ||
} | ||
|
||
export const POST = async (request: NextRequest) => { | ||
try { | ||
const secret = process.env.STRIPE_WEBHOOK_SECRET || ''; | ||
if (!secret) { | ||
throw new Error('Missing STRIPE_WEBHOOK_SECRET environment variable'); | ||
} | ||
|
||
const body = await (await request.blob()).text(); | ||
const signature = request.headers.get('stripe-signature') as string; | ||
const event: StripeType.Event = stripe.webhooks.constructEvent(body, signature, secret); | ||
|
||
if (['checkout.session.completed', 'checkout.session.async_payment_succeeded'].includes(event.type)) { | ||
// @ts-expect-error - acceptable error | ||
const printifyOrderId = event.data.object?.metadata?.printifyOrderId; | ||
if (!printifyOrderId) { | ||
throw new Error(`missing printifyOrderId on metadata, ${event.id}`); | ||
} | ||
|
||
await fulfillCheckout(printifyOrderId); | ||
} | ||
|
||
return NextResponse.json({ result: event, ok: true }); | ||
} catch (error) { | ||
console.error('Error processing request:', error); | ||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
'use client'; | ||
|
||
import Image from 'next/image'; | ||
import Link from 'next/link'; | ||
import React, { useEffect } from 'react'; | ||
import { useShoppingCart } from 'use-shopping-cart'; | ||
|
||
const OrderConfirmation = () => { | ||
const { clearCart } = useShoppingCart(); | ||
|
||
useEffect(() => { | ||
clearCart(); | ||
}, [clearCart]); | ||
|
||
return ( | ||
<div className="flex justify-center items-center my-32"> | ||
<div className="text-center"> | ||
<Image width={100} height={100} src="/icons/check-mark.png" alt="Check Mark Icon" className="w-32 h-32 object-cover mx-auto mb-4" /> | ||
<h3 className="text-2xl font-bold mb-2">Thank you for your order!</h3> | ||
{/* TODO_AUTH_ORDER */} | ||
<p className="text max-w-md text-gray-500"> | ||
You'll receive an email receipt shortly. You can checkout the status of your order anytime by visiting the <Link href="/account">"Account"</Link> page | ||
</p> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default OrderConfirmation; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.