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