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