2025.03.08

stripe-payments-in-remix

RemixでStripeを使った決済機能の導入

  • remix
  • stripe
  • react

はじめに

ReactをベースにしたフルスタックフレームワークであるRemixのアーキテクチャを使うことで、開発効率が上がり、Stripeとの統合もスムーズになります。

本記事では、RemixとStripeを活用して決済機能を導入する方法を記録しています。

"@remix-run/dev": "^2.16.0",
"stripe": "^17.7.0",

環境の準備

まず、stripe関連のパッケージをインストールします。

npm i stripe @stripe/stripe-js @stripe/react-stripe-js

これらのパッケージの役割は以下の通りです。

  • stripe:サーバー側から Stripe API を操作するためのライブラリ
  • @stripe/stripe-js:クライアント側で Stripe.js を読み込むためのライブラリ
  • @stripe/react-stripe-js:React で Stripe を統合するためのコンポーネントやフックを提供

今回、ディレクトリ構造は次のようにしました。

./
├─ app/
│   ├─ components/
│   │   └─ stripe/
│   │        └─ payment_form.tsx
│   ├─ routes/
│   │   ├─ payment.tsx
│   │   └─ purchased.tsx
│   └─ utils/
│        └─ stripe/
│             ├─ client.ts
│             └─ server.ts
├─ .env

環境変数の設定

Stripe のシークレットキーを Stripe のダッシュボードで 取得し、 .env ファイルに設定します。

STRIPE_SECRET_KEY=sk_test...

Stripe の設定

サーバー側、クライアント側とそれぞれの処理を共通化します。

import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2025-02-24.acacia",
  typescript: true,
  httpClient: Stripe.createFetchHttpClient(),
});

export async function createPaymentIntent(amount: number) {
  return await stripe.paymentIntents.create({
    amount,
    currency: "jpy",
    automatic_payment_methods: { enabled: true },
  });
}

Stripe の公開可能キーを設定し、クライアントを初期化。

import { loadStripe } from "@stripe/stripe-js";

export const stripePromise = loadStripe("pk_test...");

決済フォームの作成

ユーザーがクレジットカード情報を入力する決済フォームです。後々カスタマイズしやすいように、ElementsをNumber, Expiry, Cvcと個別で読み込んでいます。

import { useStripe, useElements, CardNumberElement, CardExpiryElement, CardCvcElement } from "@stripe/react-stripe-js";
import { Stripe, StripeElements } from "@stripe/stripe-js";
import { FormEvent } from "react";

export default function PaymentForm({
  onSubmit,
}: {
  onSubmit: (e: FormEvent<HTMLFormElement>, stripe: Stripe | null, elements: StripeElements | null) => void;
}) {

  const stripe = useStripe();
  const elements = useElements();

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    if (!stripe || !elements) {
      console.error("Stripe.js not loaded") ;
      return;
    }
    onSubmit(e, stripe, elements);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="card-number">Card Number</label>
        <CardNumberElement id="card-number" />
      </div>
      <div>
        <label htmlFor="card-expiry">Expiry Date</label>
        <CardExpiryElement id="card-expiry" />
      </div>
      <div>
        <label htmlFor="card-cvc">CVC</label>
        <CardCvcElement id="card-cvc" />
      </div>
      <button>Complete Payment</button>
    </form>
  );
}

フォームの送信処理

クライアント側でStripe に決済情報を送信する処理を追加します。以下では決済が成功した時は遷移だけ行なっていますが、サーバー側の処理は action にデータを渡していくことになります。

また、/purchasedページの実装は割愛しています。

import { json, LoaderFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { Elements, CardNumberElement } from "@stripe/react-stripe-js";
import { Stripe, StripeElements } from "@stripe/stripe-js";
import { useEffect, useState, FormEvent } from "react";
import { useNavigate } from "react-router-dom";

import PaymentForm from "~/components/stripe/payment_form";
import { stripePromise } from "~/utils/stripe/client";
import { createPaymentIntent } from "~/utils/stripe/server";

const amount = 1000;//金額の設定

export const loader: LoaderFunction = async () => {
  const paymentIntent = await createPaymentIntent(amount);
  return json({ clientSecret: paymentIntent.client_secret });
};

export default function PaymentPage() {
  const { clientSecret } = useLoaderData<{ clientSecret: string | null }>();
  const [clientSecretState, setClientSecretState] = useState<string | null>(null);
  const navigate = useNavigate();

  useEffect(() => {
    setClientSecretState(clientSecret);
  }, [clientSecret]);

  const handleSubmit = async (e: FormEvent<HTMLFormElement>, stripe: Stripe | null, elements: StripeElements | null) => {
    e.preventDefault();
    if (!stripe || !elements) return console.error("Stripe.js not loaded");

    const cardNumberElement = elements.getElement(CardNumberElement);
    if (!cardNumberElement) return console.error("Card element not found");

    try {
      const { error, paymentMethod } = await stripe.createPaymentMethod({
        type: "card",
        card: cardNumberElement,
      });
      if (error) return console.error("PaymentMethod error:", error);
        const { error: confirmError } = await stripe.confirmCardPayment(clientSecretState!, {
        payment_method: paymentMethod.id,
      });
      if (confirmError) {
        console.error("Payment failed", confirmError);
      } else {
        navigate("/purchased");
      }
    } catch (error) {
      console.error("Error:", error);
    }
  };

  return (
    <>
      <p>Amount: {amount}</p>
      {clientSecretState ?
        <Elements stripe={stripePromise} options={{ clientSecret: clientSecretState }}>
          <PaymentForm onSubmit={handleSubmit} />
        </Elements>
      : <p>Loading...</p>}
    </>
  );
}

以上

これで、Remix と Stripe を使った基本的な決済処理が行えたかと思います。Remix の強力な機能を活用することで、Stripe との統合をスムーズに行うことができました。少しでも参考になれば幸いです!