Astroを使って構築したサイトに、Stripeの決済やサブスクリプション申し込みフローを追加する方法

静的なウェブサイトを構築する方法の１つとして、Astroへの注目が高まっています。

[![スクリーンショット 2023-02-07 12.23.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/2c58cbe0-9a39-460c-1be2-6b9e083d0e8a.png)](https://astro.build/)  
[https://astro.build/](https://astro.build/)

今回は、このAstroを使ったサイトにて、Stripeを利用した決済フォームを組み込む方法を紹介します。

## 3つの組み込み方

Astroで構築したサイトにStripeの決済フォームを組み込むには、3つの方法があります。

- 1: リンクまたは埋め込みコードで実装する
- 2: リダイレクト型の決済セッションを作成する
- 3: 埋め込み型の決済フォームを実装する

１がもっとも簡単に組み込みができる反面、デザインのカスタマイズ性などは３に軍配があがります。

それぞれの実装方法を見ていきましょう。

## 1: リンクまたは埋め込みコードで実装する方法

もっとも簡単な方法は、Stripe上で発行した決済リンクや埋め込みコードを使うことです。

### Payment Linksで発行したURLを利用する

StripeのPayment Linksを使うことで、決済ページへのURLを発行できます。

![スクリーンショット 2023-02-07 12.33.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/01e28d58-4a37-acff-6a48-efd169a26722.png)

発行したURLまたはQRコードを、サイトのリンクや画像として埋め込みましょう。

![スクリーンショット 2023-02-07 12.34.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/f9648362-7a4e-a72a-01f1-caa160825bf4.png)

```js
---
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';

---

<Layout title="Welcome to Astro.">
	<main>
		<ul role="list" class="link-card-grid">
			<Card
				href="https://buy.stripe.com/test_xxxxxx"
				title="コーヒー豆を注文する"
				body="おすすめのコーヒー豆をお届けします。（税込1,500円）"
			/>
```

Astro側で注文ボタンなどを用意するだけで、決済やサブスクリプションの申し込みフローを実装できます。

![スクリーンショット 2023-02-07 12.36.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/bd32c45b-3d48-b5fb-5e92-08433194492e.png)

### 料金表を作成して埋め込む

プランによって提供する機能が変わる場合などでは、料金表を作成して埋め込むこともできます。

![スクリーンショット 2023-02-07 12.38.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/5c10bf08-9c74-4566-792d-d1841e524e0b.png)

発行されたコードを、そのままAstroのコードに追加しましょう。

```js
---
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';
---

<Layout title="Welcome to Astro.">
	<main>
		<h1>Welcome to <span class="text-gradient">Astro</span></h1>
		<script async src="https://js.stripe.com/v3/pricing-table.js"></script>
		<stripe-pricing-table pricing-table-id="prctbl_1xxxxxxx"
		publishable-key="pk_test_xxxxxx">
		</stripe-pricing-table>
		<ul role="list" class="link-card-grid">
```

これだけで料金表と決済フォームへの遷移が実装できます。

![スクリーンショット 2023-02-07 12.39.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/7616de2c-2c9b-3eca-f1ba-8ca7ce4cc2b6.png)


https://stripe.com/docs/payments/checkout/pricing-table

## 2: リダイレクト型の決済セッションを作成する方法

2つ目・3つ目の方法では、サーバー側のAPIが必要となります。

そのため、Stripeのドキュメントサイトにあるクイックスタートを利用して、ローカル環境にAPIを用意しましょう。

https://stripe.com/docs/checkout/quickstart

Astro側では、`form`タグを利用して、クイックスタートが提供するPOSTのAPI（`/create-checkout-session`）に遷移する実装を追加します。

```js
---
import Layout from '../layouts/Layout.astro';
---

<Layout title="Welcome to Astro.">
	<main>
		<h1>Welcome to <span class="text-gradient">Astro</span></h1>
		<form action='http://localhost:4200/create-checkout-session' method='post'>
			<button type='submit'>Checkout</button>
		</form>
```

これで`Checkout`ボタンをクリックすると、決済フォームへリダイレクトされるようになります。

![スクリーンショット 2023-02-07 13.11.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/3bc37f04-0df0-5337-b157-422afc247ec2.png)


## 3: 埋め込み型の決済フォームを実装する方法


3つ目の方法では、2つ目とまた異なるサーバー側のAPIを用意します。

Stripeのドキュメントサイトにあるクイックスタートを利用して、ローカル環境にAPIを用意しましょう。

https://stripe.com/docs/payments/quickstart

### Stripe.jsライブラリを追加する

フォームを埋め込むために、まずはSDKを追加しましょう。

```bash
% npm i  @stripe/stripe-js
```

### Stripe Elements用のComponentを用意する

APIで作成されたPayment Intentを利用してフォームを描画しましょう。

```html

<script hoist type='module'>
import {loadStripe} from '@stripe/stripe-js';
const stripe = await loadStripe('pk_test_TYooMQauvdEDq54NiTphI7jx');

class PaymentForm {
	private readonly publicAPIKey;
	private readonly formElementId;
	private readonly elementId;
	private readonly stripe
	private elements

	constructor(publicAPIKey, options = {}) {
		this.publicAPIKey = publicAPIKey;
		this.elementId = options.elementId || 'payment-element';
		this.formElementId = options.formElementId || 'payment-element-form';
	}
	async init() {
		this.stripe = await loadStripe(this.publicAPIKey)
	}
	create(clientSecret) {
		if (!this.stripe) return;

　		// Elementsの作成とマウントを実施
		this.elements = this.stripe.elements({
			appearance: {
				theme: 'stripe',
			},
			clientSecret
		});
		const paymentElement = this.elements.create("payment", {
			layout: "tabs",
		});
		const elementPlaceholder = document.getElementById(this.elementId);
		paymentElement.mount(elementPlaceholder);

		// FormのSubmit処理
		const paymentForm = document.getElementById(this.formElementId);
		paymentForm.addEventListener('submit', async (e) => {
			e.preventDefault();
			if (!this.stripe || !this.elements) return;
			const { error, paymentIntent } = await this.stripe.confirmPayment({
				elements: this.elements,
				redirect: 'if_required'
			});
			if (error) {
				alert(`${error.code}: ${error.message}`);
			} else {
				alert("done");
			}
		});
	}
}
	
// ページ読み込み時の動作
try {
	const response = await fetch('http://localhost:4200/create-payment-intent', {
		method: 'post'
	});
	const { clientSecret } = await response.json();
	const form = new PaymentForm('pk_test_xxxxx');
	await form.init();
	form.create(clientSecret);

} catch (e) {
	console.error(e);
}
</script>


    <form id='payment-element-form' client:only>
		<div id='payment-element'></div>
		<button type='submit'>Checkout</button>
    </form>
```

これで決済フォームが表示されます。

![スクリーンショット 2023-02-07 14.19.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/9ec95392-bb81-25c2-ff49-05de9d070fb0.png)

マウント時にエラーが出る場合は、次のような方法をとることもできます。

```js
		const elementPlaceholder = document.createElement("div");
		paymentElement.mount(elementPlaceholder);
		const targetElement = document.getElementById(this.elementId);
		targetElement.appendChild(elementPlaceholder);
```

#### [Appendix] Web Componentとして実装する

Web Componentとして実装することもできます。
この方法では、`script`タグを配置せずに実装できるようになるのがポイントです。

```html
<script hoist type='module'>
import {loadStripe} from '@stripe/stripe-js';
class StripeElement extends HTMLElement {
	private readonly stripe;
	private readonly elements;
	constructor() {
		super();

        // Stripe.jsを読み込みする
		loadStripe('pk_test_xxxxx')
			.then(stripe => {
				this.stripe = stripe;
                // 決済フォームを描画・マウントする
				return this.createPaymentElement();
			})
			.then(() => {
                // formのsubmitイベントで、決済処理を実施する
				const formElement = document.getElementById('payment-element-form');
				formElement.addEventListener('submit', async (e) => {
					e.preventDefault();
					this.handleSubmit();
				});
			});
	}
	async handleSubmit() {
		if (!this.stripe || !this.elements) return;
		const { error, paymentIntent } = await this.stripe.confirmPayment({
			elements: this.elements,
			redirect: 'if_required'
		});
		if (error) {
			alert(`${error.code}: ${error.message}`);
		} else {
			alert("done");
		}

	}
	async createPaymentElement() {
　　　　　　　　　　　　　　　　　　　　　　　　// Payment Intentを取得する
			fetch('http://localhost:4200/create-payment-intent', {
					method: 'post'
			}).then(response => response.json())
			.then(pi => {
                // Payment Intentから、フォームを描画・マウントする
				this.elements = this.stripe.elements({
					appearance: {
						theme: 'stripe',
					},
					clientSecret: pi.clientSecret
				});
				const paymentElement = this.elements.create("payment", {
					layout: "tabs",
				});
				const elementPlaceholder = document.createElement("div");
				paymentElement.mount(elementPlaceholder);
				const targetElement = document.getElementById('payment-element');
				targetElement.appendChild(elementPlaceholder);
			});

	}
}
customElements.define('stripe-element', StripeElement);
</script>


<stripe-element>
    <form id='payment-element-form' client:only>
		<div id='payment-element'></div>
		<button type='submit'>Checkout</button>
    </form>
</stripe-element>
```

### 3-1: AstroのSSRもしくはHybrid Renderingを使って、サーバー側の処理も実装する方法

Astro 2.0からサポートされたHybrid Renderingを利用し、決済に関連する部分だけSSRで動作させることもできます。

https://astro.build/blog/astro-2/

#### ライブラリのインストール

StripeのJSライブラリを追加しましょう。

```bash
$ yarn add stripe
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
warning prettier-plugin-astro@0.7.2: The engine "pnpm" appears to be invalid.
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Saved lockfile.
success Saved 7 new dependencies.
```

#### 環境変数で、シークレットAPIキーを設定する

**.env**ファイルを作成し、StripeのシークレットAPIキーを保存しましょう。

```
STRIPE_SECRET_API_KEY=sk_test_xxxx
```

https://docs.astro.build/ja/guides/environment-variables/

#### サーバー側のスクリプトでPayment Intentを作成する

`---フェンス`内でサーバー側の処理を実装しましょう。

```html
---
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';

import Stripe from 'stripe';
const stripe = new Stripe(import.meta.env.STRIPE_SECRET_API_KEY);

const paymentIntent = await stripe.paymentIntents.create({
	amount: 100,
	currency: "jpy",
	automatic_payment_methods: {
		enabled: true,
	},
});
const clientSecret = paymentIntent.client_secret;
---
```

クライアント側のJSで、`clientSecret`を利用するため、`data`属性に値を設定します。

```diff_html
		<form id='payment-element-form' client:only>
-			<div id="payment-element"></div>
+			<div id="payment-element" data-pi={clientSecret}></div>
			<button type='submit'>Checkout</button>
		</form>
```

Elementsをマウントする処理に、`data`属性から値を取得するコードを追加しましょう。

```diff_javascript

+	create(clientSecret) {
-	create() {
		if (!this.stripe) return;
+		const elementPlaceholder = document.getElementById(this.elementId);
+		const clientSecret = elementPlaceholder.dataset.pi;
		this.elements = this.stripe.elements({
			appearance: {
				theme: 'stripe',
			},
			clientSecret
		});
		const paymentElement = this.elements.create("payment", {
			layout: "tabs",
		});
-		const elementPlaceholder = document.getElementById(this.elementId);
		paymentElement.mount(elementPlaceholder);
```

APIをfetchしてデータを取得する必要がなくなりましたので、コードを削除します。

```diff_html
try {
-	const response = await fetch('http://localhost:4200/create-payment-intent', {
-		method: 'post'
-	});
-	const { clientSecret } = await response.json();
	const form = new PaymentForm('pk_test_xxxxx');
	await form.init();
+	form.create(clientSecret);
-	form.create(clientSecret);

} catch (e) {
	console.error(e);
}
</script>
```

これでAstroのSSR機能を利用した決済フォーム表示処理が実装できました。

:::note info
Checkoutのセッションについても、`---フェンス`内にセッションURLを発行することで実現できます。
Customer Portalについても同様です。
:::

もし決済ページ以外で静的な書き出し（SSG）を行いたいページがある場合は、`--フェンス`内に１行次のコードを追加しましょう。

```diff
---
+export const prerender = true;
---
```

## おわりに

StripeをAstroで構築したサイトに組み込む方法を、リンク・リダイレクト・埋め込みの３種類から紹介しました。

サブスクリプションについても、Checkoutセッションを作成する処理の引数を変更したり、Payment Intentの代わりにサブスクリプションを作成することで対応できます。

https://qiita.com/hideokamoto/items/e03984b982574ab01c31

またテストクロックを利用して、プラン変更の動作をテストすることもできます。




##　参考ドキュメントなど

https://docs.astro.build/ja/guides/client-side-scripts/

https://docs.astro.build/ja/guides/server-side-rendering/