extension-stripe

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Stripe Payment Integration

Stripe 支付集成

Stripe payment extension for Caffeine AI.
适用于Caffeine AI的Stripe支付扩展。

Overview

概述

This skill adds Stripe payment support using HTTP outcalls. The backend manages Stripe configuration, creates checkout sessions, and checks payment status. The frontend handles checkout flow and payment result pages.
该技能通过HTTP外部调用添加Stripe支付支持。后端负责管理Stripe配置、创建结账会话并检查支付状态,前端则处理结账流程和支付结果页面。

Backend

后端

For Stripe payment integration:
Prerequisite: You must follow extension-authorization first, as this integration depends on it.
There is the prefabricated module
mo:caffeineai-stripe/stripe.mo
that that cannot be modified. It provides fundamental functionality for making HTTP GET or PUT requests in the backend.
mo
import OutCall "mo:caffeineai-http-outcalls/outcall";

module {
  public type StripeConfiguration = {
    secretKey : Text;
    allowedCountries : [Text];
  };

  public type ShoppingItem = {
    currency : Text;
    productName : Text;
    productDescription : Text;
    priceInCents : Nat;
    quantity : Nat;
  };

  /// Initiate payment session for shopping items.
  /// Returns Stripe JSON reply message.
  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 };
  };

  /// Check payment status.
  public func getSessionStatus(configuration : StripeConfiguration, sessionId : Text, transform : OutCall.Transform) : async StripeSessionStatus;
};
Usage:
motoko
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 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.initState();
    include MixinAuthorization(accessControlState);

    // Shopping data
    public type Product = {
        id : Text;
        // add custom fields
    };

    let products = Map.empty<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);
    };

    // Stripe integration
    var configuration : ?Stripe.StripeConfiguration = null;

    public query func isStripeConfigured() : async Bool {
        configuration != null;
    };

    public shared ({ caller }) func setStripeConfiguration(config : Stripe.StripeConfiguration) : async () {
        if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
            Runtime.trap("Unauthorized: Only admins can perform this action");
        };
        configuration := ?config;
    };

    func getStripeConfiguration() : Stripe.StripeConfiguration {
        switch (configuration) {
            case (null) { Runtime.trap("Stripe needs to be first configured") };
            case (?value) { value };
        };
    };

    public func getStripeSessionStatus(sessionId : Text) : async Stripe.StripeSessionStatus {
        await Stripe.getSessionStatus(getStripeConfiguration(), sessionId, transform);
    };

    public shared ({ caller }) func createCheckoutSession(items : [Stripe.ShoppingItem], successUrl : Text, cancelUrl : Text) : async Text {
        await Stripe.createCheckoutSession(getStripeConfiguration(), caller, items, successUrl, cancelUrl, transform);
    };

    public query func transform(input : OutCall.TransformationInput) : async OutCall.TransformationOutput {
        OutCall.transform(input);
    };

    // Add more data and functions as needed
};
Stripe支付集成步骤:
前置条件:由于此集成依赖于扩展授权,您必须先遵循extension-authorization的指引。
我们提供了预制模块
mo:caffeineai-stripe/stripe.mo
,该模块不可修改,它为后端提供了发起HTTP GET或PUT请求的基础功能。
mo
import OutCall "mo:caffeineai-http-outcalls/outcall";

module {
  public type StripeConfiguration = {
    secretKey : Text;
    allowedCountries : [Text];
  };

  public type ShoppingItem = {
    currency : Text;
    productName : Text;
    productDescription : Text;
    priceInCents : Nat;
    quantity : Nat;
  };

  /// 为购物商品发起支付会话。
  /// 返回Stripe的JSON响应消息。
  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;
};
使用示例:
motoko
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 Map "mo:core/Map";
import Iter "mo:core/Iter";
import Text "mo:core/Text";
import Runtime "mo:core/Runtime";

actor {
    // 引入授权功能
    let accessControlState = AccessControl.initState();
    include MixinAuthorization(accessControlState);

    // 商品数据
    public type Product = {
        id : Text;
        // 添加自定义字段
    };

    let products = Map.empty<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("未授权:仅管理员可添加商品");
        };
        products.add(product.id, product);
    };

    public shared ({ caller }) func updateProduct(product : Product) : async () {
        if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
            Runtime.trap("未授权:仅管理员可更新商品");
        };
        products.add(product.id, product);
    };

    public shared ({ caller }) func deleteProduct(productId : Text) : async () {
        if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
            Runtime.trap("未授权:仅管理员可删除商品");
        };
        products.remove(productId);
    };

    // Stripe集成逻辑
    var configuration : ?Stripe.StripeConfiguration = null;

    public query func isStripeConfigured() : async Bool {
        configuration != null;
    };

    public shared ({ caller }) func setStripeConfiguration(config : Stripe.StripeConfiguration) : async () {
        if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {
            Runtime.trap("未授权:仅管理员可执行此操作");
        };
        configuration := ?config;
    };

    func getStripeConfiguration() : Stripe.StripeConfiguration {
        switch (configuration) {
            case (null) { Runtime.trap("需先配置Stripe") };
            case (?value) { value };
        };
    };

    public func getStripeSessionStatus(sessionId : Text) : async Stripe.StripeSessionStatus {
        await Stripe.getSessionStatus(getStripeConfiguration(), sessionId, transform);
    };

    public shared ({ caller }) func createCheckoutSession(items : [Stripe.ShoppingItem], successUrl : Text, cancelUrl : Text) : async Text {
        await Stripe.createCheckoutSession(getStripeConfiguration(), caller, items, successUrl, cancelUrl, transform);
    };

    public query func transform(input : OutCall.TransformationInput) : async OutCall.TransformationOutput {
        OutCall.transform(input);
    };

    // 根据需要添加更多数据和功能
};

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.
Stripe支付集成步骤:
使用示例:
  1. 实现一个PaymentSetup组件,包含以下功能:
    • 调用
      isStripeConfigured()
      setStripeConfiguration()
      方法
    • 检查Stripe支付是否已配置
    • 若未配置,则打开管理面板,引导用户通过
      StripeConfiguration
      初始化Stripe
      • Stripe密钥
      • 允许的国家列表,格式为["US", "CA", "GB"]等,具体可参考Stripe文档
    • 已配置完成后请勿再显示支付设置界面!
  2. 实现结账钩子函数:
    • 注意需要解析后端
      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不可用');
                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解析非常重要!
                const session = JSON.parse(result) as CheckoutSession;
                if (!session?.url) {
                    throw new Error('Stripe会话缺少url字段');
                }
                return session;
            }
        });
    }
  3. 实现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会话缺少url字段');
    window.location.href = session.url;
  4. 分别实现PaymentSuccess和PaymentFailure组件,处理支付成功或失败的场景
  5. 将两个特定路径路由至对应的支付状态组件:
    • 路径"/payment-success"对应PaymentSuccess组件
    • 路径"/payment-failure"对应PaymentFailure组件 您需要使用@tanstack router来实现路由配置
  6. 管理视图需提供Stripe配置菜单。若尚未配置,需在管理员登录时引导其完成Stripe配置
补充说明:请确保商品图片能在商品画布中正确渲染并调整大小