npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@soppiya/app-kit

v0.3.1

Published

Source-style reconstruction of @soppiya/app-kit from its published build artifacts.

Readme

@soppiya/app-kit

@soppiya/app-kit is a framework-agnostic JavaScript library for building embedded third-party apps in Soppiya Admin. It lets an app exchange actions with the Soppiya host to access native admin capabilities such as resource selection, navigation, menus, save bars, and tokens.

It works with plain JavaScript, React, Vue, Angular, Svelte, or any frontend that runs in the browser.

Install

npm install @soppiya/app-kit

Initialize the app

Create one App Kit client when your embedded app starts. Soppiya provides the handle and URL-safe, Base64-encoded host parameters when it loads your app.

If your app run on admin.soppiya.com dashboard you will get app handle and host from admin dashboard

import { createApp } from "@soppiya/app-kit";

const params = new URLSearchParams(window.location.search);
const handle = params.get("handle");
const host = params.get("host");
const app = createApp({ handle, host });

If you not ensure that your app always run in soppiya admin dashboard you have to manually add handle and host. This will automatically redirect to the admin dashboard if someone direct browse in your app url. Or if you don't want to redirect automatically set forceRedirect: false;

import { createApp } from "@soppiya/app-kit";

const handle = params.get("handle");
const host = params.get("host");
const app = createApp({ handle: "your app handle name", host: btoa("admin.soppiya.com"), forceRedirect: true | false });

Session tokens and access to store data

Before using actions that require store data, including ResourcePicker, the app must give Soppiya Admin a store app token. The complete flow is:

  1. Request an app client token from the host with AppClientToken.request().
  2. Receive that token through an AppClientToken.Action.RESPONSE subscription.
  3. Resolve the installation's store ID and app ID from the client-token context.
  4. Call the storeAppToken mutation at https://graph.soppiya.com with the store ID, app ID, and your app secret.
  5. Dispatch the returned storeAppToken to the embedded Soppiya Admin host using AppStoreToken.save().
  6. Open resource pickers or make other host actions that need store access.

The app client token establishes the current embedded-app context. The store app token returned by the mutation is the token that Soppiya Admin uses for store-resource and server actions.

Request the app client token

Subscribe before dispatching the request, so the response cannot be missed:

import { AppClientToken, AppStoreToken } from "@soppiya/app-kit/actions";

const unsubscribe = app.subscribe(AppClientToken.Action.RESPONSE, async (payload) => {
  if (typeof payload !== "object" || payload === null || typeof payload.token !== "string") {
    throw new Error("Soppiya Admin returned an invalid app client token.");
  }

  // Obtain a store app token using the app client token's installation context.
  const storeAppToken = await getStoreAppToken(payload.token);

  // Sends the token to the Soppiya Admin host configured when createApp() ran.
  app.dispatch(AppStoreToken.save({ token: storeAppToken }));
});

app.dispatch(AppClientToken.request());

// Call this when the app is torn down.
// unsubscribe();

Create the store app token

The GraphQL mutation is:

mutation StoreAppToken($store: ID!, $appId: String!, $appSecret: String!) {
  storeAppToken(store: $store, app_id: $appId, app_secret: $appSecret)
}

Use the store ID and app ID for the current installation. The app client token provides the app's authenticated context; resolve these identifiers from that context in your application. Never trust arbitrary values supplied by the browser.

Your app secret must remain on your server. A browser bundle can be inspected by every user, so calling graph.soppiya.com with an app secret directly from frontend code would expose it. Instead, send the client token to your backend and have the backend perform the GraphQL mutation.

