关于 extension-stripe
extension-stripe 是用于在 Caffeine AI 应用中集成 Stripe 支付的 skill,提供后端配置、结账会话和支付状态操作,并说明前端结账及结果页的实现。
主要功能
- 01Stripe 配置
- 02创建结账会话
- 03查询支付状态
- 04HTTP outcall 响应转换
- 05前端结账与结果页
适用场景
- 01为应用配置 Stripe
- 02为商品创建结账会话
- 03查询会话支付状态
- 04将用户路由到支付结果页
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ caffeinelabs/ skills --skill extension-stripe安装后在 Claude Code 中输入/extension-stripe确认已加载。
附带文件migration/v0.x.y-to-v1.x.y.md
详细介绍
Stripe 支付集成
适用于 Caffeine AI 的 Stripe 支付扩展。
概览
此技能通过 HTTP 外呼添加 Stripe 支付支持。MixinStripe 混入提供配置、创建结账会话、支付状态检查以及 HTTP 外呼 transform 回调。前端负责处理结账流程和支付结果页面。
此处不涵盖传入的 Stripe Webhook(发往 canister 的外部 POST 请求)——请使用 extension-http-endpoints 处理 http_request / http_request_update,然后在更新路径中验证 Stripe 签名。
前置条件:必须先按照 extension-authorization 操作,因为此集成依赖它。
后端
模块 API
预制模块 mo:caffeineai-stripe/stripe.mo 提供底层 Stripe HTTP 辅助函数。请勿修改它。
import OutCall "mo:caffeineai-http-outcalls/outcall";
module {
public type StripeConfiguration = {
secretKey : Text;
allowedCountries : [Text];
};
public type StripeState = {
var configuration : ?StripeConfiguration;
};
public func initState() : StripeState;
public type ShoppingItem = {
currency : Text;
productName : Text;
productDescription : Text;
priceInCents : Nat;
quantity : Nat;
};
public func createCheckoutSession(configuration : StripeConfiguration, caller : Principal, items : [ShoppingItem], successUrl : Text, cancelUrl : Text, transform : OutCall.Transform) : async Text;
public type StripeSessionStatus = {
#failed : { error : Text };
#completed : { response : Text; userPrincipal : ?Text };
};
public func getSessionStatus(configuration : StripeConfiguration, sessionId : Text, transform : OutCall.Transform) : async StripeSessionStatus;
};
在 main.mo 中设置
include MixinStripe(accessControlState, stripeState) 必须放在 main.mo 中,而不是自定义混入文件中。该混入会自动提供以下公共端点:
isStripeConfigured()setStripeConfiguration(config)createCheckoutSession(items, successUrl, cancelUrl)getStripeSessionStatus(sessionId)transform(input)— HTTP 外呼响应转换所必需
请勿重新声明这些函数。它们仅由 MixinStripe 提供。
import Stripe "mo:caffeineai-stripe/stripe";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import MixinStripe "mo:caffeineai-stripe/MixinStripe";
import Map "mo:core/Map";
import Iter "mo:core/Iter";
import Text "mo:core/Text";
import Runtime "mo:core/Runtime";
actor {
// Include authorization
let accessControlState : AccessControl.AccessControlState;
include MixinAuthorization(accessControlState, null);
let stripeState : Stripe.StripeState;
include MixinStripe(accessControlState, stripeState);
public type Product = {
id : Text;
// add custom fields
};
let products : Map.Map<Text, Product>;
public query func getProducts() : async [Product] {
products.values().toArray();
};
public shared ({ caller }) func addProduct(product : Product) : async () {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can add products");
};
products.add(product.id, product);
};
public shared ({ caller }) func updateProduct(product : Product) : async () {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can update products");
};
products.add(product.id, product);
};
public shared ({ caller }) func deleteProduct(productId : Text) : async () {
if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
Runtime.trap("Unauthorized: Only admins can delete products");
};
products.remove(productId);
};
// Add more data and functions as needed
};
迁移链头部:
import Map "mo:core/Map";
import AccessControl "mo:caffeineai-authorization/access-control";
module {
type Product = {
id : Text;
};
type StripeConfiguration = {
secretKey : Text;
allowedCountries : [Text];
};
type StripeState = {
var configuration : ?StripeConfiguration;
};
type NewActor = {
accessControlState : AccessControl.AccessControlState;
products : Map.Map<Text, Product>;
stripeState : StripeState;
};
public func migration(_old : {}) : NewActor {
{
accessControlState = AccessControl.initState();
products = Map.empty<Text, Product>();
stripeState = { var configuration = null };
};
};
};
前端
Stripe 支付集成:
用法:
-
实现一个 PaymentSetup 组件:
- 使用
isStripeConfigured()和setStripeConfiguration() - 检查是否已配置 Stripe 支付。
- 如果尚未配置,则打开管理面板,并提示用户使用
StripeConfiguration初始化 Stripe。- Stripe 密钥
- 允许的国家/地区列表,格式为 ["US", "CA", "GB"] 等,参见 Stripe 文档。
- 如果已经配置,则不要显示支付设置!
- 使用
-
实现一个结账钩子:
- 注意,需要对后端
createCheckoutSession的结果进行 JSON 解析。 - 验证解析后的会话是否包含非空的
url。如果缺失,则抛出错误且不要重定向。
import { useMutation } from '@tanstack/react-query'; import { useActor } from '@caffeineai/core-infrastructure'; import { ShoppingItem } from '../backend'; export type CheckoutSession = { id: string; url: string; }; export function useCreateCheckoutSession() { const { actor } = useActor(); return useMutation({ mutationFn: async (items: ShoppingItem[]): Promise<CheckoutSession> => { if (!actor) throw new Error('Actor not available'); const baseUrl = `${window.location.protocol}//${window.location.host}`; const successUrl = `${baseUrl}/payment-success`; const cancelUrl = `${baseUrl}/payment-failure`; const result = await actor.createCheckoutSession(items, successUrl, cancelUrl); // JSON parsing is important! const session = JSON.parse(result) as CheckoutSession; if (!session?.url) { throw new Error('Stripe session missing url'); } return session; } }); } - 注意,需要对后端
-
实现一个 Payment 组件:
- 使用
useCreateCheckoutSession() - 将
ShoppingItem[]作为输入传入。 - 分析
CheckoutSession结果。 - 将网页重定向到
CheckoutSession中的 url:这样用户就可以完成支付。 - 不要使用路由导航前往 Stripe URL。请使用
window.location.href。 - 绝不要导航到
/undefined;如果缺少session.url,则显示错误并停止。
const session = await createCheckoutSession.mutateAsync(shoppingItems); if (!session?.url) throw new Error('Stripe session missing url'); window.location.href = session.url; - 使用
-
实现 PaymentSuccess 和 PaymentFailure 组件,分别处理支付成功或失败的情况。
-
将两个特定路径路由到支付状态组件:
- 将路径 "/payment-success" 路由到 PaymentSuccess。
- 将路径 "/payment-failure" 路由到 PaymentFailure。 你需要使用 @tanstack router。
-
管理员视图提供一个用于配置 Stripe 的菜单。如果尚未配置,则在管理员登录时提示其配置 Stripe。
补充说明:确保产品图片在产品画布中正确渲染并调整大小。