Open2

Stripe SDK (React)小ネタ集

Hidetaka Okamoto(Stripe)Hidetaka Okamoto(Stripe)

Use Render Hook

目的

  • Payment Intent取得とStripe Elementのセットアップ系をカスタムフックに押し込める。
  • 親(決済フォームを表示させる操作をする画面)に、Stripe系の実装を含めずに済む

参考記事

https://engineering.linecorp.com/ja/blog/line-securities-frontend-3/

use-stripe-elements.tsx

Elementの見た目や初期表示(fallback)の設定など、Payment Elementsを使うために親でやらないといけないことを一通りまとめたHook。

import { ComponentProps, FC, ReactNode, useCallback, useState } from "react";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";

export const stripePromise = loadStripe(
    process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY as string,
  );

const useStripeElement = ({children, fallback, elementsOptions = {appearance: {
    theme: "flat",
  }}}: {
      children: ReactNode;
    fallback?: ReactNode;
    elementsOptions?: Omit<ComponentProps<typeof Elements>['options'], 'clientSecret'>
}) => {
    const [clientSecret, setClientSecret] = useState('')
    const createPaymentIntent = async () => {
        const response = await fetch('/api/payment_intent', {
            method: "POST",
            headers: {
                'content-type': 'application/json'
            },
            body: JSON.stringify({
                amount: 100
            })
        })
        const data = await response.json()
        setClientSecret(data.client_secret)
    }
    const renderPaymentElement = useCallback(():JSX.Element | null => {
        if (!clientSecret) {
            if (fallback) return <>{fallback}</>
            return null;
        }
        return (
            <Elements
              stripe={stripePromise}
              options={{
                  ...elementsOptions,
                clientSecret: clientSecret,
              }}
            >
              {children}
            </Elements>
        )
    },[clientSecret,children, fallback])
    return {
        createPaymentIntent,
        renderPaymentElement
    }
}

paymentForm.tsx

Payment Element本体。
useStripe, useElementsを使っているので、これは別にしておいた方がよさそう。

import {  FC } from "react";
import { PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";

const PaymentForm: FC = () => {
    const stripe = useStripe()
    const elements = useElements()
    return (
        <form onSubmit={async (e) => {
            e.preventDefault()
            if (!stripe || !elements) return ;
            stripe.confirmPayment({
                elements,
                confirmParams: {
                    return_url: 'https://example.com/success'
                }
            })
        }}>
        <PaymentElement />
        <button type="submit">process order</button>
        </form>
    )
}

cart.tsx

呼び出し元、いわゆる親要素。
PaymentFormを呼んでいる以外は基本的に見た目にしかフォーカスしていない。

const App:FC = () => {
    const { createPaymentIntent, renderPaymentElement } = useStripeElement({
        children: <PaymentForm />
    })
    return (
        <div>
            <button onClick={() => createPaymentIntent()}>Order</button>
            {renderPaymentElement()}
        </div>
    )
}
Hidetaka Okamoto(Stripe)Hidetaka Okamoto(Stripe)

@stripe/react-stripe-jsのElementsの引数の型を取得する

Elementsタグの型定義

type Elements = FunctionComponent<PropsWithChildren<ElementsProps>>

ただしElementsPropsがexportされていない。

optionsの型だけを取りたいなどの場合は、こう書く。

import { ComponentProps} from "react";
import { Elements } from "@stripe/react-stripe-js";

type ElementsOptions = ComponentProps<typeof Elements>['options']
const elementOptions: ElementsOptions = {
        appearance: {
            theme: 'flat'
        }
    }