// Browser code: call your own backend, not graph.soppiya.com directly.
async function getStoreAppToken(appClientToken) {
  const response = await fetch("/api/soppiya/store-app-token", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${appClientToken}`,
    },
  });

  if (!response.ok) {
    throw new Error("Unable to create a store app token.");
  }

  const { storeAppToken } = await response.json();
  if (typeof storeAppToken !== "string") {
    throw new Error("The server returned an invalid store app token.");
  }

  return storeAppToken;
}

On the backend, validate the app client token, determine the associated storeId and appId, then call Soppiya Graph with the server-only secret:

const STORE_APP_TOKEN_MUTATION = `
  mutation StoreAppToken($store: ID!, $appId: String!, $appSecret: String!) {
    storeAppToken(store: $store, app_id: $appId, app_secret: $appSecret)
  }
`;

async function createStoreAppToken({ storeId, appId }) {
  const response = await fetch("https://graph.soppiya.com", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      query: STORE_APP_TOKEN_MUTATION,
      variables: {
        store: storeId,
        appId,
        appSecret: process.env.SOPPIYA_APP_SECRET,
      },
    }),
  });

  const { data, errors } = await response.json();
  if (!response.ok || errors?.length || typeof data?.storeAppToken !== "string") {
    throw new Error("Soppiya Graph could not create a store app token.");
  }

  return data.storeAppToken;
}

Do not make calls that depend on store data until AppStoreToken.save({ token }) has been dispatched. App Kit delivers this action to the configured Soppiya Admin host; you do not need to manually call admin.soppiya.com.

Make your first App Bridge call

The following example uses ResourcePicker to open a Soppiya Admin UI that lets users browse, search, and select products from their store.

import { ResourcePicker } from "@soppiya/app-kit/actions";

const picker = ResourcePicker.create(app, {
  resourceType: ResourcePicker.ResourceType.Product,
});

picker.subscribe(ResourcePicker.Action.SELECT, (payload) => {
  console.log(payload.selection);
});

picker.dispatch(ResourcePicker.Action.OPEN);

The picker is created with the shared app client, listens for the SELECT event, then dispatches OPEN to display the host UI.

Always keep the unsubscribe function if the listener may no longer be needed:

const unsubscribe = picker.subscribe(ResourcePicker.Action.CANCEL, () => {
  console.log("Picker closed without a selection.");
});

// Call this during cleanup.
unsubscribe();

App client API

The app object returned from createApp is the connection to Soppiya Admin.

| Method | Purpose | | ------------------------------------ | -------------------------------------------------------------------- | | app.dispatch(action) | Send an action to the host. | | app.subscribe(eventName, listener) | Listen for an action from the host; returns an unsubscribe function. | | app.getState(path?) | Read host state. Supports dot paths such as "context.store". | | app.error(listener) | Listen for App Kit error actions. |

For example:

const context = await app.getState("context");
const store = await app.getState("context.store");
const stopListening = app.error((error) => {
  console.error("App Kit error:", error);
});

Available actions

Import actions from @soppiya/app-kit/actions.

import {
  AppClientToken,
  AppLink,
  AppStoreToken,
  ContextualSaveBar,
  NavigationMenu,
  Redirect,
  ResourcePicker,
} from "@soppiya/app-kit/actions";

The package currently includes:

  • ResourcePicker — choose store resources through Soppiya Admin.
  • Redirect — request navigation managed by the admin host.
  • NavigationMenu and AppLink — define app navigation links in the host.
  • ContextualSaveBar — present an admin-managed unsaved-changes interface.
  • AppClientToken and AppStoreToken — request and store app authentication tokens.

Direct action imports are also available, for example:

import * as ResourcePicker from "@soppiya/app-kit/actions/ResourcePicker";

Navigate with the admin host

Use Redirect when navigation should be coordinated with Soppiya Admin:

import { Redirect } from "@soppiya/app-kit/actions";

const redirect = Redirect.create(app);
redirect.dispatch(Redirect.Action.ADMIN_PATH, {
  path: "/dashboard",
});

Add an app navigation menu

import { AppLink, NavigationMenu } from "@soppiya/app-kit/actions";

const dashboardLink = new AppLink.AppLink(app, {
  label: "Dashboard",
  destination: "/dashboard",
});

const menu = NavigationMenu.create(app, {
  items: [dashboardLink],
});

menu.dispatch(NavigationMenu.Action.UPDATE);

Display a contextual save bar

Use the save bar whenever a user has changes that have not yet been saved.

import { ContextualSaveBar } from "@soppiya/app-kit/actions";

const saveBar = ContextualSaveBar.create(app);

saveBar.subscribe(ContextualSaveBar.Action.DISCARD, () => {
  // Restore the last saved state.
});

saveBar.dispatch(ContextualSaveBar.Action.SHOW);