@proveanything/smartlinks-auth-ui 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import React, { useEffect, useState, useRef, useCallback, useMemo, createContext, useContext } from 'react';
2
+ import React, { useEffect, useState, useMemo, useRef, useCallback, createContext, useContext } from 'react';
3
3
  import * as smartlinks from '@proveanything/smartlinks';
4
4
  import { iframe } from '@proveanything/smartlinks';
5
+ import { post } from '@proveanything/smartlinks/dist/http';
5
6
 
6
7
  const AuthContainer = ({ children, theme = 'light', className = '', config, minimal = false, }) => {
7
8
  // Apply CSS variables for customization
@@ -58,22 +59,145 @@ const AuthContainer = ({ children, theme = 'light', className = '', config, mini
58
59
  return (jsx("div", { className: containerClass, children: jsxs("div", { className: cardClass, style: !minimal && config?.branding?.buttonStyle === 'square' ? { borderRadius: '4px' } : undefined, children: [(logoUrl || title || subtitle) && (jsxs("div", { className: "auth-header", children: [logoUrl && (jsx("div", { className: "auth-logo", children: jsx("img", { src: logoUrl, alt: "Logo", style: { maxWidth: '200px', height: 'auto', objectFit: 'contain' } }) })), title && jsx("h1", { className: "auth-title", children: title }), subtitle && jsx("p", { className: "auth-subtitle", children: subtitle })] })), jsx("div", { className: "auth-content", children: children }), (config?.branding?.termsUrl || config?.branding?.privacyUrl) && (jsxs("div", { className: "auth-footer", children: [config.branding.termsUrl && jsx("a", { href: config.branding.termsUrl, target: "_blank", rel: "noopener noreferrer", children: "Terms" }), config.branding.termsUrl && config.branding.privacyUrl && jsx("span", { children: "\u2022" }), config.branding.privacyUrl && jsx("a", { href: config.branding.privacyUrl, target: "_blank", rel: "noopener noreferrer", children: "Privacy" })] }))] }) }));
59
60
  };
60
61
 
61
- const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, additionalFields = [], }) => {
62
+ /**
63
+ * Renders a form field based on schema definition
64
+ * Used in both registration and account management forms
65
+ */
66
+ const SchemaFieldRenderer = ({ field, value, onChange, disabled = false, error, className = '', }) => {
67
+ const handleChange = (newValue) => {
68
+ onChange(field.key, newValue);
69
+ };
70
+ const inputId = `field-${field.key}`;
71
+ const commonProps = {
72
+ id: inputId,
73
+ className: `auth-input ${error ? 'auth-input-error' : ''} ${className}`,
74
+ disabled: disabled || field.readOnly,
75
+ 'aria-describedby': error ? `${inputId}-error` : undefined,
76
+ };
77
+ const renderField = () => {
78
+ switch (field.widget) {
79
+ case 'select':
80
+ return (jsxs("select", { ...commonProps, value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, children: [jsxs("option", { value: "", children: ["Select ", field.label, "..."] }), field.options?.map((option) => {
81
+ const optionValue = typeof option === 'string' ? option : option.value;
82
+ const optionLabel = typeof option === 'string' ? option : option.label;
83
+ return (jsx("option", { value: optionValue, children: optionLabel }, optionValue));
84
+ })] }));
85
+ case 'checkbox':
86
+ return (jsxs("label", { className: "auth-checkbox-label", style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [jsx("input", { type: "checkbox", id: inputId, checked: !!value, onChange: (e) => handleChange(e.target.checked), disabled: disabled || field.readOnly, required: field.required, className: "auth-checkbox" }), jsx("span", { children: field.description || field.label })] }));
87
+ case 'textarea':
88
+ return (jsx("textarea", { ...commonProps, value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, placeholder: field.placeholder, rows: 3, style: { minHeight: '80px', resize: 'vertical' }, maxLength: field.validation?.maxLength }));
89
+ case 'number':
90
+ return (jsx("input", { ...commonProps, type: "number", value: value ?? '', onChange: (e) => handleChange(e.target.value ? Number(e.target.value) : undefined), required: field.required, placeholder: field.placeholder, min: field.validation?.min, max: field.validation?.max }));
91
+ case 'date':
92
+ return (jsx("input", { ...commonProps, type: "date", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required }));
93
+ case 'tel':
94
+ return (jsx("input", { ...commonProps, type: "tel", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, placeholder: field.placeholder || '+1 (555) 000-0000', autoComplete: "tel" }));
95
+ case 'email':
96
+ return (jsx("input", { ...commonProps, type: "email", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, placeholder: field.placeholder || 'email@example.com', autoComplete: "email" }));
97
+ case 'text':
98
+ default:
99
+ return (jsx("input", { ...commonProps, type: "text", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required, placeholder: field.placeholder, minLength: field.validation?.minLength, maxLength: field.validation?.maxLength, pattern: field.validation?.pattern }));
100
+ }
101
+ };
102
+ // Checkbox has its own label
103
+ if (field.widget === 'checkbox') {
104
+ return (jsxs("div", { className: "auth-form-group", children: [renderField(), error && (jsx("div", { id: `${inputId}-error`, className: "auth-field-error", role: "alert", children: error }))] }));
105
+ }
106
+ return (jsxs("div", { className: "auth-form-group", children: [jsxs("label", { htmlFor: inputId, className: "auth-label", children: [field.label, field.required && jsx("span", { style: { color: 'var(--auth-error-color, #ef4444)' }, children: " *" })] }), field.description && (jsx("p", { className: "auth-field-description", style: { fontSize: '0.85em', color: 'var(--auth-text-muted)', marginBottom: '4px' }, children: field.description })), renderField(), error && (jsx("div", { id: `${inputId}-error`, className: "auth-field-error", role: "alert", children: error }))] }));
107
+ };
108
+ /**
109
+ * Helper to get all editable fields from schema
110
+ */
111
+ const getEditableFields = (schema) => {
112
+ if (!schema)
113
+ return [];
114
+ const editableKeys = new Set(schema.settings.publicEditableFields);
115
+ const coreEditable = schema.fields.filter(f => f.editable && f.visible && editableKeys.has(f.key));
116
+ const customEditable = schema.customFields.filter(f => f.editable && f.visible);
117
+ return [...coreEditable, ...customEditable];
118
+ };
119
+ /**
120
+ * Helper to get registration fields based on config
121
+ */
122
+ const getRegistrationFields = (schema, registrationConfig) => {
123
+ if (!schema || !registrationConfig.length)
124
+ return [];
125
+ const configMap = new Map(registrationConfig.map(c => [c.key, c]));
126
+ const allFields = [...schema.fields, ...schema.customFields];
127
+ return allFields
128
+ .filter(field => {
129
+ const config = configMap.get(field.key);
130
+ return config?.showDuringRegistration && field.visible;
131
+ })
132
+ .map(field => {
133
+ const config = configMap.get(field.key);
134
+ // Allow registration config to override required status
135
+ if (config?.required !== undefined) {
136
+ return { ...field, required: config.required };
137
+ }
138
+ return field;
139
+ });
140
+ };
141
+ /**
142
+ * Sort fields by placement (inline first, then post-credentials)
143
+ */
144
+ const sortFieldsByPlacement = (fields, registrationConfig) => {
145
+ const configMap = new Map(registrationConfig.map(c => [c.key, c]));
146
+ const inline = [];
147
+ const postCredentials = [];
148
+ fields.forEach(field => {
149
+ const config = configMap.get(field.key);
150
+ if (config?.placement === 'post-credentials') {
151
+ postCredentials.push(field);
152
+ }
153
+ else {
154
+ inline.push(field);
155
+ }
156
+ });
157
+ return { inline, postCredentials };
158
+ };
159
+
160
+ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, schema, registrationFieldsConfig = [], additionalFields = [], }) => {
62
161
  const [formData, setFormData] = useState({
63
162
  email: '',
64
163
  password: '',
65
164
  displayName: '',
66
165
  });
166
+ // Custom field values (separate from core AuthFormData)
167
+ const [customFieldValues, setCustomFieldValues] = useState({});
168
+ // Get schema-driven registration fields
169
+ const schemaFields = useMemo(() => {
170
+ if (!schema || !registrationFieldsConfig.length)
171
+ return { inline: [], postCredentials: [] };
172
+ const fields = getRegistrationFields(schema, registrationFieldsConfig);
173
+ return sortFieldsByPlacement(fields, registrationFieldsConfig);
174
+ }, [schema, registrationFieldsConfig]);
175
+ // Check if we have any additional fields to show
176
+ const hasSchemaFields = schemaFields.inline.length > 0 || schemaFields.postCredentials.length > 0;
177
+ const hasLegacyFields = additionalFields.length > 0 && !hasSchemaFields;
67
178
  const handleSubmit = async (e) => {
68
179
  e.preventDefault();
69
- await onSubmit(formData);
180
+ // Merge custom field values into accountData for registration
181
+ const submitData = {
182
+ ...formData,
183
+ accountData: mode === 'register'
184
+ ? { ...(formData.accountData || {}), customFields: customFieldValues }
185
+ : undefined,
186
+ };
187
+ await onSubmit(submitData);
70
188
  };
71
189
  const handleChange = (field, value) => {
72
190
  setFormData(prev => ({ ...prev, [field]: value }));
73
191
  };
192
+ const handleCustomFieldChange = (key, value) => {
193
+ setCustomFieldValues(prev => ({ ...prev, [key]: value }));
194
+ };
195
+ const renderSchemaField = (field) => (jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key));
196
+ // Legacy field renderer (for backward compatibility)
197
+ const renderLegacyField = (field) => (jsxs("div", { className: "auth-form-group", children: [jsxs("label", { htmlFor: field.name, className: "auth-label", children: [field.label, field.required && jsx("span", { style: { color: 'var(--auth-error-color, #ef4444)' }, children: " *" })] }), field.type === 'select' ? (jsxs("select", { id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, children: [jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsx("option", { value: option, children: option }, option)))] })) : field.type === 'textarea' ? (jsx("textarea", { id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, placeholder: field.placeholder, rows: 3, style: { minHeight: '80px', resize: 'vertical' } })) : (jsx("input", { type: field.type, id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, placeholder: field.placeholder }))] }, field.name));
74
198
  return (jsxs("form", { className: "auth-form", onSubmit: handleSubmit, children: [jsxs("div", { className: "auth-form-header", children: [jsx("h2", { className: "auth-form-title", children: mode === 'login' ? 'Sign in' : 'Create account' }), jsx("p", { className: "auth-form-subtitle", children: mode === 'login'
75
199
  ? 'Welcome back! Please enter your credentials.'
76
- : 'Get started by creating your account.' })] }), error && (jsxs("div", { className: "auth-error", role: "alert", children: [jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), mode === 'register' && (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsx("input", { type: "text", id: "displayName", className: "auth-input", value: formData.displayName || '', onChange: (e) => handleChange('displayName', e.target.value), required: mode === 'register', disabled: loading, placeholder: "John Doe" })] })), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsx("input", { type: "email", id: "email", className: "auth-input", value: formData.email || '', onChange: (e) => handleChange('email', e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsx("input", { type: "password", id: "password", className: "auth-input", value: formData.password || '', onChange: (e) => handleChange('password', e.target.value), required: true, disabled: loading, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === 'login' ? 'current-password' : 'new-password', minLength: 6 })] }), mode === 'register' && additionalFields.map((field) => (jsxs("div", { className: "auth-form-group", children: [jsxs("label", { htmlFor: field.name, className: "auth-label", children: [field.label, field.required && jsx("span", { style: { color: 'var(--auth-error-color, #ef4444)' }, children: " *" })] }), field.type === 'select' ? (jsxs("select", { id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, children: [jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsx("option", { value: option, children: option }, option)))] })) : field.type === 'textarea' ? (jsx("textarea", { id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, placeholder: field.placeholder, rows: 3, style: { minHeight: '80px', resize: 'vertical' } })) : (jsx("input", { type: field.type, id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, placeholder: field.placeholder }))] }, field.name))), mode === 'login' && (jsx("div", { className: "auth-form-footer", children: jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), jsxs("div", { className: "auth-divider", children: [jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] })] }));
200
+ : 'Get started by creating your account.' })] }), error && (jsxs("div", { className: "auth-error", role: "alert", children: [jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), mode === 'register' && (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsx("input", { type: "text", id: "displayName", className: "auth-input", value: formData.displayName || '', onChange: (e) => handleChange('displayName', e.target.value), required: mode === 'register', disabled: loading, placeholder: "John Doe" })] })), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsx("input", { type: "email", id: "email", className: "auth-input", value: formData.email || '', onChange: (e) => handleChange('email', e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsx("input", { type: "password", id: "password", className: "auth-input", value: formData.password || '', onChange: (e) => handleChange('password', e.target.value), required: true, disabled: loading, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === 'login' ? 'current-password' : 'new-password', minLength: 6 })] }), mode === 'register' && hasSchemaFields && schemaFields.inline.map(renderSchemaField), mode === 'register' && hasLegacyFields && additionalFields.map(renderLegacyField), mode === 'register' && hasSchemaFields && schemaFields.postCredentials.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "auth-divider", style: { margin: '16px 0' }, children: jsx("span", { children: "Additional Information" }) }), schemaFields.postCredentials.map(renderSchemaField)] })), mode === 'login' && (jsx("div", { className: "auth-form-footer", children: jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), jsxs("div", { className: "auth-divider", children: [jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] })] }));
77
201
  };
