diff --git a/examples/with-stripe-typescript/components/Cart.tsx b/examples/with-stripe-typescript/components/Cart.tsx index eaf72baa81a50dc..2aa5893d127b1ec 100644 --- a/examples/with-stripe-typescript/components/Cart.tsx +++ b/examples/with-stripe-typescript/components/Cart.tsx @@ -1,12 +1,11 @@ import React, { ReactNode } from 'react' -import { CartProvider } from 'use-shopping-cart' -import getStripe from '../utils/get-stripejs' +import { CartProvider } from 'use-shopping-cart/react' import * as config from '../config' const Cart = ({ children }: { children: ReactNode }) => ( <>{children} diff --git a/examples/with-stripe-typescript/components/CartSummary.tsx b/examples/with-stripe-typescript/components/CartSummary.tsx index a15035d849c4c17..e7055e8c0c4b2a1 100644 --- a/examples/with-stripe-typescript/components/CartSummary.tsx +++ b/examples/with-stripe-typescript/components/CartSummary.tsx @@ -2,12 +2,13 @@ import React, { useState, useEffect } from 'react' import StripeTestCards from '../components/StripeTestCards' -import { useShoppingCart } from 'use-shopping-cart' +import { useShoppingCart } from 'use-shopping-cart/react' import { fetchPostJSON } from '../utils/api-helpers' const CartSummary = () => { const [loading, setLoading] = useState(false) const [cartEmpty, setCartEmpty] = useState(true) + const [errorMessage, setErrorMessage] = useState('') const { formattedTotalPrice, cartCount, @@ -23,14 +24,17 @@ const CartSummary = () => { ) => { event.preventDefault() setLoading(true) + setErrorMessage('') const response = await fetchPostJSON( '/api/checkout_sessions/cart', cartDetails ) - if (response.statusCode === 500) { + if (response.statusCode > 399) { console.error(response.message) + setErrorMessage(response.message) + setLoading(false) return } @@ -40,6 +44,9 @@ const CartSummary = () => { return (

Cart summary

+ {errorMessage ? ( +

Error: {errorMessage}

+ ) : null} {/* This is where we'll render our cart */}

Number of Items: {cartCount} diff --git a/examples/with-stripe-typescript/components/ClearCart.tsx b/examples/with-stripe-typescript/components/ClearCart.tsx index daf923cf35233a9..49aaf8b7ae393ee 100644 --- a/examples/with-stripe-typescript/components/ClearCart.tsx +++ b/examples/with-stripe-typescript/components/ClearCart.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { useShoppingCart } from 'use-shopping-cart' +import { useShoppingCart } from 'use-shopping-cart/react' export default function ClearCart() { const { clearCart } = useShoppingCart() diff --git a/examples/with-stripe-typescript/components/ElementsForm.tsx b/examples/with-stripe-typescript/components/ElementsForm.tsx index d438cc830f321ae..ca78b81d90fc600 100644 --- a/examples/with-stripe-typescript/components/ElementsForm.tsx +++ b/examples/with-stripe-typescript/components/ElementsForm.tsx @@ -1,44 +1,30 @@ -import React, { useState } from 'react' +import React, { useState, FC } from 'react' import CustomDonationInput from '../components/CustomDonationInput' import StripeTestCards from '../components/StripeTestCards' import PrintObject from '../components/PrintObject' import { fetchPostJSON } from '../utils/api-helpers' -import { formatAmountForDisplay } from '../utils/stripe-helpers' +import { + formatAmountForDisplay, + formatAmountFromStripe, +} from '../utils/stripe-helpers' import * as config from '../config' -import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js' - -const CARD_OPTIONS = { - iconStyle: 'solid' as const, - style: { - base: { - iconColor: '#6772e5', - color: '#6772e5', - fontWeight: '500', - fontFamily: 'Roboto, Open Sans, Segoe UI, sans-serif', - fontSize: '16px', - fontSmoothing: 'antialiased', - ':-webkit-autofill': { - color: '#fce883', - }, - '::placeholder': { - color: '#6772e5', - }, - }, - invalid: { - iconColor: '#ef2961', - color: '#ef2961', - }, - }, -} +import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js' +import { PaymentIntent } from '@stripe/stripe-js' -const ElementsForm = () => { +const ElementsForm: FC<{ + paymentIntent?: PaymentIntent | null +}> = ({ paymentIntent = null }) => { + const defaultAmout = paymentIntent + ? formatAmountFromStripe(paymentIntent.amount, paymentIntent.currency) + : Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP) const [input, setInput] = useState({ - customDonation: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP), + customDonation: defaultAmout, cardholderName: '', }) + const [paymentType, setPaymentType] = useState('') const [payment, setPayment] = useState({ status: 'initial' }) const [errorMessage, setErrorMessage] = useState('') const stripe = useStripe() @@ -80,11 +66,13 @@ const ElementsForm = () => { e.preventDefault() // Abort if form isn't valid if (!e.currentTarget.reportValidity()) return + if (!elements) return setPayment({ status: 'processing' }) // Create a PaymentIntent with the specified amount. const response = await fetchPostJSON('/api/payment_intents', { amount: input.customDonation, + payment_intent_id: paymentIntent?.id, }) setPayment(response) @@ -94,21 +82,18 @@ const ElementsForm = () => { return } - // Get a reference to a mounted CardElement. Elements knows how - // to find your CardElement because there can only ever be one of - // each type of element. - const cardElement = elements!.getElement(CardElement) - // Use your card Element with other Stripe.js APIs - const { error, paymentIntent } = await stripe!.confirmCardPayment( - response.client_secret, - { - payment_method: { - card: cardElement!, - billing_details: { name: input.cardholderName }, + const { error } = await stripe!.confirmPayment({ + elements, + confirmParams: { + return_url: 'http://localhost:3000/donate-with-elements', + payment_method_data: { + billing_details: { + name: input.cardholderName, + }, }, - } - ) + }, + }) if (error) { setPayment({ status: 'error' }) @@ -134,24 +119,20 @@ const ElementsForm = () => {

Your payment details: - + {paymentType === 'card' ? ( + + ) : null}
- { - if (e.error) { - setPayment({ status: 'error' }) - setErrorMessage( - e.error.message ?? 'An unknown error occurred' - ) - } + setPaymentType(e.value.type) }} />
diff --git a/examples/with-stripe-typescript/components/Products.tsx b/examples/with-stripe-typescript/components/Products.tsx index 4b5a6593fe69baf..db8560ecda48e73 100644 --- a/examples/with-stripe-typescript/components/Products.tsx +++ b/examples/with-stripe-typescript/components/Products.tsx @@ -1,5 +1,6 @@ -import products from '../data/products.json' -import { useShoppingCart, formatCurrencyString } from 'use-shopping-cart' +import products from '../data/products' +import { formatCurrencyString } from 'use-shopping-cart' +import { useShoppingCart } from 'use-shopping-cart/react' const Products = () => { const { addItem, removeItem } = useShoppingCart() @@ -7,7 +8,7 @@ const Products = () => { return (
{products.map((product) => ( -
+
{product.name}

{product.name}

@@ -18,13 +19,16 @@ const Products = () => {

diff --git a/examples/with-stripe-typescript/data/products.json b/examples/with-stripe-typescript/data/products.json deleted file mode 100644 index 46ea0cfbf2a708d..000000000000000 --- a/examples/with-stripe-typescript/data/products.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "name": "Bananas", - "description": "Yummy yellow fruit", - "sku": "sku_GBJ2Ep8246qeeT", - "price": 400, - "image": "https://images.unsplash.com/photo-1574226516831-e1dff420e562?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=225&q=80", - "attribution": "Photo by Priscilla Du Preez on Unsplash", - "currency": "USD" - }, - { - "name": "Tangerines", - "sku": "sku_GBJ2WWfMaGNC2Z", - "price": 100, - "image": "https://images.unsplash.com/photo-1482012792084-a0c3725f289f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=225&q=80", - "attribution": "Photo by Jonathan Pielmayer on Unsplash", - "currency": "USD" - } -] diff --git a/examples/with-stripe-typescript/data/products.ts b/examples/with-stripe-typescript/data/products.ts new file mode 100644 index 000000000000000..79547318194ff59 --- /dev/null +++ b/examples/with-stripe-typescript/data/products.ts @@ -0,0 +1,22 @@ +const product = [ + { + name: 'Bananas', + description: 'Yummy yellow fruit', + id: 'sku_GBJ2Ep8246qeeT', + price: 400, + image: + 'https://images.unsplash.com/photo-1574226516831-e1dff420e562?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=225&q=80', + attribution: 'Photo by Priscilla Du Preez on Unsplash', + currency: 'USD', + }, + { + name: 'Tangerines', + id: 'sku_GBJ2WWfMaGNC2Z', + price: 100, + image: + 'https://images.unsplash.com/photo-1482012792084-a0c3725f289f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=225&q=80', + attribution: 'Photo by Jonathan Pielmayer on Unsplash', + currency: 'USD', + }, +] +export default product diff --git a/examples/with-stripe-typescript/package.json b/examples/with-stripe-typescript/package.json index 77b1879472a8fec..38da16d8ff6f9f3 100644 --- a/examples/with-stripe-typescript/package.json +++ b/examples/with-stripe-typescript/package.json @@ -6,22 +6,22 @@ "start": "next start" }, "dependencies": { - "@stripe/react-stripe-js": "1.1.2", - "@stripe/stripe-js": "1.5.0", + "@stripe/react-stripe-js": "1.7.0", + "@stripe/stripe-js": "1.22.0", "micro": "^9.3.4", "micro-cors": "^0.1.1", "next": "latest", "react": "^17.0.2", "react-dom": "^17.0.2", - "stripe": "8.56.0", + "stripe": "8.200.0", "swr": "^0.1.16", - "use-shopping-cart": "2.1.0" + "use-shopping-cart": "3.0.5" }, "devDependencies": { "@types/micro": "^7.3.3", "@types/micro-cors": "^0.1.0", "@types/node": "^13.1.2", "@types/react": "^16.9.17", - "typescript": "^3.7.4" + "typescript": "4.5.5" } } diff --git a/examples/with-stripe-typescript/pages/api/checkout_sessions/[id].ts b/examples/with-stripe-typescript/pages/api/checkout_sessions/[id].ts index bf4fe8c0032e2e8..7dee01bda1b02d6 100644 --- a/examples/with-stripe-typescript/pages/api/checkout_sessions/[id].ts +++ b/examples/with-stripe-typescript/pages/api/checkout_sessions/[id].ts @@ -3,7 +3,7 @@ import { NextApiRequest, NextApiResponse } from 'next' import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { // https://github.com/stripe/stripe-node#configuration - apiVersion: '2020-03-02', + apiVersion: '2020-08-27', }) export default async function handler( @@ -22,6 +22,8 @@ export default async function handler( res.status(200).json(checkout_session) } catch (err) { - res.status(500).json({ statusCode: 500, message: err.message }) + const errorMessage = + err instanceof Error ? err.message : 'Internal server error' + res.status(500).json({ statusCode: 500, message: errorMessage }) } } diff --git a/examples/with-stripe-typescript/pages/api/checkout_sessions/cart.ts b/examples/with-stripe-typescript/pages/api/checkout_sessions/cart.ts index f9ea247811366e6..2ad18d6993601eb 100644 --- a/examples/with-stripe-typescript/pages/api/checkout_sessions/cart.ts +++ b/examples/with-stripe-typescript/pages/api/checkout_sessions/cart.ts @@ -8,13 +8,13 @@ import { NextApiRequest, NextApiResponse } from 'next' * The important thing is that the product info is loaded from somewhere trusted * so you know the pricing information is accurate. */ -import { validateCartItems } from 'use-shopping-cart/src/serverUtil' -import inventory from '../../../data/products.json' +import { validateCartItems } from 'use-shopping-cart/utilities/serverless' +import inventory from '../../../data/products' import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { // https://github.com/stripe/stripe-node#configuration - apiVersion: '2020-03-02', + apiVersion: '2020-08-27', }) export default async function handler( @@ -24,8 +24,10 @@ export default async function handler( if (req.method === 'POST') { try { // Validate the cart details that were sent from the client. - const cartItems = req.body - const line_items = validateCartItems(inventory, cartItems) + const line_items = validateCartItems(inventory as any, req.body) + const hasSubscription = line_items.find((item) => { + return !!item.price_data.recurring + }) // Create Checkout Sessions from body params. const params: Stripe.Checkout.SessionCreateParams = { submit_type: 'pay', @@ -37,13 +39,18 @@ export default async function handler( line_items, success_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${req.headers.origin}/use-shopping-cart`, + mode: hasSubscription ? 'subscription' : 'payment', } + const checkoutSession: Stripe.Checkout.Session = await stripe.checkout.sessions.create(params) res.status(200).json(checkoutSession) } catch (err) { - res.status(500).json({ statusCode: 500, message: err.message }) + console.log(err) + const errorMessage = + err instanceof Error ? err.message : 'Internal server error' + res.status(500).json({ statusCode: 500, message: errorMessage }) } } else { res.setHeader('Allow', 'POST') diff --git a/examples/with-stripe-typescript/pages/api/checkout_sessions/index.ts b/examples/with-stripe-typescript/pages/api/checkout_sessions/index.ts index 1f33a7cadd7d52b..bb1707fdbdd4336 100644 --- a/examples/with-stripe-typescript/pages/api/checkout_sessions/index.ts +++ b/examples/with-stripe-typescript/pages/api/checkout_sessions/index.ts @@ -6,7 +6,7 @@ import { formatAmountForStripe } from '../../../utils/stripe-helpers' import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { // https://github.com/stripe/stripe-node#configuration - apiVersion: '2020-03-02', + apiVersion: '2020-08-27', }) export default async function handler( @@ -40,7 +40,9 @@ export default async function handler( res.status(200).json(checkoutSession) } catch (err) { - res.status(500).json({ statusCode: 500, message: err.message }) + const errorMessage = + err instanceof Error ? err.message : 'Internal server error' + res.status(500).json({ statusCode: 500, message: errorMessage }) } } else { res.setHeader('Allow', 'POST') diff --git a/examples/with-stripe-typescript/pages/api/payment_intents/index.ts b/examples/with-stripe-typescript/pages/api/payment_intents/index.ts index f7a73fb22137c72..0ef19676de9420f 100644 --- a/examples/with-stripe-typescript/pages/api/payment_intents/index.ts +++ b/examples/with-stripe-typescript/pages/api/payment_intents/index.ts @@ -6,36 +6,69 @@ import { formatAmountForStripe } from '../../../utils/stripe-helpers' import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { // https://github.com/stripe/stripe-node#configuration - apiVersion: '2020-03-02', + apiVersion: '2020-08-27', }) export default async function handler( req: NextApiRequest, res: NextApiResponse ) { - if (req.method === 'POST') { - const { amount }: { amount: number } = req.body + if (req.method !== 'POST') { + res.setHeader('Allow', 'POST') + res.status(405).end('Method Not Allowed') + return + } + const { + amount, + payment_intent_id, + }: { amount: number; payment_intent_id?: string } = req.body + // Validate the amount that was passed from the client. + if (!(amount >= MIN_AMOUNT && amount <= MAX_AMOUNT)) { + res.status(500).json({ statusCode: 400, message: 'Invalid amount.' }) + return + } + if (payment_intent_id) { try { - // Validate the amount that was passed from the client. - if (!(amount >= MIN_AMOUNT && amount <= MAX_AMOUNT)) { - throw new Error('Invalid amount.') + const current_intent = await stripe.paymentIntents.retrieve( + payment_intent_id + ) + // If PaymentIntent has been created, just update the amount. + if (current_intent) { + const updated_intent = await stripe.paymentIntents.update( + payment_intent_id, + { + amount: formatAmountForStripe(amount, CURRENCY), + } + ) + res.status(200).json(updated_intent) + return } - // Create PaymentIntent from body params. - const params: Stripe.PaymentIntentCreateParams = { - payment_method_types: ['card'], - amount: formatAmountForStripe(amount, CURRENCY), - currency: CURRENCY, - description: process.env.STRIPE_PAYMENT_DESCRIPTION ?? '', + } catch (e) { + if ((e as any).code !== 'resource_missing') { + const errorMessage = + e instanceof Error ? e.message : 'Internal server error' + res.status(500).json({ statusCode: 500, message: errorMessage }) + return } - const payment_intent: Stripe.PaymentIntent = - await stripe.paymentIntents.create(params) - - res.status(200).json(payment_intent) - } catch (err) { - res.status(500).json({ statusCode: 500, message: err.message }) } - } else { - res.setHeader('Allow', 'POST') - res.status(405).end('Method Not Allowed') + } + try { + // Create PaymentIntent from body params. + const params: Stripe.PaymentIntentCreateParams = { + amount: formatAmountForStripe(amount, CURRENCY), + currency: CURRENCY, + description: process.env.STRIPE_PAYMENT_DESCRIPTION ?? '', + automatic_payment_methods: { + enabled: true, + }, + } + const payment_intent: Stripe.PaymentIntent = + await stripe.paymentIntents.create(params) + + res.status(200).json(payment_intent) + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Internal server error' + res.status(500).json({ statusCode: 500, message: errorMessage }) } } diff --git a/examples/with-stripe-typescript/pages/api/webhooks/index.ts b/examples/with-stripe-typescript/pages/api/webhooks/index.ts index 82bb44c5bddf2cf..72043581b2e3796 100644 --- a/examples/with-stripe-typescript/pages/api/webhooks/index.ts +++ b/examples/with-stripe-typescript/pages/api/webhooks/index.ts @@ -5,7 +5,7 @@ import { NextApiRequest, NextApiResponse } from 'next' import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { // https://github.com/stripe/stripe-node#configuration - apiVersion: '2020-03-02', + apiVersion: '2020-08-27', }) const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET! @@ -31,9 +31,11 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { try { event = stripe.webhooks.constructEvent(buf.toString(), sig, webhookSecret) } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error' // On error, log and return the error message. - console.log(`❌ Error message: ${err.message}`) - res.status(400).send(`Webhook Error: ${err.message}`) + if (err! instanceof Error) console.log(err) + console.log(`❌ Error message: ${errorMessage}`) + res.status(400).send(`Webhook Error: ${errorMessage}`) return } diff --git a/examples/with-stripe-typescript/pages/donate-with-elements.tsx b/examples/with-stripe-typescript/pages/donate-with-elements.tsx index 4488b5872916a62..a1b9ac93789d3b8 100644 --- a/examples/with-stripe-typescript/pages/donate-with-elements.tsx +++ b/examples/with-stripe-typescript/pages/donate-with-elements.tsx @@ -1,20 +1,45 @@ import { NextPage } from 'next' - +import { useState, useEffect } from 'react' import { Elements } from '@stripe/react-stripe-js' +import { PaymentIntent } from '@stripe/stripe-js' import getStripe from '../utils/get-stripejs' - +import { fetchPostJSON } from '../utils/api-helpers' import Layout from '../components/Layout' +import * as config from '../config' import ElementsForm from '../components/ElementsForm' const DonatePage: NextPage = () => { + const [paymentIntent, setPaymentIntent] = useState(null) + useEffect(() => { + fetchPostJSON('/api/payment_intents', { + amount: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP), + }).then((data) => { + setPaymentIntent(data) + }) + }, [setPaymentIntent]) return (

Donate with Elements

Donate to our project 💖

- - - + {paymentIntent && paymentIntent.client_secret ? ( + + + + ) : ( +

Loading...

+ )}
) diff --git a/examples/with-stripe-typescript/tsconfig.json b/examples/with-stripe-typescript/tsconfig.json index 193d144c0987ac7..ae0ccebaf86de11 100644 --- a/examples/with-stripe-typescript/tsconfig.json +++ b/examples/with-stripe-typescript/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ES2019", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, @@ -11,7 +11,9 @@ "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, "isolatedModules": true, + "incremental": true, "jsx": "preserve" }, "exclude": ["node_modules"], diff --git a/examples/with-stripe-typescript/utils/api-helpers.ts b/examples/with-stripe-typescript/utils/api-helpers.ts index 6d6f16780b709e9..c84b00c0c1e2eeb 100644 --- a/examples/with-stripe-typescript/utils/api-helpers.ts +++ b/examples/with-stripe-typescript/utils/api-helpers.ts @@ -3,7 +3,10 @@ export async function fetchGetJSON(url: string) { const data = await fetch(url).then((res) => res.json()) return data } catch (err) { - throw new Error(err.message) + if (err instanceof Error) { + throw new Error(err.message) + } + throw err } } @@ -25,6 +28,9 @@ export async function fetchPostJSON(url: string, data?: {}) { }) return await response.json() // parses JSON response into native JavaScript objects } catch (err) { - throw new Error(err.message) + if (err instanceof Error) { + throw new Error(err.message) + } + throw err } } diff --git a/examples/with-stripe-typescript/utils/stripe-helpers.ts b/examples/with-stripe-typescript/utils/stripe-helpers.ts index 06c46e6974be070..ea8ded80dc9b19c 100644 --- a/examples/with-stripe-typescript/utils/stripe-helpers.ts +++ b/examples/with-stripe-typescript/utils/stripe-helpers.ts @@ -28,3 +28,22 @@ export function formatAmountForStripe( } return zeroDecimalCurrency ? amount : Math.round(amount * 100) } + +export function formatAmountFromStripe( + amount: number, + currency: string +): number { + let numberFormat = new Intl.NumberFormat(['en-US'], { + style: 'currency', + currency: currency, + currencyDisplay: 'symbol', + }) + const parts = numberFormat.formatToParts(amount) + let zeroDecimalCurrency: boolean = true + for (let part of parts) { + if (part.type === 'decimal') { + zeroDecimalCurrency = false + } + } + return zeroDecimalCurrency ? amount : Math.round(amount / 100) +}