@proveanything/smartlinks-auth-ui 0.1.27 → 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
@@ -78,22 +78,145 @@ const AuthContainer = ({ children, theme = 'light', className = '', config, mini
78
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" })] }))] }) }));
79
79
  };
80
80
 
81
- 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 = [], }) => {
82
180
  const [formData, setFormData] = React.useState({
83
181
  email: '',
84
182
  password: '',
85
183
  displayName: '',
86
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;
87
197
  const handleSubmit = async (e) => {
88
198
  e.preventDefault();
89
- 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);
90
207
  };
91
208
  const handleChange = (field, value) => {
92
209
  setFormData(prev => ({ ...prev, [field]: value }));
93
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));
94
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'
95
218
  ? 'Welcome back! Please enter your credentials.'
96
- : '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' })] })] }));
97
220
  };
98
221
 
99
222
  const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, loading, }) => {
@@ -10661,6 +10784,8 @@ class AuthAPI {
10661
10784
  return smartlinks__namespace.authKit.login(this.clientId, email, password);
10662
10785
  }
10663
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
10664
10789
  return smartlinks__namespace.authKit.register(this.clientId, {
10665
10790
  email: data.email,
10666
10791
  password: data.password,
@@ -11167,7 +11292,7 @@ const tokenStorage = {
11167
11292
  const storage = await getStorage();
11168
11293
  const authToken = {
11169
11294
  token,
11170
- expiresAt: expiresAt || Date.now() + 3600000, // Default 1 hour
11295
+ expiresAt: expiresAt || Date.now() + (7 * 24 * 60 * 60 * 1000), // Default 7 days (matches backend JWT)
11171
11296
  };
11172
11297
  await storage.setItem(TOKEN_KEY, authToken);
11173
11298
  },
@@ -11178,7 +11303,8 @@ const tokenStorage = {
11178
11303
  return null;
11179
11304
  // Check if token is expired
11180
11305
  if (authToken.expiresAt && authToken.expiresAt < Date.now()) {
11181
- 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
11182
11308
  return null;
11183
11309
  }
11184
11310
  return authToken;
@@ -11258,6 +11384,9 @@ const tokenStorage = {
11258
11384
 
11259
11385
  const AuthContext = React.createContext(undefined);
11260
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
11261
11390
  // Contact & Interaction features
11262
11391
  collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
11263
11392
  const [user, setUser] = React.useState(null);
@@ -11678,11 +11807,11 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11678
11807
  unsubscribe();
11679
11808
  };
11680
11809
  }, [proxyMode, notifyAuthStateChange]);
11681
- const login = React.useCallback(async (authToken, authUser, authAccountData, isNewUser) => {
11810
+ const login = React.useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt) => {
11682
11811
  try {
11683
11812
  // Only persist to storage in standalone mode
11684
11813
  if (!proxyMode) {
11685
- await tokenStorage.saveToken(authToken);
11814
+ await tokenStorage.saveToken(authToken, expiresAt);
11686
11815
  await tokenStorage.saveUser(authUser);
11687
11816
  if (authAccountData) {
11688
11817
  await tokenStorage.saveAccountData(authAccountData);
@@ -11765,9 +11894,67 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11765
11894
  const storedToken = await tokenStorage.getToken();
11766
11895
  return storedToken ? storedToken.token : null;
11767
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
11768
11917
  const refreshToken = React.useCallback(async () => {
11769
- throw new Error('Token refresh must be implemented via your backend API');
11770
- }, []);
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]);
11771
11958
  const getAccount = React.useCallback(async (forceRefresh = false) => {
11772
11959
  try {
11773
11960
  if (proxyMode) {
@@ -11875,6 +12062,54 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11875
12062
  window.removeEventListener('offline', handleOffline);
11876
12063
  };
11877
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]);
11878
12113
  const value = {
11879
12114
  user,
11880
12115
  token,
@@ -11892,6 +12127,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11892
12127
  login,
11893
12128
  logout,
11894
12129
  getToken,
12130
+ getTokenInfo,
11895
12131
  refreshToken,
11896
12132
  getAccount,
11897
12133
  refreshAccount,
@@ -11909,6 +12145,14 @@ const useAuth = () => {
11909
12145
  return context;
11910
12146
  };
11911
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
+ };
11912
12156
  // Default Smartlinks Google OAuth Client ID (public - safe to expose)
11913
12157
  const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
11914
12158
  // Default auth UI configuration when no clientId is provided
@@ -11979,7 +12223,7 @@ const getFriendlyErrorMessage = (errorMessage) => {
11979
12223
  // Return original message if no pattern matches
11980
12224
  return errorMessage;
11981
12225
  };
11982
- 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, }) => {
11983
12227
  const [mode, setMode] = React.useState(initialMode);
11984
12228
  const [loading, setLoading] = React.useState(false);
11985
12229
  const [error, setError] = React.useState();
@@ -11996,6 +12240,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11996
12240
  const [configLoading, setConfigLoading] = React.useState(!skipConfigFetch);
11997
12241
  const [showEmailForm, setShowEmailForm] = React.useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
11998
12242
  const [sdkReady, setSdkReady] = React.useState(false); // Track SDK initialization state
12243
+ const [contactSchema, setContactSchema] = React.useState(null); // Schema for registration fields
11999
12244
  const log = React.useMemo(() => createLoggerWrapper(logger), [logger]);
12000
12245
  const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
12001
12246
  const auth = useAuth();
@@ -12170,6 +12415,24 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12170
12415
  };
12171
12416
  fetchConfig();
12172
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]);
12173
12436
  // Reset showEmailForm when mode changes away from login/register
12174
12437
  React.useEffect(() => {
12175
12438
  if (mode !== 'login' && mode !== 'register') {
@@ -12210,18 +12473,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12210
12473
  const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
12211
12474
  if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
12212
12475
  // Auto-login modes: Log the user in immediately if token is provided
12213
- auth.login(response.token, response.user, response.accountData, true);
12476
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12214
12477
  setAuthSuccess(true);
12215
12478
  setSuccessMessage('Email verified successfully! You are now logged in.');
12216
12479
  onAuthSuccess(response.token, response.user, response.accountData);
12217
12480
  // Clear the URL parameters
12218
12481
  const cleanUrl = window.location.href.split('?')[0];
12219
12482
  window.history.replaceState({}, document.title, cleanUrl);
12220
- // 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)
12221
12485
  if (redirectUrl) {
12222
- setTimeout(() => {
12223
- window.location.href = redirectUrl;
12224
- }, 2000);
12486
+ window.location.href = redirectUrl;
12225
12487
  }
12226
12488
  }
12227
12489
  else {
@@ -12253,18 +12515,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12253
12515
  const response = await api.verifyMagicLink(token);
12254
12516
  // Auto-login with magic link if token is provided
12255
12517
  if (response.token) {
12256
- auth.login(response.token, response.user, response.accountData, true);
12518
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12257
12519
  setAuthSuccess(true);
12258
12520
  setSuccessMessage('Magic link verified! You are now logged in.');
12259
12521
  onAuthSuccess(response.token, response.user, response.accountData);
12260
12522
  // Clear the URL parameters
12261
12523
  const cleanUrl = window.location.href.split('?')[0];
12262
12524
  window.history.replaceState({}, document.title, cleanUrl);
12263
- // 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)
12264
12527
  if (redirectUrl) {
12265
- setTimeout(() => {
12266
- window.location.href = redirectUrl;
12267
- }, 2000);
12528
+ window.location.href = redirectUrl;
12268
12529
  }
12269
12530
  }
12270
12531
  else {
@@ -12329,7 +12590,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12329
12590
  // Handle different verification modes
12330
12591
  if (verificationMode === 'immediate' && response.token) {
12331
12592
  // Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
12332
- auth.login(response.token, response.user, response.accountData, true);
12593
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12333
12594
  setAuthSuccess(true);
12334
12595
  const deadline = response.emailVerificationDeadline
12335
12596
  ? new Date(response.emailVerificationDeadline).toLocaleString()
@@ -12338,19 +12599,37 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12338
12599
  if (response.token) {
12339
12600
  onAuthSuccess(response.token, response.user, response.accountData);
12340
12601
  }
12341
- if (redirectUrl) {
12342
- setTimeout(() => {
12343
- window.location.href = redirectUrl;
12344
- }, 2000);
12345
- }
12602
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12346
12603
  }
12347
12604
  else if (verificationMode === 'verify-then-auto-login') {
12348
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
+ }
12349
12617
  setAuthSuccess(true);
12350
12618
  setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
12351
12619
  }
12352
12620
  else {
12353
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
+ }
12354
12633
  setAuthSuccess(true);
12355
12634
  setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
12356
12635
  }
@@ -12365,15 +12644,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12365
12644
  if (response.requiresEmailVerification) {
12366
12645
  throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
12367
12646
  }
12368
- auth.login(response.token, response.user, response.accountData, false);
12647
+ auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response));
12369
12648
  setAuthSuccess(true);
12370
12649
  setSuccessMessage('Login successful!');
12371
12650
  onAuthSuccess(response.token, response.user, response.accountData);
12372
- if (redirectUrl) {
12373
- setTimeout(() => {
12374
- window.location.href = redirectUrl;
12375
- }, 2000);
12376
- }
12651
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12377
12652
  }
12378
12653
  else {
12379
12654
  throw new Error('Authentication failed - please verify your email before logging in.');
@@ -12526,7 +12801,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12526
12801
  });
