@proveanything/smartlinks-auth-ui 0.1.3 → 0.1.5

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
@@ -2,7 +2,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import React, { useEffect, useState, useRef, useCallback, useMemo, createContext, useContext } from 'react';
3
3
  import * as smartlinks from '@proveanything/smartlinks';
4
4
 
5
- const AuthContainer = ({ children, theme = 'light', className = '', config, }) => {
5
+ const AuthContainer = ({ children, theme = 'light', className = '', config, minimal = false, }) => {
6
6
  // Apply CSS variables for customization
7
7
  useEffect(() => {
8
8
  if (!config?.branding)
@@ -42,11 +42,21 @@ const AuthContainer = ({ children, theme = 'light', className = '', config, }) =
42
42
  }, [config]);
43
43
  const title = config?.branding?.title;
44
44
  const subtitle = config?.branding?.subtitle;
45
- const logoUrl = config?.branding?.logoUrl || '/smartlinks-logo.png';
46
- return (jsx("div", { className: `auth-container auth-theme-${theme} ${className}`, children: jsxs("div", { className: "auth-card", style: { borderRadius: config?.branding?.buttonStyle === 'square' ? '4px' : '12px' }, children: [(jsxs("div", { className: "auth-header", children: [(jsx("div", { className: "auth-logo", children: jsx("img", { src: logoUrl, alt: "Logo", style: { maxWidth: '200px', height: 'auto', objectFit: 'contain' } }) })), title && jsx("h1", { className: "auth-title", children: title }), subtitle && jsx("p", { className: "auth-subtitle", children: subtitle })] })), jsx("div", { className: "auth-content", children: children }), (config?.branding?.termsUrl || config?.branding?.privacyUrl) && (jsxs("div", { className: "auth-footer", children: [config.branding.termsUrl && jsx("a", { href: config.branding.termsUrl, target: "_blank", rel: "noopener noreferrer", children: "Terms" }), config.branding.termsUrl && config.branding.privacyUrl && jsx("span", { children: "\u2022" }), config.branding.privacyUrl && jsx("a", { href: config.branding.privacyUrl, target: "_blank", rel: "noopener noreferrer", children: "Privacy" })] }))] }) }));
45
+ // Logo URL logic:
46
+ // - undefined/not set: use default Smartlinks logo
47
+ // - null or empty string: hide logo completely
48
+ // - string URL: use custom logo
49
+ const logoUrl = config?.branding?.logoUrl === undefined
50
+ ? 'https://smartlinks.app/smartlinks/landscape-medium.png' // Default Smartlinks logo
51
+ : config?.branding?.logoUrl || null; // Custom or explicitly hidden
52
+ const containerClass = minimal
53
+ ? `auth-minimal auth-theme-${theme} ${className}`
54
+ : `auth-container auth-theme-${theme} ${className}`;
55
+ const cardClass = minimal ? 'auth-minimal-card' : 'auth-card';
56
+ return (jsx("div", { className: containerClass, children: jsxs("div", { className: cardClass, style: !minimal && config?.branding?.buttonStyle === 'square' ? { borderRadius: '4px' } : undefined, children: [(logoUrl || title || subtitle) && (jsxs("div", { className: "auth-header", children: [logoUrl && (jsx("div", { className: "auth-logo", children: jsx("img", { src: logoUrl, alt: "Logo", style: { maxWidth: '200px', height: 'auto', objectFit: 'contain' } }) })), title && jsx("h1", { className: "auth-title", children: title }), subtitle && jsx("p", { className: "auth-subtitle", children: subtitle })] })), jsx("div", { className: "auth-content", children: children }), (config?.branding?.termsUrl || config?.branding?.privacyUrl) && (jsxs("div", { className: "auth-footer", children: [config.branding.termsUrl && jsx("a", { href: config.branding.termsUrl, target: "_blank", rel: "noopener noreferrer", children: "Terms" }), config.branding.termsUrl && config.branding.privacyUrl && jsx("span", { children: "\u2022" }), config.branding.privacyUrl && jsx("a", { href: config.branding.privacyUrl, target: "_blank", rel: "noopener noreferrer", children: "Privacy" })] }))] }) }));
47
57
  };
48
58
 
49
- const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, }) => {
59
+ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, additionalFields = [], }) => {
50
60
  const [formData, setFormData] = useState({
51
61
  email: '',
52
62
  password: '',
@@ -61,7 +71,7 @@ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading
61
71
  };
62
72
  return (jsxs("form", { className: "auth-form", onSubmit: handleSubmit, children: [jsxs("div", { className: "auth-form-header", children: [jsx("h2", { className: "auth-form-title", children: mode === 'login' ? 'Sign in' : 'Create account' }), jsx("p", { className: "auth-form-subtitle", children: mode === 'login'
63
73
  ? 'Welcome back! Please enter your credentials.'
64
- : 'Get started by creating your account.' })] }), error && (jsxs("div", { className: "auth-error", role: "alert", children: [jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), mode === 'register' && (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsx("input", { type: "text", id: "displayName", className: "auth-input", value: formData.displayName || '', onChange: (e) => handleChange('displayName', e.target.value), required: mode === 'register', disabled: loading, placeholder: "John Doe" })] })), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsx("input", { type: "email", id: "email", className: "auth-input", value: formData.email || '', onChange: (e) => handleChange('email', e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsx("input", { type: "password", id: "password", className: "auth-input", value: formData.password || '', onChange: (e) => handleChange('password', e.target.value), required: true, disabled: loading, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === 'login' ? 'current-password' : 'new-password', minLength: 6 })] }), mode === 'login' && (jsx("div", { className: "auth-form-footer", children: jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), jsxs("div", { className: "auth-divider", children: [jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] })] }));
74
+ : 'Get started by creating your account.' })] }), error && (jsxs("div", { className: "auth-error", role: "alert", children: [jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), mode === 'register' && (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsx("input", { type: "text", id: "displayName", className: "auth-input", value: formData.displayName || '', onChange: (e) => handleChange('displayName', e.target.value), required: mode === 'register', disabled: loading, placeholder: "John Doe" })] })), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsx("input", { type: "email", id: "email", className: "auth-input", value: formData.email || '', onChange: (e) => handleChange('email', e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsx("input", { type: "password", id: "password", className: "auth-input", value: formData.password || '', onChange: (e) => handleChange('password', e.target.value), required: true, disabled: loading, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === 'login' ? 'current-password' : 'new-password', minLength: 6 })] }), mode === 'register' && additionalFields.map((field) => (jsxs("div", { className: "auth-form-group", children: [jsxs("label", { htmlFor: field.name, className: "auth-label", children: [field.label, field.required && jsx("span", { style: { color: 'var(--auth-error-color, #ef4444)' }, children: " *" })] }), field.type === 'select' ? (jsxs("select", { id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, children: [jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsx("option", { value: option, children: option }, option)))] })) : field.type === 'textarea' ? (jsx("textarea", { id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, placeholder: field.placeholder, rows: 3, style: { minHeight: '80px', resize: 'vertical' } })) : (jsx("input", { type: field.type, id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, placeholder: field.placeholder }))] }, field.name))), mode === 'login' && (jsx("div", { className: "auth-form-footer", children: jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), jsxs("div", { className: "auth-divider", children: [jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] })] }));
65
75
  };
66
76
 
67
77
  const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, loading, }) => {
@@ -10672,142 +10682,724 @@ class AuthAPI {
10672
10682
  }
10673
10683
  }
10674
10684
 
