@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-kitInitialize 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:
- Request an app client token from the host with
AppClientToken.request(). - Receive that token through an
AppClientToken.Action.RESPONSEsubscription. - Resolve the installation's store ID and app ID from the client-token context.
- Call the
storeAppTokenmutation athttps://graph.soppiya.comwith the store ID, app ID, and your app secret. - Dispatch the returned
storeAppTokento the embedded Soppiya Admin host usingAppStoreToken.save(). - 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.NavigationMenuandAppLink— define app navigation links in the host.ContextualSaveBar— present an admin-managed unsaved-changes interface.AppClientTokenandAppStoreToken— 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);