12527
12802
  if (authResponse.token) {
12528
12803
  // Google OAuth can be login or signup - use isNewUser flag from backend if available
12529
- auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12804
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
12530
12805
  setAuthSuccess(true);
12531
12806
  setSuccessMessage('Google login successful!');
12532
12807
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12534,11 +12809,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12534
12809
  else {
12535
12810
  throw new Error('Authentication failed - no token received');
12536
12811
  }
12537
- if (redirectUrl) {
12538
- setTimeout(() => {
12539
- window.location.href = redirectUrl;
12540
- }, 2000);
12541
- }
12812
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12542
12813
  }
12543
12814
  catch (apiError) {
12544
12815
  const errorMessage = apiError instanceof Error ? apiError.message : 'Google login failed';
@@ -12576,7 +12847,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12576
12847
  const authResponse = await api.loginWithGoogle(idToken);
12577
12848
  if (authResponse.token) {
12578
12849
  // Google OAuth can be login or signup - use isNewUser flag from backend if available
12579
- auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12850
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
12580
12851
  setAuthSuccess(true);
12581
12852
  setSuccessMessage('Google login successful!');
12582
12853
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12584,11 +12855,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12584
12855
  else {
12585
12856
  throw new Error('Authentication failed - no token received');
12586
12857
  }
12587
- if (redirectUrl) {
12588
- setTimeout(() => {
12589
- window.location.href = redirectUrl;
12590
- }, 2000);
12591
- }
12858
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12592
12859
  setLoading(false);
12593
12860
  }
12594
12861
  catch (err) {
@@ -12638,7 +12905,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12638
12905
  // Update auth context with account data if token is provided
12639
12906
  if (response.token) {
12640
12907
  // Phone auth can be login or signup - use isNewUser flag from backend if available
12641
- auth.login(response.token, response.user, response.accountData, response.isNewUser);
12908
+ auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
12642
12909
  onAuthSuccess(response.token, response.user, response.accountData);
12643
12910
  if (redirectUrl) {
12644
12911
  window.location.href = redirectUrl;
@@ -12811,36 +13078,39 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12811
13078
  setShowResendVerification(false);
12812
13079
  setShowRequestNewReset(false);
12813
13080
  setError(undefined);
12814
- }, 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 }))] }));
12815
13082
  })() })) })) : null }));
12816
13083
  };
12817
13084
 
12818
- const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', customization = {}, }) => {
13085
+ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, className = '', customization = {}, }) => {
12819
13086
  const auth = useAuth();
12820
13087
  const [loading, setLoading] = React.useState(false);
12821
13088
  const [profile, setProfile] = React.useState(null);
12822
13089
  const [error, setError] = React.useState();
12823
13090
  const [success, setSuccess] = React.useState();
13091
+ // Schema state
13092
+ const [schema, setSchema] = React.useState(null);
13093
+ const [schemaLoading, setSchemaLoading] = React.useState(false);
12824
13094
  // Track which section is being edited
12825
13095
  const [editingSection, setEditingSection] = React.useState(null);
12826
- // Profile form state
13096
+ // Form state for core fields
12827
13097
  const [displayName, setDisplayName] = React.useState('');
12828
- // Email change state
12829
13098
  const [newEmail, setNewEmail] = React.useState('');
12830
13099
  const [emailPassword, setEmailPassword] = React.useState('');
12831
- // Password change state
12832
13100
  const [currentPassword, setCurrentPassword] = React.useState('');
12833
13101
  const [newPassword, setNewPassword] = React.useState('');
12834
13102
  const [confirmPassword, setConfirmPassword] = React.useState('');
12835
- // Phone change state (reuses existing sendPhoneCode flow)
12836
13103
  const [newPhone, setNewPhone] = React.useState('');
12837
13104
  const [phoneCode, setPhoneCode] = React.useState('');
12838
13105
  const [phoneCodeSent, setPhoneCodeSent] = React.useState(false);
13106
+ // Custom fields form state
13107
+ const [customFieldValues, setCustomFieldValues] = React.useState({});
12839
13108
  // Account deletion state
12840
13109
  const [deletePassword, setDeletePassword] = React.useState('');
12841
13110
  const [deleteConfirmText, setDeleteConfirmText] = React.useState('');
12842
13111
  const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
12843
- 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;
12844
13114
  // Reinitialize Smartlinks SDK when apiEndpoint changes
12845
13115
  React.useEffect(() => {
12846
13116
  if (apiEndpoint) {
@@ -12851,10 +13121,32 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12851
13121
  });
12852
13122
  }
12853
13123
  }, [apiEndpoint]);
