@proveanything/smartlinks-auth-ui 0.3.0 → 0.3.1
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.
- package/dist/components/SmartlinksFrame/SmartlinksFrame.d.ts +36 -0
- package/dist/components/SmartlinksFrame/SmartlinksFrame.d.ts.map +1 -0
- package/dist/components/SmartlinksFrame/index.d.ts +7 -0
- package/dist/components/SmartlinksFrame/index.d.ts.map +1 -0
- package/dist/components/SmartlinksFrame/useAdminDetection.d.ts +14 -0
- package/dist/components/SmartlinksFrame/useAdminDetection.d.ts.map +1 -0
- package/dist/components/SmartlinksFrame/useIframeMessages.d.ts +27 -0
- package/dist/components/SmartlinksFrame/useIframeMessages.d.ts.map +1 -0
- package/dist/components/SmartlinksFrame/useIframeResize.d.ts +18 -0
- package/dist/components/SmartlinksFrame/useIframeResize.d.ts.map +1 -0
- package/dist/context/AuthContext.d.ts +2 -1
- package/dist/context/AuthContext.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +624 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +627 -0
- package/dist/index.js.map +1 -1
- package/dist/types/iframeMessages.d.ts +122 -0
- package/dist/types/iframeMessages.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11447,6 +11447,7 @@ const tokenStorage = {
|
|
|
11447
11447
|
},
|
|
11448
11448
|
};
|
|
11449
11449
|
|
|
11450
|
+
// Export context for optional usage (e.g., SmartlinksFrame can work without AuthProvider)
|
|
11450
11451
|
const AuthContext = React.createContext(undefined);
|
|
11451
11452
|
const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
|
|
11452
11453
|
// Token refresh settings
|
|
@@ -14330,6 +14331,628 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
|
|
|
14330
14331
|
}, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
|
|
14331
14332
|
};
|
|
14332
14333
|
|
|
14334
|
+
// Re-import the constant since we can't import from types
|
|
14335
|
+
const KNOWN_PARAMS = new Set([
|
|
14336
|
+
'collectionId',
|
|
14337
|
+
'appId',
|
|
14338
|
+
'productId',
|
|
14339
|
+
'proofId',
|
|
14340
|
+
'isAdmin',
|
|
14341
|
+
'dark',
|
|
14342
|
+
'parentUrl',
|
|
14343
|
+
'theme',
|
|
14344
|
+
'lang',
|
|
14345
|
+
]);
|
|
14346
|
+
/**
|
|
14347
|
+
* Hook to handle all iframe postMessage communication.
|
|
14348
|
+
*
|
|
14349
|
+
* Handles:
|
|
14350
|
+
* - Route changes (deep linking)
|
|
14351
|
+
* - API proxy requests
|
|
14352
|
+
* - Auth messages (login/logout)
|
|
14353
|
+
* - File uploads (chunked)
|
|
14354
|
+
* - Redirects
|
|
14355
|
+
*
|
|
14356
|
+
* @param iframeRef - Ref to the iframe element
|
|
14357
|
+
* @param options - Configuration and callbacks
|
|
14358
|
+
*/
|
|
14359
|
+
function useIframeMessages(iframeRef, options) {
|
|
14360
|
+
const { collectionId, productId, proofId, cachedData, login, logout, onRouteChange, onError } = options;
|
|
14361
|
+
// Track uploads in progress
|
|
14362
|
+
const uploadsRef = React.useRef(new Map());
|
|
14363
|
+
// Track initial load to skip first route change
|
|
14364
|
+
const isInitialLoadRef = React.useRef(true);
|
|
14365
|
+
// Stable refs to avoid dependency issues
|
|
14366
|
+
const cachedDataRef = React.useRef(cachedData);
|
|
14367
|
+
const onRouteChangeRef = React.useRef(onRouteChange);
|
|
14368
|
+
const onErrorRef = React.useRef(onError);
|
|
14369
|
+
const loginRef = React.useRef(login);
|
|
14370
|
+
const logoutRef = React.useRef(logout);
|
|
14371
|
+
// Keep refs in sync
|
|
14372
|
+
React.useEffect(() => {
|
|
14373
|
+
cachedDataRef.current = cachedData;
|
|
14374
|
+
}, [cachedData]);
|
|
14375
|
+
React.useEffect(() => {
|
|
14376
|
+
onRouteChangeRef.current = onRouteChange;
|
|
14377
|
+
}, [onRouteChange]);
|
|
14378
|
+
React.useEffect(() => {
|
|
14379
|
+
onErrorRef.current = onError;
|
|
14380
|
+
}, [onError]);
|
|
14381
|
+
React.useEffect(() => {
|
|
14382
|
+
loginRef.current = login;
|
|
14383
|
+
}, [login]);
|
|
14384
|
+
React.useEffect(() => {
|
|
14385
|
+
logoutRef.current = logout;
|
|
14386
|
+
}, [logout]);
|
|
14387
|
+
// Send response back to iframe
|
|
14388
|
+
const sendResponse = React.useCallback((source, origin, message) => {
|
|
14389
|
+
if (source && 'postMessage' in source) {
|
|
14390
|
+
source.postMessage(message, origin);
|
|
14391
|
+
}
|
|
14392
|
+
}, []);
|
|
14393
|
+
// Handle route change messages (deep linking)
|
|
14394
|
+
const handleRouteChange = React.useCallback((data) => {
|
|
14395
|
+
// Skip initial load to prevent duplicating path
|
|
14396
|
+
if (isInitialLoadRef.current) {
|
|
14397
|
+
isInitialLoadRef.current = false;
|
|
14398
|
+
return;
|
|
14399
|
+
}
|
|
14400
|
+
const { context = {}, state = {}, path = '' } = data;
|
|
14401
|
+
// Filter out known iframe params, keep only app-specific state
|
|
14402
|
+
const filteredState = {};
|
|
14403
|
+
Object.entries(context).forEach(([key, value]) => {
|
|
14404
|
+
if (value != null && !KNOWN_PARAMS.has(key)) {
|
|
14405
|
+
filteredState[key] = value;
|
|
14406
|
+
}
|
|
14407
|
+
});
|
|
14408
|
+
Object.entries(state).forEach(([key, value]) => {
|
|
14409
|
+
if (value != null && !KNOWN_PARAMS.has(key)) {
|
|
14410
|
+
filteredState[key] = value;
|
|
14411
|
+
}
|
|
14412
|
+
});
|
|
14413
|
+
onRouteChangeRef.current?.(path, filteredState);
|
|
14414
|
+
}, []);
|
|
14415
|
+
// Handle API proxy requests
|
|
14416
|
+
const handleProxyRequest = React.useCallback(async (data, event) => {
|
|
14417
|
+
const response = {
|
|
14418
|
+
_smartlinksProxyResponse: true,
|
|
14419
|
+
id: data.id,
|
|
14420
|
+
};
|
|
14421
|
+
// Handle custom proxy requests (redirects, etc.)
|
|
14422
|
+
if ('_smartlinksCustomProxyRequest' in data && data._smartlinksCustomProxyRequest) {
|
|
14423
|
+
if (data.request === 'REDIRECT') {
|
|
14424
|
+
const url = data.params?.url;
|
|
14425
|
+
if (url) {
|
|
14426
|
+
window.location.href = url;
|
|
14427
|
+
}
|
|
14428
|
+
response.data = { success: true };
|
|
14429
|
+
sendResponse(event.source, event.origin, response);
|
|
14430
|
+
return;
|
|
14431
|
+
}
|
|
14432
|
+
}
|
|
14433
|
+
// Regular proxy request - narrow the type
|
|
14434
|
+
const proxyData = data;
|
|
14435
|
+
try {
|
|
14436
|
+
console.log('[SmartlinksFrame] Proxy request:', proxyData.method, proxyData.path);
|
|
14437
|
+
const path = proxyData.path.startsWith('/') ? proxyData.path.slice(1) : proxyData.path;
|
|
14438
|
+
const cached = cachedDataRef.current;
|
|
14439
|
+
// Check for cached data matches on GET requests
|
|
14440
|
+
if (proxyData.method === 'GET') {
|
|
14441
|
+
// Collection request
|
|
14442
|
+
if (path.includes('/collection/') && cached.collection) {
|
|
14443
|
+
const collectionIdMatch = path.match(/collection\/([^/]+)/);
|
|
14444
|
+
if (collectionIdMatch && collectionIdMatch[1] === collectionId) {
|
|
14445
|
+
response.data = JSON.parse(JSON.stringify(cached.collection));
|
|
14446
|
+
sendResponse(event.source, event.origin, response);
|
|
14447
|
+
return;
|
|
14448
|
+
}
|
|
14449
|
+
}
|
|
14450
|
+
// Product request
|
|
14451
|
+
if (path.includes('/product/') && cached.product && productId) {
|
|
14452
|
+
const productIdMatch = path.match(/product\/([^/]+)/);
|
|
14453
|
+
if (productIdMatch && productIdMatch[1] === productId) {
|
|
14454
|
+
response.data = JSON.parse(JSON.stringify(cached.product));
|
|
14455
|
+
sendResponse(event.source, event.origin, response);
|
|
14456
|
+
return;
|
|
14457
|
+
}
|
|
14458
|
+
}
|
|
14459
|
+
// Proof request
|
|
14460
|
+
if (path.includes('/proof/') && cached.proof && proofId) {
|
|
14461
|
+
const proofIdMatch = path.match(/proof\/([^/]+)/);
|
|
14462
|
+
if (proofIdMatch && proofIdMatch[1] === proofId) {
|
|
14463
|
+
response.data = JSON.parse(JSON.stringify(cached.proof));
|
|
14464
|
+
sendResponse(event.source, event.origin, response);
|
|
14465
|
+
return;
|
|
14466
|
+
}
|
|
14467
|
+
}
|
|
14468
|
+
// Account request
|
|
14469
|
+
if (path.includes('/account') && cached.user) {
|
|
14470
|
+
response.data = JSON.parse(JSON.stringify({
|
|
14471
|
+
...cached.user.accountData,
|
|
14472
|
+
uid: cached.user.uid,
|
|
14473
|
+
email: cached.user.email,
|
|
14474
|
+
displayName: cached.user.displayName,
|
|
14475
|
+
}));
|
|
14476
|
+
sendResponse(event.source, event.origin, response);
|
|
14477
|
+
return;
|
|
14478
|
+
}
|
|
14479
|
+
}
|
|
14480
|
+
// Forward to actual API using SDK's internal request method
|
|
14481
|
+
// Note: The SDK should handle this via proxy mode, but we're explicitly forwarding
|
|
14482
|
+
const apiMethod = proxyData.method.toLowerCase();
|
|
14483
|
+
// Use the SDK's http utilities
|
|
14484
|
+
let result;
|
|
14485
|
+
switch (apiMethod) {
|
|
14486
|
+
case 'get':
|
|
14487
|
+
result = await smartlinks__namespace.http?.get?.(path) ??
|
|
14488
|
+
await fetch(`/api/v1/${path}`).then(r => r.json());
|
|
14489
|
+
break;
|
|
14490
|
+
case 'post':
|
|
14491
|
+
result = await smartlinks__namespace.http?.post?.(path, proxyData.body) ??
|
|
14492
|
+
await fetch(`/api/v1/${path}`, { method: 'POST', body: JSON.stringify(proxyData.body) }).then(r => r.json());
|
|
14493
|
+
break;
|
|
14494
|
+
case 'put':
|
|
14495
|
+
result = await smartlinks__namespace.http?.put?.(path, proxyData.body) ??
|
|
14496
|
+
await fetch(`/api/v1/${path}`, { method: 'PUT', body: JSON.stringify(proxyData.body) }).then(r => r.json());
|
|
14497
|
+
break;
|
|
14498
|
+
case 'patch':
|
|
14499
|
+
result = await smartlinks__namespace.http?.patch?.(path, proxyData.body) ??
|
|
14500
|
+
await fetch(`/api/v1/${path}`, { method: 'PATCH', body: JSON.stringify(proxyData.body) }).then(r => r.json());
|
|
14501
|
+
break;
|
|
14502
|
+
case 'delete':
|
|
14503
|
+
result = await smartlinks__namespace.http?.delete?.(path) ??
|
|
14504
|
+
await fetch(`/api/v1/${path}`, { method: 'DELETE' }).then(r => r.json());
|
|
14505
|
+
break;
|
|
14506
|
+
}
|
|
14507
|
+
response.data = result;
|
|
14508
|
+
}
|
|
14509
|
+
catch (err) {
|
|
14510
|
+
console.error('[SmartlinksFrame] Proxy error:', err);
|
|
14511
|
+
response.error = err?.message || 'Unknown error';
|
|
14512
|
+
onErrorRef.current?.(err);
|
|
14513
|
+
}
|
|
14514
|
+
sendResponse(event.source, event.origin, response);
|
|
14515
|
+
}, [collectionId, productId, proofId, sendResponse]);
|
|
14516
|
+
// Handle standardized iframe messages (auth, resize, redirect)
|
|
14517
|
+
const handleStandardMessage = React.useCallback(async (data, event) => {
|
|
14518
|
+
console.log('[SmartlinksFrame] Iframe message:', data.type);
|
|
14519
|
+
switch (data.type) {
|
|
14520
|
+
case 'smartlinks:resize': {
|
|
14521
|
+
// Handled by useIframeResize hook
|
|
14522
|
+
break;
|
|
14523
|
+
}
|
|
14524
|
+
case 'smartlinks:redirect': {
|
|
14525
|
+
const url = data.payload?.url;
|
|
14526
|
+
if (url && typeof url === 'string') {
|
|
14527
|
+
window.location.href = url;
|
|
14528
|
+
}
|
|
14529
|
+
break;
|
|
14530
|
+
}
|
|
14531
|
+
case 'smartlinks:authkit:login': {
|
|
14532
|
+
const { token, user: iframeUser, accountData, messageId } = data.payload || {};
|
|
14533
|
+
// If no login function available (no AuthProvider), just acknowledge
|
|
14534
|
+
if (!loginRef.current) {
|
|
14535
|
+
console.log('[SmartlinksFrame] No AuthProvider - ignoring auth login');
|
|
14536
|
+
sendResponse(event.source, event.origin, {
|
|
14537
|
+
type: 'smartlinks:authkit:login-acknowledged',
|
|
14538
|
+
messageId,
|
|
14539
|
+
success: false,
|
|
14540
|
+
error: 'No AuthProvider available',
|
|
14541
|
+
});
|
|
14542
|
+
break;
|
|
14543
|
+
}
|
|
14544
|
+
try {
|
|
14545
|
+
// Validate token using SDK
|
|
14546
|
+
console.log('[SmartlinksFrame] Validating auth token...');
|
|
14547
|
+
await smartlinks__namespace.auth.verifyToken(token);
|
|
14548
|
+
// Use AuthProvider's login to persist session
|
|
14549
|
+
await loginRef.current(token, iframeUser, accountData);
|
|
14550
|
+
// Send acknowledgment
|
|
14551
|
+
sendResponse(event.source, event.origin, {
|
|
14552
|
+
type: 'smartlinks:authkit:login-acknowledged',
|
|
14553
|
+
messageId,
|
|
14554
|
+
success: true,
|
|
14555
|
+
});
|
|
14556
|
+
console.log('[SmartlinksFrame] Auth login acknowledged');
|
|
14557
|
+
}
|
|
14558
|
+
catch (err) {
|
|
14559
|
+
console.error('[SmartlinksFrame] Auth login failed:', err);
|
|
14560
|
+
sendResponse(event.source, event.origin, {
|
|
14561
|
+
type: 'smartlinks:authkit:login-acknowledged',
|
|
14562
|
+
messageId,
|
|
14563
|
+
success: false,
|
|
14564
|
+
error: err?.message || 'Token validation failed',
|
|
14565
|
+
});
|
|
14566
|
+
onErrorRef.current?.(err);
|
|
14567
|
+
}
|
|
14568
|
+
break;
|
|
14569
|
+
}
|
|
14570
|
+
case 'smartlinks:authkit:logout': {
|
|
14571
|
+
console.log('[SmartlinksFrame] Processing logout from iframe');
|
|
14572
|
+
if (logoutRef.current) {
|
|
14573
|
+
await logoutRef.current();
|
|
14574
|
+
}
|
|
14575
|
+
else {
|
|
14576
|
+
console.log('[SmartlinksFrame] No AuthProvider - ignoring logout');
|
|
14577
|
+
}
|
|
14578
|
+
break;
|
|
14579
|
+
}
|
|
14580
|
+
case 'smartlinks:authkit:redirect': {
|
|
14581
|
+
const url = data.payload?.url;
|
|
14582
|
+
if (url && typeof url === 'string') {
|
|
14583
|
+
window.location.href = url;
|
|
14584
|
+
}
|
|
14585
|
+
break;
|
|
14586
|
+
}
|
|
14587
|
+
}
|
|
14588
|
+
}, [sendResponse]);
|
|
14589
|
+
// Handle chunked file uploads
|
|
14590
|
+
const handleUpload = React.useCallback(async (data, event) => {
|
|
14591
|
+
const uploads = uploadsRef.current;
|
|
14592
|
+
switch (data.phase) {
|
|
14593
|
+
case 'start': {
|
|
14594
|
+
const startData = data;
|
|
14595
|
+
uploads.set(startData.id, {
|
|
14596
|
+
chunks: [],
|
|
14597
|
+
fields: startData.fields,
|
|
14598
|
+
fileInfo: startData.fileInfo,
|
|
14599
|
+
path: startData.path,
|
|
14600
|
+
});
|
|
14601
|
+
break;
|
|
14602
|
+
}
|
|
14603
|
+
case 'chunk': {
|
|
14604
|
+
const chunkData = data;
|
|
14605
|
+
const upload = uploads.get(chunkData.id);
|
|
14606
|
+
if (upload) {
|
|
14607
|
+
// Convert ArrayBuffer to Uint8Array and store as regular array buffer
|
|
14608
|
+
const uint8Array = new Uint8Array(chunkData.chunk);
|
|
14609
|
+
upload.chunks.push(uint8Array);
|
|
14610
|
+
sendResponse(event.source, event.origin, {
|
|
14611
|
+
_smartlinksProxyUpload: true,
|
|
14612
|
+
phase: 'ack',
|
|
14613
|
+
id: chunkData.id,
|
|
14614
|
+
seq: chunkData.seq,
|
|
14615
|
+
});
|
|
14616
|
+
}
|
|
14617
|
+
break;
|
|
14618
|
+
}
|
|
14619
|
+
case 'end': {
|
|
14620
|
+
const endData = data;
|
|
14621
|
+
const upload = uploads.get(endData.id);
|
|
14622
|
+
if (!upload)
|
|
14623
|
+
break;
|
|
14624
|
+
try {
|
|
14625
|
+
// Reconstruct file from chunks - convert to regular arrays for Blob
|
|
14626
|
+
const blobParts = upload.chunks.map(chunk => chunk.buffer.slice(0));
|
|
14627
|
+
const blob = new Blob(blobParts, {
|
|
14628
|
+
type: upload.fileInfo.type || 'application/octet-stream'
|
|
14629
|
+
});
|
|
14630
|
+
const formData = new FormData();
|
|
14631
|
+
upload.fields.forEach(([key, value]) => formData.append(key, value));
|
|
14632
|
+
formData.append(upload.fileInfo.key || 'file', blob, upload.fileInfo.name || 'upload.bin');
|
|
14633
|
+
// Upload via SDK or direct fetch
|
|
14634
|
+
const path = upload.path.startsWith('/') ? upload.path.slice(1) : upload.path;
|
|
14635
|
+
const baseUrl = smartlinks__namespace.getBaseUrl?.() || '/api/v1';
|
|
14636
|
+
const response = await fetch(`${baseUrl}/${path}`, {
|
|
14637
|
+
method: 'POST',
|
|
14638
|
+
body: formData,
|
|
14639
|
+
// Let browser set Content-Type with boundary
|
|
14640
|
+
});
|
|
14641
|
+
if (!response.ok) {
|
|
14642
|
+
throw new Error(`Upload failed: ${response.status}`);
|
|
14643
|
+
}
|
|
14644
|
+
const result = await response.json();
|
|
14645
|
+
sendResponse(event.source, event.origin, {
|
|
14646
|
+
_smartlinksProxyUpload: true,
|
|
14647
|
+
phase: 'done',
|
|
14648
|
+
id: endData.id,
|
|
14649
|
+
ok: true,
|
|
14650
|
+
data: result,
|
|
14651
|
+
});
|
|
14652
|
+
}
|
|
14653
|
+
catch (err) {
|
|
14654
|
+
console.error('[SmartlinksFrame] Upload failed:', err);
|
|
14655
|
+
sendResponse(event.source, event.origin, {
|
|
14656
|
+
_smartlinksProxyUpload: true,
|
|
14657
|
+
phase: 'done',
|
|
14658
|
+
id: endData.id,
|
|
14659
|
+
ok: false,
|
|
14660
|
+
error: err?.message || 'Upload failed',
|
|
14661
|
+
});
|
|
14662
|
+
onErrorRef.current?.(err);
|
|
14663
|
+
}
|
|
14664
|
+
uploads.delete(endData.id);
|
|
14665
|
+
break;
|
|
14666
|
+
}
|
|
14667
|
+
}
|
|
14668
|
+
}, [sendResponse]);
|
|
14669
|
+
// Main message handler
|
|
14670
|
+
React.useEffect(() => {
|
|
14671
|
+
const handleMessage = async (event) => {
|
|
14672
|
+
// Validate source is our iframe
|
|
14673
|
+
if (!iframeRef.current || event.source !== iframeRef.current.contentWindow) {
|
|
14674
|
+
return;
|
|
14675
|
+
}
|
|
14676
|
+
const data = event.data;
|
|
14677
|
+
if (!data || typeof data !== 'object')
|
|
14678
|
+
return;
|
|
14679
|
+
// Route changes (deep linking)
|
|
14680
|
+
if (data.type === 'smartlinks-route-change') {
|
|
14681
|
+
handleRouteChange(data);
|
|
14682
|
+
return;
|
|
14683
|
+
}
|
|
14684
|
+
// Standardized iframe messages
|
|
14685
|
+
if (data._smartlinksIframeMessage) {
|
|
14686
|
+
await handleStandardMessage(data, event);
|
|
14687
|
+
return;
|
|
14688
|
+
}
|
|
14689
|
+
// File upload proxy
|
|
14690
|
+
if (data._smartlinksProxyUpload) {
|
|
14691
|
+
await handleUpload(data, event);
|
|
14692
|
+
return;
|
|
14693
|
+
}
|
|
14694
|
+
// API proxy requests
|
|
14695
|
+
if (data._smartlinksProxyRequest) {
|
|
14696
|
+
await handleProxyRequest(data, event);
|
|
14697
|
+
return;
|
|
14698
|
+
}
|
|
14699
|
+
};
|
|
14700
|
+
window.addEventListener('message', handleMessage);
|
|
14701
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
14702
|
+
}, [iframeRef, handleRouteChange, handleStandardMessage, handleUpload, handleProxyRequest]);
|
|
14703
|
+
}
|
|
14704
|
+
|
|
14705
|
+
/**
|
|
14706
|
+
* Hook to handle iframe height management.
|
|
14707
|
+
*
|
|
14708
|
+
* Supports two modes:
|
|
14709
|
+
* 1. Viewport-based: Calculates height based on iframe position and viewport
|
|
14710
|
+
* 2. Content-based: Listens for smartlinks:resize messages from the iframe
|
|
14711
|
+
*
|
|
14712
|
+
* @param iframeRef - Ref to the iframe element
|
|
14713
|
+
* @param options - Resize configuration options
|
|
14714
|
+
* @returns Current calculated height in pixels
|
|
14715
|
+
*/
|
|
14716
|
+
function useIframeResize(iframeRef, options = {}) {
|
|
14717
|
+
const { autoResize = true, minHeight, maxHeight } = options;
|
|
14718
|
+
const [height, setHeight] = React.useState(0);
|
|
14719
|
+
// Calculate height based on viewport position
|
|
14720
|
+
const calculateHeight = React.useCallback(() => {
|
|
14721
|
+
const iframe = iframeRef.current;
|
|
14722
|
+
if (!iframe)
|
|
14723
|
+
return;
|
|
14724
|
+
const container = iframe.parentElement;
|
|
14725
|
+
if (!container)
|
|
14726
|
+
return;
|
|
14727
|
+
const rect = container.getBoundingClientRect();
|
|
14728
|
+
const viewportHeight = window.innerHeight;
|
|
14729
|
+
let calculatedHeight = Math.max(0, viewportHeight - rect.top);
|
|
14730
|
+
// Apply constraints
|
|
14731
|
+
if (minHeight !== undefined) {
|
|
14732
|
+
calculatedHeight = Math.max(calculatedHeight, minHeight);
|
|
14733
|
+
}
|
|
14734
|
+
if (maxHeight !== undefined) {
|
|
14735
|
+
calculatedHeight = Math.min(calculatedHeight, maxHeight);
|
|
14736
|
+
}
|
|
14737
|
+
setHeight(calculatedHeight);
|
|
14738
|
+
}, [minHeight, maxHeight, iframeRef]);
|
|
14739
|
+
// Handle viewport resize events
|
|
14740
|
+
React.useEffect(() => {
|
|
14741
|
+
if (!autoResize)
|
|
14742
|
+
return;
|
|
14743
|
+
// Initial calculation with multiple retries for late layout
|
|
14744
|
+
calculateHeight();
|
|
14745
|
+
const t1 = setTimeout(calculateHeight, 10);
|
|
14746
|
+
const t2 = setTimeout(calculateHeight, 100);
|
|
14747
|
+
const t3 = setTimeout(calculateHeight, 500);
|
|
14748
|
+
const handleResize = () => calculateHeight();
|
|
14749
|
+
window.addEventListener('resize', handleResize);
|
|
14750
|
+
window.addEventListener('orientationchange', handleResize);
|
|
14751
|
+
return () => {
|
|
14752
|
+
clearTimeout(t1);
|
|
14753
|
+
clearTimeout(t2);
|
|
14754
|
+
clearTimeout(t3);
|
|
14755
|
+
window.removeEventListener('resize', handleResize);
|
|
14756
|
+
window.removeEventListener('orientationchange', handleResize);
|
|
14757
|
+
};
|
|
14758
|
+
}, [autoResize, calculateHeight]);
|
|
14759
|
+
// Listen for content-based resize messages from iframe
|
|
14760
|
+
React.useEffect(() => {
|
|
14761
|
+
const handleMessage = (event) => {
|
|
14762
|
+
// Validate source is our iframe
|
|
14763
|
+
if (iframeRef.current && event.source !== iframeRef.current.contentWindow) {
|
|
14764
|
+
return;
|
|
14765
|
+
}
|
|
14766
|
+
const data = event.data;
|
|
14767
|
+
// Handle smartlinks:resize message
|
|
14768
|
+
if (data?._smartlinksIframeMessage && data?.type === 'smartlinks:resize') {
|
|
14769
|
+
const contentHeight = data.payload?.height;
|
|
14770
|
+
if (typeof contentHeight === 'number' && contentHeight > 0) {
|
|
14771
|
+
let newHeight = contentHeight;
|
|
14772
|
+
// Apply constraints
|
|
14773
|
+
if (minHeight !== undefined) {
|
|
14774
|
+
newHeight = Math.max(newHeight, minHeight);
|
|
14775
|
+
}
|
|
14776
|
+
if (maxHeight !== undefined) {
|
|
14777
|
+
newHeight = Math.min(newHeight, maxHeight);
|
|
14778
|
+
}
|
|
14779
|
+
setHeight(newHeight);
|
|
14780
|
+
}
|
|
14781
|
+
}
|
|
14782
|
+
};
|
|
14783
|
+
window.addEventListener('message', handleMessage);
|
|
14784
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
14785
|
+
}, [iframeRef, minHeight, maxHeight]);
|
|
14786
|
+
return height;
|
|
14787
|
+
}
|
|
14788
|
+
|
|
14789
|
+
/**
|
|
14790
|
+
* Hook to detect if the current user is an admin of the collection or proof.
|
|
14791
|
+
*
|
|
14792
|
+
* Admin status is determined by checking if the user's UID exists in the
|
|
14793
|
+
* `roles` object of either the collection or proof.
|
|
14794
|
+
*
|
|
14795
|
+
* @param collection - Collection object with optional roles
|
|
14796
|
+
* @param proof - Proof object with optional roles
|
|
14797
|
+
* @param user - Current authenticated user
|
|
14798
|
+
* @returns boolean indicating admin status
|
|
14799
|
+
*/
|
|
14800
|
+
function useAdminDetection(collection, proof, user) {
|
|
14801
|
+
return React.useMemo(() => {
|
|
14802
|
+
if (!user?.uid)
|
|
14803
|
+
return false;
|
|
14804
|
+
// Check collection roles
|
|
14805
|
+
if (collection?.roles && collection.roles[user.uid]) {
|
|
14806
|
+
return true;
|
|
14807
|
+
}
|
|
14808
|
+
// Check proof roles (for proof-level admin)
|
|
14809
|
+
if (proof?.roles && proof.roles[user.uid]) {
|
|
14810
|
+
return true;
|
|
14811
|
+
}
|
|
14812
|
+
return false;
|
|
14813
|
+
}, [collection?.roles, proof?.roles, user?.uid]);
|
|
14814
|
+
}
|
|
14815
|
+
|
|
14816
|
+
/**
|
|
14817
|
+
* SmartlinksFrame - Parent-side iframe wrapper for embedding SmartLinks microapps.
|
|
14818
|
+
*
|
|
14819
|
+
* This component handles all bidirectional communication with the embedded iframe:
|
|
14820
|
+
* - API proxy requests (with optional caching)
|
|
14821
|
+
* - Authentication state synchronization (when inside AuthProvider)
|
|
14822
|
+
* - Deep linking / route changes
|
|
14823
|
+
* - Resize management
|
|
14824
|
+
* - File upload proxying
|
|
14825
|
+
*
|
|
14826
|
+
* Can be used with or without AuthProvider - authentication is optional.
|
|
14827
|
+
*
|
|
14828
|
+
* @example
|
|
14829
|
+
* ```tsx
|
|
14830
|
+
* // With authentication
|
|
14831
|
+
* <AuthProvider collectionId="my-collection">
|
|
14832
|
+
* <SmartlinksFrame
|
|
14833
|
+
* collectionId="my-collection"
|
|
14834
|
+
* appId="warranty-app"
|
|
14835
|
+
* appUrl="https://warranty.lovable.app"
|
|
14836
|
+
* collection={collectionData}
|
|
14837
|
+
* />
|
|
14838
|
+
* </AuthProvider>
|
|
14839
|
+
*
|
|
14840
|
+
* // Without authentication (public/anonymous access)
|
|
14841
|
+
* <SmartlinksFrame
|
|
14842
|
+
* collectionId="my-collection"
|
|
14843
|
+
* appId="info-app"
|
|
14844
|
+
* appUrl="https://info-app.lovable.app"
|
|
14845
|
+
* />
|
|
14846
|
+
* ```
|
|
14847
|
+
*/
|
|
14848
|
+
const SmartlinksFrame = ({ collectionId, appId, productId, proofId, appUrl, version = 'stable', collection, product, proof, initialPath, onRouteChange, autoResize = true, minHeight, maxHeight, onReady, onError, className, style, }) => {
|
|
14849
|
+
const iframeRef = React.useRef(null);
|
|
14850
|
+
// Get auth context if available (optional - SmartlinksFrame works without AuthProvider)
|
|
14851
|
+
const authContext = React.useContext(AuthContext);
|
|
14852
|
+
const user = authContext?.user ?? null;
|
|
14853
|
+
const login = authContext?.login;
|
|
14854
|
+
const logout = authContext?.logout;
|
|
14855
|
+
// Compute isAdmin from collection/proof roles
|
|
14856
|
+
const isAdmin = useAdminDetection(collection, proof, user);
|
|
14857
|
+
// Handle resize
|
|
14858
|
+
const height = useIframeResize(iframeRef, { autoResize, minHeight, maxHeight });
|
|
14859
|
+
// Handle all iframe messages
|
|
14860
|
+
useIframeMessages(iframeRef, {
|
|
14861
|
+
collectionId,
|
|
14862
|
+
productId,
|
|
14863
|
+
proofId,
|
|
14864
|
+
cachedData: { collection, product, proof, user },
|
|
14865
|
+
login,
|
|
14866
|
+
logout,
|
|
14867
|
+
onRouteChange,
|
|
14868
|
+
onError,
|
|
14869
|
+
});
|
|
14870
|
+
// Build iframe URL with all context params
|
|
14871
|
+
const frameSrc = React.useMemo(() => {
|
|
14872
|
+
const params = new URLSearchParams();
|
|
14873
|
+
// Required context
|
|
14874
|
+
params.set('collectionId', collectionId);
|
|
14875
|
+
params.set('appId', appId);
|
|
14876
|
+
// Optional context
|
|
14877
|
+
if (productId)
|
|
14878
|
+
params.set('productId', productId);
|
|
14879
|
+
if (proofId)
|
|
14880
|
+
params.set('proofId', proofId);
|
|
14881
|
+
if (isAdmin)
|
|
14882
|
+
params.set('isAdmin', 'true');
|
|
14883
|
+
// Dark mode from collection
|
|
14884
|
+
const isDark = collection?.dark ?? false;
|
|
14885
|
+
params.set('dark', isDark ? '1' : '0');
|
|
14886
|
+
// Include parent URL for downstream redirects/context
|
|
14887
|
+
try {
|
|
14888
|
+
params.set('parentUrl', window.location.href);
|
|
14889
|
+
}
|
|
14890
|
+
catch {
|
|
14891
|
+
// Ignore if can't access location
|
|
14892
|
+
}
|
|
14893
|
+
// Encode theme from collection if available
|
|
14894
|
+
if (collection) {
|
|
14895
|
+
try {
|
|
14896
|
+
const themeData = {
|
|
14897
|
+
p: collection.primaryColor,
|
|
14898
|
+
s: collection.secondaryColor,
|
|
14899
|
+
m: collection.dark ? 'd' : 'l',
|
|
14900
|
+
};
|
|
14901
|
+
// Only include if we have meaningful data
|
|
14902
|
+
if (themeData.p || themeData.s) {
|
|
14903
|
+
params.set('theme', btoa(JSON.stringify(themeData)));
|
|
14904
|
+
}
|
|
14905
|
+
}
|
|
14906
|
+
catch {
|
|
14907
|
+
// Ignore encoding errors
|
|
14908
|
+
}
|
|
14909
|
+
}
|
|
14910
|
+
// Determine base URL
|
|
14911
|
+
let base = appUrl;
|
|
14912
|
+
// If appUrl doesn't look like a full URL, construct from smartlinks.app
|
|
14913
|
+
if (!appUrl.startsWith('http')) {
|
|
14914
|
+
base = `https://smartlinks.app/apps/${appId}/${version}`;
|
|
14915
|
+
}
|
|
14916
|
+
// Clean up base URL
|
|
14917
|
+
base = base.replace(/#\/?$/, ''); // Remove trailing hash
|
|
14918
|
+
if (base.endsWith('/')) {
|
|
14919
|
+
base = base.slice(0, -1);
|
|
14920
|
+
}
|
|
14921
|
+
// Build hash path
|
|
14922
|
+
let hashPath = initialPath || '';
|
|
14923
|
+
if (hashPath && !hashPath.startsWith('/')) {
|
|
14924
|
+
hashPath = '/' + hashPath;
|
|
14925
|
+
}
|
|
14926
|
+
if (hashPath === '/') {
|
|
14927
|
+
hashPath = '';
|
|
14928
|
+
}
|
|
14929
|
+
// Construct final URL: base#/path?params
|
|
14930
|
+
return `${base}/#/${hashPath}?${params.toString()}`.replace('/#//', '/#/');
|
|
14931
|
+
}, [
|
|
14932
|
+
collectionId,
|
|
14933
|
+
appId,
|
|
14934
|
+
productId,
|
|
14935
|
+
proofId,
|
|
14936
|
+
appUrl,
|
|
14937
|
+
version,
|
|
14938
|
+
initialPath,
|
|
14939
|
+
isAdmin,
|
|
14940
|
+
collection,
|
|
14941
|
+
]);
|
|
14942
|
+
// Calculate container style
|
|
14943
|
+
const containerStyle = {
|
|
14944
|
+
height: autoResize && height > 0 ? `${height}px` : '100%',
|
|
14945
|
+
width: '100%',
|
|
14946
|
+
...style,
|
|
14947
|
+
};
|
|
14948
|
+
return (jsxRuntime.jsx("div", { className: className, style: containerStyle, children: jsxRuntime.jsx("iframe", { ref: iframeRef, src: frameSrc, frameBorder: 0, width: "100%", height: "100%", allow: "camera;microphone;fullscreen;geolocation;identity-credentials-get", onLoad: onReady, style: {
|
|
14949
|
+
display: 'block',
|
|
14950
|
+
width: '100%',
|
|
14951
|
+
height: '100%',
|
|
14952
|
+
border: 0,
|
|
14953
|
+
} }) }));
|
|
14954
|
+
};
|
|
14955
|
+
|
|
14333
14956
|
const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
|
|
14334
14957
|
const { isAuthenticated, isLoading } = useAuth();
|
|
14335
14958
|
// Show loading state
|
|
@@ -14407,6 +15030,7 @@ exports.FirebaseAuthUI = SmartlinksAuthUI;
|
|
|
14407
15030
|
exports.ProtectedRoute = ProtectedRoute;
|
|
14408
15031
|
exports.SchemaFieldRenderer = SchemaFieldRenderer;
|
|
14409
15032
|
exports.SmartlinksAuthUI = SmartlinksAuthUI;
|
|
15033
|
+
exports.SmartlinksFrame = SmartlinksFrame;
|
|
14410
15034
|
exports.getDefaultAuthKitId = getDefaultAuthKitId;
|
|
14411
15035
|
exports.getEditableFields = getEditableFields;
|
|
14412
15036
|
exports.getErrorCode = getErrorCode;
|
|
@@ -14420,5 +15044,8 @@ exports.isServerError = isServerError;
|
|
|
14420
15044
|
exports.setDefaultAuthKitId = setDefaultAuthKitId;
|
|
14421
15045
|
exports.sortFieldsByPlacement = sortFieldsByPlacement;
|
|
14422
15046
|
exports.tokenStorage = tokenStorage;
|
|
15047
|
+
exports.useAdminDetection = useAdminDetection;
|
|
14423
15048
|
exports.useAuth = useAuth;
|
|
15049
|
+
exports.useIframeMessages = useIframeMessages;
|
|
15050
|
+
exports.useIframeResize = useIframeResize;
|
|
14424
15051
|
//# sourceMappingURL=index.js.map
|