@shopkit/app-shell
v2.0.1
Published
App shell components for Shopkit storefronts
Readme
@shopkit/app-shell
App shell components for Shopkit storefronts. Provides master wrapper, analytics, toast notifications, head scripts, and navigation progress indicator.
Installation
bun add @shopkit/app-shellPeer Dependencies
{
"react": "^18.0.0",
"react-dom": "^18.0.0",
"next": ">=14.0.0",
"next-intl": ">=4.0.0",
"@shopkit/core": "^0.1.0",
"@shopkit/asset-cache": "^1.0.0"
}Quick Start
// layout.tsx
import { AppShell, AppHead, AppAnalytics } from "@shopkit/app-shell";
export default async function RootLayout({ children }) {
const theme = await getTheme();
const messages = await getMessages();
return (
<html>
<head>
<AppHead />
</head>
<body>
<AppShell config={{ merchantName: "my-store", locale: "en", messages, theme }}>
{children}
</AppShell>
<AppAnalytics />
</body>
</html>
);
}Components
AppShell
Master provider wrapper that combines all required providers into a single component.
import { AppShell } from "@shopkit/app-shell";
<AppShell
config={{
merchantName: "my-store",
locale: "en",
messages: { /* i18n messages */ },
theme: { /* theme object */ },
}}
>
{children}
</AppShell>Props:
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| config.merchantName | string | Yes | Merchant identifier |
| config.locale | string | Yes | Current locale (e.g., "en", "hi") |
| config.messages | Record<string, unknown> | Yes | i18n messages |
| config.theme | Theme \| null | Yes | Theme from @shopkit/core |
| config.analytics | AnalyticsConfig | No | Analytics configuration |
| config.fonts | FontConfig | No | Font configuration |
| config.experiments | ExperimentConfig | No | A/B testing config |
Includes:
AppRouteLoader- Navigation progress barNextIntlClientProvider- i18n providerThemeProvider- Theme managementAppToast+AppToastContainer- Toast notificationsAuthProvider- Authentication contextAssetCacheInitializer- Asset cachingAppAnalyticsInit- Analytics initialization
AppHead
Head scripts and resource hints for optimal performance.
import { AppHead } from "@shopkit/app-shell";
<head>
<AppHead
config={{
abTesting: {
prtConfigUrl: "https://...",
prtAbUrl: "https://...",
},
prefetch: {
dnsPrefetch: ["https://cdn.example.com"],
preloadScripts: ["https://example.com/script.js"],
},
}}
/>
</head>Props:
| Prop | Type | Description |
|------|------|-------------|
| config.abTesting.prtConfigUrl | string | A/B testing config URL |
| config.abTesting.prtAbUrl | string | A/B testing script URL |
| config.prefetch.dnsPrefetch | string[] | Domains to DNS prefetch |
| config.prefetch.preloadScripts | string[] | Scripts to preload |
| config.clarity.projectId | string | Microsoft Clarity ID |
Environment Variables:
NEXT_PUBLIC_PRT_CONFIG_URLNEXT_PUBLIC_PRT_AB_URLNEXT_PUBLIC_CLARITY_ID
AppAnalytics
Body analytics scripts for GA, PostHog, and Facebook Pixel.
import { AppAnalytics } from "@shopkit/app-shell";
<body>
{/* ... content ... */}
<AppAnalytics
config={{
googleAnalytics: { measurementId: "G-XXXXX" },
posthog: { apiKey: "phc_XXXXX", apiHost: "https://..." },
facebookPixel: { pixelId: "XXXXX" },
}}
/>
</body>Environment Variables (used as defaults):
NEXT_PUBLIC_GA_ID- Google Analytics measurement IDNEXT_PUBLIC_GOOGLE_ADS_ID- Google Ads conversion IDNEXT_PUBLIC_POSTHOG_KEY- PostHog API keyNEXT_PUBLIC_POSTHOG_HOST- PostHog API hostNEXT_PUBLIC_PIXEL_ID- Facebook Pixel ID
AppProductTracker
Product detail page (PDP) view tracking with engagement metrics.
import { AppProductTracker } from "@shopkit/app-shell";
<AppProductTracker
productData={{
id: "variant-123",
title: "Amazing Product",
price: 999.99,
currency: "INR",
category: "Electronics",
brand: "MyBrand",
}}
dwellTimeSeconds={20}
disableDwellTracking={false}
/>Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| productData.id | string | Required | Product/variant ID |
| productData.title | string | Required | Product title |
| productData.price | number | Required | Product price |
| productData.currency | string | "INR" | Currency code |
| productData.category | string | - | Product category |
| productData.brand | string | - | Brand name |
| dwellTimeSeconds | number | 20 | Seconds before firing engagement event |
| disableDwellTracking | boolean | false | Disable dwell tracking |
Events Fired:
view_item- Immediately on mountviewed_product- After dwell time (engagement signal)
Toast System
AppToast (Provider)
import { AppToast, AppToastContainer } from "@shopkit/app-shell";
<AppToast>
{children}
<AppToastContainer />
</AppToast>useToast (Hook)
import { useToast } from "@shopkit/app-shell";
function MyComponent() {
const { addToast, removeToast, clearToasts, toasts } = useToast();
const showSuccess = () => {
addToast({
type: "success",
title: "Success!",
message: "Item added to cart",
duration: 3000, // optional, defaults to 3000ms
});
};
return <button onClick={showSuccess}>Add to Cart</button>;
}Toast Types:
success- Green success messageerror- Red error messagewarning- Amber warning messageinfo- Blue info message
Toast Containers:
AppToastContainer- Bottom-right, minimal styleAppToastContainerV1- Top-right, with icons
AppRouteLoader
Navigation progress indicator that shows during page transitions.
import { AppRouteLoader } from "@shopkit/app-shell";
// Usually included in AppShell, but can be used standalone
<AppRouteLoader />Features:
- Animated progress bar (0-90% during load)
- Completes to 100% on navigation finish
- Ignores external links, button clicks, modifier keys
- Handles browser back/forward navigation
- 5-second timeout fallback
Apps Platform — storefront integration
The Apps Platform mounts third-party apps inside sandboxed iframes embedded in your storefront. @shopkit/app-shell exports the React surface for it:
| Symbol | Role |
|---|---|
| <AppsPlatformProvider> | Surface-agnostic root. Wraps the layout once and holds RPC handlers, shell URL, audit + error sinks. |
| <EmbeddedApps> | Embed surface. One iframe per app matching a position (e.g. footer banner). |
| <AppBlockSlot> | Block surface. Multi-handle dispatch per (handle, pageType) (e.g. rating-stars on PDP). |
| <AppAccountSlot> | Account-extension surface. Multi-slot dispatch per slot (e.g. account.profile). |
Peer dep: @shopkit/apps-platform >=0.1.0. Manifest types accept either the narrow HostAdapterManifest (from apps-platform) or the full AppManifest (from @shopkit/apps-manifest) — structural typing handles both.
1. Set up the provider once at the layout root
// layout.tsx
import { AppsPlatformProvider } from "@shopkit/app-shell";
export default async function RootLayout({ children }) {
return (
<AppsPlatformProvider
shellUrl="https://apps.ratio.win/sandbox.html"
rpcHandlers={{
"cart.read": () => cartStore.read(),
"customer.read": () => customerStore.read(),
"product.read": (args) => productStore.read((args as { id: string }).id),
"order.list": () => orderStore.list(),
}}
audit={(event) => analytics.rpcAudit(event)}
onError={(err, ctx) => Sentry.captureException(err, { extra: ctx })}
>
<AppShell>{children}</AppShell>
</AppsPlatformProvider>
);
}Deprecated alias:
EmbeddedAppsProvideris a re-export ofAppsPlatformProviderfrom when only the Embed surface existed. Storefronts that imported the old name still work; new code should useAppsPlatformProvider.
2. Mount surface widgets where they belong
Embed — <EmbeddedApps position="...">
import { EmbeddedApps } from "@shopkit/app-shell";
export default async function ProductPage({ params }) {
const apps = await getInstalledEmbedApps();
return (
<>
<ProductDetails />
<EmbeddedApps
position="pdp-below-description"
apps={apps}
pageContext={{ productHandle: params.handle }}
/>
</>
);
}position matches the manifest.surfaces[embed].position field (hoisted to InstalledEmbedApp.position by your resolver). Apps with mount: "lazy" defer via requestIdleCallback.
Block — <AppBlockSlot handle pageType>
import { AppBlockSlot } from "@shopkit/app-shell";
export default async function ProductPage({ params }) {
const apps = await getInstalledBlockApps();
return (
<>
<AppBlockSlot
handle="rating-stars"
pageType="product"
apps={apps}
pageContext={{ productHandle: params.handle }}
/>
<ProductDetails />
<AppBlockSlot handle="reviews-feed" pageType="product" apps={apps} />
</>
);
}Filters apps whose manifest has surfaces[block].blocks[] containing a block where handle === <prop> AND pages[] includes pageType. A single Block app can declare N handles in one manifest; the bundle dispatches on sdk.init.block.handle.
Account — <AppAccountSlot slot>
import { AppAccountSlot } from "@shopkit/app-shell";
export default async function ProfilePage() {
const apps = await getInstalledAccountApps();
return (
<main>
<AppAccountSlot
slot="account.profile"
apps={apps}
pageContext={{ customerId: session.customerId }}
/>
</main>
);
}Filters apps whose manifest has an account-extension surface entry where slot === <prop>. One Account app can declare N slots — each as its own surface entry in the manifest — and the bundle dispatches on sdk.init.account.slot.
3. RPC handlers
Each handler in rpcHandlers resolves one capability-scoped method an installed app may call. The handler signature: (args: unknown) => unknown | Promise<unknown>. Throw inside the handler to surface as rpc:result { ok: false } to the app; the runtime adds capability validation before your handler runs.
The methods registered in @shopkit/apps-capabilities/methods.ts are the source of truth — you only need to implement handlers for methods your installed apps actually request:
| Method | Capability | Surface allowlist |
|---|---|---|
| cart.read | cart:read | embed, block, checkout-extension |
| cart.addItem / removeItem / updateQty | cart:write | block |
| customer.read | customer:read | embed, block, account-extension, checkout-extension, admin-extension |
| customer.update | customer:write | account-extension |
| product.read | product:read | embed, block, admin-extension |
| order.read / order.list | order:read | account-extension, admin-extension |
| address.read | address:read | checkout-extension |
| events.subscribe / publish | events:subscribe / publish | embed, block |
| settings.read / write | settings:read / write | admin-extension |
4. Install resolvers
Two patterns shipped in the monorepo, both legitimate:
Simple (workspace-import + in-process JWT mint) — apps/storefront-starter/src/integrations/apps-platform/. Manifests imported from workspace packages, hardcoded list, HS256 JWT signed in-process via a shared module. Pages with ISR (revalidate = N) stay static — no HTTP roundtrip during render. Best for: getting started, demos, ISR-heavy storefronts.
Production-shape (mock backend + HTTP mint) — apps-ratio/apps-platform-example/src/integrations/apps-platform/. Next.js API routes serve the install list (/api/v1/apps/installed) and mint sessions (/api/v1/session/mint). Resolvers fetch(...) per render with cache: "no-store". Forces dynamic rendering, exercises the full Layer-1 contract end-to-end. Best for: validating the wire protocol, integration testing.
Both produce the same InstalledEmbedApp[] / InstalledBlockApp[] / InstalledAccountApp[] shape that the slot widgets consume — the choice is a render-strategy tradeoff, not a contract difference.
5. What the runtime gives you
- Mount lifecycle: widgets diff
appsvs currently-mounted slots on every render. New → create iframe + handshake. Dropped →adapter.unmount()+container.remove(). Existing → leave alone. - Sandbox isolation: every iframe gets
sandbox="allow-scripts"(noallow-same-origin). Apps run in their own opaque-origin process. - Per-instance capability scoping: the host validates each RPC against the SCOPE of the placement. A block declaring
["product:read"]cannot callcart.readeven if a sibling block in the same app can. - Mount key includes the placement:
<AppBlockSlot>keys mounts as${handle}:${appId},<AppAccountSlot>as${slot}:${appId}. Changing the prop force-remounts — the adapter bakes the placement into its validator at construction.
For the wire protocol, capability catalog, and host-side details, see @shopkit/apps-platform.
Type Exports
import type {
// AppShell
AppShellConfig,
AppShellProps,
AnalyticsConfig,
FontConfig,
ExperimentConfig,
// AppHead
AppHeadConfig,
AppHeadProps,
ABTestingConfig,
PrefetchConfig,
ClarityConfig,
// AppAnalytics
AppAnalyticsConfig,
AppAnalyticsProps,
GoogleAnalyticsConfig,
PostHogConfig,
FacebookPixelConfig,
TrackingContext,
AppAnalyticsInitConfig,
AppAnalyticsInitProps,
TrackedProductData,
AppProductTrackerProps,
// AppToast
Toast,
ToastContextType,
AppToastProps,
// Apps Platform — provider
AppsPlatformProviderProps,
EmbeddedAppsContextValue, // surface-agnostic config carried by the provider
// Apps Platform — widget props + installed-app types
EmbeddedAppsProps,
InstalledEmbedApp,
AppBlockSlotProps,
InstalledBlockApp,
HostAdapterBlockDecl, // re-exported from @shopkit/apps-platform
AppAccountSlotProps,
InstalledAccountApp,
} from "@shopkit/app-shell";Deprecated Phase 1 aliases are also exported for back-compat: EmbeddedAppsProvider (= AppsPlatformProvider), useEmbeddedAppsContext (= useAppsPlatformContext), EmbeddedAppsProviderProps (= AppsPlatformProviderProps).
Environment Variables Reference
# Google Analytics
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GOOGLE_ADS_ID=AW-XXXXXXXXXX
# Facebook Pixel
NEXT_PUBLIC_PIXEL_ID=XXXXXXXXXX
# PostHog
NEXT_PUBLIC_POSTHOG_KEY=phc_XXXXXXXXXX
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
# A/B Testing
NEXT_PUBLIC_PRT_CONFIG_URL=https://...
NEXT_PUBLIC_PRT_AB_URL=https://...
# Microsoft Clarity
NEXT_PUBLIC_CLARITY_ID=XXXXXXXXXX
# Shopify Analytics
NEXT_PUBLIC_SHOPIFY_SHOP_ID=XXXXX
NEXT_PUBLIC_SHOPIFY_STORE_ANALYTIC_DOMAIN=example.myshopify.comTesting
bun run test # Run tests
bun run test:watch # Watch mode
bun run test:coverage # With coverageDevelopment
bun run dev # Watch mode for development
bun run build # Build the package
bun run typecheck # Type check
bun run clean # Clean dist folderLicense
MIT
