@proveanything/smartlinks
Version:
Official JavaScript/TypeScript SDK for the Smartlinks API
265 lines (197 loc) • 11.2 kB
Markdown
# SmartLinks Containers
> **Copy this file into `node_modules/@proveanything/smartlinks/docs/containers.md`** in the published SDK package.
Containers are the **full public app experience** packaged as an embeddable React component. Unlike widgets (lightweight previews/cards), containers render the complete public interface — all pages, routing, and features — inside a parent React application.
## Widgets vs Containers
| Aspect | Widget | Container |
| --------------- | ---------------------------------- | -------------------------------------------- |
| **Purpose** | Lightweight preview / summary | Full app experience |
| **Bundle size** | ~10KB (app-specific code only) | ~150KB+ (full public app) |
| **Loading** | Loaded immediately with page | Lazy-loaded on demand |
| **Routing** | None (single component) | MemoryRouter (parent owns URL bar) |
| **Use case** | Cards, thumbnails, quick glance | "Open full view", embedded experiences |
| **Build output**| `widgets.umd.js` / `widgets.es.js` | `containers.umd.js` / `containers.es.js` |
### Why Separate Bundles?
Imagine a homepage displaying 10 app widgets. If containers were bundled with widgets, loading 10 small widget cards would also pull in 10 full app bundles — potentially megabytes of unused code. By splitting them:
1. **Widgets load fast** — parent loads `widgets.*.js` for all apps on the page (~10KB each)
2. **Containers load on demand** — only when user clicks "Open full view" does `containers.*.js` get fetched
## Container Props
Container props extend the standard `SmartLinksWidgetProps` with an additional `className` prop:
```typescript
interface SmartLinksContainerProps extends SmartLinksWidgetProps {
/** Optional className for the container wrapper element */
className?: string;
}
```
All standard widget props apply:
| Prop | Type | Required | Description |
| ----------------- | -------------- | -------- | --------------------------------------- |
| `collectionId` | string | ✅ | Collection context |
| `appId` | string | ✅ | App identifier |
| `SL` | SmartLinks SDK | ✅ | Pre-initialized SDK instance |
| `productId` | string | ❌ | Product context |
| `proofId` | string | ❌ | Proof context |
| `user` | object | ❌ | Current user info |
| `onNavigate` | function | ❌ | Navigation callback (accepts `NavigationRequest` or legacy string) |
| `size` | string | ❌ | `"compact"`, `"standard"`, or `"large"` |
| `lang` | string | ❌ | Language code (e.g., `"en"`) |
| `translations` | object | ❌ | Translation overrides |
| `className` | string | ❌ | CSS class for the wrapper element |
## Cross-App Navigation
Containers support the same **structured navigation requests** as widgets. When a container needs to navigate to another app within the portal, it emits a `NavigationRequest` via the `onNavigate` callback. The portal orchestrator interprets the request, preserves hierarchy context, and performs the navigation.
### NavigationRequest
```typescript
interface NavigationRequest {
/** Target app ID to activate */
appId: string;
/** Deep link / page within the target app (forwarded as pageId) */
deepLink?: string;
/** Extra params forwarded to the target app */
params?: Record<string, string>;
/** Optionally switch to a specific product before showing the app */
productId?: string;
/** Optionally switch to a specific proof before showing the app */
proofId?: string;
}
```
### Usage in Containers
```typescript
// Inside a container component
const handleNavigateToWarranty = () => {
onNavigate?.({
appId: 'warranty-app',
deepLink: 'register',
params: { source: 'product-page' },
});
};
// Switch product context and open an app
const handleViewRelatedProduct = (productId: string) => {
onNavigate?.({
appId: 'product-info',
productId,
});
};
```
### How It Works
```text
Container button "Register Warranty"
→ onNavigate({ appId: 'warranty', deepLink: 'register', params: { ref: 'container' } })
→ Portal orchestrator receives NavigationRequest
→ Calls actions.navigateToApp('warranty', 'register')
→ Target app loads with extraParams: { pageId: 'register', ref: 'container' }
→ collectionId, productId, proofId, theme all preserved automatically
```
### Backward Compatibility
The `onNavigate` callback accepts both structured `NavigationRequest` objects and legacy strings. Existing containers that call `onNavigate('/some-path')` continue to work. **New containers should always use the structured `NavigationRequest` format.**
See `widgets.md` for the full `NavigationRequest` documentation and additional examples.
## Architecture
Containers use **MemoryRouter** (not HashRouter) because the parent app owns the browser's URL bar. Context is passed via props rather than URL parameters. Each container gets its own `QueryClient` to avoid cache collisions with the parent app.
```
Parent App (owns URL bar, provides globals)
└── <PublicContainer> ← Container component
├── QueryClientProvider (isolated)
├── MemoryRouter (internal routing)
├── LanguageProvider
└── PublicPage (+ all sub-routes)
```
## Usage in Parent Apps
### ESM (Modern Bundlers)
```typescript
// Lazy-load the container only when needed
const { PublicContainer } = await import('https://my-app.com/containers.es.js');
<PublicContainer
collectionId="abc"
appId="my-app"
productId="prod-123"
SL={SL}
onNavigate={(request) => {
// The portal orchestrator handles NavigationRequest objects
// automatically when using ContentOrchestrator / OrchestratedPortal
}}
lang="en"
className="max-w-4xl mx-auto"
/>
```
### UMD (Script Tag)
```html
<!-- Ensure shared globals are set up first (see Shared Dependencies Contract) -->
<script src="https://my-app.com/containers.umd.js"></script>
<script>
const { PublicContainer } = window.SmartLinksContainers;
// Render with React
</script>
```
## Shared Dependencies Contract
Containers use the **exact same Shared Dependencies Contract** as widgets. No additional globals are needed. The parent app must expose these globals before loading container bundles:
- React, ReactDOM, jsxRuntime
- SL (SmartLinks SDK)
- CVA (class-variance-authority) — **uppercase to avoid `cva.cva` collision**
- ReactRouterDOM, ReactQuery
- LucideReact, dateFns, LiquidJS
- 12 Radix UI primitives (Slot, Dialog, Popover, Tooltip, Tabs, Accordion, Select, ScrollArea, Label, Toast, Progress, Avatar)
See `widgets.md` for the complete table with globals and version expectations.
> **Why `CVA` not `cva`?** The `class-variance-authority` package exports a named function called `cva`. If the UMD global is also `cva`, the wrapper resolves it as `window.cva.cva` — a double-nesting bug. Using uppercase `CVA` avoids this collision.
## Build Configuration
Containers are built via a dedicated Vite config: `vite.config.container.ts`.
### Enable Container Builds
Add to your `.env` file:
```
VITE_ENABLE_CONTAINERS=true
```
### Build Command
```bash
# Full pipeline (main + widgets + containers)
vite build && vite build --config vite.config.widget.ts && vite build --config vite.config.container.ts
# Containers only
vite build --config vite.config.container.ts
```
### Build Output
```text
dist/
├── containers.umd.js # Full app container (UMD)
├── containers.es.js # Full app container (ESM)
└── containers.css # Container styles
```
When `VITE_ENABLE_CONTAINERS` is not set, the build produces a harmless `containers.stub.js` and skips quickly.
## File Structure
```
src/containers/
├── index.ts # Main exports barrel + ContainerManifest
├── types.ts # SmartLinksContainerProps interface
├── stub.ts # Minimal stub for skipped builds
└── PublicContainer.tsx # Full public app wrapper
```
### Creating a New Container
1. Create your container component in `src/containers/MyContainer.tsx`
2. Export it from `src/containers/index.ts`
3. Add it to the `CONTAINER_MANIFEST` in `src/containers/index.ts`
4. Ensure it uses `MemoryRouter` (not HashRouter)
5. Give it its own `QueryClient` to avoid cache collisions
## Key Differences from Iframe Apps
| Concern | Iframe App | Container |
| ----------------- | --------------------------------- | ---------------------------------- |
| **Isolation** | Full browser isolation | Shares parent's React tree |
| **URL control** | Owns its own URL (HashRouter) | Parent owns URL (MemoryRouter) |
| **Context source**| URL parameters | React props |
| **Styling** | Fully isolated CSS | Inherits parent CSS variables |
| **Communication** | postMessage | Direct props / callbacks |
| **Auth** | Via `SL.auth.getAccount()` | Via `user` prop from parent |
| **Navigation** | `window.parent.postMessage` | `onNavigate` with `NavigationRequest` |
## Troubleshooting
| Issue | Cause | Fix |
| -------------------------- | ---------------------------------------- | ---------------------------------------------------- |
| Container doesn't render | Missing shared globals | Ensure all Shared Dependencies are on `window` |
| Styles don't apply | Missing `containers.css` | Load the CSS file alongside the JS bundle |
| Routing doesn't work | Using HashRouter instead of MemoryRouter | Containers must use MemoryRouter |
| Query cache conflicts | Sharing parent's QueryClient | Each container needs its own `QueryClient` instance |
| `cva.cva` runtime error | Global set to lowercase `cva` | Use uppercase `CVA` for the global name |
| Navigation does nothing | Using legacy string with `onNavigate` | Use structured `NavigationRequest` object instead |