12854
- // 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
12855
13147
  React.useEffect(() => {
12856
13148
  loadProfile();
12857
- }, [clientId]);
13149
+ }, [clientId, collectionId]);
12858
13150
  const loadProfile = async () => {
12859
13151
  if (!auth.isAuthenticated) {
12860
13152
  setError('You must be logged in to manage your account');
@@ -12863,10 +13155,7 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12863
13155
  setLoading(true);
12864
13156
  setError(undefined);
12865
13157
  try {
12866
- // TODO: Backend implementation required
12867
- // Endpoint: GET /api/v1/authkit/:clientId/account/profile
12868
- // SDK method: smartlinks.authKit.getProfile(clientId)
12869
- // Temporary mock data for UI testing
13158
+ // Get base profile from auth context
12870
13159
  const profileData = {
12871
13160
  uid: auth.user?.uid || '',
12872
13161
  email: auth.user?.email,
@@ -12878,6 +13167,17 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12878
13167
  };
12879
13168
  setProfile(profileData);
12880
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
+ }
12881
13181
  }
12882
13182
  catch (err) {
12883
13183
  const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
@@ -12888,28 +13188,39 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12888
13188
  setLoading(false);
12889
13189
  }
12890
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));
12891
13211
  const handleUpdateProfile = async (e) => {
12892
13212
  e.preventDefault();
12893
13213
  setLoading(true);
12894
13214
  setError(undefined);
12895
13215
  setSuccess(undefined);
12896
13216
  try {
12897
- // TODO: Backend implementation required
12898
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-profile
12899
- // SDK method: smartlinks.authKit.updateProfile(clientId, updateData)
12900
- setError('Backend API not yet implemented. See console for required endpoint.');
12901
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-profile');
12902
- console.log('Update data:', { displayName });
12903
- // Uncomment when backend is ready:
12904
- // const updateData: ProfileUpdateData = {
12905
- // displayName: displayName || undefined,
12906
- // photoURL: photoURL || undefined,
12907
- // };
12908
- // const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
12909
- // setProfile(updatedProfile);
12910
- // setSuccess('Profile updated successfully!');
12911
- // setEditingSection(null);
12912
- // 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
+ }
12913
13224
  }
