返回 Skills 目录
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.