Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[example] Upgrade the with-stripe-typescript example app #33761

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be hard coded to localhost?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a URL of the redirect destination.
So if we can set the current page url, we don't have to hard code it.
eg.)

return_url: window && window.location ? [location.protocol, location.hostname,'donate-with-elements'].join('/') ; 'http://localhost:3000/donate-with-elements'

But, it this example works well in SSG/SSR mode?
That's my worrying.

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