カテゴリー
Sveltekit & StripeでWebページに『Embedded Checkout』をマウントさせる方法
※ 当ページには【広告/PR】を含む場合があります。
2024/10/27

弊社は決済APIとして
通常、Stripeは
設計者の思惑は色々とあるかと思いますが、決済作業はできるだけ自社のドメイン内で行わせたい、というニーズに答える形で登場したのが、
今回はSveltekitでこの「Embedded Checkout」を統合させたときの作業の内容を防備録として残します。
Embedded Checkout(ページ埋め込みCheckout)を使おう
現状、実装しやすいのはStripe「Checkout」で、こちらを長らく採用としてきたStripeユーザーがほとんどかと思います。
この場合、決済するカスタマーは一旦、Stripeの提供する決済ページに直にアクセスする必要がありました。
昨今のセキュリティ事情に敏感なカスタマーからすると、当初アクセスしていたドメイン外へ誘導されるのは、何かしらストレスであり、商品の購入に至る前に離脱される可能性が増して、ちょっとした機会損失のリスクとも捉えられるようになりました。
そこで着目すべきは、「Embedded Checkout」という比較的新しい決済方法です。
Embedded Checkoutは、Checkout Sessionを
ui_mode: 'embedded'
client_secret
Sveltekitプロジェクトでの事前準備
SveltekitからEmbedded Checkoutを利用する場合、まずサーバー用とクライアント用の両方のstripeライブラリを導入します。
事始めの前の若干の注意点として、クライアント側では
stripe-js
他方、サーバーサイドで利用されるライブラリは、
stripe
似て非なるものですので、2つを混合して使わないようにしましょう。
では、プロジェクトにこれらをインストールしましょう。
$ yarn add stripe @stripe/stripe-js
なお、
.env
VITE_PUBLIC_BASE_URL=http://hoge.hoge.com
#ローカル(ポート5173)で試す場合には
#VITE_PUBLIC_BASE_URL=http://localhost:5173
VITE_STRIPE_API_KEY=pk_****
VITE_STRIPE_SECRET_KEY=sk_****
Embedded Checkoutの実装作業
さて、簡単のために、Sveltekitプロジェクトのsrcフォルダ内で、今回のテーマに関連するファイル構造だけを示すと、以下のようにしています。
src/
├── app.html
├── lib
│ └── components
│ └── CheckoutModalEmbedded.svelte
└── routes
├── +layout.svelte
├── +page.svelte
├── api
│ └── checkout_embedded
│ └── +server.ts
├── checkout-return
│ ├── +page.server.ts
│ └── +page.svelte
└── shop
├── +page.svelte
└── +page.ts
まずはSvelteコンポーネントとしてEmbedded Checkout UIを使うことを念頭に、実装はおおよそ以下のようになります。
<script lang="ts">
import '@sveltejs/kit';
import { goto } from '$app/navigation';
import type { Writable } from 'svelte/store';
import { getContext } from 'svelte';
import { loadStripe } from '@stripe/stripe-js';
//👇簡単な商品情報モデル
export let product: {id: number, title: string, price: number};
let checkout: any;
//👇Checkout処理中かどうかのフラグ
let isProccessing = false;
//👇ContextからStripeキー(writableストア)を取得
const stripe_key: Writable<string> = getContext('stripe_key');
let sk = '';
stripe_key.subscribe(value => { sk = value; })
function closeOutside(event: any) {
if (event.target != event.currentTarget) { return; }
close();
}
function close() {
history.back();
}
const handleCancel = async () => {
if (checkout) {
await checkout.destroy();
}
if (isProccessing) {
isProccessing = false;
}
close();
};
//👇Embedded Checkout UIを表示して決済を開始させる
const startCheckout = async () => {
isProccessing = true;
try {
//☆👇決済APIの秘密情報はサーバー側のロジックで処理させる
const response = await fetch(`/api/checkout_embedded`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(product),
});
const { clientSecret } = await response.json();
if (clientSecret) {
const stripe = await loadStripe(sk);
if (checkout) { await checkout.destroy(); }
checkout = await stripe?.initEmbeddedCheckout({ clientSecret });
//ここで#checkout要素にEmbedded Checkout UIをマウント
checkout.mount('#checkout');
} else {
console.error("Invalid response data:", clientSecret);
checkout.unmount();
isProccessing = false;
}
} catch (err) {
console.error("Error in startCheckout:", err);
if (checkout) {
await checkout.destroy();
isProccessing = false;
}
}
};
</script>
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div on:click={closeOutside}>
<h3>この商品を購入しますか?</h3>
{#if isProccessing}
<button on:click={handleCancel}>
キャンセル
</button>
{:else}
<button on:click={startCheckout}>
購入する
</button>
{/if}
</div>
<!--👇Embedded Checkout UIのマウント先 -->
<div id="checkout"></div>
このコンポーネントを適当なページで呼び出せば、Embedded Checkoutが表示されるようになります。
まだ残りの機能を実装していないので、順次編集していきます。
決済処理でのセキュアなパートはAPIルートを設けて、サーバー側で処理するようにします。
import Stripe from "stripe";
import { json } from '@sveltejs/kit';
//👇Stripe APIキーによる初期化
const stripe_sk_key = import.meta.env.VITE_STRIPE_SECRET_KEY!
const stripe = new Stripe(stripe_sk_key);
const baseUrl = import.meta.env.VITE_PUBLIC_BASE_URL!;
export async function POST({request, cookie}: any) {
const { title, price, id } = await request.json();
try {
//👇チェックアウトセッションの作成
const session = await stripe.checkout.sessions.create({
ui_mode: 'embedded',
payment_method_types: ["card"],
metadata: {
product_id: id,
issued_date: new Date(new Date().toUTCString()).toISOString()
},
//👇テストだけで使うユーザー名(-->本番で使うには別のロジックが必要)
client_reference_id: "dummy_user",
line_items: [
{
price_data: {
currency: "jpy",
product_data: {
name: title,
},
unit_amount: price,
},
quantity: 1,
},
],
mode: "payment",
return_url: `${baseUrl}/checkout-return?session_id={CHECKOUT_SESSION_ID}`,
});
return json({clientSecret: session.client_secret});
} catch (err: any) {
return json({ message: err.message });
}
}
export async function GET({ url }: any) {
const _sid = url.searchParams.get('session_id');
const session = await stripe.checkout.sessions.retrieve(_sid);
return json({
status: session.status,
customer_email: session.customer_details!.email,
product_id: session.metadata!.product_id,
issued_date: session.metadata!.issued_date
});
}
ここではアプリケーション内部から
/api/checkout_embedded
POST
GET
ついで、決済後に返却されるページも必要になります。
<script lang="ts">
export let data: any;
</script>
{#if data.status === 'complete'}
<section id="success">
<p>
[ご注文日時:{data.issued_date}]
商品ID({data.product_id})をお買い上げありがとうございました!
</p>
</section>
{:else}
<section id="fail">
<p>お取引ステータス:[{data.status}]</p>
<p>何かしらの内部エラーでトランザクションは失敗しました...</p>
</section>
{/if}
また、リターンページに決済結果が返される際の処理は
+page.server.ts
import type { PageServerLoad } from './$types';
export const load = (async ({ url, fetch }: any) => {
const sessionId = url.searchParams.get('session_id');
const response = await fetch(`/api/checkout_embedded?session_id=${sessionId}`, {
method: "GET",
});
return await response.json();
}) satisfies PageServerLoad;
適当なルートページでEmmbedded Checkoutコンポーネントを呼び出す
さきほどまでの内容で、Emmbedded CheckoutがSvelteコンポーネントで使えるようになりました。
ここでは一例として、
shop
<script lang="ts">
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
import type { PageData } from './$types';
import CheckoutModalEmbedded from "../lib/component/CheckoutModalEmbedded.svelte";
export let data: PageData;
const stripe_key = writable();
$: stripe_key.set(data.stripe_key);
//👇子コンポーネントがアクセスできるようにContextに追加
setContext('stripe_key', stripe_key);
</script>
<div>
<CheckoutModalEmbedded product={product}/>
</div>
この際、Stripe APIの公開キーのほうは
+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = () => {
return {
stripe_key: import.meta.env.VITE_STRIPE_API_KEY ?? ''
};
}
CSP(Content Security Policy)にホワイトリスト登録する
Sveltekitでは、
Cross Site Scripting (XSS)
つまり、Stripe側の提供するJSスクリプトを動作させる場合、CSPにStripeオリジンのスクリプトを通過させるように設定しないといけません。
SveltekitでCSPのホワイトリストを追加する場合、
svelte.config.js
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
//...省略
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: [
vitePreprocess({ script: true })
],
kit: {
//...省略
csp: {
directives: {
'script-src': [
'self',
'https://*.stripe.com',
]
}
}
}
};
export default config;
これによって、ビルド後にサイトのメタ情報で、
stripe
これですべての設定が完了したら、サーバー起動して動くかどうかをチェックしてみましょう。
独自ドメインにEmbedded Checkoutがマウント出来ていたらOKです。
430x962

これだけでも十分、見栄えがオンラインショップらしくなりました。
まとめ
以上、SveltekitにStripeのEmbedded Checkoutを実装するときに注意したいポイントなどを中心に解説していきました。
少し難しく感じられたかもしれませんが、この機会にネットショップを自前でフルスクラッチするのに欠かせない、決済機能を比較的簡単に構築できるため、じっくりと使い方を理解されると良いでしょう。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー