mcpField
S
salla-addon-purchase-embedded

salla-addon-purchase-embedded

github.com/sallaappDevelopment281 installsUpdated 3w ago
Repository

About salla-addon-purchase-embedded

A skill for building the add-on purchase flow inside an embedded Salla app. It uses the embedded SDK Checkout module to list add-ons, open Salla’s checkout drawer, and show a pending result until server-side activation.

Key features

  1. 01Fetch add-ons and their identifiers
  2. 02Open the native checkout drawer
  3. 03Listen for checkout results
  4. 04Wait for webhook-based activation

Use cases

  1. 01Render available add-ons in an embedded app
  2. 02Start checkout using a returned add-on slug
  3. 03Show a pending state after payment
  4. 04Restore the flow after a 3DS redirect

MCP support

The purchase flow uses the embedded SDK Checkout module, not an MCP tool; this skill describes no MCP action for checkout.

Install

Copy the command and run it in a terminal.

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

After installing, type/salla-addon-purchase-embeddedin Claude Code to confirm it loaded.

Details

From the project README

Salla Addon Purchase — Embedded UX

Buying an addon happens inside the embedded app — a native checkout drawer opens in the Salla Merchant Dashboard, not a separate page or external redirect. This skill owns only the purchase UX: list the add-ons, open the checkout, react to the result. Everything after payment — activation, entitlement, renewal — is webhook-driven and owned by salla-addon-purchase.

Tools & MCPs

The in-iframe purchase is an embedded-SDK Checkout call, not an MCP tool — no MCP action runs here.

Hand-offSkill
SDK init / handshake / No-Chromesalla-embedded-app
Addon definition, activation webhook, entitlementsalla-addon-purchase
Pricing primitivessalla-app-billing

Prerequisite — a working embedded app

Build the embedded app first via salla-embedded-app (SDK initialized, handshake done, session token verified server-side, No-Chrome). From here on, assume an authenticated embedded session.

Gate: "Embedded SDK initialized and the session token verified server-side?"


The Checkout module

The purchase entry point is the embedded SDK Checkout module — three methods cover the full flow:

These three Checkout-module docs are the source of truth for the frontend purchase cycle. SDK signatures and payloads are version-dependent across @salla.sa/embedded-sdk releases — confirm them against the docs or the package, and don't hardcode prices or URLs. The addon slug is YOUR pre-known identifier — you set it as the addon's slug at publish and match it as item_slug in the subscription webhooks + API — so referencing your own slug directly is expected.


Step 1 — List the app's add-ons (getAddons)

Add-ons are defined in the Pricing step of the App Publish form in the Partners Portal. getAddons returns the authoritative list at runtime; the host caches it for ~30 minutes, so repeated calls are cheap.

Source: 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() resolves (never throws) on network/timeout errors — always handle success: false. Use the returned slug values verbatim in create(); a slug that doesn't match an available addon fails checkout.

Gate: "Slugs and prices come from getAddons(), not hardcoded?"


Step 2 — Open the checkout (create) and hear the result (onResult)

create() opens the native payment drawer in the Salla Dashboard. The merchant pays inside Salla billing — you never collect payment. Register onResult first (before any create() call) so you don't miss the response, including results delivered after a 3DS redirect reloads the iframe.

Sources: 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");
  }
}

For a basket, pass an array — create([{ type: "addon", slug, quantity: 2 }, …]). type is currently always "addon". Use context to stash the route/step so you can restore it when the iframe reloads after 3DS.

After checkout the merchant is back in the iframe; the session token may have rotated. If a backend API call returns 401, call embedded.auth.refresh() (→ salla-embedded-app) and re-bootstrap.

Gate: "onResult registered before create(), and checkout opened via create() with a real getAddons() slug — not a hardcoded URL?"


Step 3 — Hand off to the webhook to unlock

A success result (or status: "paid") means checkout completed in the iframe — treat it as a UI signal only. The feature unlocks via the activation webhook, owned by salla-addon-purchase (it processes app.subscription.started, item_type: "addon", matched by item_slug, signature-verified and idempotent — transport rules in salla-webhooks).

On a successful result, show a pending state and let the activation flip it:

// 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

Poll/refresh entitlements from your verified backend; reveal the feature once the entitlement is persisted server-side, and keep the pending state until the webhook lands.

Gate: "Result shows pending only; the feature reveals only after salla-addon-purchase persists the entitlement?"


End-to-End (this skill owns the top half)

[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

Red Flags

Tempting thoughtWhy it's wrong
"onResult returned success, so I'll unlock the addon now."The client result only moves the UI to a pending state — activate the entitlement only on the signature-verified app.subscription.started webhook (item_type: "addon"), via salla-addon-purchase (Steps 2–3).
"I'll add my own card form / payment step in the embedded page."Salla owns billing. Open the native drawer with checkout.create({type:"addon", slug}) — never collect or store payment yourself (The Checkout module / Step 2).
"I'll match the purchased addon by its name or array index."Match by item_slug — the stable identifier you set as the addon's slug and receive on every lifecycle event (Step 3).
"I'll trust the client result to track what the merchant owns."Reconcile entitlement against the server (the webhook + the subscriptions API), not the client onResult — the token may even rotate mid-flow (embedded.auth.refresh) (Step 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

Comments

Markdown supported