From fe10cabe524240b1edc3f554e5d6bd2c4a1b2f6e Mon Sep 17 00:00:00 2001 From: Mathias Gabel Date: Mon, 20 May 2024 00:14:37 -0700 Subject: [PATCH 1/6] Payment intents instead of product page --- app/(api)/api/checkout_sessions/get.ts | 29 +++ app/(api)/api/checkout_sessions/post.ts | 33 ++++ app/(api)/api/checkout_sessions/route.ts | 2 + .../_components/PaymentForm/CardSection.js | 29 +++ .../_components/PaymentForm/CheckoutForm.js | 52 ++++++ .../_components/PaymentForm/PaymentForm.tsx | 40 +++++ app/(pages)/checkout/page.tsx | 6 +- package-lock.json | 165 +++++++++++++----- package.json | 5 +- tsconfig.json | 2 +- 10 files changed, 314 insertions(+), 49 deletions(-) create mode 100644 app/(api)/api/checkout_sessions/get.ts create mode 100644 app/(api)/api/checkout_sessions/post.ts create mode 100644 app/(api)/api/checkout_sessions/route.ts create mode 100644 app/(pages)/checkout/_components/PaymentForm/CardSection.js create mode 100644 app/(pages)/checkout/_components/PaymentForm/CheckoutForm.js create mode 100644 app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx diff --git a/app/(api)/api/checkout_sessions/get.ts b/app/(api)/api/checkout_sessions/get.ts new file mode 100644 index 0000000..b677283 --- /dev/null +++ b/app/(api)/api/checkout_sessions/get.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server'; +import stripe from 'stripe'; + +export async function GET(request: NextRequest) { + try { + const auth_stripe = new stripe(process.env.STRIPE_SECRET_KEY!); + const searchParams = request.nextUrl.searchParams; + const session_id = searchParams.get('session_id'); + + const session = await auth_stripe.checkout.sessions.retrieve( + session_id as string + ); + + return NextResponse.json( + { + customer_email: session.customer_details!.email, + }, + { + status: 200, + } + ); + } catch (e) { + const error = e as stripe.StripeRawError; + return NextResponse.json( + { ok: false, body: null, error: error.message }, + { status: 400 } + ); + } +} diff --git a/app/(api)/api/checkout_sessions/post.ts b/app/(api)/api/checkout_sessions/post.ts new file mode 100644 index 0000000..ca05d9c --- /dev/null +++ b/app/(api)/api/checkout_sessions/post.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { headers } from 'next/headers'; +import stripe from 'stripe'; + +export async function POST(_: NextRequest) { + try { + const headersList = headers(); + const origin = headersList.get('origin'); + const auth_stripe = new stripe(process.env.STRIPE_SECRET_KEY!); + + // Create Checkout Sessions from body params. + const session = await auth_stripe.paymentIntents.create({ + amount: 1099, + currency: 'usd', + payment_method_types: ['card'], + }); + + return NextResponse.json( + { + clientSecret: session.client_secret, + }, + { + status: 200, + } + ); + } catch (e) { + const error = e as stripe.StripeRawError; + return NextResponse.json( + { ok: false, body: null, error: error.message }, + { status: 400 } + ); + } +} diff --git a/app/(api)/api/checkout_sessions/route.ts b/app/(api)/api/checkout_sessions/route.ts new file mode 100644 index 0000000..53f8630 --- /dev/null +++ b/app/(api)/api/checkout_sessions/route.ts @@ -0,0 +1,2 @@ +export { POST } from './post'; +export { GET } from './get'; diff --git a/app/(pages)/checkout/_components/PaymentForm/CardSection.js b/app/(pages)/checkout/_components/PaymentForm/CardSection.js new file mode 100644 index 0000000..eea0ff6 --- /dev/null +++ b/app/(pages)/checkout/_components/PaymentForm/CardSection.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { CardElement } from '@stripe/react-stripe-js'; +// import './Styles.css'; +const CARD_ELEMENT_OPTIONS = { + style: { + base: { + color: '#32325d', + fontFamily: '"Helvetica Neue", Helvetica, sans-serif', + fontSmoothing: 'antialiased', + fontSize: '16px', + '::placeholder': { + color: '#aab7c4', + }, + }, + invalid: { + color: '#fa755a', + iconColor: '#fa755a', + }, + }, +}; +function CardSection() { + return ( + + ); +} +export default CardSection; diff --git a/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.js b/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.js new file mode 100644 index 0000000..16300dd --- /dev/null +++ b/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.js @@ -0,0 +1,52 @@ +import React from 'react'; +import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; + +import CardSection from './CardSection'; + +export default function CheckoutForm({ secret }) { + const stripe = useStripe(); + const elements = useElements(); + + const handleSubmit = async (event) => { + // We don't want to let default form submission happen here, + // which would refresh the page. + event.preventDefault(); + + if (!stripe || !elements) { + // Stripe.js hasn't yet loaded. + // Make sure to disable form submission until Stripe.js has loaded. + return; + } + + const result = await stripe.confirmCardPayment(`${secret}`, { + payment_method: { + card: elements.getElement(CardElement), + billing_details: { + name: 'Jenny Rosen', + email: 'matt@gmail.com', + }, + }, + }); + + if (result.error) { + // Show error to your customer (for example, insufficient funds) + console.log(result.error.message); + } else { + // The payment has been processed! + if (result.paymentIntent.status === 'succeeded') { + // Show a success message to your customer + // There's a risk of the customer closing the window before callback + // execution. Set up a webhook or plugin to listen for the + // payment_intent.succeeded event that handles any business critical + // post-payment actions. + } + } + }; + + return ( +
+ + + + ); +} diff --git a/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx b/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx new file mode 100644 index 0000000..afd05ee --- /dev/null +++ b/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx @@ -0,0 +1,40 @@ +'use client'; + +import React, { useState, useEffect, useCallback } from 'react'; +import { loadStripe } from '@stripe/stripe-js'; +import { Elements } from '@stripe/react-stripe-js'; +import CheckoutForm from './CheckoutForm'; + +const stripePromise = loadStripe( + process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY! +); + +export default function App() { + const [clientSecret, setClientSecret] = useState(''); + + const fetchClientSecret = useCallback(async () => { + try { + const response = await fetch('/api/checkout_sessions', { + method: 'POST', + }); + const data = await response.json(); + setClientSecret(data.clientSecret); + } catch (error) { + console.error('Error fetching client secret:', error); + } + }, []); + + useEffect(() => { + fetchClientSecret(); + }, [fetchClientSecret]); + + return ( +
+ {clientSecret && ( + + + + )} +
+ ); +} diff --git a/app/(pages)/checkout/page.tsx b/app/(pages)/checkout/page.tsx index c11df52..75751bd 100644 --- a/app/(pages)/checkout/page.tsx +++ b/app/(pages)/checkout/page.tsx @@ -1,11 +1,15 @@ import styles from './page.module.scss'; import DeliveryFormSection from './_components/CheckoutForm/DeliveryFormSection/DeliveryFormSection'; import CheckoutCart from './_components/CheckoutCart/CheckoutCart'; +import PaymentForm from './_components/PaymentForm/PaymentForm'; export default function Checkout() { return (
- +
+ + +
); diff --git a/package-lock.json b/package-lock.json index a25bab8..edff300 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,16 @@ "dependencies": { "@apollo/client": "^3.9.4", "@apollo/experimental-nextjs-app-support": "^0.8.0", + "@stripe/react-stripe-js": "^2.7.1", + "@stripe/stripe-js": "^3.4.0", "mongodb": "^6.3.0", "next": "14.0.4", "react": "^18", "react-dom": "^18", "react-icons": "^4.12.0", "readline": "^1.3.0", - "sass": "^1.69.5" + "sass": "^1.69.5", + "stripe": "^15.6.0" }, "devDependencies": { "@next/eslint-plugin-next": "^14.0.4", @@ -680,6 +683,27 @@ "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==", "dev": true }, + "node_modules/@stripe/react-stripe-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.7.1.tgz", + "integrity": "sha512-/i13alp27HaSBbMM6kW0jhy8KqdtOL1T/EcRjFjfhvt+CBtMEg8TD7y28W3oZG0+OBDdCyGGnXgNgrKPYQH40g==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": "^1.44.1 || ^2.0.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-3.4.0.tgz", + "integrity": "sha512-a2kUP7OrsV0SSIk3UxWa+cnrW+PPIyuCbWIBH8vxfHIqmyeQN/d0lsplZJ2h7MlLsU/sB3EyhNBkhLLT+zHwKw==", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -704,7 +728,6 @@ "version": "20.10.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1504,14 +1527,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dev": true, + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1824,17 +1851,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -2034,6 +2063,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-iterator-helpers": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", @@ -3114,7 +3162,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3153,16 +3200,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dev": true, + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3298,7 +3348,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -3359,12 +3408,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3374,7 +3422,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3386,7 +3433,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3413,7 +3459,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -4519,7 +4564,6 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5113,6 +5157,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5698,15 +5756,16 @@ "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", - "dev": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5748,14 +5807,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6031,6 +6093,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-15.6.0.tgz", + "integrity": "sha512-ARG46eQHMmHspnDpj3QTAH8GyEqtE0nesbzpTtQDT/C9nHvOFYri3mIzHEzArzDcKX7HSleTu2VpYoDZIIH7nA==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -6491,8 +6565,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/untildify": { "version": "4.0.0", diff --git a/package.json b/package.json index fbb2e3f..e7088ff 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,16 @@ "dependencies": { "@apollo/client": "^3.9.4", "@apollo/experimental-nextjs-app-support": "^0.8.0", + "@stripe/react-stripe-js": "^2.7.1", + "@stripe/stripe-js": "^3.4.0", "mongodb": "^6.3.0", "next": "14.0.4", "react": "^18", "react-dom": "^18", "react-icons": "^4.12.0", "readline": "^1.3.0", - "sass": "^1.69.5" + "sass": "^1.69.5", + "stripe": "^15.6.0" }, "devDependencies": { "@next/eslint-plugin-next": "^14.0.4", diff --git a/tsconfig.json b/tsconfig.json index 499d252..2dd7bab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,6 +31,6 @@ "@datatypes/*": ["./app/(api)/_types/*"] } }, - "include": ["nextEnv.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": ["nextEnv.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/(pages)/checkout/_components/PaymentForm/CardSection.js"], "exclude": ["node_modules"] } \ No newline at end of file From d278166dfb6e3ea2bf2004ee477273f229df1f61 Mon Sep 17 00:00:00 2001 From: Mathias Gabel Date: Mon, 20 May 2024 10:34:36 -0700 Subject: [PATCH 2/6] Return payment method --- .../{CardSection.js => CardSection.tsx} | 0 .../{CheckoutForm.js => CheckoutForm.tsx} | 21 ++++++++++++++++--- tsconfig.json | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) rename app/(pages)/checkout/_components/PaymentForm/{CardSection.js => CardSection.tsx} (100%) rename app/(pages)/checkout/_components/PaymentForm/{CheckoutForm.js => CheckoutForm.tsx} (68%) diff --git a/app/(pages)/checkout/_components/PaymentForm/CardSection.js b/app/(pages)/checkout/_components/PaymentForm/CardSection.tsx similarity index 100% rename from app/(pages)/checkout/_components/PaymentForm/CardSection.js rename to app/(pages)/checkout/_components/PaymentForm/CardSection.tsx diff --git a/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.js b/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx similarity index 68% rename from app/(pages)/checkout/_components/PaymentForm/CheckoutForm.js rename to app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx index 16300dd..0e154dd 100644 --- a/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.js +++ b/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx @@ -2,12 +2,16 @@ import React from 'react'; import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; import CardSection from './CardSection'; +import Stripe from 'stripe'; -export default function CheckoutForm({ secret }) { +export default function CheckoutForm({ secret }: { secret: string }) { const stripe = useStripe(); const elements = useElements(); + const key = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY; - const handleSubmit = async (event) => { + const payment = new Stripe(`${key}`); + + const handleSubmit = async (event: { preventDefault: () => void }) => { // We don't want to let default form submission happen here, // which would refresh the page. event.preventDefault(); @@ -20,7 +24,7 @@ export default function CheckoutForm({ secret }) { const result = await stripe.confirmCardPayment(`${secret}`, { payment_method: { - card: elements.getElement(CardElement), + card: elements.getElement(CardElement)!, billing_details: { name: 'Jenny Rosen', email: 'matt@gmail.com', @@ -39,6 +43,17 @@ export default function CheckoutForm({ secret }) { // execution. Set up a webhook or plugin to listen for the // payment_intent.succeeded event that handles any business critical // post-payment actions. + console.log('Payment success'); + console.log(result.paymentIntent); + const intent = result.paymentIntent.payment_method; + + const paymentMethod = await payment.paymentMethods.retrieve( + `${intent}`, + { + apiKey: `${key}`, + } + ); + console.log(paymentMethod); } } }; diff --git a/tsconfig.json b/tsconfig.json index 2dd7bab..e81cc2b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,6 +31,6 @@ "@datatypes/*": ["./app/(api)/_types/*"] } }, - "include": ["nextEnv.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/(pages)/checkout/_components/PaymentForm/CardSection.js"], + "include": ["nextEnv.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/(pages)/checkout/_components/PaymentForm/CardSection.tsx"], "exclude": ["node_modules"] } \ No newline at end of file From a3208bb60f75abaa6794a046e5c2be263ea9df91 Mon Sep 17 00:00:00 2001 From: Mathias Gabel Date: Wed, 22 May 2024 12:42:38 -0700 Subject: [PATCH 3/6] Create payment amounts from shopping cart --- app/(api)/api/checkout_sessions/post.ts | 6 ++++-- .../_components/CheckoutCart/CheckoutCart.tsx | 5 +++-- .../_components/PaymentForm/CheckoutForm.tsx | 2 -- .../_components/PaymentForm/PaymentForm.tsx | 17 ++++++++++++++--- app/(pages)/checkout/page.tsx | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/app/(api)/api/checkout_sessions/post.ts b/app/(api)/api/checkout_sessions/post.ts index ca05d9c..cc91c6e 100644 --- a/app/(api)/api/checkout_sessions/post.ts +++ b/app/(api)/api/checkout_sessions/post.ts @@ -2,15 +2,17 @@ import { NextRequest, NextResponse } from 'next/server'; import { headers } from 'next/headers'; import stripe from 'stripe'; -export async function POST(_: NextRequest) { +export async function POST(request: NextRequest) { try { + const { total } = await request.json(); + console.log('The total is: ', total); const headersList = headers(); const origin = headersList.get('origin'); const auth_stripe = new stripe(process.env.STRIPE_SECRET_KEY!); // Create Checkout Sessions from body params. const session = await auth_stripe.paymentIntents.create({ - amount: 1099, + amount: total * 100, currency: 'usd', payment_method_types: ['card'], }); diff --git a/app/(pages)/checkout/_components/CheckoutCart/CheckoutCart.tsx b/app/(pages)/checkout/_components/CheckoutCart/CheckoutCart.tsx index 8ee0640..b2557b0 100644 --- a/app/(pages)/checkout/_components/CheckoutCart/CheckoutCart.tsx +++ b/app/(pages)/checkout/_components/CheckoutCart/CheckoutCart.tsx @@ -8,10 +8,11 @@ export default function CheckoutCart() { if (loading) { return 'loading...'; } + return (
- {JSON.stringify(cart)} -

total cost: ${compute_total()}

+ {/* {JSON.stringify(cart)} */} + {/*

total cost: ${compute_total()}

*/}
); } diff --git a/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx b/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx index 0e154dd..16f3ab0 100644 --- a/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx +++ b/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; - import CardSection from './CardSection'; import Stripe from 'stripe'; @@ -8,7 +7,6 @@ export default function CheckoutForm({ secret }: { secret: string }) { const stripe = useStripe(); const elements = useElements(); const key = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY; - const payment = new Stripe(`${key}`); const handleSubmit = async (event: { preventDefault: () => void }) => { diff --git a/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx b/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx index afd05ee..3a872c8 100644 --- a/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx +++ b/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx @@ -4,20 +4,27 @@ import React, { useState, useEffect, useCallback } from 'react'; import { loadStripe } from '@stripe/stripe-js'; import { Elements } from '@stripe/react-stripe-js'; import CheckoutForm from './CheckoutForm'; +import { useShoppingCart } from '@hooks/useShoppingCart'; const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY! ); export default function App() { + const { loading, cart, compute_total } = useShoppingCart(); const [clientSecret, setClientSecret] = useState(''); - const fetchClientSecret = useCallback(async () => { + const fetchClientSecret = useCallback(async (total: number) => { try { const response = await fetch('/api/checkout_sessions', { method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ total }), }); const data = await response.json(); + console.log(data); setClientSecret(data.clientSecret); } catch (error) { console.error('Error fetching client secret:', error); @@ -25,8 +32,12 @@ export default function App() { }, []); useEffect(() => { - fetchClientSecret(); - }, [fetchClientSecret]); + if (!loading) { + const total = compute_total(); + console.log('total:', total); + fetchClientSecret(total); + } + }, [loading, compute_total, fetchClientSecret]); return (
diff --git a/app/(pages)/checkout/page.tsx b/app/(pages)/checkout/page.tsx index 75751bd..f7c2d5c 100644 --- a/app/(pages)/checkout/page.tsx +++ b/app/(pages)/checkout/page.tsx @@ -9,8 +9,8 @@ export default function Checkout() {
+
-
); } From 7443274ec98b6fd1c19a2b4a352fb0772cbd11bf Mon Sep 17 00:00:00 2001 From: Mathias Gabel Date: Mon, 27 May 2024 10:53:52 -0700 Subject: [PATCH 4/6] Removed some unneeded console.logs --- app/(api)/api/checkout_sessions/post.ts | 6 +----- .../checkout/_components/CheckoutCart/CheckoutCart.tsx | 3 +++ .../checkout/_components/PaymentForm/PaymentForm.tsx | 2 -- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/(api)/api/checkout_sessions/post.ts b/app/(api)/api/checkout_sessions/post.ts index cc91c6e..09ea17e 100644 --- a/app/(api)/api/checkout_sessions/post.ts +++ b/app/(api)/api/checkout_sessions/post.ts @@ -1,16 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; -import { headers } from 'next/headers'; import stripe from 'stripe'; export async function POST(request: NextRequest) { try { const { total } = await request.json(); - console.log('The total is: ', total); - const headersList = headers(); - const origin = headersList.get('origin'); const auth_stripe = new stripe(process.env.STRIPE_SECRET_KEY!); - // Create Checkout Sessions from body params. + // Creates a checkout session from body params. const session = await auth_stripe.paymentIntents.create({ amount: total * 100, currency: 'usd', diff --git a/app/(pages)/checkout/_components/CheckoutCart/CheckoutCart.tsx b/app/(pages)/checkout/_components/CheckoutCart/CheckoutCart.tsx index b2557b0..55e9cbc 100644 --- a/app/(pages)/checkout/_components/CheckoutCart/CheckoutCart.tsx +++ b/app/(pages)/checkout/_components/CheckoutCart/CheckoutCart.tsx @@ -1,3 +1,6 @@ +// This file could honestly be removed but for now I'll keep it here +// in case it still has some use I might not be seeing + 'use client'; import styles from './CheckoutCart.module.scss'; diff --git a/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx b/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx index 3a872c8..001b934 100644 --- a/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx +++ b/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx @@ -24,7 +24,6 @@ export default function App() { body: JSON.stringify({ total }), }); const data = await response.json(); - console.log(data); setClientSecret(data.clientSecret); } catch (error) { console.error('Error fetching client secret:', error); @@ -34,7 +33,6 @@ export default function App() { useEffect(() => { if (!loading) { const total = compute_total(); - console.log('total:', total); fetchClientSecret(total); } }, [loading, compute_total, fetchClientSecret]); From db77e396e89af585fbfd6dd2c6c78668e5cec259 Mon Sep 17 00:00:00 2001 From: Mathias Gabel Date: Mon, 27 May 2024 17:12:16 -0700 Subject: [PATCH 5/6] Unhardcoded billing details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A few things: - Moved PaymentForm to ShippingInfo.tsx to be able to pass billing info as props - The billing info form is wrapped as a div instead of a form to prevent nested forms, and also because there's no button connected to it - A few components have been changed to accept a handful of props for billing info In summary, the billing info is not hardcoded and uses whatever information is inputted in our form 👍 --- .../DeliveryFormSection.tsx | 4 +-- .../ShippingInfo/ShippingInfo.tsx | 10 ++++++ .../_components/PaymentForm/CardSection.tsx | 10 +++--- .../_components/PaymentForm/CheckoutForm.tsx | 35 ++++++++++++++++--- .../_components/PaymentForm/PaymentForm.tsx | 29 +++++++++++++-- app/(pages)/checkout/page.tsx | 4 --- 6 files changed, 75 insertions(+), 17 deletions(-) diff --git a/app/(pages)/checkout/_components/CheckoutForm/DeliveryFormSection/DeliveryFormSection.tsx b/app/(pages)/checkout/_components/CheckoutForm/DeliveryFormSection/DeliveryFormSection.tsx index 974f557..f26047a 100644 --- a/app/(pages)/checkout/_components/CheckoutForm/DeliveryFormSection/DeliveryFormSection.tsx +++ b/app/(pages)/checkout/_components/CheckoutForm/DeliveryFormSection/DeliveryFormSection.tsx @@ -7,10 +7,10 @@ import styles from './DeliveryFormSection.module.scss'; export default function DeliveryFormSection() { return ( -
+

Delivery

- +
); } diff --git a/app/(pages)/checkout/_components/CheckoutForm/DeliveryFormSection/ShippingInfo/ShippingInfo.tsx b/app/(pages)/checkout/_components/CheckoutForm/DeliveryFormSection/ShippingInfo/ShippingInfo.tsx index 8069494..d9759e9 100644 --- a/app/(pages)/checkout/_components/CheckoutForm/DeliveryFormSection/ShippingInfo/ShippingInfo.tsx +++ b/app/(pages)/checkout/_components/CheckoutForm/DeliveryFormSection/ShippingInfo/ShippingInfo.tsx @@ -1,5 +1,6 @@ import InputField from '../InputField/InputField'; import Address from '../Address/Address'; +import PaymentForm from '../../../PaymentForm/PaymentForm'; import { useState } from 'react'; import { GoQuestion } from 'react-icons/go'; @@ -78,6 +79,15 @@ export default function ShippingInfo() { icon={GoQuestion} handleFieldChange={updateShippingField} /> + ); } diff --git a/app/(pages)/checkout/_components/PaymentForm/CardSection.tsx b/app/(pages)/checkout/_components/PaymentForm/CardSection.tsx index eea0ff6..214dc95 100644 --- a/app/(pages)/checkout/_components/PaymentForm/CardSection.tsx +++ b/app/(pages)/checkout/_components/PaymentForm/CardSection.tsx @@ -20,10 +20,12 @@ const CARD_ELEMENT_OPTIONS = { }; function CardSection() { return ( - +
+ +
); } export default CardSection; diff --git a/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx b/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx index 16f3ab0..1d750d6 100644 --- a/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx +++ b/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx @@ -3,7 +3,25 @@ import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; import CardSection from './CardSection'; import Stripe from 'stripe'; -export default function CheckoutForm({ secret }: { secret: string }) { +export default function CheckoutForm({ + secret, + name, + address, + address2, + city, + state, + zip, + phone, +}: { + secret: string; + name: string; + address: string; + address2: string; + city: string; + state: string; + zip: string; + phone: string; +}) { const stripe = useStripe(); const elements = useElements(); const key = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY; @@ -19,13 +37,22 @@ export default function CheckoutForm({ secret }: { secret: string }) { // Make sure to disable form submission until Stripe.js has loaded. return; } + console.log('address:', address); const result = await stripe.confirmCardPayment(`${secret}`, { payment_method: { card: elements.getElement(CardElement)!, billing_details: { - name: 'Jenny Rosen', - email: 'matt@gmail.com', + name: name, + address: { + city: city, + country: 'US', + line1: address, + line2: address2, + postal_code: zip, + state: state, + }, + phone: phone, }, }, }); @@ -41,8 +68,6 @@ export default function CheckoutForm({ secret }: { secret: string }) { // execution. Set up a webhook or plugin to listen for the // payment_intent.succeeded event that handles any business critical // post-payment actions. - console.log('Payment success'); - console.log(result.paymentIntent); const intent = result.paymentIntent.payment_method; const paymentMethod = await payment.paymentMethods.retrieve( diff --git a/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx b/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx index 001b934..b6bff12 100644 --- a/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx +++ b/app/(pages)/checkout/_components/PaymentForm/PaymentForm.tsx @@ -10,7 +10,23 @@ const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY! ); -export default function App() { +export default function PaymentForm({ + name, + address, + address2, + city, + state, + zip, + phone, +}: { + name: string; + address: string; + address2: string; + city: string; + state: string; + zip: string; + phone: string; +}) { const { loading, cart, compute_total } = useShoppingCart(); const [clientSecret, setClientSecret] = useState(''); @@ -41,7 +57,16 @@ export default function App() {
{clientSecret && ( - + )}
diff --git a/app/(pages)/checkout/page.tsx b/app/(pages)/checkout/page.tsx index f7c2d5c..fc9d88b 100644 --- a/app/(pages)/checkout/page.tsx +++ b/app/(pages)/checkout/page.tsx @@ -1,15 +1,11 @@ import styles from './page.module.scss'; import DeliveryFormSection from './_components/CheckoutForm/DeliveryFormSection/DeliveryFormSection'; -import CheckoutCart from './_components/CheckoutCart/CheckoutCart'; -import PaymentForm from './_components/PaymentForm/PaymentForm'; export default function Checkout() { return (
- -
); From b4dd85eef52c432ef74d528d5d508e23d17c576b Mon Sep 17 00:00:00 2001 From: Mathias Gabel Date: Wed, 29 May 2024 15:50:44 -0700 Subject: [PATCH 6/6] GraphQL mutation and queries for orders Just wrote out a mutation for posting billing info to our own database rathe than having to hop on Stripe to manage orders. This is done ONLY after the order successfully goes through Stripe. Also I made two queries for grabbing all the orders or just one order from our database. Only thing is that everything isn't tested because I couldn't see if my branch had a way for me to run a server (if that's setup already) (Austin or Brandon please save me tomorrow in meeting if you see this before) --- app/(api)/_graphql/mutations/saveInfo.ts | 37 +++++++++++++++++ app/(api)/_graphql/queries/getOrders.ts | 41 +++++++++++++++++++ .../_components/PaymentForm/CheckoutForm.tsx | 25 ++++++++++- 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 app/(api)/_graphql/mutations/saveInfo.ts create mode 100644 app/(api)/_graphql/queries/getOrders.ts diff --git a/app/(api)/_graphql/mutations/saveInfo.ts b/app/(api)/_graphql/mutations/saveInfo.ts new file mode 100644 index 0000000..953471c --- /dev/null +++ b/app/(api)/_graphql/mutations/saveInfo.ts @@ -0,0 +1,37 @@ +/* +A mutation that I hope works that's meant to save the billing info +so that we don't have to go on Stripe to see it +*/ + +import { gql } from '@apollo/client'; + +export const SAVE_BILLING_INFO = gql` + mutation SaveBillingInfo( + $secret: String! + $id: String! + $name: String! + $address: String! + $address2: String + $city: String! + $state: String! + $zip: String! + $phone: String! + $paymentMethod: String! + ) { + saveBillingInfo( + secret: $secret + id: $id + name: $name + address: $address + address2: $address2 + city: $city + state: $state + zip: $zip + phone: $phone + paymentMethod: $paymentMethod + ) { + success + message + } + } +`; diff --git a/app/(api)/_graphql/queries/getOrders.ts b/app/(api)/_graphql/queries/getOrders.ts new file mode 100644 index 0000000..9d61aa7 --- /dev/null +++ b/app/(api)/_graphql/queries/getOrders.ts @@ -0,0 +1,41 @@ +// Queries for grabbing orders that I hope works T_T + +import { gql } from '@apollo/client'; + +export const GET_ALL_ORDERS = gql` + query GetAllOrders { + orders { + id + secret + name + address + address2 + city + state + zip + phone + paymentMethod + createdAt + updatedAt + } + } +`; + +export const GET_ORDER_BY_ID = gql` + query GetOrderById($id: [ID]) { + order(id: $id) { + id + secret + name + address + address2 + city + state + zip + phone + paymentMethod + createdAt + updatedAt + } + } +`; diff --git a/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx b/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx index 1d750d6..324613c 100644 --- a/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx +++ b/app/(pages)/checkout/_components/PaymentForm/CheckoutForm.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; import CardSection from './CardSection'; import Stripe from 'stripe'; +import { useMutation } from '@apollo/client'; +import { SAVE_BILLING_INFO } from '@graphql/mutations/saveInfo'; export default function CheckoutForm({ secret, @@ -26,6 +28,7 @@ export default function CheckoutForm({ const elements = useElements(); const key = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY; const payment = new Stripe(`${key}`); + const [saveBillingInfo] = useMutation(SAVE_BILLING_INFO); const handleSubmit = async (event: { preventDefault: () => void }) => { // We don't want to let default form submission happen here, @@ -37,7 +40,6 @@ export default function CheckoutForm({ // Make sure to disable form submission until Stripe.js has loaded. return; } - console.log('address:', address); const result = await stripe.confirmCardPayment(`${secret}`, { payment_method: { @@ -76,7 +78,26 @@ export default function CheckoutForm({ apiKey: `${key}`, } ); - console.log(paymentMethod); + console.log('method: ', paymentMethod); + + try { + const { data } = await saveBillingInfo({ + variables: { + secret, + paymentMethod: paymentMethod.id, + name: paymentMethod.billing_details.name, + address: paymentMethod.billing_details.address?.line1, + address2: paymentMethod.billing_details.address?.line2, + city: paymentMethod.billing_details.address?.city, + state: paymentMethod.billing_details.address?.state, + zip: paymentMethod.billing_details.address?.postal_code, + phone: paymentMethod.billing_details.phone, + }, + }); + console.log(data); + } catch (error) { + console.error('Someone sux at graphql routes (error msg): ', error); + } } } };