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