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