mcpField
S
salla-addon-purchase-embedded

salla-addon-purchase-embedded

github.com/sallaapp开发281 次安装3 周前更新
代码仓库

关于 salla-addon-purchase-embedded

用于构建 Salla 嵌入式应用内附加项购买流程的技能。它通过嵌入式 SDK Checkout 模块列出附加项、打开 Salla 结账抽屉,并在服务端激活前显示待处理状态。

主要功能

  1. 01获取附加项及其标识
  2. 02打开原生结账抽屉
  3. 03监听结账结果
  4. 04等待基于 Webhook 的激活

适用场景

  1. 01在嵌入式应用中展示可购附加项
  2. 02使用返回的附加项 slug 发起结账
  3. 03付款后显示待处理状态
  4. 04在 3DS 跳转后恢复流程

MCP 相关

购买流程使用嵌入式 SDK Checkout 模块,而非 MCP 工具;本技能未描述结账相关的 MCP 操作。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/sallaapp/salla-partners-agent-kit --skill salla-addon-purchase-embedded

安装后在 Claude Code 中输入/salla-addon-purchase-embedded确认已加载。

详细介绍

Salla 插件购买 — 嵌入式 UX

购买插件在嵌入式应用内进行——原生结账抽屉会在 Salla Merchant Dashboard 中打开,而不是跳转到单独的页面或外部链接。此技能仅负责购买 UX:列出插件、打开结账抽屉、处理结果。支付后的所有事项——激活、权益、续订——均由 Webhook 驱动,并由 salla-addon-purchase 负责。

工具与 MCP

iframe 内购买使用的是嵌入式 SDK Checkout 调用,而不是 MCP 工具——此处不会运行任何 MCP 操作。

交接内容技能
SDK 初始化/握手/无导航栏salla-embedded-app
插件定义、激活 Webhook、权益salla-addon-purchase
定价基础要素salla-app-billing

前置条件 — 可正常运行的嵌入式应用

先通过 salla-embedded-app 构建嵌入式应用(SDK 已初始化、握手已完成、会话令牌已在服务端验证、无导航栏)。从此处开始,假设会话已通过身份验证并处于嵌入式环境中。

检查项:“嵌入式 SDK 是否已初始化,并且会话令牌已在服务端验证?”


Checkout 模块

购买入口是嵌入式 SDK 的 Checkout 模块——三个方法即可覆盖完整流程:

这三份 Checkout 模块文档是前端购买周期的权威依据。SDK 签名和载荷会因 @salla.sa/embedded-sdk 版本不同而有所变化——请对照文档或软件包确认,不要硬编码价格或 URL。插件的 slug 是你预先确定的标识符——发布时将其设为插件的 slug,并在订阅 Webhook 和 API 中将其作为 item_slug 进行匹配——因此直接引用自己的 slug 是预期做法。


步骤 1 — 列出应用的插件(getAddons)

插件在 Partners Portal 的应用发布表单的 Pricing 步骤中定义。 getAddons 会在运行时返回权威列表;宿主会缓存约 30 分钟,因此重复调用的开销很低。

来源:https://docs.salla.dev/embedded-sdk/modules/checkout/add-ons.md

import { embedded } from "@salla.sa/embedded-sdk";

// embedded.checkout.getAddons(): Promise<GetAddonsResult>
//   GetAddonsResult = { success: boolean; addons?: AddonInfo[]; error?: { code; message } }
//   AddonInfo = { slug; name; price; product_id; product_price_id }
async function loadAddons() {
  embedded.ui.loading.show();
  const result = await embedded.checkout.getAddons();
  embedded.ui.loading.hide();

  if (!result.success) {
    embedded.ui.toast.error(result.error?.message ?? "Could not load addons.");
    return;
  }

  // Render with Salla's design tokens (No-Chrome): name, what it unlocks, price.
  // Each "Buy" wires the addon's slug straight into startAddonPurchase (Step 2).
  result.addons!.forEach((addon) => {
    renderAddonCard({
      name: addon.name,
      price: addon.price,
      onBuy: () => startAddonPurchase(addon.slug),
    });
  });
}

getAddons() 遇到网络/超时错误时会正常返回(不会抛出异常)——务必处理 success: false。在 create() 中原样使用返回的 slug 值;与可用插件不匹配的 slug 会导致结账失败。

检查项:“Slug 和价格是否来自 getAddons(),而非硬编码?”


Step 2 — 打开结账界面(create)并监听结果(onResult)

create() 会在 Salla Dashboard 中打开原生支付抽屉。商家在 Salla 账单页面内完成付款 — 你无需收款。请先注册 onResult(在调用任何 create() 之前),以免错过响应,包括 3DS 重定向重新加载 iframe 后收到的结果。

来源: https://docs.salla.dev/embedded-sdk/modules/checkout/create.md · https://docs.salla.dev/embedded-sdk/modules/checkout/result.md

import { embedded } from "@salla.sa/embedded-sdk";

// Register the result listener once, during init — catches post-3DS-redirect results too.
// embedded.checkout.onResult(callback): () => void   (returns an unsubscribe fn)
//   CheckoutResult = {
//     success: boolean;
//     order_id?: string;
//     status: "paid" | "pending" | "failed" | "cancelled" | "success";
//     error?: { code; message };
//     context?: unknown;   // whatever you passed to create()'s config.context
//   }
const unsubscribe = embedded.checkout.onResult((result) => {
  if (result.context?.route) router.push(result.context.route); // restore state after 3DS
  if (result.success) {
    // Checkout completed — show PENDING, do NOT unlock here (Step 3).
    embedded.ui.toast.show("Finishing your purchase…");
  } else if (result.error) {
    embedded.ui.toast.error(result.error.message);
  }
});
// call unsubscribe() on unmount to avoid leaks.

// embedded.checkout.create(input, config?): void
//   input  = CheckoutItem | CheckoutItem[]
//   CheckoutItem = { type: "addon"; slug: string; quantity?: number /* default 1 */ }
//   config = { context?: unknown }   // persisted across 3DS redirects, returned in onResult
function startAddonPurchase(slug: string) {
  try {
    embedded.checkout.create(
      { type: "addon", slug },
      { context: { source: "embedded-upsell" } },
    );
  } catch (e) {
    // create() throws synchronously on invalid input
    // ("At least one item is required", "...must have a valid slug/type").
    embedded.ui.toast.error("Couldn't start checkout");
  }
}

对于商品篮,请传入一个数组 — create([{ type: "addon", slug, quantity: 2 }, …])。type 目前始终为 "addon"。使用 context 暂存路由/步骤,以便 iframe 在 3DS 后重新加载时恢复。

结账后,商家会返回 iframe;会话令牌可能已轮换。如果后端 API 调用返回 401,请调用 embedded.auth.refresh()(→ salla-embedded-app)并重新初始化。

检查项:“是否已在 create() 之前注册 onResult,并通过 create() 使用真实的 getAddons() slug 打开结账,而非使用硬编码 URL?”


Step 3 — 移交给 Webhook 以解锁功能

success 结果(或 status: "paid")表示结账已在 iframe 中完成 — 仅将其视为 UI 信号。功能通过激活 Webhook 解锁,该 Webhook 由 salla-addon-purchase 负责(它会处理 app.subscription.started、item_type: "addon",通过 item_slug 匹配,并进行签名验证和幂等处理 — 传输规则见 salla-webhooks)。

收到成功结果后,显示待处理状态,并等待激活完成:

// inside onResult, on result.success:
embedded.ui.toast.show("Finishing your purchase…");
// when salla-addon-purchase has activated the entitlement, reveal the feature:
embedded.ui.toast.success("Addon activated"); // after entitlement is persisted server-side

从经过验证的后端轮询/刷新权益;只有在权益持久化到服务器后才显示该功能,并在 Webhook 到达之前保持待处理状态。

检查项:“结果是否仅显示待处理状态?功能是否仅在 salla-addon-purchase 持久化权益后才显示?”


端到端(此技能负责上半部分)

[embedded page] getAddons() → render offer  (this skill)
      │  merchant clicks Buy
      ▼
checkout.create({ type:"addon", slug })  (this skill — opens native drawer)
      │  merchant pays in Salla billing  (3DS may redirect & reload the iframe)
      ▼
checkout.onResult(result) → pending state  (this skill; token may rotate → embedded.auth.refresh)
      ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
      ▼  (webhook — salla-addon-purchase owns this)
app.subscription.started (addon) → activate entitlement → reveal feature

危险信号

容易产生的想法为什么不对
“onResult 返回了 success,所以我现在就解锁插件。”客户端结果只能将 UI 切换到待处理状态 — 仅在通过签名验证的 app.subscription.started Webhook 上激活权益(item_type: "addon"),由 salla-addon-purchase 处理(步骤 2–3)。
“我要在嵌入页面中添加自己的银行卡表单/支付步骤。”账单由 Salla 负责。 使用 checkout.create({type:"addon", slug}) 打开原生抽屉 — 绝不自行收集或存储付款信息(结账模块/步骤 2)。
“我要通过已购插件的名称或数组索引进行匹配。”通过 item_slug 匹配 — 这是你为插件设置的稳定标识符 slug,每次生命周期事件都会收到它(步骤 3)。
“我要相信客户端结果来跟踪商家拥有的内容。”通过服务器核对权益(Webhook + 订阅 API),而非客户端 onResult — 令牌甚至可能在流程中途轮换(embedded.auth.refresh)(步骤 3)。

Key Resources

ResourceURL / Skill
Embedded SDK setup / No-Chromesalla-embedded-app skill
Checkout · Get Add-onshttps://docs.salla.dev/embedded-sdk/modules/checkout/add-ons.md
Checkout · Createhttps://docs.salla.dev/embedded-sdk/modules/checkout/create.md
Checkout · Subscribe Resulthttps://docs.salla.dev/embedded-sdk/modules/checkout/result.md
Activation + entitlement lifecyclesalla-addon-purchase skill
Pricing / definitionsalla-app-billing skill
@salla.sa/embedded-sdknpm package (Checkout module authority)
Partners Portalhttps://salla.partners

评论

支持 Markdown