10675
- const TOKEN_KEY = 'smartlinks_auth_token';
10676
- const USER_KEY = 'smartlinks_auth_user';
10677
- const ACCOUNT_DATA_KEY = 'smartlinks_account_data';
10685
+ /**
10686
+ * Persistent Storage Layer for Smartlinks Auth
10687
+ *
10688
+ * Implements a tiered persistence strategy similar to Firebase Auth:
10689
+ * 1. Primary: IndexedDB (async, large capacity, cross-tab)
10690
+ * 2. Fallback: localStorage (sync, universal support)
10691
+ *
10692
+ * Provides unified async API for all storage operations with automatic
10693
+ * fallback handling and cross-tab synchronization.
10694
+ */
10695
+ const DB_NAME = 'smartlinks_auth_db';
10696
+ const DB_VERSION = 1;
10697
+ const STORE_NAME = 'auth_state';
10698
+ const STORAGE_CHANNEL = 'smartlinks_auth_storage';
10699
+ /**
10700
+ * IndexedDB Storage Implementation
10701
+ */
10702
+ class IndexedDBStorage {
10703
+ constructor() {
10704
+ this.db = null;
10705
+ this.initPromise = null;
10706
+ this.channel = null;
10707
+ this.initPromise = this.initialize();
10708
+ // Set up BroadcastChannel for cross-tab communication
10709
+ if (typeof BroadcastChannel !== 'undefined') {
10710
+ this.channel = new BroadcastChannel(STORAGE_CHANNEL);
10711
+ }
10712
+ }
10713
+ async initialize() {
10714
+ return new Promise((resolve, reject) => {
10715
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
10716
+ request.onerror = () => {
10717
+ console.warn('IndexedDB initialization failed:', request.error);
10718
+ reject(request.error);
10719
+ };
10720
+ request.onsuccess = () => {
10721
+ this.db = request.result;
10722
+ resolve();
10723
+ };
10724
+ request.onupgradeneeded = (event) => {
10725
+ const db = event.target.result;
10726
+ // Create object store if it doesn't exist
10727
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
10728
+ db.createObjectStore(STORE_NAME);
10729
+ }
10730
+ };
10731
+ });
10732
+ }
10733
+ async ensureInitialized() {
10734
+ if (this.initPromise) {
10735
+ await this.initPromise;
10736
+ }
10737
+ }
10738
+ broadcastChange(event) {
10739
+ if (this.channel) {
10740
+ try {
10741
+ this.channel.postMessage(event);
10742
+ }
10743
+ catch (error) {
10744
+ console.warn('Failed to broadcast storage change:', error);
10745
+ }
10746
+ }
10747
+ }
10748
+ async setItem(key, value) {
10749
+ await this.ensureInitialized();
10750
+ return new Promise((resolve, reject) => {
10751
+ if (!this.db) {
10752
+ reject(new Error('IndexedDB not initialized'));
10753
+ return;
10754
+ }
10755
+ const transaction = this.db.transaction([STORE_NAME], 'readwrite');
10756
+ const store = transaction.objectStore(STORE_NAME);
10757
+ const request = store.put(value, key);
10758
+ request.onsuccess = () => {
10759
+ this.broadcastChange({
10760
+ type: 'set',
10761
+ key,
10762
+ value,
10763
+ timestamp: Date.now(),
10764
+ });
10765
+ resolve();
10766
+ };
10767
+ request.onerror = () => {
10768
+ reject(request.error);
10769
+ };
10770
+ });
10771
+ }
10772
+ async getItem(key) {
10773
+ await this.ensureInitialized();
10774
+ return new Promise((resolve, reject) => {
10775
+ if (!this.db) {
10776
+ reject(new Error('IndexedDB not initialized'));
10777
+ return;
10778
+ }
10779
+ const transaction = this.db.transaction([STORE_NAME], 'readonly');
10780
+ const store = transaction.objectStore(STORE_NAME);
10781
+ const request = store.get(key);
10782
+ request.onsuccess = () => {
10783
+ resolve(request.result !== undefined ? request.result : null);
10784
+ };
10785
+ request.onerror = () => {
10786
+ reject(request.error);
10787
+ };
10788
+ });
10789
+ }
10790
+ async removeItem(key) {
10791
+ await this.ensureInitialized();
10792
+ return new Promise((resolve, reject) => {
10793
+ if (!this.db) {
10794
+ reject(new Error('IndexedDB not initialized'));
10795
+ return;
10796
+ }
10797
+ const transaction = this.db.transaction([STORE_NAME], 'readwrite');
10798
+ const store = transaction.objectStore(STORE_NAME);
10799
+ const request = store.delete(key);
10800
+ request.onsuccess = () => {
10801
+ this.broadcastChange({
10802
+ type: 'remove',
10803
+ key,
10804
+ timestamp: Date.now(),
10805
+ });
10806
+ resolve();
10807
+ };
10808
+ request.onerror = () => {
10809
+ reject(request.error);
10810
+ };
10811
+ });
10812
+ }
10813
+ async clear() {
10814
+ await this.ensureInitialized();
10815
+ return new Promise((resolve, reject) => {
10816
+ if (!this.db) {
10817
+ reject(new Error('IndexedDB not initialized'));
10818
+ return;
10819
+ }
10820
+ const transaction = this.db.transaction([STORE_NAME], 'readwrite');
10821
+ const store = transaction.objectStore(STORE_NAME);
10822
+ const request = store.clear();
10823
+ request.onsuccess = () => {
10824
+ this.broadcastChange({
10825
+ type: 'clear',
10826
+ timestamp: Date.now(),
10827
+ });
10828
+ resolve();
10829
+ };
10830
+ request.onerror = () => {
10831
+ reject(request.error);
10832
+ };
10833
+ });
10834
+ }
10835
+ async isAvailable() {
10836
+ try {
10837
+ await this.ensureInitialized();
10838
+ return this.db !== null;
10839
+ }
10840
+ catch {
10841
+ return false;
10842
+ }
10843
+ }
10844
+ getBackend() {
10845
+ return 'indexeddb';
10846
+ }
10847
+ destroy() {
10848
+ if (this.db) {
10849
+ this.db.close();
10850
+ this.db = null;
10851
+ }
10852
+ if (this.channel) {
10853
+ this.channel.close();
10854
+ this.channel = null;
10855
+ }
10856
+ }
10857
+ }
10858
+ /**
10859
+ * localStorage Storage Implementation (Fallback)
10860
+ */
10861
+ class LocalStorageAdapter {
10862
+ constructor() {
10863
+ this.prefix = 'smartlinks_auth_';
10864
+ this.channel = null;
10865
+ // Set up BroadcastChannel for cross-tab communication
10866
+ if (typeof BroadcastChannel !== 'undefined') {
10867
+ this.channel = new BroadcastChannel(STORAGE_CHANNEL);
10868
+ }
10869
+ }
10870
+ broadcastChange(event) {
10871
+ if (this.channel) {
10872
+ try {
10873
+ this.channel.postMessage(event);
10874
+ }
10875
+ catch (error) {
10876
+ console.warn('Failed to broadcast storage change:', error);
10877
+ }
10878
+ }
10879
+ }
10880
+ async setItem(key, value) {
10881
+ try {
10882
+ const serialized = JSON.stringify(value);
10883
+ localStorage.setItem(this.prefix + key, serialized);
10884
+ this.broadcastChange({
10885
+ type: 'set',
10886
+ key,
10887
+ value,
10888
+ timestamp: Date.now(),
10889
+ });
10890
+ }
10891
+ catch (error) {
10892
+ console.error('localStorage setItem failed:', error);
10893
+ throw error;
10894
+ }
10895
+ }
10896
+ async getItem(key) {
10897
+ try {
10898
+ const stored = localStorage.getItem(this.prefix + key);
10899
+ if (!stored)
10900
+ return null;
10901
+ return JSON.parse(stored);
10902
+ }
10903
+ catch (error) {
10904
+ console.error('localStorage getItem failed:', error);
10905
+ return null;
10906
+ }
10907
+ }
10908
+ async removeItem(key) {
10909
+ try {
10910
+ localStorage.removeItem(this.prefix + key);
10911
+ this.broadcastChange({
10912
+ type: 'remove',
10913
+ key,
10914
+ timestamp: Date.now(),
10915
+ });
10916
+ }
10917
+ catch (error) {
10918
+ console.error('localStorage removeItem failed:', error);
10919
+ throw error;
10920
+ }
10921
+ }
10922
+ async clear() {
10923
+ try {
10924
+ const keys = Object.keys(localStorage);
10925
+ keys.forEach(key => {
10926
+ if (key.startsWith(this.prefix)) {
10927
+ localStorage.removeItem(key);
10928
+ }
10929
+ });
10930
+ this.broadcastChange({
10931
+ type: 'clear',
10932
+ timestamp: Date.now(),
10933
+ });
10934
+ }
10935
+ catch (error) {
10936
+ console.error('localStorage clear failed:', error);
10937
+ throw error;
10938
+ }
10939
+ }
10940
+ async isAvailable() {
10941
+ try {
10942
+ const testKey = '__smartlinks_test__';
10943
+ localStorage.setItem(testKey, 'test');
10944
+ localStorage.removeItem(testKey);
10945
+ return true;
10946
+ }
10947
+ catch {
10948
+ return false;
10949
+ }
10950
+ }
10951
+ getBackend() {
10952
+ return 'localstorage';
10953
+ }
10954
+ destroy() {
10955
+ if (this.channel) {
10956
+ this.channel.close();
10957
+ this.channel = null;
10958
+ }
10959
+ }
10960
+ }
10961
+ /**
10962
+ * In-Memory Storage Implementation (No Persistence)
10963
+ */
10964
+ class InMemoryStorage {
10965
+ constructor() {
10966
+ this.storage = new Map();
10967
+ }
10968
+ async setItem(key, value) {
10969
+ this.storage.set(key, value);
10970
+ }
10971
+ async getItem(key) {
10972
+ return this.storage.has(key) ? this.storage.get(key) : null;
10973
+ }
10974
+ async removeItem(key) {
10975
+ this.storage.delete(key);
10976
+ }
10977
+ async clear() {
10978
+ this.storage.clear();
10979
+ }
10980
+ async isAvailable() {
10981
+ return true;
10982
+ }
10983
+ getBackend() {
10984
+ return 'none';
10985
+ }
10986
+ destroy() {
10987
+ this.storage.clear();
10988
+ }
10989
+ }
10990
+ /**
10991
+ * Storage Factory - Creates appropriate storage backend with automatic fallback
10992
+ */
10993
+ class StorageFactory {
10994
+ static async createStorage() {
10995
+ if (this.instance) {
10996
+ return this.instance;
10997
+ }
10998
+ // Try IndexedDB first
10999
+ try {
11000
+ const indexedDB = new IndexedDBStorage();
11001
+ const available = await indexedDB.isAvailable();
11002
+ if (available) {
11003
+ console.log('[PersistentStorage] Using IndexedDB backend');
11004
+ this.instance = indexedDB;
11005
+ return indexedDB;
11006
+ }
11007
+ indexedDB.destroy();
11008
+ }
11009
+ catch (error) {
11010
+ console.warn('[PersistentStorage] IndexedDB failed:', error);
11011
+ }
11012
+ // Fallback to localStorage
11013
+ try {
11014
+ const localStorage = new LocalStorageAdapter();
11015
+ const available = await localStorage.isAvailable();
11016
+ if (available) {
11017
+ console.log('[PersistentStorage] Using localStorage backend (fallback)');
11018
+ this.instance = localStorage;
11019
+ return localStorage;
11020
+ }
11021
+ localStorage.destroy();
11022
+ }
11023
+ catch (error) {
11024
+ console.warn('[PersistentStorage] localStorage failed:', error);
11025
+ }
11026
+ // Final fallback to in-memory (no persistence)
11027
+ console.warn('[PersistentStorage] Using in-memory storage (no persistence)');
11028
+ this.instance = new InMemoryStorage();
11029
+ return this.instance;
11030
+ }
11031
+ static getInstance() {
11032
+ return this.instance;
11033
+ }
11034
+ static reset() {
11035
+ if (this.instance && 'destroy' in this.instance) {
11036
+ this.instance.destroy();
11037
+ }
11038
+ this.instance = null;
11039
+ }
11040
+ }
11041
+ StorageFactory.instance = null;
11042
+ /**
11043
+ * Main storage export - creates singleton storage instance
11044
+ */
11045
+ let storageInstance = null;
11046
+ let storageInitPromise = null;
11047
+ async function getStorage() {
11048
+ if (storageInstance) {
11049
+ return storageInstance;
11050
+ }
11051
+ if (!storageInitPromise) {
11052
+ storageInitPromise = StorageFactory.createStorage();
11053
+ }
11054
+ storageInstance = await storageInitPromise;
11055
+ return storageInstance;
11056
+ }
11057
+ /**
11058
+ * Listen for storage changes from other tabs
11059
+ */
11060
+ function onStorageChange(callback) {
11061
+ if (typeof BroadcastChannel === 'undefined') {
11062
+ console.warn('BroadcastChannel not supported, cross-tab sync disabled');
11063
+ return () => { };
11064
+ }
11065
+ const channel = new BroadcastChannel(STORAGE_CHANNEL);
11066
+ channel.onmessage = (event) => {
11067
+ callback(event.data);
11068
+ };
11069
+ return () => {
11070
+ channel.close();
11071
+ };
11072
+ }
11073
+
11074
+ const TOKEN_KEY = 'token';
11075
+ const USER_KEY = 'user';
11076
+ const ACCOUNT_DATA_KEY = 'account_data';
11077
+ const ACCOUNT_INFO_KEY = 'account_info';
11078
+ const ACCOUNT_INFO_TTL = 5 * 60 * 1000; // 5 minutes default
11079
+ /**
11080
+ * Token Storage Layer
11081
+ *
11082
+ * Manages authentication tokens, user data, and account data using
11083
+ * the persistent storage layer (IndexedDB + localStorage fallback).
11084
+ * All methods are async to support IndexedDB operations.
11085
+ */
10678
11086
  const tokenStorage = {
10679
- saveToken(token, expiresAt) {
11087
+ async saveToken(token, expiresAt) {
11088
+ const storage = await getStorage();
10680
11089
  const authToken = {
10681
11090
  token,
10682
11091
  expiresAt: expiresAt || Date.now() + 3600000, // Default 1 hour
10683
11092
  };
10684
- localStorage.setItem(TOKEN_KEY, JSON.stringify(authToken));
11093
+ await storage.setItem(TOKEN_KEY, authToken);
10685
11094
  },
10686
- getToken() {
10687
- const stored = localStorage.getItem(TOKEN_KEY);
10688
- if (!stored)
11095
+ async getToken() {
11096
+ const storage = await getStorage();
11097
+ const authToken = await storage.getItem(TOKEN_KEY);
11098
+ if (!authToken)
10689
11099
  return null;
10690
- try {
10691
- const authToken = JSON.parse(stored);
10692
- // Check if token is expired
10693
- if (authToken.expiresAt && authToken.expiresAt < Date.now()) {
10694
- this.clearToken();
10695
- return null;
10696
- }
10697
- return authToken;
10698
- }
10699
- catch {
11100
+ // Check if token is expired
11101
+ if (authToken.expiresAt && authToken.expiresAt < Date.now()) {
11102
+ await this.clearToken();
10700
11103
  return null;
10701
11104
  }
11105
+ return authToken;
10702
11106
  },
10703
- clearToken() {
10704
- localStorage.removeItem(TOKEN_KEY);
11107
+ async clearToken() {
11108
+ const storage = await getStorage();
11109
+ await storage.removeItem(TOKEN_KEY);
10705
11110
  },
10706
- saveUser(user) {
10707
- localStorage.setItem(USER_KEY, JSON.stringify(user));
11111
+ async saveUser(user) {
11112
+ const storage = await getStorage();
11113
+ await storage.setItem(USER_KEY, user);
10708
11114
  },
10709
- getUser() {
10710
- const stored = localStorage.getItem(USER_KEY);
10711
- if (!stored)
10712
- return null;
10713
- try {
10714
- return JSON.parse(stored);
10715
- }
10716
- catch {
10717
- return null;
10718
- }
11115
+ async getUser() {
11116
+ const storage = await getStorage();
11117
+ return await storage.getItem(USER_KEY);
10719
11118
  },
10720
- clearUser() {
10721
- localStorage.removeItem(USER_KEY);
11119
+ async clearUser() {
11120
+ const storage = await getStorage();
11121
+ await storage.removeItem(USER_KEY);
10722
11122
  },
10723
- clearAll() {
10724
- this.clearToken();
10725
- this.clearUser();
10726
- this.clearAccountData();
11123
+ async clearAll() {
11124
+ await this.clearToken();
11125
+ await this.clearUser();
11126
+ await this.clearAccountData();
11127
+ await this.clearAccountInfo();
10727
11128
  },
10728
- saveAccountData(data) {
10729
- localStorage.setItem(ACCOUNT_DATA_KEY, JSON.stringify(data));
11129
+ async saveAccountData(data) {
11130
+ const storage = await getStorage();
11131
+ await storage.setItem(ACCOUNT_DATA_KEY, data);
10730
11132
  },
10731
- getAccountData() {
10732
- const stored = localStorage.getItem(ACCOUNT_DATA_KEY);
10733
- if (!stored)
10734
- return null;
10735
- try {
10736
- return JSON.parse(stored);
10737
- }
10738
- catch {
11133
+ async getAccountData() {
11134
+ const storage = await getStorage();
11135
+ return await storage.getItem(ACCOUNT_DATA_KEY);
11136
+ },
11137
+ async clearAccountData() {
11138
+ const storage = await getStorage();
11139
+ await storage.removeItem(ACCOUNT_DATA_KEY);
11140
+ },
11141
+ async saveAccountInfo(accountInfo, ttl = ACCOUNT_INFO_TTL) {
11142
+ const storage = await getStorage();
11143
+ const cachedData = {
11144
+ data: accountInfo,
11145
+ cachedAt: Date.now(),
11146
+ expiresAt: Date.now() + ttl,
11147
+ };
11148
+ await storage.setItem(ACCOUNT_INFO_KEY, cachedData);
11149
+ },
11150
+ async getAccountInfo() {
11151
+ const storage = await getStorage();
11152
+ const cached = await storage.getItem(ACCOUNT_INFO_KEY);
11153
+ if (!cached)
10739
11154
  return null;
10740
- }
11155
+ const isStale = cached.expiresAt < Date.now();
11156
+ return {
11157
+ data: cached.data,
11158
+ isStale,
11159
+ };
10741
11160
  },
10742
- clearAccountData() {
10743
- localStorage.removeItem(ACCOUNT_DATA_KEY);
11161
+ async clearAccountInfo() {
11162
+ const storage = await getStorage();
11163
+ await storage.removeItem(ACCOUNT_INFO_KEY);
10744
11164
  },
10745
11165
  };
10746
11166
 
10747
11167
  const AuthContext = createContext(undefined);
10748
- const AuthProvider = ({ children }) => {
11168
+ const AuthProvider = ({ children, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false }) => {
10749
11169
  const [user, setUser] = useState(null);
10750
11170
  const [token, setToken] = useState(null);
10751
11171
  const [accountData, setAccountData] = useState(null);
11172
+ const [accountInfo, setAccountInfo] = useState(null);
10752
11173
  const [isLoading, setIsLoading] = useState(true);
10753
- // Initialize auth state from localStorage
11174
+ const callbacksRef = useRef(new Set());
11175
+ // Notify all subscribers of auth state changes
11176
+ const notifyAuthStateChange = useCallback((type, currentUser, currentToken, currentAccountData, currentAccountInfo) => {
11177
+ callbacksRef.current.forEach(callback => {
11178
+ try {
11179
+ callback({
11180
+ type,
11181
+ user: currentUser,
11182
+ token: currentToken,
11183
+ accountData: currentAccountData,
11184
+ accountInfo: currentAccountInfo
11185
+ });
11186
+ }
11187
+ catch (error) {
11188
+ console.error('[AuthContext] Error in auth state change callback:', error);
11189
+ }
11190
+ });
11191
+ }, []);
11192
+ // Initialize auth state from persistent storage
11193
+ useEffect(() => {
11194
+ const initializeAuth = async () => {
11195
+ try {
11196
+ const storedToken = await tokenStorage.getToken();
11197
+ const storedUser = await tokenStorage.getUser();
11198
+ const storedAccountData = await tokenStorage.getAccountData();
11199
+ if (storedToken && storedUser) {
11200
+ setToken(storedToken.token);
11201
+ setUser(storedUser);
11202
+ setAccountData(storedAccountData);
11203
+ // Set bearer token in global Smartlinks SDK via auth.verifyToken
11204
+ smartlinks.auth.verifyToken(storedToken.token).catch(err => {
11205
+ console.warn('Failed to restore bearer token on init:', err);
11206
+ });
11207
+ }
11208
+ // Load cached account info if available
11209
+ const cachedAccountInfo = await tokenStorage.getAccountInfo();
11210
+ if (cachedAccountInfo && !cachedAccountInfo.isStale) {
11211
+ setAccountInfo(cachedAccountInfo.data);
11212
+ }
11213
+ }
11214
+ catch (error) {
11215
+ console.error('Failed to initialize auth from storage:', error);
11216
+ }
11217
+ finally {
11218
+ setIsLoading(false);
11219
+ }
11220
+ };
11221
+ initializeAuth();
11222
+ }, []);
11223
+ // Cross-tab synchronization - listen for auth changes in other tabs
10754
11224
  useEffect(() => {
10755
- const storedToken = tokenStorage.getToken();
10756
- const storedUser = tokenStorage.getUser();
10757
- const storedAccountData = tokenStorage.getAccountData();
10758
- if (storedToken && storedUser) {
10759
- setToken(storedToken.token);
10760
- setUser(storedUser);
10761
- setAccountData(storedAccountData);
11225
+ console.log('[AuthContext] Setting up cross-tab synchronization');
11226
+ const unsubscribe = onStorageChange(async (event) => {
11227
+ console.log('[AuthContext] Cross-tab storage event:', event.type, event.key);
11228
+ try {
11229
+ if (event.type === 'clear') {
11230
+ // Another tab cleared all storage (logout)
11231
+ console.log('[AuthContext] Detected logout in another tab');
11232
+ setToken(null);
11233
+ setUser(null);
11234
+ setAccountData(null);
11235
+ setAccountInfo(null);
11236
+ smartlinks.auth.logout();
11237
+ notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null);
11238
+ }
11239
+ else if (event.type === 'remove' && (event.key === 'token' || event.key === 'user')) {
11240
+ // Another tab removed token or user (logout)
11241
+ console.log('[AuthContext] Detected token/user removal in another tab');
11242
+ setToken(null);
11243
+ setUser(null);
11244
+ setAccountData(null);
11245
+ setAccountInfo(null);
11246
+ smartlinks.auth.logout();
11247
+ notifyAuthStateChange('CROSS_TAB_SYNC', null, null, null);
11248
+ }
11249
+ else if (event.type === 'set' && event.key === 'token') {
11250
+ // Another tab set a new token (login)
11251
+ console.log('[AuthContext] Detected login in another tab');
11252
+ const storedToken = await tokenStorage.getToken();
11253
+ const storedUser = await tokenStorage.getUser();
11254
+ const storedAccountData = await tokenStorage.getAccountData();
11255
+ if (storedToken && storedUser) {
11256
+ setToken(storedToken.token);
11257
+ setUser(storedUser);
11258
+ setAccountData(storedAccountData);
11259
+ // Set bearer token in global Smartlinks SDK
11260
+ smartlinks.auth.verifyToken(storedToken.token).catch(err => {
11261
+ console.warn('[AuthContext] Failed to restore bearer token from cross-tab sync:', err);
11262
+ });
11263
+ notifyAuthStateChange('CROSS_TAB_SYNC', storedUser, storedToken.token, storedAccountData);
11264
+ }
11265
+ }
11266
+ else if (event.type === 'set' && event.key === 'account_info') {
11267
+ // Another tab fetched fresh account info
11268
+ const cached = await tokenStorage.getAccountInfo();
11269
+ if (cached && !cached.isStale) {
11270
+ setAccountInfo(cached.data);
11271
+ console.log('[AuthContext] Account info synced from another tab');
11272
+ }
11273
+ }
11274
+ }
11275
+ catch (error) {
11276
+ console.error('[AuthContext] Error handling cross-tab sync:', error);
11277
+ }
11278
+ });
11279
+ return () => {
11280
+ console.log('[AuthContext] Cleaning up cross-tab synchronization');
11281
+ unsubscribe();
11282
+ };
11283
+ }, [notifyAuthStateChange]);
11284
+ const login = useCallback(async (authToken, authUser, authAccountData) => {
11285
+ try {
11286
+ // Store token, user, and account data
11287
+ await tokenStorage.saveToken(authToken);
11288
+ await tokenStorage.saveUser(authUser);
11289
+ if (authAccountData) {
11290
+ await tokenStorage.saveAccountData(authAccountData);
11291
+ }
11292
+ setToken(authToken);
11293
+ setUser(authUser);
11294
+ setAccountData(authAccountData || null);
10762
11295
  // Set bearer token in global Smartlinks SDK via auth.verifyToken
10763
- smartlinks.auth.verifyToken(storedToken.token).catch(err => {
10764
- console.warn('Failed to restore bearer token on init:', err);
11296
+ // This both validates the token and sets it for future API calls
11297
+ smartlinks.auth.verifyToken(authToken).catch(err => {
11298
+ console.warn('Failed to set bearer token on login:', err);
10765
11299
  });
11300
+ notifyAuthStateChange('LOGIN', authUser, authToken, authAccountData || null);
11301
+ // Optionally preload account info on login
11302
+ if (preloadAccountInfo) {
11303
+ // Preload after login completes (non-blocking)
11304
+ getAccount(true).catch(error => {
11305
+ console.warn('[AuthContext] Failed to preload account info:', error);
11306
+ });
11307
+ }
10766
11308
  }
10767
- setIsLoading(false);
10768
- }, []);
10769
- const login = useCallback((authToken, authUser, authAccountData) => {
10770
- // Store token, user, and account data
10771
- tokenStorage.saveToken(authToken);
10772
- tokenStorage.saveUser(authUser);
10773
- if (authAccountData) {
10774
- tokenStorage.saveAccountData(authAccountData);
10775
- }
10776
- setToken(authToken);
10777
- setUser(authUser);
10778
- setAccountData(authAccountData || null);
10779
- // Set bearer token in global Smartlinks SDK via auth.verifyToken
10780
- // This both validates the token and sets it for future API calls
10781
- smartlinks.auth.verifyToken(authToken).catch(err => {
10782
- console.warn('Failed to set bearer token on login:', err);
10783
- });
10784
- }, []);
11309
+ catch (error) {
11310
+ console.error('Failed to save auth data to storage:', error);
11311
+ throw error;
11312
+ }
11313
+ }, [notifyAuthStateChange, preloadAccountInfo]);
10785
11314
  const logout = useCallback(async () => {
10786
- // Clear local storage
10787
- tokenStorage.clearAll();
10788
- setToken(null);
10789
- setUser(null);
10790
- setAccountData(null);
10791
- // Clear bearer token from global Smartlinks SDK
10792
- smartlinks.auth.logout();
10793
- }, []);
10794
- const getToken = useCallback(() => {
10795
- const storedToken = tokenStorage.getToken();
11315
+ try {
11316
+ // Clear persistent storage
11317
+ await tokenStorage.clearAll();
11318
+ setToken(null);
11319
+ setUser(null);
11320
+ setAccountData(null);
11321
+ setAccountInfo(null);
11322
+ // Clear bearer token from global Smartlinks SDK
11323
+ smartlinks.auth.logout();
11324
+ notifyAuthStateChange('LOGOUT', null, null, null);
11325
+ }
11326
+ catch (error) {
11327
+ console.error('Failed to clear auth data from storage:', error);
11328
+ }
11329
+ }, [notifyAuthStateChange]);
11330
+ const getToken = useCallback(async () => {
11331
+ const storedToken = await tokenStorage.getToken();
10796
11332
  return storedToken ? storedToken.token : null;
10797
11333
  }, []);
10798
11334
  const refreshToken = useCallback(async () => {
10799
11335
  throw new Error('Token refresh must be implemented via your backend API');
10800
11336
  }, []);
11337
+ // Get account with intelligent caching
11338
+ const getAccount = useCallback(async (forceRefresh = false) => {
11339
+ try {
11340
+ // Check if user is authenticated
11341
+ if (!token) {
11342
+ throw new Error('Not authenticated. Please login first.');
11343
+ }
11344
+ // Check cache unless force refresh
11345
+ if (!forceRefresh) {
11346
+ const cached = await tokenStorage.getAccountInfo();
11347
+ if (cached && !cached.isStale) {
11348
+ console.log('[AuthContext] Returning cached account info');
11349
+ return cached.data;
11350
+ }
11351
+ }
11352
+ // Fetch fresh data from API
11353
+ console.log('[AuthContext] Fetching fresh account info from API');
11354
+ const freshAccountInfo = await smartlinks.auth.getAccount();
11355
+ // Cache the fresh data
11356
+ await tokenStorage.saveAccountInfo(freshAccountInfo, accountCacheTTL);
11357
+ setAccountInfo(freshAccountInfo);
11358
+ notifyAuthStateChange('ACCOUNT_REFRESH', user, token, accountData, freshAccountInfo);
11359
+ return freshAccountInfo;
11360
+ }
11361
+ catch (error) {
11362
+ console.error('[AuthContext] Failed to get account info:', error);
11363
+ // Fallback to stale cache if API fails
11364
+ const cached = await tokenStorage.getAccountInfo();
11365
+ if (cached) {
11366
+ console.warn('[AuthContext] Returning stale cached data due to API error');
11367
+ return cached.data;
11368
+ }
11369
+ throw error;
11370
+ }
11371
+ }, [token, accountCacheTTL, user, accountData, notifyAuthStateChange]);
11372
+ // Convenience method for explicit refresh
11373
+ const refreshAccount = useCallback(async () => {
11374
+ return await getAccount(true);
11375
+ }, [getAccount]);
11376
+ // Clear account cache
11377
+ const clearAccountCache = useCallback(async () => {
11378
+ await tokenStorage.clearAccountInfo();
11379
+ setAccountInfo(null);
11380
+ }, []);
11381
+ const onAuthStateChange = useCallback((callback) => {
11382
+ callbacksRef.current.add(callback);
11383
+ // Return unsubscribe function
11384
+ return () => {
11385
+ callbacksRef.current.delete(callback);
11386
+ };
11387
+ }, []);
10801
11388
  const value = {
10802
11389
  user,
10803
11390
  token,
10804
11391
  accountData,
11392
+ accountInfo,
10805
11393
  isAuthenticated: !!token && !!user,
10806
11394
  isLoading,
10807
11395
  login,
10808
11396
  logout,
10809
11397
  getToken,
10810
11398
  refreshToken,
11399
+ getAccount,
11400
+ refreshAccount,
11401
+ clearAccountCache,
11402
+ onAuthStateChange,
10811
11403
  };
10812
11404
  return jsx(AuthContext.Provider, { value: value, children: children });
10813
11405
  };
@@ -10819,997 +11411,1093 @@ const useAuth = () => {
10819
11411
  return context;
10820
11412
  };
10821
11413
 
10822
- const AccountManagement = ({ apiEndpoint, clientId, onProfileUpdated, onEmailChangeRequested, onPasswordChanged, onAccountDeleted, onError, theme = 'light', className = '', customization = {}, }) => {
10823
- const auth = useAuth();
11414
+ // Default Smartlinks Google OAuth Client ID (public - safe to expose)
11415
+ const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
11416
+ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, minimal = false, }) => {
11417
+ const [mode, setMode] = useState(initialMode);
10824
11418
  const [loading, setLoading] = useState(false);
10825
- const [profile, setProfile] = useState(null);
10826
11419
  const [error, setError] = useState();
10827
- const [success, setSuccess] = useState();
10828
- // Track which section is being edited
10829
- const [editingSection, setEditingSection] = useState(null);
10830
- // Profile form state
10831
- const [displayName, setDisplayName] = useState('');
10832
- // Email change state
10833
- const [newEmail, setNewEmail] = useState('');
10834
- const [emailPassword, setEmailPassword] = useState('');
10835
- // Password change state
10836
- const [currentPassword, setCurrentPassword] = useState('');
10837
- const [newPassword, setNewPassword] = useState('');
10838
- const [confirmPassword, setConfirmPassword] = useState('');
10839
- // Phone change state (reuses existing sendPhoneCode flow)
10840
- const [newPhone, setNewPhone] = useState('');
10841
- const [phoneCode, setPhoneCode] = useState('');
10842
- const [phoneCodeSent, setPhoneCodeSent] = useState(false);
10843
- // Account deletion state
10844
- const [deletePassword, setDeletePassword] = useState('');
10845
- const [deleteConfirmText, setDeleteConfirmText] = useState('');
10846
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
10847
- const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, } = customization;
10848
- // Reinitialize Smartlinks SDK when apiEndpoint changes
11420
+ const [resetSuccess, setResetSuccess] = useState(false);
11421
+ const [authSuccess, setAuthSuccess] = useState(false);
11422
+ const [successMessage, setSuccessMessage] = useState();
11423
+ const [showResendVerification, setShowResendVerification] = useState(false);
11424
+ const [resendEmail, setResendEmail] = useState();
11425
+ const [showRequestNewReset, setShowRequestNewReset] = useState(false);
11426
+ const [resetRequestEmail, setResetRequestEmail] = useState();
11427
+ const [resetToken, setResetToken] = useState(); // Store the reset token from URL
11428
+ const [config, setConfig] = useState(null);
11429
+ const [configLoading, setConfigLoading] = useState(!skipConfigFetch);
11430
+ const [showEmailForm, setShowEmailForm] = useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
11431
+ const api = new AuthAPI(apiEndpoint, clientId, clientName);
11432
+ const auth = useAuth();
11433
+ // Reinitialize Smartlinks SDK when apiEndpoint changes (for test/dev scenarios)
11434
+ // IMPORTANT: Preserve bearer token during reinitialization
10849
11435
  useEffect(() => {
10850
- if (apiEndpoint) {
10851
- smartlinks.initializeApi({
10852
- baseURL: apiEndpoint,
10853
- proxyMode: false,
10854
- ngrokSkipBrowserWarning: true,
10855
- });
10856
- }
10857
- }, [apiEndpoint]);
10858
- // Load user profile on mount
11436
+ const reinitializeWithToken = async () => {
11437
+ if (apiEndpoint) {
11438
+ // Get current token before reinitializing
11439
+ const currentToken = await auth.getToken();
11440
+ smartlinks.initializeApi({
11441
+ baseURL: apiEndpoint,
11442
+ proxyMode: false, // Direct API calls when custom endpoint is provided
11443
+ ngrokSkipBrowserWarning: true,
11444
+ });
11445
+ // Restore bearer token after reinitialization using auth.verifyToken
11446
+ if (currentToken) {
11447
+ smartlinks.auth.verifyToken(currentToken).catch(err => {
11448
+ console.warn('Failed to restore bearer token after reinit:', err);
11449
+ });
11450
+ }
11451
+ }
11452
+ };
11453
+ reinitializeWithToken();
11454
+ }, [apiEndpoint, auth]);
11455
+ // Get the effective redirect URL (use prop or default to current page)
11456
+ const getRedirectUrl = () => {
11457
+ if (redirectUrl)
11458
+ return redirectUrl;
11459
+ // Get the full current URL including hash routes
11460
+ // Remove any existing query parameters to avoid duplication
11461
+ const currentUrl = window.location.href.split('?')[0];
11462
+ return currentUrl;
11463
+ };
11464
+ // Fetch UI configuration
10859
11465
  useEffect(() => {
10860
- loadProfile();
10861
- }, [clientId]);
10862
- const loadProfile = async () => {
10863
- if (!auth.isAuthenticated) {
10864
- setError('You must be logged in to manage your account');
11466
+ if (skipConfigFetch) {
11467
+ setConfig(customization || {});
10865
11468
  return;
10866
11469
  }
11470
+ const fetchConfig = async () => {
11471
+ try {
11472
+ // Check localStorage cache first
11473
+ const cacheKey = `auth_ui_config_${clientId || 'default'}`;
11474
+ const cached = localStorage.getItem(cacheKey);
11475
+ if (cached) {
11476
+ const { config: cachedConfig, timestamp } = JSON.parse(cached);
11477
+ const age = Date.now() - timestamp;
11478
+ // Use cache if less than 1 hour old
11479
+ if (age < 3600000) {
11480
+ setConfig({ ...cachedConfig, ...customization });
11481
+ setConfigLoading(false);
11482
+ // Fetch in background to update cache
11483
+ api.fetchConfig().then(freshConfig => {
11484
+ localStorage.setItem(cacheKey, JSON.stringify({
11485
+ config: freshConfig,
11486
+ timestamp: Date.now()
11487
+ }));
11488
+ });
11489
+ return;
11490
+ }
11491
+ }
11492
+ // Fetch from API
11493
+ const fetchedConfig = await api.fetchConfig();
11494
+ // Merge with customization props (props take precedence)
11495
+ const mergedConfig = { ...fetchedConfig, ...customization };
11496
+ setConfig(mergedConfig);
11497
+ // Cache the fetched config
11498
+ localStorage.setItem(cacheKey, JSON.stringify({
11499
+ config: fetchedConfig,
11500
+ timestamp: Date.now()
11501
+ }));
11502
+ }
11503
+ catch (err) {
11504
+ console.error('Failed to fetch config:', err);
11505
+ setConfig(customization || {});
11506
+ }
11507
+ finally {
11508
+ setConfigLoading(false);
11509
+ }
11510
+ };
11511
+ fetchConfig();
11512
+ }, [apiEndpoint, clientId, customization, skipConfigFetch]);
11513
+ // Reset showEmailForm when mode changes away from login/register
11514
+ useEffect(() => {
11515
+ if (mode !== 'login' && mode !== 'register') {
11516
+ setShowEmailForm(false);
11517
+ }
11518
+ }, [mode]);
11519
+ // Handle URL-based auth flows (email verification, password reset)
11520
+ useEffect(() => {
11521
+ // Helper to get URL parameters from either hash or search
11522
+ const getUrlParams = () => {
11523
+ // First check if there are params in the hash (for hash routing)
11524
+ const hash = window.location.hash;
11525
+ const hashQueryIndex = hash.indexOf('?');
11526
+ if (hashQueryIndex !== -1) {
11527
+ // Extract query string from hash (e.g., #/test?mode=verifyEmail&token=abc)
11528
+ const hashQuery = hash.substring(hashQueryIndex + 1);
11529
+ return new URLSearchParams(hashQuery);
11530
+ }
11531
+ // Fall back to regular search params (for non-hash routing)
11532
+ return new URLSearchParams(window.location.search);
11533
+ };
11534
+ const params = getUrlParams();
11535
+ const urlMode = params.get('mode');
11536
+ const token = params.get('token');
11537
+ console.log('URL params detected:', { urlMode, token, hash: window.location.hash, search: window.location.search });
11538
+ if (urlMode && token) {
11539
+ handleURLBasedAuth(urlMode, token);
11540
+ }
11541
+ }, []);
11542
+ const handleURLBasedAuth = async (urlMode, token) => {
10867
11543
  setLoading(true);
10868
11544
  setError(undefined);
10869
11545
  try {
10870
- // TODO: Backend implementation required
10871
- // Endpoint: GET /api/v1/authkit/:clientId/account/profile
10872
- // SDK method: smartlinks.authKit.getProfile(clientId)
10873
- // Temporary mock data for UI testing
10874
- const profileData = {
10875
- uid: auth.user?.uid || '',
10876
- email: auth.user?.email,
10877
- displayName: auth.user?.displayName,
10878
- phoneNumber: auth.user?.phoneNumber,
10879
- photoURL: auth.user?.photoURL,
10880
- emailVerified: true,
10881
- accountData: auth.accountData || {},
10882
- };
10883
- setProfile(profileData);
10884
- setDisplayName(profileData.displayName || '');
11546
+ if (urlMode === 'verifyEmail') {
11547
+ console.log('Verifying email with token:', token);
11548
+ const response = await api.verifyEmailWithToken(token);
11549
+ // Get email verification mode from response or config
11550
+ const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
11551
+ if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
11552
+ // Auto-login modes: Log the user in immediately if token is provided
11553
+ auth.login(response.token, response.user, response.accountData);
11554
+ setAuthSuccess(true);
11555
+ setSuccessMessage('Email verified successfully! You are now logged in.');
11556
+ onAuthSuccess(response.token, response.user, response.accountData);
11557
+ // Clear the URL parameters
11558
+ const cleanUrl = window.location.href.split('?')[0];
11559
+ window.history.replaceState({}, document.title, cleanUrl);
11560
+ // Redirect after a brief delay to show success message
11561
+ if (redirectUrl) {
11562
+ setTimeout(() => {
11563
+ window.location.href = redirectUrl;
11564
+ }, 2000);
11565
+ }
11566
+ }
11567
+ else {
11568
+ // verify-then-manual-login mode or no token: Show success but require manual login
11569
+ setAuthSuccess(true);
11570
+ setSuccessMessage('Email verified successfully! Please log in with your credentials.');
11571
+ // Clear the URL parameters
11572
+ const cleanUrl = window.location.href.split('?')[0];
11573
+ window.history.replaceState({}, document.title, cleanUrl);
11574
+ // Switch back to login mode after a delay
11575
+ setTimeout(() => {
11576
+ setAuthSuccess(false);
11577
+ setMode('login');
11578
+ }, 3000);
11579
+ }
11580
+ }
11581
+ else if (urlMode === 'resetPassword') {
11582
+ console.log('Verifying reset token:', token);
11583
+ // Verify token is valid, then show password reset form
11584
+ await api.verifyResetToken(token);
11585
+ setResetToken(token); // Store token for use in password reset
11586
+ setMode('reset-password');
11587
+ // Clear the URL parameters
11588
+ const cleanUrl = window.location.href.split('?')[0];
11589
+ window.history.replaceState({}, document.title, cleanUrl);
11590
+ }
11591
+ else if (urlMode === 'magicLink') {
11592
+ console.log('Verifying magic link token:', token);
11593
+ const response = await api.verifyMagicLink(token);
11594
+ // Auto-login with magic link if token is provided
11595
+ if (response.token) {
11596
+ auth.login(response.token, response.user, response.accountData);
11597
+ setAuthSuccess(true);
11598
+ setSuccessMessage('Magic link verified! You are now logged in.');
11599
+ onAuthSuccess(response.token, response.user, response.accountData);
11600
+ // Clear the URL parameters
11601
+ const cleanUrl = window.location.href.split('?')[0];
11602
+ window.history.replaceState({}, document.title, cleanUrl);
11603
+ // Redirect after a brief delay to show success message
11604
+ if (redirectUrl) {
11605
+ setTimeout(() => {
11606
+ window.location.href = redirectUrl;
11607
+ }, 2000);
11608
+ }
11609
+ }
11610
+ else {
11611
+ throw new Error('Authentication failed - no token received');
11612
+ }
11613
+ }
10885
11614
  }
10886
11615
  catch (err) {
10887
- const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
10888
- setError(errorMessage);
10889
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11616
+ console.error('URL-based auth error:', err);
11617
+ const errorMessage = err instanceof Error ? err.message : 'An error occurred';
11618
+ // If it's an email verification error (expired/invalid token), show resend option
11619
+ if (urlMode === 'verifyEmail') {
11620
+ setError(`${errorMessage} - Please enter your email below to receive a new verification link.`);
11621
+ setShowResendVerification(true);
11622
+ setMode('login'); // Show the login form UI
11623
+ // Clear the URL parameters
11624
+ const cleanUrl = window.location.href.split('?')[0];
11625
+ window.history.replaceState({}, document.title, cleanUrl);
11626
+ }
11627
+ else if (urlMode === 'resetPassword') {
11628
+ // If password reset token is invalid/expired, show request new reset link option
11629
+ setError(`${errorMessage} - Please enter your email below to receive a new password reset link.`);
11630
+ setShowRequestNewReset(true);
11631
+ setMode('login');
11632
+ // Clear the URL parameters
11633
+ const cleanUrl = window.location.href.split('?')[0];
11634
+ window.history.replaceState({}, document.title, cleanUrl);
11635
+ }
11636
+ else if (urlMode === 'magicLink') {
11637
+ // If magic link is invalid/expired
11638
+ setError(`${errorMessage} - Please request a new magic link below.`);
11639
+ setMode('magic-link');
11640
+ // Clear the URL parameters
11641
+ const cleanUrl = window.location.href.split('?')[0];
11642
+ window.history.replaceState({}, document.title, cleanUrl);
11643
+ }
11644
+ else {
11645
+ setError(errorMessage);
11646
+ }
11647
+ onAuthError?.(err instanceof Error ? err : new Error('An error occurred'));
10890
11648
  }
10891
11649
  finally {
10892
11650
  setLoading(false);
10893
11651
  }
10894
11652
  };
10895
- const handleUpdateProfile = async (e) => {
10896
- e.preventDefault();
11653
+ const handleEmailAuth = async (data) => {
10897
11654
  setLoading(true);
10898
11655
  setError(undefined);
10899
- setSuccess(undefined);
11656
+ setAuthSuccess(false);
10900
11657
  try {
10901
- // TODO: Backend implementation required
10902
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-profile
10903
- // SDK method: smartlinks.authKit.updateProfile(clientId, updateData)
10904
- setError('Backend API not yet implemented. See console for required endpoint.');
10905
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-profile');
10906
- console.log('Update data:', { displayName });
10907
- // Uncomment when backend is ready:
10908
- // const updateData: ProfileUpdateData = {
10909
- // displayName: displayName || undefined,
10910
- // photoURL: photoURL || undefined,
10911
- // };
10912
- // const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
10913
- // setProfile(updatedProfile);
10914
- // setSuccess('Profile updated successfully!');
10915
- // setEditingSection(null);
10916
- // onProfileUpdated?.(updatedProfile);
11658
+ const response = mode === 'login'
11659
+ ? await api.login(data.email, data.password)
11660
+ : await api.register({
11661
+ ...data,
11662
+ accountData: mode === 'register' ? accountData : undefined,
11663
+ redirectUrl: getRedirectUrl(), // Include redirect URL for email verification
11664
+ });
11665
+ // Get email verification mode from response or config (default: verify-then-auto-login)
11666
+ const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
11667
+ const gracePeriodHours = config?.emailVerification?.gracePeriodHours || 24;
11668
+ if (mode === 'register') {
11669
+ // Handle different verification modes
11670
+ if (verificationMode === 'immediate' && response.token) {
11671
+ // Immediate mode: Log in right away if token is provided
11672
+ auth.login(response.token, response.user, response.accountData);
11673
+ setAuthSuccess(true);
11674
+ const deadline = response.emailVerificationDeadline
11675
+ ? new Date(response.emailVerificationDeadline).toLocaleString()
11676
+ : `${gracePeriodHours} hours`;
11677
+ setSuccessMessage(`Account created! You're logged in now. Please verify your email by ${deadline} to keep your account active.`);
11678
+ if (response.token) {
11679
+ onAuthSuccess(response.token, response.user, response.accountData);
11680
+ }
11681
+ if (redirectUrl) {
11682
+ setTimeout(() => {
11683
+ window.location.href = redirectUrl;
11684
+ }, 2000);
11685
+ }
11686
+ }
11687
+ else if (verificationMode === 'verify-then-auto-login') {
11688
+ // Verify-then-auto-login mode: Don't log in yet, but will auto-login after email verification
11689
+ setAuthSuccess(true);
11690
+ setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
11691
+ }
11692
+ else {
11693
+ // verify-then-manual-login mode: Traditional flow
11694
+ setAuthSuccess(true);
11695
+ setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
11696
+ }
11697
+ }
11698
+ else {
11699
+ // Login mode - always log in if token is provided
11700
+ if (response.token) {
11701
+ // Check for account lock or verification requirements
11702
+ if (response.accountLocked) {
11703
+ throw new Error('Your account has been locked due to unverified email. Please check your email or request a new verification link.');
11704
+ }
11705
+ if (response.requiresEmailVerification) {
11706
+ throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
11707
+ }
11708
+ auth.login(response.token, response.user, response.accountData);
11709
+ setAuthSuccess(true);
11710
+ setSuccessMessage('Login successful!');
11711
+ onAuthSuccess(response.token, response.user, response.accountData);
11712
+ if (redirectUrl) {
11713
+ setTimeout(() => {
11714
+ window.location.href = redirectUrl;
11715
+ }, 2000);
11716
+ }
11717
+ }
11718
+ else {
11719
+ throw new Error('Authentication failed - please verify your email before logging in.');
11720
+ }
11721
+ }
10917
11722
  }
10918
11723
  catch (err) {
10919
- const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
10920
- setError(errorMessage);
10921
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11724
+ const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
11725
+ // Check if error is about email already registered
11726
+ if (mode === 'register' && errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) {
11727
+ setShowResendVerification(true);
11728
+ setResendEmail(data.email);
11729
+ setError('This email is already registered. If you didn\'t receive the verification email, you can resend it below.');
11730
+ }
11731
+ else {
11732
+ setError(errorMessage);
11733
+ }
11734
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
10922
11735
  }
10923
11736
  finally {
10924
11737
  setLoading(false);
10925
11738
  }
10926
11739
  };
10927
- const cancelEdit = () => {
10928
- setEditingSection(null);
10929
- setDisplayName(profile?.displayName || '');
10930
- setNewEmail('');
10931
- setEmailPassword('');
10932
- setCurrentPassword('');
10933
- setNewPassword('');
10934
- setConfirmPassword('');
10935
- setNewPhone('');
10936
- setPhoneCode('');
10937
- setPhoneCodeSent(false);
10938
- setError(undefined);
10939
- setSuccess(undefined);
10940
- };
10941
- const handleChangeEmail = async (e) => {
10942
- e.preventDefault();
11740
+ const handleResendVerification = async () => {
11741
+ if (!resendEmail)
11742
+ return;
10943
11743
  setLoading(true);
10944
11744
  setError(undefined);
10945
- setSuccess(undefined);
10946
11745
  try {
10947
- // TODO: Backend implementation required
10948
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-email
10949
- // SDK method: smartlinks.authKit.changeEmail(clientId, newEmail, password)
10950
- // Note: No verification flow for now - direct email update
10951
- setError('Backend API not yet implemented. See console for required endpoint.');
10952
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-email');
10953
- console.log('Data:', { newEmail });
10954
- // Uncomment when backend is ready:
10955
- // await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
10956
- // setSuccess('Email changed successfully!');
10957
- // setEditingSection(null);
10958
- // setNewEmail('');
10959
- // setEmailPassword('');
10960
- // onEmailChangeRequested?.();
10961
- // await loadProfile(); // Reload to show new email
11746
+ // For resend, we need the userId. If we don't have it, we need to handle this differently
11747
+ // The backend should ideally handle this case
11748
+ await api.resendVerification('unknown', resendEmail, getRedirectUrl());
11749
+ setAuthSuccess(true);
11750
+ setSuccessMessage('Verification email sent! Please check your inbox.');
11751
+ setShowResendVerification(false);
10962
11752
  }
10963
11753
  catch (err) {
10964
- const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
11754
+ const errorMessage = err instanceof Error ? err.message : 'Failed to resend verification email';
10965
11755
  setError(errorMessage);
10966
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11756
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
10967
11757
  }
10968
11758
  finally {
10969
11759
  setLoading(false);
10970
11760
  }
10971
11761
  };
10972
- const handleChangePassword = async (e) => {
10973
- e.preventDefault();
10974
- if (newPassword !== confirmPassword) {
10975
- setError('New passwords do not match');
10976
- return;
10977
- }
10978
- if (newPassword.length < 6) {
10979
- setError('Password must be at least 6 characters');
11762
+ const handleRequestNewReset = async () => {
11763
+ if (!resetRequestEmail)
10980
11764
  return;
10981
- }
10982
11765
  setLoading(true);
10983
11766
  setError(undefined);
10984
- setSuccess(undefined);
10985
11767
  try {
10986
- // TODO: Backend implementation required
10987
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-password
10988
- // SDK method: smartlinks.authKit.changePassword(clientId, currentPassword, newPassword)
10989
- setError('Backend API not yet implemented. See console for required endpoint.');
10990
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-password');
10991
- console.log('Data: currentPassword and newPassword provided');
10992
- // Uncomment when backend is ready:
10993
- // await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
10994
- // setSuccess('Password changed successfully!');
10995
- // setEditingSection(null);
10996
- // setCurrentPassword('');
10997
- // setNewPassword('');
10998
- // setConfirmPassword('');
10999
- // onPasswordChanged?.();
11768
+ await api.requestPasswordReset(resetRequestEmail, getRedirectUrl());
11769
+ setAuthSuccess(true);
11770
+ setSuccessMessage('Password reset email sent! Please check your inbox.');
11771
+ setShowRequestNewReset(false);
11772
+ setResetRequestEmail('');
11000
11773
  }
11001
11774
  catch (err) {
11002
- const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
11775
+ const errorMessage = err instanceof Error ? err.message : 'Failed to send password reset email';
11003
11776
  setError(errorMessage);
11004
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11777
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11005
11778
  }
11006
11779
  finally {
11007
11780
  setLoading(false);
11008
11781
  }
11009
11782
  };
11010
- const handleSendPhoneCode = async () => {
11783
+ const handleGoogleLogin = async () => {
11784
+ // Use custom client ID from config, or fall back to default Smartlinks client ID
11785
+ const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
11786
+ // Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
11787
+ const oauthFlow = config?.googleOAuthFlow || 'oneTap';
11011
11788
  setLoading(true);
11012
11789
  setError(undefined);
11013
11790
  try {
11014
- await smartlinks.authKit.sendPhoneCode(clientId, newPhone);
11015
- setPhoneCodeSent(true);
11016
- setSuccess('Verification code sent to your phone');
11791
+ const google = window.google;
11792
+ if (!google) {
11793
+ throw new Error('Google Identity Services not loaded. Please check your internet connection.');
11794
+ }
11795
+ if (oauthFlow === 'popup') {
11796
+ // Use OAuth2 popup flow (works in iframes but requires popup permission)
11797
+ if (!google.accounts.oauth2) {
11798
+ throw new Error('Google OAuth2 not available');
11799
+ }
11800
+ const client = google.accounts.oauth2.initTokenClient({
11801
+ client_id: googleClientId,
11802
+ scope: 'openid email profile',
11803
+ callback: async (response) => {
11804
+ try {
11805
+ if (response.error) {
11806
+ throw new Error(response.error_description || response.error);
11807
+ }
11808
+ const accessToken = response.access_token;
11809
+ // Send access token to backend
11810
+ const authResponse = await api.loginWithGoogle(accessToken);
11811
+ if (authResponse.token) {
11812
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData);
11813
+ setAuthSuccess(true);
11814
+ setSuccessMessage('Google login successful!');
11815
+ onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
11816
+ }
11817
+ else {
11818
+ throw new Error('Authentication failed - no token received');
11819
+ }
11820
+ if (redirectUrl) {
11821
+ setTimeout(() => {
11822
+ window.location.href = redirectUrl;
11823
+ }, 2000);
11824
+ }
11825
+ setLoading(false);
11826
+ }
11827
+ catch (err) {
11828
+ const errorMessage = err instanceof Error ? err.message : 'Google login failed';
11829
+ setError(errorMessage);
11830
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11831
+ setLoading(false);
11832
+ }
11833
+ },
11834
+ });
11835
+ client.requestAccessToken();
11836
+ }
11837
+ else {
11838
+ // Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
11839
+ google.accounts.id.initialize({
11840
+ client_id: googleClientId,
11841
+ callback: async (response) => {
11842
+ try {
11843
+ const idToken = response.credential;
11844
+ const authResponse = await api.loginWithGoogle(idToken);
11845
+ if (authResponse.token) {
11846
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData);
11847
+ setAuthSuccess(true);
11848
+ setSuccessMessage('Google login successful!');
11849
+ onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
11850
+ }
11851
+ else {
11852
+ throw new Error('Authentication failed - no token received');
11853
+ }
11854
+ if (redirectUrl) {
11855
+ setTimeout(() => {
11856
+ window.location.href = redirectUrl;
11857
+ }, 2000);
11858
+ }
11859
+ setLoading(false);
11860
+ }
11861
+ catch (err) {
11862
+ const errorMessage = err instanceof Error ? err.message : 'Google login failed';
11863
+ setError(errorMessage);
11864
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11865
+ setLoading(false);
11866
+ }
11867
+ },
11868
+ auto_select: false,
11869
+ cancel_on_tap_outside: true,
11870
+ });
11871
+ google.accounts.id.prompt((notification) => {
11872
+ if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
11873
+ setLoading(false);
11874
+ }
11875
+ });
11876
+ }
11017
11877
  }
11018
11878
  catch (err) {
11019
- const errorMessage = err instanceof Error ? err.message : 'Failed to send verification code';
11879
+ const errorMessage = err instanceof Error ? err.message : 'Google login failed';
11020
11880
  setError(errorMessage);
11021
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11022
- }
11023
- finally {
11881
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11024
11882
  setLoading(false);
11025
11883
  }
11026
11884
  };
11027
- const handleUpdatePhone = async (e) => {
11028
- e.preventDefault();
11885
+ const handlePhoneAuth = async (phoneNumber, verificationCode) => {
11029
11886
  setLoading(true);
11030
11887
  setError(undefined);
11031
- setSuccess(undefined);
11032
11888
  try {
11033
- // TODO: Backend implementation required
11034
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-phone
11035
- // SDK method: smartlinks.authKit.updatePhone(clientId, phoneNumber, verificationCode)
11036
- setError('Backend API not yet implemented. See console for required endpoint.');
11037
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-phone');
11038
- console.log('Data:', { phoneNumber: newPhone, code: phoneCode });
11039
- // Uncomment when backend is ready:
11040
- // await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
11041
- // setSuccess('Phone number updated successfully!');
11042
- // setEditingSection(null);
11043
- // setNewPhone('');
11044
- // setPhoneCode('');
11045
- // setPhoneCodeSent(false);
11046
- // await loadProfile();
11889
+ if (!verificationCode) {
11890
+ // Send verification code via Twilio Verify Service
11891
+ await api.sendPhoneCode(phoneNumber);
11892
+ // Twilio Verify Service tracks the verification by phone number
11893
+ // No need to store verificationId
11894
+ }
11895
+ else {
11896
+ // Verify code - Twilio identifies the verification by phone number
11897
+ const response = await api.verifyPhoneCode(phoneNumber, verificationCode);
11898
+ // Update auth context with account data if token is provided
11899
+ if (response.token) {
11900
+ auth.login(response.token, response.user, response.accountData);
11901
+ onAuthSuccess(response.token, response.user, response.accountData);
11902
+ if (redirectUrl) {
11903
+ window.location.href = redirectUrl;
11904
+ }
11905
+ }
11906
+ else {
11907
+ throw new Error('Authentication failed - no token received');
11908
+ }
11909
+ }
11047
11910
  }
11048
11911
  catch (err) {
11049
- const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
11912
+ const errorMessage = err instanceof Error ? err.message : 'Phone authentication failed';
11050
11913
  setError(errorMessage);
11051
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11914
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11052
11915
  }
11053
11916
  finally {
11054
11917
  setLoading(false);
11055
11918
  }
11056
11919
  };
11057
- const handleDeleteAccount = async () => {
11058
- if (deleteConfirmText !== 'DELETE') {
11059
- setError('Please type DELETE to confirm account deletion');
11060
- return;
11920
+ const handlePasswordReset = async (emailOrPassword, confirmPassword) => {
11921
+ setLoading(true);
11922
+ setError(undefined);
11923
+ try {
11924
+ if (resetToken && confirmPassword) {
11925
+ // Complete password reset with token
11926
+ await api.completePasswordReset(resetToken, emailOrPassword);
11927
+ setResetSuccess(true);
11928
+ setResetToken(undefined); // Clear token after successful reset
11929
+ }
11930
+ else {
11931
+ // Request password reset email
11932
+ await api.requestPasswordReset(emailOrPassword, getRedirectUrl());
11933
+ setResetSuccess(true);
11934
+ }
11061
11935
  }
11062
- if (!deletePassword) {
11063
- setError('Password is required');
11064
- return;
11936
+ catch (err) {
11937
+ const errorMessage = err instanceof Error ? err.message : 'Password reset failed';
11938
+ setError(errorMessage);
11939
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11940
+ }
11941
+ finally {
11942
+ setLoading(false);
11065
11943
  }
11944
+ };
11945
+ const handleMagicLink = async (email) => {
11066
11946
  setLoading(true);
11067
11947
  setError(undefined);
11068
11948
  try {
11069
- // TODO: Backend implementation required
11070
- // Endpoint: DELETE /api/v1/authkit/:clientId/account/delete
11071
- // SDK method: smartlinks.authKit.deleteAccount(clientId, password, confirmText)
11072
- // Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
11073
- setError('Backend API not yet implemented. See console for required endpoint.');
11074
- console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
11075
- console.log('Data: password and confirmText="DELETE" provided');
11076
- console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
11077
- // Uncomment when backend is ready:
11078
- // await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
11079
- // setSuccess('Account deleted successfully');
11080
- // onAccountDeleted?.();
11081
- // await auth.logout();
11949
+ await api.sendMagicLink(email, getRedirectUrl());
11950
+ setAuthSuccess(true);
11951
+ setSuccessMessage('Magic link sent! Check your email to log in.');
11082
11952
  }
11083
11953
  catch (err) {
11084
- const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
11954
+ const errorMessage = err instanceof Error ? err.message : 'Failed to send magic link';
11085
11955
  setError(errorMessage);
11086
- onError?.(err instanceof Error ? err : new Error(errorMessage));
11956
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11087
11957
  }
11088
11958
  finally {
11089
11959
  setLoading(false);
11090
11960
  }
11091
11961
  };
11092
- if (!auth.isAuthenticated) {
11093
- return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Please log in to manage your account" }) }));
11094
- }
11095
- if (loading && !profile) {
11096
- return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
11097
- }
11098
- return (jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Display Name" }), jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsx("div", { className: "form-group", children: jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Email Address" }), jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Password" }), jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Phone Number" }), jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxs("section", { className: "account-section danger-zone", children: [jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxs("div", { className: "delete-confirm", children: [jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsx("button", { type: "button", onClick: () => {
11099
- setShowDeleteConfirm(false);
11100
- setDeletePassword('');
11101
- setDeleteConfirmText('');
11102
- }, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
11103
- };
11104
-
11105
- const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
11106
- const { isAuthenticated, isLoading } = useAuth();
11107
- // Show loading state
11108
- if (isLoading) {
11109
- return jsx("div", { children: "Loading..." });
11110
- }
11111
- // If not authenticated, redirect or show fallback
11112
- if (!isAuthenticated) {
11113
- if (redirectTo) {
11114
- window.location.href = redirectTo;
11115
- return null;
11116
- }
11117
- return fallback ? jsx(Fragment, { children: fallback }) : jsx("div", { children: "Access denied. Please log in." });
11962
+ if (configLoading) {
11963
+ return (jsx(AuthContainer, { theme: theme, className: className, minimal: minimal || config?.branding?.minimal || false, children: jsx("div", { style: { textAlign: 'center', padding: '2rem' }, children: jsx("div", { className: "auth-spinner" }) }) }));
11118
11964
  }
11119
- // Render protected content
11120
- return jsx(Fragment, { children: children });
11965
+ return (jsx(AuthContainer, { theme: theme, className: className, config: config, minimal: minimal || config?.branding?.minimal || false, children: authSuccess ? (jsxs("div", { style: { textAlign: 'center', padding: '2rem' }, children: [jsx("div", { style: {
11966
+ color: 'var(--auth-primary-color, #4F46E5)',
11967
+ fontSize: '3rem',
11968
+ marginBottom: '1rem'
11969
+ }, children: "\u2713" }), jsx("h2", { style: {
11970
+ marginBottom: '0.5rem',
11971
+ fontSize: '1.5rem',
11972
+ fontWeight: 600
11973
+ }, children: successMessage?.includes('verified') ? 'Email Verified!' :
11974
+ successMessage?.includes('Magic link') ? 'Check Your Email!' :
11975
+ mode === 'register' ? 'Account Created!' : 'Login Successful!' }), jsx("p", { style: {
11976
+ color: '#6B7280',
11977
+ fontSize: '0.875rem'
11978
+ }, children: successMessage })] })) : mode === 'magic-link' ? (jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
11979
+ setMode('login');
11980
+ setResetSuccess(false);
11981
+ setResetToken(undefined); // Clear token when going back
11982
+ }, loading: loading, error: error, success: resetSuccess, token: resetToken })) : (mode === 'login' || mode === 'register') ? (jsx(Fragment, { children: showResendVerification ? (jsxs("div", { style: { marginTop: '1rem', padding: '1.5rem', backgroundColor: 'rgba(79, 70, 229, 0.05)', borderRadius: '0.5rem' }, children: [jsx("h3", { style: { marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600, color: 'var(--auth-text-color, #374151)' }, children: "Verification Link Expired" }), jsx("p", { style: { marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--auth-text-color, #6B7280)', lineHeight: '1.5' }, children: "Your verification link has expired or is no longer valid. Please enter your email address below and we'll send you a new verification link." }), jsx("input", { type: "email", value: resendEmail || '', onChange: (e) => setResendEmail(e.target.value), placeholder: "your@email.com", style: {
11983
+ width: '100%',
11984
+ padding: '0.625rem',
11985
+ marginBottom: '1rem',
11986
+ border: '1px solid var(--auth-border-color, #D1D5DB)',
11987
+ borderRadius: '0.375rem',
11988
+ fontSize: '0.875rem',
11989
+ boxSizing: 'border-box'
11990
+ } }), jsxs("div", { style: { display: 'flex', gap: '0.75rem' }, children: [jsx("button", { onClick: handleResendVerification, disabled: loading || !resendEmail, style: {
11991
+ flex: 1,
11992
+ padding: '0.625rem 1rem',
11993
+ backgroundColor: 'var(--auth-primary-color, #4F46E5)',
11994
+ color: 'white',
11995
+ border: 'none',
11996
+ borderRadius: '0.375rem',
11997
+ cursor: (loading || !resendEmail) ? 'not-allowed' : 'pointer',
11998
+ fontSize: '0.875rem',
11999
+ fontWeight: 500,
12000
+ opacity: (loading || !resendEmail) ? 0.6 : 1
12001
+ }, children: loading ? 'Sending...' : 'Send New Verification Link' }), jsx("button", { onClick: () => {
12002
+ setShowResendVerification(false);
12003
+ setResendEmail('');
12004
+ setError(undefined);
12005
+ }, disabled: loading, style: {
12006
+ padding: '0.625rem 1rem',
12007
+ backgroundColor: 'transparent',
12008
+ color: 'var(--auth-text-color, #6B7280)',
12009
+ border: '1px solid var(--auth-border-color, #D1D5DB)',
12010
+ borderRadius: '0.375rem',
12011
+ cursor: loading ? 'not-allowed' : 'pointer',
12012
+ fontSize: '0.875rem',
12013
+ fontWeight: 500,
12014
+ opacity: loading ? 0.6 : 1
12015
+ }, children: "Cancel" })] })] })) : showRequestNewReset ? (jsxs("div", { style: { marginTop: '1rem', padding: '1.5rem', backgroundColor: 'rgba(239, 68, 68, 0.05)', borderRadius: '0.5rem' }, children: [jsx("h3", { style: { marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600, color: 'var(--auth-text-color, #374151)' }, children: "Password Reset Link Expired" }), jsx("p", { style: { marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--auth-text-color, #6B7280)', lineHeight: '1.5' }, children: "Your password reset link has expired or is no longer valid. Please enter your email address below and we'll send you a new password reset link." }), jsx("input", { type: "email", value: resetRequestEmail || '', onChange: (e) => setResetRequestEmail(e.target.value), placeholder: "your@email.com", style: {
12016
+ width: '100%',
12017
+ padding: '0.625rem',
12018
+ marginBottom: '1rem',
12019
+ border: '1px solid var(--auth-border-color, #D1D5DB)',
12020
+ borderRadius: '0.375rem',
12021
+ fontSize: '0.875rem',
12022
+ boxSizing: 'border-box'
12023
+ } }), jsxs("div", { style: { display: 'flex', gap: '0.75rem' }, children: [jsx("button", { onClick: handleRequestNewReset, disabled: loading || !resetRequestEmail, style: {
12024
+ flex: 1,
12025
+ padding: '0.625rem 1rem',
12026
+ backgroundColor: '#EF4444',
12027
+ color: 'white',
12028
+ border: 'none',
12029
+ borderRadius: '0.375rem',
12030
+ cursor: (loading || !resetRequestEmail) ? 'not-allowed' : 'pointer',
12031
+ fontSize: '0.875rem',
12032
+ fontWeight: 500,
12033
+ opacity: (loading || !resetRequestEmail) ? 0.6 : 1
12034
+ }, children: loading ? 'Sending...' : 'Send New Reset Link' }), jsx("button", { onClick: () => {
12035
+ setShowRequestNewReset(false);
12036
+ setResetRequestEmail('');
12037
+ setError(undefined);
12038
+ }, disabled: loading, style: {
12039
+ padding: '0.625rem 1rem',
12040
+ backgroundColor: 'transparent',
12041
+ color: 'var(--auth-text-color, #6B7280)',
12042
+ border: '1px solid var(--auth-border-color, #D1D5DB)',
12043
+ borderRadius: '0.375rem',
12044
+ cursor: loading ? 'not-allowed' : 'pointer',
12045
+ fontSize: '0.875rem',
12046
+ fontWeight: 500,
12047
+ opacity: loading ? 0.6 : 1
12048
+ }, children: "Cancel" })] })] })) : (jsx(Fragment, { children: (() => {
12049
+ const emailDisplayMode = config?.emailDisplayMode || 'form';
12050
+ const providerOrder = config?.providerOrder || (config?.enabledProviders || enabledProviders);
12051
+ const actualProviders = config?.enabledProviders || enabledProviders;
12052
+ // Button mode: show provider selection first, then email form if email is selected
12053
+ if (emailDisplayMode === 'button' && !showEmailForm) {
12054
+ return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
12055
+ }
12056
+ // Form mode or email button was clicked: show email form with other providers
12057
+ return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
12058
+ marginBottom: '1rem',
12059
+ padding: '0.5rem',
12060
+ background: 'none',
12061
+ border: 'none',
12062
+ color: 'var(--auth-text-color, #6B7280)',
12063
+ cursor: 'pointer',
12064
+ fontSize: '0.875rem',
12065
+ display: 'flex',
12066
+ alignItems: 'center',
12067
+ gap: '0.25rem'
12068
+ }, children: "\u2190 Back to options" })), jsx(EmailAuthForm, { mode: mode, onSubmit: handleEmailAuth, onModeSwitch: () => {
12069
+ setMode(mode === 'login' ? 'register' : 'login');
12070
+ setShowResendVerification(false);
12071
+ setShowRequestNewReset(false);
12072
+ setError(undefined);
12073
+ }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
12074
+ })() })) })) : null }));
11121
12075
  };
11122
12076
 
11123
- const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', 'phone'], providerOrder, emailDisplayMode = 'form', theme = 'light', className, }) => {
11124
- const showEmail = enabledProviders.includes('email');
11125
- const showGoogle = enabledProviders.includes('google');
11126
- const showPhone = enabledProviders.includes('phone');
11127
- const showMagicLink = enabledProviders.includes('magic-link');
11128
- // Determine ordered providers (excluding email if in button mode)
11129
- const orderedProviders = providerOrder && providerOrder.length > 0
11130
- ? providerOrder.filter(p => enabledProviders.includes(p) && p !== 'email')
11131
- : enabledProviders.filter(p => p !== 'email');
11132
- const hasOtherProviders = showGoogle || showPhone || showMagicLink;
11133
- // Render provider button helper
11134
- const renderProviderButton = (provider) => {
11135
- if (provider === 'google' && showGoogle) {
11136
- return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", children: [jsx("path", { d: "M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z", fill: "#4285F4" }), jsx("path", { d: "M9 18c2.43 0 4.467-.806 5.956-2.183l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z", fill: "#34A853" }), jsx("path", { d: "M3.964 10.707c-.18-.54-.282-1.117-.282-1.707 0-.593.102-1.167.282-1.707V4.961H.957C.347 6.175 0 7.548 0 9s.348 2.825.957 4.039l3.007-2.332z", fill: "#FBBC05" }), jsx("path", { d: "M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z", fill: "#EA4335" })] }), jsx("span", { children: "Continue with Google" })] }, "google"));
11137
- }
11138
- if (provider === 'phone' && showPhone) {
11139
- return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" }) }), jsx("span", { children: "Continue with Phone" })] }, "phone"));
11140
- }
11141
- if (provider === 'magic-link' && showMagicLink) {
11142
- return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Magic Link" })] }, "magic-link"));
11143
- }
11144
- if (provider === 'email' && showEmail && emailDisplayMode === 'button') {
11145
- return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Email" })] }, "email"));
11146
- }
11147
- return null;
11148
- };
11149
- return (jsx(AuthContainer, { theme: theme, className: className, config: customization, children: emailDisplayMode === 'button' ? (jsx("div", { className: "auth-provider-buttons", children: orderedProviders.concat(showEmail ? ['email'] : []).map(provider => renderProviderButton(provider)) })) : (
11150
- /* Form mode: show email form first, then other providers */
11151
- jsxs(Fragment, { children: [showEmail && (jsxs("div", { className: "auth-form", children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Email" }), jsx("input", { type: "email", className: "auth-input", placeholder: "Enter your email", disabled: true })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Password" }), jsx("input", { type: "password", className: "auth-input", placeholder: "Enter your password", disabled: true })] }), jsx("button", { className: "auth-button auth-button-primary", disabled: true, children: "Sign In" }), jsx("div", { style: { textAlign: 'center', marginTop: '1rem' }, children: jsx("button", { className: "auth-link", disabled: true, children: "Forgot password?" }) }), jsxs("div", { style: { textAlign: 'center', marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--auth-text-muted, #6B7280)' }, children: ["Don't have an account?", ' ', jsx("button", { className: "auth-link", disabled: true, children: "Sign up" })] })] })), hasOtherProviders && (jsxs(Fragment, { children: [showEmail && (jsx("div", { className: "auth-or-divider", children: jsx("span", { children: "or continue with" }) })), jsx("div", { className: "auth-provider-buttons", children: orderedProviders.map(provider => renderProviderButton(provider)) })] }))] })) }));
11152
- };
11153
-
11154
- // Default Smartlinks Google OAuth Client ID (public - safe to expose)
11155
- const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
11156
- const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, }) => {
11157
- const [mode, setMode] = useState(initialMode);
12077
+ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', customization = {}, }) => {
12078
+ const auth = useAuth();
11158
12079
  const [loading, setLoading] = useState(false);
12080
+ const [profile, setProfile] = useState(null);
11159
12081
  const [error, setError] = useState();
11160
- const [resetSuccess, setResetSuccess] = useState(false);
11161
- const [authSuccess, setAuthSuccess] = useState(false);
11162
- const [successMessage, setSuccessMessage] = useState();
11163
- const [showResendVerification, setShowResendVerification] = useState(false);
11164
- const [resendEmail, setResendEmail] = useState();
11165
- const [showRequestNewReset, setShowRequestNewReset] = useState(false);
11166
- const [resetRequestEmail, setResetRequestEmail] = useState();
11167
- const [resetToken, setResetToken] = useState(); // Store the reset token from URL
11168
- const [config, setConfig] = useState(null);
11169
- const [configLoading, setConfigLoading] = useState(!skipConfigFetch);
11170
- const [showEmailForm, setShowEmailForm] = useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
11171
- const api = new AuthAPI(apiEndpoint, clientId, clientName);
11172
- const auth = useAuth();
11173
- // Reinitialize Smartlinks SDK when apiEndpoint changes (for test/dev scenarios)
11174
- // IMPORTANT: Preserve bearer token during reinitialization
12082
+ const [success, setSuccess] = useState();
12083
+ // Track which section is being edited
12084
+ const [editingSection, setEditingSection] = useState(null);
12085
+ // Profile form state
12086
+ const [displayName, setDisplayName] = useState('');
12087
+ // Email change state
12088
+ const [newEmail, setNewEmail] = useState('');
12089
+ const [emailPassword, setEmailPassword] = useState('');
12090
+ // Password change state
12091
+ const [currentPassword, setCurrentPassword] = useState('');
12092
+ const [newPassword, setNewPassword] = useState('');
12093
+ const [confirmPassword, setConfirmPassword] = useState('');
12094
+ // Phone change state (reuses existing sendPhoneCode flow)
12095
+ const [newPhone, setNewPhone] = useState('');
12096
+ const [phoneCode, setPhoneCode] = useState('');
12097
+ const [phoneCodeSent, setPhoneCodeSent] = useState(false);
12098
+ // Account deletion state
12099
+ const [deletePassword, setDeletePassword] = useState('');
12100
+ const [deleteConfirmText, setDeleteConfirmText] = useState('');
12101
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
12102
+ const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, } = customization;
12103
+ // Reinitialize Smartlinks SDK when apiEndpoint changes
11175
12104
  useEffect(() => {
11176
12105
  if (apiEndpoint) {
11177
- // Get current token before reinitializing
11178
- const currentToken = auth.getToken();
11179
12106
  smartlinks.initializeApi({
11180
12107
  baseURL: apiEndpoint,
11181
- proxyMode: false, // Direct API calls when custom endpoint is provided
12108
+ proxyMode: false,
11182
12109
  ngrokSkipBrowserWarning: true,
11183
12110
  });
11184
- // Restore bearer token after reinitialization using auth.verifyToken
11185
- if (currentToken) {
11186
- smartlinks.auth.verifyToken(currentToken).catch(err => {
11187
- console.warn('Failed to restore bearer token after reinit:', err);
11188
- });
11189
- }
11190
12111
  }
11191
- }, [apiEndpoint, auth]);
11192
- // Get the effective redirect URL (use prop or default to current page)
11193
- const getRedirectUrl = () => {
11194
- if (redirectUrl)
11195
- return redirectUrl;
11196
- // Get the full current URL including hash routes
11197
- // Remove any existing query parameters to avoid duplication
11198
- const currentUrl = window.location.href.split('?')[0];
11199
- return currentUrl;
11200
- };
11201
- // Fetch UI configuration
12112
+ }, [apiEndpoint]);
12113
+ // Load user profile on mount
11202
12114
  useEffect(() => {
11203
- if (skipConfigFetch) {
11204
- setConfig(customization || {});
12115
+ loadProfile();
12116
+ }, [clientId]);
12117
+ const loadProfile = async () => {
12118
+ if (!auth.isAuthenticated) {
12119
+ setError('You must be logged in to manage your account');
11205
12120
  return;
11206
12121
  }
11207
- const fetchConfig = async () => {
11208
- try {
11209
- // Check localStorage cache first
11210
- const cacheKey = `auth_ui_config_${clientId || 'default'}`;
11211
- const cached = localStorage.getItem(cacheKey);
11212
- if (cached) {
11213
- const { config: cachedConfig, timestamp } = JSON.parse(cached);
11214
- const age = Date.now() - timestamp;
11215
- // Use cache if less than 1 hour old
11216
- if (age < 3600000) {
11217
- setConfig({ ...cachedConfig, ...customization });
11218
- setConfigLoading(false);
11219
- // Fetch in background to update cache
11220
- api.fetchConfig().then(freshConfig => {
11221
- localStorage.setItem(cacheKey, JSON.stringify({
11222
- config: freshConfig,
11223
- timestamp: Date.now()
11224
- }));
11225
- });
11226
- return;
11227
- }
11228
- }
11229
- // Fetch from API
11230
- const fetchedConfig = await api.fetchConfig();
11231
- // Merge with customization props (props take precedence)
11232
- const mergedConfig = { ...fetchedConfig, ...customization };
11233
- setConfig(mergedConfig);
11234
- // Cache the fetched config
11235
- localStorage.setItem(cacheKey, JSON.stringify({
11236
- config: fetchedConfig,
11237
- timestamp: Date.now()
11238
- }));
11239
- }
11240
- catch (err) {
11241
- console.error('Failed to fetch config:', err);
11242
- setConfig(customization || {});
11243
- }
11244
- finally {
11245
- setConfigLoading(false);
11246
- }
11247
- };
11248
- fetchConfig();
11249
- }, [apiEndpoint, clientId, customization, skipConfigFetch]);
11250
- // Reset showEmailForm when mode changes away from login/register
11251
- useEffect(() => {
11252
- if (mode !== 'login' && mode !== 'register') {
11253
- setShowEmailForm(false);
11254
- }
11255
- }, [mode]);
11256
- // Handle URL-based auth flows (email verification, password reset)
11257
- useEffect(() => {
11258
- // Helper to get URL parameters from either hash or search
11259
- const getUrlParams = () => {
11260
- // First check if there are params in the hash (for hash routing)
11261
- const hash = window.location.hash;
11262
- const hashQueryIndex = hash.indexOf('?');
11263
- if (hashQueryIndex !== -1) {
11264
- // Extract query string from hash (e.g., #/test?mode=verifyEmail&token=abc)
11265
- const hashQuery = hash.substring(hashQueryIndex + 1);
11266
- return new URLSearchParams(hashQuery);
11267
- }
11268
- // Fall back to regular search params (for non-hash routing)
11269
- return new URLSearchParams(window.location.search);
11270
- };
11271
- const params = getUrlParams();
11272
- const urlMode = params.get('mode');
11273
- const token = params.get('token');
11274
- console.log('URL params detected:', { urlMode, token, hash: window.location.hash, search: window.location.search });
11275
- if (urlMode && token) {
11276
- handleURLBasedAuth(urlMode, token);
11277
- }
11278
- }, []);
11279
- const handleURLBasedAuth = async (urlMode, token) => {
11280
- setLoading(true);
11281
- setError(undefined);
11282
- try {
11283
- if (urlMode === 'verifyEmail') {
11284
- console.log('Verifying email with token:', token);
11285
- const response = await api.verifyEmailWithToken(token);
11286
- // Get email verification mode from response or config
11287
- const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
11288
- if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
11289
- // Auto-login modes: Log the user in immediately if token is provided
11290
- auth.login(response.token, response.user, response.accountData);
11291
- setAuthSuccess(true);
11292
- setSuccessMessage('Email verified successfully! You are now logged in.');
11293
- onAuthSuccess(response.token, response.user, response.accountData);
11294
- // Clear the URL parameters
11295
- const cleanUrl = window.location.href.split('?')[0];
11296
- window.history.replaceState({}, document.title, cleanUrl);
11297
- // Redirect after a brief delay to show success message
11298
- if (redirectUrl) {
11299
- setTimeout(() => {
11300
- window.location.href = redirectUrl;
11301
- }, 2000);
11302
- }
11303
- }
11304
- else {
11305
- // verify-then-manual-login mode or no token: Show success but require manual login
11306
- setAuthSuccess(true);
11307
- setSuccessMessage('Email verified successfully! Please log in with your credentials.');
11308
- // Clear the URL parameters
11309
- const cleanUrl = window.location.href.split('?')[0];
11310
- window.history.replaceState({}, document.title, cleanUrl);
11311
- // Switch back to login mode after a delay
11312
- setTimeout(() => {
11313
- setAuthSuccess(false);
11314
- setMode('login');
11315
- }, 3000);
11316
- }
11317
- }
11318
- else if (urlMode === 'resetPassword') {
11319
- console.log('Verifying reset token:', token);
11320
- // Verify token is valid, then show password reset form
11321
- await api.verifyResetToken(token);
11322
- setResetToken(token); // Store token for use in password reset
11323
- setMode('reset-password');
11324
- // Clear the URL parameters
11325
- const cleanUrl = window.location.href.split('?')[0];
11326
- window.history.replaceState({}, document.title, cleanUrl);
11327
- }
11328
- else if (urlMode === 'magicLink') {
11329
- console.log('Verifying magic link token:', token);
11330
- const response = await api.verifyMagicLink(token);
11331
- // Auto-login with magic link if token is provided
11332
- if (response.token) {
11333
- auth.login(response.token, response.user, response.accountData);
11334
- setAuthSuccess(true);
11335
- setSuccessMessage('Magic link verified! You are now logged in.');
11336
- onAuthSuccess(response.token, response.user, response.accountData);
11337
- // Clear the URL parameters
11338
- const cleanUrl = window.location.href.split('?')[0];
11339
- window.history.replaceState({}, document.title, cleanUrl);
11340
- // Redirect after a brief delay to show success message
11341
- if (redirectUrl) {
11342
- setTimeout(() => {
11343
- window.location.href = redirectUrl;
11344
- }, 2000);
11345
- }
11346
- }
11347
- else {
11348
- throw new Error('Authentication failed - no token received');
11349
- }
11350
- }
11351
- }
11352
- catch (err) {
11353
- console.error('URL-based auth error:', err);
11354
- const errorMessage = err instanceof Error ? err.message : 'An error occurred';
11355
- // If it's an email verification error (expired/invalid token), show resend option
11356
- if (urlMode === 'verifyEmail') {
11357
- setError(`${errorMessage} - Please enter your email below to receive a new verification link.`);
11358
- setShowResendVerification(true);
11359
- setMode('login'); // Show the login form UI
11360
- // Clear the URL parameters
11361
- const cleanUrl = window.location.href.split('?')[0];
11362
- window.history.replaceState({}, document.title, cleanUrl);
11363
- }
11364
- else if (urlMode === 'resetPassword') {
11365
- // If password reset token is invalid/expired, show request new reset link option
11366
- setError(`${errorMessage} - Please enter your email below to receive a new password reset link.`);
11367
- setShowRequestNewReset(true);
11368
- setMode('login');
11369
- // Clear the URL parameters
11370
- const cleanUrl = window.location.href.split('?')[0];
11371
- window.history.replaceState({}, document.title, cleanUrl);
11372
- }
11373
- else if (urlMode === 'magicLink') {
11374
- // If magic link is invalid/expired
11375
- setError(`${errorMessage} - Please request a new magic link below.`);
11376
- setMode('magic-link');
11377
- // Clear the URL parameters
11378
- const cleanUrl = window.location.href.split('?')[0];
11379
- window.history.replaceState({}, document.title, cleanUrl);
11380
- }
11381
- else {
11382
- setError(errorMessage);
11383
- }
11384
- onAuthError?.(err instanceof Error ? err : new Error('An error occurred'));
11385
- }
11386
- finally {
11387
- setLoading(false);
11388
- }
11389
- };
11390
- const handleEmailAuth = async (data) => {
11391
12122
  setLoading(true);
11392
12123
  setError(undefined);
11393
- setAuthSuccess(false);
11394
12124
  try {
11395
- const response = mode === 'login'
11396
- ? await api.login(data.email, data.password)
11397
- : await api.register({
11398
- ...data,
11399
- accountData: mode === 'register' ? accountData : undefined,
11400
- redirectUrl: getRedirectUrl(), // Include redirect URL for email verification
11401
- });
11402
- // Get email verification mode from response or config (default: verify-then-auto-login)
11403
- const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
11404
- const gracePeriodHours = config?.emailVerification?.gracePeriodHours || 24;
11405
- if (mode === 'register') {
11406
- // Handle different verification modes
11407
- if (verificationMode === 'immediate' && response.token) {
11408
- // Immediate mode: Log in right away if token is provided
11409
- auth.login(response.token, response.user, response.accountData);
11410
- setAuthSuccess(true);
11411
- const deadline = response.emailVerificationDeadline
11412
- ? new Date(response.emailVerificationDeadline).toLocaleString()
11413
- : `${gracePeriodHours} hours`;
11414
- setSuccessMessage(`Account created! You're logged in now. Please verify your email by ${deadline} to keep your account active.`);
11415
- if (response.token) {
11416
- onAuthSuccess(response.token, response.user, response.accountData);
11417
- }
11418
- if (redirectUrl) {
11419
- setTimeout(() => {
11420
- window.location.href = redirectUrl;
11421
- }, 2000);
11422
- }
11423
- }
11424
- else if (verificationMode === 'verify-then-auto-login') {
11425
- // Verify-then-auto-login mode: Don't log in yet, but will auto-login after email verification
11426
- setAuthSuccess(true);
11427
- setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
11428
- }
11429
- else {
11430
- // verify-then-manual-login mode: Traditional flow
11431
- setAuthSuccess(true);
11432
- setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
11433
- }
11434
- }
11435
- else {
11436
- // Login mode - always log in if token is provided
11437
- if (response.token) {
11438
- // Check for account lock or verification requirements
11439
- if (response.accountLocked) {
11440
- throw new Error('Your account has been locked due to unverified email. Please check your email or request a new verification link.');
11441
- }
11442
- if (response.requiresEmailVerification) {
11443
- throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
11444
- }
11445
- auth.login(response.token, response.user, response.accountData);
11446
- setAuthSuccess(true);
11447
- setSuccessMessage('Login successful!');
11448
- onAuthSuccess(response.token, response.user, response.accountData);
11449
- if (redirectUrl) {
11450
- setTimeout(() => {
11451
- window.location.href = redirectUrl;
11452
- }, 2000);
11453
- }
11454
- }
11455
- else {
11456
- throw new Error('Authentication failed - please verify your email before logging in.');
11457
- }
11458
- }
12125
+ // TODO: Backend implementation required
12126
+ // Endpoint: GET /api/v1/authkit/:clientId/account/profile
12127
+ // SDK method: smartlinks.authKit.getProfile(clientId)
12128
+ // Temporary mock data for UI testing
12129
+ const profileData = {
12130
+ uid: auth.user?.uid || '',
12131
+ email: auth.user?.email,
12132
+ displayName: auth.user?.displayName,
12133
+ phoneNumber: auth.user?.phoneNumber,
12134
+ photoURL: auth.user?.photoURL,
12135
+ emailVerified: true,
12136
+ accountData: auth.accountData || {},
12137
+ };
12138
+ setProfile(profileData);
12139
+ setDisplayName(profileData.displayName || '');
11459
12140
  }
11460
12141
  catch (err) {
11461
- const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
11462
- // Check if error is about email already registered
11463
- if (mode === 'register' && errorMessage.toLowerCase().includes('already') && errorMessage.toLowerCase().includes('email')) {
11464
- setShowResendVerification(true);
11465
- setResendEmail(data.email);
11466
- setError('This email is already registered. If you didn\'t receive the verification email, you can resend it below.');
11467
- }
11468
- else {
11469
- setError(errorMessage);
11470
- }
11471
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12142
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
12143
+ setError(errorMessage);
12144
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
11472
12145
  }
11473
12146
  finally {
11474
12147
  setLoading(false);
11475
12148
  }
11476
12149
  };
11477
- const handleResendVerification = async () => {
11478
- if (!resendEmail)
11479
- return;
12150
+ const handleUpdateProfile = async (e) => {
12151
+ e.preventDefault();
11480
12152
  setLoading(true);
11481
12153
  setError(undefined);
12154
+ setSuccess(undefined);
11482
12155
  try {
11483
- // For resend, we need the userId. If we don't have it, we need to handle this differently
11484
- // The backend should ideally handle this case
11485
- await api.resendVerification('unknown', resendEmail, getRedirectUrl());
11486
- setAuthSuccess(true);
11487
- setSuccessMessage('Verification email sent! Please check your inbox.');
11488
- setShowResendVerification(false);
12156
+ // TODO: Backend implementation required
12157
+ // Endpoint: POST /api/v1/authkit/:clientId/account/update-profile
12158
+ // SDK method: smartlinks.authKit.updateProfile(clientId, updateData)
12159
+ setError('Backend API not yet implemented. See console for required endpoint.');
12160
+ console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-profile');
12161
+ console.log('Update data:', { displayName });
12162
+ // Uncomment when backend is ready:
12163
+ // const updateData: ProfileUpdateData = {
12164
+ // displayName: displayName || undefined,
12165
+ // photoURL: photoURL || undefined,
12166
+ // };
12167
+ // const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
12168
+ // setProfile(updatedProfile);
12169
+ // setSuccess('Profile updated successfully!');
12170
+ // setEditingSection(null);
12171
+ // onProfileUpdated?.(updatedProfile);
11489
12172
  }
11490
12173
  catch (err) {
11491
- const errorMessage = err instanceof Error ? err.message : 'Failed to resend verification email';
12174
+ const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
11492
12175
  setError(errorMessage);
11493
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12176
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
11494
12177
  }
11495
12178
  finally {
11496
12179
  setLoading(false);
11497
12180
  }
11498
12181
  };
11499
- const handleRequestNewReset = async () => {
11500
- if (!resetRequestEmail)
11501
- return;
12182
+ const cancelEdit = () => {
12183
+ setEditingSection(null);
12184
+ setDisplayName(profile?.displayName || '');
12185
+ setNewEmail('');
12186
+ setEmailPassword('');
12187
+ setCurrentPassword('');
12188
+ setNewPassword('');
12189
+ setConfirmPassword('');
12190
+ setNewPhone('');
12191
+ setPhoneCode('');
12192
+ setPhoneCodeSent(false);
12193
+ setError(undefined);
12194
+ setSuccess(undefined);
12195
+ };
12196
+ const handleChangeEmail = async (e) => {
12197
+ e.preventDefault();
11502
12198
  setLoading(true);
11503
12199
  setError(undefined);
12200
+ setSuccess(undefined);
11504
12201
  try {
11505
- await api.requestPasswordReset(resetRequestEmail, getRedirectUrl());
11506
- setAuthSuccess(true);
11507
- setSuccessMessage('Password reset email sent! Please check your inbox.');
11508
- setShowRequestNewReset(false);
11509
- setResetRequestEmail('');
12202
+ // TODO: Backend implementation required
12203
+ // Endpoint: POST /api/v1/authkit/:clientId/account/change-email
12204
+ // SDK method: smartlinks.authKit.changeEmail(clientId, newEmail, password)
12205
+ // Note: No verification flow for now - direct email update
12206
+ setError('Backend API not yet implemented. See console for required endpoint.');
12207
+ console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-email');
12208
+ console.log('Data:', { newEmail });
12209
+ // Uncomment when backend is ready:
12210
+ // await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
12211
+ // setSuccess('Email changed successfully!');
12212
+ // setEditingSection(null);
12213
+ // setNewEmail('');
12214
+ // setEmailPassword('');
12215
+ // onEmailChangeRequested?.();
12216
+ // await loadProfile(); // Reload to show new email
11510
12217
  }
11511
12218
  catch (err) {
11512
- const errorMessage = err instanceof Error ? err.message : 'Failed to send password reset email';
12219
+ const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
11513
12220
  setError(errorMessage);
11514
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12221
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
11515
12222
  }
11516
12223
  finally {
11517
12224
  setLoading(false);
11518
12225
  }
11519
12226
  };
11520
- const handleGoogleLogin = async () => {
11521
- // Use custom client ID from config, or fall back to default Smartlinks client ID
11522
- const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
11523
- // Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
11524
- const oauthFlow = config?.googleOAuthFlow || 'oneTap';
12227
+ const handleChangePassword = async (e) => {
12228
+ e.preventDefault();
12229
+ if (newPassword !== confirmPassword) {
12230
+ setError('New passwords do not match');
12231
+ return;
12232
+ }
12233
+ if (newPassword.length < 6) {
12234
+ setError('Password must be at least 6 characters');
12235
+ return;
12236
+ }
11525
12237
  setLoading(true);
11526
12238
  setError(undefined);
12239
+ setSuccess(undefined);
11527
12240
  try {
11528
- const google = window.google;
11529
- if (!google) {
11530
- throw new Error('Google Identity Services not loaded. Please check your internet connection.');
11531
- }
11532
- if (oauthFlow === 'popup') {
11533
- // Use OAuth2 popup flow (works in iframes but requires popup permission)
11534
- if (!google.accounts.oauth2) {
11535
- throw new Error('Google OAuth2 not available');
11536
- }
11537
- const client = google.accounts.oauth2.initTokenClient({
11538
- client_id: googleClientId,
11539
- scope: 'openid email profile',
11540
- callback: async (response) => {
11541
- try {
11542
- if (response.error) {
11543
- throw new Error(response.error_description || response.error);
11544
- }
11545
- const accessToken = response.access_token;
11546
- // Send access token to backend
11547
- const authResponse = await api.loginWithGoogle(accessToken);
11548
- if (authResponse.token) {
11549
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
11550
- setAuthSuccess(true);
11551
- setSuccessMessage('Google login successful!');
11552
- onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
11553
- }
11554
- else {
11555
- throw new Error('Authentication failed - no token received');
11556
- }
11557
- if (redirectUrl) {
11558
- setTimeout(() => {
11559
- window.location.href = redirectUrl;
11560
- }, 2000);
11561
- }
11562
- setLoading(false);
11563
- }
11564
- catch (err) {
11565
- const errorMessage = err instanceof Error ? err.message : 'Google login failed';
11566
- setError(errorMessage);
11567
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11568
- setLoading(false);
11569
- }
11570
- },
11571
- });
11572
- client.requestAccessToken();
11573
- }
11574
- else {
11575
- // Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
11576
- google.accounts.id.initialize({
11577
- client_id: googleClientId,
11578
- callback: async (response) => {
11579
- try {
11580
- const idToken = response.credential;
11581
- const authResponse = await api.loginWithGoogle(idToken);
11582
- if (authResponse.token) {
11583
- auth.login(authResponse.token, authResponse.user, authResponse.accountData);
11584
- setAuthSuccess(true);
11585
- setSuccessMessage('Google login successful!');
11586
- onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
11587
- }
11588
- else {
11589
- throw new Error('Authentication failed - no token received');
11590
- }
11591
- if (redirectUrl) {
11592
- setTimeout(() => {
11593
- window.location.href = redirectUrl;
11594
- }, 2000);
11595
- }
11596
- setLoading(false);
11597
- }
11598
- catch (err) {
11599
- const errorMessage = err instanceof Error ? err.message : 'Google login failed';
11600
- setError(errorMessage);
11601
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
11602
- setLoading(false);
11603
- }
11604
- },
11605
- auto_select: false,
11606
- cancel_on_tap_outside: true,
11607
- });
11608
- google.accounts.id.prompt((notification) => {
11609
- if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
11610
- setLoading(false);
11611
- }
11612
- });
11613
- }
12241
+ // TODO: Backend implementation required
12242
+ // Endpoint: POST /api/v1/authkit/:clientId/account/change-password
12243
+ // SDK method: smartlinks.authKit.changePassword(clientId, currentPassword, newPassword)
12244
+ setError('Backend API not yet implemented. See console for required endpoint.');
12245
+ console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-password');
12246
+ console.log('Data: currentPassword and newPassword provided');
12247
+ // Uncomment when backend is ready:
12248
+ // await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
12249
+ // setSuccess('Password changed successfully!');
12250
+ // setEditingSection(null);
12251
+ // setCurrentPassword('');
12252
+ // setNewPassword('');
12253
+ // setConfirmPassword('');
12254
+ // onPasswordChanged?.();
11614
12255
  }
11615
12256
  catch (err) {
11616
- const errorMessage = err instanceof Error ? err.message : 'Google login failed';
12257
+ const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
11617
12258
  setError(errorMessage);
11618
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12259
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
12260
+ }
12261
+ finally {
11619
12262
  setLoading(false);
11620
12263
  }
11621
12264
  };
11622
- const handlePhoneAuth = async (phoneNumber, verificationCode) => {
12265
+ const handleSendPhoneCode = async () => {
11623
12266
  setLoading(true);
11624
12267
  setError(undefined);
11625
12268
  try {
11626
- if (!verificationCode) {
11627
- // Send verification code via Twilio Verify Service
11628
- await api.sendPhoneCode(phoneNumber);
11629
- // Twilio Verify Service tracks the verification by phone number
11630
- // No need to store verificationId
11631
- }
11632
- else {
11633
- // Verify code - Twilio identifies the verification by phone number
11634
- const response = await api.verifyPhoneCode(phoneNumber, verificationCode);
11635
- // Update auth context with account data if token is provided
11636
- if (response.token) {
11637
- auth.login(response.token, response.user, response.accountData);
11638
- onAuthSuccess(response.token, response.user, response.accountData);
11639
- if (redirectUrl) {
11640
- window.location.href = redirectUrl;
11641
- }
11642
- }
11643
- else {
11644
- throw new Error('Authentication failed - no token received');
11645
- }
11646
- }
12269
+ await smartlinks.authKit.sendPhoneCode(clientId, newPhone);
12270
+ setPhoneCodeSent(true);
12271
+ setSuccess('Verification code sent to your phone');
12272
+ }
12273
+ catch (err) {
12274
+ const errorMessage = err instanceof Error ? err.message : 'Failed to send verification code';
12275
+ setError(errorMessage);
12276
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
12277
+ }
12278
+ finally {
12279
+ setLoading(false);
12280
+ }
12281
+ };
12282
+ const handleUpdatePhone = async (e) => {
12283
+ e.preventDefault();
12284
+ setLoading(true);
12285
+ setError(undefined);
12286
+ setSuccess(undefined);
12287
+ try {
12288
+ // TODO: Backend implementation required
12289
+ // Endpoint: POST /api/v1/authkit/:clientId/account/update-phone
12290
+ // SDK method: smartlinks.authKit.updatePhone(clientId, phoneNumber, verificationCode)
12291
+ setError('Backend API not yet implemented. See console for required endpoint.');
12292
+ console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-phone');
12293
+ console.log('Data:', { phoneNumber: newPhone, code: phoneCode });
12294
+ // Uncomment when backend is ready:
12295
+ // await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
12296
+ // setSuccess('Phone number updated successfully!');
12297
+ // setEditingSection(null);
12298
+ // setNewPhone('');
12299
+ // setPhoneCode('');
12300
+ // setPhoneCodeSent(false);
12301
+ // await loadProfile();
11647
12302
  }
11648
12303
  catch (err) {
11649
- const errorMessage = err instanceof Error ? err.message : 'Phone authentication failed';
12304
+ const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
11650
12305
  setError(errorMessage);
11651
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12306
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
11652
12307
  }
11653
12308
  finally {
11654
12309
  setLoading(false);
11655
12310
  }
11656
12311
  };
11657
- const handlePasswordReset = async (emailOrPassword, confirmPassword) => {
12312
+ const handleDeleteAccount = async () => {
12313
+ if (deleteConfirmText !== 'DELETE') {
12314
+ setError('Please type DELETE to confirm account deletion');
12315
+ return;
12316
+ }
12317
+ if (!deletePassword) {
12318
+ setError('Password is required');
12319
+ return;
12320
+ }
11658
12321
  setLoading(true);
11659
12322
  setError(undefined);
11660
12323
  try {
11661
- if (resetToken && confirmPassword) {
11662
- // Complete password reset with token
11663
- await api.completePasswordReset(resetToken, emailOrPassword);
11664
- setResetSuccess(true);
11665
- setResetToken(undefined); // Clear token after successful reset
11666
- }
11667
- else {
11668
- // Request password reset email
11669
- await api.requestPasswordReset(emailOrPassword, getRedirectUrl());
11670
- setResetSuccess(true);
11671
- }
12324
+ // TODO: Backend implementation required
12325
+ // Endpoint: DELETE /api/v1/authkit/:clientId/account/delete
12326
+ // SDK method: smartlinks.authKit.deleteAccount(clientId, password, confirmText)
12327
+ // Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
12328
+ setError('Backend API not yet implemented. See console for required endpoint.');
12329
+ console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
12330
+ console.log('Data: password and confirmText="DELETE" provided');
12331
+ console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
12332
+ // Uncomment when backend is ready:
12333
+ // await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
12334
+ // setSuccess('Account deleted successfully');
12335
+ // onAccountDeleted?.();
12336
+ // await auth.logout();
11672
12337
  }
11673
12338
  catch (err) {
11674
- const errorMessage = err instanceof Error ? err.message : 'Password reset failed';
12339
+ const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
11675
12340
  setError(errorMessage);
11676
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12341
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
11677
12342
  }
11678
12343
  finally {
11679
12344
  setLoading(false);
11680
12345
  }
11681
12346
  };
11682
- const handleMagicLink = async (email) => {
12347
+ if (!auth.isAuthenticated) {
12348
+ return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Please log in to manage your account" }) }));
12349
+ }
12350
+ if (loading && !profile) {
12351
+ return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
12352
+ }
12353
+ return (jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Display Name" }), jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsx("div", { className: "form-group", children: jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Email Address" }), jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Password" }), jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Phone Number" }), jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxs("section", { className: "account-section danger-zone", children: [jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxs("div", { className: "delete-confirm", children: [jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsx("button", { type: "button", onClick: () => {
12354
+ setShowDeleteConfirm(false);
12355
+ setDeletePassword('');
12356
+ setDeleteConfirmText('');
12357
+ }, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
12358
+ };
12359
+
12360
+ const SmartlinksClaimUI = ({ apiEndpoint, clientId, clientName, collectionId, productId, proofId, onClaimSuccess, onClaimError, additionalFields = [], theme = 'light', className = '', minimal = false, customization = {}, }) => {
12361
+ const auth = useAuth();
12362
+ const [claimStep, setClaimStep] = useState(auth.isAuthenticated ? 'questions' : 'auth');
12363
+ const [claimData, setClaimData] = useState({});
12364
+ const [error, setError] = useState();
12365
+ const [loading, setLoading] = useState(false);
12366
+ const handleAuthSuccess = (token, user, accountData) => {
12367
+ // Authentication successful
12368
+ auth.login(token, user, accountData);
12369
+ // If no additional questions, proceed directly to claim
12370
+ if (additionalFields.length === 0) {
12371
+ executeClaim(user);
12372
+ }
12373
+ else {
12374
+ setClaimStep('questions');
12375
+ }
12376
+ };
12377
+ const handleQuestionSubmit = async (e) => {
12378
+ e.preventDefault();
12379
+ // Validate required fields
12380
+ const missingFields = additionalFields
12381
+ .filter(field => field.required && !claimData[field.name])
12382
+ .map(field => field.label);
12383
+ if (missingFields.length > 0) {
12384
+ setError(`Please fill in: ${missingFields.join(', ')}`);
12385
+ return;
12386
+ }
12387
+ // Execute claim with collected data
12388
+ if (auth.user) {
12389
+ executeClaim(auth.user);
12390
+ }
12391
+ };
12392
+ const executeClaim = async (user) => {
12393
+ setClaimStep('claiming');
11683
12394
  setLoading(true);
11684
12395
  setError(undefined);
11685
12396
  try {
11686
- await api.sendMagicLink(email, getRedirectUrl());
11687
- setAuthSuccess(true);
11688
- setSuccessMessage('Magic link sent! Check your email to log in.');
12397
+ // Create attestation to claim the proof
12398
+ const response = await smartlinks.attestation.create(collectionId, productId, proofId, {
12399
+ public: {
12400
+ claimed: true,
12401
+ claimedAt: new Date().toISOString(),
12402
+ claimedBy: user.uid,
12403
+ ...claimData,
12404
+ },
12405
+ private: {},
12406
+ proof: {},
12407
+ });
12408
+ setClaimStep('success');
12409
+ // Call success callback
12410
+ onClaimSuccess({
12411
+ proofId,
12412
+ user,
12413
+ claimData,
12414
+ attestationId: response.id,
12415
+ });
11689
12416
  }
11690
12417
  catch (err) {
11691
- const errorMessage = err instanceof Error ? err.message : 'Failed to send magic link';
12418
+ console.error('Claim error:', err);
12419
+ const errorMessage = err instanceof Error ? err.message : 'Failed to claim proof';
11692
12420
  setError(errorMessage);
11693
- onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12421
+ onClaimError?.(err instanceof Error ? err : new Error(errorMessage));
12422
+ setClaimStep(additionalFields.length > 0 ? 'questions' : 'auth');
11694
12423
  }
11695
12424
  finally {
11696
12425
  setLoading(false);
11697
12426
  }
11698
12427
  };
11699
- if (configLoading) {
11700
- return (jsx(AuthContainer, { theme: theme, className: className, children: jsx("div", { style: { textAlign: 'center', padding: '2rem' }, children: jsx("div", { className: "auth-spinner" }) }) }));
12428
+ const handleFieldChange = (fieldName, value) => {
12429
+ setClaimData(prev => ({
12430
+ ...prev,
12431
+ [fieldName]: value,
12432
+ }));
12433
+ };
12434
+ // Render authentication step
12435
+ if (claimStep === 'auth') {
12436
+ return (jsx("div", { className: className, children: jsx(SmartlinksAuthUI, { apiEndpoint: apiEndpoint, clientId: clientId, clientName: clientName, onAuthSuccess: handleAuthSuccess, onAuthError: onClaimError, theme: theme, minimal: minimal, customization: customization.authConfig }) }));
11701
12437
  }
11702
- return (jsx(AuthContainer, { theme: theme, className: className, config: config, children: authSuccess ? (jsxs("div", { style: { textAlign: 'center', padding: '2rem' }, children: [jsx("div", { style: {
11703
- color: 'var(--auth-primary-color, #4F46E5)',
11704
- fontSize: '3rem',
11705
- marginBottom: '1rem'
11706
- }, children: "\u2713" }), jsx("h2", { style: {
11707
- marginBottom: '0.5rem',
11708
- fontSize: '1.5rem',
11709
- fontWeight: 600
11710
- }, children: successMessage?.includes('verified') ? 'Email Verified!' :
11711
- successMessage?.includes('Magic link') ? 'Check Your Email!' :
11712
- mode === 'register' ? 'Account Created!' : 'Login Successful!' }), jsx("p", { style: {
11713
- color: '#6B7280',
11714
- fontSize: '0.875rem'
11715
- }, children: successMessage })] })) : mode === 'magic-link' ? (jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
11716
- setMode('login');
11717
- setResetSuccess(false);
11718
- setResetToken(undefined); // Clear token when going back
11719
- }, loading: loading, error: error, success: resetSuccess, token: resetToken })) : (mode === 'login' || mode === 'register') ? (jsx(Fragment, { children: showResendVerification ? (jsxs("div", { style: { marginTop: '1rem', padding: '1.5rem', backgroundColor: 'rgba(79, 70, 229, 0.05)', borderRadius: '0.5rem' }, children: [jsx("h3", { style: { marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600, color: 'var(--auth-text-color, #374151)' }, children: "Verification Link Expired" }), jsx("p", { style: { marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--auth-text-color, #6B7280)', lineHeight: '1.5' }, children: "Your verification link has expired or is no longer valid. Please enter your email address below and we'll send you a new verification link." }), jsx("input", { type: "email", value: resendEmail || '', onChange: (e) => setResendEmail(e.target.value), placeholder: "your@email.com", style: {
11720
- width: '100%',
11721
- padding: '0.625rem',
11722
- marginBottom: '1rem',
11723
- border: '1px solid var(--auth-border-color, #D1D5DB)',
11724
- borderRadius: '0.375rem',
11725
- fontSize: '0.875rem',
11726
- boxSizing: 'border-box'
11727
- } }), jsxs("div", { style: { display: 'flex', gap: '0.75rem' }, children: [jsx("button", { onClick: handleResendVerification, disabled: loading || !resendEmail, style: {
11728
- flex: 1,
11729
- padding: '0.625rem 1rem',
11730
- backgroundColor: 'var(--auth-primary-color, #4F46E5)',
11731
- color: 'white',
11732
- border: 'none',
11733
- borderRadius: '0.375rem',
11734
- cursor: (loading || !resendEmail) ? 'not-allowed' : 'pointer',
11735
- fontSize: '0.875rem',
11736
- fontWeight: 500,
11737
- opacity: (loading || !resendEmail) ? 0.6 : 1
11738
- }, children: loading ? 'Sending...' : 'Send New Verification Link' }), jsx("button", { onClick: () => {
11739
- setShowResendVerification(false);
11740
- setResendEmail('');
11741
- setError(undefined);
11742
- }, disabled: loading, style: {
11743
- padding: '0.625rem 1rem',
11744
- backgroundColor: 'transparent',
11745
- color: 'var(--auth-text-color, #6B7280)',
11746
- border: '1px solid var(--auth-border-color, #D1D5DB)',
11747
- borderRadius: '0.375rem',
11748
- cursor: loading ? 'not-allowed' : 'pointer',
11749
- fontSize: '0.875rem',
11750
- fontWeight: 500,
11751
- opacity: loading ? 0.6 : 1
11752
- }, children: "Cancel" })] })] })) : showRequestNewReset ? (jsxs("div", { style: { marginTop: '1rem', padding: '1.5rem', backgroundColor: 'rgba(239, 68, 68, 0.05)', borderRadius: '0.5rem' }, children: [jsx("h3", { style: { marginBottom: '0.75rem', fontSize: '1rem', fontWeight: 600, color: 'var(--auth-text-color, #374151)' }, children: "Password Reset Link Expired" }), jsx("p", { style: { marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--auth-text-color, #6B7280)', lineHeight: '1.5' }, children: "Your password reset link has expired or is no longer valid. Please enter your email address below and we'll send you a new password reset link." }), jsx("input", { type: "email", value: resetRequestEmail || '', onChange: (e) => setResetRequestEmail(e.target.value), placeholder: "your@email.com", style: {
11753
- width: '100%',
11754
- padding: '0.625rem',
11755
- marginBottom: '1rem',
11756
- border: '1px solid var(--auth-border-color, #D1D5DB)',
11757
- borderRadius: '0.375rem',
11758
- fontSize: '0.875rem',
11759
- boxSizing: 'border-box'
11760
- } }), jsxs("div", { style: { display: 'flex', gap: '0.75rem' }, children: [jsx("button", { onClick: handleRequestNewReset, disabled: loading || !resetRequestEmail, style: {
11761
- flex: 1,
11762
- padding: '0.625rem 1rem',
11763
- backgroundColor: '#EF4444',
11764
- color: 'white',
11765
- border: 'none',
11766
- borderRadius: '0.375rem',
11767
- cursor: (loading || !resetRequestEmail) ? 'not-allowed' : 'pointer',
11768
- fontSize: '0.875rem',
11769
- fontWeight: 500,
11770
- opacity: (loading || !resetRequestEmail) ? 0.6 : 1
11771
- }, children: loading ? 'Sending...' : 'Send New Reset Link' }), jsx("button", { onClick: () => {
11772
- setShowRequestNewReset(false);
11773
- setResetRequestEmail('');
11774
- setError(undefined);
11775
- }, disabled: loading, style: {
11776
- padding: '0.625rem 1rem',
11777
- backgroundColor: 'transparent',
11778
- color: 'var(--auth-text-color, #6B7280)',
11779
- border: '1px solid var(--auth-border-color, #D1D5DB)',
11780
- borderRadius: '0.375rem',
11781
- cursor: loading ? 'not-allowed' : 'pointer',
11782
- fontSize: '0.875rem',
11783
- fontWeight: 500,
11784
- opacity: loading ? 0.6 : 1
11785
- }, children: "Cancel" })] })] })) : (jsx(Fragment, { children: (() => {
11786
- const emailDisplayMode = config?.emailDisplayMode || 'form';
11787
- const providerOrder = config?.providerOrder || (config?.enabledProviders || enabledProviders);
11788
- const actualProviders = config?.enabledProviders || enabledProviders;
11789
- // Button mode: show provider selection first, then email form if email is selected
11790
- if (emailDisplayMode === 'button' && !showEmailForm) {
11791
- return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
11792
- }
11793
- // Form mode or email button was clicked: show email form with other providers
11794
- return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
11795
- marginBottom: '1rem',
11796
- padding: '0.5rem',
11797
- background: 'none',
11798
- border: 'none',
11799
- color: 'var(--auth-text-color, #6B7280)',
11800
- cursor: 'pointer',
11801
- fontSize: '0.875rem',
11802
- display: 'flex',
11803
- alignItems: 'center',
11804
- gap: '0.25rem'
11805
- }, children: "\u2190 Back to options" })), jsx(EmailAuthForm, { mode: mode, onSubmit: handleEmailAuth, onModeSwitch: () => {
11806
- setMode(mode === 'login' ? 'register' : 'login');
11807
- setShowResendVerification(false);
11808
- setShowRequestNewReset(false);
11809
- setError(undefined);
11810
- }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
11811
- })() })) })) : null }));
12438
+ // Render additional questions step
12439
+ if (claimStep === 'questions') {
12440
+ return (jsxs("div", { className: `claim-questions ${className}`, children: [jsxs("div", { className: "claim-header mb-6", children: [jsx("h2", { className: "text-2xl font-bold mb-2", children: customization.claimTitle || 'Complete Your Claim' }), customization.claimDescription && (jsx("p", { className: "text-muted-foreground", children: customization.claimDescription }))] }), error && (jsx("div", { className: "claim-error bg-destructive/10 text-destructive px-4 py-3 rounded-md mb-4", children: error })), jsxs("form", { onSubmit: handleQuestionSubmit, className: "claim-form space-y-4", children: [additionalFields.map((field) => (jsxs("div", { className: "claim-field", children: [jsxs("label", { htmlFor: field.name, className: "block text-sm font-medium mb-2", children: [field.label, field.required && jsx("span", { className: "text-destructive ml-1", children: "*" })] }), field.type === 'textarea' ? (jsx("textarea", { id: field.name, name: field.name, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", rows: 4 })) : field.type === 'select' ? (jsxs("select", { id: field.name, name: field.name, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", children: [jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsx("option", { value: option, children: option }, option)))] })) : (jsx("input", { id: field.name, name: field.name, type: field.type, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background" }))] }, field.name))), jsx("button", { type: "submit", disabled: loading, className: "claim-submit-button w-full bg-primary text-primary-foreground px-4 py-2 rounded-md font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed", children: loading ? 'Claiming...' : 'Submit Claim' })] })] }));
12441
+ }
12442
+ // Render claiming step (loading state)
12443
+ if (claimStep === 'claiming') {
12444
+ return (jsxs("div", { className: `claim-loading ${className} flex flex-col items-center justify-center py-12`, children: [jsx("div", { className: "claim-spinner w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mb-4" }), jsx("p", { className: "text-muted-foreground", children: "Claiming your product..." })] }));
12445
+ }
12446
+ // Render success step
12447
+ if (claimStep === 'success') {
12448
+ return (jsxs("div", { className: `claim-success ${className} text-center py-12`, children: [jsx("div", { className: "claim-success-icon w-16 h-16 bg-green-500 text-white rounded-full flex items-center justify-center text-3xl font-bold mx-auto mb-4", children: "\u2713" }), jsx("h2", { className: "text-2xl font-bold mb-2", children: "Claim Successful!" }), jsx("p", { className: "text-muted-foreground", children: customization.successMessage || 'Your product has been successfully claimed and registered to your account.' })] }));
12449
+ }
12450
+ return null;
12451
+ };
12452
+
12453
+ const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
12454
+ const { isAuthenticated, isLoading } = useAuth();
12455
+ // Show loading state
12456
+ if (isLoading) {
12457
+ return jsx("div", { children: "Loading..." });
12458
+ }
12459
+ // If not authenticated, redirect or show fallback
12460
+ if (!isAuthenticated) {
12461
+ if (redirectTo) {
12462
+ window.location.href = redirectTo;
12463
+ return null;
12464
+ }
12465
+ return fallback ? jsx(Fragment, { children: fallback }) : jsx("div", { children: "Access denied. Please log in." });
12466
+ }
12467
+ // Render protected content
12468
+ return jsx(Fragment, { children: children });
12469
+ };
12470
+
12471
+ const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', 'phone'], providerOrder, emailDisplayMode = 'form', theme = 'light', className, minimal = false, }) => {
12472
+ const showEmail = enabledProviders.includes('email');
12473
+ const showGoogle = enabledProviders.includes('google');
12474
+ const showPhone = enabledProviders.includes('phone');
12475
+ const showMagicLink = enabledProviders.includes('magic-link');
12476
+ // Determine ordered providers (excluding email if in button mode)
12477
+ const orderedProviders = providerOrder && providerOrder.length > 0
12478
+ ? providerOrder.filter(p => enabledProviders.includes(p) && p !== 'email')
12479
+ : enabledProviders.filter(p => p !== 'email');
12480
+ const hasOtherProviders = showGoogle || showPhone || showMagicLink;
12481
+ // Render provider button helper
12482
+ const renderProviderButton = (provider) => {
12483
+ if (provider === 'google' && showGoogle) {
12484
+ return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", children: [jsx("path", { d: "M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z", fill: "#4285F4" }), jsx("path", { d: "M9 18c2.43 0 4.467-.806 5.956-2.183l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z", fill: "#34A853" }), jsx("path", { d: "M3.964 10.707c-.18-.54-.282-1.117-.282-1.707 0-.593.102-1.167.282-1.707V4.961H.957C.347 6.175 0 7.548 0 9s.348 2.825.957 4.039l3.007-2.332z", fill: "#FBBC05" }), jsx("path", { d: "M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z", fill: "#EA4335" })] }), jsx("span", { children: "Continue with Google" })] }, "google"));
12485
+ }
12486
+ if (provider === 'phone' && showPhone) {
12487
+ return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" }) }), jsx("span", { children: "Continue with Phone" })] }, "phone"));
12488
+ }
12489
+ if (provider === 'magic-link' && showMagicLink) {
12490
+ return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Magic Link" })] }, "magic-link"));
12491
+ }
12492
+ if (provider === 'email' && showEmail && emailDisplayMode === 'button') {
12493
+ return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Email" })] }, "email"));
12494
+ }
12495
+ return null;
12496
+ };
12497
+ return (jsx(AuthContainer, { theme: theme, className: className, config: customization, minimal: minimal, children: emailDisplayMode === 'button' ? (jsx("div", { className: "auth-provider-buttons", children: orderedProviders.concat(showEmail ? ['email'] : []).map(provider => renderProviderButton(provider)) })) : (
12498
+ /* Form mode: show email form first, then other providers */
12499
+ jsxs(Fragment, { children: [showEmail && (jsxs("div", { className: "auth-form", children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Email" }), jsx("input", { type: "email", className: "auth-input", placeholder: "Enter your email", disabled: true })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Password" }), jsx("input", { type: "password", className: "auth-input", placeholder: "Enter your password", disabled: true })] }), jsx("button", { className: "auth-button auth-button-primary", disabled: true, children: "Sign In" }), jsx("div", { style: { textAlign: 'center', marginTop: '1rem' }, children: jsx("button", { className: "auth-link", disabled: true, children: "Forgot password?" }) }), jsxs("div", { style: { textAlign: 'center', marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--auth-text-muted, #6B7280)' }, children: ["Don't have an account?", ' ', jsx("button", { className: "auth-link", disabled: true, children: "Sign up" })] })] })), hasOtherProviders && (jsxs(Fragment, { children: [showEmail && (jsx("div", { className: "auth-or-divider", children: jsx("span", { children: "or continue with" }) })), jsx("div", { className: "auth-provider-buttons", children: orderedProviders.map(provider => renderProviderButton(provider)) })] }))] })) }));
11812
12500
  };
11813
12501
 
11814
- export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SmartlinksAuthUI, tokenStorage, useAuth };
12502
+ export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SmartlinksAuthUI, SmartlinksClaimUI, tokenStorage, useAuth };
11815
12503
  //# sourceMappingURL=index.esm.js.map