@proveanything/smartlinks-auth-ui 0.3.1 → 0.3.3
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 +11 -21
- package/dist/components/SmartlinksFrame/SmartlinksFrame.d.ts.map +1 -1
- package/dist/components/SmartlinksFrame/helpers.d.ts +19 -0
- package/dist/components/SmartlinksFrame/helpers.d.ts.map +1 -0
- package/dist/components/SmartlinksFrame/index.d.ts +2 -2
- package/dist/components/SmartlinksFrame/index.d.ts.map +1 -1
- package/dist/components/SmartlinksFrame/useIframeMessages.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +302 -257
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +303 -256
- package/dist/index.js.map +1 -1
- package/dist/types/iframeMessages.d.ts +81 -40
- package/dist/types/iframeMessages.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14331,6 +14331,291 @@ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, class
|
|
|
14331
14331
|
}, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
|
|
14332
14332
|
};
|
|
14333
14333
|
|
|
14334
|
+
/**
|
|
14335
|
+
* Helper functions for SmartlinksFrame
|
|
14336
|
+
* Separated to avoid circular dependencies
|
|
14337
|
+
*/
|
|
14338
|
+
/**
|
|
14339
|
+
* Compute admin status from collection/proof roles.
|
|
14340
|
+
*/
|
|
14341
|
+
function isAdminFromRoles(user, collection, proof) {
|
|
14342
|
+
if (!user?.uid)
|
|
14343
|
+
return false;
|
|
14344
|
+
const proofRole = proof?.roles?.[user.uid];
|
|
14345
|
+
if (proofRole === 'admin' || proofRole === 'owner')
|
|
14346
|
+
return true;
|
|
14347
|
+
const collectionRole = collection?.roles?.[user.uid];
|
|
14348
|
+
if (collectionRole === 'admin' || collectionRole === 'owner')
|
|
14349
|
+
return true;
|
|
14350
|
+
return false;
|
|
14351
|
+
}
|
|
14352
|
+
/**
|
|
14353
|
+
* Build iframe src URL with query parameters.
|
|
14354
|
+
*/
|
|
14355
|
+
function buildIframeSrc(baseUrl, params) {
|
|
14356
|
+
const url = new URL(baseUrl);
|
|
14357
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
14358
|
+
if (value !== undefined) {
|
|
14359
|
+
url.searchParams.set(key, value);
|
|
14360
|
+
}
|
|
14361
|
+
});
|
|
14362
|
+
return url.toString();
|
|
14363
|
+
}
|
|
14364
|
+
|
|
14365
|
+
/**
|
|
14366
|
+
* SmartlinksFrame - React wrapper for embedding SmartLinks microapps.
|
|
14367
|
+
*
|
|
14368
|
+
* This component handles iframe creation and message passing for SmartLinks apps.
|
|
14369
|
+
* It requires only collectionId and appId - the SDK handles URL discovery and API proxying.
|
|
14370
|
+
*
|
|
14371
|
+
* @example
|
|
14372
|
+
* ```tsx
|
|
14373
|
+
* // Simple usage - SDK resolves app URL automatically
|
|
14374
|
+
* <SmartlinksFrame
|
|
14375
|
+
* collectionId="my-collection"
|
|
14376
|
+
* appId="warranty-app"
|
|
14377
|
+
* />
|
|
14378
|
+
*
|
|
14379
|
+
* // With optional product context
|
|
14380
|
+
* <SmartlinksFrame
|
|
14381
|
+
* collectionId="my-collection"
|
|
14382
|
+
* appId="warranty-app"
|
|
14383
|
+
* productId="product-123"
|
|
14384
|
+
* />
|
|
14385
|
+
* ```
|
|
14386
|
+
*/
|
|
14387
|
+
const SmartlinksFrame = ({ collectionId, appId, productId, proofId, version = 'stable', collection, product: _product, // Destructured but currently unused - kept for cache optimization
|
|
14388
|
+
proof, initialPath, onRouteChange, autoResize = true, minHeight, maxHeight, onReady, onError, className, style, }) => {
|
|
14389
|
+
const iframeRef = React.useRef(null);
|
|
14390
|
+
const [src, setSrc] = React.useState(null);
|
|
14391
|
+
const [height, setHeight] = React.useState(minHeight || 400);
|
|
14392
|
+
const [error, setError] = React.useState(null);
|
|
14393
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
14394
|
+
// Get auth context if available (optional - works without AuthProvider)
|
|
14395
|
+
const authContext = React.useContext(AuthContext);
|
|
14396
|
+
const user = authContext?.user ?? null;
|
|
14397
|
+
// Compute isAdmin from collection/proof roles
|
|
14398
|
+
const isAdmin = isAdminFromRoles(user ? { uid: user.uid } : null, collection, proof);
|
|
14399
|
+
// Handle messages from iframe
|
|
14400
|
+
const handleMessage = React.useCallback((event) => {
|
|
14401
|
+
const data = event.data;
|
|
14402
|
+
if (!data || typeof data !== 'object')
|
|
14403
|
+
return;
|
|
14404
|
+
// Handle resize messages
|
|
14405
|
+
if (data.type === 'smartlinks:resize' && autoResize) {
|
|
14406
|
+
let newHeight = data.payload?.height || data.height;
|
|
14407
|
+
if (typeof newHeight === 'number') {
|
|
14408
|
+
if (minHeight)
|
|
14409
|
+
newHeight = Math.max(newHeight, minHeight);
|
|
14410
|
+
if (maxHeight)
|
|
14411
|
+
newHeight = Math.min(newHeight, maxHeight);
|
|
14412
|
+
setHeight(newHeight);
|
|
14413
|
+
}
|
|
14414
|
+
}
|
|
14415
|
+
// Handle route change messages
|
|
14416
|
+
if (data.type === 'smartlinks-route-change' && onRouteChange) {
|
|
14417
|
+
onRouteChange(data.path || '/', data.state || {});
|
|
14418
|
+
}
|
|
14419
|
+
// Handle ready message
|
|
14420
|
+
if (data.type === 'smartlinks:ready') {
|
|
14421
|
+
setIsLoading(false);
|
|
14422
|
+
onReady?.();
|
|
14423
|
+
}
|
|
14424
|
+
}, [autoResize, minHeight, maxHeight, onRouteChange, onReady]);
|
|
14425
|
+
// Set up message listener
|
|
14426
|
+
React.useEffect(() => {
|
|
14427
|
+
window.addEventListener('message', handleMessage);
|
|
14428
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
14429
|
+
}, [handleMessage]);
|
|
14430
|
+
// Resolve app URL and set iframe src
|
|
14431
|
+
React.useEffect(() => {
|
|
14432
|
+
setError(null);
|
|
14433
|
+
setIsLoading(true);
|
|
14434
|
+
setSrc(null);
|
|
14435
|
+
// Try to get app URL from collection apps config
|
|
14436
|
+
const resolveAppUrl = async () => {
|
|
14437
|
+
try {
|
|
14438
|
+
// Use SDK to get collection data which may include apps config
|
|
14439
|
+
const collectionData = await smartlinks__namespace.collection.get(collectionId);
|
|
14440
|
+
const collectionAsAny = collectionData;
|
|
14441
|
+
const apps = collectionAsAny.apps ?? [];
|
|
14442
|
+
const app = apps.find((a) => a.id === appId);
|
|
14443
|
+
if (!app?.url) {
|
|
14444
|
+
throw new Error(`App "${appId}" not found in collection "${collectionId}"`);
|
|
14445
|
+
}
|
|
14446
|
+
// Get the appropriate URL based on version
|
|
14447
|
+
let baseUrl = app.url;
|
|
14448
|
+
if (version === 'development' && app.versions?.development) {
|
|
14449
|
+
baseUrl = app.versions.development;
|
|
14450
|
+
}
|
|
14451
|
+
else if (version === 'stable' && app.versions?.stable) {
|
|
14452
|
+
baseUrl = app.versions.stable;
|
|
14453
|
+
}
|
|
14454
|
+
// Build iframe URL with context params
|
|
14455
|
+
const iframeSrc = buildIframeSrc(baseUrl, {
|
|
14456
|
+
collectionId,
|
|
14457
|
+
appId,
|
|
14458
|
+
productId,
|
|
14459
|
+
proofId,
|
|
14460
|
+
isAdmin: isAdmin ? '1' : '0',
|
|
14461
|
+
dark: document.documentElement.classList.contains('dark') ? '1' : '0',
|
|
14462
|
+
});
|
|
14463
|
+
// Add initial path if provided
|
|
14464
|
+
const finalSrc = initialPath
|
|
14465
|
+
? `${iframeSrc}#${initialPath}`
|
|
14466
|
+
: iframeSrc;
|
|
14467
|
+
setSrc(finalSrc);
|
|
14468
|
+
setIsLoading(false);
|
|
14469
|
+
}
|
|
14470
|
+
catch (err) {
|
|
14471
|
+
const errorMsg = err instanceof Error ? err.message : 'Failed to resolve app URL';
|
|
14472
|
+
console.error('[SmartlinksFrame] Error:', errorMsg);
|
|
14473
|
+
setError(errorMsg);
|
|
14474
|
+
setIsLoading(false);
|
|
14475
|
+
onError?.(err instanceof Error ? err : new Error(errorMsg));
|
|
14476
|
+
}
|
|
14477
|
+
};
|
|
14478
|
+
resolveAppUrl();
|
|
14479
|
+
}, [collectionId, appId, productId, proofId, version, initialPath, isAdmin, onError]);
|
|
14480
|
+
// Calculate container style
|
|
14481
|
+
const containerStyle = {
|
|
14482
|
+
position: 'relative',
|
|
14483
|
+
height: autoResize ? `${height}px` : '100%',
|
|
14484
|
+
width: '100%',
|
|
14485
|
+
...style,
|
|
14486
|
+
};
|
|
14487
|
+
return (jsxRuntime.jsxs("div", { className: className, style: containerStyle, children: [error && (jsxRuntime.jsxs("div", { style: {
|
|
14488
|
+
padding: '0.5rem 1rem',
|
|
14489
|
+
backgroundColor: '#fee2e2',
|
|
14490
|
+
color: '#dc2626',
|
|
14491
|
+
borderBottom: '1px solid #fecaca',
|
|
14492
|
+
fontSize: '0.875rem',
|
|
14493
|
+
}, children: ["Error: ", error] })), isLoading && !error && (jsxRuntime.jsx("div", { style: {
|
|
14494
|
+
display: 'flex',
|
|
14495
|
+
alignItems: 'center',
|
|
14496
|
+
justifyContent: 'center',
|
|
14497
|
+
height: '100%',
|
|
14498
|
+
minHeight: minHeight || 200,
|
|
14499
|
+
color: '#6b7280',
|
|
14500
|
+
}, children: "Loading..." })), src && (jsxRuntime.jsx("iframe", { ref: iframeRef, src: src, frameBorder: 0, width: "100%", height: "100%", allow: "camera;microphone;fullscreen;geolocation;identity-credentials-get", style: {
|
|
14501
|
+
display: isLoading ? 'none' : 'block',
|
|
14502
|
+
width: '100%',
|
|
14503
|
+
height: autoResize ? `${height}px` : '100%',
|
|
14504
|
+
border: 'none',
|
|
14505
|
+
} }))] }));
|
|
14506
|
+
};
|
|
14507
|
+
|
|
14508
|
+
/**
|
|
14509
|
+
* Hook to detect if the current user is an admin of the collection or proof.
|
|
14510
|
+
*
|
|
14511
|
+
* Admin status is determined by checking if the user's UID exists in the
|
|
14512
|
+
* `roles` object of either the collection or proof.
|
|
14513
|
+
*
|
|
14514
|
+
* @param collection - Collection object with optional roles
|
|
14515
|
+
* @param proof - Proof object with optional roles
|
|
14516
|
+
* @param user - Current authenticated user
|
|
14517
|
+
* @returns boolean indicating admin status
|
|
14518
|
+
*/
|
|
14519
|
+
function useAdminDetection(collection, proof, user) {
|
|
14520
|
+
return React.useMemo(() => {
|
|
14521
|
+
if (!user?.uid)
|
|
14522
|
+
return false;
|
|
14523
|
+
// Check collection roles
|
|
14524
|
+
if (collection?.roles && collection.roles[user.uid]) {
|
|
14525
|
+
return true;
|
|
14526
|
+
}
|
|
14527
|
+
// Check proof roles (for proof-level admin)
|
|
14528
|
+
if (proof?.roles && proof.roles[user.uid]) {
|
|
14529
|
+
return true;
|
|
14530
|
+
}
|
|
14531
|
+
return false;
|
|
14532
|
+
}, [collection?.roles, proof?.roles, user?.uid]);
|
|
14533
|
+
}
|
|
14534
|
+
|
|
14535
|
+
/**
|
|
14536
|
+
* Hook to handle iframe height management.
|
|
14537
|
+
*
|
|
14538
|
+
* Supports two modes:
|
|
14539
|
+
* 1. Viewport-based: Calculates height based on iframe position and viewport
|
|
14540
|
+
* 2. Content-based: Listens for smartlinks:resize messages from the iframe
|
|
14541
|
+
*
|
|
14542
|
+
* @param iframeRef - Ref to the iframe element
|
|
14543
|
+
* @param options - Resize configuration options
|
|
14544
|
+
* @returns Current calculated height in pixels
|
|
14545
|
+
*/
|
|
14546
|
+
function useIframeResize(iframeRef, options = {}) {
|
|
14547
|
+
const { autoResize = true, minHeight, maxHeight } = options;
|
|
14548
|
+
const [height, setHeight] = React.useState(0);
|
|
14549
|
+
// Calculate height based on viewport position
|
|
14550
|
+
const calculateHeight = React.useCallback(() => {
|
|
14551
|
+
const iframe = iframeRef.current;
|
|
14552
|
+
if (!iframe)
|
|
14553
|
+
return;
|
|
14554
|
+
const container = iframe.parentElement;
|
|
14555
|
+
if (!container)
|
|
14556
|
+
return;
|
|
14557
|
+
const rect = container.getBoundingClientRect();
|
|
14558
|
+
const viewportHeight = window.innerHeight;
|
|
14559
|
+
let calculatedHeight = Math.max(0, viewportHeight - rect.top);
|
|
14560
|
+
// Apply constraints
|
|
14561
|
+
if (minHeight !== undefined) {
|
|
14562
|
+
calculatedHeight = Math.max(calculatedHeight, minHeight);
|
|
14563
|
+
}
|
|
14564
|
+
if (maxHeight !== undefined) {
|
|
14565
|
+
calculatedHeight = Math.min(calculatedHeight, maxHeight);
|
|
14566
|
+
}
|
|
14567
|
+
setHeight(calculatedHeight);
|
|
14568
|
+
}, [minHeight, maxHeight, iframeRef]);
|
|
14569
|
+
// Handle viewport resize events
|
|
14570
|
+
React.useEffect(() => {
|
|
14571
|
+
if (!autoResize)
|
|
14572
|
+
return;
|
|
14573
|
+
// Initial calculation with multiple retries for late layout
|
|
14574
|
+
calculateHeight();
|
|
14575
|
+
const t1 = setTimeout(calculateHeight, 10);
|
|
14576
|
+
const t2 = setTimeout(calculateHeight, 100);
|
|
14577
|
+
const t3 = setTimeout(calculateHeight, 500);
|
|
14578
|
+
const handleResize = () => calculateHeight();
|
|
14579
|
+
window.addEventListener('resize', handleResize);
|
|
14580
|
+
window.addEventListener('orientationchange', handleResize);
|
|
14581
|
+
return () => {
|
|
14582
|
+
clearTimeout(t1);
|
|
14583
|
+
clearTimeout(t2);
|
|
14584
|
+
clearTimeout(t3);
|
|
14585
|
+
window.removeEventListener('resize', handleResize);
|
|
14586
|
+
window.removeEventListener('orientationchange', handleResize);
|
|
14587
|
+
};
|
|
14588
|
+
}, [autoResize, calculateHeight]);
|
|
14589
|
+
// Listen for content-based resize messages from iframe
|
|
14590
|
+
React.useEffect(() => {
|
|
14591
|
+
const handleMessage = (event) => {
|
|
14592
|
+
// Validate source is our iframe
|
|
14593
|
+
if (iframeRef.current && event.source !== iframeRef.current.contentWindow) {
|
|
14594
|
+
return;
|
|
14595
|
+
}
|
|
14596
|
+
const data = event.data;
|
|
14597
|
+
// Handle smartlinks:resize message
|
|
14598
|
+
if (data?._smartlinksIframeMessage && data?.type === 'smartlinks:resize') {
|
|
14599
|
+
const contentHeight = data.payload?.height;
|
|
14600
|
+
if (typeof contentHeight === 'number' && contentHeight > 0) {
|
|
14601
|
+
let newHeight = contentHeight;
|
|
14602
|
+
// Apply constraints
|
|
14603
|
+
if (minHeight !== undefined) {
|
|
14604
|
+
newHeight = Math.max(newHeight, minHeight);
|
|
14605
|
+
}
|
|
14606
|
+
if (maxHeight !== undefined) {
|
|
14607
|
+
newHeight = Math.min(newHeight, maxHeight);
|
|
14608
|
+
}
|
|
14609
|
+
setHeight(newHeight);
|
|
14610
|
+
}
|
|
14611
|
+
}
|
|
14612
|
+
};
|
|
14613
|
+
window.addEventListener('message', handleMessage);
|
|
14614
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
14615
|
+
}, [iframeRef, minHeight, maxHeight]);
|
|
14616
|
+
return height;
|
|
14617
|
+
}
|
|
14618
|
+
|
|
14334
14619
|
// Re-import the constant since we can't import from types
|
|
14335
14620
|
const KNOWN_PARAMS = new Set([
|
|
14336
14621
|
'collectionId',
|
|
@@ -14401,12 +14686,12 @@ function useIframeMessages(iframeRef, options) {
|
|
|
14401
14686
|
// Filter out known iframe params, keep only app-specific state
|
|
14402
14687
|
const filteredState = {};
|
|
14403
14688
|
Object.entries(context).forEach(([key, value]) => {
|
|
14404
|
-
if (value != null && !KNOWN_PARAMS.has(key)) {
|
|
14689
|
+
if (value != null && !KNOWN_PARAMS.has(key) && typeof value === 'string') {
|
|
14405
14690
|
filteredState[key] = value;
|
|
14406
14691
|
}
|
|
14407
14692
|
});
|
|
14408
14693
|
Object.entries(state).forEach(([key, value]) => {
|
|
14409
|
-
if (value != null && !KNOWN_PARAMS.has(key)) {
|
|
14694
|
+
if (value != null && !KNOWN_PARAMS.has(key) && typeof value === 'string') {
|
|
14410
14695
|
filteredState[key] = value;
|
|
14411
14696
|
}
|
|
14412
14697
|
});
|
|
@@ -14422,7 +14707,7 @@ function useIframeMessages(iframeRef, options) {
|
|
|
14422
14707
|
if ('_smartlinksCustomProxyRequest' in data && data._smartlinksCustomProxyRequest) {
|
|
14423
14708
|
if (data.request === 'REDIRECT') {
|
|
14424
14709
|
const url = data.params?.url;
|
|
14425
|
-
if (url) {
|
|
14710
|
+
if (typeof url === 'string') {
|
|
14426
14711
|
window.location.href = url;
|
|
14427
14712
|
}
|
|
14428
14713
|
response.data = { success: true };
|
|
@@ -14544,9 +14829,20 @@ function useIframeMessages(iframeRef, options) {
|
|
|
14544
14829
|
try {
|
|
14545
14830
|
// Validate token using SDK
|
|
14546
14831
|
console.log('[SmartlinksFrame] Validating auth token...');
|
|
14547
|
-
|
|
14832
|
+
if (typeof token === 'string') {
|
|
14833
|
+
await smartlinks__namespace.auth.verifyToken(token);
|
|
14834
|
+
}
|
|
14835
|
+
// Cast user to expected shape
|
|
14836
|
+
const userPayload = iframeUser;
|
|
14837
|
+
// Validate user object has required properties
|
|
14838
|
+
const authUser = {
|
|
14839
|
+
uid: userPayload?.uid || '',
|
|
14840
|
+
email: userPayload?.email,
|
|
14841
|
+
displayName: userPayload?.displayName,
|
|
14842
|
+
accountData: accountData || userPayload?.accountData,
|
|
14843
|
+
};
|
|
14548
14844
|
// Use AuthProvider's login to persist session
|
|
14549
|
-
await loginRef.current(token,
|
|
14845
|
+
await loginRef.current(token, authUser, accountData);
|
|
14550
14846
|
// Send acknowledgment
|
|
14551
14847
|
sendResponse(event.source, event.origin, {
|
|
14552
14848
|
type: 'smartlinks:authkit:login-acknowledged',
|
|
@@ -14702,257 +14998,6 @@ function useIframeMessages(iframeRef, options) {
|
|
|
14702
14998
|
}, [iframeRef, handleRouteChange, handleStandardMessage, handleUpload, handleProxyRequest]);
|
|
14703
14999
|
}
|
|
14704
15000
|
|
|
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
|
-
|
|
14956
15001
|
const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
|
|
14957
15002
|
const { isAuthenticated, isLoading } = useAuth();
|
|
14958
15003
|
// Show loading state
|
|
@@ -15031,12 +15076,14 @@ exports.ProtectedRoute = ProtectedRoute;
|
|
|
15031
15076
|
exports.SchemaFieldRenderer = SchemaFieldRenderer;
|
|
15032
15077
|
exports.SmartlinksAuthUI = SmartlinksAuthUI;
|
|
15033
15078
|
exports.SmartlinksFrame = SmartlinksFrame;
|
|
15079
|
+
exports.buildIframeSrc = buildIframeSrc;
|
|
15034
15080
|
exports.getDefaultAuthKitId = getDefaultAuthKitId;
|
|
15035
15081
|
exports.getEditableFields = getEditableFields;
|
|
15036
15082
|
exports.getErrorCode = getErrorCode;
|
|
15037
15083
|
exports.getErrorStatusCode = getErrorStatusCode;
|
|
15038
15084
|
exports.getFriendlyErrorMessage = getFriendlyErrorMessage;
|
|
15039
15085
|
exports.getRegistrationFields = getRegistrationFields;
|
|
15086
|
+
exports.isAdminFromRoles = isAdminFromRoles;
|
|
15040
15087
|
exports.isAuthError = isAuthError;
|
|
15041
15088
|
exports.isConflictError = isConflictError;
|
|
15042
15089
|
exports.isRateLimitError = isRateLimitError;
|