12914
13225
  catch (err) {
12915
13226
  const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
@@ -12920,19 +13231,29 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12920
13231
  setLoading(false);
12921
13232
  }
12922
13233
  };
12923
- const cancelEdit = () => {
12924
- setEditingSection(null);
12925
- setDisplayName(profile?.displayName || '');
12926
- setNewEmail('');
12927
- setEmailPassword('');
12928
- setCurrentPassword('');
12929
- setNewPassword('');
12930
- setConfirmPassword('');
12931
- setNewPhone('');
12932
- setPhoneCode('');
12933
- 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);
12934
13241
  setError(undefined);
12935
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
+ }
12936
13257
  };
12937
13258
  const handleChangeEmail = async (e) => {
12938
13259
  e.preventDefault();
@@ -12940,21 +13261,12 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12940
13261
  setError(undefined);
12941
13262
  setSuccess(undefined);
12942
13263
  try {
12943
- // TODO: Backend implementation required
12944
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-email
12945
- // SDK method: smartlinks.authKit.changeEmail(clientId, newEmail, password)
12946
- // Note: No verification flow for now - direct email update
12947
- setError('Backend API not yet implemented. See console for required endpoint.');
12948
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-email');
12949
- console.log('Data:', { newEmail });
12950
- // Uncomment when backend is ready:
12951
- // await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
12952
- // setSuccess('Email changed successfully!');
12953
- // setEditingSection(null);
12954
- // setNewEmail('');
12955
- // setEmailPassword('');
12956
- // onEmailChangeRequested?.();
12957
- // 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('');
12958
13270
  }
12959
13271
  catch (err) {
12960
13272
  const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
@@ -12979,20 +13291,12 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12979
13291
  setError(undefined);
12980
13292
  setSuccess(undefined);
12981
13293
  try {
12982
- // TODO: Backend implementation required
12983
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-password
12984
- // SDK method: smartlinks.authKit.changePassword(clientId, currentPassword, newPassword)
12985
- setError('Backend API not yet implemented. See console for required endpoint.');
12986
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-password');
12987
- console.log('Data: currentPassword and newPassword provided');
12988
- // Uncomment when backend is ready:
12989
- // await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
12990
- // setSuccess('Password changed successfully!');
12991
- // setEditingSection(null);
12992
- // setCurrentPassword('');
12993
- // setNewPassword('');
12994
- // setConfirmPassword('');
12995
- // onPasswordChanged?.();
13294
+ await smartlinks__namespace.authKit.changePassword(clientId, currentPassword, newPassword);
13295
+ setSuccess('Password changed successfully!');
13296
+ setEditingSection(null);
13297
+ setCurrentPassword('');
13298
+ setNewPassword('');
13299
+ setConfirmPassword('');
12996
13300
  }
12997
13301
  catch (err) {
12998
13302
  const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
@@ -13026,20 +13330,13 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13026
13330
  setError(undefined);
13027
13331
  setSuccess(undefined);
13028
13332
  try {
13029
- // TODO: Backend implementation required
13030
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-phone
13031
- // SDK method: smartlinks.authKit.updatePhone(clientId, phoneNumber, verificationCode)
13032
- setError('Backend API not yet implemented. See console for required endpoint.');
13033
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-phone');
13034
- console.log('Data:', { phoneNumber: newPhone, code: phoneCode });
13035
- // Uncomment when backend is ready:
13036
- // await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
13037
- // setSuccess('Phone number updated successfully!');
13038
- // setEditingSection(null);
13039
- // setNewPhone('');
13040
- // setPhoneCode('');
13041
- // setPhoneCodeSent(false);
13042
- // 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();
13043
13340
  }
13044
13341
  catch (err) {
13045
13342
  const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
@@ -13062,19 +13359,9 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13062
13359
  setLoading(true);
13063
13360
  setError(undefined);
13064
13361
  try {
13065
- // TODO: Backend implementation required
13066
- // Endpoint: DELETE /api/v1/authkit/:clientId/account/delete
13067
- // SDK method: smartlinks.authKit.deleteAccount(clientId, password, confirmText)
13068
- // Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
13069
- setError('Backend API not yet implemented. See console for required endpoint.');
13070
- console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
13071
- console.log('Data: password and confirmText="DELETE" provided');
13072
- console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
13073
- // Uncomment when backend is ready:
13074
- // await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
13075
- // setSuccess('Account deleted successfully');
13076
- // onAccountDeleted?.();
13077
- // await auth.logout();
13362
+ await smartlinks__namespace.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
13363
+ setSuccess('Account deleted successfully');
13364
+ await auth.logout();
13078
13365
  }
13079
13366
  catch (err) {
13080
13367
  const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
@@ -13085,120 +13372,24 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13085
13372
  setLoading(false);
13086
13373
  }
13087
13374
  };
13375
+ const handleCustomFieldChange = (key, value) => {
13376
+ setCustomFieldValues(prev => ({ ...prev, [key]: value }));
13377
+ };
13088
13378
  if (!auth.isAuthenticated) {
13089
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" }) }));
13090
13380
  }
13091
- if (loading && !profile) {
13381
+ if ((loading || schemaLoading) && !profile) {
13092
13382
  return (jsxRuntime.jsx("div", { className: `account-management ${className}`, children: jsxRuntime.jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
13093
13383
  }
13094
- 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: () => {
13095
13387
  setShowDeleteConfirm(false);
13096
13388
  setDeletePassword('');
13097
13389
  setDeleteConfirmText('');
13098
13390
  }, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
13099
13391
  };
13100
13392
 
13101
- const SmartlinksClaimUI = (props) => {
13102
- // Destructure AFTER logging raw props to debug proxyMode issue
13103
- const { apiEndpoint, clientId, clientName, collectionId, productId, proofId, onClaimSuccess, onClaimError, additionalFields = [], theme = 'light', className = '', minimal = false, proxyMode = false, customization = {}, } = props;
13104
- // Debug logging for proxyMode - log RAW props first
13105
- console.log('[SmartlinksClaimUI] 🔍 RAW props received:', props);
13106
- console.log('[SmartlinksClaimUI] 🔍 props.proxyMode value:', props.proxyMode);
13107
- console.log('[SmartlinksClaimUI] 🔍 typeof props.proxyMode:', typeof props.proxyMode);
13108
- console.log('[SmartlinksClaimUI] 🎯 Destructured proxyMode:', proxyMode);
13109
- const auth = useAuth();
13110
- const [claimStep, setClaimStep] = React.useState(auth.isAuthenticated ? 'questions' : 'auth');
13111
- const [claimData, setClaimData] = React.useState({});
13112
- const [error, setError] = React.useState();
13113
- const [loading, setLoading] = React.useState(false);
13114
- const handleAuthSuccess = (token, user, accountData) => {
13115
- // Authentication successful
13116
- auth.login(token, user, accountData);
13117
- // If no additional questions, proceed directly to claim
13118
- if (additionalFields.length === 0) {
13119
- executeClaim(user);
13120
- }
13121
- else {
13122
- setClaimStep('questions');
13123
- }
13124
- };
13125
- const handleQuestionSubmit = async (e) => {
13126
- e.preventDefault();
13127
- // Validate required fields
13128
- const missingFields = additionalFields
13129
- .filter(field => field.required && !claimData[field.name])
13130
- .map(field => field.label);
13131
- if (missingFields.length > 0) {
13132
- setError(`Please fill in: ${missingFields.join(', ')}`);
13133
- return;
13134
- }
13135
- // Execute claim with collected data
13136
- if (auth.user) {
13137
- executeClaim(auth.user);
13138
- }
13139
- };
13140
- const executeClaim = async (user) => {
13141
- setClaimStep('claiming');
13142
- setLoading(true);
13143
- setError(undefined);
13144
- try {
13145
- // Create attestation to claim the proof
13146
- const response = await smartlinks__namespace.attestation.create(collectionId, productId, proofId, {
13147
- public: {
13148
- claimed: true,
13149
- claimedAt: new Date().toISOString(),
13150
- claimedBy: user.uid,
13151
- ...claimData,
13152
- },
13153
- private: {},
13154
- proof: {},
13155
- });
13156
- setClaimStep('success');
13157
- // Call success callback
13158
- onClaimSuccess({
13159
- proofId,
13160
- user,
13161
- claimData,
13162
- attestationId: response.id,
13163
- });
13164
- }
13165
- catch (err) {
13166
- console.error('Claim error:', err);
13167
- const errorMessage = err instanceof Error ? err.message : 'Failed to claim proof';
13168
- setError(errorMessage);
13169
- onClaimError?.(err instanceof Error ? err : new Error(errorMessage));
13170
- setClaimStep(additionalFields.length > 0 ? 'questions' : 'auth');
13171
- }
13172
- finally {
13173
- setLoading(false);
13174
- }
13175
- };
13176
- const handleFieldChange = (fieldName, value) => {
13177
- setClaimData(prev => ({
13178
- ...prev,
13179
- [fieldName]: value,
13180
- }));
13181
- };
13182
- // Render authentication step
13183
- if (claimStep === 'auth') {
13184
- console.log('[SmartlinksClaimUI] 🔑 Rendering auth step with proxyMode:', proxyMode);
13185
- 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 }) }));
13186
- }
13187
- // Render additional questions step
13188
- if (claimStep === 'questions') {
13189
- 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' })] })] }));
13190
- }
13191
- // Render claiming step (loading state)
13192
- if (claimStep === 'claiming') {
13193
- 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..." })] }));
13194
- }
13195
- // Render success step
13196
- if (claimStep === 'success') {
13197
- 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.' })] }));
13198
- }
13199
- return null;
13200
- };
13201
-
13202
13393
  const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
13203
13394
  const { isAuthenticated, isLoading } = useAuth();
13204
13395
  // Show loading state
@@ -13253,8 +13444,11 @@ exports.AuthProvider = AuthProvider;
13253
13444
  exports.AuthUIPreview = AuthUIPreview;
13254
13445
  exports.FirebaseAuthUI = SmartlinksAuthUI;
13255
13446
  exports.ProtectedRoute = ProtectedRoute;
13447
+ exports.SchemaFieldRenderer = SchemaFieldRenderer;
13256
13448
  exports.SmartlinksAuthUI = SmartlinksAuthUI;
13257
- exports.SmartlinksClaimUI = SmartlinksClaimUI;
13449
+ exports.getEditableFields = getEditableFields;
13450
+ exports.getRegistrationFields = getRegistrationFields;
13451
+ exports.sortFieldsByPlacement = sortFieldsByPlacement;
13258
13452
  exports.tokenStorage = tokenStorage;
13259
13453
  exports.useAuth = useAuth;
13260
13454
  //# sourceMappingURL=index.js.map