@proveanything/smartlinks-auth-ui 0.5.21 → 0.6.0

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,217 @@
1
+ # IframeResponder Debugging Guide
2
+
3
+ ## Issue Summary
4
+
5
+ The `SmartlinksFrame` React component calls `IframeResponder.attach(iframe)` but:
6
+ - The promise never resolves (or silently fails)
7
+ - No `src` URL is returned to set on the iframe
8
+ - The iframe remains empty with no content
9
+
10
+ ## Logs Needed
11
+
12
+ Please add `console.log` statements at these key points in `iframeResponder.ts`:
13
+
14
+ ### 1. Constructor Initialization
15
+
16
+ ```typescript
17
+ constructor(options: IframeResponderOptions) {
18
+ console.log('[IframeResponder] Constructor called', {
19
+ collectionId: options.collectionId,
20
+ appId: options.appId,
21
+ productId: options.productId,
22
+ hasCache: !!options.cache,
23
+ hasCachedApps: !!options.cache?.apps,
24
+ });
25
+
26
+ this.options = options;
27
+ this.cache = options.cache;
28
+ this.resolveReady = null;
29
+ this.ready = new Promise((resolve) => {
30
+ this.resolveReady = resolve;
31
+ });
32
+
33
+ // After resolveAppUrl() is called:
34
+ this.resolveAppUrl()
35
+ .then(() => {
36
+ console.log('[IframeResponder] App URL resolved successfully:', this.appUrl);
37
+ this.resolveReady?.();
38
+ })
39
+ .catch((err) => {
40
+ console.error('[IframeResponder] App URL resolution failed:', err);
41
+ // Make sure this error is propagated!
42
+ });
43
+ }
44
+ ```
45
+
46
+ ### 2. resolveAppUrl Method
47
+
48
+ ```typescript
49
+ private async resolveAppUrl(): Promise<void> {
50
+ console.log('[IframeResponder] resolveAppUrl started');
51
+
52
+ // Check cache first
53
+ const cachedApps = this.cache.apps;
54
+ if (cachedApps) {
55
+ console.log('[IframeResponder] Found cached apps:', cachedApps.length);
56
+ const app = cachedApps.find(a => a.id === this.options.appId);
57
+ if (app) {
58
+ this.appUrl = this.getVersionUrl(app);
59
+ console.log('[IframeResponder] Using cached app URL:', this.appUrl);
60
+ return;
61
+ }
62
+ console.log('[IframeResponder] App not found in cache, fetching from API');
63
+ }
64
+
65
+ // Fetch from API
66
+ try {
67
+ console.log('[IframeResponder] Calling cache.getOrFetch for apps');
68
+ const apps = await cache.getOrFetch<CollectionApp[]>(
69
+ `apps:${this.options.collectionId}`,
70
+ () => this.fetchApps(),
71
+ { ttl: 5 * 60 * 1000, storage: 'session' }
72
+ );
73
+
74
+ console.log('[IframeResponder] Got apps from API:', apps?.length, apps);
75
+
76
+ const app = apps.find(a => a.id === this.options.appId);
77
+ if (!app) {
78
+ console.error('[IframeResponder] App not found:', this.options.appId, 'Available:', apps.map(a => a.id));
79
+ throw new Error(`App "${this.options.appId}" not found in collection`);
80
+ }
81
+
82
+ this.appUrl = this.getVersionUrl(app);
83
+ console.log('[IframeResponder] Resolved app URL:', this.appUrl);
84
+ } catch (err) {
85
+ console.error('[IframeResponder] resolveAppUrl error:', err);
86
+ this.options.onError?.(err);
87
+ throw err;
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### 3. fetchApps Method
93
+
94
+ ```typescript
95
+ private async fetchApps(): Promise<CollectionApp[]> {
96
+ const url = `/api/v1/public/collection/${this.options.collectionId}/apps`;
97
+ console.log('[IframeResponder] Fetching apps from:', url);
98
+
99
+ try {
100
+ const response = await fetch(url);
101
+ console.log('[IframeResponder] Fetch response status:', response.status);
102
+
103
+ if (!response.ok) {
104
+ const text = await response.text();
105
+ console.error('[IframeResponder] Fetch failed:', response.status, text);
106
+ throw new Error(`Failed to fetch apps: ${response.status}`);
107
+ }
108
+
109
+ const data = await response.json();
110
+ console.log('[IframeResponder] Fetch response data:', data);
111
+ return data.items || data;
112
+ } catch (err) {
113
+ console.error('[IframeResponder] fetchApps exception:', err);
114
+ throw err;
115
+ }
116
+ }
117
+ ```
118
+
119
+ ### 4. attach Method
120
+
121
+ ```typescript
122
+ async attach(iframe: HTMLIFrameElement): Promise<string> {
123
+ console.log('[IframeResponder] attach() called, waiting for ready...');
124
+
125
+ await this.ready;
126
+
127
+ console.log('[IframeResponder] Ready resolved, appUrl:', this.appUrl);
128
+
129
+ this.iframe = iframe;
130
+
131
+ // Set up listeners...
132
+
133
+ const src = this.buildIframeSrc();
134
+ console.log('[IframeResponder] Built iframe src:', src);
135
+
136
+ return src;
137
+ }
138
+ ```
139
+
140
+ ### 5. buildIframeSrc Method
141
+
142
+ ```typescript
143
+ private buildIframeSrc(): string {
144
+ console.log('[IframeResponder] buildIframeSrc called, appUrl:', this.appUrl);
145
+
146
+ if (!this.appUrl) {
147
+ console.error('[IframeResponder] Cannot build src - appUrl is null!');
148
+ throw new Error('App URL not resolved');
149
+ }
150
+
151
+ const { collectionId, appId, productId } = this.options;
152
+ const { origin: base, hashPath } = new URL(this.appUrl);
153
+
154
+ const params = new URLSearchParams({
155
+ collectionId,
156
+ appId,
157
+ });
158
+
159
+ if (productId) {
160
+ params.append('productId', productId);
161
+ }
162
+
163
+ const finalUrl = `${base}/#/${hashPath}?${params.toString()}`;
164
+ console.log('[IframeResponder] Final iframe URL:', finalUrl);
165
+ return finalUrl;
166
+ }
167
+ ```
168
+
169
+ ## What We Need to See
170
+
171
+ 1. **Does the constructor complete?** - "Constructor called" log
172
+ 2. **Does URL resolution start?** - "resolveAppUrl started" log
173
+ 3. **Is the API called?** - "Fetching apps from" log
174
+ 4. **Does the API respond?** - "Fetch response status" log
175
+ 5. **Is the app found?** - "Resolved app URL" log
176
+ 6. **Does attach wait properly?** - "attach() called" and "Ready resolved" logs
177
+ 7. **Is the src built?** - "Built iframe src" log
178
+
179
+ ## Likely Failure Points
180
+
181
+ Based on symptoms (no logs after "Initializing"), the issue is probably:
182
+
183
+ 1. **`this.ready` promise never resolves** - `resolveAppUrl()` fails silently or hangs
184
+ 2. **API fetch hangs** - CORS issue, wrong URL, or network timeout
185
+ 3. **cache.getOrFetch hangs** - Storage access issue or internal error
186
+ 4. **Error swallowed** - An error occurs but isn't logged/propagated
187
+
188
+ ## Quick Test
189
+
190
+ Add this to verify the SDK is working at all:
191
+
192
+ ```typescript
193
+ // In IframeResponder constructor
194
+ console.log('[IframeResponder] SDK version check:', {
195
+ hasCache: typeof cache !== 'undefined',
196
+ hasCacheGetOrFetch: typeof cache?.getOrFetch === 'function',
197
+ });
198
+ ```
199
+
200
+ ## Expected Console Output (Happy Path)
201
+
202
+ ```
203
+ [IframeResponder] Constructor called { collectionId: "power-pop", appId: "dynamicPage" }
204
+ [IframeResponder] resolveAppUrl started
205
+ [IframeResponder] Calling cache.getOrFetch for apps
206
+ [IframeResponder] Fetching apps from: /api/v1/public/collection/power-pop/apps
207
+ [IframeResponder] Fetch response status: 200
208
+ [IframeResponder] Fetch response data: { items: [...] }
209
+ [IframeResponder] Got apps from API: 5
210
+ [IframeResponder] Resolved app URL: https://my-app.lovable.app
211
+ [IframeResponder] App URL resolved successfully: https://my-app.lovable.app
212
+ [IframeResponder] attach() called, waiting for ready...
213
+ [IframeResponder] Ready resolved, appUrl: https://my-app.lovable.app
214
+ [IframeResponder] Built iframe src: https://my-app.lovable.app/#/?collectionId=power-pop&appId=dynamicPage
215
+ ```
216
+
217
+ If any of these logs are missing, that tells us where it's failing.
@@ -0,0 +1,302 @@
1
+ # SmartlinksFrame Component
2
+
3
+ React component for embedding SmartLinks microapps with automatic URL resolution, API proxying, authentication sync, and deep linking.
4
+
5
+ ## Installation
6
+
7
+ The component is part of the auth-module and requires `@proveanything/smartlinks` SDK v1.4+:
8
+
9
+ ```bash
10
+ npm install @proveanything/smartlinks
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```tsx
16
+ import { SmartlinksFrame } from '@smartlinks/auth-module';
17
+
18
+ // URL is auto-resolved from collection.getAppsConfig()
19
+ <SmartlinksFrame
20
+ collectionId="acme-wines"
21
+ appId="warranty-registration"
22
+ productId="wine-bottle-123"
23
+ />
24
+ ```
25
+
26
+ ## Features
27
+
28
+ - **Automatic URL Resolution** - Fetches app config from collection and resolves the correct iframe URL
29
+ - **API Request Proxying** - Forwards API requests from iframe to parent's authenticated session
30
+ - **Smart Caching** - Reduces API calls by serving cached collection, product, and proof data
31
+ - **Authentication Sync** - Handles login/logout events between parent and iframe via AuthProvider
32
+ - **Deep Linking** - Synchronizes iframe routes with parent URL state
33
+ - **Responsive Sizing** - Automatically adjusts iframe height to content
34
+
35
+ ## Props
36
+
37
+ | Prop | Type | Required | Description |
38
+ |------|------|----------|-------------|
39
+ | `collectionId` | `string` | ✅ | Collection context |
40
+ | `appId` | `string` | ✅ | App to load (e.g., 'warranty-registration') |
41
+ | `productId` | `string` | | Product context |
42
+ | `proofId` | `string` | | Proof context |
43
+ | `appUrl` | `string` | | Override URL (for local development) |
44
+ | `version` | `'stable' \| 'development'` | | App version (default: 'stable') |
45
+ | `collection` | `object` | | Pre-cached collection data |
46
+ | `product` | `object` | | Pre-cached product data |
47
+ | `proof` | `object` | | Pre-cached proof data |
48
+ | `initialPath` | `string` | | Initial hash path (e.g., '/settings') |
49
+ | `onRouteChange` | `(path, state) => void` | | Called when iframe route changes |
50
+ | `autoResize` | `boolean` | | Auto-adjust height (default: true) |
51
+ | `minHeight` | `number` | | Minimum height in pixels |
52
+ | `maxHeight` | `number` | | Maximum height in pixels |
53
+ | `onReady` | `() => void` | | Called when iframe loads |
54
+ | `onError` | `(error) => void` | | Called on errors |
55
+ | `className` | `string` | | Container CSS class |
56
+ | `style` | `CSSProperties` | | Container inline styles |
57
+
58
+ ## Usage Examples
59
+
60
+ ### Basic Embedding
61
+
62
+ ```tsx
63
+ // App URL resolved automatically from collection apps config
64
+ <SmartlinksFrame
65
+ collectionId="acme-wines"
66
+ appId="warranty"
67
+ productId="cabernet-2023"
68
+ />
69
+ ```
70
+
71
+ ### With Pre-fetched Data (Faster Loading)
72
+
73
+ ```tsx
74
+ import { useQuery } from '@tanstack/react-query';
75
+ import * as smartlinks from '@proveanything/smartlinks';
76
+
77
+ const { data: collection } = useQuery(
78
+ ['collection', collectionId],
79
+ () => smartlinks.collection.get(collectionId)
80
+ );
81
+
82
+ const { data: product } = useQuery(
83
+ ['product', productId],
84
+ () => smartlinks.product.get(productId)
85
+ );
86
+
87
+ <SmartlinksFrame
88
+ collectionId={collectionId}
89
+ appId="warranty"
90
+ productId={productId}
91
+ collection={collection}
92
+ product={product}
93
+ />
94
+ ```
95
+
96
+ ### With AuthProvider (Recommended)
97
+
98
+ When wrapped in `AuthProvider`, authentication is automatically synced:
99
+
100
+ ```tsx
101
+ import { AuthProvider, SmartlinksFrame } from '@smartlinks/auth-module';
102
+
103
+ <AuthProvider>
104
+ <SmartlinksFrame
105
+ collectionId="acme-wines"
106
+ appId="warranty"
107
+ />
108
+ </AuthProvider>
109
+ ```
110
+
111
+ ### Local Development Override
112
+
113
+ ```tsx
114
+ <SmartlinksFrame
115
+ collectionId="acme-wines"
116
+ appId="warranty"
117
+ appUrl="http://localhost:5173"
118
+ version="development"
119
+ />
120
+ ```
121
+
122
+ ### Deep Linking
123
+
124
+ ```tsx
125
+ <SmartlinksFrame
126
+ collectionId="acme-wines"
127
+ appId="warranty"
128
+ initialPath="/products/wine-123"
129
+ onRouteChange={(path, state) => {
130
+ // Update parent URL with iframe route
131
+ const url = new URL(window.location.href);
132
+ url.searchParams.set('app-path', path);
133
+ Object.entries(state).forEach(([key, value]) => {
134
+ url.searchParams.set(key, value);
135
+ });
136
+ window.history.pushState({}, '', url);
137
+ }}
138
+ />
139
+ ```
140
+
141
+ ### Responsive Height with Constraints
142
+
143
+ ```tsx
144
+ <SmartlinksFrame
145
+ collectionId="acme-wines"
146
+ appId="warranty"
147
+ autoResize={true}
148
+ minHeight={400}
149
+ maxHeight={800}
150
+ />
151
+ ```
152
+
153
+ ## Authentication Integration
154
+
155
+ The component automatically integrates with `AuthProvider`:
156
+
157
+ 1. **Admin Detection**: Uses `isAdminFromRoles()` to detect admin status from collection/proof roles
158
+ 2. **Login Sync**: When iframe triggers login, calls `AuthProvider.login()` to sync session
159
+ 3. **Logout Sync**: When iframe triggers logout, calls `AuthProvider.logout()`
160
+
161
+ ### Without AuthProvider
162
+
163
+ Works in anonymous mode - API requests still proxied, but no auth sync:
164
+
165
+ ```tsx
166
+ // No AuthProvider wrapper - works but no login/logout sync
167
+ <SmartlinksFrame
168
+ collectionId="acme-wines"
169
+ appId="public-app"
170
+ />
171
+ ```
172
+
173
+ ## SDK Helpers
174
+
175
+ The component uses these SDK functions internally (also exported for direct use):
176
+
177
+ ### `isAdminFromRoles(user, collection, proof?)`
178
+
179
+ ```tsx
180
+ import { isAdminFromRoles } from '@smartlinks/auth-module';
181
+ // or
182
+ import { isAdminFromRoles } from '@proveanything/smartlinks';
183
+
184
+ const isAdmin = isAdminFromRoles(user, collection);
185
+ ```
186
+
187
+ ### `buildIframeSrc(options)`
188
+
189
+ Manually construct iframe URL:
190
+
191
+ ```tsx
192
+ import { buildIframeSrc } from '@smartlinks/auth-module';
193
+
194
+ const src = buildIframeSrc({
195
+ appUrl: 'https://warranty.lovable.app',
196
+ collectionId: 'acme-wines',
197
+ appId: 'warranty',
198
+ productId: 'wine-123',
199
+ isAdmin: false,
200
+ dark: true,
201
+ theme: { primary: '#FF6B6B' },
202
+ initialPath: '/register',
203
+ });
204
+ ```
205
+
206
+ ## Caching
207
+
208
+ The SDK caches app configuration to avoid redundant API calls:
209
+
210
+ ```tsx
211
+ import * as smartlinks from '@proveanything/smartlinks';
212
+
213
+ // Apps cached for 5 minutes in sessionStorage
214
+ const apps = await smartlinks.cache.getOrFetch(
215
+ `apps:${collectionId}`,
216
+ () => smartlinks.collection.getAppsConfig(collectionId),
217
+ { ttl: 5 * 60 * 1000, storage: 'session' }
218
+ );
219
+
220
+ // Invalidate cache
221
+ smartlinks.cache.invalidate(`apps:${collectionId}`);
222
+
223
+ // Clear all cache
224
+ smartlinks.cache.clear();
225
+ ```
226
+
227
+ ## Migration from Direct IframeResponder
228
+
229
+ If you were using `IframeResponder` directly, the React component handles everything:
230
+
231
+ ```tsx
232
+ // BEFORE: Manual IframeResponder usage
233
+ const responder = new smartlinks.IframeResponder({
234
+ collectionId: 'acme-wines',
235
+ appId: 'warranty',
236
+ onAuthLogin: async (token, user) => {
237
+ smartlinks.setBearerToken(token);
238
+ },
239
+ onResize: (h) => {
240
+ iframe.style.height = `${h}px`;
241
+ },
242
+ });
243
+ const src = await responder.attach(iframe);
244
+
245
+ // AFTER: React component handles everything
246
+ <SmartlinksFrame
247
+ collectionId="acme-wines"
248
+ appId="warranty"
249
+ />
250
+ ```
251
+
252
+ ## Backend Requirements
253
+
254
+ The SDK expects this endpoint to be available:
255
+
256
+ ```
257
+ GET /public/collection/:collectionId/apps-config
258
+
259
+ Response:
260
+ {
261
+ "apps": [
262
+ {
263
+ "id": "warranty-registration",
264
+ "srcAppId": "warranty-v2",
265
+ "name": "Warranty Registration",
266
+ "publicIframeUrl": "https://warranty.lovable.app",
267
+ "active": true,
268
+ "category": "Commerce",
269
+ "usage": {
270
+ "collection": true,
271
+ "product": true,
272
+ "proof": false,
273
+ "widget": false
274
+ }
275
+ }
276
+ ]
277
+ }
278
+ ```
279
+
280
+ Each app must have a `publicIframeUrl` configured for URL auto-resolution to work.
281
+
282
+ ## Troubleshooting
283
+
284
+ ### App not loading
285
+ - Verify the appId exists in collection apps configuration
286
+ - Check browser console for errors
287
+ - Ensure `publicIframeUrl` is set in app config
288
+
289
+ ### Authentication not syncing
290
+ - Wrap component in `AuthProvider`
291
+ - Check that auth messages are being received
292
+ - Verify `onAuthLogin` is being called
293
+
294
+ ### Height not updating
295
+ - Ensure `autoResize={true}` (default)
296
+ - Check that iframe is sending resize messages
297
+ - Try setting explicit `minHeight`/`maxHeight`
298
+
299
+ ### URL not resolving
300
+ - Provide explicit `appUrl` prop as fallback
301
+ - Verify backend `/apps-config` endpoint is available
302
+ - Check network tab for API errors