Skills に戻る
caffeinelabs/skillsチェック済み

SKILL DETAIL

extension-stripe

caffeinelabs/skills/extension-stripe

This skill adds Stripe payment support to Caffeine AI using HTTP outcalls. The backend manages Stripe configuration, creates checkout sessions, and checks payment status. The frontend handles the checkout flow and payment result pages. To integrate, you must first follow the extension-authorization skill. The backend provides a prefabricated module mo:caffeineai-stripe/stripe.mo that offers functions to create checkout sessions and retrieve session status. The frontend requires implementing a payment setup component, a checkout hook, a payment component, and payment success/failure pages, along with routing for those pages.

インストール · 189出典を見る

Installation

npx skills add https://github.com/caffeinelabs/skills --skill extension-stripe

スキルファイル

SKILL.md

最終同期 · 2026/10/04

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.