Forked from pasindu1/gist:afd415430378bb68fc29a341d61262fd
Last active
June 10, 2024 12:11
-
-
Save roni5/d7e52d552df7e291db5f4ad72fa17aa3 to your computer and use it in GitHub Desktop.
folder-structure
This file contains hidden or 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
| import { useState } from "react"; | |
| import { useFormState } from "react-dom"; | |
| import { addToCart } from "./actions.js"; | |
| function AddToCartForm({ itemID, itemTitle }) { | |
| const [message, formAction] = useFormState(addToCart, null); | |
| return ( | |
| <form action={formAction}> | |
| <h2>{itemTitle}</h2> | |
| <input type="hidden" name="itemID" value={itemID} /> | |
| <button type="submit">Add to Cart</button> | |
| {message} | |
| </form> | |
| ); | |
| } | |
| export default function App() { | |
| return ( | |
| <> | |
| <AddToCartForm itemID="1" itemTitle="JavaScript: The Definitive Guide" /> | |
| <AddToCartForm itemID="2" itemTitle="JavaScript: The Good Parts" /> | |
| </> | |
| ); | |
| } | |
| action |
This file contains hidden or 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
| // components/CheckoutForm.tsx | |
| import React, { useState } from 'react'; | |
| import { loadStripe } from '@stripe/stripe-js'; | |
| import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; | |
| import axios from 'axios'; | |
| const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string); | |
| const CheckoutForm: React.FC = () => { | |
| const [input, setInput] = useState<{ cardholderName: string }>({ | |
| cardholderName: '', | |
| }); | |
| const [clientSecret, setClientSecret] = useState<string | null>(null); | |
| const stripe = useStripe(); | |
| const elements = useElements(); | |
| const handleSubmit = async (event: React.FormEvent) => { | |
| event.preventDefault(); | |
| if (!stripe || !elements) { | |
| return; | |
| } | |
| const { data } = await axios.post('/api/create-payment-intent', { | |
| items: [ | |
| { priceId: 'price_1Hh1Ya2eZvKYlo2CYz6LFqx2', quantity: 1 }, // Example price ID from Stripe | |
| ], | |
| }); | |
| setClientSecret(data.clientSecret); | |
| if (clientSecret) { | |
| const cardElement = elements.getElement(CardElement); | |
| if (cardElement) { | |
| const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, { | |
| payment_method: { | |
| card: cardElement, | |
| billing_details: { | |
| name: input.cardholderName, | |
| }, | |
| }, | |
| }); | |
| if (error) { | |
| console.error(error); | |
| } else if (paymentIntent && paymentIntent.status === 'succeeded') { | |
| console.log('Payment succeeded!'); | |
| } | |
| } | |
| } | |
| }; | |
| return ( | |
| <form onSubmit={handleSubmit}> | |
| <label> | |
| Cardholder Name | |
| <input | |
| type="text" | |
| value={input.cardholderName} | |
| onChange={(e) => setInput({ ...input, cardholderName: e.target.value })} | |
| /> | |
| </label> | |
| <CardElement /> | |
| <button type="submit" disabled={!stripe}> | |
| Pay | |
| </button> | |
| </form> | |
| ); | |
| }; | |
| const WrappedCheckoutForm: React.FC = () => ( | |
| <Elements stripe={stripePromise}> | |
| <CheckoutForm /> | |
| </Elements> | |
| ); | |
| export default WrappedCheckoutForm; |
This file contains hidden or 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
| Backend Endpoint: The /api/create-payment-intent endpoint calculates the total amount based on product prices retrieved from Stripe and creates a payment intent. | |
| Price IDs: Use Stripe price IDs (priceId) to retrieve the unit amount for each product item. | |
| Calculate Total Amount: Sum the unit amounts multiplied by their respective quantities to get the total amount. | |
| // pages/api/create-payment-intent.ts | |
| import { NextApiRequest, NextApiResponse } from 'next'; | |
| import Stripe from 'stripe'; | |
| const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { | |
| apiVersion: '2023-08-16', | |
| }); | |
| type CartItem = { | |
| priceId: string; | |
| quantity: number; | |
| }; | |
| export default async function handler( | |
| req: NextApiRequest, | |
| res: NextApiResponse | |
| ) { | |
| if (req.method === 'POST') { | |
| try { | |
| const { items }: { items: CartItem[] } = req.body; | |
| // Fetch product prices from Stripe | |
| const prices = await Promise.all( | |
| items.map(async (item) => { | |
| const price = await stripe.prices.retrieve(item.priceId); | |
| return { | |
| unit_amount: price.unit_amount, | |
| quantity: item.quantity, | |
| }; | |
| }) | |
| ); | |
| const amount = prices.reduce( | |
| (total, price) => total + (price.unit_amount ?? 0) * price.quantity, | |
| 0 | |
| ); | |
| const paymentIntent = await stripe.paymentIntents.create({ | |
| amount, | |
| currency: 'usd', | |
| automatic_payment_methods: { enabled: true }, | |
| }); | |
| res.status(200).send({ | |
| clientSecret: paymentIntent.client_secret, | |
| }); | |
| } catch (error) { | |
| console.error(error); | |
| res.status(500).send({ error: 'Failed to create payment intent' }); | |
| } | |
| } else { | |
| res.setHeader('Allow', 'POST'); | |
| res.status(405).end('Method Not Allowed'); | |
| } | |
| } |
This file contains hidden or 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
| "use client"; | |
| import type { StripeError } from "@stripe/stripe-js"; | |
| import * as React from "react"; | |
| import { | |
| useStripe, | |
| useElements, | |
| PaymentElement, | |
| Elements, | |
| } from "@stripe/react-stripe-js"; | |
| import StripeTestCards from "./StripeTestCards"; | |
| import { formatAmountForDisplay } from "@/utils/stripe-helpers"; | |
| import * as config from "@/config"; | |
| import getStripe from "@/utils/get-stripejs"; | |
| import { createPaymentIntent } from "@/actions/cart/stripe"; | |
| type Props = { | |
| productPrices: { | |
| productId: string; | |
| priceId: string; | |
| productName: string; | |
| unit_amount: number | null; | |
| currency: string | null; | |
| }[]; | |
| }; | |
| function CheckoutForm5({ productPrices }: Props) { | |
| const [selectedPriceId, setSelectedPriceId] = React.useState<string | null>( | |
| productPrices.length > 0 ? productPrices[0].priceId : null | |
| ); | |
| const [input, setInput] = React.useState<{ | |
| amount: number; | |
| cardholderName: string; | |
| quantity: number; | |
| }>({ | |
| amount: | |
| productPrices.find((p) => p.priceId === selectedPriceId)?.unit_amount || 0, | |
| cardholderName: "", | |
| quantity: 1, | |
| }); | |
| const [paymentType, setPaymentType] = React.useState<string>(""); | |
| const [payment, setPayment] = React.useState<{ | |
| status: "initial" | "processing" | "error"; | |
| }>({ status: "initial" }); | |
| const [errorMessage, setErrorMessage] = React.useState<string>(""); | |
| const stripe = useStripe(); | |
| const elements = useElements(); | |
| const PaymentStatus = ({ status }: { status: string }) => { | |
| switch (status) { | |
| case "processing": | |
| case "requires_payment_method": | |
| case "requires_confirmation": | |
| return <h2>Processing...</h2>; | |
| case "requires_action": | |
| return <h2>Authenticating...</h2>; | |
| case "succeeded": | |
| return <h2>Payment Succeeded 🎉</h2>; | |
| case "error": | |
| return ( | |
| <> | |
| <h2>Error ❌</h2> | |
| <p className="error-message">{errorMessage}</p> | |
| </> | |
| ); | |
| default: | |
| return null; | |
| } | |
| }; | |
| const handleInputChange: React.ChangeEventHandler< | |
| HTMLInputElement | HTMLSelectElement | |
| > = (e) => { | |
| if (e.target.name === "priceId") { | |
| const price = productPrices.find((p) => p.priceId === e.target.value); | |
| setSelectedPriceId(e.target.value); | |
| if (price?.unit_amount) { | |
| setInput({ ...input, amount: price.unit_amount * input.quantity }); | |
| elements?.update({ | |
| amount: price.unit_amount * input.quantity, | |
| }); | |
| } | |
| } else { | |
| setInput({ | |
| ...input, | |
| [e.target.name]: e.target.value, | |
| }); | |
| if (e.target.name === "quantity" && selectedPriceId) { | |
| const price = productPrices.find((p) => p.priceId === selectedPriceId); | |
| if (price?.unit_amount) { | |
| elements?.update({ | |
| amount: price.unit_amount * Number(e.target.value), | |
| }); | |
| } | |
| } | |
| } | |
| }; | |
| const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => { | |
| try { | |
| e.preventDefault(); | |
| // Abort if form isn't valid | |
| if (!e.currentTarget.reportValidity()) return; | |
| if (!elements || !stripe) return; | |
| setPayment({ status: "processing" }); | |
| const { error: submitError } = await elements.submit(); | |
| if (submitError) { | |
| setPayment({ status: "error" }); | |
| setErrorMessage(submitError.message ?? "An unknown error occurred"); | |
| return; | |
| } | |
| const formData = new FormData(e.target as HTMLFormElement); | |
| formData.append( | |
| "amount", | |
| ( | |
| (productPrices.find((p) => p.priceId === selectedPriceId) | |
| ?.unit_amount || 0) * input.quantity | |
| ).toString() | |
| ); | |
| formData.append("priceId", selectedPriceId || ""); | |
| const { client_secret: clientSecret } = await createPaymentIntent( | |
| formData | |
| ); | |
| const { error: confirmError } = await stripe!.confirmPayment({ | |
| elements, | |
| clientSecret, | |
| confirmParams: { | |
| return_url: `${window.location.origin}/front`, | |
| payment_method_data: { | |
| billing_details: { | |
| name: input.cardholderName, | |
| }, | |
| }, | |
| }, | |
| }); | |
| if (confirmError) { | |
| setPayment({ status: "error" }); | |
| setErrorMessage(confirmError.message ?? "An unknown error occurred"); | |
| } | |
| } catch (err) { | |
| const { message } = err as StripeError; | |
| setPayment({ status: "error" }); | |
| setErrorMessage(message ?? "An unknown error occurred"); | |
| } | |
| }; | |
| return ( | |
| <> | |
| <form onSubmit={handleSubmit}> | |
| <StripeTestCards /> | |
| <fieldset className="elements-style"> | |
| <legend>Your payment details:</legend> | |
| {paymentType === "card" ? ( | |
| <input | |
| placeholder="Cardholder name" | |
| className="elements-style" | |
| type="Text" | |
| name="cardholderName" | |
| onChange={handleInputChange} | |
| required | |
| /> | |
| ) : null} | |
| <div className="FormRow elements-style"> | |
| <PaymentElement | |
| onChange={(e) => { | |
| setPaymentType(e.value.type); | |
| }} | |
| /> | |
| </div> | |
| <label htmlFor="priceId">Amount:</label> | |
| <select | |
| id="priceId" | |
| name="priceId" | |
| value={selectedPriceId || ""} | |
| onChange={handleInputChange} | |
| className="elements-style" | |
| > | |
| {productPrices.map((price) => ( | |
| <option key={price.priceId} value={price.priceId}> | |
| {price.productName} -{" "} | |
| {formatAmountForDisplay( | |
| price.unit_amount || 0, | |
| price.currency || config.CURRENCY | |
| )} | |
| </option> | |
| ))} | |
| </select> | |
| </fieldset> | |
| <label> | |
| Quantity: | |
| <input | |
| type="number" | |
| name="quantity" | |
| className="elements-style" | |
| value={input.quantity} | |
| onChange={handleInputChange} | |
| min="1" | |
| /> | |
| </label> | |
| <button | |
| className="elements-style-background" | |
| type="submit" | |
| disabled={ | |
| !["initial", "succeeded", "error"].includes(payment.status) || | |
| !stripe || | |
| !selectedPriceId | |
| } | |
| > | |
| Pay Now | |
| </button> | |
| </form> | |
| <PaymentStatus status={payment.status} /> | |
| </> | |
| ); | |
| } | |
| export default function ElementsForm({ | |
| productPrices, | |
| }: { | |
| productPrices: Props["productPrices"]; | |
| }): JSX.Element { | |
| return ( | |
| <Elements | |
| stripe={getStripe()} | |
| options={{ | |
| appearance: { | |
| variables: { | |
| colorIcon: "#5469d4", | |
| fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif", | |
| }, | |
| }, | |
| currency: config.CURRENCY, | |
| mode: "payment", | |
| amount: | |
| productPrices.length > 0 | |
| ? productPrices[0].unit_amount || 1400 * 100 // Provide a default if no product prices are available | |
| : 1400 * 100, | |
| }} | |
| > | |
| <CheckoutForm5 productPrices={productPrices} /> | |
| </Elements> | |
| ); | |
| } |
This file contains hidden or 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
| "use client"; | |
| import type { StripeError } from "@stripe/stripe-js"; | |
| import * as React from "react"; | |
| import { useState, useEffect } from 'react'; | |
| import { | |
| useStripe, | |
| useElements, | |
| CardElement, | |
| PaymentElement, | |
| Elements, | |
| } from "@stripe/react-stripe-js"; | |
| //import CustomDonationInput from "./CustomDonationInput"; | |
| import StripeTestCards from "./StripeTestCards"; | |
| import { formatAmountForDisplay } from "@/utils/stripe-helpers"; | |
| import * as config from "@/config"; | |
| import getStripe from "@/utils/get-stripejs"; | |
| import { createPaymentIntent } from "@/actions/cart/stripe"; | |
| // import { loadStripe } from '@stripe/stripe-js'; | |
| // const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string); | |
| type ProductPrice = { | |
| productId: string; | |
| priceId: string; | |
| productName: string; | |
| unit_amount: number; | |
| currency: string; | |
| quantity: number | 1; | |
| }; | |
| function CheckoutForm3(): JSX.Element { | |
| const [paymentType, setPaymentType] = React.useState<string>(""); | |
| const [payment, setPayment] = React.useState<{ | |
| status: "initial" | "processing" | "error"; | |
| }>({ status: "initial" }); | |
| const [errorMessage, setErrorMessage] = React.useState<string>(""); | |
| const [products, setProducts] = useState<ProductPrice[]>([]); | |
| const [selectedProduct, setSelectedProduct] = useState<string | null>(null); | |
| const [inputCard, setInputCard] = useState<{ cardholderName: string }>({ cardholderName: '' }); | |
| const [input, setInput] = React.useState<{ | |
| cardholderName: string | |
| quantity: number;}>({ | |
| cardholderName: '', | |
| quantity: 1, | |
| }); | |
| const [quantity, setQuantity] = useState<number>(1); | |
| const [clientSecret, setClientSecret] = useState<string | null>(null); | |
| const stripe = useStripe(); | |
| const elements = useElements(); | |
| const PaymentStatus = ({ status }: { status: string }) => { | |
| switch (status) { | |
| case "processing": | |
| case "requires_payment_method": | |
| case "requires_confirmation": | |
| return <h2>Processing...</h2>; | |
| case "requires_action": | |
| return <h2>Authenticating...</h2>; | |
| case "succeeded": | |
| return <h2>Payment Succeeded 🎉</h2>; | |
| case "error": | |
| return ( | |
| <> | |
| <h2>Error ❌</h2> | |
| <p className="error-message">{errorMessage}</p> | |
| </> | |
| ); | |
| default: | |
| return null; | |
| } | |
| }; | |
| const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { | |
| setInput({ | |
| ...input, | |
| [e.currentTarget.name]: e.currentTarget.value, | |
| }); | |
| //elements?.update({ amount: 1400 * 100 }); | |
| }; | |
| useEffect(() => { | |
| // Fetch product prices from the backend | |
| const fetchProductPrices = async () => { | |
| try { | |
| const response = await fetch('/api/get-product-prices'); | |
| if (!response.ok) { | |
| throw new Error('Network response was not ok'); | |
| } | |
| const data = await response.json(); | |
| setProducts(data); | |
| } catch (error) { | |
| console.error('Error fetching product prices:', error); | |
| } | |
| }; | |
| fetchProductPrices(); | |
| }, []); | |
| // const handleSubmit = async (event: React.FormEvent) => { | |
| // event.preventDefault(); | |
| // if (!stripe || !elements || !selectedProduct) { | |
| // return; | |
| // } | |
| // const product = products.find(product => product.productId === selectedProduct); | |
| // if (!product) { | |
| // console.error('Selected product not found'); | |
| // return; | |
| // } | |
| // try { | |
| // const response = await fetch('/api/create-payment-intent', { | |
| // method: 'POST', | |
| // headers: { | |
| // 'Content-Type': 'application/json', | |
| // }, | |
| // body: JSON.stringify({ | |
| // items: [{ priceId: product.priceId, quantity }], | |
| // }), | |
| // }); | |
| // if (!response.ok) { | |
| // throw new Error('Network response was not ok'); | |
| // } | |
| // const data = await response.json(); | |
| // setClientSecret(data.clientSecret); | |
| // if (clientSecret) { | |
| // const cardElement = elements.getElement(CardElement); | |
| // if (cardElement) { | |
| // const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, { | |
| // payment_method: { | |
| // card: cardElement, | |
| // billing_details: { | |
| // name: input.cardholderName, | |
| // }, | |
| // }, | |
| // }); | |
| // if (error) { | |
| // console.error(error); | |
| // } else if (paymentIntent && paymentIntent.status === 'succeeded') { | |
| // console.log('Payment succeeded!'); | |
| // } | |
| // } | |
| // } | |
| // } catch (error) { | |
| // console.error('Error:', error); | |
| // } | |
| // }; | |
| // const handleSubmit = async (event: React.FormEvent) => { | |
| // event.preventDefault(); | |
| // if (!stripe || !elements || !selectedProduct) { | |
| // return; | |
| // } | |
| // const product = products.find(product => product.productId === selectedProduct); | |
| // if (!product) { | |
| // console.error('Selected product not found'); | |
| // return; | |
| // } | |
| // try { | |
| // // Create a FormData object | |
| // const formData = new FormData(); | |
| // formData.append('priceId', product.priceId); | |
| // formData.append('quantity', input.quantity.toString()); | |
| // // Call createPaymentIntent with the FormData object | |
| // const { client_secret: clientSecret } = await createPaymentIntent(formData); | |
| // if (!clientSecret) { | |
| // console.error('Failed to retrieve client secret for payment'); | |
| // return; | |
| // } | |
| // const cardElement = elements.getElement(CardElement); | |
| // if (!cardElement) { | |
| // console.error('Card element not found'); | |
| // return; | |
| // } | |
| // const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, { | |
| // payment_method: { | |
| // card: cardElement, | |
| // billing_details: { | |
| // name: input.cardholderName, | |
| // }, | |
| // }, | |
| // }); | |
| // if (error) { | |
| // console.error('Error confirming card payment:', error); | |
| // } else if (paymentIntent && paymentIntent.status === 'succeeded') { | |
| // console.log('Payment succeeded!'); | |
| // } | |
| // } catch (error) { | |
| // console.error('Error processing payment:', error); | |
| // } | |
| // }; | |
| const handleSubmit = async (event: React.FormEvent) => { | |
| event.preventDefault(); | |
| if (!stripe || !elements || !selectedProduct) { | |
| return; | |
| } | |
| const product = products.find(product => product.productId === selectedProduct); | |
| if (!product) { | |
| console.error('Selected product not found'); | |
| return; | |
| } | |
| try { | |
| // Calculate the total amount based on the selected product and quantity | |
| const totalAmount = product.unit_amount * input.quantity; | |
| // Create a FormData object | |
| const formData = new FormData(); | |
| formData.append('priceId', product.priceId); | |
| formData.append('quantity', input.quantity.toString()); | |
| formData.append('amount', totalAmount.toString()); // Pass the total amount to the backend | |
| // Call createPaymentIntent with the FormData object | |
| const { client_secret: clientSecret } = await createPaymentIntent(formData); | |
| if (!clientSecret) { | |
| console.error('Failed to retrieve client secret for payment'); | |
| return; | |
| } | |
| const cardElement = elements.getElement(CardElement); | |
| if (!cardElement) { | |
| console.error('Card element not found'); | |
| return; | |
| } | |
| const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, { | |
| payment_method: { | |
| card: cardElement, | |
| billing_details: { | |
| name: input.cardholderName, | |
| }, | |
| }, | |
| }); | |
| if (error) { | |
| console.error('Error confirming card payment:', error); | |
| } else if (paymentIntent && paymentIntent.status === 'succeeded') { | |
| console.log('Payment succeeded!'); | |
| } | |
| } catch (error) { | |
| console.error('Error processing payment:', error); | |
| } | |
| }; | |
| return ( | |
| <form onSubmit={handleSubmit}> | |
| <label> | |
| Cardholder Name | |
| <input | |
| type="text" | |
| value={input.cardholderName} | |
| onChange={(e) => setInput({ ...input, cardholderName: e.target.value })} | |
| /> | |
| </label> | |
| <label> | |
| Select Product | |
| <select onChange={(e) => setSelectedProduct(e.target.value)} value={selectedProduct || ''}> | |
| <option value="" disabled> | |
| Select a product | |
| </option> | |
| {products.map((product) => ( | |
| <option key={product.productId} value={product.productId}> | |
| {product.productName} - {product.unit_amount / 100} {product.currency.toUpperCase()} | |
| </option> | |
| ))} | |
| </select> | |
| </label> | |
| <label> | |
| Quantity: | |
| <input | |
| type="number" | |
| name="quantity" | |
| className="elements-style" | |
| value={input.quantity} | |
| onChange={handleInputChange} | |
| min="1" | |
| /> | |
| </label> | |
| <label> | |
| Quantity | |
| <input | |
| type="number" | |
| value={quantity} | |
| onChange={(e) => setQuantity(parseInt(e.target.value, 10))} | |
| min="1" | |
| /> | |
| </label> | |
| <CardElement /> | |
| <button type="submit" disabled={!stripe}> | |
| Pay | |
| </button> | |
| </form> | |
| ); | |
| }; | |
| export default function ElementsForm(): JSX.Element { | |
| return ( | |
| <Elements | |
| stripe={getStripe()} | |
| options={{ | |
| appearance: { | |
| variables: { | |
| colorIcon: "#5469d4", | |
| fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif", | |
| }, | |
| }, | |
| currency: config.CURRENCY, | |
| mode: "payment", | |
| }} | |
| > | |
| <CheckoutForm3 /> | |
| </Elements> | |
| ); | |
| } |
This file contains hidden or 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
| src/ | |
| |--app/ | |
| | |--layout.tsx | |
| | |--page.tsx | |
| | |--api/ | |
| | | |--route.ts | |
| | | | |users/ | |
| | | | |--route.ts |
This file contains hidden or 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
| import { Card } from '@/app/ui/dashboard/cards'; | |
| import RevenueChart from '@/app/ui/dashboard/revenue-chart'; | |
| import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; | |
| import { lusitana } from '@/app/ui/fonts'; | |
| export default async function Page() { | |
| return ( | |
| <main> | |
| <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}> | |
| Dashboard | |
| </h1> | |
| https://dev.to/w3tsa/nextjs-14-fetching-data-elm | |
| <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4"> | |
| {/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */} | |
| {/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */} | |
| {/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */} | |
| {/* <Card | |
| title="Total Customers" | |
| value={numberOfCustomers} | |
| type="customers" | |
| /> */} | |
| </div> | |
| <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8"> | |
| {/* <RevenueChart revenue={revenue} /> */} | |
| {/* <LatestInvoices latestInvoices={latestInvoices} /> */} | |
| </div> | |
| </main> | |
| ); | |
| } |
This file contains hidden or 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
| "use client"; | |
| import type { StripeError } from "@stripe/stripe-js"; | |
| import * as React from "react"; | |
| import { | |
| useStripe, | |
| useElements, | |
| PaymentElement, | |
| Elements, | |
| } from "@stripe/react-stripe-js"; | |
| import StripeTestCards from "./StripeTestCards"; | |
| import { formatAmountForDisplay } from "@/utils/stripe-helpers"; | |
| import * as config from "@/config"; | |
| import getStripe from "@/utils/get-stripejs"; | |
| import { createPaymentIntent } from "@/actions/cart/stripe"; | |
| function CheckoutForm(): JSX.Element { | |
| const [input, setInput] = React.useState<{ | |
| amount: number; | |
| cardholderName: string; | |
| quantity: number; | |
| productId: string; | |
| }>({ | |
| amount: 1400, | |
| cardholderName: "", | |
| quantity: 1, | |
| productId: "", | |
| }); | |
| const [paymentType, setPaymentType] = React.useState<string>(""); | |
| const [payment, setPayment] = React.useState<{ | |
| status: "initial" | "processing" | "error"; | |
| }>({ status: "initial" }); | |
| const [errorMessage, setErrorMessage] = React.useState<string>(""); | |
| const stripe = useStripe(); | |
| const elements = useElements(); | |
| const PaymentStatus = ({ status }: { status: string }) => { | |
| switch (status) { | |
| case "processing": | |
| case "requires_payment_method": | |
| case "requires_confirmation": | |
| return <h2>Processing...</h2>; | |
| case "requires_action": | |
| return <h2>Authenticating...</h2>; | |
| case "succeeded": | |
| return <h2>Payment Succeeded 🎉</h2>; | |
| case "error": | |
| return ( | |
| <> | |
| <h2>Error ❌</h2> | |
| <p className="error-message">{errorMessage}</p> | |
| </> | |
| ); | |
| default: | |
| return null; | |
| } | |
| }; | |
| const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { | |
| setInput({ | |
| ...input, | |
| [e.currentTarget.name]: e.currentTarget.value, | |
| }); | |
| }; | |
| const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => { | |
| try { | |
| e.preventDefault(); | |
| if (!e.currentTarget.reportValidity()) return; | |
| if (!elements || !stripe) return; | |
| setPayment({ status: "processing" }); | |
| const { error: submitError } = await elements.submit(); | |
| if (submitError) { | |
| setPayment({ status: "error" }); | |
| setErrorMessage(submitError.message ?? "An unknown error occurred"); | |
| return; | |
| } | |
| // Fetch product price from the API | |
| const response = await fetch("/api/get-product-prices"); | |
| if (!response.ok) { | |
| throw new Error("Failed to fetch product prices"); | |
| } | |
| const productPrices = await response.json(); | |
| // Find the product price based on the selected product ID | |
| const selectedProductPrice = productPrices.find( | |
| (productPrice: any) => productPrice.productId === input.productId | |
| ); | |
| if (!selectedProductPrice) { | |
| throw new Error("Selected product price not found"); | |
| } | |
| // Calculate the total amount based on the selected product price and quantity | |
| const totalAmount = selectedProductPrice.price * input.quantity; | |
| console.log("Total Amount:", totalAmount); // Debugging line | |
| // Create a PaymentIntent with the specified amount | |
| const formData = new FormData(e.target as HTMLFormElement); | |
| formData.append("amount", totalAmount.toString()); | |
| const { client_secret: clientSecret } = await createPaymentIntent(formData); | |
| console.log("Client Secret:", clientSecret); // Debugging line | |
| // Use your card Element with other Stripe.js APIs | |
| const { error: confirmError } = await stripe!.confirmPayment({ | |
| elements, | |
| clientSecret, | |
| confirmParams: { | |
| return_url: `${window.location.origin}/front`, | |
| payment_method_data: { | |
| billing_details: { | |
| name: input.cardholderName, | |
| }, | |
| }, | |
| }, | |
| }); | |
| if (confirmError) { | |
| setPayment({ status: "error" }); | |
| setErrorMessage(confirmError.message ?? "An unknown error occurred"); | |
| } else { | |
| setPayment({ status: "succeeded" }); | |
| } | |
| } catch (err) { | |
| const { message } = err as StripeError; | |
| setPayment({ status: "error" }); | |
| setErrorMessage(message ?? "An unknown error occurred"); | |
| } | |
| }; | |
| return ( | |
| <> | |
| <form onSubmit={handleSubmit}> | |
| <StripeTestCards /> | |
| <fieldset className="elements-style"> | |
| <legend>Your payment details:</legend> | |
| {paymentType === "card" ? ( | |
| <input | |
| placeholder="Cardholder name" | |
| className="elements-style" | |
| type="text" | |
| name="cardholderName" | |
| onChange={handleInputChange} | |
| required | |
| /> | |
| ) : null} | |
| <div className="FormRow elements-style"> | |
| <PaymentElement | |
| onChange={(e) => { | |
| setPaymentType(e.value.type); | |
| }} | |
| /> | |
| </div> | |
| <input | |
| type="hidden" | |
| name="productId" | |
| value={input.productId} | |
| onChange={handleInputChange} | |
| /> | |
| </fieldset> | |
| <label> | |
| Quantity: | |
| <input | |
| type="number" | |
| name="quantity" | |
| className="elements-style" | |
| value={input.quantity} | |
| onChange={handleInputChange} | |
| min="1" | |
| /> | |
| </label> | |
| <button | |
| className="elements-style-background" | |
| type="submit" | |
| disabled={ | |
| !["initial", "succeeded", "error"].includes(payment.status) || | |
| !stripe | |
| } | |
| > | |
| Pay Now | |
| </button> | |
| </form> | |
| <PaymentStatus status={payment.status} /> | |
| </> | |
| ); | |
| } | |
| export default function ElementsForm(): JSX.Element { | |
| return ( | |
| <Elements | |
| stripe={getStripe()} | |
| options={{ | |
| appearance: { | |
| variables: { | |
| colorIcon: "#5469d4", | |
| fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif", | |
| }, | |
| }, | |
| currency: config.CURRENCY, | |
| mode: "payment", | |
| }} | |
| > | |
| <CheckoutForm /> | |
| </Elements> | |
| ); | |
| } |
This file contains hidden or 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
| "use client"; | |
| import type { StripeError } from "@stripe/stripe-js"; | |
| import * as React from "react"; | |
| import { | |
| useStripe, | |
| useElements, | |
| PaymentElement, | |
| Elements, | |
| } from "@stripe/react-stripe-js"; | |
| import StripeTestCards from "./StripeTestCards"; | |
| import { formatAmountForDisplay } from "@/utils/stripe-helpers"; | |
| import * as config from "@/config"; | |
| import getStripe from "@/utils/get-stripejs"; | |
| import { createPaymentIntent } from "@/actions/cart/stripe"; | |
| function CheckoutForm(): JSX.Element { | |
| const [input, setInput] = React.useState<{ | |
| amount: number; | |
| cardholderName: string; | |
| quantity: number; | |
| productId: string; | |
| }>({ | |
| amount: 1400, | |
| cardholderName: "", | |
| quantity: 1, | |
| productId: "", | |
| }); | |
| const [paymentType, setPaymentType] = React.useState<string>(""); | |
| const [payment, setPayment] = React.useState<{ | |
| status: "initial" | "processing" | "error"; | |
| }>({ status: "initial" }); | |
| const [errorMessage, setErrorMessage] = React.useState<string>(""); | |
| const stripe = useStripe(); | |
| const elements = useElements(); | |
| const PaymentStatus = ({ status }: { status: string }) => { | |
| switch (status) { | |
| case "processing": | |
| case "requires_payment_method": | |
| case "requires_confirmation": | |
| return <h2>Processing...</h2>; | |
| case "requires_action": | |
| return <h2>Authenticating...</h2>; | |
| case "succeeded": | |
| return <h2>Payment Succeeded 🎉</h2>; | |
| case "error": | |
| return ( | |
| <> | |
| <h2>Error ❌</h2> | |
| <p className="error-message">{errorMessage}</p> | |
| </> | |
| ); | |
| default: | |
| return null; | |
| } | |
| }; | |
| const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { | |
| setInput({ | |
| ...input, | |
| [e.currentTarget.name]: e.currentTarget.value, | |
| }); | |
| }; | |
| const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => { | |
| try { | |
| e.preventDefault(); | |
| if (!e.currentTarget.reportValidity()) return; | |
| if (!elements || !stripe) return; | |
| setPayment({ status: "processing" }); | |
| const { error: submitError } = await elements.submit(); | |
| if (submitError) { | |
| setPayment({ status: "error" }); | |
| setErrorMessage(submitError.message ?? "An unknown error occurred"); | |
| return; | |
| } | |
| // Fetch product price from the API | |
| const response = await fetch("/api/get-product-prices"); | |
| if (!response.ok) { | |
| throw new Error("Failed to fetch product prices"); | |
| } | |
| const productPrices = await response.json(); | |
| // Find the product price based on the selected product ID | |
| const selectedProductPrice = productPrices.find( | |
| (productPrice: any) => productPrice.productId === input.productId | |
| ); | |
| if (!selectedProductPrice) { | |
| throw new Error("Selected product price not found"); | |
| } | |
| // Calculate the total amount based on the selected product price and quantity | |
| const totalAmount = selectedProductPrice.price * input.quantity; | |
| console.log("Total Amount:", totalAmount); // Debugging line | |
| // Create a PaymentIntent with the specified amount | |
| const formData = new FormData(e.target as HTMLFormElement); | |
| formData.append("amount", totalAmount.toString()); | |
| const { client_secret: clientSecret } = await createPaymentIntent(formData); | |
| console.log("Client Secret:", clientSecret); // Debugging line | |
| // Use your card Element with other Stripe.js APIs | |
| const { error: confirmError } = await stripe!.confirmPayment({ | |
| elements, | |
| clientSecret, | |
| confirmParams: { | |
| return_url: `${window.location.origin}/front`, | |
| payment_method_data: { | |
| billing_details: { | |
| name: input.cardholderName, | |
| }, | |
| }, | |
| }, | |
| }); | |
| if (confirmError) { | |
| setPayment({ status: "error" }); | |
| setErrorMessage(confirmError.message ?? "An unknown error occurred"); | |
| } else { | |
| setPayment({ status: "succeeded" }); | |
| } | |
| } catch (err) { | |
| const { message } = err as StripeError; | |
| setPayment({ status: "error" }); | |
| setErrorMessage(message ?? "An unknown error occurred"); | |
| } | |
| }; | |
| return ( | |
| <> | |
| <form onSubmit={handleSubmit}> | |
| <StripeTestCards /> | |
| <fieldset className="elements-style"> | |
| <legend>Your payment details:</legend> | |
| {paymentType === "card" ? ( | |
| <input | |
| placeholder="Cardholder name" | |
| className="elements-style" | |
| type="text" | |
| name="cardholderName" | |
| onChange={handleInputChange} | |
| required | |
| /> | |
| ) : null} | |
| <div className="FormRow elements-style"> | |
| <PaymentElement | |
| onChange={(e) => { | |
| setPaymentType(e.value.type); | |
| }} | |
| /> | |
| </div> | |
| <input | |
| type="hidden" | |
| name="productId" | |
| value={input.productId} | |
| onChange={handleInputChange} | |
| /> | |
| </fieldset> | |
| <label> | |
| Quantity: | |
| <input | |
| type="number" | |
| name="quantity" | |
| className="elements-style" | |
| value={input.quantity} | |
| onChange={handleInputChange} | |
| min="1" | |
| /> | |
| </label> | |
| <button | |
| className="elements-style-background" | |
| type="submit" | |
| disabled={ | |
| !["initial", "succeeded", "error"].includes(payment.status) || | |
| !stripe | |
| } | |
| > | |
| Pay Now | |
| </button> | |
| </form> | |
| <PaymentStatus status={payment.status} /> | |
| </> | |
| ); | |
| } | |
| export default function ElementsForm(): JSX.Element { | |
| return ( | |
| <Elements | |
| stripe={getStripe()} | |
| options={{ | |
| appearance: { | |
| variables: { | |
| colorIcon: "#5469d4", | |
| fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif", | |
| }, | |
| }, | |
| currency: config.CURRENCY, | |
| mode: "payment", | |
| }} | |
| > | |
| <CheckoutForm /> | |
| </Elements> | |
| ); | |
| } |
This file contains hidden or 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
| import { Card } from '@/app/ui/dashboard/cards'; | |
| import RevenueChart from '@/app/ui/dashboard/revenue-chart'; | |
| import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; | |
| import { lusitana } from '@/app/ui/fonts'; | |
| export default async function Page() { | |
| return ( | |
| <main> | |
| <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}> | |
| Dashboard | |
| </h1> | |
| https://dev.to/w3tsa/nextjs-14-fetching-data-elm | |
| <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4"> | |
| {/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */} | |
| {/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */} | |
| {/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */} | |
| {/* <Card | |
| title="Total Customers" | |
| value={numberOfCustomers} | |
| type="customers" | |
| /> */} | |
| </div> | |
| <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8"> | |
| {/* <RevenueChart revenue={revenue} /> */} | |
| {/* <LatestInvoices latestInvoices={latestInvoices} /> */} | |
| </div> | |
| </main> | |
| ); | |
| } |
This file contains hidden or 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
| import { Card } from '@/app/ui/dashboard/cards'; | |
| import RevenueChart from '@/app/ui/dashboard/revenue-chart'; | |
| import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; | |
| import { lusitana } from '@/app/ui/fonts'; | |
| export default async function Page() { | |
| return ( | |
| <main> | |
| <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}> | |
| Dashboard | |
| </h1> | |
| https://dev.to/w3tsa/nextjs-14-fetching-data-elm | |
| <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4"> | |
| {/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */} | |
| {/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */} | |
| {/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */} | |
| {/* <Card | |
| title="Total Customers" | |
| value={numberOfCustomers} | |
| type="customers" | |
| /> */} | |
| </div> | |
| <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8"> | |
| {/* <RevenueChart revenue={revenue} /> */} | |
| {/* <LatestInvoices latestInvoices={latestInvoices} /> */} | |
| </div> | |
| </main> | |
| ); | |
| } |
This file contains hidden or 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
| import { Card } from '@/app/ui/dashboard/cards'; | |
| import RevenueChart from '@/app/ui/dashboard/revenue-chart'; | |
| import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; | |
| import { lusitana } from '@/app/ui/fonts'; | |
| export default async function Page() { | |
| return ( | |
| <main> | |
| <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}> | |
| Dashboard | |
| </h1> | |
| https://dev.to/w3tsa/nextjs-14-fetching-data-elm | |
| <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4"> | |
| {/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */} | |
| {/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */} | |
| {/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */} | |
| {/* <Card | |
| title="Total Customers" | |
| value={numberOfCustomers} | |
| type="customers" | |
| /> */} | |
| </div> | |
| <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8"> | |
| {/* <RevenueChart revenue={revenue} /> */} | |
| {/* <LatestInvoices latestInvoices={latestInvoices} /> */} | |
| </div> | |
| </main> | |
| ); | |
| } |
This file contains hidden or 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
| import { Card } from '@/app/ui/dashboard/cards'; | |
| import RevenueChart from '@/app/ui/dashboard/revenue-chart'; | |
| import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; | |
| import { lusitana } from '@/app/ui/fonts'; | |
| export default async function Page() { | |
| return ( | |
| <main> | |
| <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}> | |
| Dashboard | |
| </h1> | |
| https://dev.to/w3tsa/nextjs-14-fetching-data-elm | |
| <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4"> | |
| {/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */} | |
| {/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */} | |
| {/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */} | |
| {/* <Card | |
| title="Total Customers" | |
| value={numberOfCustomers} | |
| type="customers" | |
| /> */} | |
| </div> | |
| <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8"> | |
| {/* <RevenueChart revenue={revenue} /> */} | |
| {/* <LatestInvoices latestInvoices={latestInvoices} /> */} | |
| </div> | |
| </main> | |
| ); | |
| } |
This file contains hidden or 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
| import { Card } from '@/app/ui/dashboard/cards'; | |
| import RevenueChart from '@/app/ui/dashboard/revenue-chart'; | |
| import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; | |
| import { lusitana } from '@/app/ui/fonts'; | |
| export default async function Page() { | |
| return ( | |
| <main> | |
| <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}> | |
| Dashboard | |
| </h1> | |
| https://dev.to/w3tsa/nextjs-14-fetching-data-elm | |
| <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4"> | |
| {/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */} | |
| {/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */} | |
| {/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */} | |
| {/* <Card | |
| title="Total Customers" | |
| value={numberOfCustomers} | |
| type="customers" | |
| /> */} | |
| </div> | |
| <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8"> | |
| {/* <RevenueChart revenue={revenue} /> */} | |
| {/* <LatestInvoices latestInvoices={latestInvoices} /> */} | |
| </div> | |
| </main> | |
| ); | |
| } |
This file contains hidden or 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
| import { Card } from '@/app/ui/dashboard/cards'; | |
| import RevenueChart from '@/app/ui/dashboard/revenue-chart'; | |
| import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; | |
| import { lusitana } from '@/app/ui/fonts'; | |
| export default async function Page() { | |
| return ( | |
| <main> | |
| <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}> | |
| Dashboard | |
| </h1> | |
| https://dev.to/w3tsa/nextjs-14-fetching-data-elm | |
| <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4"> | |
| {/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */} | |
| {/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */} | |
| {/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */} | |
| {/* <Card | |
| title="Total Customers" | |
| value={numberOfCustomers} | |
| type="customers" | |
| /> */} | |
| </div> | |
| <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8"> | |
| {/* <RevenueChart revenue={revenue} /> */} | |
| {/* <LatestInvoices latestInvoices={latestInvoices} /> */} | |
| </div> | |
| </main> | |
| ); | |
| } |
This file contains hidden or 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
| // pages/api/get-product-prices.ts | |
| import { NextApiRequest, NextApiResponse } from 'next'; | |
| import Stripe from 'stripe'; | |
| const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { | |
| apiVersion: '2023-08-16', | |
| }); | |
| export default async function handler( | |
| req: NextApiRequest, | |
| res: NextApiResponse | |
| ) { | |
| try { | |
| const products = await stripe.products.list(); | |
| const prices = await stripe.prices.list(); | |
| const productPrices = products.data.map(product => { | |
| const price = prices.data.find(price => price.product === product.id); | |
| return { | |
| productId: product.id, | |
| priceId: price?.id, | |
| productName: product.name, | |
| unit_amount: price?.unit_amount, | |
| currency: price?.currency, | |
| }; | |
| }); | |
| res.status(200).json(productPrices); | |
| } catch (error) { | |
| console.error(error); | |
| res.status(500).send({ error: 'Failed to fetch product prices' }); | |
| } | |
| } |
This file contains hidden or 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
| "use server"; | |
| import type { Stripe } from "stripe"; | |
| import { headers } from "next/headers"; | |
| import { CURRENCY } from "@/config"; | |
| import { stripe } from "@/lib/stripe"; | |
| //import { calculateOrderAmount } from "../calculateOrderAmount"; | |
| import {ShoppingCartList} from "app/api/users/data"; | |
| import {calculateTotalPrice} from "app/api/users/data"; | |
| import { type ShoppingCart } from "app/api/users/data"; | |
| // const amount = calculateOrderAmount(items); | |
| //console.log(amount); // has const items: CartItem[] = [{}] | |
| // const calculateOrderAmount = (items: CartItem[]): number => { | |
| // return items.reduce( | |
| // (total, item) => total + item.price_data.unit_amount * item.quantity, | |
| // 0 | |
| // ); | |
| // }; | |
| const calculateOrderAmount = (items: ShoppingCart[]): number => { | |
| return calculateTotalPrice(items); | |
| }; | |
| // const calculateOrderAmount = (quantity: number): number => { | |
| // const pricePerItem = 1400; // Adjust this to your actual item price | |
| // return pricePerItem * quantity; | |
| // }; | |
| export async function createCheckoutSession( | |
| data: FormData, | |
| ): Promise<{ client_secret: string | null; url: string | null }> { | |
| const ui_mode = data.get( | |
| "uiMode", | |
| ) as Stripe.Checkout.SessionCreateParams.UiMode; | |
| const quantity = parseInt(data.get("quantity") as string, 10); | |
| //const amount = calculateOrderAmount(quantity); | |
| const amount = calculateOrderAmount(ShoppingCartList); | |
| const origin: string = headers().get("origin") as string; | |
| const checkoutSession: Stripe.Checkout.Session = | |
| await stripe.checkout.sessions.create({ | |
| mode: "payment", | |
| submit_type: "auto", | |
| line_items: [ | |
| { | |
| quantity: 1, | |
| price_data: { | |
| currency: CURRENCY, | |
| product_data: { | |
| name: "Custom amount donation", | |
| }, | |
| unit_amount: amount / quantity, | |
| // unit_amount: parseInt(data.get("amount") as string), | |
| //unit_amount: 1400, | |
| }, | |
| }, | |
| ], | |
| //console.log(amount); | |
| ...(ui_mode === "hosted" && { | |
| success_url: `${origin}/donate-with-checkout/result?session_id={CHECKOUT_SESSION_ID}`, | |
| cancel_url: `${origin}/donate-with-checkout`, | |
| }), | |
| ...(ui_mode === "embedded" && { | |
| return_url: `${origin}/donate-with-embedded-checkout/result?session_id={CHECKOUT_SESSION_ID}`, | |
| }), | |
| ui_mode, | |
| }); | |
| return { | |
| client_secret: checkoutSession.client_secret, | |
| url: checkoutSession.url, | |
| }; | |
| } | |
| // amount: calculateOrderAmount(items), | |
| // amount: parseInt(data.get("amount") as string), | |
| export async function createPaymentIntent( | |
| data: FormData, | |
| ): Promise<{ client_secret: string }> { | |
| const quantity = parseInt(data.get("quantity") as string, 10); | |
| const amount = calculateOrderAmount(quantity); | |
| const paymentIntent: Stripe.PaymentIntent = | |
| await stripe.paymentIntents.create({ | |
| amount, | |
| automatic_payment_methods: { enabled: true }, | |
| currency: CURRENCY, | |
| }); | |
| return { client_secret: paymentIntent.client_secret as string }; | |
| } |
This file contains hidden or 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
| "use server"; | |
| import type { Stripe } from "stripe"; | |
| import { headers } from "next/headers"; | |
| import { CURRENCY } from "@/config"; | |
| import { stripe } from "@/lib/stripe"; | |
| //import { calculateOrderAmount } from "../calculateOrderAmount"; | |
| import {ShoppingCartList} from "app/api/users/data"; | |
| import {calculateTotalPrice} from "app/api/users/data"; | |
| import { type ShoppingCart } from "app/api/users/data"; | |
| // const amount = calculateOrderAmount(items); | |
| //console.log(amount); // has const items: CartItem[] = [{}] | |
| // const calculateOrderAmount = (items: CartItem[]): number => { | |
| // return items.reduce( | |
| // (total, item) => total + item.price_data.unit_amount * item.quantity, | |
| // 0 | |
| // ); | |
| // }; | |
| const calculateOrderAmount = (items: ShoppingCart[]): number => { | |
| return calculateTotalPrice(items); | |
| }; | |
| // const calculateOrderAmount = (quantity: number): number => { | |
| // const pricePerItem = 1400; // Adjust this to your actual item price | |
| // return pricePerItem * quantity; | |
| // }; | |
| export async function createCheckoutSession( | |
| data: FormData, | |
| ): Promise<{ client_secret: string | null; url: string | null }> { | |
| const ui_mode = data.get( | |
| "uiMode", | |
| ) as Stripe.Checkout.SessionCreateParams.UiMode; | |
| const quantity = parseInt(data.get("quantity") as string, 10); | |
| //const amount = calculateOrderAmount(quantity); | |
| const amount = calculateOrderAmount(ShoppingCartList); | |
| const origin: string = headers().get("origin") as string; | |
| const checkoutSession: Stripe.Checkout.Session = | |
| await stripe.checkout.sessions.create({ | |
| mode: "payment", | |
| submit_type: "auto", | |
| // line_items: [ | |
| // { | |
| // quantity: 1, | |
| // price_data: { | |
| // currency: CURRENCY, | |
| // product_data: { | |
| // name: "Custom amount donation", | |
| // }, | |
| // unit_amount: amount / quantity, | |
| // // unit_amount: parseInt(data.get("amount") as string), | |
| // //unit_amount: 1400, | |
| // }, | |
| // }, | |
| // ], | |
| line_items: ShoppingCartList.map(item => ({ | |
| quantity: item.quantity, | |
| price_data: { | |
| currency: CURRENCY, | |
| product_data: { | |
| name: item.name, | |
| }, | |
| unit_amount: item.price * 100, // Ensure unit amount is in cents | |
| }, | |
| })), | |
| ...(ui_mode === "hosted" && { | |
| success_url: `${origin}/donate-with-checkout/result?session_id={CHECKOUT_SESSION_ID}`, | |
| cancel_url: `${origin}/donate-with-checkout`, | |
| }), | |
| ...(ui_mode === "embedded" && { | |
| return_url: `${origin}/donate-with-embedded-checkout/result?session_id={CHECKOUT_SESSION_ID}`, | |
| }), | |
| ui_mode, | |
| }); | |
| return { | |
| client_secret: checkoutSession.client_secret, | |
| url: checkoutSession.url, | |
| }; | |
| } | |
| // amount: calculateOrderAmount(items), | |
| // amount: parseInt(data.get("amount") as string), | |
| export async function createPaymentIntent( | |
| data: FormData, | |
| ): Promise<{ client_secret: string }> { | |
| const quantity = parseInt(data.get("quantity") as string, 10); | |
| //const amount = calculateOrderAmount(quantity); | |
| const amount = calculateOrderAmount(ShoppingCartList); | |
| const paymentIntent: Stripe.PaymentIntent = | |
| await stripe.paymentIntents.create({ | |
| amount, | |
| automatic_payment_methods: { enabled: true }, | |
| currency: CURRENCY, | |
| }); | |
| return { client_secret: paymentIntent.client_secret as string }; | |
| } |
This file contains hidden or 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
| // components/CheckoutForm.tsx | |
| import React, { useState, useEffect } from 'react'; | |
| import { loadStripe } from '@stripe/stripe-js'; | |
| import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; | |
| import axios from 'axios'; | |
| const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string); | |
| type ProductPrice = { | |
| productId: string; | |
| priceId: string; | |
| productName: string; | |
| unit_amount: number; | |
| currency: string; | |
| }; | |
| const CheckoutForm: React.FC = () => { | |
| const [products, setProducts] = useState<ProductPrice[]>([]); | |
| const [selectedProduct, setSelectedProduct] = useState<string | null>(null); | |
| const [input, setInput] = useState<{ cardholderName: string }>({ cardholderName: '' }); | |
| const [clientSecret, setClientSecret] = useState<string | null>(null); | |
| const stripe = useStripe(); | |
| const elements = useElements(); | |
| useEffect(() => { | |
| // Fetch product prices from the backend | |
| const fetchProductPrices = async () => { | |
| try { | |
| const response = await axios.get('/api/get-product-prices'); | |
| setProducts(response.data); | |
| } catch (error) { | |
| console.error('Error fetching product prices:', error); | |
| } | |
| }; | |
| fetchProductPrices(); | |
| }, []); | |
| const handleSubmit = async (event: React.FormEvent) => { | |
| event.preventDefault(); | |
| if (!stripe || !elements || !selectedProduct) { | |
| return; | |
| } | |
| const product = products.find(product => product.productId === selectedProduct); | |
| if (!product) { | |
| console.error('Selected product not found'); | |
| return; | |
| } | |
| const { data } = await axios.post('/api/create-payment-intent', { | |
| items: [{ priceId: product.priceId, quantity: 1 }], | |
| }); | |
| setClientSecret(data.clientSecret); | |
| if (clientSecret) { | |
| const cardElement = elements.getElement(CardElement); | |
| if (cardElement) { | |
| const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, { | |
| payment_method: { | |
| card: cardElement, | |
| billing_details: { | |
| name: input.cardholderName, | |
| }, | |
| }, | |
| }); | |
| if (error) { | |
| console.error(error); | |
| } else if (paymentIntent && paymentIntent.status === 'succeeded') { | |
| console.log('Payment succeeded!'); | |
| } | |
| } | |
| } | |
| }; | |
| return ( | |
| <form onSubmit={handleSubmit}> | |
| <label> | |
| Cardholder Name | |
| <input | |
| type="text" | |
| value={input.cardholderName} | |
| onChange={(e) => setInput({ ...input, cardholderName: e.target.value })} | |
| /> | |
| </label> | |
| <label> | |
| Select Product | |
| <select onChange={(e) => setSelectedProduct(e.target.value)} value={selectedProduct || ''}> | |
| <option value="" disabled> | |
| Select a product | |
| </option> | |
| {products.map((product) => ( | |
| <option key={product.productId} value={product.productId}> | |
| {product.productName} - {product.unit_amount / 100} {product.currency.toUpperCase()} | |
| </option> | |
| ))} | |
| </select> | |
| </label> | |
| <CardElement /> | |
| <button type="submit" disabled={!stripe}> | |
| Pay | |
| </button> | |
| </form> | |
| ); | |
| }; | |
| const WrappedCheckoutForm: React.FC = () => ( | |
| <Elements stripe={stripePromise}> | |
| <CheckoutForm /> | |
| </Elements> | |
| ); | |
| export default WrappedCheckoutForm; |
This file contains hidden or 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
| "use client"; | |
| import type { StripeError } from "@stripe/stripe-js"; | |
| import * as React from "react"; | |
| import { | |
| useStripe, | |
| useElements, | |
| PaymentElement, | |
| Elements, | |
| } from "@stripe/react-stripe-js"; | |
| //import CustomDonationInput from "./CustomDonationInput"; | |
| import StripeTestCards from "./StripeTestCards"; | |
| import { formatAmountForDisplay } from "@/utils/stripe-helpers"; | |
| import * as config from "@/config"; | |
| import getStripe from "@/utils/get-stripejs"; | |
| import { createPaymentIntent } from "@/actions/cart/stripe"; | |
| function CheckoutForm(): JSX.Element { | |
| const [input, setInput] = React.useState<{ | |
| amount: number; | |
| cardholderName: string; | |
| quantity: number; | |
| }>({ | |
| amount: 1400 * 100, | |
| cardholderName: "", | |
| quantity: 1, | |
| }); | |
| const [paymentType, setPaymentType] = React.useState<string>(""); | |
| const [payment, setPayment] = React.useState<{ | |
| status: "initial" | "processing" | "error"; | |
| }>({ status: "initial" }); | |
| const [errorMessage, setErrorMessage] = React.useState<string>(""); | |
| const stripe = useStripe(); | |
| const elements = useElements(); | |
| const PaymentStatus = ({ status }: { status: string }) => { | |
| switch (status) { | |
| case "processing": | |
| case "requires_payment_method": | |
| case "requires_confirmation": | |
| return <h2>Processing...</h2>; | |
| case "requires_action": | |
| return <h2>Authenticating...</h2>; | |
| case "succeeded": | |
| return <h2>Payment Succeeded 🎉</h2>; | |
| case "error": | |
| return ( | |
| <> | |
| <h2>Error ❌</h2> | |
| <p className="error-message">{errorMessage}</p> | |
| </> | |
| ); | |
| default: | |
| return null; | |
| } | |
| }; | |
| const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { | |
| setInput({ | |
| ...input, | |
| [e.currentTarget.name]: e.currentTarget.value, | |
| }); | |
| elements?.update({ amount: 1400 * 100 }); | |
| }; | |
| const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => { | |
| try { | |
| e.preventDefault(); | |
| // Abort if form isn't valid | |
| if (!e.currentTarget.reportValidity()) return; | |
| if (!elements || !stripe) return; | |
| setPayment({ status: "processing" }); | |
| const { error: submitError } = await elements.submit(); | |
| if (submitError) { | |
| setPayment({ status: "error" }); | |
| setErrorMessage(submitError.message ?? "An unknown error occurred"); | |
| return; | |
| } | |
| // Create a PaymentIntent with the specified amount. | |
| // const { client_secret: clientSecret } = await createPaymentIntent( | |
| // new FormData(e.target as HTMLFormElement) | |
| // ); | |
| const formData = new FormData(e.target as HTMLFormElement); | |
| formData.append("amount", (1400 * input.quantity).toString()); | |
| const { client_secret: clientSecret } = await createPaymentIntent(formData); | |
| // Use your card Element with other Stripe.js APIs | |
| //donate-with-elements/result | |
| const { error: confirmError } = await stripe!.confirmPayment({ | |
| elements, | |
| clientSecret, | |
| confirmParams: { | |
| return_url: `${window.location.origin}/front`, | |
| payment_method_data: { | |
| billing_details: { | |
| name: input.cardholderName, | |
| }, | |
| }, | |
| }, | |
| }); | |
| if (confirmError) { | |
| setPayment({ status: "error" }); | |
| setErrorMessage(confirmError.message ?? "An unknown error occurred"); | |
| } | |
| } catch (err) { | |
| const { message } = err as StripeError; | |
| setPayment({ status: "error" }); | |
| setErrorMessage(message ?? "An unknown error occurred"); | |
| } | |
| }; | |
| return ( | |
| <> | |
| <form onSubmit={handleSubmit}> | |
| <StripeTestCards /> | |
| <fieldset className="elements-style"> | |
| <legend>Your payment details:</legend> | |
| {paymentType === "card" ? ( | |
| <input | |
| placeholder="Cardholder name" | |
| className="elements-style" | |
| type="Text" | |
| name="cardholderName" | |
| onChange={handleInputChange} | |
| required | |
| /> | |
| ) : null} | |
| <div className="FormRow elements-style"> | |
| <PaymentElement | |
| onChange={(e) => { | |
| setPaymentType(e.value.type); | |
| }} | |
| /> | |
| </div> | |
| </fieldset> | |
| <label> | |
| Quantity: | |
| <input | |
| type="number" | |
| name="quantity" | |
| className="elements-style" | |
| value={input.quantity} | |
| onChange={handleInputChange} | |
| min="1" | |
| /> | |
| </label> | |
| <button | |
| className="elements-style-background" | |
| type="submit" | |
| disabled={ | |
| !["initial", "succeeded", "error"].includes(payment.status) || | |
| !stripe | |
| } | |
| > | |
| Pay Now | |
| {/* Donate {formatAmountForDisplay(input.customDonation, config.CURRENCY)} */} | |
| </button> | |
| </form> | |
| <PaymentStatus status={payment.status} /> | |
| </> | |
| ); | |
| } | |
| export default function ElementsForm(): JSX.Element { | |
| return ( | |
| <Elements | |
| stripe={getStripe()} | |
| options={{ | |
| appearance: { | |
| variables: { | |
| colorIcon: "#5469d4", | |
| fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif", | |
| }, | |
| }, | |
| currency: config.CURRENCY, | |
| mode: "payment", | |
| amount: 1400 * 100, | |
| }} | |
| > | |
| <CheckoutForm /> | |
| </Elements> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment