@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.js CHANGED
@@ -70,29 +70,153 @@ const AuthContainer = ({ children, theme = 'light', className = '', config, mini
70
70
  const logoUrl = config?.branding?.logoUrl === undefined
71
71
  ? 'https://smartlinks.app/smartlinks/landscape-medium.png' // Default Smartlinks logo
72
72
  : config?.branding?.logoUrl || null; // Custom or explicitly hidden
73
+ const inheritHostStyles = config?.branding?.inheritHostStyles ? 'auth-inherit-host' : '';
73
74
  const containerClass = minimal
74
- ? `auth-minimal auth-theme-${theme} ${className}`
75
- : `auth-container auth-theme-${theme} ${className}`;
75
+ ? `auth-minimal auth-theme-${theme} ${inheritHostStyles} ${className}`
76
+ : `auth-container auth-theme-${theme} ${inheritHostStyles} ${className}`;
76
77
  const cardClass = minimal ? 'auth-minimal-card' : 'auth-card';
77
78
  return (jsxRuntime.jsx("div", { className: containerClass, children: jsxRuntime.jsxs("div", { className: cardClass, style: !minimal && config?.branding?.buttonStyle === 'square' ? { borderRadius: '4px' } : undefined, children: [(logoUrl || title || subtitle) && (jsxRuntime.jsxs("div", { className: "auth-header", children: [logoUrl && (jsxRuntime.jsx("div", { className: "auth-logo", children: jsxRuntime.jsx("img", { src: logoUrl, alt: "Logo", style: { maxWidth: '200px', height: 'auto', objectFit: 'contain' } }) })), title && jsxRuntime.jsx("h1", { className: "auth-title", children: title }), subtitle && jsxRuntime.jsx("p", { className: "auth-subtitle", children: subtitle })] })), jsxRuntime.jsx("div", { className: "auth-content", children: children }), (config?.branding?.termsUrl || config?.branding?.privacyUrl) && (jsxRuntime.jsxs("div", { className: "auth-footer", children: [config.branding.termsUrl && jsxRuntime.jsx("a", { href: config.branding.termsUrl, target: "_blank", rel: "noopener noreferrer", children: "Terms" }), config.branding.termsUrl && config.branding.privacyUrl && jsxRuntime.jsx("span", { children: "\u2022" }), config.branding.privacyUrl && jsxRuntime.jsx("a", { href: config.branding.privacyUrl, target: "_blank", rel: "noopener noreferrer", children: "Privacy" })] }))] }) }));
78
79
  };
79
80
 
80
- const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, additionalFields = [], }) => {
81
+ /**
82
+ * Renders a form field based on schema definition
83
+ * Used in both registration and account management forms
84
+ */
85
+ const SchemaFieldRenderer = ({ field, value, onChange, disabled = false, error, className = '', }) => {
86
+ const handleChange = (newValue) => {
87
+ onChange(field.key, newValue);
88
+ };
89
+ const inputId = `field-${field.key}`;
90
+ const commonProps = {
91
+ id: inputId,
92
+ className: `auth-input ${error ? 'auth-input-error' : ''} ${className}`,
93
+ disabled: disabled || field.readOnly,
94
+ 'aria-describedby': error ? `${inputId}-error` : undefined,
95
+ };
96
+ const renderField = () => {
97
+ switch (field.widget) {
98
+ case 'select':
99
+ return (jsxRuntime.jsxs("select", { ...commonProps, value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, children: [jsxRuntime.jsxs("option", { value: "", children: ["Select ", field.label, "..."] }), field.options?.map((option) => {
100
+ const optionValue = typeof option === 'string' ? option : option.value;
101
+ const optionLabel = typeof option === 'string' ? option : option.label;
102
+ return (jsxRuntime.jsx("option", { value: optionValue, children: optionLabel }, optionValue));
103
+ })] }));
104
+ case 'checkbox':
105
+ return (jsxRuntime.jsxs("label", { className: "auth-checkbox-label", style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [jsxRuntime.jsx("input", { type: "checkbox", id: inputId, checked: !!value, onChange: (e) => handleChange(e.target.checked), disabled: disabled || field.readOnly, required: field.required, className: "auth-checkbox" }), jsxRuntime.jsx("span", { children: field.description || field.label })] }));
106
+ case 'textarea':
107
+ return (jsxRuntime.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 }));
108
+ case 'number':
109
+ return (jsxRuntime.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 }));
110
+ case 'date':
111
+ return (jsxRuntime.jsx("input", { ...commonProps, type: "date", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required }));
112
+ case 'tel':
113
+ return (jsxRuntime.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" }));
114
+ case 'email':
115
+ return (jsxRuntime.jsx("input", { ...commonProps, type: "email", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, placeholder: field.placeholder || 'email@example.com', autoComplete: "email" }));
116
+ case 'text':
117
+ default:
118
+ return (jsxRuntime.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 }));
119
+ }
120
+ };
121
+ // Checkbox has its own label
122
+ if (field.widget === 'checkbox') {
123
+ return (jsxRuntime.jsxs("div", { className: "auth-form-group", children: [renderField(), error && (jsxRuntime.jsx("div", { id: `${inputId}-error`, className: "auth-field-error", role: "alert", children: error }))] }));
124
+ }
125
+ return (jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsxs("label", { htmlFor: inputId, className: "auth-label", children: [field.label, field.required && jsxRuntime.jsx("span", { style: { color: 'var(--auth-error-color, #ef4444)' }, children: " *" })] }), field.description && (jsxRuntime.jsx("p", { className: "auth-field-description", style: { fontSize: '0.85em', color: 'var(--auth-text-muted)', marginBottom: '4px' }, children: field.description })), renderField(), error && (jsxRuntime.jsx("div", { id: `${inputId}-error`, className: "auth-field-error", role: "alert", children: error }))] }));
126
+ };
127
+ /**
128
+ * Helper to get all editable fields from schema
129
+ */
130
+ const getEditableFields = (schema) => {
131
+ if (!schema)
132
+ return [];
133
+ const editableKeys = new Set(schema.settings.publicEditableFields);
134
+ const coreEditable = schema.fields.filter(f => f.editable && f.visible && editableKeys.has(f.key));
135
+ const customEditable = schema.customFields.filter(f => f.editable && f.visible);
136
+ return [...coreEditable, ...customEditable];
137
+ };
138
+ /**
139
+ * Helper to get registration fields based on config
140
+ */
141
+ const getRegistrationFields = (schema, registrationConfig) => {
142
+ if (!schema || !registrationConfig.length)
143
+ return [];
144
+ const configMap = new Map(registrationConfig.map(c => [c.key, c]));
145
+ const allFields = [...schema.fields, ...schema.customFields];
146
+ return allFields
147
+ .filter(field => {
148
+ const config = configMap.get(field.key);
149
+ return config?.showDuringRegistration && field.visible;
150
+ })
151
+ .map(field => {
152
+ const config = configMap.get(field.key);
153
+ // Allow registration config to override required status
154
+ if (config?.required !== undefined) {
155
+ return { ...field, required: config.required };
156
+ }
157
+ return field;
158
+ });
159
+ };
160
+ /**
161
+ * Sort fields by placement (inline first, then post-credentials)
162
+ */
163
+ const sortFieldsByPlacement = (fields, registrationConfig) => {
164
+ const configMap = new Map(registrationConfig.map(c => [c.key, c]));
165
+ const inline = [];
166
+ const postCredentials = [];
167
+ fields.forEach(field => {
168
+ const config = configMap.get(field.key);
169
+ if (config?.placement === 'post-credentials') {
170
+ postCredentials.push(field);
171
+ }
172
+ else {
173
+ inline.push(field);
174
+ }
175
+ });
176
+ return { inline, postCredentials };
177
+ };
178
+
179
+ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, schema, registrationFieldsConfig = [], additionalFields = [], }) => {
81
180
  const [formData, setFormData] = React.useState({
82
181
  email: '',
83
182
  password: '',
84
183
  displayName: '',
85
184
  });
185
+ // Custom field values (separate from core AuthFormData)
186
+ const [customFieldValues, setCustomFieldValues] = React.useState({});
187
+ // Get schema-driven registration fields
188
+ const schemaFields = React.useMemo(() => {
189
+ if (!schema || !registrationFieldsConfig.length)
190
+ return { inline: [], postCredentials: [] };
191
+ const fields = getRegistrationFields(schema, registrationFieldsConfig);
192
+ return sortFieldsByPlacement(fields, registrationFieldsConfig);
193
+ }, [schema, registrationFieldsConfig]);
194
+ // Check if we have any additional fields to show
195
+ const hasSchemaFields = schemaFields.inline.length > 0 || schemaFields.postCredentials.length > 0;
196
+ const hasLegacyFields = additionalFields.length > 0 && !hasSchemaFields;
86
197
  const handleSubmit = async (e) => {
87
198
  e.preventDefault();
88
- await onSubmit(formData);
199
+ // Merge custom field values into accountData for registration
200
+ const submitData = {
201
+ ...formData,
202
+ accountData: mode === 'register'
203
+ ? { ...(formData.accountData || {}), customFields: customFieldValues }
204
+ : undefined,
205
+ };
206
+ await onSubmit(submitData);
89
207
  };
90
208
  const handleChange = (field, value) => {
91
209
  setFormData(prev => ({ ...prev, [field]: value }));
92
210
  };
211
+ const handleCustomFieldChange = (key, value) => {
212
+ setCustomFieldValues(prev => ({ ...prev, [key]: value }));
213
+ };
214
+ const renderSchemaField = (field) => (jsxRuntime.jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key));
215
+ // Legacy field renderer (for backward compatibility)
216
+ const renderLegacyField = (field) => (jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsxs("label", { htmlFor: field.name, className: "auth-label", children: [field.label, field.required && jsxRuntime.jsx("span", { style: { color: 'var(--auth-error-color, #ef4444)' }, children: " *" })] }), field.type === 'select' ? (jsxRuntime.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: [jsxRuntime.jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsxRuntime.jsx("option", { value: option, children: option }, option)))] })) : field.type === 'textarea' ? (jsxRuntime.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' } })) : (jsxRuntime.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));
93
217
  return (jsxRuntime.jsxs("form", { className: "auth-form", onSubmit: handleSubmit, children: [jsxRuntime.jsxs("div", { className: "auth-form-header", children: [jsxRuntime.jsx("h2", { className: "auth-form-title", children: mode === 'login' ? 'Sign in' : 'Create account' }), jsxRuntime.jsx("p", { className: "auth-form-subtitle", children: mode === 'login'
94
218
  ? 'Welcome back! Please enter your credentials.'
95
- : 'Get started by creating your account.' })] }), error && (jsxRuntime.jsxs("div", { className: "auth-error", role: "alert", children: [jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsxRuntime.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' && (jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsxRuntime.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" })] })), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsxRuntime.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" })] }), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsxRuntime.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) => (jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsxs("label", { htmlFor: field.name, className: "auth-label", children: [field.label, field.required && jsxRuntime.jsx("span", { style: { color: 'var(--auth-error-color, #ef4444)' }, children: " *" })] }), field.type === 'select' ? (jsxRuntime.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: [jsxRuntime.jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsxRuntime.jsx("option", { value: option, children: option }, option)))] })) : field.type === 'textarea' ? (jsxRuntime.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' } })) : (jsxRuntime.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' && (jsxRuntime.jsx("div", { className: "auth-form-footer", children: jsxRuntime.jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsxRuntime.jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsxRuntime.jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), jsxRuntime.jsxs("div", { className: "auth-divider", children: [jsxRuntime.jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsxRuntime.jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] })] }));
219
+ : 'Get started by creating your account.' })] }), error && (jsxRuntime.jsxs("div", { className: "auth-error", role: "alert", children: [jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsxRuntime.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' && (jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsxRuntime.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" })] })), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsxRuntime.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" })] }), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsxRuntime.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 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "auth-divider", style: { margin: '16px 0' }, children: jsxRuntime.jsx("span", { children: "Additional Information" }) }), schemaFields.postCredentials.map(renderSchemaField)] })), mode === 'login' && (jsxRuntime.jsx("div", { className: "auth-form-footer", children: jsxRuntime.jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsxRuntime.jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsxRuntime.jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), jsxRuntime.jsxs("div", { className: "auth-divider", children: [jsxRuntime.jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsxRuntime.jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] })] }));
96
220
  };
97
221
 
98
222
  const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, loading, }) => {
@@ -10660,6 +10784,8 @@ class AuthAPI {
10660
10784
  return smartlinks__namespace.authKit.login(this.clientId, email, password);
10661
10785
  }
10662
10786
  async register(data) {
10787
+ // Note: redirectUrl is not passed to register - verification email is sent separately
10788
+ // via sendEmailVerification() after registration for verify-then-* modes
10663
10789
  return smartlinks__namespace.authKit.register(this.clientId, {
10664
10790
  email: data.email,
10665
10791
  password: data.password,
@@ -11166,7 +11292,7 @@ const tokenStorage = {
11166
11292
  const storage = await getStorage();
11167
11293
  const authToken = {
11168
11294
  token,
11169
- expiresAt: expiresAt || Date.now() + 3600000, // Default 1 hour
11295
+ expiresAt: expiresAt || Date.now() + (7 * 24 * 60 * 60 * 1000), // Default 7 days (matches backend JWT)
11170
11296
  };
11171
11297
  await storage.setItem(TOKEN_KEY, authToken);
11172
11298
  },
@@ -11177,7 +11303,8 @@ const tokenStorage = {
11177
11303
  return null;
11178
11304
  // Check if token is expired
11179
11305
  if (authToken.expiresAt && authToken.expiresAt < Date.now()) {
11180
- await this.clearToken();
11306
+ console.log('[TokenStorage] Token expired at:', new Date(authToken.expiresAt).toISOString(), '- clearing all auth data');
11307
+ await this.clearAll(); // Clear ALL auth data to prevent orphaned state
11181
11308
  return null;
11182
11309
  }
11183
11310
  return authToken;
@@ -11257,6 +11384,9 @@ const tokenStorage = {
11257
11384
 
11258
11385
  const AuthContext = React.createContext(undefined);
11259
11386
  const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
11387
+ // Token refresh settings
11388
+ enableAutoRefresh = true, refreshThresholdPercent = 75, // Refresh when 75% of token lifetime has passed
11389
+ refreshCheckInterval = 60 * 1000, // Check every minute
11260
11390
  // Contact & Interaction features
11261
11391
  collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
11262
11392
  const [user, setUser] = React.useState(null);
@@ -11677,11 +11807,11 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11677
11807
  unsubscribe();
11678
11808
  };
11679
11809
  }, [proxyMode, notifyAuthStateChange]);
11680
- const login = React.useCallback(async (authToken, authUser, authAccountData, isNewUser) => {
11810
+ const login = React.useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt) => {
11681
11811
  try {
11682
11812
  // Only persist to storage in standalone mode
11683
11813
  if (!proxyMode) {
11684
- await tokenStorage.saveToken(authToken);
11814
+ await tokenStorage.saveToken(authToken, expiresAt);
11685
11815
  await tokenStorage.saveUser(authUser);
11686
11816
  if (authAccountData) {
11687
11817
  await tokenStorage.saveAccountData(authAccountData);
@@ -11764,9 +11894,67 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11764
11894
  const storedToken = await tokenStorage.getToken();
11765
11895
  return storedToken ? storedToken.token : null;
11766
11896
  }, [proxyMode, token]);
11897
+ // Get token with expiration info
11898
+ const getTokenInfo = React.useCallback(async () => {
11899
+ if (proxyMode) {
11900
+ // In proxy mode, we don't have expiration info
11901
+ if (token) {
11902
+ return { token, expiresAt: 0, expiresIn: 0 };
11903
+ }
11904
+ return null;
11905
+ }
11906
+ const storedToken = await tokenStorage.getToken();
11907
+ if (!storedToken?.token || !storedToken.expiresAt) {
11908
+ return null;
11909
+ }
11910
+ return {
11911
+ token: storedToken.token,
11912
+ expiresAt: storedToken.expiresAt,
11913
+ expiresIn: Math.max(0, storedToken.expiresAt - Date.now()),
11914
+ };
11915
+ }, [proxyMode, token]);
11916
+ // Refresh token - validates current token and extends session if backend supports it
11767
11917
  const refreshToken = React.useCallback(async () => {
11768
- throw new Error('Token refresh must be implemented via your backend API');
11769
- }, []);
11918
+ if (proxyMode) {
11919
+ console.log('[AuthContext] Proxy mode: token refresh handled by parent');
11920
+ throw new Error('Token refresh in proxy mode is handled by the parent application');
11921
+ }
11922
+ const storedToken = await tokenStorage.getToken();
11923
+ if (!storedToken?.token) {
11924
+ throw new Error('No token to refresh. Please login first.');
11925
+ }
11926
+ try {
11927
+ console.log('[AuthContext] Refreshing token...');
11928
+ // Verify current token is still valid
11929
+ const verifyResult = await smartlinks__namespace.auth.verifyToken(storedToken.token);
11930
+ if (!verifyResult.valid) {
11931
+ console.warn('[AuthContext] Token is no longer valid, clearing session');
11932
+ await logout();
11933
+ throw new Error('Token expired or invalid. Please login again.');
11934
+ }
11935
+ // Token is valid - extend its expiration locally
11936
+ // Backend JWT remains valid, we just update our local tracking
11937
+ const newExpiresAt = Date.now() + (7 * 24 * 60 * 60 * 1000); // 7 days from now
11938
+ await tokenStorage.saveToken(storedToken.token, newExpiresAt);
11939
+ console.log('[AuthContext] Token verified and expiration extended to:', new Date(newExpiresAt).toISOString());
11940
+ // Update verified state
11941
+ setIsVerified(true);
11942
+ pendingVerificationRef.current = false;
11943
+ notifyAuthStateChange('TOKEN_REFRESH', user, storedToken.token, accountData, accountInfo, true, contact, contactId);
11944
+ return storedToken.token;
11945
+ }
11946
+ catch (error) {
11947
+ console.error('[AuthContext] Token refresh failed:', error);
11948
+ // If it's a network error, don't logout
11949
+ if (isNetworkError(error)) {
11950
+ console.warn('[AuthContext] Network error during refresh, keeping session');
11951
+ throw error;
11952
+ }
11953
+ // Auth error - clear session
11954
+ await logout();
11955
+ throw new Error('Token refresh failed. Please login again.');
11956
+ }
11957
+ }, [proxyMode, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
11770
11958
  const getAccount = React.useCallback(async (forceRefresh = false) => {
11771
11959
  try {
11772
11960
  if (proxyMode) {
@@ -11874,6 +12062,54 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11874
12062
  window.removeEventListener('offline', handleOffline);
11875
12063
  };
11876
12064
  }, [proxyMode, token, user, retryVerification]);
12065
+ // Automatic background token refresh
12066
+ React.useEffect(() => {
12067
+ if (proxyMode || !enableAutoRefresh || !token || !user) {
12068
+ return;
12069
+ }
12070
+ console.log('[AuthContext] Setting up automatic token refresh (interval:', refreshCheckInterval, 'ms, threshold:', refreshThresholdPercent, '%)');
12071
+ const checkAndRefresh = async () => {
12072
+ try {
12073
+ const storedToken = await tokenStorage.getToken();
12074
+ if (!storedToken?.expiresAt) {
12075
+ console.log('[AuthContext] No token expiration info, skipping refresh check');
12076
+ return;
12077
+ }
12078
+ const now = Date.now();
12079
+ const tokenLifetime = storedToken.expiresAt - (storedToken.expiresAt - (7 * 24 * 60 * 60 * 1000)); // Assume 7-day lifetime
12080
+ const tokenAge = now - (storedToken.expiresAt - (7 * 24 * 60 * 60 * 1000));
12081
+ const percentUsed = (tokenAge / tokenLifetime) * 100;
12082
+ // Calculate time remaining
12083
+ const timeRemaining = storedToken.expiresAt - now;
12084
+ const hoursRemaining = Math.round(timeRemaining / (60 * 60 * 1000));
12085
+ if (percentUsed >= refreshThresholdPercent) {
12086
+ console.log(`[AuthContext] Token at ${Math.round(percentUsed)}% lifetime (${hoursRemaining}h remaining), refreshing...`);
12087
+ try {
12088
+ await refreshToken();
12089
+ console.log('[AuthContext] Automatic token refresh successful');
12090
+ }
12091
+ catch (refreshError) {
12092
+ console.warn('[AuthContext] Automatic token refresh failed:', refreshError);
12093
+ // Don't logout on refresh failure - user can still use the app until token actually expires
12094
+ }
12095
+ }
12096
+ else {
12097
+ console.log(`[AuthContext] Token at ${Math.round(percentUsed)}% lifetime (${hoursRemaining}h remaining), no refresh needed`);
12098
+ }
12099
+ }
12100
+ catch (error) {
12101
+ console.error('[AuthContext] Error checking token for refresh:', error);
12102
+ }
12103
+ };
12104
+ // Check immediately on mount
12105
+ checkAndRefresh();
12106
+ // Set up periodic check
12107
+ const intervalId = setInterval(checkAndRefresh, refreshCheckInterval);
12108
+ return () => {
12109
+ console.log('[AuthContext] Cleaning up automatic token refresh timer');
12110
+ clearInterval(intervalId);
12111
+ };
12112
+ }, [proxyMode, enableAutoRefresh, refreshCheckInterval, refreshThresholdPercent, token, user, refreshToken]);
11877
12113
  const value = {
11878
12114
  user,
11879
12115
  token,
@@ -11891,6 +12127,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11891
12127
  login,
11892
12128
  logout,
11893
12129
  getToken,
12130
+ getTokenInfo,
11894
12131
  refreshToken,
11895
12132
  getAccount,
11896
12133
  refreshAccount,
@@ -11908,6 +12145,14 @@ const useAuth = () => {
11908
12145
  return context;
11909
12146
  };
11910
12147
 
12148
+ // Helper to calculate expiration from AuthResponse
12149
+ const getExpirationFromResponse = (response) => {
12150
+ if (response.expiresAt)
12151
+ return response.expiresAt;
12152
+ if (response.expiresIn)
12153
+ return Date.now() + response.expiresIn;
12154
+ return undefined; // Will use 7-day default in tokenStorage
12155
+ };
11911
12156
  // Default Smartlinks Google OAuth Client ID (public - safe to expose)
11912
12157
  const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
11913
12158
  // Default auth UI configuration when no clientId is provided
@@ -11978,7 +12223,7 @@ const getFriendlyErrorMessage = (errorMessage) => {
11978
12223
  // Return original message if no pattern matches
11979
12224
  return errorMessage;
11980
12225
  };
11981
- 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, }) => {
12226
+ 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, }) => {
11982
12227
  const [mode, setMode] = React.useState(initialMode);
11983
12228
  const [loading, setLoading] = React.useState(false);
11984
12229
  const [error, setError] = React.useState();
@@ -11995,6 +12240,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11995
12240
  const [configLoading, setConfigLoading] = React.useState(!skipConfigFetch);
11996
12241
  const [showEmailForm, setShowEmailForm] = React.useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
11997
12242
  const [sdkReady, setSdkReady] = React.useState(false); // Track SDK initialization state
12243
+ const [contactSchema, setContactSchema] = React.useState(null); // Schema for registration fields
11998
12244
  const log = React.useMemo(() => createLoggerWrapper(logger), [logger]);
11999
12245
  const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
12000
12246
  const auth = useAuth();
@@ -12169,6 +12415,24 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12169
12415
  };
12170
12416
  fetchConfig();
12171
12417
  }, [apiEndpoint, clientId, customization, skipConfigFetch, sdkReady, proxyMode, log]);
12418
+ // Fetch contact schema for registration fields when collectionId is provided
12419
+ React.useEffect(() => {
12420
+ if (!collectionId || !sdkReady)
12421
+ return;
12422
+ const fetchSchema = async () => {
12423
+ try {
12424
+ console.log('[SmartlinksAuthUI] 📋 Fetching contact schema for collection:', collectionId);
12425
+ const schema = await smartlinks__namespace.contact.publicGetSchema(collectionId);
12426
+ console.log('[SmartlinksAuthUI] ✅ Schema loaded:', schema);
12427
+ setContactSchema(schema);
12428
+ }
12429
+ catch (err) {
12430
+ console.warn('[SmartlinksAuthUI] ⚠️ Failed to fetch schema (non-fatal):', err);
12431
+ // Non-fatal - registration will work without schema fields
12432
+ }
12433
+ };
12434
+ fetchSchema();
12435
+ }, [collectionId, sdkReady]);
12172
12436
  // Reset showEmailForm when mode changes away from login/register
12173
12437
  React.useEffect(() => {
12174
12438
  if (mode !== 'login' && mode !== 'register') {
@@ -12209,18 +12473,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12209
12473
  const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
12210
12474
  if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
12211
12475
  // Auto-login modes: Log the user in immediately if token is provided
12212
- auth.login(response.token, response.user, response.accountData, true);
12476
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12213
12477
  setAuthSuccess(true);
12214
12478
  setSuccessMessage('Email verified successfully! You are now logged in.');
12215
12479
  onAuthSuccess(response.token, response.user, response.accountData);
12216
12480
  // Clear the URL parameters
12217
12481
  const cleanUrl = window.location.href.split('?')[0];
12218
12482
  window.history.replaceState({}, document.title, cleanUrl);
12219
- // Redirect after a brief delay to show success message
12483
+ // For email verification deep links, redirect immediately if configured
12484
+ // (user came from an email link, so redirect is expected behavior)
12220
12485
  if (redirectUrl) {
12221
- setTimeout(() => {
12222
- window.location.href = redirectUrl;
12223
- }, 2000);
12486
+ window.location.href = redirectUrl;
12224
12487
  }
12225
12488
  }
12226
12489
  else {
@@ -12252,18 +12515,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12252
12515
  const response = await api.verifyMagicLink(token);
12253
12516
  // Auto-login with magic link if token is provided
12254
12517
  if (response.token) {
12255
- auth.login(response.token, response.user, response.accountData, true);
12518
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12256
12519
  setAuthSuccess(true);
12257
12520
  setSuccessMessage('Magic link verified! You are now logged in.');
12258
12521
  onAuthSuccess(response.token, response.user, response.accountData);
12259
12522
  // Clear the URL parameters
12260
12523
  const cleanUrl = window.location.href.split('?')[0];
12261
12524
  window.history.replaceState({}, document.title, cleanUrl);
12262
- // Redirect after a brief delay to show success message
12525
+ // For magic link deep links, redirect immediately if configured
12526
+ // (user came from an email link, so redirect is expected behavior)
12263
12527
  if (redirectUrl) {
12264
- setTimeout(() => {
12265
- window.location.href = redirectUrl;
12266
- }, 2000);
12528
+ window.location.href = redirectUrl;
12267
12529
  }
12268
12530
  }
12269
12531
  else {
@@ -12328,7 +12590,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12328
12590
  // Handle different verification modes
12329
12591
  if (verificationMode === 'immediate' && response.token) {
12330
12592
  // Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
12331
- auth.login(response.token, response.user, response.accountData, true);
12593
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12332
12594
  setAuthSuccess(true);
12333
12595
  const deadline = response.emailVerificationDeadline
12334
12596
  ? new Date(response.emailVerificationDeadline).toLocaleString()
@@ -12337,19 +12599,37 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12337
12599
  if (response.token) {
12338
12600
  onAuthSuccess(response.token, response.user, response.accountData);
12339
12601
  }
12340
- if (redirectUrl) {
12341
- setTimeout(() => {
12342
- window.location.href = redirectUrl;
12343
- }, 2000);
12344
- }
12602
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12345
12603
  }
12346
12604
  else if (verificationMode === 'verify-then-auto-login') {
12347
12605
  // Verify-then-auto-login mode: Don't log in yet, but will auto-login after email verification
12606
+ // Send the verification email since backend register may not send it automatically
12607
+ if (response.user?.uid && data.email) {
12608
+ try {
12609
+ await api.sendEmailVerification(response.user.uid, data.email, getRedirectUrl());
12610
+ log.log('Verification email sent after registration');
12611
+ }
12612
+ catch (verifyError) {
12613
+ log.warn('Failed to send verification email after registration:', verifyError);
12614
+ // Don't fail the registration, just log the warning
12615
+ }
12616
+ }
12348
12617
  setAuthSuccess(true);
12349
12618
  setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
12350
12619
  }
12351
12620
  else {
12352
12621
  // verify-then-manual-login mode: Traditional flow
12622
+ // Send the verification email since backend register may not send it automatically
12623
+ if (response.user?.uid && data.email) {
12624
+ try {
12625
+ await api.sendEmailVerification(response.user.uid, data.email, getRedirectUrl());
12626
+ log.log('Verification email sent after registration');
12627
+ }
12628
+ catch (verifyError) {
12629
+ log.warn('Failed to send verification email after registration:', verifyError);
12630
+ // Don't fail the registration, just log the warning
12631
+ }
12632
+ }
12353
12633
  setAuthSuccess(true);
12354
12634
  setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
12355
12635
  }
@@ -12364,15 +12644,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12364
12644
  if (response.requiresEmailVerification) {
12365
12645
  throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
12366
12646
  }
12367
- auth.login(response.token, response.user, response.accountData, false);
12647
+ auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response));
12368
12648
  setAuthSuccess(true);
12369
12649
  setSuccessMessage('Login successful!');
12370
12650
  onAuthSuccess(response.token, response.user, response.accountData);
12371
- if (redirectUrl) {
12372
- setTimeout(() => {
12373
- window.location.href = redirectUrl;
12374
- }, 2000);
12375
- }
12651
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12376
12652
  }
12377
12653
  else {
12378
12654
  throw new Error('Authentication failed - please verify your email before logging in.');
@@ -12525,7 +12801,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12525
12801
  });
12526
12802
  if (authResponse.token) {
12527
12803
  // Google OAuth can be login or signup - use isNewUser flag from backend if available
12528
- auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12804
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
12529
12805
  setAuthSuccess(true);
12530
12806
  setSuccessMessage('Google login successful!');
12531
12807
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12533,11 +12809,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12533
12809
  else {
12534
12810
  throw new Error('Authentication failed - no token received');
12535
12811
  }
12536
- if (redirectUrl) {
12537
- setTimeout(() => {
12538
- window.location.href = redirectUrl;
12539
- }, 2000);
12540
- }
12812
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12541
12813
  }
12542
12814
  catch (apiError) {
12543
12815
  const errorMessage = apiError instanceof Error ? apiError.message : 'Google login failed';
@@ -12575,7 +12847,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12575
12847
  const authResponse = await api.loginWithGoogle(idToken);
12576
12848
  if (authResponse.token) {
12577
12849
  // Google OAuth can be login or signup - use isNewUser flag from backend if available
12578
- auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12850
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
12579
12851
  setAuthSuccess(true);
12580
12852
  setSuccessMessage('Google login successful!');
12581
12853
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12583,11 +12855,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12583
12855
  else {
12584
12856
  throw new Error('Authentication failed - no token received');
12585
12857
  }
12586
- if (redirectUrl) {
12587
- setTimeout(() => {
12588
- window.location.href = redirectUrl;
12589
- }, 2000);
12590
- }
12858
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12591
12859
  setLoading(false);
12592
12860
  }
12593
12861
  catch (err) {
@@ -12637,7 +12905,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12637
12905
  // Update auth context with account data if token is provided
12638
12906
  if (response.token) {
12639
12907
  // Phone auth can be login or signup - use isNewUser flag from backend if available
12640
- auth.login(response.token, response.user, response.accountData, response.isNewUser);
12908
+ auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
12641
12909
  onAuthSuccess(response.token, response.user, response.accountData);
12642
12910
  if (redirectUrl) {
12643
12911
  window.location.href = redirectUrl;
@@ -12810,36 +13078,39 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12810
13078
  setShowResendVerification(false);
12811
13079
  setShowRequestNewReset(false);
12812
13080
  setError(undefined);
12813
- }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
13081
+ }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, schema: contactSchema, registrationFieldsConfig: config?.registrationFields, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
12814
13082
  })() })) })) : null }));
12815
13083
  };
12816
13084
 
12817
- const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', customization = {}, }) => {
13085
+ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, className = '', customization = {}, }) => {
12818
13086
  const auth = useAuth();
12819
13087
  const [loading, setLoading] = React.useState(false);
12820
13088
  const [profile, setProfile] = React.useState(null);
12821
13089
  const [error, setError] = React.useState();
12822
13090
  const [success, setSuccess] = React.useState();
13091
+ // Schema state
13092
+ const [schema, setSchema] = React.useState(null);
13093
+ const [schemaLoading, setSchemaLoading] = React.useState(false);
12823
13094
  // Track which section is being edited
12824
13095
  const [editingSection, setEditingSection] = React.useState(null);
12825
- // Profile form state
13096
+ // Form state for core fields
12826
13097
  const [displayName, setDisplayName] = React.useState('');
12827
- // Email change state
12828
13098
  const [newEmail, setNewEmail] = React.useState('');
12829
13099
  const [emailPassword, setEmailPassword] = React.useState('');
12830
- // Password change state
12831
13100
  const [currentPassword, setCurrentPassword] = React.useState('');
12832
13101
  const [newPassword, setNewPassword] = React.useState('');
12833
13102
  const [confirmPassword, setConfirmPassword] = React.useState('');
12834
- // Phone change state (reuses existing sendPhoneCode flow)
12835
13103
  const [newPhone, setNewPhone] = React.useState('');
12836
13104
  const [phoneCode, setPhoneCode] = React.useState('');
12837
13105
  const [phoneCodeSent, setPhoneCodeSent] = React.useState(false);
13106
+ // Custom fields form state
13107
+ const [customFieldValues, setCustomFieldValues] = React.useState({});
12838
13108
  // Account deletion state
12839
13109
  const [deletePassword, setDeletePassword] = React.useState('');
12840
13110
  const [deleteConfirmText, setDeleteConfirmText] = React.useState('');
12841
13111
  const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
12842
- const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, } = customization;
13112
+ const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, showCustomFields = true, // New: show schema-driven custom fields
13113
+ } = customization;
12843
13114
  // Reinitialize Smartlinks SDK when apiEndpoint changes
12844
13115
  React.useEffect(() => {
12845
13116
  if (apiEndpoint) {
@@ -12850,10 +13121,32 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12850
13121
  });
12851
13122
  }
12852
13123
  }, [apiEndpoint]);
12853
- // Load user profile on mount
13124
+ // Load schema when collectionId is available
13125
+ React.useEffect(() => {
13126
+ if (!collectionId)
13127
+ return;
13128
+ const loadSchema = async () => {
13129
+ setSchemaLoading(true);
13130
+ try {
13131
+ console.log('[AccountManagement] Loading schema for collection:', collectionId);
13132
+ const schemaResult = await smartlinks__namespace.contact.publicGetSchema(collectionId);
13133
+ console.log('[AccountManagement] Schema loaded:', schemaResult);
13134
+ setSchema(schemaResult);
13135
+ }
13136
+ catch (err) {
13137
+ console.warn('[AccountManagement] Failed to load schema:', err);
13138
+ // Non-fatal - component works without schema
13139
+ }
13140
+ finally {
13141
+ setSchemaLoading(false);
13142
+ }
13143
+ };
13144
+ loadSchema();
13145
+ }, [collectionId]);
13146
+ // Load user profile and contact data on mount
12854
13147
  React.useEffect(() => {
12855
13148
  loadProfile();
12856
- }, [clientId]);
13149
+ }, [clientId, collectionId]);
12857
13150
  const loadProfile = async () => {
12858
13151
  if (!auth.isAuthenticated) {
12859
13152
  setError('You must be logged in to manage your account');
@@ -12862,10 +13155,7 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12862
13155
  setLoading(true);
12863
13156
  setError(undefined);
12864
13157
  try {
12865
- // TODO: Backend implementation required
12866
- // Endpoint: GET /api/v1/authkit/:clientId/account/profile
12867
- // SDK method: smartlinks.authKit.getProfile(clientId)
12868
- // Temporary mock data for UI testing
13158
+ // Get base profile from auth context
12869
13159
  const profileData = {
12870
13160
  uid: auth.user?.uid || '',
12871
13161
  email: auth.user?.email,
@@ -12877,6 +13167,17 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12877
13167
  };
12878
13168
  setProfile(profileData);
12879
13169
  setDisplayName(profileData.displayName || '');
13170
+ // Load contact custom fields if collectionId provided
13171
+ if (collectionId && auth.contact) {
13172
+ setCustomFieldValues(auth.contact.customFields || {});
13173
+ }
13174
+ else if (collectionId) {
13175
+ // Try to fetch contact
13176
+ const contact = await auth.getContact?.();
13177
+ if (contact?.customFields) {
13178
+ setCustomFieldValues(contact.customFields);
13179
+ }
13180
+ }
12880
13181
  }
12881
13182
  catch (err) {
12882
13183
  const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
@@ -12887,28 +13188,39 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12887
13188
  setLoading(false);
12888
13189
  }
12889
13190
  };
13191
+ const cancelEdit = React.useCallback(() => {
13192
+ setEditingSection(null);
13193
+ setDisplayName(profile?.displayName || '');
13194
+ setNewEmail('');
13195
+ setEmailPassword('');
13196
+ setCurrentPassword('');
13197
+ setNewPassword('');
13198
+ setConfirmPassword('');
13199
+ setNewPhone('');
13200
+ setPhoneCode('');
13201
+ setPhoneCodeSent(false);
13202
+ setError(undefined);
13203
+ setSuccess(undefined);
13204
+ // Reset custom fields to original values
13205
+ if (auth.contact?.customFields) {
13206
+ setCustomFieldValues(auth.contact.customFields);
13207
+ }
13208
+ }, [profile, auth.contact]);
13209
+ // Get editable custom fields from schema
13210
+ const editableCustomFields = getEditableFields(schema).filter(f => !['email', 'displayName', 'phone', 'phoneNumber'].includes(f.key));
12890
13211
  const handleUpdateProfile = async (e) => {
12891
13212
  e.preventDefault();
12892
13213
  setLoading(true);
12893
13214
  setError(undefined);
12894
13215
  setSuccess(undefined);
12895
13216
  try {
12896
- // TODO: Backend implementation required
12897
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-profile
12898
- // SDK method: smartlinks.authKit.updateProfile(clientId, updateData)
12899
- setError('Backend API not yet implemented. See console for required endpoint.');
12900
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-profile');
12901
- console.log('Update data:', { displayName });
12902
- // Uncomment when backend is ready:
12903
- // const updateData: ProfileUpdateData = {
12904
- // displayName: displayName || undefined,
12905
- // photoURL: photoURL || undefined,
12906
- // };
12907
- // const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
12908
- // setProfile(updatedProfile);
12909
- // setSuccess('Profile updated successfully!');
12910
- // setEditingSection(null);
12911
- // onProfileUpdated?.(updatedProfile);
13217
+ await smartlinks__namespace.authKit.updateProfile(clientId, { displayName });
13218
+ setSuccess('Profile updated successfully!');
13219
+ setEditingSection(null);
13220
+ // Update local state
13221
+ if (profile) {
13222
+ setProfile({ ...profile, displayName });
13223
+ }
12912
13224
  }
12913
13225
  catch (err) {
12914
13226
  const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
@@ -12919,19 +13231,29 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12919
13231
  setLoading(false);
12920
13232
  }
12921
13233
  };
12922
- const cancelEdit = () => {
12923
- setEditingSection(null);
12924
- setDisplayName(profile?.displayName || '');
12925
- setNewEmail('');
12926
- setEmailPassword('');
12927
- setCurrentPassword('');
12928
- setNewPassword('');
12929
- setConfirmPassword('');
12930
- setNewPhone('');
12931
- setPhoneCode('');
12932
- setPhoneCodeSent(false);
13234
+ const handleUpdateCustomFields = async (e) => {
13235
+ e.preventDefault();
13236
+ if (!collectionId) {
13237
+ setError('Collection ID is required to update custom fields');
13238
+ return;
13239
+ }
13240
+ setLoading(true);
12933
13241
  setError(undefined);
12934
13242
  setSuccess(undefined);
13243
+ try {
13244
+ console.log('[AccountManagement] Updating custom fields:', customFieldValues);
13245
+ await auth.updateContactCustomFields?.(customFieldValues);
13246
+ setSuccess('Profile updated successfully!');
13247
+ setEditingSection(null);
13248
+ }
13249
+ catch (err) {
13250
+ const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
13251
+ setError(errorMessage);
13252
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
13253
+ }
13254
+ finally {
13255
+ setLoading(false);
13256
+ }
12935
13257
  };
12936
13258
  const handleChangeEmail = async (e) => {
12937
13259
  e.preventDefault();
@@ -12939,21 +13261,12 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12939
13261
  setError(undefined);
12940
13262
  setSuccess(undefined);
12941
13263
  try {
12942
- // TODO: Backend implementation required
12943
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-email
12944
- // SDK method: smartlinks.authKit.changeEmail(clientId, newEmail, password)
12945
- // Note: No verification flow for now - direct email update
12946
- setError('Backend API not yet implemented. See console for required endpoint.');
12947
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-email');
12948
- console.log('Data:', { newEmail });
12949
- // Uncomment when backend is ready:
12950
- // await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
12951
- // setSuccess('Email changed successfully!');
12952
- // setEditingSection(null);
12953
- // setNewEmail('');
12954
- // setEmailPassword('');
12955
- // onEmailChangeRequested?.();
12956
- // await loadProfile(); // Reload to show new email
13264
+ const redirectUrl = window.location.href;
13265
+ await smartlinks__namespace.authKit.changeEmail(clientId, newEmail, emailPassword, redirectUrl);
13266
+ setSuccess('Email change requested. Please check your new email for verification.');
13267
+ setEditingSection(null);
13268
+ setNewEmail('');
13269
+ setEmailPassword('');
12957
13270
  }
12958
13271
  catch (err) {
12959
13272
  const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
@@ -12978,20 +13291,12 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12978
13291
  setError(undefined);
12979
13292
  setSuccess(undefined);
12980
13293
  try {
12981
- // TODO: Backend implementation required
12982
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-password
12983
- // SDK method: smartlinks.authKit.changePassword(clientId, currentPassword, newPassword)
12984
- setError('Backend API not yet implemented. See console for required endpoint.');
12985
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-password');
12986
- console.log('Data: currentPassword and newPassword provided');
12987
- // Uncomment when backend is ready:
12988
- // await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
12989
- // setSuccess('Password changed successfully!');
12990
- // setEditingSection(null);
12991
- // setCurrentPassword('');
12992
- // setNewPassword('');
12993
- // setConfirmPassword('');
12994
- // onPasswordChanged?.();
13294
+ await smartlinks__namespace.authKit.changePassword(clientId, currentPassword, newPassword);
13295
+ setSuccess('Password changed successfully!');
13296
+ setEditingSection(null);
13297
+ setCurrentPassword('');
13298
+ setNewPassword('');
13299
+ setConfirmPassword('');
12995
13300
  }
12996
13301
  catch (err) {
12997
13302
  const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
@@ -13025,20 +13330,13 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13025
13330
  setError(undefined);
13026
13331
  setSuccess(undefined);
13027
13332
  try {
13028
- // TODO: Backend implementation required
13029
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-phone
13030
- // SDK method: smartlinks.authKit.updatePhone(clientId, phoneNumber, verificationCode)
13031
- setError('Backend API not yet implemented. See console for required endpoint.');
13032
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-phone');
13033
- console.log('Data:', { phoneNumber: newPhone, code: phoneCode });
13034
- // Uncomment when backend is ready:
13035
- // await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
13036
- // setSuccess('Phone number updated successfully!');
13037
- // setEditingSection(null);
13038
- // setNewPhone('');
13039
- // setPhoneCode('');
13040
- // setPhoneCodeSent(false);
13041
- // await loadProfile();
13333
+ await smartlinks__namespace.authKit.updatePhone(clientId, newPhone, phoneCode);
13334
+ setSuccess('Phone number updated successfully!');
13335
+ setEditingSection(null);
13336
+ setNewPhone('');
13337
+ setPhoneCode('');
13338
+ setPhoneCodeSent(false);
13339
+ await loadProfile();
13042
13340
  }
13043
13341
  catch (err) {
13044
13342
  const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
@@ -13061,19 +13359,9 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13061
13359
  setLoading(true);
13062
13360
  setError(undefined);
13063
13361
  try {
13064
- // TODO: Backend implementation required
13065
- // Endpoint: DELETE /api/v1/authkit/:clientId/account/delete
13066
- // SDK method: smartlinks.authKit.deleteAccount(clientId, password, confirmText)
13067
- // Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
13068
- setError('Backend API not yet implemented. See console for required endpoint.');
13069
- console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
13070
- console.log('Data: password and confirmText="DELETE" provided');
13071
- console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
13072
- // Uncomment when backend is ready:
13073
- // await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
13074
- // setSuccess('Account deleted successfully');
13075
- // onAccountDeleted?.();
13076
- // await auth.logout();
13362
+ await smartlinks__namespace.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
13363
+ setSuccess('Account deleted successfully');
13364
+ await auth.logout();
13077
13365
  }
13078
13366
  catch (err) {
13079
13367
  const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
@@ -13084,120 +13372,24 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13084
13372
  setLoading(false);
13085
13373
  }
13086
13374
  };
13375
+ const handleCustomFieldChange = (key, value) => {
13376
+ setCustomFieldValues(prev => ({ ...prev, [key]: value }));
13377
+ };
13087
13378
  if (!auth.isAuthenticated) {
13088
13379
  return (jsxRuntime.jsx("div", { className: `account-management ${className}`, children: jsxRuntime.jsx("p", { className: "text-muted-foreground", children: "Please log in to manage your account" }) }));
13089
13380
  }
13090
- if (loading && !profile) {
13381
+ if ((loading || schemaLoading) && !profile) {
13091
13382
  return (jsxRuntime.jsx("div", { className: `account-management ${className}`, children: jsxRuntime.jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
13092
13383
  }
13093
- return (jsxRuntime.jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsxRuntime.jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsxRuntime.jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Display Name" }), jsxRuntime.jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxRuntime.jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsxRuntime.jsx("div", { className: "form-group", children: jsxRuntime.jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Email Address" }), jsxRuntime.jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsxRuntime.jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsxRuntime.jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxRuntime.jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsxRuntime.jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsxRuntime.jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Password" }), jsxRuntime.jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxRuntime.jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsxRuntime.jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsxRuntime.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 })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsxRuntime.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 })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Phone Number" }), jsxRuntime.jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxRuntime.jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsxRuntime.jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsxRuntime.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 })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxRuntime.jsxs("section", { className: "account-section danger-zone", children: [jsxRuntime.jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsxRuntime.jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxRuntime.jsxs("div", { className: "delete-confirm", children: [jsxRuntime.jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsxRuntime.jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsxRuntime.jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsxRuntime.jsx("button", { type: "button", onClick: () => {
13384
+ return (jsxRuntime.jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsxRuntime.jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsxRuntime.jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Display Name" }), jsxRuntime.jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxRuntime.jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsxRuntime.jsx("div", { className: "form-group", children: jsxRuntime.jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showCustomFields && collectionId && editableCustomFields.length > 0 && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsx("h3", { className: "section-title", style: { fontSize: '0.9rem', fontWeight: 600, marginBottom: '12px' }, children: "Additional Information" }), editingSection !== 'customFields' ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [editableCustomFields.map((field) => (jsxRuntime.jsx("div", { className: "account-field", children: jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: field.label }), jsxRuntime.jsx("div", { className: "field-value", children: customFieldValues[field.key] !== undefined && customFieldValues[field.key] !== ''
13385
+ ? String(customFieldValues[field.key])
13386
+ : 'Not set' })] }) }, field.key))), jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('customFields'), className: "auth-button button-secondary", style: { marginTop: '8px' }, children: "Edit Information" })] })) : (jsxRuntime.jsxs("form", { onSubmit: handleUpdateCustomFields, className: "edit-form", children: [editableCustomFields.map((field) => (jsxRuntime.jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key))), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Email Address" }), jsxRuntime.jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsxRuntime.jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsxRuntime.jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxRuntime.jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsxRuntime.jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsxRuntime.jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Password" }), jsxRuntime.jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxRuntime.jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsxRuntime.jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsxRuntime.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 })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsxRuntime.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 })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Phone Number" }), jsxRuntime.jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxRuntime.jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsxRuntime.jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsxRuntime.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 })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxRuntime.jsxs("section", { className: "account-section danger-zone", children: [jsxRuntime.jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsxRuntime.jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxRuntime.jsxs("div", { className: "delete-confirm", children: [jsxRuntime.jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsxRuntime.jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsxRuntime.jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsxRuntime.jsx("button", { type: "button", onClick: () => {
13094
13387
  setShowDeleteConfirm(false);
13095
13388
  setDeletePassword('');
13096
13389
  setDeleteConfirmText('');
13097
13390
  }, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
13098
13391
  };
13099
13392
 
13100
- const SmartlinksClaimUI = (props) => {
13101
- // Destructure AFTER logging raw props to debug proxyMode issue
13102
- const { apiEndpoint, clientId, clientName, collectionId, productId, proofId, onClaimSuccess, onClaimError, additionalFields = [], theme = 'light', className = '', minimal = false, proxyMode = false, customization = {}, } = props;
13103
- // Debug logging for proxyMode - log RAW props first
13104
- console.log('[SmartlinksClaimUI] 🔍 RAW props received:', props);
13105
- console.log('[SmartlinksClaimUI] 🔍 props.proxyMode value:', props.proxyMode);
13106
- console.log('[SmartlinksClaimUI] 🔍 typeof props.proxyMode:', typeof props.proxyMode);
13107
- console.log('[SmartlinksClaimUI] 🎯 Destructured proxyMode:', proxyMode);
13108
- const auth = useAuth();
13109
- const [claimStep, setClaimStep] = React.useState(auth.isAuthenticated ? 'questions' : 'auth');
13110
- const [claimData, setClaimData] = React.useState({});
13111
- const [error, setError] = React.useState();
13112
- const [loading, setLoading] = React.useState(false);
13113
- const handleAuthSuccess = (token, user, accountData) => {
13114
- // Authentication successful
13115
- auth.login(token, user, accountData);
13116
- // If no additional questions, proceed directly to claim
13117
- if (additionalFields.length === 0) {
13118
- executeClaim(user);
13119
- }
13120
- else {
13121
- setClaimStep('questions');
13122
- }
13123
- };
13124
- const handleQuestionSubmit = async (e) => {
13125
- e.preventDefault();
13126
- // Validate required fields
13127
- const missingFields = additionalFields
13128
- .filter(field => field.required && !claimData[field.name])
13129
- .map(field => field.label);
13130
- if (missingFields.length > 0) {
13131
- setError(`Please fill in: ${missingFields.join(', ')}`);
13132
- return;
13133
- }
13134
- // Execute claim with collected data
13135
- if (auth.user) {
13136
- executeClaim(auth.user);
13137
- }
13138
- };
13139
- const executeClaim = async (user) => {
13140
- setClaimStep('claiming');
13141
- setLoading(true);
13142
- setError(undefined);
13143
- try {
13144
- // Create attestation to claim the proof
13145
- const response = await smartlinks__namespace.attestation.create(collectionId, productId, proofId, {
13146
- public: {
13147
- claimed: true,
13148
- claimedAt: new Date().toISOString(),
13149
- claimedBy: user.uid,
13150
- ...claimData,
13151
- },
13152
- private: {},
13153
- proof: {},
13154
- });
13155
- setClaimStep('success');
13156
- // Call success callback
13157
- onClaimSuccess({
13158
- proofId,
13159
- user,
13160
- claimData,
13161
- attestationId: response.id,
13162
- });
13163
- }
13164
- catch (err) {
13165
- console.error('Claim error:', err);
13166
- const errorMessage = err instanceof Error ? err.message : 'Failed to claim proof';
13167
- setError(errorMessage);
13168
- onClaimError?.(err instanceof Error ? err : new Error(errorMessage));
13169
- setClaimStep(additionalFields.length > 0 ? 'questions' : 'auth');
13170
- }
13171
- finally {
13172
- setLoading(false);
13173
- }
13174
- };
13175
- const handleFieldChange = (fieldName, value) => {
13176
- setClaimData(prev => ({
13177
- ...prev,
13178
- [fieldName]: value,
13179
- }));
13180
- };
13181
- // Render authentication step
13182
- if (claimStep === 'auth') {
13183
- console.log('[SmartlinksClaimUI] 🔑 Rendering auth step with proxyMode:', proxyMode);
13184
- return (jsxRuntime.jsx("div", { className: className, children: jsxRuntime.jsx(SmartlinksAuthUI, { apiEndpoint: apiEndpoint, clientId: clientId, clientName: clientName, onAuthSuccess: handleAuthSuccess, onAuthError: onClaimError, theme: theme, minimal: minimal, proxyMode: proxyMode, customization: customization.authConfig }) }));
13185
- }
13186
- // Render additional questions step
13187
- if (claimStep === 'questions') {
13188
- return (jsxRuntime.jsxs("div", { className: `claim-questions ${className}`, children: [jsxRuntime.jsxs("div", { className: "claim-header mb-6", children: [jsxRuntime.jsx("h2", { className: "text-2xl font-bold mb-2", children: customization.claimTitle || 'Complete Your Claim' }), customization.claimDescription && (jsxRuntime.jsx("p", { className: "text-muted-foreground", children: customization.claimDescription }))] }), error && (jsxRuntime.jsx("div", { className: "claim-error bg-destructive/10 text-destructive px-4 py-3 rounded-md mb-4", children: error })), jsxRuntime.jsxs("form", { onSubmit: handleQuestionSubmit, className: "claim-form space-y-4", children: [additionalFields.map((field) => (jsxRuntime.jsxs("div", { className: "claim-field", children: [jsxRuntime.jsxs("label", { htmlFor: field.name, className: "block text-sm font-medium mb-2", children: [field.label, field.required && jsxRuntime.jsx("span", { className: "text-destructive ml-1", children: "*" })] }), field.type === 'textarea' ? (jsxRuntime.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' ? (jsxRuntime.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: [jsxRuntime.jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsxRuntime.jsx("option", { value: option, children: option }, option)))] })) : (jsxRuntime.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))), jsxRuntime.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' })] })] }));
13189
- }
13190
- // Render claiming step (loading state)
13191
- if (claimStep === 'claiming') {
13192
- return (jsxRuntime.jsxs("div", { className: `claim-loading ${className} flex flex-col items-center justify-center py-12`, children: [jsxRuntime.jsx("div", { className: "claim-spinner w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mb-4" }), jsxRuntime.jsx("p", { className: "text-muted-foreground", children: "Claiming your product..." })] }));
13193
- }
13194
- // Render success step
13195
- if (claimStep === 'success') {
13196
- return (jsxRuntime.jsxs("div", { className: `claim-success ${className} text-center py-12`, children: [jsxRuntime.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" }), jsxRuntime.jsx("h2", { className: "text-2xl font-bold mb-2", children: "Claim Successful!" }), jsxRuntime.jsx("p", { className: "text-muted-foreground", children: customization.successMessage || 'Your product has been successfully claimed and registered to your account.' })] }));
13197
- }
13198
- return null;
13199
- };
13200
-
13201
13393
  const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
13202
13394
  const { isAuthenticated, isLoading } = useAuth();
13203
13395
  // Show loading state
@@ -13252,8 +13444,11 @@ exports.AuthProvider = AuthProvider;
13252
13444
  exports.AuthUIPreview = AuthUIPreview;
13253
13445
  exports.FirebaseAuthUI = SmartlinksAuthUI;
13254
13446
  exports.ProtectedRoute = ProtectedRoute;
13447
+ exports.SchemaFieldRenderer = SchemaFieldRenderer;
13255
13448
  exports.SmartlinksAuthUI = SmartlinksAuthUI;
13256
- exports.SmartlinksClaimUI = SmartlinksClaimUI;
13449
+ exports.getEditableFields = getEditableFields;
13450
+ exports.getRegistrationFields = getRegistrationFields;
13451
+ exports.sortFieldsByPlacement = sortFieldsByPlacement;
13257
13452
  exports.tokenStorage = tokenStorage;
13258
13453
  exports.useAuth = useAuth;
13259
13454
  //# sourceMappingURL=index.js.map