78
202
 
79
203
  const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, loading, }) => {
@@ -10641,6 +10765,8 @@ class AuthAPI {
10641
10765
  return smartlinks.authKit.login(this.clientId, email, password);
10642
10766
  }
10643
10767
  async register(data) {
10768
+ // Note: redirectUrl is not passed to register - verification email is sent separately
10769
+ // via sendEmailVerification() after registration for verify-then-* modes
10644
10770
  return smartlinks.authKit.register(this.clientId, {
10645
10771
  email: data.email,
10646
10772
  password: data.password,
@@ -10664,6 +10790,18 @@ class AuthAPI {
10664
10790
  // Pass token to SDK - backend verifies with Google
10665
10791
  return smartlinks.authKit.googleLogin(this.clientId, token);
10666
10792
  }
10793
+ async loginWithGoogleCode(code, redirectUri) {
10794
+ this.log.log('loginWithGoogleCode called:', {
10795
+ codeLength: code?.length,
10796
+ redirectUri,
10797
+ });
10798
+ // Exchange authorization code for tokens via backend
10799
+ // Use direct HTTP call since SDK may not have this method in authKit namespace yet
10800
+ return post(`/api/v1/authkit/${this.clientId}/google-code`, {
10801
+ code,
10802
+ redirectUri,
10803
+ });
10804
+ }
10667
10805
  async sendPhoneCode(phoneNumber) {
10668
10806
  return smartlinks.authKit.sendPhoneCode(this.clientId, phoneNumber);
10669
10807
  }
@@ -11147,7 +11285,7 @@ const tokenStorage = {
11147
11285
  const storage = await getStorage();
11148
11286
  const authToken = {
11149
11287
  token,
11150
- expiresAt: expiresAt || Date.now() + 3600000, // Default 1 hour
11288
+ expiresAt: expiresAt || Date.now() + (7 * 24 * 60 * 60 * 1000), // Default 7 days (matches backend JWT)
11151
11289
  };
11152
11290
  await storage.setItem(TOKEN_KEY, authToken);
11153
11291
  },
@@ -11158,7 +11296,8 @@ const tokenStorage = {
11158
11296
  return null;
11159
11297
  // Check if token is expired
11160
11298
  if (authToken.expiresAt && authToken.expiresAt < Date.now()) {
11161
- await this.clearToken();
11299
+ console.log('[TokenStorage] Token expired at:', new Date(authToken.expiresAt).toISOString(), '- clearing all auth data');
11300
+ await this.clearAll(); // Clear ALL auth data to prevent orphaned state
11162
11301
  return null;
11163
11302
  }
11164
11303
  return authToken;
@@ -11238,6 +11377,9 @@ const tokenStorage = {
11238
11377
 
11239
11378
  const AuthContext = createContext(undefined);
11240
11379
  const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
11380
+ // Token refresh settings
11381
+ enableAutoRefresh = true, refreshThresholdPercent = 75, // Refresh when 75% of token lifetime has passed
11382
+ refreshCheckInterval = 60 * 1000, // Check every minute
11241
11383
  // Contact & Interaction features
11242
11384
  collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
11243
11385
  const [user, setUser] = useState(null);
@@ -11658,11 +11800,11 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11658
11800
  unsubscribe();
11659
11801
  };
11660
11802
  }, [proxyMode, notifyAuthStateChange]);
11661
- const login = useCallback(async (authToken, authUser, authAccountData, isNewUser) => {
11803
+ const login = useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt) => {
11662
11804
  try {
11663
11805
  // Only persist to storage in standalone mode
11664
11806
  if (!proxyMode) {
11665
- await tokenStorage.saveToken(authToken);
11807
+ await tokenStorage.saveToken(authToken, expiresAt);
11666
11808
  await tokenStorage.saveUser(authUser);
11667
11809
  if (authAccountData) {
11668
11810
  await tokenStorage.saveAccountData(authAccountData);
@@ -11745,9 +11887,67 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11745
11887
  const storedToken = await tokenStorage.getToken();
11746
11888
  return storedToken ? storedToken.token : null;
11747
11889
  }, [proxyMode, token]);
11890
+ // Get token with expiration info
11891
+ const getTokenInfo = useCallback(async () => {
11892
+ if (proxyMode) {
11893
+ // In proxy mode, we don't have expiration info
11894
+ if (token) {
11895
+ return { token, expiresAt: 0, expiresIn: 0 };
11896
+ }
11897
+ return null;
11898
+ }
11899
+ const storedToken = await tokenStorage.getToken();
11900
+ if (!storedToken?.token || !storedToken.expiresAt) {
11901
+ return null;
11902
+ }
11903
+ return {
11904
+ token: storedToken.token,
11905
+ expiresAt: storedToken.expiresAt,
11906
+ expiresIn: Math.max(0, storedToken.expiresAt - Date.now()),
11907
+ };
11908
+ }, [proxyMode, token]);
11909
+ // Refresh token - validates current token and extends session if backend supports it
11748
11910
  const refreshToken = useCallback(async () => {
11749
- throw new Error('Token refresh must be implemented via your backend API');
11750
- }, []);
11911
+ if (proxyMode) {
11912
+ console.log('[AuthContext] Proxy mode: token refresh handled by parent');
11913
+ throw new Error('Token refresh in proxy mode is handled by the parent application');
11914
+ }
11915
+ const storedToken = await tokenStorage.getToken();
11916
+ if (!storedToken?.token) {
11917
+ throw new Error('No token to refresh. Please login first.');
11918
+ }
11919
+ try {
11920
+ console.log('[AuthContext] Refreshing token...');
11921
+ // Verify current token is still valid
11922
+ const verifyResult = await smartlinks.auth.verifyToken(storedToken.token);
11923
+ if (!verifyResult.valid) {
11924
+ console.warn('[AuthContext] Token is no longer valid, clearing session');
11925
+ await logout();
11926
+ throw new Error('Token expired or invalid. Please login again.');
11927
+ }
11928
+ // Token is valid - extend its expiration locally
11929
+ // Backend JWT remains valid, we just update our local tracking
11930
+ const newExpiresAt = Date.now() + (7 * 24 * 60 * 60 * 1000); // 7 days from now
11931
+ await tokenStorage.saveToken(storedToken.token, newExpiresAt);
11932
+ console.log('[AuthContext] Token verified and expiration extended to:', new Date(newExpiresAt).toISOString());
11933
+ // Update verified state
11934
+ setIsVerified(true);
11935
+ pendingVerificationRef.current = false;
11936
+ notifyAuthStateChange('TOKEN_REFRESH', user, storedToken.token, accountData, accountInfo, true, contact, contactId);
11937
+ return storedToken.token;
11938
+ }
11939
+ catch (error) {
11940
+ console.error('[AuthContext] Token refresh failed:', error);
11941
+ // If it's a network error, don't logout
11942
+ if (isNetworkError(error)) {
11943
+ console.warn('[AuthContext] Network error during refresh, keeping session');
11944
+ throw error;
11945
+ }
11946
+ // Auth error - clear session
11947
+ await logout();
11948
+ throw new Error('Token refresh failed. Please login again.');
11949
+ }
11950
+ }, [proxyMode, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
11751
11951
  const getAccount = useCallback(async (forceRefresh = false) => {
11752
11952
  try {
11753
11953
  if (proxyMode) {
@@ -11855,6 +12055,54 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11855
12055
  window.removeEventListener('offline', handleOffline);
11856
12056
  };
11857
12057
  }, [proxyMode, token, user, retryVerification]);
12058
+ // Automatic background token refresh
12059
+ useEffect(() => {
12060
+ if (proxyMode || !enableAutoRefresh || !token || !user) {
12061
+ return;
12062
+ }
12063
+ console.log('[AuthContext] Setting up automatic token refresh (interval:', refreshCheckInterval, 'ms, threshold:', refreshThresholdPercent, '%)');
12064
+ const checkAndRefresh = async () => {
12065
+ try {
12066
+ const storedToken = await tokenStorage.getToken();
12067
+ if (!storedToken?.expiresAt) {
12068
+ console.log('[AuthContext] No token expiration info, skipping refresh check');
12069
+ return;
12070
+ }
12071
+ const now = Date.now();
12072
+ const tokenLifetime = storedToken.expiresAt - (storedToken.expiresAt - (7 * 24 * 60 * 60 * 1000)); // Assume 7-day lifetime
12073
+ const tokenAge = now - (storedToken.expiresAt - (7 * 24 * 60 * 60 * 1000));
12074
+ const percentUsed = (tokenAge / tokenLifetime) * 100;
12075
+ // Calculate time remaining
12076
+ const timeRemaining = storedToken.expiresAt - now;
12077
+ const hoursRemaining = Math.round(timeRemaining / (60 * 60 * 1000));
12078
+ if (percentUsed >= refreshThresholdPercent) {
12079
+ console.log(`[AuthContext] Token at ${Math.round(percentUsed)}% lifetime (${hoursRemaining}h remaining), refreshing...`);
12080
+ try {
12081
+ await refreshToken();
12082
+ console.log('[AuthContext] Automatic token refresh successful');
12083
+ }
12084
+ catch (refreshError) {
12085
+ console.warn('[AuthContext] Automatic token refresh failed:', refreshError);
12086
+ // Don't logout on refresh failure - user can still use the app until token actually expires
12087
+ }
12088
+ }
12089
+ else {
12090
+ console.log(`[AuthContext] Token at ${Math.round(percentUsed)}% lifetime (${hoursRemaining}h remaining), no refresh needed`);
12091
+ }
12092
+ }
12093
+ catch (error) {
12094
+ console.error('[AuthContext] Error checking token for refresh:', error);
12095
+ }
12096
+ };
12097
+ // Check immediately on mount
12098
+ checkAndRefresh();
12099
+ // Set up periodic check
12100
+ const intervalId = setInterval(checkAndRefresh, refreshCheckInterval);
12101
+ return () => {
12102
+ console.log('[AuthContext] Cleaning up automatic token refresh timer');
12103
+ clearInterval(intervalId);
12104
+ };
12105
+ }, [proxyMode, enableAutoRefresh, refreshCheckInterval, refreshThresholdPercent, token, user, refreshToken]);
11858
12106
  const value = {
11859
12107
  user,
11860
12108
  token,
@@ -11872,6 +12120,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
11872
12120
  login,
11873
12121
  logout,
11874
12122
  getToken,
12123
+ getTokenInfo,
11875
12124
  refreshToken,
11876
12125
  getAccount,
11877
12126
  refreshAccount,
@@ -11889,6 +12138,14 @@ const useAuth = () => {
11889
12138
  return context;
11890
12139
  };
11891
12140
 
12141
+ // Helper to calculate expiration from AuthResponse
12142
+ const getExpirationFromResponse = (response) => {
12143
+ if (response.expiresAt)
12144
+ return response.expiresAt;
12145
+ if (response.expiresIn)
12146
+ return Date.now() + response.expiresIn;
12147
+ return undefined; // Will use 7-day default in tokenStorage
12148
+ };
11892
12149
  // Default Smartlinks Google OAuth Client ID (public - safe to expose)
11893
12150
  const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
11894
12151
  // Default auth UI configuration when no clientId is provided
@@ -11932,6 +12189,28 @@ const loadGoogleIdentityServices = () => {
11932
12189
  document.head.appendChild(script);
11933
12190
  });
11934
12191
  };
12192
+ // Helper to detect WebView environments (Android/iOS)
12193
+ const detectWebView = () => {
12194
+ const ua = navigator.userAgent;
12195
+ // Android WebView detection
12196
+ if (/Android/i.test(ua)) {
12197
+ // Modern Android WebViews include "wv" in UA string
12198
+ if (/\bwv\b/i.test(ua))
12199
+ return true;
12200
+ // Check for legacy Android bridge
12201
+ if (typeof window.Android !== 'undefined')
12202
+ return true;
12203
+ }
12204
+ // iOS WKWebView detection
12205
+ if (/iPhone|iPad|iPod/i.test(ua)) {
12206
+ const hasWebKitHandlers = !!window.webkit?.messageHandlers;
12207
+ const isSafari = !!window.safari;
12208
+ // WKWebView has webkit handlers but no safari object
12209
+ if (hasWebKitHandlers && !isSafari)
12210
+ return true;
12211
+ }
12212
+ return false;
12213
+ };
11935
12214
  // Helper to convert generic SDK errors to user-friendly messages
11936
12215
  const getFriendlyErrorMessage = (errorMessage) => {
11937
12216
  // Check for common HTTP status codes in the error message
@@ -11959,7 +12238,7 @@ const getFriendlyErrorMessage = (errorMessage) => {
11959
12238
  // Return original message if no pattern matches
11960
12239
  return errorMessage;
11961
12240
  };
11962
- 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, }) => {
12241
+ 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, }) => {
11963
12242
  const [mode, setMode] = useState(initialMode);
11964
12243
  const [loading, setLoading] = useState(false);
11965
12244
  const [error, setError] = useState();
@@ -11976,6 +12255,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
11976
12255
  const [configLoading, setConfigLoading] = useState(!skipConfigFetch);
11977
12256
  const [showEmailForm, setShowEmailForm] = useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
11978
12257
  const [sdkReady, setSdkReady] = useState(false); // Track SDK initialization state
12258
+ const [contactSchema, setContactSchema] = useState(null); // Schema for registration fields
11979
12259
  const log = useMemo(() => createLoggerWrapper(logger), [logger]);
11980
12260
  const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
11981
12261
  const auth = useAuth();
@@ -12150,6 +12430,24 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12150
12430
  };
12151
12431
  fetchConfig();
12152
12432
  }, [apiEndpoint, clientId, customization, skipConfigFetch, sdkReady, proxyMode, log]);
12433
+ // Fetch contact schema for registration fields when collectionId is provided
12434
+ useEffect(() => {
12435
+ if (!collectionId || !sdkReady)
12436
+ return;
12437
+ const fetchSchema = async () => {
12438
+ try {
12439
+ console.log('[SmartlinksAuthUI] 📋 Fetching contact schema for collection:', collectionId);
12440
+ const schema = await smartlinks.contact.publicGetSchema(collectionId);
12441
+ console.log('[SmartlinksAuthUI] ✅ Schema loaded:', schema);
12442
+ setContactSchema(schema);
12443
+ }
12444
+ catch (err) {
12445
+ console.warn('[SmartlinksAuthUI] ⚠️ Failed to fetch schema (non-fatal):', err);
12446
+ // Non-fatal - registration will work without schema fields
12447
+ }
12448
+ };
12449
+ fetchSchema();
12450
+ }, [collectionId, sdkReady]);
12153
12451
  // Reset showEmailForm when mode changes away from login/register
12154
12452
  useEffect(() => {
12155
12453
  if (mode !== 'login' && mode !== 'register') {
@@ -12174,8 +12472,15 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12174
12472
  const params = getUrlParams();
12175
12473
  const urlMode = params.get('mode');
12176
12474
  const token = params.get('token');
12177
- log.log('URL params detected:', { urlMode, token, hash: window.location.hash, search: window.location.search });
12178
- if (urlMode && token) {
12475
+ // Check for Google OAuth redirect callback
12476
+ const authCode = params.get('code');
12477
+ const state = params.get('state');
12478
+ log.log('URL params detected:', { urlMode, token, authCode: !!authCode, state: !!state, hash: window.location.hash, search: window.location.search });
12479
+ if (authCode && state) {
12480
+ // Google OAuth redirect callback
12481
+ handleGoogleAuthCodeCallback(authCode, state);
12482
+ }
12483
+ else if (urlMode && token) {
12179
12484
  handleURLBasedAuth(urlMode, token);
12180
12485
  }
12181
12486
  }, []);
@@ -12190,18 +12495,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12190
12495
  const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
12191
12496
  if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
12192
12497
  // Auto-login modes: Log the user in immediately if token is provided
12193
- auth.login(response.token, response.user, response.accountData, true);
12498
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12194
12499
  setAuthSuccess(true);
12195
12500
  setSuccessMessage('Email verified successfully! You are now logged in.');
12196
12501
  onAuthSuccess(response.token, response.user, response.accountData);
12197
12502
  // Clear the URL parameters
12198
12503
  const cleanUrl = window.location.href.split('?')[0];
12199
12504
  window.history.replaceState({}, document.title, cleanUrl);
12200
- // Redirect after a brief delay to show success message
12505
+ // For email verification deep links, redirect immediately if configured
12506
+ // (user came from an email link, so redirect is expected behavior)
12201
12507
  if (redirectUrl) {
12202
- setTimeout(() => {
12203
- window.location.href = redirectUrl;
12204
- }, 2000);
12508
+ window.location.href = redirectUrl;
12205
12509
  }
12206
12510
  }
12207
12511
  else {
@@ -12233,18 +12537,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12233
12537
  const response = await api.verifyMagicLink(token);
12234
12538
  // Auto-login with magic link if token is provided
12235
12539
  if (response.token) {
12236
- auth.login(response.token, response.user, response.accountData, true);
12540
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12237
12541
  setAuthSuccess(true);
12238
12542
  setSuccessMessage('Magic link verified! You are now logged in.');
12239
12543
  onAuthSuccess(response.token, response.user, response.accountData);
12240
12544
  // Clear the URL parameters
12241
12545
  const cleanUrl = window.location.href.split('?')[0];
12242
12546
  window.history.replaceState({}, document.title, cleanUrl);
12243
- // Redirect after a brief delay to show success message
12547
+ // For magic link deep links, redirect immediately if configured
12548
+ // (user came from an email link, so redirect is expected behavior)
12244
12549
  if (redirectUrl) {
12245
- setTimeout(() => {
12246
- window.location.href = redirectUrl;
12247
- }, 2000);
12550
+ window.location.href = redirectUrl;
12248
12551
  }
12249
12552
  }
12250
12553
  else {
@@ -12290,6 +12593,43 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12290
12593
  setLoading(false);
12291
12594
  }
12292
12595
  };
12596
+ // Handle Google OAuth authorization code callback (from redirect flow)
12597
+ const handleGoogleAuthCodeCallback = async (code, stateParam) => {
12598
+ setLoading(true);
12599
+ setError(undefined);
12600
+ try {
12601
+ // Parse state to get context
12602
+ const state = JSON.parse(decodeURIComponent(stateParam));
12603
+ log.log('Google OAuth redirect callback:', { clientId: state.clientId, returnPath: state.returnPath });
12604
+ // Determine the redirect URI that was used (must match exactly)
12605
+ const redirectUri = state.redirectUri || window.location.origin + window.location.pathname;
12606
+ // Exchange authorization code for tokens
12607
+ const response = await api.loginWithGoogleCode(code, redirectUri);
12608
+ if (response.token) {
12609
+ auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
12610
+ setAuthSuccess(true);
12611
+ setSuccessMessage('Google login successful!');
12612
+ onAuthSuccess(response.token, response.user, response.accountData);
12613
+ }
12614
+ else {
12615
+ throw new Error('Authentication failed - no token received');
12616
+ }
12617
+ // Clean URL parameters
12618
+ const cleanUrl = window.location.origin + window.location.pathname + (state.returnPath?.includes('#') ? state.returnPath.split('?')[0] : window.location.hash.split('?')[0]);
12619
+ window.history.replaceState({}, document.title, cleanUrl);
12620
+ }
12621
+ catch (err) {
12622
+ const errorMessage = err instanceof Error ? err.message : 'Google login failed';
12623
+ setError(getFriendlyErrorMessage(errorMessage));
12624
+ onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
12625
+ // Clean URL parameters even on error
12626
+ const cleanUrl = window.location.origin + window.location.pathname + window.location.hash.split('?')[0];
12627
+ window.history.replaceState({}, document.title, cleanUrl);
12628
+ }
12629
+ finally {
12630
+ setLoading(false);
12631
+ }
12632
+ };
12293
12633
  const handleEmailAuth = async (data) => {
12294
12634
  setLoading(true);
12295
12635
  setError(undefined);
@@ -12309,7 +12649,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12309
12649
  // Handle different verification modes
12310
12650
  if (verificationMode === 'immediate' && response.token) {
12311
12651
  // Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
12312
- auth.login(response.token, response.user, response.accountData, true);
12652
+ auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
12313
12653
  setAuthSuccess(true);
12314
12654
  const deadline = response.emailVerificationDeadline
12315
12655
  ? new Date(response.emailVerificationDeadline).toLocaleString()
@@ -12318,19 +12658,37 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12318
12658
  if (response.token) {
12319
12659
  onAuthSuccess(response.token, response.user, response.accountData);
12320
12660
  }
12321
- if (redirectUrl) {
12322
- setTimeout(() => {
12323
- window.location.href = redirectUrl;
12324
- }, 2000);
12325
- }
12661
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12326
12662
  }
12327
12663
  else if (verificationMode === 'verify-then-auto-login') {
12328
12664
  // Verify-then-auto-login mode: Don't log in yet, but will auto-login after email verification
12665
+ // Send the verification email since backend register may not send it automatically
12666
+ if (response.user?.uid && data.email) {
12667
+ try {
12668
+ await api.sendEmailVerification(response.user.uid, data.email, getRedirectUrl());
12669
+ log.log('Verification email sent after registration');
12670
+ }
12671
+ catch (verifyError) {
12672
+ log.warn('Failed to send verification email after registration:', verifyError);
12673
+ // Don't fail the registration, just log the warning
12674
+ }
12675
+ }
12329
12676
  setAuthSuccess(true);
12330
12677
  setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
12331
12678
  }
12332
12679
  else {
12333
12680
  // verify-then-manual-login mode: Traditional flow
12681
+ // Send the verification email since backend register may not send it automatically
12682
+ if (response.user?.uid && data.email) {
12683
+ try {
12684
+ await api.sendEmailVerification(response.user.uid, data.email, getRedirectUrl());
12685
+ log.log('Verification email sent after registration');
12686
+ }
12687
+ catch (verifyError) {
12688
+ log.warn('Failed to send verification email after registration:', verifyError);
12689
+ // Don't fail the registration, just log the warning
12690
+ }
12691
+ }
12334
12692
  setAuthSuccess(true);
12335
12693
  setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
12336
12694
  }
@@ -12345,15 +12703,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12345
12703
  if (response.requiresEmailVerification) {
12346
12704
  throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
12347
12705
  }
12348
- auth.login(response.token, response.user, response.accountData, false);
12706
+ auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response));
12349
12707
  setAuthSuccess(true);
12350
12708
  setSuccessMessage('Login successful!');
12351
12709
  onAuthSuccess(response.token, response.user, response.accountData);
12352
- if (redirectUrl) {
12353
- setTimeout(() => {
12354
- window.location.href = redirectUrl;
12355
- }, 2000);
12356
- }
12710
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12357
12711
  }
12358
12712
  else {
12359
12713
  throw new Error('Authentication failed - please verify your email before logging in.');
@@ -12428,11 +12782,16 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12428
12782
  // Use custom client ID from config, or fall back to default Smartlinks client ID
12429
12783
  const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
12430
12784
  // Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
12431
- const oauthFlow = config?.googleOAuthFlow || 'oneTap';
12785
+ const configuredFlow = config?.googleOAuthFlow || 'oneTap';
12786
+ // For oneTap, automatically use redirect flow in WebView environments
12787
+ const isWebView = detectWebView();
12788
+ const oauthFlow = (configuredFlow === 'oneTap' && isWebView) ? 'redirect' : configuredFlow;
12432
12789
  // Log Google Auth configuration for debugging
12433
12790
  log.log('Google Auth initiated:', {
12434
12791
  googleClientId,
12435
- oauthFlow,
12792
+ configuredFlow,
12793
+ effectiveFlow: oauthFlow,
12794
+ isWebView,
12436
12795
  currentOrigin: window.location.origin,
12437
12796
  currentHref: window.location.href,
12438
12797
  configGoogleClientId: config?.googleClientId,
@@ -12448,7 +12807,37 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12448
12807
  throw new Error('Google Identity Services failed to initialize');
12449
12808
  }
12450
12809
  log.log('Google Identity Services loaded, using flow:', oauthFlow);
12451
- if (oauthFlow === 'popup') {
12810
+ if (oauthFlow === 'redirect') {
12811
+ // Use OAuth2 redirect flow (works in WebViews and everywhere)
12812
+ if (!google.accounts.oauth2) {
12813
+ throw new Error('Google OAuth2 not available');
12814
+ }
12815
+ // Build the redirect URI - use current URL without query params
12816
+ const redirectUri = getRedirectUrl();
12817
+ // Build state parameter to preserve context across redirect
12818
+ const state = encodeURIComponent(JSON.stringify({
12819
+ clientId,
12820
+ returnPath: window.location.hash || window.location.pathname,
12821
+ redirectUri,
12822
+ }));
12823
+ log.log('Initializing Google OAuth2 redirect flow:', {
12824
+ client_id: googleClientId,
12825
+ scope: 'openid email profile',
12826
+ redirect_uri: redirectUri,
12827
+ state,
12828
+ });
12829
+ const client = google.accounts.oauth2.initCodeClient({
12830
+ client_id: googleClientId,
12831
+ scope: 'openid email profile',
12832
+ ux_mode: 'redirect',
12833
+ redirect_uri: redirectUri,
12834
+ state,
12835
+ });
12836
+ // This will navigate away from the page
12837
+ client.requestCode();
12838
+ return; // Don't set loading to false - we're navigating away
12839
+ }
12840
+ else if (oauthFlow === 'popup') {
12452
12841
  // Use OAuth2 popup flow (works in iframes but requires popup permission)
12453
12842
  if (!google.accounts.oauth2) {
12454
12843
  throw new Error('Google OAuth2 not available');
@@ -12506,7 +12895,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12506
12895
  });
12507
12896
  if (authResponse.token) {
12508
12897
  // Google OAuth can be login or signup - use isNewUser flag from backend if available
12509
- auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12898
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
12510
12899
  setAuthSuccess(true);
12511
12900
  setSuccessMessage('Google login successful!');
12512
12901
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12514,11 +12903,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12514
12903
  else {
12515
12904
  throw new Error('Authentication failed - no token received');
12516
12905
  }
12517
- if (redirectUrl) {
12518
- setTimeout(() => {
12519
- window.location.href = redirectUrl;
12520
- }, 2000);
12521
- }
12906
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12522
12907
  }
12523
12908
  catch (apiError) {
12524
12909
  const errorMessage = apiError instanceof Error ? apiError.message : 'Google login failed';
@@ -12543,7 +12928,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12543
12928
  client.requestAccessToken();
12544
12929
  }
12545
12930
  else {
12546
- // Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
12931
+ // Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes or WebViews)
12547
12932
  log.log('Initializing Google OneTap flow:', {
12548
12933
  client_id: googleClientId,
12549
12934
  origin: window.location.origin,
@@ -12556,7 +12941,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12556
12941
  const authResponse = await api.loginWithGoogle(idToken);
12557
12942
  if (authResponse.token) {
12558
12943
  // Google OAuth can be login or signup - use isNewUser flag from backend if available
12559
- auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
12944
+ auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
12560
12945
  setAuthSuccess(true);
12561
12946
  setSuccessMessage('Google login successful!');
12562
12947
  onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
@@ -12564,11 +12949,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12564
12949
  else {
12565
12950
  throw new Error('Authentication failed - no token received');
12566
12951
  }
12567
- if (redirectUrl) {
12568
- setTimeout(() => {
12569
- window.location.href = redirectUrl;
12570
- }, 2000);
12571
- }
12952
+ // Note: No automatic redirect - app controls navigation via onAuthSuccess callback
12572
12953
  setLoading(false);
12573
12954
  }
12574
12955
  catch (err) {
@@ -12580,8 +12961,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12580
12961
  },
12581
12962
  auto_select: false,
12582
12963
  cancel_on_tap_outside: true,
12583
- // Note: use_fedcm_for_prompt omitted - requires Permissions-Policy header on hosting server
12584
- // Will be needed when FedCM becomes mandatory in the future
12964
+ use_fedcm_for_prompt: true, // Enable FedCM for future browser compatibility
12585
12965
  });
12586
12966
  // Use timeout fallback instead of deprecated notification methods
12587
12967
  // (isNotDisplayed/isSkippedMoment will stop working when FedCM becomes mandatory)
@@ -12618,7 +12998,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12618
12998
  // Update auth context with account data if token is provided
12619
12999
  if (response.token) {
12620
13000
  // Phone auth can be login or signup - use isNewUser flag from backend if available
12621
- auth.login(response.token, response.user, response.accountData, response.isNewUser);
13001
+ auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
12622
13002
  onAuthSuccess(response.token, response.user, response.accountData);
12623
13003
  if (redirectUrl) {
12624
13004
  window.location.href = redirectUrl;
@@ -12791,36 +13171,39 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
12791
13171
  setShowResendVerification(false);
12792
13172
  setShowRequestNewReset(false);
12793
13173
  setError(undefined);
12794
- }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
13174
+ }, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, schema: contactSchema, registrationFieldsConfig: config?.registrationFields, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
12795
13175
  })() })) })) : null }));
