Next.js × Squareで決済を実装:Square Web Payments SDKの導入ガイド

2024/07/25に公開

はじめに

本記事では、Squareを活用した決済処理の実装について紹介します。Squareは、オンライン・オフラインの決済手段を提供するサービスで、開発者向けのAPIも充実しており、柔軟なカスタマイズが可能です。特に、クレジットカード情報をアプリケーション側で保持せずに安全に決済を行える点が魅力です。

今回は、Next.jsを用いた決済システムの構築を例に、Squareの導入方法、データベース設計、処理フロー、具体的な実装について詳しく解説します。Squareを使った決済処理の導入を検討している方や、実装の参考にしたい方に役立つ内容となっています。

実装イメージ

購入ボタンをクリックするとSquareの決済画面に遷移します。

必要な情報を入力し「支払い」処理をすると、支払いが完了するというものになります。

クレジットカード番号の入力などは全てSquare側が提供している画面になっており、アプリケーションではカード番号などの情報は保持していません。

Image from Gyazo

Squareとは?(ウェブ開発者向け)

Square は、オンライン・オフラインの決済処理を簡単に導入できる決済サービスです。POSシステムやEC決済、定期課金、APIを提供しており、開発者は柔軟にカスタマイズできます。

開発者視点での特徴

1️.多様な決済手段に対応

  • クレジットカード(Visa, Mastercard, Amex, JCB など)
  • デビットカード
  • モバイル決済(Apple Pay, Google Pay)
  • 電子請求書・定期課金

2.RESTful API が充実

  • Payments API: クレカ決済・モバイル決済を処理
  • Orders API: 注文管理が可能
  • Invoices API: 電子請求書の発行・管理
  • Subscriptions API: 定期課金のセットアップが可能
  • Customers API: 顧客データを一元管理
    👉 メリット: シンプルな REST API なので、バックエンド・フロントエンドどちらからも扱いやすい

3.サーバーレス・クラウド環境でも動作

  • AWS Lambda や Firebase Functions でも利用可能
  • Webhook で支払い完了をリアルタイム処理
    👉 メリット: バックエンドの管理コストを削減できる

メリット: バックエンドの管理コストを削減できる

3.PCI DSS に準拠

  • カード情報の保存・処理を Square 側で管理
  • セキュリティ対策(トークン化、暗号化)が組み込み済み
    👉 メリット: PCI DSS 対応の手間を削減できる

Square公式のNext.js連携ブログ

実はSquare公式ブログでNext.js(App Router)での実装例がブログとして紹介されています。

https://developer.squareup.com/blog/accept-payments-with-square-using-next-js-app-router/

実装当初はこちらを使った方が早いと思ったのですが、若干の問題点が見つかり諦めました。

こちらの実装例ではNext.jsのクライアント画面でクレジットカードの入力を促すよう設定しています。

入力自体はコンポーネントやバリデーションを含んだライブラリを使っています。

https://github.com/weareseeed/react-square-web-payments-sdk

これはこれでコンポーネントを使えば楽なのですが、

  • クレジットカードを記憶して2回目以降も使いたい
  • クレジットカード以外の決済をしたい

などの要件が控えていたこともあり見送りました。

素早くクレジットカード決済のみ実装したいのであれば上記の手段でも可能ですが、今回の要件ではマッチせず諦めました。

システム構成

Next.jsでフロントの画面を提供しており、データの保存先としてMySQL、決済処理としてSquareを使用しています。

基本的にはNext.jsのサーバーサイドでこれらのバックエンドサービスと連携しています。

決済処理についてはクレジットカードの入力画面をSquareが提供しているため、Squareからユーザーへ矢印が伸びています。

Image from Gyazo

ER図

DBのER図になります。
実際のサービスでは数多くのテーブルを使用していますが、今回必要なのは下記の3テーブルとなります。

  • Course: ユーザーが購入するコンテンツみたいなもの
  • Purchase: ユーザーの購入履歴
  • SquareCustomer: サービスが管理しているuserIdとSquareで管理するカスタマーIDを紐付けるテーブル

Image from Gyazo

SquareCustomerについてはどのテーブルともリレーションがありません。

Squareにアクセスし必要な情報を取得しサービスと連携して何か情報提供する際にuserIdを変換するのみの役割となります。

処理フロー

