Skip to content

Commit

Permalink
[example] Upgrade the with-stripe-typescript example app (vercel#33761)
Browse files Browse the repository at this point in the history
Hi team!
I found in the `with-stripe-typescript` example that I need to update these things.

- `use-shopping-cart` is launched a new major version
- Stripe launched a new useful payment element named Stripe Payment Element

So I've updated the example app to support these updates.

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`

## How to test it

Please check the README.md of the example.
https://github.com/vercel/next.js/blob/canary/examples/with-stripe-typescript/README.md

Thanks!
  • Loading branch information
hideokamoto-stripe authored and natew committed Feb 16, 2022
1 parent 4481a6b commit b0bd456
Show file tree
Hide file tree
Showing 17 changed files with 228 additions and 136 deletions.
7 changes: 3 additions & 4 deletions 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 }) => (
<CartProvider
mode="checkout-session"
stripe={getStripe()}
cartMode="checkout-session"
stripe={process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string}
currency={config.CURRENCY}
>
<>{children}</>
Expand Down
11 changes: 9 additions & 2 deletions examples/with-stripe-typescript/components/CartSummary.tsx
Expand Up @@ -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,
Expand All @@ -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
}

Expand All @@ -40,6 +44,9 @@ const CartSummary = () => {
return (
<form onSubmit={handleCheckout}>
<h2>Cart summary</h2>
{errorMessage ? (
<p style={{ color: 'red' }}>Error: {errorMessage}</p>
) : null}
{/* This is where we'll render our cart */}
<p suppressHydrationWarning>
<strong>Number of Items:</strong> {cartCount}
Expand Down
2 changes: 1 addition & 1 deletion 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()
Expand Down
97 changes: 39 additions & 58 deletions 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()
Expand Down Expand Up @@ -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)

Expand All @@ -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' })
Expand All @@ -134,24 +119,20 @@ const ElementsForm = () => {
<StripeTestCards />
<fieldset className="elements-style">
<legend>Your payment details:</legend>
<input
placeholder="Cardholder name"
className="elements-style"
type="Text"
name="cardholderName"
onChange={handleInputChange}
required
/>
{paymentType === 'card' ? (
<input
placeholder="Cardholder name"
className="elements-style"
type="Text"
name="cardholderName"
onChange={handleInputChange}
required
/>
) : null}
<div className="FormRow elements-style">
<CardElement
options={CARD_OPTIONS}
<PaymentElement
onChange={(e) => {
if (e.error) {
setPayment({ status: 'error' })
setErrorMessage(
e.error.message ?? 'An unknown error occurred'
)
}
setPaymentType(e.value.type)
}}
/>
</div>
Expand Down
14 changes: 9 additions & 5 deletions examples/with-stripe-typescript/components/Products.tsx
@@ -1,13 +1,14 @@
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()

return (
<section className="products">
{products.map((product) => (
<div key={product.sku} className="product">
<div key={product.id} className="product">
<img src={product.image} alt={product.name} />
<h2>{product.name}</h2>
<p className="price">
Expand All @@ -18,13 +19,16 @@ const Products = () => {
</p>
<button
className="cart-style-background"
onClick={() => addItem(product)}
onClick={() => {
console.log(product)
addItem(product)
}}
>
Add to cart
</button>
<button
className="cart-style-background"
onClick={() => removeItem(product.sku)}
onClick={() => removeItem(product.id)}
>
Remove
</button>
Expand Down
19 changes: 0 additions & 19 deletions examples/with-stripe-typescript/data/products.json

This file was deleted.

22 changes: 22 additions & 0 deletions 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
10 changes: 5 additions & 5 deletions examples/with-stripe-typescript/package.json
Expand Up @@ -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"
}
}
Expand Up @@ -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(
Expand All @@ -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 })
}
}
Expand Up @@ -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(
Expand All @@ -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',
Expand All @@ -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')
Expand Down

0 comments on commit b0bd456

Please sign in to comment.