実現したいこと
Fiebase と Stripe を組み合わせてサブスクなどを実装したいです。
Firebase側とStripe側の設定は終わっています。
【目標】
・テスト送信して実際に成功
・Firebaseにusers/userId/に書き込む
・成功メールを送信する
発生している問題・分からないこと
stripe trigger checkout.session.completed
すると以下のエラーが出る。
【問題】
・テスト送信失敗
・Firebaseにusers/userId/に書き込み失敗
・成功メールを未送信
が出ます。
エラーメッセージ
error
12025-11-05 13:42:38 <-- [400] POST https://your-products-id.cloudfunctions.net/stripeWebhook [....]
該当のソースコード
index.js
1// functions/index.js 2import { onRequest, onCall } from "firebase-functions/v2/https"; 3import { setGlobalOptions } from "firebase-functions/v2"; 4import { logger } from "firebase-functions"; 5import admin from "firebase-admin"; 6import Stripe from "stripe"; 7 8setGlobalOptions({ maxInstances: 10 }); 9 10// ====================================================== 11// Firebase 初期化 12// ====================================================== 13if (!admin.apps.length) { 14 admin.initializeApp(); 15} 16const db = admin.firestore(); 17 18// ====================================================== 19// Stripe 初期化 20// ====================================================== 21const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { 22 apiVersion: "2024-06-20", 23}); 24 25// ====================================================== 26// Webhook 関数(rawBody 対応) 27// ====================================================== 28export const stripeWebhook = onRequest( 29 { 30 secrets: ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"], 31 region: "us-central1", 32 memory: "256MiB", 33 timeoutSeconds: 60, 34 rawBody: true, // ここが必須 35 }, 36 async (req, res) => { 37 const sig = req.headers["stripe-signature"]; 38 if (!sig) { 39 logger.warn("No stripe-signature"); 40 return res.status(400).send("Missing signature"); 41 } 42 43 let event; 44 try { 45 // rawBody は Buffer になっている 46 event = stripe.webhooks.constructEvent( 47 req.rawBody, 48 sig, 49 process.env.STRIPE_WEBHOOK_SECRET 50 ); 51 } catch (err) { 52 logger.error("Webhook signature failed", err.message); 53 return res.status(400).send(`Webhook Error: ${err.message}`); 54 } 55 56 logger.info(`✅ Event received: ${event.type}`); 57 58 if (event.type === "checkout.session.completed") { 59 const session = event.data.object; 60 const userId = session.client_reference_id; 61 62 if (!userId) { 63 logger.warn("No client_reference_id", session.id); 64 return res.status(400).send("No userId"); 65 } 66 67 // Firestore 書き込み 68 try { 69 await db.collection("users").doc(userId).set( 70 { 71 stripeCustomerId: session.customer, 72 subscription: { 73 active: true, 74 stripeSubscriptionId: session.subscription, 75 plan: session.metadata?.plan || "basic", 76 updatedAt: admin.firestore.FieldValue.serverTimestamp(), 77 }, 78 }, 79 { merge: true } 80 ); 81 logger.info("Subscription activated", { userId }); 82 } catch (error) { 83 logger.error("Firestore write failed", error); 84 return res.status(500).send("Firestore error"); 85 } 86 87 // 歓迎メール送信 88 const email = session.customer_details?.email; 89 if (email) { 90 await sendWelcomeEmail(email, session.metadata?.plan || "basic"); 91 } 92 } 93 94 res.json({ received: true }); 95 } 96); 97 98// ====================================================== 99// Checkout セッション作成 100// ====================================================== 101export const createCheckoutSession = onCall( 102 { 103 secrets: ["STRIPE_SECRET_KEY"], 104 region: "us-central1", 105 }, 106 async (request) => { 107 const uid = request.auth?.uid; 108 if (!uid) throw new Error("ログインが必要です。"); 109 110 const priceId = request.data.priceId; 111 const customerId = await getOrCreateCustomer(uid, stripe, db); 112 113 const session = await stripe.checkout.sessions.create({ 114 mode: "subscription", 115 payment_method_types: ["card"], 116 customer: customerId, 117 line_items: [{ price: priceId, quantity: 1 }], 118 client_reference_id: uid, 119 metadata: { plan: request.data.plan || "basic" }, 120 success_url: "https://your-domain.com/success.html", 121 cancel_url: "https://your-domain.com/cancel.html", 122 }); 123 124 return { url: session.url }; 125 } 126); 127 128// ====================================================== 129// カスタマー取得 or 作成 130// ====================================================== 131async function getOrCreateCustomer(uid, stripe, db) { 132 const userRef = db.collection("users").doc(uid); 133 const userSnap = await userRef.get(); 134 135 if (userSnap.exists && userSnap.data()?.stripeCustomerId) { 136 return userSnap.data().stripeCustomerId; 137 } 138 139 const userRecord = await admin.auth().getUser(uid); 140 const customer = await stripe.customers.create({ 141 email: userRecord.email, 142 metadata: { uid }, 143 }); 144 145 await userRef.set({ stripeCustomerId: customer.id }, { merge: true }); 146 return customer.id; 147} 148 149// ====================================================== 150// 歓迎メール送信関数 151// ====================================================== 152async function sendWelcomeEmail(email, plan) { 153 const nodemailer = await import("nodemailer"); // 関数内 import 154 const transporter = nodemailer.createTransport({ 155 service: "gmail", 156 auth: { 157 user: "your-gmail@gmail.com", 158 pass: process.env.GMAIL_APP_PASSWORD, 159 }, 160 }); 161 162 const mailOptions = { 163 from: "メルカリ売上管理ツール <noreply@your-domain.com>", 164 to: email, 165 subject: "有料プランへようこそ!", 166 html: ` 167 <h2>おめでとうございます!</h2> 168 <p><strong>${plan}プラン</strong>が有効になりました!</p> 169 <p>今すぐ自動売上取得がスタートします。</p> 170 <p><a href="https://your-tool.com">ツールにログイン</a></p> 171 <hr> 172 <small>30日間返金保証付き | support@your-domain.com</small> 173 `, 174 }; 175 176 await transporter.sendMail(mailOptions); 177 logger.info("Welcome email sent", { email }); 178} 179
firebase.json
1{ 2 "functions": { 3 "source": "functions" 4 }, 5 "emulators": { 6 "functions": { 7 "port": 8358 8 }, 9 "firestore": { 10 "port": 8082 11 }, 12 "ui": { 13 "enabled": true 14 } 15 } 16} 17
packege.json
1{ 2 "name": "functions", 3 "type": "module", 4 "scripts": { 5 "lint": "eslint .", 6 "serve": "firebase emulators:start --only functions", 7 "shell": "firebase functions:shell", 8 "start": "npm run shell", 9 "deploy": "firebase deploy --only functions", 10 "logs": "firebase functions:log" 11 }, 12 "engines": { 13 "node": "20" 14 }, 15 "main": "index.js", 16 "dependencies": { 17 "express": "^5.1.0", 18 "firebase-admin": "^12.6.0", 19 "firebase-functions": "^5.1.1", 20 "nodemailer": "^7.0.10", 21 "stripe": "^16.12.0" 22 }, 23 "devDependencies": { 24 "eslint": "^8.15.0", 25 "eslint-config-google": "^0.14.0" 26 }, 27 "private": true 28} 29
試したこと・調べたこと
- teratailやGoogle等で検索した
- ソースコードを自分なりに変更した
- 知人に聞いた
- その他
上記の詳細・結果
ChatGPTおよびgrokを使ってエラー解決に臨んだもののうまくいきませんでした。
補足
特になし
あなたの回答
tips
プレビュー