詳細な実装の前に決済の開始から終了までの処理フローをまとめました。

  1. 購入ボタンのクリック

  2. Next.jsのサーバーサイド(今回はAPIを使用)でリクエストを受け取り
    リクエスト内容を受け取りバリデーションなどを必要に行う。

  3. リクエストのあったユーザーについてSquareのカスタマーIDがあるかDBに問い合わせ
    カスタマーIDが存在しない場合は、Squareに問い合わせて新規にカスタマーIDを発行し、DBに登録(SquareCustomerテーブル)

  4. Next.jsのサーバーサイドからSquareに支払い用画面のURL発行のリクエスト

  5. Squareから返ってきた支払い用画面のURLをユーザーに渡す

  6. ユーザーが支払い用画面でクレジットカード番号の入力など行い支払い処理

  7. Squareで決済処理

  8. 決済が正常終了した場合に、Squareから Webhook で支払い完了通知

  9. Next.jsのサーバーサイドでDBに購入履歴の登録
    Purchaseテーブルを使用

  10. ユーザーが購入されたコンテンツの閲覧

実装

上記の処理フローをベースにそれぞれの実装でどのようなコードになっているか補足します。

  1. 購入ボタンのクリック

こちらは購入画面の実装部分です。

getChapterでDBにアクセスし該当のコースが購入済みか確認します。

購入前であれば、CourseEnrollButtonボタンを表示します。

