caffeinelabs/skills已通过检查
SKILL DETAIL
extension-stripe
caffeinelabs/skills/extension-stripe
此技能为 Caffeine AI 添加了基于 Stripe 的支付支持,通过 HTTP 调用实现。后端负责管理 Stripe 配置、创建结账会话和检查支付状态;前端处理结账流程和支付结果页面。 集成此技能需要先遵循 extension-authorization 技能。后端提供了预制的模块 mo:caffeineai-stripe/stripe.mo,提供创建结账会话和检查会话状态的功能。前端需要实现支付设置组件、结账钩子、支付组件以及支付成功/失败页面,并配置相应的路由。
安装量 · 189查看来源
Installation
npx skills add https://github.com/caffeinelabs/skills --skill extension-stripe
技能文件
SKILL.md
最近同步 · 2026年10月4日
migration/v0.x.y-to-v1.x.y.md›
# Migration: v0.x.y -> v1.x.y
## Summary
v1 updates the `caffeineai-stripe` mops package to `1.0.0` and moves all required Stripe actor endpoints into `MixinStripe`. Apps no longer declare `isStripeConfigured`, `setStripeConfiguration`, `createCheckoutSession`, `getStripeSessionStatus`, or `transform` manually in `main.mo` or custom mixins. Lint rules now check for `include MixinStripe(accessControlState, stripeState)` instead of per-function declarations in the actor body. Configuration state is owned at actor top level as `stripeState : Stripe.StripeState` and passed into the mixin.
This is **not** a no-op on stable state. v0 persisted `var configuration : ?Stripe.StripeConfiguration`. v1 replaces that actor field with `stripeState` wrapping the same optional config. Motoko matches actor fields by name and type, so the upgrade needs an explicit `src/backend/migrations/` step or it fails `mops check` / discards Stripe secrets.
Prerequisite: `caffeineai-authorization` `1.0.0` with `include MixinAuthorization(accessControlState, null)` in `main.mo`.
## Breaking changes
### 1. `mops.toml` version bump
```diff
-caffeineai-stripe = "0.1.2"
+caffeineai-stripe = "1.0.0"
```
The package now depends on `caffeineai-authorization` for admin checks inside the mixin.
### 2. Replace manual Stripe endpoints with `MixinStripe`
Remove all hand-written Stripe actor endpoints and the old `var configuration` field. Add the mixin in `main.mo` (not in a custom mixin file).
Under enhanced migration (Caffeine-generated apps), declare the field with a type only — do **not** call `Stripe.initState()` in the actor body (`M0250`). The migration in section 3 supplies the value.
```diff
import Stripe "mo:caffeineai-stripe/stripe";
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
-import OutCall "mo:caffeineai-http-outcalls/outcall";
+import MixinStripe "mo:caffeineai-stripe/MixinStripe";
import Runtime "mo:core/Runtime";
actor {
let accessControlState = AccessControl.initState();
include MixinAuthorization(accessControlState, null);
+ let stripeState : Stripe.StripeState;
+ include MixinStripe(accessControlState, stripeState);
- var configuration : ?Stripe.StripeConfiguration = null;
-
- public query func isStripeConfigured() : async Bool { ... };
- public shared ({ caller }) func setStripeConfiguration(...) : async () { ... };
- public func getStripeSessionStatus(sessionId : Text) : async Stripe.StripeSessionStatus { ... };
- public shared ({ caller }) func createCheckoutSession(...) : async Text { ... };
- public query func transform(input : OutCall.TransformationInput) : async OutCall.TransformationOutput { ... };
};
```
If Stripe endpoints live in a custom mixin (for example `fees-subjects-transport-timetable-csv-api.mo`), delete them there and add `let stripeState : Stripe.StripeState; include MixinStripe(accessControlState, stripeState)` in `main.mo` instead.
`Stripe.initState()` in [SKILL.md](../SKILL.md) is a state-shape hint for projects without enhanced migration. Do not copy that initializer into an EM actor body.
### 3. Map `configuration` into `stripeState`
`StripeState` is `{ var configuration : ?StripeConfiguration }`. The payload type is unchanged; the actor field is not. Motoko treats this as a **rename plus restructure**:
- remove `configuration`
- add `stripeState`
That requires an explicit migration. A `main.mo` swap alone will fail stable-compatibility checks (`M0170`) or drop the secret key until someone reconfigures Stripe.
Follow [migrating-motoko-actors](../../migrating-motoko-actors/SKILL.md): at most one pending file per build; if this build already added a migration, fold this mapping into that file. Inline old and new types — no project imports, only `mo:core/...`. List **every** stable field in both `OldActor` and `NewActor`; omitting `configuration` from `OldActor` discards it.
`OldActor` is the `NewActor` of the previous file in `src/backend/migrations/` (lex order), or the pre-conversion shape if that is the chain start — never the current `main.mo`. Pass every unchanged field through. The Stripe-specific mapping is:
```motoko
stripeState = { var configuration = old.configuration };
```
Do **not** use `Stripe.initState()` here. That would set `configuration` to `null` and wipe live keys.
Sketch (other fields omitted — copy them from the previous `NewActor`):
```motoko
// src/backend/migrations/YYYYMMDD_HHMMSS.mo
module {
type StripeConfiguration = {
secretKey : Text;
allowedCountries : [Text];
};
type StripeState = {
var configuration : ?StripeConfiguration;
};
type OldActor = {
var configuration : ?StripeConfiguration;
// every other previously deployed stable field
};
type NewActor = {
stripeState : StripeState;
// every other stable field, including new actor fields
};
public func migration(old : OldActor) : NewActor {
{
stripeState = { var configuration = old.configuration };
// pass through every other field
};
};
};
```
Then run `mops check --fix` until it passes, and `mops build` once at the end.
### 4. Do not redeclare mixin-provided functions
Lint rules now error if any of these are declared directly in the actor:
- `isStripeConfigured`
- `setStripeConfiguration`
- `createCheckoutSession`
- `getStripeSessionStatus`
- `transform`
### 5. Frontend unchanged
Actor method names and signatures are unchanged. No frontend migration is required beyond rebuilding bindings after the backend update.
## Upgrade checklist
- [ ] Bump `caffeineai-stripe` to `1.0.0` in `mops.toml`
- [ ] Ensure `caffeineai-authorization` `1.0.0` is installed and `include MixinAuthorization(accessControlState, null)` is present in `main.mo`
- [ ] Add `let stripeState : Stripe.StripeState` and `include MixinStripe(accessControlState, stripeState)` in `main.mo`
- [ ] Remove the five mixin-provided functions and the old `var configuration` declaration from `main.mo` and any custom mixins
- [ ] Add or fold a `src/backend/migrations/` step that sets `stripeState = { var configuration = old.configuration }` and passes through every other stable field
- [ ] Run `mops install`, `mops check --fix`, `mops build`, and `mops lint`
- [ ] Regenerate frontend bindings if your workflow requires it
SKILL.md›
---
name: extension-stripe
description: Payment support based on Stripe, supporting credit cards and debit cards
version: 1.0.0
compatibility:
mops:
caffeineai-stripe: "~1.0.0"
caffeineai-http-outcalls: "~0.1.3"
caffeineai-authorization: "~1.0.1"
caffeineai-subscription: [none]
---
# Stripe Payment Integration
Stripe payment extension for [Caffeine AI](https://caffeine.ai?utm_source=caffeine-skill&utm_medium=referral).
## Overview
This skill adds Stripe payment support using HTTP outcalls. The `MixinStripe` mixin provides configuration, checkout session creation, payment status checks, and the HTTP outcall `transform` callback. The frontend handles checkout flow and payment result pages.
Prerequisite: You must follow [extension-authorization](../extension-authorization/SKILL.md) first, as this integration depends on it.
# Backend
## Module API
The prefabricated module `mo:caffeineai-stripe/stripe.mo` provides low-level Stripe HTTP helpers. Do not modify it.
```mo:caffeineai-stripe/stripe.mo
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;
};
```
## Setup in main.mo
`include MixinStripe(accessControlState, stripeState)` MUST be placed in `main.mo`, not in a custom mixin file. The mixin provides these public endpoints automatically:
- `isStripeConfigured()`
- `setStripeConfiguration(config)`
- `createCheckoutSession(items, successUrl, cancelUrl)`
- `getStripeSessionStatus(sessionId)`
- `transform(input)` — required for HTTP outcall response transformation
Do NOT redeclare any of these functions. They are provided exclusively by `MixinStripe`.
```motoko filepath=src/backend/main.mo
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
};
```
The migration chain head:
```motoko filepath=src/backend/migrations/00000000_000000.mo
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 };
};
};
};
```
# Frontend
For Stripe payment integration:
Usage:
1. Implement a PaymentSetup component with:
* Use `isStripeConfigured()` and `setStripeConfiguration()`
* Checks whether Stripe payment is configured.
* If not, opens an admin panel and asks the user to initialze Stripe with `StripeConfiguration`.
- Stripe secret key
- List of allowed countries, notation ["US", "CA", "GB"] etc., see the Stripe documentation.
* Do not show the payment setup when it has already been configured!
2. Implement a checkout hook:
* Note that JSON parsing of backend `createCheckoutSession` result is needed.
* Validate that the parsed session includes a non-empty `url`. If missing, throw an error and do not redirect.
```
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;
}
});
}
```
3. Implement a Payment component with:
* `useCreateCheckoutSession()`
* Pass `ShoppingItem[]` as input.
* Anaylze the `CheckoutSession` result.
* Redirect webpage to url in `CheckoutSession`: This allows the user to complete the payment.
* Do NOT use router navigation for the Stripe URL. Use `window.location.href`.
* Never navigate to `/undefined`; if `session.url` is missing, show an error and stop.
```
const session = await createCheckoutSession.mutateAsync(shoppingItems);
if (!session?.url) throw new Error('Stripe session missing url');
window.location.href = session.url;
```
4. Implement a PaymentSuccess and PaymentFailure component to handle payment success or failure, respectively.
5. Route two specific paths to the payment status components:
* Path "/payment-success" to PaymentSuccess.
* Path "/payment-failure" to PaymentFailure.
You need to use @tanstack router.
6. The admin view offers a menu to configure Stripe. If not yet configured, it asks the admin to configure Stripe on login.
Side note: Make sure that product images are properly rendered and resized inside the product canvas.