十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于 Next.js App Router 与 TypeScript 集成 Stripe:Checkout / Elements / Webhook 全流程实战指南

基于 Next.js App Router 与 TypeScript 集成 Stripe:Checkout / Elements / Webhook 全流程实战指南 基于 Next.js App Router 与 TypeScript 集成 StripeCheckout / Elements / Webhook 全流程实战指南【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js本文以 Next.js 官方仓库中with-stripe-typescript示例捐赠支付为完整蓝本讲解在 Next.js App Router TypeScript 项目中端到端接入 Stripe 支付的标准姿势如何用 Server Actions 创建 Checkout Session 与 PaymentIntent、如何在客户端用 react-stripe-js 渲染支付表单、如何在服务端通过 Route Handler 校验并消费 Webhook。读完本文你将掌握一套可复制的“服务端创建支付凭证 客户端安全采集卡信息 异步对账”的支付集成框架并了解金额格式化、零货币小数、webhook 签名验证等易踩坑细节。示例概览它解决什么问题该示例是一个全栈 TypeScript 捐赠应用代码位于仓库的 examples/with-stripe-typescript 目录核心依赖如下见 package.json前端Next.js stripe/react-stripe-js 提供的Elements/PaymentElement用于受控采集卡号等敏感信息PCI 合规由 Stripe.js 接管后端Next.js 的Route Handlers处理 Webhook与Server Actions创建支付会话 / PaymentIntent配合同样用 TypeScript 编写的stripe-node官方 SDK。示例同时演示了三种主流的支付形态且在测试模式下即可完整体验托管 CheckoutHosted Checkout跳转到 Stripe 托管的支付页完成支付跳转页由 Stripe 全权渲染嵌入式 CheckoutEmbedded Checkout同一套 Server Action通过ui_mode: embedded配合client_secret在页面内嵌支付页Stripe Elements自定义 UI通过 PaymentIntent 在自有页面渲染 Stripe 支付组件Payment Element。后文将逐一从源码拆解这三条调用链。Demo 与测试卡片示例提供在线演示默认运行在 Stripetest mode。测试环境中使用以下固定卡号常规成功支付卡号4242424242424242配合任意 CVC 与未来有效期触发 3D Secure 验证流程使用卡号4000002760003184。Stripe 的完整测试场景拒付、银行拒绝、跨币种等都有对应的专用卡号可在 Stripe 官方测试文档中按需选用。项目的首页组件中也内置了测试卡片提示相关代码位于 components/StripeTestCards.tsx可用于展示给访客。下图展示了两种捐赠入口的实际交互效果Demo 动图存于示例的 public 目录核心目录与文件职责在深入代码前先建立“哪个文件负责什么”的地图路径均相对仓库根目录目录 / 文件职责app/donate-with-checkout/page.tsx“托管 Checkout”捐赠页服务端组件渲染CheckoutFormapp/donate-with-checkout/result/page.tsxCheckout 成功回跳页用session_id拉取 Checkout Session 对象app/donate-with-elements/page.tsx“Elements”捐赠页服务端组件渲染ElementsFormapp/donate-with-elements/result/page.tsxPaymentIntent 成功回跳页用payment_intent参数拉取对象app/donate-with-embedded-checkout/嵌入式 Checkout 相关页面与上面两个流程共用同一 Server Actionapp/actions/stripe.tsServer ActionscreateCheckoutSession/createPaymentIntentapp/api/webhooks/route.tsRoute Handler接收 Stripe Webhook 并做签名校验lib/stripe.ts初始化stripe-node单例含server-only保护config/index.ts货币与金额上下限配置utils/stripe-helpers.ts金额展示 / 提交格式转换utils/get-stripejs.ts懒加载 Stripe.js返回 Promisecomponents/表单、金额输入、结果 JSON 打印等客户端组件前端与服务端组件的边界很清晰页面是 Server Component负责向服务端发起“创建支付会话”的调用并渲染元数据表单是 Client Component用 react-stripe-js 完成卡的采集与确认。目录内各页面还配有error.tsx与独立的layout.tsx处理加载失败与结果页布局。快速开始本地跑起来示例可以用create-next-app的--example参数直接脚手架生成三种包管理器任选其一npx create-next-app --example with-stripe-typescript with-stripe-typescript-appyarn create next-app --example with-stripe-typescript with-stripe-typescript-apppnpm create next-app --example with-stripe-typescript with-stripe-typescript-app第一步复制环境变量文件把示例中的.env.local.example复制为项目根目录的.env.localcp .env.local.example .env.local.env.local.example位于 examples/with-stripe-typescript/.env.local.example的内容决定了本示例需要哪些密钥# Stripe keys # https://dashboard.stripe.com/apikeys NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYpk_12345 STRIPE_SECRET_KEYsk_12345 STRIPE_PAYMENT_DESCRIPTIONSoftware development services # https://stripe.com/docs/webhooks/signatures STRIPE_WEBHOOK_SECRETwhsec_1234第二步填入真实的 API Keys运行本示例需要一个 Stripe 账号。登录 Stripe开发者后台后即可找到两组密钥并替换占位值环境变量对应密钥使用场景NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYPublishable Keypk_...以NEXT_PUBLIC_前缀暴露给浏览器供 Stripe.js 初始化使用STRIPE_SECRET_KEYSecret Keysk_...仅存服务端供stripe-node创建 Checkout Session / PaymentIntent / 查询对象STRIPE_WEBHOOK_SECRETWebhook 签名密钥whsec_...仅在服务端用于校验 Webhook 事件签名本地调试时由 Stripe CLI 生成STRIPE_PAYMENT_DESCRIPTION自定义描述可选由开发者自行用于支付描述文案字符串型这里有两个重要的边界约束正是 lib/stripe.ts 第一行import server-only的原因Secret Key 绝不能进客户端 bundle。用server-only包显式声明一旦该模块被客户端代码误 import构建阶段就会直接报错从机制上杜绝密钥泄露。第三步安装依赖并启动npm install npm run dev # 或 yarn yarn dev # 或 pnpm install pnpm dev对应脚本定义在 package.jsondevnext、buildnext build、startnext start。金额与货币配置驱动的边界控制支付的金额不是任意写死的。示例把金额相关的业务规则收敛到 config/index.ts 一处export const CURRENCY usd; // Set your amount limits: Use float for decimal currencies and // Integer for zero-decimal currencies: https://stripe.com/docs/currencies#zero-decimal. export const MIN_AMOUNT 10.0; export const MAX_AMOUNT 5000.0; export const AMOUNT_STEP 5.0;CURRENCY结算币种示例为usdMIN_AMOUNT/MAX_AMOUNT金额下限与上限单位按币种约定美元这类十进制货币用浮点供客户端表单做校验避免非预期的大额/小额请求打到 StripeAMOUNT_STEP允许输入/选择的金额步长。金额格式化的关键坑零小数货币这是 Stripe 集成里最容易出 bug 的地方。Stripe API 内部一律使用“最小货币单位”整数表示金额美元要乘 100即“分”而日元、韩元这类零小数货币则直接使用整数原值不能乘 100。utils/stripe-helpers.ts 中的formatAmountForStripe正是为处理这一差异而存在export function formatAmountForStripe( amount: number, currency: string, ): number { let numberFormat new Intl.NumberFormat([en-US], { style: currency, currency: currency, currencyDisplay: symbol, }); const parts numberFormat.formatToParts(amount); let zeroDecimalCurrency: boolean true; for (let part of parts) { if (part.type decimal) { zeroDecimalCurrency false; } } return zeroDecimalCurrency ? amount : Math.round(amount * 100); }它的技巧是用Intl.NumberFormat格式化当前币种金额然后检查formatToParts输出中是否包含decimal段若存在小数点说明是十进制货币转换为分amount * 100若没有小数点则判定为零小数货币原样返回。同文件还提供了formatAmountForDisplay把 Stripe 返回的整数金额重新格式化为用户可读的货币显示串。支付流程一托管 CheckoutHosted Checkout“自定义金额 跳转 Stripe Checkout 支付页”是最少代码的接入方式。其入口页面 app/donate-with-checkout/page.tsx 是一个服务端组件导出metadata设置页面标题然后在默认导出组件中渲染客户端表单CheckoutForm uiModehosted /。服务端真正干活的是 app/actions/stripe.ts 里的 Server ActioncreateCheckoutSessionexport async function createCheckoutSession( data: FormData, ): Promise{ client_secret: string | null; url: string | null } { const ui_mode data.get(uiMode) as Stripe.Checkout.SessionCreateParams.UiMode; const origin: string headers().get(origin) as string; const checkoutSession: Stripe.Checkout.Session await stripe.checkout.sessions.create({ mode: payment, submit_type: donate, line_items: [ { quantity: 1, price_data: { currency: CURRENCY, product_data: { name: Custom amount donation }, unit_amount: formatAmountForStripe( Number(data.get(customDonation) as string), CURRENCY, ), }, }, ], ...(ui_mode hosted { success_url: ${origin}/donate-with-checkout/result?session_id{CHECKOUT_SESSION_ID}, cancel_url: ${origin}/donate-with-checkout, }), ...(ui_mode embedded { return_url: ${origin}/donate-with-embedded-checkout/result?session_id{CHECKOUT_SESSION_ID}, }), ui_mode, }); return { client_secret: checkoutSession.client_secret, url: checkoutSession.url }; }几个值得展开的实现细节use server指令文件顶部的use server声明整个模块只运行在服务端。Server Action 直接接收表单FormData表单里的customDonation金额与uiMode在此被读取回跳 URL 动态拼接通过next/headers的headers()拿到请求的origin据此拼出success_url与cancel_url这样部署在不同域名下也无需硬编码回调地址动态行价格金额走formatAmountForStripe转成 Stripe 整数分mode: payment表示单次支付非订阅submit_type: donate让 Checkout 页呈现捐赠语义一个 Action 服务两种模式ui_mode为hosted时返回url整页跳转为embedded时返回client_secret供嵌入式 Checkout 在页内渲染。仓库中同样存在 app/donate-with-embedded-checkout/ 页面目录即嵌入式形态的落地页。支付完成后 Stripe 会带着{CHECKOUT_SESSION_ID}占位符被替换成的真实session_id回跳到成功页。注意此时仍不应信任浏览器带来的参数应回源校验。成功页 app/donate-with-checkout/result/page.tsx 正是这样做的它是个async服务端组件收到searchParams.session_id后调用stripe.checkout.sessions.retrieve(session_id, { expand: [line_items, payment_intent] })取回服务端真实状态并把 PaymentIntent 状态展示出来缺参时直接throw new Error(Please provide a valid session_id ...)交给同目录的error.tsx渲染。支付流程二Stripe ElementsPaymentIntent 自定义 UI如果不想跳走页面而是把支付组件嵌到自己的表单里就需要 PaymentIntent Payment Element 组合。Server Action app/actions/stripe.ts 中的createPaymentIntent只做一件事——在服务端创建一个待支付的 PaymentIntent并把client_secret交给前端export async function createPaymentIntent( data: FormData, ): Promise{ client_secret: string } { const paymentIntent: Stripe.PaymentIntent await stripe.paymentIntents.create({ amount: formatAmountForStripe( Number(data.get(customDonation) as string), CURRENCY, ), automatic_payment_methods: { enabled: true }, currency: CURRENCY, }); return { client_secret: paymentIntent.client_secret as string }; }要点说明automatic_payment_methods: { enabled: true }让 Stripe 根据账户配置自动为 PaymentIntent 启用可用支付方式开发期无需手工枚举卡、钱包等渠道client_secret不能落入日志或缓存它用于在浏览器端完成支付确认属于敏感凭证前端流程页面在 app/donate-with-elements/page.tsx表单在 components/ElementsForm.tsx大致是先用elements提供的加载函数拿到 Stripe.js 实例参见 utils/get-stripejs.ts以NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY初始化再通过Elements包裹自定义表单在PaymentElement里采集卡信息提交时调用stripe.confirmPayment({ clientSecret, return_url })。示例把return_url指向了同目录下的result页Stripe 会在确认支付后带着payment_intent形如pi_...参数重定向回该页。成功页 app/donate-with-elements/result/page.tsx 同样在服务端回源校验用searchParams.payment_intent调用stripe.paymentIntents.retrieve(...)拉取真实状态并展示缺少参数则抛出异常引导用户提供合法的pi_...值。为便于开发者调试两条成功链路的页面都复用了 components/PrintObject.tsx把 Stripe 返回的完整对象 JSON 打印在页面上——这正是检查“我到底拿到了哪些字段”最直观的手段。支付流程三Webhook 处理支付后事件前端回跳只能拿到“同步”状态真正可靠的支付结果确认要靠Webhook异步推送例如用于发货、记账、更新订单状态。示例在 app/api/webhooks/route.ts 用一个 Route Handler 实现export async function POST(req: Request) { let event: Stripe.Event; try { event stripe.webhooks.constructEvent( await (await req.blob()).text(), req.headers.get(stripe-signature) as string, process.env.STRIPE_WEBHOOK_SECRET as string, ); } catch (err) { const errorMessage err instanceof Error ? err.message : Unknown error; if (!(err instanceof Error)) console.log(err); console.log(❌ Error message: ${errorMessage}); return NextResponse.json( { message: Webhook Error: ${errorMessage} }, { status: 400 }, ); } console.log(✅ Success:, event.id); const permittedEvents: string[] [ checkout.session.completed, payment_intent.succeeded, payment_intent.payment_failed, ]; if (permittedEvents.includes(event.type)) { let data; try { switch (event.type) { case checkout.session.completed: data event.data.object as Stripe.Checkout.Session; console.log( CheckoutSession status: ${data.payment_status}); break; case payment_intent.payment_failed: data event.data.object as Stripe.PaymentIntent; console.log(❌ Payment failed: ${data.last_payment_error?.message}); break; case payment_intent.succeeded: data event.data.object as Stripe.PaymentIntent; console.log( PaymentIntent status: ${data.status}); break; default: throw new Error(Unhandled event: ${event.type}); } } catch (error) { console.log(error); return NextResponse.json( { message: Webhook handler failed }, { status: 500 }, ); } } return NextResponse.json({ message: Received }, { status: 200 }); }这段代码揭示了 Webhook 处理的两个硬性要求必须先验签再信内容。stripe.webhooks.constructEvent用stripe-signature请求头与STRIPE_WEBHOOK_SECRET校验载荷签名与时间戳验签失败会抛错并返回 400。收到请求的 payload 需先完整读出原文示例通过(await req.blob()).text()消费 body因为它要参与 HMAC 签名计算事件白名单 幂等处理心态。示例把允许处理的事件收敛为checkout.session.completed、payment_intent.succeeded、payment_intent.payment_failed三个数组元素switch分支分别按Checkout.Session/PaymentIntent强类型解析对象。真实项目中分支内应改为写入自己的业务数据库并在处理前做事件去重Stripe 会重试投递生产逻辑不应只停留在console.log。返回响应也很有讲究处理成功返回 200示例回{ message: Received }业务分支出错返回 500 以触发 Stripe 后续重试。本地开发用 Stripe CLI 转发 Webhook本地机器没有公网地址Stripe 无法直接回调。官方提供 Stripe CLI 做隧道转发。首先安装 CLI 并登录关联自己的 Stripe 账户然后启动转发把 Webhook 打到本地路由stripe listen --forward-to localhost:3000/api/webhooksCLI 启动后会在控制台打印一个whsec_...的 webhook secret把它填进.env.local的STRIPE_WEBHOOK_SECRET并重启 dev server本地即可收到并验签真实事件。生产在 Stripe Dashboard 配置 Live Webhook部署完成后把“部署 URL 路径”形如https://your-url.vercel.app/api/webhooks在 Stripe Dashboard 中登记为 live webhook endpoint并把事件类型如checkout.session.completed、payment_intent.succeeded订阅到该 endpoint。创建后可查看并复制 endpoint 的签名密钥whsec_***将其作为新的环境变量加入部署平台在 Vercel 项目 Dashboard 进入 Settings在 General settings 的 “Environment Variables” 区域新增STRIPE_WEBHOOK_SECRET新增环境变量后必须重新部署/重建改动才会进入生产代码在 Deployments 中选择最近一次部署点击 “Visit” 旁的操作菜单并选择 “Redeploy”。Stripe 客户端实例配置项与版本策略lib/stripe.ts 中实例化stripe-node的方式值得直接复用import server-only; import Stripe from stripe; export const stripe new Stripe(process.env.STRIPE_SECRET_KEY as string, { apiVersion: 2023-10-16, appInfo: { name: nextjs-with-stripe-typescript-demo, url: https://nextjs-with-stripe-typescript-demo.vercel.app, }, });apiVersion显式固定 Stripe API 版本避免 Stripe 升级 API 导致对象结构变化带来隐性破坏示例包版本锁定stripe14.8.0在升级依赖时应留意 SDK 支持的 API 版本对应关系appInfo把应用名与地址带给 Stripe便于官方在排查请求时识别流量来源属于工程化友好项单例导出模块级export const stripe确保整个服务进程复用同一个客户端连接配置。部署到 Vercel该应用可直接部署到 Vercel 云端Next.js 官方部署文档有完整说明两种方式部署本地项目把本地仓库推送到 Git 远端后导入 Vercel。关键提醒导入项目时务必在 Environment Variables 配置界面把三个变量NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY、STRIPE_SECRET_KEY生产环境还需STRIPE_WEBHOOK_SECRET逐一设置使其与本地.env.local保持一致部署官方模板也可直接基于本示例模板一键克隆部署模板已预置上述两个密钥的输入项NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY与STRIPE_SECRET_KEY克隆后回到本地上文的流程补齐 Webhook 配置即可。部署上线并配置好 live webhook 后推荐再完整走一遍三类验证用4242424242424242验证成功链路、用4000002760003184验证 3D Secure 挑战链路、在 Dashboard 的 Webhook 日志里确认checkout.session.completed/payment_intent.succeeded事件确实被你的 Route Handler 消费。小结一套可复用的集成范式纵观整个示例可以提炼出三条值得带走的原则服务端创建、客户端确认、异步对账三层分离Checkout Session / PaymentIntent 一律由 Server Action Secret Key 创建卡信息只经 Stripe Elements/Stripe.js 采集与确认client_secret留在浏览器业务最终状态以 Webhook 为准金额边界全部收敛币种、上下限、步长在config展示/换算在utils配合零小数货币判断避免到处散落* 100回跳页只展示、不信任任何 success / return 回跳页都要用session_id/payment_intent回源 Stripe 校验真实状态Webhook 则必须用STRIPE_WEBHOOK_SECRET验签后按事件白名单分支处理。如需在自己的 Next.js TypeScript 项目中复刻可直接对照 examples/with-stripe-typescript 目录中的页面、Actions、Route Handler 与配置逐文件迁移。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表