カテゴリー
Sveltekit & StripeでWebページに『Embedded Checkout』をマウントさせる方法
※ 当ページには【広告/PR】を含む場合があります。
2024/10/27
Embedded Checkout(ページ埋め込みCheckout)を使おう
ui_mode: 'embedded'
client_secret
Sveltekitプロジェクトでの事前準備
stripe-js
stripe
$ 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の実装作業
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
<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>
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コンポーネントを呼び出す
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>
+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)にホワイトリスト登録する
Cross Site Scripting (XSS)
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
まとめ
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー