@proveanything/smartlinks-auth-ui 0.1.25 → 0.1.28

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
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import React, { useEffect, useState, useRef, useCallback, useMemo, createContext, useContext } from 'react';
2
+ import React, { useEffect, useState, useMemo, useRef, useCallback, createContext, useContext } from 'react';
3
3
  import * as smartlinks from '@proveanything/smartlinks';
4
4
  import { iframe } from '@proveanything/smartlinks';
5
5
 
@@ -50,29 +50,153 @@ const AuthContainer = ({ children, theme = 'light', className = '', config, mini
50
50
  const logoUrl = config?.branding?.logoUrl === undefined
51
51
  ? 'https://smartlinks.app/smartlinks/landscape-medium.png' // Default Smartlinks logo
52
52
  : config?.branding?.logoUrl || null; // Custom or explicitly hidden
53
+ const inheritHostStyles = config?.branding?.inheritHostStyles ? 'auth-inherit-host' : '';
53
54
  const containerClass = minimal
54
- ? `auth-minimal auth-theme-${theme} ${className}`
55
- : `auth-container auth-theme-${theme} ${className}`;
55
+ ? `auth-minimal auth-theme-${theme} ${inheritHostStyles} ${className}`
56
+ : `auth-container auth-theme-${theme} ${inheritHostStyles} ${className}`;
56
57
  const cardClass = minimal ? 'auth-minimal-card' : 'auth-card';
57
58
  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" })] }))] }) }));
58
59
  };
59
60
 
60
- const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, additionalFields = [], }) => {
61
+ /**
62
+ * Renders a form field based on schema definition
63
+ * Used in both registration and account management forms
64
+ */
65
+ const SchemaFieldRenderer = ({ field, value, onChange, disabled = false, error, className = '', }) => {
66
+ const handleChange = (newValue) => {
67
+ onChange(field.key, newValue);
68
+ };
69
+ const inputId = `field-${field.key}`;
70
+ const commonProps = {
71
+ id: inputId,
72
+ className: `auth-input ${error ? 'auth-input-error' : ''} ${className}`,
73
+ disabled: disabled || field.readOnly,
74
+ 'aria-describedby': error ? `${inputId}-error` : undefined,
75
+ };
76
+ const renderField = () => {
77
+ switch (field.widget) {
78
+ case 'select':
79
+ return (jsxs("select", { ...commonProps, value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, children: [jsxs("option", { value: "", children: ["Select ", field.label, "..."] }), field.options?.map((option) => {
80
+ const optionValue = typeof option === 'string' ? option : option.value;
81
+ const optionLabel = typeof option === 'string' ? option : option.label;
82
+ return (jsx("option", { value: optionValue, children: optionLabel }, optionValue));
83
+ })] }));
84
+ case 'checkbox':
85
+ return (jsxs("label", { className: "auth-checkbox-label", style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [jsx("input", { type: "checkbox", id: inputId, checked: !!value, onChange: (e) => handleChange(e.target.checked), disabled: disabled || field.readOnly, required: field.required, className: "auth-checkbox" }), jsx("span", { children: field.description || field.label })] }));
86
+ case 'textarea':
87
+ return (jsx("textarea", { ...commonProps, value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, placeholder: field.placeholder, rows: 3, style: { minHeight: '80px', resize: 'vertical' }, maxLength: field.validation?.maxLength }));
88
+ case 'number':
89
+ return (jsx("input", { ...commonProps, type: "number", value: value ?? '', onChange: (e) => handleChange(e.target.value ? Number(e.target.value) : undefined), required: field.required, placeholder: field.placeholder, min: field.validation?.min, max: field.validation?.max }));
90
+ case 'date':
91
+ return (jsx("input", { ...commonProps, type: "date", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required }));
92
+ case 'tel':
93
+ return (jsx("input", { ...commonProps, type: "tel", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, placeholder: field.placeholder || '+1 (555) 000-0000', autoComplete: "tel" }));
94
+ case 'email':
95
+ return (jsx("input", { ...commonProps, type: "email", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, placeholder: field.placeholder || 'email@example.com', autoComplete: "email" }));
96
+ case 'text':
97
+ default:
98
+ return (jsx("input", { ...commonProps, type: "text", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, placeholder: field.placeholder, minLength: field.validation?.minLength, maxLength: field.validation?.maxLength, pattern: field.validation?.pattern }));
99
+ }
100
+ };
101
+ // Checkbox has its own label
102
+ if (field.widget === 'checkbox') {
103
+ return (jsxs("div", { className: "auth-form-group", children: [renderField(), error && (jsx("div", { id: `${inputId}-error`, className: "auth-field-error", role: "alert", children: error }))] }));
104
+ }
105
+ return (jsxs("div", { className: "auth-form-group", children: [jsxs("label", { htmlFor: inputId, className: "auth-label", children: [field.label, field.required && jsx("span", { style: { color: 'var(--auth-error-color, #ef4444)' }, children: " *" })] }), field.description && (jsx("p", { className: "auth-field-description", style: { fontSize: '0.85em', color: 'var(--auth-text-muted)', marginBottom: '4px' }, children: field.description })), renderField(), error && (jsx("div", { id: `${inputId}-error`, className: "auth-field-error", role: "alert", children: error }))] }));
106
+ };
107
+ /**
108
+ * Helper to get all editable fields from schema
109
+ */
110
+ const getEditableFields = (schema) => {
111
+ if (!schema)
112
+ return [];
113
+ const editableKeys = new Set(schema.settings.publicEditableFields);
114
+ const coreEditable = schema.fields.filter(f => f.editable && f.visible && editableKeys.has(f.key));
115
+ const customEditable = schema.customFields.filter(f => f.editable && f.visible);
116
+ return [...coreEditable, ...customEditable];
117
+ };
118
+ /**
119
+ * Helper to get registration fields based on config
120
+ */
121
+ const getRegistrationFields = (schema, registrationConfig) => {
122
+ if (!schema || !registrationConfig.length)
123
+ return [];
124
+ const configMap = new Map(registrationConfig.map(c => [c.key, c]));
125
+ const allFields = [...schema.fields, ...schema.customFields];
126
+ return allFields
127
+ .filter(field => {
128
+ const config = configMap.get(field.key);
129
+ return config?.showDuringRegistration && field.visible;
130
+ })
131
+ .map(field => {
132
+ const config = configMap.get(field.key);
133
+ // Allow registration config to override required status
134
+ if (config?.required !== undefined) {
135
+ return { ...field, required: config.required };
136
+ }
137
+ return field;
138
+ });
139
+ };
140
+ /**
141
+ * Sort fields by placement (inline first, then post-credentials)
142
+ */
143
+ const sortFieldsByPlacement = (fields, registrationConfig) => {
144
+ const configMap = new Map(registrationConfig.map(c => [c.key, c]));
145
+ const inline = [];
146
+ const postCredentials = [];
147
+ fields.forEach(field => {
148
+ const config = configMap.get(field.key);
149
+ if (config?.placement === 'post-credentials') {
150
+ postCredentials.push(field);
151
+ }
152
+ else {
153
+ inline.push(field);
154
+ }
155
+ });
156
+ return { inline, postCredentials };
157
+ };
158
+
159
+ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, schema, registrationFieldsConfig = [], additionalFields = [], }) => {
61
160
  const [formData, setFormData] = useState({
62
161
  email: '',
63
162
  password: '',
64
163
  displayName: '',
65
164
  });
165
+ // Custom field values (separate from core AuthFormData)
166
+ const [customFieldValues, setCustomFieldValues] = useState({});
167
+ // Get schema-driven registration fields
168
+ const schemaFields = useMemo(() => {
169
+ if (!schema || !registrationFieldsConfig.length)
170
+ return { inline: [], postCredentials: [] };
171
+ const fields = getRegistrationFields(schema, registrationFieldsConfig);
172
+ return sortFieldsByPlacement(fields, registrationFieldsConfig);
173
+ }, [schema, registrationFieldsConfig]);
174
+ // Check if we have any additional fields to show
175
+ const hasSchemaFields = schemaFields.inline.length > 0 || schemaFields.postCredentials.length > 0;
176
+ const hasLegacyFields = additionalFields.length > 0 && !hasSchemaFields;
66
177
  const handleSubmit = async (e) => {
67
178
  e.preventDefault();
68
- await onSubmit(formData);
179
+ // Merge custom field values into accountData for registration
180
+ const submitData = {
181
+ ...formData,
182
+ accountData: mode === 'register'
183
+ ? { ...(formData.accountData || {}), customFields: customFieldValues }
184
+ : undefined,
185
+ };
186
+ await onSubmit(submitData);
69
187
  };
70
188
  const handleChange = (field, value) => {
71
189
  setFormData(prev => ({ ...prev, [field]: value }));
72
190
  };
191
+ const handleCustomFieldChange = (key, value) => {
192
+ setCustomFieldValues(prev => ({ ...prev, [key]: value }));
193
+ };
194
+ const renderSchemaField = (field) => (jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key));
195
+ // Legacy field renderer (for backward compatibility)
196
+ const renderLegacyField = (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));
73
197
  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'
74
198
  ? 'Welcome back! Please enter your credentials.'
75
- : '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' })] })] }));
199
+ : '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' && hasSchemaFields && schemaFields.inline.map(renderSchemaField), mode === 'register' && hasLegacyFields && additionalFields.map(renderLegacyField), mode === 'register' && hasSchemaFields && schemaFields.postCredentials.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "auth-divider", style: { margin: '16px 0' }, children: jsx("span", { children: "Additional Information" }) }), schemaFields.postCredentials.map(renderSchemaField)] })), 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' })] })] }));
76
200
  };
77
201
 
78
202
  const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, loading, }) => {
@@ -10640,6 +10764,8 @@ class AuthAPI {
10640
10764
  return smartlinks.authKit.login(this.clientId, email, password);
10641
10765
  }
10642
10766
  async register(data) {
10767
+ // Note: redirectUrl is not passed to register - verification email is sent separately
10768
+ // via sendEmailVerification() after registration for verify-then-* modes
10643
10769
  return smartlinks.authKit.register(this.clientId, {
10644
10770
  email: data.email,
10645
10771
  password: data.password,
@@ -11146,7 +11272,7 @@ const tokenStorage = {
11146
11272
  const storage = await getStorage();
11147
11273
  const authToken = {
11148
11274
  token,
11149
- expiresAt: expiresAt || Date.now() + 3600000, // Default 1 hour
11275
+ expiresAt: expiresAt || Date.now() + (7 * 24 * 60 * 60 * 1000), // Default 7 days (matches backend JWT)
11150
11276
  };
11151
11277
  await storage.setItem(TOKEN_KEY, authToken);
11152
11278
  },
@@ -11157,7 +11283,8 @@ const tokenStorage = {
11157
11283
  return null;
11158
11284
  // Check if token is expired
11159
11285
  if (authToken.expiresAt && authToken.expiresAt < Date.now()) {
11160
- await this.clearToken();
11286
+ console.log('[TokenStorage] Token expired at:', new Date(authToken.expiresAt).toISOString(), '- clearing all auth data');
11287
+ await this.clearAll(); // Clear ALL auth data to prevent orphaned state
11161
11288
  return null;
11162
11289
  }
11163
11290
  return authToken;
@@ -11237,6 +11364,9 @@ const tokenStorage = {
11237
11364
 
11238
11365
  const AuthContext = createContext(undefined);
11239
11366
  const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
11367
+ // Token refresh settings
11368
+ enableAutoRefresh = true, refreshThresholdPercent = 75, // Refresh when 75% of token lifetime has passed
11369
+ refreshCheckInterval = 60 * 1000, // Check every minute
11240
11370
  // Contact & Interaction features
11241
11371
  collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
11242
11372
  const [user, setUser] = useState(null);
@@ -11657,11 +11787,11 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11657
11787
  unsubscribe();
11658
11788
  };
11659
11789
  }, [proxyMode, notifyAuthStateChange]);
11660
- const login = useCallback(async (authToken, authUser, authAccountData, isNewUser) => {
11790
+ const login = useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt) => {
11661
11791
  try {
11662
11792
  // Only persist to storage in standalone mode
11663
11793
  if (!proxyMode) {
11664
- await tokenStorage.saveToken(authToken);
11794
+ await tokenStorage.saveToken(authToken, expiresAt);
11665
11795
  await tokenStorage.saveUser(authUser);
11666
11796
  if (authAccountData) {
11667
11797
  await tokenStorage.saveAccountData(authAccountData);
@@ -11744,9 +11874,67 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11744
11874
  const storedToken = await tokenStorage.getToken();
11745
11875
  return storedToken ? storedToken.token : null;
11746
11876
  }, [proxyMode, token]);
11877
+ // Get token with expiration info
11878
+ const getTokenInfo = useCallback(async () => {
11879
+ if (proxyMode) {
11880
+ // In proxy mode, we don't have expiration info
11881
+ if (token) {
11882
+ return { token, expiresAt: 0, expiresIn: 0 };
11883
+ }
11884
+ return null;
11885
+ }
11886
+ const storedToken = await tokenStorage.getToken();
11887
+ if (!storedToken?.token || !storedToken.expiresAt) {
11888
+ return null;
11889
+ }
11890
+ return {
11891
+ token: storedToken.token,
11892
+ expiresAt: storedToken.expiresAt,
11893
+ expiresIn: Math.max(0, storedToken.expiresAt - Date.now()),
11894
+ };
11895
+ }, [proxyMode, token]);
11896
+ // Refresh token - validates current token and extends session if backend supports it
11747
11897
  const refreshToken = useCallback(async () => {
11748
- throw new Error('Token refresh must be implemented via your backend API');
11749
- }, []);
11898
+ if (proxyMode) {
11899
+ console.log('[AuthContext] Proxy mode: token refresh handled by parent');
11900
+ throw new Error('Token refresh in proxy mode is handled by the parent application');
11901
+ }
11902
+ const storedToken = await tokenStorage.getToken();
11903
+ if (!storedToken?.token) {
11904
+ throw new Error('No token to refresh. Please login first.');
11905
+ }
11906
+ try {
11907
+ console.log('[AuthContext] Refreshing token...');
11908
+ // Verify current token is still valid
11909
+ const verifyResult = await smartlinks.auth.verifyToken(storedToken.token);
11910
+ if (!verifyResult.valid) {
11911
+ console.warn('[AuthContext] Token is no longer valid, clearing session');
11912
+ await logout();
11913
+ throw new Error('Token expired or invalid. Please login again.');
11914
+ }
11915
+ // Token is valid - extend its expiration locally
11916
+ // Backend JWT remains valid, we just update our local tracking
11917
+ const newExpiresAt = Date.now() + (7 * 24 * 60 * 60 * 1000); // 7 days from now
11918
+ await tokenStorage.saveToken(storedToken.token, newExpiresAt);
11919
+ console.log('[AuthContext] Token verified and expiration extended to:', new Date(newExpiresAt).toISOString());
11920
+ // Update verified state
11921
+ setIsVerified(true);
11922
+ pendingVerificationRef.current = false;
11923
+ notifyAuthStateChange('TOKEN_REFRESH', user, storedToken.token, accountData, accountInfo, true, contact, contactId);
11924
+ return storedToken.token;
11925
+ }
11926
+ catch (error) {
11927
+ console.error('[AuthContext] Token refresh failed:', error);
11928
+ // If it's a network error, don't logout
11929
+ if (isNetworkError(error)) {
11930
+ console.warn('[AuthContext] Network error during refresh, keeping session');
11931
+ throw error;
11932
+ }
11933
+ // Auth error - clear session
11934
+ await logout();
11935
+ throw new Error('Token refresh failed. Please login again.');
11936
+ }
11937
+ }, [proxyMode, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
11750
11938
  const getAccount = useCallback(async (forceRefresh = false) => {
11751
11939
  try {
11752
11940
  if (proxyMode) {
@@ -11854,6 +12042,54 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11854
12042
  window.removeEventListener('offline', handleOffline);
11855
12043
  };
11856
12044
  }, [proxyMode, token, user, retryVerification]);
12045
+ // Automatic background token refresh
12046
+ useEffect(() => {
12047
+ if (proxyMode || !enableAutoRefresh || !token || !user) {
12048
+ return;
12049
+ }
12050
+ console.log('[AuthContext] Setting up automatic token refresh (interval:', refreshCheckInterval, 'ms, threshold:', refreshThresholdPercent, '%)');
12051
+ const checkAndRefresh = async () => {
12052
+ try {
12053
+ const storedToken = await tokenStorage.getToken();
12054
+ if (!storedToken?.expiresAt) {
12055
+ console.log('[AuthContext] No token expiration info, skipping refresh check');
12056
+ return;
12057
+ }
12058
+ const now = Date.now();
12059
+ const tokenLifetime = storedToken.expiresAt - (storedToken.expiresAt - (7 * 24 * 60 * 60 * 1000)); // Assume 7-day lifetime
12060
+ const tokenAge = now - (storedToken.expiresAt - (7 * 24 * 60 * 60 * 1000));
12061
+ const percentUsed = (tokenAge / tokenLifetime) * 100;
12062
+ // Calculate time remaining
12063
+ const timeRemaining = storedToken.expiresAt - now;
12064
+ const hoursRemaining = Math.round(timeRemaining / (60 * 60 * 1000));
12065
+ if (percentUsed >= refreshThresholdPercent) {
12066
+ console.log(`[AuthContext] Token at ${Math.round(percentUsed)}% lifetime (${hoursRemaining}h remaining), refreshing...`);
12067
+ try {
12068
+ await refreshToken();
12069
+ console.log('[AuthContext] Automatic token refresh successful');
12070
+ }
12071
+ catch (refreshError) {
12072
+ console.warn('[AuthContext] Automatic token refresh failed:', refreshError);
12073
+ // Don't logout on refresh failure - user can still use the app until token actually expires
12074
+ }
12075
+ }
12076
+ else {
12077
+ console.log(`[AuthContext] Token at ${Math.round(percentUsed)}% lifetime (${hoursRemaining}h remaining), no refresh needed`);
12078
+ }
12079
+ }
12080
+ catch (error) {
12081
+ console.error('[AuthContext] Error checking token for refresh:', error);
12082
+ }
12083
+ };
12084
+ // Check immediately on mount
12085
+ checkAndRefresh();
12086
+ // Set up periodic check
12087
+ const intervalId = setInterval(checkAndRefresh, refreshCheckInterval);
12088
+ return () => {
12089
+ console.log('[AuthContext] Cleaning up automatic token refresh timer');
12090
+ clearInterval(intervalId);
12091
+ };
12092
+ }, [proxyMode, enableAutoRefresh, refreshCheckInterval, refreshThresholdPercent, token, user, refreshToken]);
11857
12093
  const value = {
11858
12094
  user,
11859
12095
  token,
@@ -11871,6 +12107,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11871
12107
  login,
11872
12108
  logout,
11873
12109
  getToken,
12110
+ getTokenInfo,
11874
12111
  refreshToken,
11875
12112
  getAccount,
11876
12113
  refreshAccount,
@@ -11888,6 +12125,14 @@ const useAuth = () => {
11888
12125
  return context;
11889
12126
  };
11890
12127
 
12128
+ // Helper to calculate expiration from AuthResponse
12129
+ const getExpirationFromResponse = (response) => {
12130
+ if (response.expiresAt)
12131
+ return response.expiresAt;
12132
+ if (response.expiresIn)
12133
+ return Date.now() + response.expiresIn;
12134
+ return undefined; // Will use 7-day default in tokenStorage
12135
+ };
11891
12136
  // Default Smartlinks Google OAuth Client ID (public - safe to expose)
11892
12137
  const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
11893
12138
  // Default auth UI configuration when no clientId is provided
@@ -11958,7 +12203,7 @@ const getFriendlyErrorMessage = (errorMessage) => {
11958
12203
  // Return original message if no pattern matches
11959
12204
  return errorMessage;
11960
12205
  };
11961
- const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'auto', className, customization, skipConfigFetch = false, minimal = false, logger, proxyMode = false, }) => {
12206
+ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'auto', className, customization, skipConfigFetch = false, minimal = false, logger, proxyMode = false, collectionId, }) => {
11962
12207
  const [mode, setMode] = useState(initialMode);
11963
12208
  const [loading, setLoading] = useState(false);
11964
12209
  const [error, setError] = useState();
@@ -11975,6 +12220,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11975
12220
  const [configLoading, setConfigLoading] = useState(!skipConfigFetch);
11976
12221
  const [showEmailForm, setShowEmailForm] = useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
11977
12222
  const [sdkReady, setSdkReady] = useState(false); // Track SDK initialization state
12223
+ const [contactSchema, setContactSchema] = useState(null); // Schema for registration fields
11978
12224
  const log = useMemo(() => createLoggerWrapper(logger), [logger]);
11979
12225
  const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
11980
12226
  const auth = useAuth();
@@ -12149,6 +12395,24 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12149
12395
  };
12150
12396
  fetchConfig();
12151
12397
  }, [apiEndpoint, clientId, customization, skipConfigFetch, sdkReady, proxyMode, log]);
12398
+ // Fetch contact schema for registration fields when collectionId is provided
12399
+ useEffect(() => {
12400
+ if (!collectionId || !sdkReady)
12401
+ return;
12402
+ const fetchSchema = async () => {
12403
+ try {
12404
+ console.log('[SmartlinksAuthUI] 📋 Fetching contact schema for collection:', collectionId);
12405
+ const schema = await smartlinks.contact.publicGetSchema(collectionId);
12406
+ console.log('[SmartlinksAuthUI] ✅ Schema loaded:', schema);
12407
+ setContactSchema(schema);
12408
+ }
12409
+ catch (err) {
12410
+ console.warn('[SmartlinksAuthUI] ⚠️ Failed to fetch schema (non-fatal):', err);
12411
+ // Non-fatal - registration will work without schema fields
12412
+ }
12413
+ };
12414
+ fetchSchema();
12415
+ }, [collectionId, sdkReady]);
12152
12416
  // Reset showEmailForm when mode changes away from login/register
12153
12417
  useEffect(() => {
12154
12418
  if (mode !== 'login' && mode !== 'register') {
@@ -12189,18 +12453,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12189
12453
  const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
12190
12454
  if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
12191
12455
  // Auto-login modes: Log the user in immediately if token is provided
12192
- auth.login(response.token, response.user, response.accountData, true);
12456
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12193
12457
  setAuthSuccess(true);
12194
12458
  setSuccessMessage('Email verified successfully! You are now logged in.');
12195
12459
  onAuthSuccess(response.token, response.user, response.accountData);
12196
12460
  // Clear the URL parameters
12197
12461
  const cleanUrl = window.location.href.split('?')[0];
12198
12462
  window.history.replaceState({}, document.title, cleanUrl);
12199
- // Redirect after a brief delay to show success message
12463
+ // For email verification deep links, redirect immediately if configured
12464
+ // (user came from an email link, so redirect is expected behavior)
12200
12465
  if (redirectUrl) {
12201
- setTimeout(() => {
12202
- window.location.href = redirectUrl;
12203
- }, 2000);
12466
+ window.location.href = redirectUrl;
12204
12467
  }
12205
12468
  }
12206
12469
  else {
@@ -12232,18 +12495,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12232
12495
  const response = await api.verifyMagicLink(token);
12233
12496
  // Auto-login with magic link if token is provided
12234
12497
  if (response.token) {
12235
- auth.login(response.token, response.user, response.accountData, true);
12498
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12236
12499
  setAuthSuccess(true);
12237
12500
  setSuccessMessage('Magic link verified! You are now logged in.');
12238
12501
  onAuthSuccess(response.token, response.user, response.accountData);
12239
12502
  // Clear the URL parameters
12240
12503
  const cleanUrl = window.location.href.split('?')[0];
12241
12504
  window.history.replaceState({}, document.title, cleanUrl);
12242
- // Redirect after a brief delay to show success message
12505
+ // For magic link deep links, redirect immediately if configured
12506
+ // (user came from an email link, so redirect is expected behavior)
12243
12507
  if (redirectUrl) {
12244
- setTimeout(() => {
12245
- window.location.href = redirectUrl;
12246
- }, 2000);
12508
+ window.location.href = redirectUrl;
12247
12509
  }
12248
12510
  }
12249
12511
  else {
@@ -12308,7 +12570,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12308
12570
  // Handle different verification modes
12309
12571
  if (verificationMode === 'immediate' && response.token) {
12310
12572
  // Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
12311
- auth.login(response.token, response.user, response.accountData, true);
12573
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12312
12574
  setAuthSuccess(true);
12313
12575
  const deadline = response.emailVerificationDeadline
12314
12576
  ? new Date(response.emailVerificationDeadline).toLocaleString()
@@ -12317,19 +12579,37 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12317
12579
  if (response.token) {
12318
12580
  onAuthSuccess(response.token, response.user, response.accountData);
12319
12581
  }
12320
- if (redirectUrl) {
12321
- setTimeout(() => {
12322
- window.location.href = redirectUrl;
12323
- }, 2000);
12324
- }
12582
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12325
12583
  }
12326
12584
  else if (verificationMode === 'verify-then-auto-login') {
12327
12585
  // Verify-then-auto-login mode: Don't log in yet, but will auto-login after email verification
12586
+ // Send the verification email since backend register may not send it automatically
12587
+ if (response.user?.uid && data.email) {
12588
+ try {
12589
+ await api.sendEmailVerification(response.user.uid, data.email, getRedirectUrl());
12590
+ log.log('Verification email sent after registration');
12591
+ }
12592
+ catch (verifyError) {
12593
+ log.warn('Failed to send verification email after registration:', verifyError);
12594
+ // Don't fail the registration, just log the warning
12595
+ }
12596
+ }
12328
12597
  setAuthSuccess(true);
12329
12598
  setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
12330
12599
  }
12331
12600
  else {
12332
12601
  // verify-then-manual-login mode: Traditional flow
12602
+ // Send the verification email since backend register may not send it automatically
12603
+ if (response.user?.uid && data.email) {
12604
+ try {
12605
+ await api.sendEmailVerification(response.user.uid, data.email, getRedirectUrl());
12606
+ log.log('Verification email sent after registration');
12607
+ }
12608
+ catch (verifyError) {
12609
+ log.warn('Failed to send verification email after registration:', verifyError);
12610
+ // Don't fail the registration, just log the warning
12611
+ }
12612
+ }
12333
12613
  setAuthSuccess(true);
12334
12614
  setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
12335
12615
  }
@@ -12344,15 +12624,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12344
12624
  if (response.requiresEmailVerification) {
12345
12625
  throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
12346
12626
  }
12347
- auth.login(response.token, response.user, response.accountData, false);
12627
+ auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response));
12348
12628
  setAuthSuccess(true);
12349
12629
  setSuccessMessage('Login successful!');
12350
12630
  onAuthSuccess(response.token, response.user, response.accountData);
12351
- if (redirectUrl) {
12352
- setTimeout(() => {
12353
- window.location.href = redirectUrl;
12354
- }, 2000);
12355
- }
12631
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12356
12632
  }
12357
12633
  else {
12358
12634
  throw new Error('Authentication failed - please verify your email before logging in.');
@@ -12505,7 +12781,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12505
12781
  });
12506
12782
  if (authResponse.token) {
12507
12783
  // Google OAuth can be login or signup - use isNewUser flag from backend if available
12508
- auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12784
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
12509
12785
  setAuthSuccess(true);
12510
12786
  setSuccessMessage('Google login successful!');
12511
12787
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12513,11 +12789,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12513
12789
  else {
12514
12790
  throw new Error('Authentication failed - no token received');
12515
12791
  }
12516
- if (redirectUrl) {
12517
- setTimeout(() => {
12518
- window.location.href = redirectUrl;
12519
- }, 2000);
12520
- }
12792
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12521
12793
  }
12522
12794
  catch (apiError) {
12523
12795
  const errorMessage = apiError instanceof Error ? apiError.message : 'Google login failed';
@@ -12555,7 +12827,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12555
12827
  const authResponse = await api.loginWithGoogle(idToken);
12556
12828
  if (authResponse.token) {
12557
12829
  // Google OAuth can be login or signup - use isNewUser flag from backend if available
12558
- auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12830
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
12559
12831
  setAuthSuccess(true);
12560
12832
  setSuccessMessage('Google login successful!');
12561
12833
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12563,11 +12835,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12563
12835
  else {
12564
12836
  throw new Error('Authentication failed - no token received');
12565
12837
  }
12566
- if (redirectUrl) {
12567
- setTimeout(() => {
12568
- window.location.href = redirectUrl;
12569
- }, 2000);
12570
- }
12838
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12571
12839
  setLoading(false);
12572
12840
  }
12573
12841
  catch (err) {
@@ -12617,7 +12885,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12617
12885
  // Update auth context with account data if token is provided
12618
12886
  if (response.token) {
12619
12887
  // Phone auth can be login or signup - use isNewUser flag from backend if available
12620
- auth.login(response.token, response.user, response.accountData, response.isNewUser);
12888
+ auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
12621
12889
  onAuthSuccess(response.token, response.user, response.accountData);
12622
12890
  if (redirectUrl) {
12623
12891
  window.location.href = redirectUrl;
@@ -12790,36 +13058,39 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12790
13058
  setShowResendVerification(false);
12791
13059
  setShowRequestNewReset(false);
12792
13060
  setError(undefined);
12793
- }, 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 }))] }));
13061
+ }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, schema: contactSchema, registrationFieldsConfig: config?.registrationFields, 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 }))] }));
12794
13062
  })() })) })) : null }));