12796
13176
  };
12797
13177
 
12798
- const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', customization = {}, }) => {
13178
+ const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, className = '', customization = {}, }) => {
12799
13179
  const auth = useAuth();
12800
13180
  const [loading, setLoading] = useState(false);
12801
13181
  const [profile, setProfile] = useState(null);
12802
13182
  const [error, setError] = useState();
12803
13183
  const [success, setSuccess] = useState();
13184
+ // Schema state
13185
+ const [schema, setSchema] = useState(null);
13186
+ const [schemaLoading, setSchemaLoading] = useState(false);
12804
13187
  // Track which section is being edited
12805
13188
  const [editingSection, setEditingSection] = useState(null);
12806
- // Profile form state
13189
+ // Form state for core fields
12807
13190
  const [displayName, setDisplayName] = useState('');
12808
- // Email change state
12809
13191
  const [newEmail, setNewEmail] = useState('');
12810
13192
  const [emailPassword, setEmailPassword] = useState('');
12811
- // Password change state
12812
13193
  const [currentPassword, setCurrentPassword] = useState('');
12813
13194
  const [newPassword, setNewPassword] = useState('');
12814
13195
  const [confirmPassword, setConfirmPassword] = useState('');
12815
- // Phone change state (reuses existing sendPhoneCode flow)
12816
13196
  const [newPhone, setNewPhone] = useState('');
12817
13197
  const [phoneCode, setPhoneCode] = useState('');
12818
13198
  const [phoneCodeSent, setPhoneCodeSent] = useState(false);
13199
+ // Custom fields form state
13200
+ const [customFieldValues, setCustomFieldValues] = useState({});
12819
13201
  // Account deletion state
12820
13202
  const [deletePassword, setDeletePassword] = useState('');
12821
13203
  const [deleteConfirmText, setDeleteConfirmText] = useState('');
12822
13204
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
12823
- const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, } = customization;
13205
+ const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, showCustomFields = true, // New: show schema-driven custom fields
13206
+ } = customization;
12824
13207
  // Reinitialize Smartlinks SDK when apiEndpoint changes
