@proveanything/smartlinks 1.3.8 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,463 @@
1
+ # IframeResponder - Parent-Side Iframe Communication
2
+
3
+ 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.
4
+
5
+ ## Features
6
+
7
+ - **Automatic URL Resolution** - Fetches app configuration from collection and resolves the correct URL
8
+ - **API Request Proxying** - Forwards API requests from iframe to parent's authenticated session
9
+ - **Smart Caching** - Reduces API calls by serving cached collection, product, and proof data
10
+ - **Authentication Sync** - Handles login/logout events between parent and iframe
11
+ - **Deep Linking** - Synchronizes iframe routes with parent URL state
12
+ - **Responsive Sizing** - Calculates and reports optimal iframe dimensions
13
+ - **Chunked File Uploads** - Proxies large file uploads through the parent
14
+
15
+ ## Basic Usage
16
+
17
+ ### 1. Create and Attach Responder
18
+
19
+ ```typescript
20
+ import * as smartlinks from '@proveanything/smartlinks';
21
+
22
+ // Initialize SDK
23
+ smartlinks.initializeApi({
24
+ baseURL: 'https://api.smartlinks.io',
25
+ apiKey: 'your-api-key',
26
+ });
27
+
28
+ // Create responder (URL resolved automatically from collection apps)
29
+ const responder = new smartlinks.IframeResponder({
30
+ collectionId: 'acme-wines',
31
+ appId: 'warranty-registration',
32
+ productId: 'wine-bottle-123', // Optional context
33
+ onAuthLogin: async (token, user) => {
34
+ // Handle authentication from iframe
35
+ smartlinks.setBearerToken(token);
36
+ console.log('User logged in:', user);
37
+ },
38
+ onResize: (height) => {
39
+ // Update iframe height
40
+ iframeElement.style.height = `${height}px`;
41
+ },
42
+ });
43
+
44
+ // Attach to iframe element
45
+ const iframe = document.getElementById('app-iframe') as HTMLIFrameElement;
46
+ const src = await responder.attach(iframe);
47
+ iframe.src = src;
48
+
49
+ // Cleanup when done
50
+ // responder.destroy();
51
+ ```
52
+
53
+ ### 2. With Pre-cached Data (Faster Loading)
54
+
55
+ ```typescript
56
+ // Fetch data upfront
57
+ const collection = await smartlinks.collection.get('acme-wines');
58
+ const product = await smartlinks.product.get('wine-bottle-123');
59
+ const user = await smartlinks.auth.getAccountInfo();
60
+
61
+ const responder = new smartlinks.IframeResponder({
62
+ collectionId: 'acme-wines',
63
+ appId: 'warranty-registration',
64
+ productId: 'wine-bottle-123',
65
+
66
+ // Pre-populate cache for instant API responses
67
+ cache: {
68
+ collection,
69
+ product,
70
+ user: user ? {
71
+ uid: user.uid,
72
+ email: user.email,
73
+ displayName: user.displayName,
74
+ accountData: user.accountData,
75
+ } : null,
76
+ },
77
+
78
+ onAuthLogin: async (token, user) => {
79
+ smartlinks.setBearerToken(token);
80
+ },
81
+ });
82
+
83
+ const src = await responder.attach(iframeElement);
84
+ iframeElement.src = src;
85
+ ```
86
+
87
+ ## Configuration Options
88
+
89
+ ### IframeResponderOptions
90
+
91
+ ```typescript
92
+ interface IframeResponderOptions {
93
+ // Required
94
+ collectionId: string; // Collection context
95
+ appId: string; // App to load (e.g., 'warranty-registration')
96
+
97
+ // Optional Context
98
+ productId?: string; // Product context
99
+ proofId?: string; // Proof context
100
+ isAdmin?: boolean; // Admin mode flag
101
+
102
+ // URL Configuration
103
+ version?: 'stable' | 'development'; // App version (default: 'stable')
104
+ appUrl?: string; // Override URL (for local dev)
105
+ initialPath?: string; // Initial hash path (e.g., '/settings')
106
+
107
+ // Data
108
+ cache?: CachedData; // Pre-cached data
109
+
110
+ // Callbacks
111
+ onAuthLogin?: (token: string, user: any, accountData?: any) => Promise<void>;
112
+ onAuthLogout?: () => Promise<void>;
113
+ onRouteChange?: (path: string, state: Record<string, string>) => void;
114
+ onResize?: (height: number) => void;
115
+ onError?: (error: Error) => void;
116
+ onReady?: () => void;
117
+ }
118
+ ```
119
+
120
+ ## Advanced Examples
121
+
122
+ ### Deep Linking / Route Synchronization
123
+
124
+ Keep the parent URL in sync with iframe navigation:
125
+
126
+ ```typescript
127
+ const responder = new smartlinks.IframeResponder({
128
+ collectionId: 'acme-wines',
129
+ appId: 'warranty',
130
+ initialPath: '/products/wine-123',
131
+
132
+ onRouteChange: (path, state) => {
133
+ // Update parent URL with iframe route
134
+ const url = new URL(window.location.href);
135
+ url.searchParams.set('app-path', path);
136
+ Object.entries(state).forEach(([key, value]) => {
137
+ url.searchParams.set(key, value);
138
+ });
139
+ window.history.pushState({}, '', url);
140
+ },
141
+ });
142
+ ```
143
+
144
+ ### Local Development Override
145
+
146
+ ```typescript
147
+ const responder = new smartlinks.IframeResponder({
148
+ collectionId: 'acme-wines',
149
+ appId: 'warranty',
150
+
151
+ // Override URL for local development
152
+ appUrl: 'http://localhost:5173',
153
+ version: 'development',
154
+ });
155
+ ```
156
+
157
+ ### Admin Mode with Role Detection
158
+
159
+ ```typescript
160
+ const user = await smartlinks.auth.getAccountInfo();
161
+ const collection = await smartlinks.collection.get('acme-wines');
162
+
163
+ const isAdmin = smartlinks.isAdminFromRoles(user, collection);
164
+
165
+ const responder = new smartlinks.IframeResponder({
166
+ collectionId: 'acme-wines',
167
+ appId: 'warranty',
168
+ isAdmin, // Pass admin status to iframe
169
+ cache: { collection, user },
170
+ });
171
+ ```
172
+
173
+ ### Responsive Height Management
174
+
175
+ ```typescript
176
+ const responder = new smartlinks.IframeResponder({
177
+ collectionId: 'acme-wines',
178
+ appId: 'warranty',
179
+
180
+ onResize: (height) => {
181
+ // Smooth height transitions
182
+ iframe.style.transition = 'height 0.3s ease';
183
+ iframe.style.height = `${height}px`;
184
+
185
+ // Or calculate viewport-based height
186
+ const maxHeight = window.innerHeight - 100;
187
+ iframe.style.height = `${Math.min(height, maxHeight)}px`;
188
+ },
189
+ });
190
+ ```
191
+
192
+ ## Helper Functions
193
+
194
+ ### buildIframeSrc()
195
+
196
+ Manually construct an iframe src URL without using the full responder:
197
+
198
+ ```typescript
199
+ const src = smartlinks.buildIframeSrc({
200
+ appUrl: 'https://warranty.lovable.app',
201
+ collectionId: 'acme-wines',
202
+ appId: 'warranty',
203
+ productId: 'wine-123',
204
+ isAdmin: false,
205
+ dark: true,
206
+ theme: {
207
+ primary: '#FF6B6B',
208
+ secondary: '#4ECDC4',
209
+ },
210
+ initialPath: '/register',
211
+ });
212
+
213
+ iframe.src = src;
214
+ ```
215
+
216
+ ### isAdminFromRoles()
217
+
218
+ Check if a user has admin access:
219
+
220
+ ```typescript
221
+ const user = await smartlinks.auth.getAccountInfo();
222
+ const collection = await smartlinks.collection.get('acme-wines');
223
+
224
+ const isAdmin = smartlinks.isAdminFromRoles(user, collection);
225
+ // or with proof
226
+ const isAdminOfProof = smartlinks.isAdminFromRoles(user, collection, proof);
227
+ ```
228
+
229
+ ## Cache Utilities
230
+
231
+ The `cache` module provides TTL-based caching:
232
+
233
+ ```typescript
234
+ import * as smartlinks from '@proveanything/smartlinks';
235
+
236
+ // Cache with 5-minute TTL
237
+ const apps = await smartlinks.cache.getOrFetch(
238
+ 'apps:acme-wines',
239
+ () => smartlinks.collection.getApps('acme-wines'),
240
+ { ttl: 5 * 60 * 1000, storage: 'session' }
241
+ );
242
+
243
+ // Invalidate cache
244
+ smartlinks.cache.invalidate('apps:acme-wines');
245
+
246
+ // Clear all cache
247
+ smartlinks.cache.clear();
248
+ ```
249
+
250
+ ### Cache Storage Options
251
+
252
+ - `memory` - In-memory only (default, cleared on page reload)
253
+ - `session` - sessionStorage (persists across page reloads in same tab)
254
+ - `local` - localStorage (persists across browser sessions)
255
+
256
+ ## Backend API Requirement
257
+
258
+ The SDK uses the existing `getAppsConfig()` endpoint:
259
+
260
+ ```
261
+ GET /public/collection/:collectionId/apps-config
262
+
263
+ Response:
264
+ {
265
+ "apps": [
266
+ {
267
+ "id": "warranty-registration",
268
+ "srcAppId": "warranty-v2",
269
+ "name": "Warranty Registration",
270
+ "publicIframeUrl": "https://warranty.lovable.app",
271
+ "active": true,
272
+ "category": "Commerce",
273
+ "usage": {
274
+ "collection": true,
275
+ "product": true,
276
+ "proof": false,
277
+ "widget": false
278
+ }
279
+ }
280
+ ]
281
+ }
282
+ ```
283
+
284
+ Each app must have a `publicIframeUrl` configured for the IframeResponder to work.
285
+
286
+ ## Complete Integration Example
287
+
288
+ ```typescript
289
+ import * as smartlinks from '@proveanything/smartlinks';
290
+
291
+ // Initialize SDK
292
+ smartlinks.initializeApi({
293
+ baseURL: 'https://api.smartlinks.io',
294
+ apiKey: 'your-api-key',
295
+ });
296
+
297
+ class AppManager {
298
+ private responder: smartlinks.IframeResponder | null = null;
299
+
300
+ async loadApp(containerId: string) {
301
+ // Create iframe
302
+ const iframe = document.createElement('iframe');
303
+ iframe.style.width = '100%';
304
+ iframe.style.border = 'none';
305
+ document.getElementById(containerId)?.appendChild(iframe);
306
+
307
+ // Fetch context data
308
+ const collection = await smartlinks.collection.get('acme-wines');
309
+ const product = await smartlinks.product.get('wine-123');
310
+
311
+ // Determine admin status
312
+ try {
313
+ const user = await smartlinks.auth.getAccountInfo();
314
+ const isAdmin = smartlinks.isAdminFromRoles(user, collection);
315
+
316
+ // Create responder
317
+ this.responder = new smartlinks.IframeResponder({
318
+ collectionId: 'acme-wines',
319
+ appId: 'warranty',
320
+ productId: 'wine-123',
321
+ isAdmin,
322
+ cache: { collection, product, user },
323
+
324
+ onAuthLogin: async (token, user) => {
325
+ smartlinks.setBearerToken(token);
326
+ this.responder?.updateCache({ user });
327
+ },
328
+
329
+ onAuthLogout: async () => {
330
+ smartlinks.setBearerToken('');
331
+ this.responder?.updateCache({ user: null });
332
+ },
333
+
334
+ onRouteChange: (path, state) => {
335
+ console.log('Route changed:', path, state);
336
+ },
337
+
338
+ onResize: (height) => {
339
+ iframe.style.height = `${height}px`;
340
+ },
341
+
342
+ onError: (error) => {
343
+ console.error('Iframe error:', error);
344
+ },
345
+ });
346
+
347
+ // Attach and load
348
+ const src = await this.responder.attach(iframe);
349
+ iframe.src = src;
350
+
351
+ } catch (err) {
352
+ console.error('Failed to load app:', err);
353
+ }
354
+ }
355
+
356
+ destroy() {
357
+ this.responder?.destroy();
358
+ this.responder = null;
359
+ }
360
+ }
361
+
362
+ // Usage
363
+ const manager = new AppManager();
364
+ await manager.loadApp('app-container');
365
+
366
+ // Cleanup on unmount
367
+ // manager.destroy();
368
+ ```
369
+
370
+ ## Message Protocol
371
+
372
+ The responder handles these message types from the iframe:
373
+
374
+ ### Route Changes
375
+ ```typescript
376
+ {
377
+ type: 'smartlinks-route-change',
378
+ path: '/products/wine-123',
379
+ context: { collectionId: 'acme-wines', appId: 'warranty' },
380
+ state: { tab: 'details' }
381
+ }
382
+ ```
383
+
384
+ ### Resize Events
385
+ ```typescript
386
+ {
387
+ _smartlinksIframeMessage: true,
388
+ type: 'smartlinks:resize',
389
+ payload: { height: 800 }
390
+ }
391
+ ```
392
+
393
+ ### Authentication
394
+ ```typescript
395
+ {
396
+ _smartlinksIframeMessage: true,
397
+ type: 'smartlinks:authkit:login',
398
+ payload: { token: '...', user: {...}, messageId: 'abc123' }
399
+ }
400
+ ```
401
+
402
+ ### API Proxy
403
+ ```typescript
404
+ {
405
+ _smartlinksProxyRequest: true,
406
+ id: 'req-123',
407
+ method: 'GET',
408
+ path: 'public/product/wine-123',
409
+ headers: { 'Authorization': 'Bearer ...' }
410
+ }
411
+ ```
412
+
413
+ ## TypeScript Support
414
+
415
+ All types are exported for full TypeScript support:
416
+
417
+ ```typescript
418
+ import type {
419
+ IframeResponderOptions,
420
+ CachedData,
421
+ CollectionApp,
422
+ RouteChangeMessage,
423
+ SmartlinksIframeMessage,
424
+ } from '@proveanything/smartlinks';
425
+ ```
426
+
427
+ ## Browser Compatibility
428
+
429
+ - Modern browsers with ES6+ support
430
+ - Requires `postMessage`, `MessageEvent`, `ResizeObserver` APIs
431
+ - Falls back gracefully in Node.js environments (no-ops for browser-only features)
432
+
433
+ ## Best Practices
434
+
435
+ 1. **Pre-cache data** when possible to eliminate loading delays
436
+ 2. **Handle onError** to catch and log communication issues
437
+ 3. **Clean up** responder with `destroy()` when unmounting
438
+ 4. **Use version control** to test against development versions
439
+ 5. **Validate tokens** server-side for security
440
+ 6. **Set height dynamically** based on content using `onResize`
441
+ 7. **Sync routes** with parent navigation for better UX
442
+
443
+ ## Troubleshooting
444
+
445
+ ### App not loading
446
+ - Verify the appId exists in collection apps configuration
447
+ - Check browser console for errors
448
+ - Ensure backend API endpoint is available
449
+
450
+ ### Authentication not syncing
451
+ - Implement `onAuthLogin` callback
452
+ - Verify token is being set with `setBearerToken()`
453
+ - Check that auth messages are being received
454
+
455
+ ### Height not updating
456
+ - Implement `onResize` callback
457
+ - Ensure iframe has CSS styling for height changes
458
+ - Check that iframe is sending resize messages
459
+
460
+ ### Cache not working
461
+ - Verify cache storage option is supported (sessionStorage/localStorage)
462
+ - Check browser storage isn't disabled
463
+ - Clear cache and retry with `smartlinks.cache.clear()`
package/dist/iframe.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { IframeResponder, isAdminFromRoles, buildIframeSrc, } from './iframeResponder';
2
+ export type { IframeResponderOptions, CachedData, CollectionApp, RouteChangeMessage, SmartlinksIframeMessage, ProxyRequest, CustomProxyRequest, UploadStartMessage, UploadChunkMessage, UploadEndMessage, } from './types/iframeResponder';
1
3
  export declare namespace iframe {
2
4
  interface IframeResizeOptions {
3
5
  /** Minimum ms between height postMessages (default 100). */
package/dist/iframe.js CHANGED
@@ -2,6 +2,8 @@
2
2
  // Utilities to communicate with parent window when running inside an iframe.
3
3
  // These helpers are optional and safe in non-browser / Node environments.
4
4
  // They build on the existing proxyMode infrastructure but can also be used standalone.
5
+ // Re-export IframeResponder for parent-side iframe communication
6
+ export { IframeResponder, isAdminFromRoles, buildIframeSrc, } from './iframeResponder';
5
7
  export var iframe;
6
8
  (function (iframe) {
7
9
  let autoResizeTimer;
@@ -0,0 +1,93 @@
1
+ import type { IframeResponderOptions, CachedData } from './types/iframeResponder';
2
+ /**
3
+ * Parent-side iframe responder for SmartLinks microapp embedding.
4
+ *
5
+ * Handles all bidirectional communication with embedded iframes:
6
+ * - API proxy requests (with caching)
7
+ * - Authentication state synchronization
8
+ * - Deep linking / route changes
9
+ * - Resize management
10
+ * - Chunked file upload proxying
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const responder = new IframeResponder({
15
+ * collectionId: 'my-collection',
16
+ * appId: 'warranty',
17
+ * onAuthLogin: async (token, user) => {
18
+ * smartlinks.setBearerToken(token);
19
+ * },
20
+ * });
21
+ *
22
+ * const src = await responder.attach(iframeElement);
23
+ * iframeElement.src = src;
24
+ *
25
+ * // Cleanup
26
+ * responder.destroy();
27
+ * ```
28
+ */
29
+ export declare class IframeResponder {
30
+ private iframe;
31
+ private options;
32
+ private cache;
33
+ private uploads;
34
+ private isInitialLoad;
35
+ private messageHandler;
36
+ private resizeHandler;
37
+ private appUrl;
38
+ private ready;
39
+ private resolveReady;
40
+ constructor(options: IframeResponderOptions);
41
+ /**
42
+ * Attach to an iframe element.
43
+ * Returns the src URL to set on the iframe.
44
+ */
45
+ attach(iframe: HTMLIFrameElement): Promise<string>;
46
+ /**
47
+ * Update cached data (e.g., after user logs in).
48
+ */
49
+ updateCache(data: Partial<CachedData>): void;
50
+ /**
51
+ * Cleanup - remove event listeners and clear state.
52
+ */
53
+ destroy(): void;
54
+ private resolveAppUrl;
55
+ private getVersionUrl;
56
+ private buildIframeSrc;
57
+ private calculateViewportHeight;
58
+ private handleMessage;
59
+ private handleRouteChange;
60
+ private handleStandardMessage;
61
+ private handleProxyRequest;
62
+ private getCachedResponse;
63
+ private handleUpload;
64
+ private sendResponse;
65
+ }
66
+ /**
67
+ * Determine admin status from collection/proof roles.
68
+ */
69
+ export declare function isAdminFromRoles(user: {
70
+ uid: string;
71
+ } | null | undefined, collection?: {
72
+ roles?: Record<string, any>;
73
+ }, proof?: {
74
+ roles?: Record<string, any>;
75
+ }): boolean;
76
+ /**
77
+ * Build iframe src URL with all context params.
78
+ * Standalone helper for cases where you don't need the full IframeResponder.
79
+ */
80
+ export declare function buildIframeSrc(options: {
81
+ appUrl: string;
82
+ collectionId: string;
83
+ appId: string;
84
+ productId?: string;
85
+ proofId?: string;
86
+ isAdmin?: boolean;
87
+ dark?: boolean;
88
+ theme?: {
89
+ primary?: string;
90
+ secondary?: string;
91
+ };
92
+ initialPath?: string;
93
+ }): string;