12795
13063
  };
12796
13064
 
12797
- const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', customization = {}, }) => {
13065
+ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, className = '', customization = {}, }) => {
12798
13066
  const auth = useAuth();
12799
13067
  const [loading, setLoading] = useState(false);
12800
13068
  const [profile, setProfile] = useState(null);
12801
13069
  const [error, setError] = useState();
12802
13070
  const [success, setSuccess] = useState();
13071
+ // Schema state
13072
+ const [schema, setSchema] = useState(null);
13073
+ const [schemaLoading, setSchemaLoading] = useState(false);
12803
13074
  // Track which section is being edited
12804
13075
  const [editingSection, setEditingSection] = useState(null);
12805
- // Profile form state
13076
+ // Form state for core fields
12806
13077
  const [displayName, setDisplayName] = useState('');
12807
- // Email change state
12808
13078
  const [newEmail, setNewEmail] = useState('');
12809
13079
  const [emailPassword, setEmailPassword] = useState('');
12810
- // Password change state
12811
13080
  const [currentPassword, setCurrentPassword] = useState('');
12812
13081
  const [newPassword, setNewPassword] = useState('');
12813
13082
  const [confirmPassword, setConfirmPassword] = useState('');
12814
- // Phone change state (reuses existing sendPhoneCode flow)
12815
13083
  const [newPhone, setNewPhone] = useState('');
12816
13084
  const [phoneCode, setPhoneCode] = useState('');
12817
13085
  const [phoneCodeSent, setPhoneCodeSent] = useState(false);
13086
+ // Custom fields form state
13087
+ const [customFieldValues, setCustomFieldValues] = useState({});
12818
13088
  // Account deletion state
12819
13089
  const [deletePassword, setDeletePassword] = useState('');
12820
13090
  const [deleteConfirmText, setDeleteConfirmText] = useState('');
12821
13091
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
12822
- const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, } = customization;
13092
+ const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, showCustomFields = true, // New: show schema-driven custom fields
13093
+ } = customization;
12823
13094
  // Reinitialize Smartlinks SDK when apiEndpoint changes
12824
13095
  useEffect(() => {
12825
13096
  if (apiEndpoint) {
@@ -12830,10 +13101,32 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12830
13101
  });
12831
13102
  }
12832
13103
  }, [apiEndpoint]);
12833
- // Load user profile on mount
13104
+ // Load schema when collectionId is available
13105
+ useEffect(() => {
13106
+ if (!collectionId)
13107
+ return;
13108
+ const loadSchema = async () => {
13109
+ setSchemaLoading(true);
13110
+ try {
13111
+ console.log('[AccountManagement] Loading schema for collection:', collectionId);
13112
+ const schemaResult = await smartlinks.contact.publicGetSchema(collectionId);
13113
+ console.log('[AccountManagement] Schema loaded:', schemaResult);
13114
+ setSchema(schemaResult);
13115
+ }
13116
+ catch (err) {
13117
+ console.warn('[AccountManagement] Failed to load schema:', err);
13118
+ // Non-fatal - component works without schema
13119
+ }
13120
+ finally {
13121
+ setSchemaLoading(false);
13122
+ }
13123
+ };
13124
+ loadSchema();
13125
+ }, [collectionId]);
13126
+ // Load user profile and contact data on mount
12834
13127
  useEffect(() => {
12835
13128
  loadProfile();
12836
- }, [clientId]);
13129
+ }, [clientId, collectionId]);
12837
13130
  const loadProfile = async () => {
12838
13131
  if (!auth.isAuthenticated) {
12839
13132
  setError('You must be logged in to manage your account');
@@ -12842,10 +13135,7 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12842
13135
  setLoading(true);
12843
13136
  setError(undefined);
12844
13137
  try {
12845
- // TODO: Backend implementation required
12846
- // Endpoint: GET /api/v1/authkit/:clientId/account/profile
12847
- // SDK method: smartlinks.authKit.getProfile(clientId)
12848
- // Temporary mock data for UI testing
13138
+ // Get base profile from auth context
12849
13139
  const profileData = {
12850
13140
  uid: auth.user?.uid || '',
12851
13141
  email: auth.user?.email,
@@ -12857,6 +13147,17 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12857
13147
  };
12858
13148
  setProfile(profileData);
12859
13149
  setDisplayName(profileData.displayName || '');
13150
+ // Load contact custom fields if collectionId provided
13151
+ if (collectionId && auth.contact) {
13152
+ setCustomFieldValues(auth.contact.customFields || {});
13153
+ }
13154
+ else if (collectionId) {
13155
+ // Try to fetch contact
13156
+ const contact = await auth.getContact?.();
13157
+ if (contact?.customFields) {
13158
+ setCustomFieldValues(contact.customFields);
13159
+ }
13160
+ }
12860
13161
  }
12861
13162
  catch (err) {
12862
13163
  const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
@@ -12867,28 +13168,39 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12867
13168
  setLoading(false);
12868
13169
  }
12869
13170
  };
13171
+ const cancelEdit = useCallback(() => {
13172
+ setEditingSection(null);
13173
+ setDisplayName(profile?.displayName || '');
13174
+ setNewEmail('');
13175
+ setEmailPassword('');
13176
+ setCurrentPassword('');
13177
+ setNewPassword('');
13178
+ setConfirmPassword('');
13179
+ setNewPhone('');
13180
+ setPhoneCode('');
13181
+ setPhoneCodeSent(false);
13182
+ setError(undefined);
13183
+ setSuccess(undefined);
13184
+ // Reset custom fields to original values
13185
+ if (auth.contact?.customFields) {
13186
+ setCustomFieldValues(auth.contact.customFields);
13187
+ }
13188
+ }, [profile, auth.contact]);
13189
+ // Get editable custom fields from schema
13190
+ const editableCustomFields = getEditableFields(schema).filter(f => !['email', 'displayName', 'phone', 'phoneNumber'].includes(f.key));
12870
13191
  const handleUpdateProfile = async (e) => {
12871
13192
  e.preventDefault();
12872
13193
  setLoading(true);
12873
13194
  setError(undefined);
12874
13195
  setSuccess(undefined);
12875
13196
  try {
12876
- // TODO: Backend implementation required
12877
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-profile
12878
- // SDK method: smartlinks.authKit.updateProfile(clientId, updateData)
12879
- setError('Backend API not yet implemented. See console for required endpoint.');
12880
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-profile');
12881
- console.log('Update data:', { displayName });
12882
- // Uncomment when backend is ready:
12883
- // const updateData: ProfileUpdateData = {
12884
- // displayName: displayName || undefined,
12885
- // photoURL: photoURL || undefined,
12886
- // };
12887
- // const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
12888
- // setProfile(updatedProfile);
12889
- // setSuccess('Profile updated successfully!');
12890
- // setEditingSection(null);
12891
- // onProfileUpdated?.(updatedProfile);
13197
+ await smartlinks.authKit.updateProfile(clientId, { displayName });
13198
+ setSuccess('Profile updated successfully!');
13199
+ setEditingSection(null);
13200
+ // Update local state
13201
+ if (profile) {
13202
+ setProfile({ ...profile, displayName });
13203
+ }
12892
13204
  }
12893
13205
  catch (err) {
12894
13206
  const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
@@ -12899,19 +13211,29 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12899
13211
  setLoading(false);
12900
13212
  }
12901
13213
  };
12902
- const cancelEdit = () => {
12903
- setEditingSection(null);
12904
- setDisplayName(profile?.displayName || '');
12905
- setNewEmail('');
12906
- setEmailPassword('');
12907
- setCurrentPassword('');
12908
- setNewPassword('');
12909
- setConfirmPassword('');
12910
- setNewPhone('');
12911
- setPhoneCode('');
12912
- setPhoneCodeSent(false);
13214
+ const handleUpdateCustomFields = async (e) => {
13215
+ e.preventDefault();
13216
+ if (!collectionId) {
13217
+ setError('Collection ID is required to update custom fields');
13218
+ return;
13219
+ }
13220
+ setLoading(true);
12913
13221
  setError(undefined);
12914
13222
  setSuccess(undefined);
13223
+ try {
13224
+ console.log('[AccountManagement] Updating custom fields:', customFieldValues);
13225
+ await auth.updateContactCustomFields?.(customFieldValues);
13226
+ setSuccess('Profile updated successfully!');
13227
+ setEditingSection(null);
13228
+ }
13229
+ catch (err) {
13230
+ const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
13231
+ setError(errorMessage);
13232
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
13233
+ }
13234
+ finally {
13235
+ setLoading(false);
13236
+ }
12915
13237
  };
12916
13238
  const handleChangeEmail = async (e) => {
12917
13239
  e.preventDefault();
@@ -12919,21 +13241,12 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12919
13241
  setError(undefined);
12920
13242
  setSuccess(undefined);
12921
13243
  try {
12922
- // TODO: Backend implementation required
12923
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-email
12924
- // SDK method: smartlinks.authKit.changeEmail(clientId, newEmail, password)
12925
- // Note: No verification flow for now - direct email update
12926
- setError('Backend API not yet implemented. See console for required endpoint.');
12927
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-email');
12928
- console.log('Data:', { newEmail });
12929
- // Uncomment when backend is ready:
12930
- // await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
12931
- // setSuccess('Email changed successfully!');
12932
- // setEditingSection(null);
12933
- // setNewEmail('');
12934
- // setEmailPassword('');
12935
- // onEmailChangeRequested?.();
12936
- // await loadProfile(); // Reload to show new email
13244
+ const redirectUrl = window.location.href;
13245
+ await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword, redirectUrl);
13246
+ setSuccess('Email change requested. Please check your new email for verification.');
13247
+ setEditingSection(null);
13248
+ setNewEmail('');
13249
+ setEmailPassword('');
12937
13250
  }
12938
13251
  catch (err) {
12939
13252
  const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
@@ -12958,20 +13271,12 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12958
13271
  setError(undefined);
12959
13272
  setSuccess(undefined);
12960
13273
  try {
12961
- // TODO: Backend implementation required
12962
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-password
12963
- // SDK method: smartlinks.authKit.changePassword(clientId, currentPassword, newPassword)
12964
- setError('Backend API not yet implemented. See console for required endpoint.');
12965
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-password');
12966
- console.log('Data: currentPassword and newPassword provided');
12967
- // Uncomment when backend is ready:
12968
- // await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
12969
- // setSuccess('Password changed successfully!');
12970
- // setEditingSection(null);
12971
- // setCurrentPassword('');
12972
- // setNewPassword('');
12973
- // setConfirmPassword('');
12974
- // onPasswordChanged?.();
13274
+ await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
13275
+ setSuccess('Password changed successfully!');
13276
+ setEditingSection(null);
13277
+ setCurrentPassword('');
13278
+ setNewPassword('');
13279
+ setConfirmPassword('');
12975
13280
  }
12976
13281
  catch (err) {
12977
13282
  const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
@@ -13005,20 +13310,13 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13005
13310
  setError(undefined);
13006
13311
  setSuccess(undefined);
13007
13312
  try {
13008
- // TODO: Backend implementation required
13009
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-phone
13010
- // SDK method: smartlinks.authKit.updatePhone(clientId, phoneNumber, verificationCode)
13011
- setError('Backend API not yet implemented. See console for required endpoint.');
13012
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-phone');
13013
- console.log('Data:', { phoneNumber: newPhone, code: phoneCode });
13014
- // Uncomment when backend is ready:
13015
- // await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
13016
- // setSuccess('Phone number updated successfully!');
13017
- // setEditingSection(null);
13018
- // setNewPhone('');
13019
- // setPhoneCode('');
13020
- // setPhoneCodeSent(false);
13021
- // await loadProfile();
13313
+ await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
13314
+ setSuccess('Phone number updated successfully!');
13315
+ setEditingSection(null);
13316
+ setNewPhone('');
13317
+ setPhoneCode('');
13318
+ setPhoneCodeSent(false);
13319
+ await loadProfile();
13022
13320
  }
13023
13321
  catch (err) {
13024
13322
  const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
@@ -13041,19 +13339,9 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13041
13339
  setLoading(true);
13042
13340
  setError(undefined);
13043
13341
  try {
13044
- // TODO: Backend implementation required
13045
- // Endpoint: DELETE /api/v1/authkit/:clientId/account/delete
13046
- // SDK method: smartlinks.authKit.deleteAccount(clientId, password, confirmText)
13047
- // Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
13048
- setError('Backend API not yet implemented. See console for required endpoint.');
13049
- console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
13050
- console.log('Data: password and confirmText="DELETE" provided');
13051
- console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
13052
- // Uncomment when backend is ready:
13053
- // await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
13054
- // setSuccess('Account deleted successfully');
13055
- // onAccountDeleted?.();
13056
- // await auth.logout();
13342
+ await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
13343
+ setSuccess('Account deleted successfully');
13344
+ await auth.logout();
13057
13345
  }
13058
13346
  catch (err) {
13059
13347
  const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
@@ -13064,120 +13352,24 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13064
13352
  setLoading(false);
13065
13353
  }
13066
13354
  };
13355
+ const handleCustomFieldChange = (key, value) => {
13356
+ setCustomFieldValues(prev => ({ ...prev, [key]: value }));
13357
+ };
13067
13358
  if (!auth.isAuthenticated) {
13068
13359
  return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Please log in to manage your account" }) }));
13069
13360
  }
13070
- if (loading && !profile) {
13361
+ if ((loading || schemaLoading) && !profile) {
13071
13362
  return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
13072
13363
  }
13073
- 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: () => {
13364
+ 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" })] })] }))] })), showCustomFields && collectionId && editableCustomFields.length > 0 && (jsxs("section", { className: "account-section", children: [jsx("h3", { className: "section-title", style: { fontSize: '0.9rem', fontWeight: 600, marginBottom: '12px' }, children: "Additional Information" }), editingSection !== 'customFields' ? (jsxs(Fragment, { children: [editableCustomFields.map((field) => (jsx("div", { className: "account-field", children: jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: field.label }), jsx("div", { className: "field-value", children: customFieldValues[field.key] !== undefined && customFieldValues[field.key] !== ''
13365
+ ? String(customFieldValues[field.key])
13366
+ : 'Not set' })] }) }, field.key))), jsx("button", { type: "button", onClick: () => setEditingSection('customFields'), className: "auth-button button-secondary", style: { marginTop: '8px' }, children: "Edit Information" })] })) : (jsxs("form", { onSubmit: handleUpdateCustomFields, className: "edit-form", children: [editableCustomFields.map((field) => (jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key))), 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: () => {
13074
13367
  setShowDeleteConfirm(false);
13075
13368
  setDeletePassword('');
13076
13369
  setDeleteConfirmText('');
13077
13370
  }, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
13078
13371
  };
13079
13372
 
13080
- const SmartlinksClaimUI = (props) => {
13081
- // Destructure AFTER logging raw props to debug proxyMode issue
13082
- const { apiEndpoint, clientId, clientName, collectionId, productId, proofId, onClaimSuccess, onClaimError, additionalFields = [], theme = 'light', className = '', minimal = false, proxyMode = false, customization = {}, } = props;
13083
- // Debug logging for proxyMode - log RAW props first
13084
- console.log('[SmartlinksClaimUI] 🔍 RAW props received:', props);
13085
- console.log('[SmartlinksClaimUI] 🔍 props.proxyMode value:', props.proxyMode);
13086
- console.log('[SmartlinksClaimUI] 🔍 typeof props.proxyMode:', typeof props.proxyMode);
13087
- console.log('[SmartlinksClaimUI] 🎯 Destructured proxyMode:', proxyMode);
13088
- const auth = useAuth();
13089
- const [claimStep, setClaimStep] = useState(auth.isAuthenticated ? 'questions' : 'auth');
13090
- const [claimData, setClaimData] = useState({});
13091
- const [error, setError] = useState();
13092
- const [loading, setLoading] = useState(false);
13093
- const handleAuthSuccess = (token, user, accountData) => {
13094
- // Authentication successful
13095
- auth.login(token, user, accountData);
13096
- // If no additional questions, proceed directly to claim
13097
- if (additionalFields.length === 0) {
13098
- executeClaim(user);
13099
- }
13100
- else {
13101
- setClaimStep('questions');
13102
- }
13103
- };
13104
- const handleQuestionSubmit = async (e) => {
13105
- e.preventDefault();
13106
- // Validate required fields
13107
- const missingFields = additionalFields
13108
- .filter(field => field.required && !claimData[field.name])
13109
- .map(field => field.label);
13110
- if (missingFields.length > 0) {
13111
- setError(`Please fill in: ${missingFields.join(', ')}`);
13112
- return;
13113
- }
13114
- // Execute claim with collected data
13115
- if (auth.user) {
13116
- executeClaim(auth.user);
13117
- }
13118
- };
13119
- const executeClaim = async (user) => {
13120
- setClaimStep('claiming');
13121
- setLoading(true);
13122
- setError(undefined);
13123
- try {
13124
- // Create attestation to claim the proof
13125
- const response = await smartlinks.attestation.create(collectionId, productId, proofId, {
13126
- public: {
13127
- claimed: true,
13128
- claimedAt: new Date().toISOString(),
13129
- claimedBy: user.uid,
13130
- ...claimData,
13131
- },
13132
- private: {},
13133
- proof: {},
13134
- });
13135
- setClaimStep('success');
13136
- // Call success callback
13137
- onClaimSuccess({
13138
- proofId,
13139
- user,
13140
- claimData,
13141
- attestationId: response.id,
13142
- });
13143
- }
13144
- catch (err) {
13145
- console.error('Claim error:', err);
13146
- const errorMessage = err instanceof Error ? err.message : 'Failed to claim proof';
13147
- setError(errorMessage);
13148
- onClaimError?.(err instanceof Error ? err : new Error(errorMessage));
13149
- setClaimStep(additionalFields.length > 0 ? 'questions' : 'auth');
13150
- }
13151
- finally {
13152
- setLoading(false);
13153
- }
13154
- };
13155
- const handleFieldChange = (fieldName, value) => {
13156
- setClaimData(prev => ({
13157
- ...prev,
13158
- [fieldName]: value,
13159
- }));
13160
- };
13161
- // Render authentication step
13162
- if (claimStep === 'auth') {
13163
- console.log('[SmartlinksClaimUI] 🔑 Rendering auth step with proxyMode:', proxyMode);
13164
- return (jsx("div", { className: className, children: jsx(SmartlinksAuthUI, { apiEndpoint: apiEndpoint, clientId: clientId, clientName: clientName, onAuthSuccess: handleAuthSuccess, onAuthError: onClaimError, theme: theme, minimal: minimal, proxyMode: proxyMode, customization: customization.authConfig }) }));
13165
- }
13166
- // Render additional questions step
13167
- if (claimStep === 'questions') {
13168
- 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' })] })] }));
13169
- }
13170
- // Render claiming step (loading state)
13171
- if (claimStep === 'claiming') {
13172
- 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..." })] }));
13173
- }
13174
- // Render success step
13175
- if (claimStep === 'success') {
13176
- 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.' })] }));
13177
- }
13178
- return null;
13179
- };
13180
-
13181
13373
  const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
13182
13374
  const { isAuthenticated, isLoading } = useAuth();
13183
13375
  // Show loading state
@@ -13227,5 +13419,5 @@ const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', '
13227
13419
  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)) })] }))] })) }));
13228
13420
  };
13229
13421
 
13230
- export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SmartlinksAuthUI, SmartlinksClaimUI, tokenStorage, useAuth };
13422
+ export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SchemaFieldRenderer, SmartlinksAuthUI, getEditableFields, getRegistrationFields, sortFieldsByPlacement, tokenStorage, useAuth };
13231
13423
  //# sourceMappingURL=index.esm.js.map