12825
13208
  useEffect(() => {
12826
13209
  if (apiEndpoint) {
@@ -12831,10 +13214,32 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12831
13214
  });
12832
13215
  }
12833
13216
  }, [apiEndpoint]);
12834
- // Load user profile on mount
13217
+ // Load schema when collectionId is available
13218
+ useEffect(() => {
13219
+ if (!collectionId)
13220
+ return;
13221
+ const loadSchema = async () => {
13222
+ setSchemaLoading(true);
13223
+ try {
13224
+ console.log('[AccountManagement] Loading schema for collection:', collectionId);
13225
+ const schemaResult = await smartlinks.contact.publicGetSchema(collectionId);
13226
+ console.log('[AccountManagement] Schema loaded:', schemaResult);
13227
+ setSchema(schemaResult);
13228
+ }
13229
+ catch (err) {
13230
+ console.warn('[AccountManagement] Failed to load schema:', err);
13231
+ // Non-fatal - component works without schema
13232
+ }
13233
+ finally {
13234
+ setSchemaLoading(false);
13235
+ }
13236
+ };
13237
+ loadSchema();
13238
+ }, [collectionId]);
13239
+ // Load user profile and contact data on mount
12835
13240
  useEffect(() => {
12836
13241
  loadProfile();
12837
- }, [clientId]);
13242
+ }, [clientId, collectionId]);
12838
13243
  const loadProfile = async () => {
12839
13244
  if (!auth.isAuthenticated) {
12840
13245
  setError('You must be logged in to manage your account');
@@ -12843,10 +13248,7 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12843
13248
  setLoading(true);
12844
13249
  setError(undefined);
12845
13250
  try {
12846
- // TODO: Backend implementation required
12847
- // Endpoint: GET /api/v1/authkit/:clientId/account/profile
12848
- // SDK method: smartlinks.authKit.getProfile(clientId)
12849
- // Temporary mock data for UI testing
13251
+ // Get base profile from auth context
12850
13252
  const profileData = {
12851
13253
  uid: auth.user?.uid || '',
12852
13254
  email: auth.user?.email,
@@ -12858,6 +13260,17 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12858
13260
  };
12859
13261
  setProfile(profileData);
12860
13262
  setDisplayName(profileData.displayName || '');
13263
+ // Load contact custom fields if collectionId provided
13264
+ if (collectionId && auth.contact) {
13265
+ setCustomFieldValues(auth.contact.customFields || {});
13266
+ }
13267
+ else if (collectionId) {
13268
+ // Try to fetch contact
13269
+ const contact = await auth.getContact?.();
13270
+ if (contact?.customFields) {
13271
+ setCustomFieldValues(contact.customFields);
13272
+ }
13273
+ }
12861
13274
  }
12862
13275
  catch (err) {
12863
13276
  const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
@@ -12868,28 +13281,39 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12868
13281
  setLoading(false);
12869
13282
  }
12870
13283
  };
13284
+ const cancelEdit = useCallback(() => {
13285
+ setEditingSection(null);
13286
+ setDisplayName(profile?.displayName || '');
13287
+ setNewEmail('');
13288
+ setEmailPassword('');
13289
+ setCurrentPassword('');
13290
+ setNewPassword('');
13291
+ setConfirmPassword('');
13292
+ setNewPhone('');
13293
+ setPhoneCode('');
13294
+ setPhoneCodeSent(false);
13295
+ setError(undefined);
13296
+ setSuccess(undefined);
13297
+ // Reset custom fields to original values
13298
+ if (auth.contact?.customFields) {
13299
+ setCustomFieldValues(auth.contact.customFields);
13300
+ }
13301
+ }, [profile, auth.contact]);
13302
+ // Get editable custom fields from schema
13303
+ const editableCustomFields = getEditableFields(schema).filter(f => !['email', 'displayName', 'phone', 'phoneNumber'].includes(f.key));
12871
13304
  const handleUpdateProfile = async (e) => {
12872
13305
  e.preventDefault();
12873
13306
  setLoading(true);
12874
13307
  setError(undefined);
12875
13308
  setSuccess(undefined);
12876
13309
  try {
12877
- // TODO: Backend implementation required
12878
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-profile
12879
- // SDK method: smartlinks.authKit.updateProfile(clientId, updateData)
12880
- setError('Backend API not yet implemented. See console for required endpoint.');
12881
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-profile');
12882
- console.log('Update data:', { displayName });
12883
- // Uncomment when backend is ready:
12884
- // const updateData: ProfileUpdateData = {
12885
- // displayName: displayName || undefined,
12886
- // photoURL: photoURL || undefined,
12887
- // };
12888
- // const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
12889
- // setProfile(updatedProfile);
12890
- // setSuccess('Profile updated successfully!');
12891
- // setEditingSection(null);
12892
- // onProfileUpdated?.(updatedProfile);
13310
+ await smartlinks.authKit.updateProfile(clientId, { displayName });
13311
+ setSuccess('Profile updated successfully!');
13312
+ setEditingSection(null);
13313
+ // Update local state
13314
+ if (profile) {
13315
+ setProfile({ ...profile, displayName });
13316
+ }
12893
13317
  }
12894
13318
  catch (err) {
12895
13319
  const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
@@ -12900,19 +13324,29 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12900
13324
  setLoading(false);
12901
13325
  }
12902
13326
  };
12903
- const cancelEdit = () => {
12904
- setEditingSection(null);
12905
- setDisplayName(profile?.displayName || '');
12906
- setNewEmail('');
12907
- setEmailPassword('');
12908
- setCurrentPassword('');
12909
- setNewPassword('');
12910
- setConfirmPassword('');
12911
- setNewPhone('');
12912
- setPhoneCode('');
12913
- setPhoneCodeSent(false);
13327
+ const handleUpdateCustomFields = async (e) => {
13328
+ e.preventDefault();
13329
+ if (!collectionId) {
13330
+ setError('Collection ID is required to update custom fields');
13331
+ return;
13332
+ }
13333
+ setLoading(true);
12914
13334
  setError(undefined);
12915
13335
  setSuccess(undefined);
13336
+ try {
13337
+ console.log('[AccountManagement] Updating custom fields:', customFieldValues);
13338
+ await auth.updateContactCustomFields?.(customFieldValues);
13339
+ setSuccess('Profile updated successfully!');
13340
+ setEditingSection(null);
13341
+ }
13342
+ catch (err) {
13343
+ const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
13344
+ setError(errorMessage);
13345
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
13346
+ }
13347
+ finally {
13348
+ setLoading(false);
13349
+ }
12916
13350
  };
12917
13351
  const handleChangeEmail = async (e) => {
12918
13352
  e.preventDefault();
@@ -12920,21 +13354,12 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12920
13354
  setError(undefined);
12921
13355
  setSuccess(undefined);
12922
13356
  try {
12923
- // TODO: Backend implementation required
12924
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-email
12925
- // SDK method: smartlinks.authKit.changeEmail(clientId, newEmail, password)
12926
- // Note: No verification flow for now - direct email update
12927
- setError('Backend API not yet implemented. See console for required endpoint.');
12928
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-email');
12929
- console.log('Data:', { newEmail });
12930
- // Uncomment when backend is ready:
12931
- // await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
12932
- // setSuccess('Email changed successfully!');
12933
- // setEditingSection(null);
12934
- // setNewEmail('');
12935
- // setEmailPassword('');
12936
- // onEmailChangeRequested?.();
12937
- // await loadProfile(); // Reload to show new email
13357
+ const redirectUrl = window.location.href;
13358
+ await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword, redirectUrl);
13359
+ setSuccess('Email change requested. Please check your new email for verification.');
13360
+ setEditingSection(null);
13361
+ setNewEmail('');
13362
+ setEmailPassword('');
12938
13363
  }
12939
13364
  catch (err) {
12940
13365
  const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
@@ -12959,20 +13384,12 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
12959
13384
  setError(undefined);
12960
13385
  setSuccess(undefined);
12961
13386
  try {
12962
- // TODO: Backend implementation required
12963
- // Endpoint: POST /api/v1/authkit/:clientId/account/change-password
12964
- // SDK method: smartlinks.authKit.changePassword(clientId, currentPassword, newPassword)
12965
- setError('Backend API not yet implemented. See console for required endpoint.');
12966
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-password');
12967
- console.log('Data: currentPassword and newPassword provided');
12968
- // Uncomment when backend is ready:
12969
- // await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
12970
- // setSuccess('Password changed successfully!');
12971
- // setEditingSection(null);
12972
- // setCurrentPassword('');
12973
- // setNewPassword('');
12974
- // setConfirmPassword('');
12975
- // onPasswordChanged?.();
13387
+ await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
13388
+ setSuccess('Password changed successfully!');
13389
+ setEditingSection(null);
13390
+ setCurrentPassword('');
13391
+ setNewPassword('');
13392
+ setConfirmPassword('');
12976
13393
  }
12977
13394
  catch (err) {
12978
13395
  const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
@@ -13006,20 +13423,13 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13006
13423
  setError(undefined);
13007
13424
  setSuccess(undefined);
13008
13425
  try {
13009
- // TODO: Backend implementation required
13010
- // Endpoint: POST /api/v1/authkit/:clientId/account/update-phone
13011
- // SDK method: smartlinks.authKit.updatePhone(clientId, phoneNumber, verificationCode)
13012
- setError('Backend API not yet implemented. See console for required endpoint.');
13013
- console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-phone');
13014
- console.log('Data:', { phoneNumber: newPhone, code: phoneCode });
13015
- // Uncomment when backend is ready:
13016
- // await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
13017
- // setSuccess('Phone number updated successfully!');
13018
- // setEditingSection(null);
13019
- // setNewPhone('');
13020
- // setPhoneCode('');
13021
- // setPhoneCodeSent(false);
13022
- // await loadProfile();
13426
+ await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
13427
+ setSuccess('Phone number updated successfully!');
13428
+ setEditingSection(null);
13429
+ setNewPhone('');
13430
+ setPhoneCode('');
13431
+ setPhoneCodeSent(false);
13432
+ await loadProfile();
13023
13433
  }
13024
13434
  catch (err) {
13025
13435
  const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
@@ -13042,19 +13452,9 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13042
13452
  setLoading(true);
13043
13453
  setError(undefined);
13044
13454
  try {
13045
- // TODO: Backend implementation required
13046
- // Endpoint: DELETE /api/v1/authkit/:clientId/account/delete
13047
- // SDK method: smartlinks.authKit.deleteAccount(clientId, password, confirmText)
13048
- // Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
13049
- setError('Backend API not yet implemented. See console for required endpoint.');
13050
- console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
13051
- console.log('Data: password and confirmText="DELETE" provided');
13052
- console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
13053
- // Uncomment when backend is ready:
13054
- // await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
13055
- // setSuccess('Account deleted successfully');
13056
- // onAccountDeleted?.();
13057
- // await auth.logout();
13455
+ await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
13456
+ setSuccess('Account deleted successfully');
13457
+ await auth.logout();
13058
13458
  }
13059
13459
  catch (err) {
13060
13460
  const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
@@ -13065,120 +13465,24 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
13065
13465
  setLoading(false);
13066
13466
  }
13067
13467
  };
13468
+ const handleCustomFieldChange = (key, value) => {
13469
+ setCustomFieldValues(prev => ({ ...prev, [key]: value }));
13470
+ };
13068
13471
  if (!auth.isAuthenticated) {
13069
13472
  return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Please log in to manage your account" }) }));
13070
13473
  }
13071
- if (loading && !profile) {
13474
+ if ((loading || schemaLoading) && !profile) {
13072
13475
  return (jsx("div", { className: `account-management ${className}`, children: jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
13073
13476
  }
13074
- return (jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Display Name" }), jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsx("div", { className: "form-group", children: jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Email Address" }), jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Password" }), jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Phone Number" }), jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxs("section", { className: "account-section danger-zone", children: [jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxs("div", { className: "delete-confirm", children: [jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsx("button", { type: "button", onClick: () => {
13477
+ return (jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Display Name" }), jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsx("div", { className: "form-group", children: jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showCustomFields && collectionId && editableCustomFields.length > 0 && (jsxs("section", { className: "account-section", children: [jsx("h3", { className: "section-title", style: { fontSize: '0.9rem', fontWeight: 600, marginBottom: '12px' }, children: "Additional Information" }), editingSection !== 'customFields' ? (jsxs(Fragment, { children: [editableCustomFields.map((field) => (jsx("div", { className: "account-field", children: jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: field.label }), jsx("div", { className: "field-value", children: customFieldValues[field.key] !== undefined && customFieldValues[field.key] !== ''
13478
+ ? String(customFieldValues[field.key])
13479
+ : 'Not set' })] }) }, field.key))), jsx("button", { type: "button", onClick: () => setEditingSection('customFields'), className: "auth-button button-secondary", style: { marginTop: '8px' }, children: "Edit Information" })] })) : (jsxs("form", { onSubmit: handleUpdateCustomFields, className: "edit-form", children: [editableCustomFields.map((field) => (jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key))), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Email Address" }), jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Password" }), jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsxs("div", { className: "account-field", children: [jsxs("div", { className: "field-info", children: [jsx("label", { className: "field-label", children: "Phone Number" }), jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxs("section", { className: "account-section danger-zone", children: [jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxs("div", { className: "delete-confirm", children: [jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxs("div", { className: "button-group", children: [jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsx("button", { type: "button", onClick: () => {
13075
13480
  setShowDeleteConfirm(false);
13076
13481
  setDeletePassword('');
13077
13482
  setDeleteConfirmText('');
13078
13483
  }, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
13079
13484
  };
13080
13485
 
13081
- const SmartlinksClaimUI = (props) => {
13082
- // Destructure AFTER logging raw props to debug proxyMode issue
13083
- const { apiEndpoint, clientId, clientName, collectionId, productId, proofId, onClaimSuccess, onClaimError, additionalFields = [], theme = 'light', className = '', minimal = false, proxyMode = false, customization = {}, } = props;
13084
- // Debug logging for proxyMode - log RAW props first
13085
- console.log('[SmartlinksClaimUI] 🔍 RAW props received:', props);
13086
- console.log('[SmartlinksClaimUI] 🔍 props.proxyMode value:', props.proxyMode);
13087
- console.log('[SmartlinksClaimUI] 🔍 typeof props.proxyMode:', typeof props.proxyMode);
13088
- console.log('[SmartlinksClaimUI] 🎯 Destructured proxyMode:', proxyMode);
13089
- const auth = useAuth();
13090
- const [claimStep, setClaimStep] = useState(auth.isAuthenticated ? 'questions' : 'auth');
13091
- const [claimData, setClaimData] = useState({});
13092
- const [error, setError] = useState();
13093
- const [loading, setLoading] = useState(false);
13094
- const handleAuthSuccess = (token, user, accountData) => {
13095
- // Authentication successful
13096
- auth.login(token, user, accountData);
13097
- // If no additional questions, proceed directly to claim
13098
- if (additionalFields.length === 0) {
13099
- executeClaim(user);
13100
- }
13101
- else {
13102
- setClaimStep('questions');
13103
- }
13104
- };
13105
- const handleQuestionSubmit = async (e) => {
13106
- e.preventDefault();
13107
- // Validate required fields
13108
- const missingFields = additionalFields
13109
- .filter(field => field.required && !claimData[field.name])
13110
- .map(field => field.label);
13111
- if (missingFields.length > 0) {
13112
- setError(`Please fill in: ${missingFields.join(', ')}`);
13113
- return;
13114
- }
13115
- // Execute claim with collected data
13116
- if (auth.user) {
13117
- executeClaim(auth.user);
13118
- }
13119
- };
13120
- const executeClaim = async (user) => {
13121
- setClaimStep('claiming');
13122
- setLoading(true);
13123
- setError(undefined);
13124
- try {
13125
- // Create attestation to claim the proof
13126
- const response = await smartlinks.attestation.create(collectionId, productId, proofId, {
13127
- public: {
13128
- claimed: true,
13129
- claimedAt: new Date().toISOString(),
13130
- claimedBy: user.uid,
13131
- ...claimData,
13132
- },
13133
- private: {},
13134
- proof: {},
13135
- });
13136
- setClaimStep('success');
13137
- // Call success callback
13138
- onClaimSuccess({
13139
- proofId,
13140
- user,
13141
- claimData,
13142
- attestationId: response.id,
13143
- });
13144
- }
13145
- catch (err) {
13146
- console.error('Claim error:', err);
13147
- const errorMessage = err instanceof Error ? err.message : 'Failed to claim proof';
13148
- setError(errorMessage);
13149
- onClaimError?.(err instanceof Error ? err : new Error(errorMessage));
13150
- setClaimStep(additionalFields.length > 0 ? 'questions' : 'auth');
13151
- }
13152
- finally {
13153
- setLoading(false);
13154
- }
13155
- };
13156
- const handleFieldChange = (fieldName, value) => {
13157
- setClaimData(prev => ({
13158
- ...prev,
13159
- [fieldName]: value,
13160
- }));
13161
- };
13162
- // Render authentication step
13163
- if (claimStep === 'auth') {
13164
- console.log('[SmartlinksClaimUI] 🔑 Rendering auth step with proxyMode:', proxyMode);
13165
- return (jsx("div", { className: className, children: jsx(SmartlinksAuthUI, { apiEndpoint: apiEndpoint, clientId: clientId, clientName: clientName, onAuthSuccess: handleAuthSuccess, onAuthError: onClaimError, theme: theme, minimal: minimal, proxyMode: proxyMode, customization: customization.authConfig }) }));
13166
- }
13167
- // Render additional questions step
13168
- if (claimStep === 'questions') {
13169
- return (jsxs("div", { className: `claim-questions ${className}`, children: [jsxs("div", { className: "claim-header mb-6", children: [jsx("h2", { className: "text-2xl font-bold mb-2", children: customization.claimTitle || 'Complete Your Claim' }), customization.claimDescription && (jsx("p", { className: "text-muted-foreground", children: customization.claimDescription }))] }), error && (jsx("div", { className: "claim-error bg-destructive/10 text-destructive px-4 py-3 rounded-md mb-4", children: error })), jsxs("form", { onSubmit: handleQuestionSubmit, className: "claim-form space-y-4", children: [additionalFields.map((field) => (jsxs("div", { className: "claim-field", children: [jsxs("label", { htmlFor: field.name, className: "block text-sm font-medium mb-2", children: [field.label, field.required && jsx("span", { className: "text-destructive ml-1", children: "*" })] }), field.type === 'textarea' ? (jsx("textarea", { id: field.name, name: field.name, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", rows: 4 })) : field.type === 'select' ? (jsxs("select", { id: field.name, name: field.name, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", children: [jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsx("option", { value: option, children: option }, option)))] })) : (jsx("input", { id: field.name, name: field.name, type: field.type, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background" }))] }, field.name))), jsx("button", { type: "submit", disabled: loading, className: "claim-submit-button w-full bg-primary text-primary-foreground px-4 py-2 rounded-md font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed", children: loading ? 'Claiming...' : 'Submit Claim' })] })] }));
13170
- }
13171
- // Render claiming step (loading state)
13172
- if (claimStep === 'claiming') {
13173
- return (jsxs("div", { className: `claim-loading ${className} flex flex-col items-center justify-center py-12`, children: [jsx("div", { className: "claim-spinner w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mb-4" }), jsx("p", { className: "text-muted-foreground", children: "Claiming your product..." })] }));
13174
- }
13175
- // Render success step
13176
- if (claimStep === 'success') {
13177
- return (jsxs("div", { className: `claim-success ${className} text-center py-12`, children: [jsx("div", { className: "claim-success-icon w-16 h-16 bg-green-500 text-white rounded-full flex items-center justify-center text-3xl font-bold mx-auto mb-4", children: "\u2713" }), jsx("h2", { className: "text-2xl font-bold mb-2", children: "Claim Successful!" }), jsx("p", { className: "text-muted-foreground", children: customization.successMessage || 'Your product has been successfully claimed and registered to your account.' })] }));
13178
- }
13179
- return null;
13180
- };
13181
-
13182
13486
  const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
13183
13487
  const { isAuthenticated, isLoading } = useAuth();
13184
13488
  // Show loading state
@@ -13228,5 +13532,5 @@ const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', '
13228
13532
  jsxs(Fragment, { children: [showEmail && (jsxs("div", { className: "auth-form", children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Email" }), jsx("input", { type: "email", className: "auth-input", placeholder: "Enter your email", disabled: true })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { className: "auth-label", children: "Password" }), jsx("input", { type: "password", className: "auth-input", placeholder: "Enter your password", disabled: true })] }), jsx("button", { className: "auth-button auth-button-primary", disabled: true, children: "Sign In" }), jsx("div", { style: { textAlign: 'center', marginTop: '1rem' }, children: jsx("button", { className: "auth-link", disabled: true, children: "Forgot password?" }) }), jsxs("div", { style: { textAlign: 'center', marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--auth-text-muted, #6B7280)' }, children: ["Don't have an account?", ' ', jsx("button", { className: "auth-link", disabled: true, children: "Sign up" })] })] })), hasOtherProviders && (jsxs(Fragment, { children: [showEmail && (jsx("div", { className: "auth-or-divider", children: jsx("span", { children: "or continue with" }) })), jsx("div", { className: "auth-provider-buttons", children: orderedProviders.map(provider => renderProviderButton(provider)) })] }))] })) }));
13229
13533
  };
13230
13534
 
13231
- export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SmartlinksAuthUI, SmartlinksClaimUI, tokenStorage, useAuth };
13535
+ export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SchemaFieldRenderer, SmartlinksAuthUI, getEditableFields, getRegistrationFields, sortFieldsByPlacement, tokenStorage, useAuth };
13232
13536
  //# sourceMappingURL=index.esm.js.map