const ChapterIdPage = async ({
  params
}: {
  params: { courseId: string; chapterId: string }
}) => {
  const { userId } = auth();

  if (!userId) {
    return redirect("/");
  }

  const {
    chapter,
    course,
    muxData,
    attachments,
    nextChapter,
    userProgress,
    purchase,
  } = await getChapter({
    userId,
    chapterId: params.chapterId,
    courseId: params.courseId,
  });

  return (
    <div>
      <div className="flex flex-col max-w-4xl mx-auto pb-20">
        <div>
          <div className="p-4 flex flex-col md:flex-row items-center justify-between">
            <h2 className="text-2xl font-semibold mb-2">
              {chapter.title}
            </h2>
            {purchase ? (
              <CourseProgressButton
                chapterId={params.chapterId}
                courseId={params.courseId}
                nextChapterId={nextChapter?.id}
                isCompleted={!!userProgress?.isCompleted}
              />
            ) : (
              <CourseEnrollButton
                courseId={params.courseId}
                price={course.price!}
              />
            )}
          </div>

次に購入ボタンの実装です。

購入ボタンをクリックすると、API(/api/courses/${courseId}/checkout)にアクセスし、レスポンスからURLを取得し別ページで開くよう実装します。

URLをどのように生成しクライアントサイドに返しているかは「4. Next.jsのサーバーサイドからSquareに支払い用画面のURL発行のリクエスト」でご説明します。

export const CourseEnrollButton = ({
  price,
  courseId,
}: CourseEnrollButtonProps) => {
  const [isLoading, setIsLoading] = useState(false);

  const onClick = async () => {
    try {
      setIsLoading(true);

      const response = await axios.post(`/api/courses/${courseId}/checkout`)

      window.location.assign(response.data.url);
    } catch {
      toast.error("エラーが発生しました");
    } finally {
      setIsLoading(false);
    }
  }

  return (
    <Button
      onClick={onClick}
      disabled={isLoading}
      size="sm"
      className="min-w-[140px]"
    >
      {isLoading ? <div className="w-full flex items-center justify-center"><Loader2 className="h-6 w-6 animate-spin text-secondary" /></div> : `販売価格 ${formatPrice(price)}`}
    </Button>
  )
}
  1. Next.jsのサーバーサイド(今回はAPIを使用)でリクエストを受け取り

API(/api/courses/${courseId}/checkout)の具体的な実装部分になります。

今回のプロジェクトではClerkという認証サービスを使っておりcurrentUserを使うとサーバーサイドでユーザー情報が取得できます。

それらを使い存在するユーザーか確認を行い、今回購入するコース(コンテンツ)が存在するかなどリクエストのバリデーションを行います。

export async function POST(
  req: Request,
  { params }: { params: { courseId: string } }
) {
  try {
    const user = await currentUser();

    if (!user || !user.id || !user.emailAddresses?.[0]?.emailAddress) {
      return new NextResponse("Unauthorized", { status: 401 });
    }

    const course = await db.course.findUnique({
      where: {
        id: params.courseId,
        isPublished: true,
      },
      include: {
        chapters: true
      }
    });

    const purchase = await db.purchase.findUnique({
      where: {
        userId_courseId: {
          userId: user.id,
          courseId: params.courseId
        }
      }
    });

    if (purchase) {
      return new NextResponse("Already purchased", { status: 400 });
    }

    if (!course) {
      return new NextResponse("Not found", { status: 404 });
    }

    if (!course.price) {
      return new NextResponse("Not for sale", { status: 400 });
    }

    if (course.chapters.length === 0) {
      return new NextResponse("No chapters", { status: 400 });
    }

  1. リクエストのあったユーザーについてSquareのカスタマーIDがあるかDBに問い合わせ

db.squareCustomer.findUniqueにて、SquareカスタマーIDの登録があるか確認をしています。

まだカスタマーIDが作成されていなければ、アクセスしたユーザーのEmailを使って、client.customersApi.createCustomerにてSquareでカスタマーIDを作成します。

その後、DBに登録します。

export async function POST(
  req: Request,
  { params }: { params: { courseId: string } }
) {
  try {
    const user = await currentUser();

    if (!user || !user.id || !user.emailAddresses?.[0]?.emailAddress) {
      return new NextResponse("Unauthorized", { status: 401 });
    }

    const client = new Client({
      environment: Environment.Production,
      accessToken: process.env.SQUARE_ACCESS_TOKEN!
    });

    let squareCustomer = await db.squareCustomer.findUnique({
      where: {
        userId: user.id,
      },
      select: {
        squareCustomerId: true,
      }
    });

    if (!squareCustomer) {
      const response = await client.customersApi.createCustomer({
        emailAddress: user.emailAddresses[0].emailAddress,
      });

      squareCustomer = await db.squareCustomer.create({
        data: {
          userId: user.id,
          squareCustomerId: response.result.customer?.id || '',
        }
      });
    }

  1. Next.jsのサーバーサイドからSquareに支払い用画面のURL発行のリクエスト

client.checkoutApi.createPaymentLinkで支払い用画面のURLを発行するようリクエストを出します。

支払い用画面でユーザーが支払い処理をした後にコンテンツの画面に戻るようにするために、checkoutOptionsを使ってリダイレクト先のURLを指定しています。

createPaymentLink() の概要

const response = await client.checkoutApi.createPaymentLink({...});
  • client.checkoutApi.createPaymentLink() を使用し、Squareの決済リンクを作成。
  • 購入者がリンクをクリックすると決済画面に遷移 し、支払いが完了するとSquareがWebhookを送信。
  • response には 作成された決済リンクの情報(URLなど)が格納されます。

決済リクエストのパラメータ

idempotencyKey: リクエストの冪等性(べきとうせい)を保証

idempotencyKey: uuidv4(),
  • uuidv4() を使用し、一意なID(UUID)を生成。
  • 同じリクエストが複数回送信されても、決済が重複しないようにするための仕組み。

order: 注文情報

order: {
  locationId: process.env.SQUARE_LOCATION_ID!,
  customerId: squareCustomer.squareCustomerId,
  lineItems: [...],
  metadata: {...}
}
  • locationId(店舗ID): Squareで設定した決済処理を行う場所(店舗)を指定。
  • customerId: Squareに登録された顧客ID。
  • lineItems: 購入する商品の詳細(名前、数量、価格など)。
  • metadata: 追加情報(course_id など)を埋め込む(後で参照可能)。

lineItems: 購入アイテム情報

lineItems: [
  {
    name: course.title,
    quantity: '1',
    itemType: 'ITEM',
    metadata: {
      'course_id': course.id,
      'user_id': user.id,
    },
    basePriceMoney: {
      amount: BigInt(course.price),
      currency: 'JPY'
    }
  }
]

各購入アイテムの詳細:

  • name: 商品名(例: course.title)。
  • quantity: 商品の購入数(この場合は 1)。
  • itemType: 商品の種類("ITEM")。
  • metadata: 商品ごとの追加情報(コースID、ユーザーID)。
  • basePriceMoney: 商品の価格情報。
    • amount: 金額(BigInt(course.price) で整数値を使用)。
    • currency: 通貨(JPY = 日本円)。

checkoutOptions: 決済後のリダイレクトURL

checkoutOptions: {
  redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/courses/${course.id}/chapters/${course.chapters[0].id}`
}
  • 決済完了後にリダイレクトされるURL を指定。
  • 購入者がコンテンツを購入した後、最初のチャプター(course.chapters[0])のページに移動 するよう設定しています。

prePopulatedData: 事前入力データ

prePopulatedData: {
  buyerEmail: user.emailAddresses[0].emailAddress,
}
  • 購入者のメールアドレスを事前入力。
  • Squareの決済画面で、ユーザーのメールアドレス欄に自動入力されます。

paymentNote: 支払いメモ

paymentNote: course.id
  • 決済トランザクションにコースIDを紐付ける ためのメモ。
  • Squareの管理画面などで、どのコースの決済かを簡単に識別できるようになります。

最後に全体のコードを載せておきます。

    const response = await client.checkoutApi.createPaymentLink({
      idempotencyKey: uuidv4(),
      order: {
        locationId: process.env.SQUARE_LOCATION_ID!,
        customerId: squareCustomer.squareCustomerId,
        lineItems: [
          {
            name: course.title,
            quantity: '1',
            itemType: 'ITEM',
            metadata: {
              'course_id': course.id,
              'user_id': user.id,
            },
            basePriceMoney: {
              amount: BigInt(course.price),
              currency: 'JPY'
            }
          }
        ],
        metadata: {
          'course_id': course.id,
        }
      },
      checkoutOptions: {
        redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/courses/${course.id}/chapters/${course.chapters[0].id}`
      },
      prePopulatedData: {
        buyerEmail: user.emailAddresses[0].emailAddress,
      },
      paymentNote: course.id
    });
  1. Squareから返ってきた支払い用画面のURLをユーザーに渡す

クライアントサイドに返すレスポンスとしてSquareから受け取ったURLをセットします。

今回は分割してコードを書いていますが、実際は2〜5のコードがAPIの一連の処理となります。

    return NextResponse.json({ url: response.result.paymentLink?.longUrl });
  1. ユーザーが支払い用画面でクレジットカード番号の入力など行い支払い処理

5で受け取ったURLで画面を開くと支払い処理が可能となっています。
こちらはSquare内部の処理となります。

  1. Squareで決済処理

  2. 決済が正常終了した場合に、Squareから Webhook で支払い完了通知

支払い完了通知を受け取る処理を実装する前に、支払い完了通知を受け取るURLをSquareで設定します。

webhookの設定画面で通知先のURLを設定します。

合わせてどのEventを通知するかの設定も行う必要があり今回はpayment.craetedpayment.updatedを設定しています。

Image from Gyazo

Image from Gyazo

次に受け取るNext.jsのAPIの設定になります。

このAPIは、SquareのWebhookイベントを処理するためのエンドポイントを提供します。POSTメソッドを使用し、Squareから送信されるWebhookリクエストを受け取ります。以下に各処理の補足を記載します。

リクエストボディの取得

  • req.text() を使用し、リクエストのボディ(JSON形式のデータ)を文字列として取得。
  • Webhookのリクエストは application/json で送信されるため、文字列のまま扱うことで署名検証時の改変を防ぐ。
export async function POST(req: Request) {
  const body = await req.text();

Squareの署名検証

  const signature = headers().get("x-square-hmacsha256-signature") || "";
  • SquareのWebhookリクエストには、x-square-hmacsha256-signature というヘッダーが含まれます。
  • これは、Squareがリクエストの正当性を保証するために付与するHMAC署名。
  • 署名が取得できない場合は、空文字 "" を代入(ただし、検証時にエラーとなります)

Webhookの署名検証

    const result = WebhooksHelper.isValidWebhookEventSignature(
      body,
      signature,
      SIGNATURE_KEY,
      NOTIFICATION_URL
    );
  • WebhooksHelper.isValidWebhookEventSignature() を使って 署名の正当性を確認。
  • SIGNATURE_KEY は SquareのWebhookシークレットキー(管理画面で設定)。
  • NOTIFICATION_URL は このエンドポイントのURL(Square側で設定したWebhookの送信先)。
  • 署名が正しくない場合、リクエストは不正と判断されます。

Webhookイベントのパース

    const event = JSON.parse(body);
  • 受信したリクエストボディ(JSON文字列)をパースし、JavaScriptオブジェクトに変換。
  • Webhookのペイロードには type(イベント種別)や data(詳細データ)が含まれます。

受信データの分解

const { type, data } = event;
const { object } = data;
const { payment } = object;
const { note, customer_id, order_id, status } = payment;
  • SquareのWebhookイベント構造に基づいてデータを展開。
  • type には イベントの種類(例: "payment.created") が入ります。
  • payment オブジェクト内に 決済情報(注文ID、顧客ID、メモ、決済ステータス など) が含まれます。

Webhookの処理条件

if (result && type === 'payment.created' && status === 'APPROVED') {
  • result が true(署名検証OK)かつ、
  • type が "payment.created"(支払いが新規作成されたイベント)かつ、
  • status が "APPROVED"(承認済みの決済) の場合のみ処理を進める。
  • これにより 不要なWebhookリクエストをフィルタリング できます。

必須データの確認

if (!customer_id || !note || !order_id) {
  throw new Error('Webhook Error: Missing metadata');
}
  • customer_id(Squareの顧客ID)、note(メモ情報)、order_id(注文ID)のいずれかが欠けていた場合、エラーをスロー。
  • 不完全なデータで処理を進めないようにするためのバリデーションとなります。

SquareカスタマーIDを検索

const user = await db.squareCustomer.findUnique({
  where: {
    squareCustomerId: customer_id,
  }
});
  • Squareの customer_id をもとに データベース内のユーザー情報を検索。
  • db.squareCustomer.findUnique() は、DBを使用して一意のユーザー情報を取得する処理となります。

顧客が存在しない場合のエラーハンドリング

if (!user) {
  throw new Error('Webhook Error: User not found');
}
  • 対応するユーザーが見つからなかった場合、エラーをスロー。
  • これにより、存在しない顧客の情報を処理しないように防ぎます。

最後に全体のコードを載せておきます。

export async function POST(req: Request) {
  const body = await req.text();
  const signature = headers().get("x-square-hmacsha256-signature") || "";
  try {
    const result = WebhooksHelper.isValidWebhookEventSignature(
      body,
      signature,
      SIGNATURE_KEY,
      NOTIFICATION_URL
    );

    const event = JSON.parse(body);
    console.log('event', JSON.stringify(event, null, 2));

    const { type, data } = event
    const { object } = data
    const { payment } = object
    const { note, customer_id, order_id, status } = payment
    if (result && type === 'payment.created' && status === 'APPROVED' ) {
      if (!customer_id || !note || !order_id) {
        throw new Error('Webhook Error: Missing metadata');
      }

      const user = await db.squareCustomer.findUnique({
        where: {
          squareCustomerId: customer_id,
        }
      });

      if (!user) {
        throw new Error('Webhook Error: User not found');
      }
  1. Next.jsのサーバーサイドでDBに購入履歴の登録

購入情報の保存

await db.purchase.create({
  data: {
    courseId: String(note),
    userId: user.userId,
    orderId: order_id,
  }
});
  • 支払いが承認されている場合、購入情報(コースID、ユーザーID、注文ID)をデータベースに保存します。
  • note は支払いに関連するメタデータとして使われ、courseId に設定されます。

成功レスポンス

return new NextResponse(`Webhook Success: ${type}`, { status: 200 });
  • Webhookイベントの処理が成功した場合、200 OKのレスポンスとともに、「Webhook Success: type」のメッセージを返します。
  1. ユーザーが購入されたコンテンツの閲覧

「4. Next.jsのサーバーサイドからSquareに支払い用画面のURL発行のリクエスト」のcheckoutOptionsで設定した決済後のリダイレクトURLでコンテンツの閲覧画面に遷移しますのでユーザーは視聴を開始できます。

さいごに

Squareを活用することで、セキュアかつ柔軟な決済システムを簡単に構築できることがわかりました。特に、クレジットカード情報をアプリケーション側で保持せずに済むため、セキュリティ対策の負担を大幅に軽減できる点は大きなメリットです。

また、Next.jsを用いたフロントエンドとバックエンドの連携を活用することで、決済処理のフローをスムーズに実装できました。Squareの多彩なAPIを組み合わせれば、今後さらに高度な決済機能を導入することも可能です。

本記事が、Squareを利用した決済システムの導入を考えている開発者の一助になれば幸いです。

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ

関連する弊社の支援サービス