@proveanything/smartlinks
Version:
Official JavaScript/TypeScript SDK for the Smartlinks API
464 lines (366 loc) • 12.6 kB
Markdown
# IframeResponder - Parent-Side Iframe Communication
The `IframeResponder` enables bidirectional communication between a parent application and an embedded iframe, both using the SmartLinks SDK. This allows seamless integration of microapps within your application with automatic API proxying, authentication sync, and deep linking.
## Features
- **Automatic URL Resolution** - Fetches app configuration from collection and resolves the correct URL
- **API Request Proxying** - Forwards API requests from iframe to parent's authenticated session
- **Smart Caching** - Reduces API calls by serving cached collection, product, and proof data
- **Authentication Sync** - Handles login/logout events between parent and iframe
- **Deep Linking** - Synchronizes iframe routes with parent URL state
- **Responsive Sizing** - Calculates and reports optimal iframe dimensions
- **Chunked File Uploads** - Proxies large file uploads through the parent
## Basic Usage
### 1. Create and Attach Responder
```typescript
import * as smartlinks from '@proveanything/smartlinks';
// Initialize SDK
smartlinks.initializeApi({
baseURL: 'https://api.smartlinks.io',
apiKey: 'your-api-key',
});
// Create responder (URL resolved automatically from collection apps)
const responder = new smartlinks.IframeResponder({
collectionId: 'acme-wines',
appId: 'warranty-registration',
productId: 'wine-bottle-123', // Optional context
onAuthLogin: async (token, user) => {
// Handle authentication from iframe
smartlinks.setBearerToken(token);
console.log('User logged in:', user);
},
onResize: (height) => {
// Update iframe height
iframeElement.style.height = `${height}px`;
},
});
// Attach to iframe element
const iframe = document.getElementById('app-iframe') as HTMLIFrameElement;
const src = await responder.attach(iframe);
iframe.src = src;
// Cleanup when done
// responder.destroy();
```
### 2. With Pre-cached Data (Faster Loading)
```typescript
// Fetch data upfront
const collection = await smartlinks.collection.get('acme-wines');
const product = await smartlinks.product.get('wine-bottle-123');
const user = await smartlinks.auth.getAccountInfo();
const responder = new smartlinks.IframeResponder({
collectionId: 'acme-wines',
appId: 'warranty-registration',
productId: 'wine-bottle-123',
// Pre-populate cache for instant API responses
cache: {
collection,
product,
user: user ? {
uid: user.uid,
email: user.email,
displayName: user.displayName,
accountData: user.accountData,
} : null,
},
onAuthLogin: async (token, user) => {
smartlinks.setBearerToken(token);
},
});
const src = await responder.attach(iframeElement);
iframeElement.src = src;
```
## Configuration Options
### IframeResponderOptions
```typescript
interface IframeResponderOptions {
// Required
collectionId: string; // Collection context
appId: string; // App to load (e.g., 'warranty-registration')
// Optional Context
productId?: string; // Product context
proofId?: string; // Proof context
isAdmin?: boolean; // Admin mode flag
// URL Configuration
version?: 'stable' | 'development'; // App version (default: 'stable')
appUrl?: string; // Override URL (for local dev)
initialPath?: string; // Initial hash path (e.g., '/settings')
// Data
cache?: CachedData; // Pre-cached data
// Callbacks
onAuthLogin?: (token: string, user: any, accountData?: any) => Promise<void>;
onAuthLogout?: () => Promise<void>;
onRouteChange?: (path: string, state: Record<string, string>) => void;
onResize?: (height: number) => void;
onError?: (error: Error) => void;
onReady?: () => void;
}
```
## Advanced Examples
### Deep Linking / Route Synchronization
Keep the parent URL in sync with iframe navigation:
```typescript
const responder = new smartlinks.IframeResponder({
collectionId: 'acme-wines',
appId: 'warranty',
initialPath: '/products/wine-123',
onRouteChange: (path, state) => {
// Update parent URL with iframe route
const url = new URL(window.location.href);
url.searchParams.set('app-path', path);
Object.entries(state).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
window.history.pushState({}, '', url);
},
});
```
### Local Development Override
```typescript
const responder = new smartlinks.IframeResponder({
collectionId: 'acme-wines',
appId: 'warranty',
// Override URL for local development
appUrl: 'http://localhost:5173',
version: 'development',
});
```
### Admin Mode with Role Detection
```typescript
const user = await smartlinks.auth.getAccountInfo();
const collection = await smartlinks.collection.get('acme-wines');
const isAdmin = smartlinks.isAdminFromRoles(user, collection);
const responder = new smartlinks.IframeResponder({
collectionId: 'acme-wines',
appId: 'warranty',
isAdmin, // Pass admin status to iframe
cache: { collection, user },
});
```
### Responsive Height Management
```typescript
const responder = new smartlinks.IframeResponder({
collectionId: 'acme-wines',
appId: 'warranty',
onResize: (height) => {
// Smooth height transitions
iframe.style.transition = 'height 0.3s ease';
iframe.style.height = `${height}px`;
// Or calculate viewport-based height
const maxHeight = window.innerHeight - 100;
iframe.style.height = `${Math.min(height, maxHeight)}px`;
},
});
```
## Helper Functions
### buildIframeSrc()
Manually construct an iframe src URL without using the full responder:
```typescript
const src = smartlinks.buildIframeSrc({
appUrl: 'https://warranty.lovable.app',
collectionId: 'acme-wines',
appId: 'warranty',
productId: 'wine-123',
isAdmin: false,
dark: true,
theme: {
primary: '#FF6B6B',
secondary: '#4ECDC4',
},
initialPath: '/register',
});
iframe.src = src;
```
### isAdminFromRoles()
Check if a user has admin access:
```typescript
const user = await smartlinks.auth.getAccountInfo();
const collection = await smartlinks.collection.get('acme-wines');
const isAdmin = smartlinks.isAdminFromRoles(user, collection);
// or with proof
const isAdminOfProof = smartlinks.isAdminFromRoles(user, collection, proof);
```
## Cache Utilities
The `cache` module provides TTL-based caching:
```typescript
import * as smartlinks from '@proveanything/smartlinks';
// Cache with 5-minute TTL
const apps = await smartlinks.cache.getOrFetch(
'apps:acme-wines',
() => smartlinks.collection.getApps('acme-wines'),
{ ttl: 5 * 60 * 1000, storage: 'session' }
);
// Invalidate cache
smartlinks.cache.invalidate('apps:acme-wines');
// Clear all cache
smartlinks.cache.clear();
```
### Cache Storage Options
- `memory` - In-memory only (default, cleared on page reload)
- `session` - sessionStorage (persists across page reloads in same tab)
- `local` - localStorage (persists across browser sessions)
## Backend API Requirement
The SDK uses the existing `getAppsConfig()` endpoint:
```
GET /public/collection/:collectionId/apps-config
Response:
{
"apps": [
{
"id": "warranty-registration",
"srcAppId": "warranty-v2",
"name": "Warranty Registration",
"publicIframeUrl": "https://warranty.lovable.app",
"active": true,
"category": "Commerce",
"usage": {
"collection": true,
"product": true,
"proof": false,
"widget": false
}
}
]
}
```
Each app must have a `publicIframeUrl` configured for the IframeResponder to work.
## Complete Integration Example
```typescript
import * as smartlinks from '@proveanything/smartlinks';
// Initialize SDK
smartlinks.initializeApi({
baseURL: 'https://api.smartlinks.io',
apiKey: 'your-api-key',
});
class AppManager {
private responder: smartlinks.IframeResponder | null = null;
async loadApp(containerId: string) {
// Create iframe
const iframe = document.createElement('iframe');
iframe.style.width = '100%';
iframe.style.border = 'none';
document.getElementById(containerId)?.appendChild(iframe);
// Fetch context data
const collection = await smartlinks.collection.get('acme-wines');
const product = await smartlinks.product.get('wine-123');
// Determine admin status
try {
const user = await smartlinks.auth.getAccountInfo();
const isAdmin = smartlinks.isAdminFromRoles(user, collection);
// Create responder
this.responder = new smartlinks.IframeResponder({
collectionId: 'acme-wines',
appId: 'warranty',
productId: 'wine-123',
isAdmin,
cache: { collection, product, user },
onAuthLogin: async (token, user) => {
smartlinks.setBearerToken(token);
this.responder?.updateCache({ user });
},
onAuthLogout: async () => {
smartlinks.setBearerToken('');
this.responder?.updateCache({ user: null });
},
onRouteChange: (path, state) => {
console.log('Route changed:', path, state);
},
onResize: (height) => {
iframe.style.height = `${height}px`;
},
onError: (error) => {
console.error('Iframe error:', error);
},
});
// Attach and load
const src = await this.responder.attach(iframe);
iframe.src = src;
} catch (err) {
console.error('Failed to load app:', err);
}
}
destroy() {
this.responder?.destroy();
this.responder = null;
}
}
// Usage
const manager = new AppManager();
await manager.loadApp('app-container');
// Cleanup on unmount
// manager.destroy();
```
## Message Protocol
The responder handles these message types from the iframe:
### Route Changes
```typescript
{
type: 'smartlinks-route-change',
path: '/products/wine-123',
context: { collectionId: 'acme-wines', appId: 'warranty' },
state: { tab: 'details' }
}
```
### Resize Events
```typescript
{
_smartlinksIframeMessage: true,
type: 'smartlinks:resize',
payload: { height: 800 }
}
```
### Authentication
```typescript
{
_smartlinksIframeMessage: true,
type: 'smartlinks:authkit:login',
payload: { token: '...', user: {...}, messageId: 'abc123' }
}
```
### API Proxy
```typescript
{
_smartlinksProxyRequest: true,
id: 'req-123',
method: 'GET',
path: 'public/product/wine-123',
headers: { 'Authorization': 'Bearer ...' }
}
```
## TypeScript Support
All types are exported for full TypeScript support:
```typescript
import type {
IframeResponderOptions,
CachedData,
CollectionApp,
RouteChangeMessage,
SmartlinksIframeMessage,
} from '@proveanything/smartlinks';
```
## Browser Compatibility
- Modern browsers with ES6+ support
- Requires `postMessage`, `MessageEvent`, `ResizeObserver` APIs
- Falls back gracefully in Node.js environments (no-ops for browser-only features)
## Best Practices
1. **Pre-cache data** when possible to eliminate loading delays
2. **Handle onError** to catch and log communication issues
3. **Clean up** responder with `destroy()` when unmounting
4. **Use version control** to test against development versions
5. **Validate tokens** server-side for security
6. **Set height dynamically** based on content using `onResize`
7. **Sync routes** with parent navigation for better UX
## Troubleshooting
### App not loading
- Verify the appId exists in collection apps configuration
- Check browser console for errors
- Ensure backend API endpoint is available
### Authentication not syncing
- Implement `onAuthLogin` callback
- Verify token is being set with `setBearerToken()`
- Check that auth messages are being received
### Height not updating
- Implement `onResize` callback
- Ensure iframe has CSS styling for height changes
- Check that iframe is sending resize messages
### Cache not working
- Verify cache storage option is supported (sessionStorage/localStorage)
- Check browser storage isn't disabled
- Clear cache and retry with `smartlinks.cache.clear()`