@proveanything/smartlinks-auth-ui 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -47
- package/dist/api.d.ts +1 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/components/AccountManagement.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.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +558 -254
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +560 -253
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +41 -52
- 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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
4
|
var React = require('react');
|
|
5
5
|
var smartlinks = require('@proveanything/smartlinks');
|
|
6
|
+
var http = require('@proveanything/smartlinks/dist/http');
|
|
6
7
|
|
|
7
8
|
function _interopNamespaceDefault(e) {
|
|
8
9
|
var n = Object.create(null);
|
|
@@ -78,22 +79,145 @@ const AuthContainer = ({ children, theme = 'light', className = '', config, mini
|
|
|
78
79
|
return (jsxRuntime.jsx("div", { className: containerClass, children: jsxRuntime.jsxs("div", { className: cardClass, style: !minimal && config?.branding?.buttonStyle === 'square' ? { borderRadius: '4px' } : undefined, children: [(logoUrl || title || subtitle) && (jsxRuntime.jsxs("div", { className: "auth-header", children: [logoUrl && (jsxRuntime.jsx("div", { className: "auth-logo", children: jsxRuntime.jsx("img", { src: logoUrl, alt: "Logo", style: { maxWidth: '200px', height: 'auto', objectFit: 'contain' } }) })), title && jsxRuntime.jsx("h1", { className: "auth-title", children: title }), subtitle && jsxRuntime.jsx("p", { className: "auth-subtitle", children: subtitle })] })), jsxRuntime.jsx("div", { className: "auth-content", children: children }), (config?.branding?.termsUrl || config?.branding?.privacyUrl) && (jsxRuntime.jsxs("div", { className: "auth-footer", children: [config.branding.termsUrl && jsxRuntime.jsx("a", { href: config.branding.termsUrl, target: "_blank", rel: "noopener noreferrer", children: "Terms" }), config.branding.termsUrl && config.branding.privacyUrl && jsxRuntime.jsx("span", { children: "\u2022" }), config.branding.privacyUrl && jsxRuntime.jsx("a", { href: config.branding.privacyUrl, target: "_blank", rel: "noopener noreferrer", children: "Privacy" })] }))] }) }));
|
|
79
80
|
};
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Renders a form field based on schema definition
|
|
84
|
+
* Used in both registration and account management forms
|
|
85
|
+
*/
|
|
86
|
+
const SchemaFieldRenderer = ({ field, value, onChange, disabled = false, error, className = '', }) => {
|
|
87
|
+
const handleChange = (newValue) => {
|
|
88
|
+
onChange(field.key, newValue);
|
|
89
|
+
};
|
|
90
|
+
const inputId = `field-${field.key}`;
|
|
91
|
+
const commonProps = {
|
|
92
|
+
id: inputId,
|
|
93
|
+
className: `auth-input ${error ? 'auth-input-error' : ''} ${className}`,
|
|
94
|
+
disabled: disabled || field.readOnly,
|
|
95
|
+
'aria-describedby': error ? `${inputId}-error` : undefined,
|
|
96
|
+
};
|
|
97
|
+
const renderField = () => {
|
|
98
|
+
switch (field.widget) {
|
|
99
|
+
case 'select':
|
|
100
|
+
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) => {
|
|
101
|
+
const optionValue = typeof option === 'string' ? option : option.value;
|
|
102
|
+
const optionLabel = typeof option === 'string' ? option : option.label;
|
|
103
|
+
return (jsxRuntime.jsx("option", { value: optionValue, children: optionLabel }, optionValue));
|
|
104
|
+
})] }));
|
|
105
|
+
case 'checkbox':
|
|
106
|
+
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 })] }));
|
|
107
|
+
case 'textarea':
|
|
108
|
+
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 }));
|
|
109
|
+
case 'number':
|
|
110
|
+
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 }));
|
|
111
|
+
case 'date':
|
|
112
|
+
return (jsxRuntime.jsx("input", { ...commonProps, type: "date", value: value || '', onChange: (e) => handleChange(e.target.value), required: field.required }));
|
|
113
|
+
case 'tel':
|
|
114
|
+
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" }));
|
|
115
|
+
case 'email':
|
|
116
|
+
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" }));
|
|
117
|
+
case 'text':
|
|
118
|
+
default:
|
|
119
|
+
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 }));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
// Checkbox has its own label
|
|
123
|
+
if (field.widget === 'checkbox') {
|
|
124
|
+
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 }))] }));
|
|
125
|
+
}
|
|
126
|
+
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 }))] }));
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Helper to get all editable fields from schema
|
|
130
|
+
*/
|
|
131
|
+
const getEditableFields = (schema) => {
|
|
132
|
+
if (!schema)
|
|
133
|
+
return [];
|
|
134
|
+
const editableKeys = new Set(schema.settings.publicEditableFields);
|
|
135
|
+
const coreEditable = schema.fields.filter(f => f.editable && f.visible && editableKeys.has(f.key));
|
|
136
|
+
const customEditable = schema.customFields.filter(f => f.editable && f.visible);
|
|
137
|
+
return [...coreEditable, ...customEditable];
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Helper to get registration fields based on config
|
|
141
|
+
*/
|
|
142
|
+
const getRegistrationFields = (schema, registrationConfig) => {
|
|
143
|
+
if (!schema || !registrationConfig.length)
|
|
144
|
+
return [];
|
|
145
|
+
const configMap = new Map(registrationConfig.map(c => [c.key, c]));
|
|
146
|
+
const allFields = [...schema.fields, ...schema.customFields];
|
|
147
|
+
return allFields
|
|
148
|
+
.filter(field => {
|
|
149
|
+
const config = configMap.get(field.key);
|
|
150
|
+
return config?.showDuringRegistration && field.visible;
|
|
151
|
+
})
|
|
152
|
+
.map(field => {
|
|
153
|
+
const config = configMap.get(field.key);
|
|
154
|
+
// Allow registration config to override required status
|
|
155
|
+
if (config?.required !== undefined) {
|
|
156
|
+
return { ...field, required: config.required };
|
|
157
|
+
}
|
|
158
|
+
return field;
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Sort fields by placement (inline first, then post-credentials)
|
|
163
|
+
*/
|
|
164
|
+
const sortFieldsByPlacement = (fields, registrationConfig) => {
|
|
165
|
+
const configMap = new Map(registrationConfig.map(c => [c.key, c]));
|
|
166
|
+
const inline = [];
|
|
167
|
+
const postCredentials = [];
|
|
168
|
+
fields.forEach(field => {
|
|
169
|
+
const config = configMap.get(field.key);
|
|
170
|
+
if (config?.placement === 'post-credentials') {
|
|
171
|
+
postCredentials.push(field);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
inline.push(field);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return { inline, postCredentials };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, schema, registrationFieldsConfig = [], additionalFields = [], }) => {
|
|
82
181
|
const [formData, setFormData] = React.useState({
|
|
83
182
|
email: '',
|
|
84
183
|
password: '',
|
|
85
184
|
displayName: '',
|
|
86
185
|
});
|
|
186
|
+
// Custom field values (separate from core AuthFormData)
|
|
187
|
+
const [customFieldValues, setCustomFieldValues] = React.useState({});
|
|
188
|
+
// Get schema-driven registration fields
|
|
189
|
+
const schemaFields = React.useMemo(() => {
|
|
190
|
+
if (!schema || !registrationFieldsConfig.length)
|
|
191
|
+
return { inline: [], postCredentials: [] };
|
|
192
|
+
const fields = getRegistrationFields(schema, registrationFieldsConfig);
|
|
193
|
+
return sortFieldsByPlacement(fields, registrationFieldsConfig);
|
|
194
|
+
}, [schema, registrationFieldsConfig]);
|
|
195
|
+
// Check if we have any additional fields to show
|
|
196
|
+
const hasSchemaFields = schemaFields.inline.length > 0 || schemaFields.postCredentials.length > 0;
|
|
197
|
+
const hasLegacyFields = additionalFields.length > 0 && !hasSchemaFields;
|
|
87
198
|
const handleSubmit = async (e) => {
|
|
88
199
|
e.preventDefault();
|
|
89
|
-
|
|
200
|
+
// Merge custom field values into accountData for registration
|
|
201
|
+
const submitData = {
|
|
202
|
+
...formData,
|
|
203
|
+
accountData: mode === 'register'
|
|
204
|
+
? { ...(formData.accountData || {}), customFields: customFieldValues }
|
|
205
|
+
: undefined,
|
|
206
|
+
};
|
|
207
|
+
await onSubmit(submitData);
|
|
90
208
|
};
|
|
91
209
|
const handleChange = (field, value) => {
|
|
92
210
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
93
211
|
};
|
|
212
|
+
const handleCustomFieldChange = (key, value) => {
|
|
213
|
+
setCustomFieldValues(prev => ({ ...prev, [key]: value }));
|
|
214
|
+
};
|
|
215
|
+
const renderSchemaField = (field) => (jsxRuntime.jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key));
|
|
216
|
+
// Legacy field renderer (for backward compatibility)
|
|
217
|
+
const renderLegacyField = (field) => (jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsxs("label", { htmlFor: field.name, className: "auth-label", children: [field.label, field.required && jsxRuntime.jsx("span", { style: { color: 'var(--auth-error-color, #ef4444)' }, children: " *" })] }), field.type === 'select' ? (jsxRuntime.jsxs("select", { id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, children: [jsxRuntime.jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsxRuntime.jsx("option", { value: option, children: option }, option)))] })) : field.type === 'textarea' ? (jsxRuntime.jsx("textarea", { id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, placeholder: field.placeholder, rows: 3, style: { minHeight: '80px', resize: 'vertical' } })) : (jsxRuntime.jsx("input", { type: field.type, id: field.name, className: "auth-input", value: formData[field.name] || '', onChange: (e) => handleChange(field.name, e.target.value), required: field.required, disabled: loading, placeholder: field.placeholder }))] }, field.name));
|
|
94
218
|
return (jsxRuntime.jsxs("form", { className: "auth-form", onSubmit: handleSubmit, children: [jsxRuntime.jsxs("div", { className: "auth-form-header", children: [jsxRuntime.jsx("h2", { className: "auth-form-title", children: mode === 'login' ? 'Sign in' : 'Create account' }), jsxRuntime.jsx("p", { className: "auth-form-subtitle", children: mode === 'login'
|
|
95
219
|
? 'Welcome back! Please enter your credentials.'
|
|
96
|
-
: 'Get started by creating your account.' })] }), error && (jsxRuntime.jsxs("div", { className: "auth-error", role: "alert", children: [jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsxRuntime.jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), mode === 'register' && (jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsxRuntime.jsx("input", { type: "text", id: "displayName", className: "auth-input", value: formData.displayName || '', onChange: (e) => handleChange('displayName', e.target.value), required: mode === 'register', disabled: loading, placeholder: "John Doe" })] })), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsxRuntime.jsx("input", { type: "email", id: "email", className: "auth-input", value: formData.email || '', onChange: (e) => handleChange('email', e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] }), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsxRuntime.jsx("input", { type: "password", id: "password", className: "auth-input", value: formData.password || '', onChange: (e) => handleChange('password', e.target.value), required: true, disabled: loading, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === 'login' ? 'current-password' : 'new-password', minLength: 6 })] }), mode === 'register' &&
|
|
220
|
+
: 'Get started by creating your account.' })] }), error && (jsxRuntime.jsxs("div", { className: "auth-error", role: "alert", children: [jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: jsxRuntime.jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z" }) }), error] })), mode === 'register' && (jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "displayName", className: "auth-label", children: "Full Name" }), jsxRuntime.jsx("input", { type: "text", id: "displayName", className: "auth-input", value: formData.displayName || '', onChange: (e) => handleChange('displayName', e.target.value), required: mode === 'register', disabled: loading, placeholder: "John Doe" })] })), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "email", className: "auth-label", children: "Email address" }), jsxRuntime.jsx("input", { type: "email", id: "email", className: "auth-input", value: formData.email || '', onChange: (e) => handleChange('email', e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] }), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "password", className: "auth-label", children: "Password" }), jsxRuntime.jsx("input", { type: "password", id: "password", className: "auth-input", value: formData.password || '', onChange: (e) => handleChange('password', e.target.value), required: true, disabled: loading, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === 'login' ? 'current-password' : 'new-password', minLength: 6 })] }), mode === 'register' && hasSchemaFields && schemaFields.inline.map(renderSchemaField), mode === 'register' && hasLegacyFields && additionalFields.map(renderLegacyField), mode === 'register' && hasSchemaFields && schemaFields.postCredentials.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "auth-divider", style: { margin: '16px 0' }, children: jsxRuntime.jsx("span", { children: "Additional Information" }) }), schemaFields.postCredentials.map(renderSchemaField)] })), mode === 'login' && (jsxRuntime.jsx("div", { className: "auth-form-footer", children: jsxRuntime.jsx("button", { type: "button", className: "auth-link", onClick: onForgotPassword, disabled: loading, children: "Forgot password?" }) })), jsxRuntime.jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsxRuntime.jsx("span", { className: "auth-spinner" })) : mode === 'login' ? ('Sign in') : ('Create account') }), jsxRuntime.jsxs("div", { className: "auth-divider", children: [jsxRuntime.jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), jsxRuntime.jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' })] })] }));
|
|
97
221
|
};
|
|
98
222
|
|
|
99
223
|
const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, loading, }) => {
|
|
@@ -10661,6 +10785,8 @@ class AuthAPI {
|
|
|
10661
10785
|
return smartlinks__namespace.authKit.login(this.clientId, email, password);
|
|
10662
10786
|
}
|
|
10663
10787
|
async register(data) {
|
|
10788
|
+
// Note: redirectUrl is not passed to register - verification email is sent separately
|
|
10789
|
+
// via sendEmailVerification() after registration for verify-then-* modes
|
|
10664
10790
|
return smartlinks__namespace.authKit.register(this.clientId, {
|
|
10665
10791
|
email: data.email,
|
|
10666
10792
|
password: data.password,
|
|
@@ -10684,6 +10810,18 @@ class AuthAPI {
|
|
|
10684
10810
|
// Pass token to SDK - backend verifies with Google
|
|
10685
10811
|
return smartlinks__namespace.authKit.googleLogin(this.clientId, token);
|
|
10686
10812
|
}
|
|
10813
|
+
async loginWithGoogleCode(code, redirectUri) {
|
|
10814
|
+
this.log.log('loginWithGoogleCode called:', {
|
|
10815
|
+
codeLength: code?.length,
|
|
10816
|
+
redirectUri,
|
|
10817
|
+
});
|
|
10818
|
+
// Exchange authorization code for tokens via backend
|
|
10819
|
+
// Use direct HTTP call since SDK may not have this method in authKit namespace yet
|
|
10820
|
+
return http.post(`/api/v1/authkit/${this.clientId}/google-code`, {
|
|
10821
|
+
code,
|
|
10822
|
+
redirectUri,
|
|
10823
|
+
});
|
|
10824
|
+
}
|
|
10687
10825
|
async sendPhoneCode(phoneNumber) {
|
|
10688
10826
|
return smartlinks__namespace.authKit.sendPhoneCode(this.clientId, phoneNumber);
|
|
10689
10827
|
}
|
|
@@ -11167,7 +11305,7 @@ const tokenStorage = {
|
|
|
11167
11305
|
const storage = await getStorage();
|
|
11168
11306
|
const authToken = {
|
|
11169
11307
|
token,
|
|
11170
|
-
expiresAt: expiresAt || Date.now() +
|
|
11308
|
+
expiresAt: expiresAt || Date.now() + (7 * 24 * 60 * 60 * 1000), // Default 7 days (matches backend JWT)
|
|
11171
11309
|
};
|
|
11172
11310
|
await storage.setItem(TOKEN_KEY, authToken);
|
|
11173
11311
|
},
|
|
@@ -11178,7 +11316,8 @@ const tokenStorage = {
|
|
|
11178
11316
|
return null;
|
|
11179
11317
|
// Check if token is expired
|
|
11180
11318
|
if (authToken.expiresAt && authToken.expiresAt < Date.now()) {
|
|
11181
|
-
|
|
11319
|
+
console.log('[TokenStorage] Token expired at:', new Date(authToken.expiresAt).toISOString(), '- clearing all auth data');
|
|
11320
|
+
await this.clearAll(); // Clear ALL auth data to prevent orphaned state
|
|
11182
11321
|
return null;
|
|
11183
11322
|
}
|
|
11184
11323
|
return authToken;
|
|
@@ -11258,6 +11397,9 @@ const tokenStorage = {
|
|
|
11258
11397
|
|
|
11259
11398
|
const AuthContext = React.createContext(undefined);
|
|
11260
11399
|
const AuthProvider = ({ children, proxyMode = false, accountCacheTTL = 5 * 60 * 1000, preloadAccountInfo = false,
|
|
11400
|
+
// Token refresh settings
|
|
11401
|
+
enableAutoRefresh = true, refreshThresholdPercent = 75, // Refresh when 75% of token lifetime has passed
|
|
11402
|
+
refreshCheckInterval = 60 * 1000, // Check every minute
|
|
11261
11403
|
// Contact & Interaction features
|
|
11262
11404
|
collectionId, enableContactSync, enableInteractionTracking, interactionAppId, interactionConfig, }) => {
|
|
11263
11405
|
const [user, setUser] = React.useState(null);
|
|
@@ -11678,11 +11820,11 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
11678
11820
|
unsubscribe();
|
|
11679
11821
|
};
|
|
11680
11822
|
}, [proxyMode, notifyAuthStateChange]);
|
|
11681
|
-
const login = React.useCallback(async (authToken, authUser, authAccountData, isNewUser) => {
|
|
11823
|
+
const login = React.useCallback(async (authToken, authUser, authAccountData, isNewUser, expiresAt) => {
|
|
11682
11824
|
try {
|
|
11683
11825
|
// Only persist to storage in standalone mode
|
|
11684
11826
|
if (!proxyMode) {
|
|
11685
|
-
await tokenStorage.saveToken(authToken);
|
|
11827
|
+
await tokenStorage.saveToken(authToken, expiresAt);
|
|
11686
11828
|
await tokenStorage.saveUser(authUser);
|
|
11687
11829
|
if (authAccountData) {
|
|
11688
11830
|
await tokenStorage.saveAccountData(authAccountData);
|
|
@@ -11765,9 +11907,67 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
11765
11907
|
const storedToken = await tokenStorage.getToken();
|
|
11766
11908
|
return storedToken ? storedToken.token : null;
|
|
11767
11909
|
}, [proxyMode, token]);
|
|
11910
|
+
// Get token with expiration info
|
|
11911
|
+
const getTokenInfo = React.useCallback(async () => {
|
|
11912
|
+
if (proxyMode) {
|
|
11913
|
+
// In proxy mode, we don't have expiration info
|
|
11914
|
+
if (token) {
|
|
11915
|
+
return { token, expiresAt: 0, expiresIn: 0 };
|
|
11916
|
+
}
|
|
11917
|
+
return null;
|
|
11918
|
+
}
|
|
11919
|
+
const storedToken = await tokenStorage.getToken();
|
|
11920
|
+
if (!storedToken?.token || !storedToken.expiresAt) {
|
|
11921
|
+
return null;
|
|
11922
|
+
}
|
|
11923
|
+
return {
|
|
11924
|
+
token: storedToken.token,
|
|
11925
|
+
expiresAt: storedToken.expiresAt,
|
|
11926
|
+
expiresIn: Math.max(0, storedToken.expiresAt - Date.now()),
|
|
11927
|
+
};
|
|
11928
|
+
}, [proxyMode, token]);
|
|
11929
|
+
// Refresh token - validates current token and extends session if backend supports it
|
|
11768
11930
|
const refreshToken = React.useCallback(async () => {
|
|
11769
|
-
|
|
11770
|
-
|
|
11931
|
+
if (proxyMode) {
|
|
11932
|
+
console.log('[AuthContext] Proxy mode: token refresh handled by parent');
|
|
11933
|
+
throw new Error('Token refresh in proxy mode is handled by the parent application');
|
|
11934
|
+
}
|
|
11935
|
+
const storedToken = await tokenStorage.getToken();
|
|
11936
|
+
if (!storedToken?.token) {
|
|
11937
|
+
throw new Error('No token to refresh. Please login first.');
|
|
11938
|
+
}
|
|
11939
|
+
try {
|
|
11940
|
+
console.log('[AuthContext] Refreshing token...');
|
|
11941
|
+
// Verify current token is still valid
|
|
11942
|
+
const verifyResult = await smartlinks__namespace.auth.verifyToken(storedToken.token);
|
|
11943
|
+
if (!verifyResult.valid) {
|
|
11944
|
+
console.warn('[AuthContext] Token is no longer valid, clearing session');
|
|
11945
|
+
await logout();
|
|
11946
|
+
throw new Error('Token expired or invalid. Please login again.');
|
|
11947
|
+
}
|
|
11948
|
+
// Token is valid - extend its expiration locally
|
|
11949
|
+
// Backend JWT remains valid, we just update our local tracking
|
|
11950
|
+
const newExpiresAt = Date.now() + (7 * 24 * 60 * 60 * 1000); // 7 days from now
|
|
11951
|
+
await tokenStorage.saveToken(storedToken.token, newExpiresAt);
|
|
11952
|
+
console.log('[AuthContext] Token verified and expiration extended to:', new Date(newExpiresAt).toISOString());
|
|
11953
|
+
// Update verified state
|
|
11954
|
+
setIsVerified(true);
|
|
11955
|
+
pendingVerificationRef.current = false;
|
|
11956
|
+
notifyAuthStateChange('TOKEN_REFRESH', user, storedToken.token, accountData, accountInfo, true, contact, contactId);
|
|
11957
|
+
return storedToken.token;
|
|
11958
|
+
}
|
|
11959
|
+
catch (error) {
|
|
11960
|
+
console.error('[AuthContext] Token refresh failed:', error);
|
|
11961
|
+
// If it's a network error, don't logout
|
|
11962
|
+
if (isNetworkError(error)) {
|
|
11963
|
+
console.warn('[AuthContext] Network error during refresh, keeping session');
|
|
11964
|
+
throw error;
|
|
11965
|
+
}
|
|
11966
|
+
// Auth error - clear session
|
|
11967
|
+
await logout();
|
|
11968
|
+
throw new Error('Token refresh failed. Please login again.');
|
|
11969
|
+
}
|
|
11970
|
+
}, [proxyMode, user, accountData, accountInfo, contact, contactId, notifyAuthStateChange, isNetworkError, logout]);
|
|
11771
11971
|
const getAccount = React.useCallback(async (forceRefresh = false) => {
|
|
11772
11972
|
try {
|
|
11773
11973
|
if (proxyMode) {
|
|
@@ -11875,6 +12075,54 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
11875
12075
|
window.removeEventListener('offline', handleOffline);
|
|
11876
12076
|
};
|
|
11877
12077
|
}, [proxyMode, token, user, retryVerification]);
|
|
12078
|
+
// Automatic background token refresh
|
|
12079
|
+
React.useEffect(() => {
|
|
12080
|
+
if (proxyMode || !enableAutoRefresh || !token || !user) {
|
|
12081
|
+
return;
|
|
12082
|
+
}
|
|
12083
|
+
console.log('[AuthContext] Setting up automatic token refresh (interval:', refreshCheckInterval, 'ms, threshold:', refreshThresholdPercent, '%)');
|
|
12084
|
+
const checkAndRefresh = async () => {
|
|
12085
|
+
try {
|
|
12086
|
+
const storedToken = await tokenStorage.getToken();
|
|
12087
|
+
if (!storedToken?.expiresAt) {
|
|
12088
|
+
console.log('[AuthContext] No token expiration info, skipping refresh check');
|
|
12089
|
+
return;
|
|
12090
|
+
}
|
|
12091
|
+
const now = Date.now();
|
|
12092
|
+
const tokenLifetime = storedToken.expiresAt - (storedToken.expiresAt - (7 * 24 * 60 * 60 * 1000)); // Assume 7-day lifetime
|
|
12093
|
+
const tokenAge = now - (storedToken.expiresAt - (7 * 24 * 60 * 60 * 1000));
|
|
12094
|
+
const percentUsed = (tokenAge / tokenLifetime) * 100;
|
|
12095
|
+
// Calculate time remaining
|
|
12096
|
+
const timeRemaining = storedToken.expiresAt - now;
|
|
12097
|
+
const hoursRemaining = Math.round(timeRemaining / (60 * 60 * 1000));
|
|
12098
|
+
if (percentUsed >= refreshThresholdPercent) {
|
|
12099
|
+
console.log(`[AuthContext] Token at ${Math.round(percentUsed)}% lifetime (${hoursRemaining}h remaining), refreshing...`);
|
|
12100
|
+
try {
|
|
12101
|
+
await refreshToken();
|
|
12102
|
+
console.log('[AuthContext] Automatic token refresh successful');
|
|
12103
|
+
}
|
|
12104
|
+
catch (refreshError) {
|
|
12105
|
+
console.warn('[AuthContext] Automatic token refresh failed:', refreshError);
|
|
12106
|
+
// Don't logout on refresh failure - user can still use the app until token actually expires
|
|
12107
|
+
}
|
|
12108
|
+
}
|
|
12109
|
+
else {
|
|
12110
|
+
console.log(`[AuthContext] Token at ${Math.round(percentUsed)}% lifetime (${hoursRemaining}h remaining), no refresh needed`);
|
|
12111
|
+
}
|
|
12112
|
+
}
|
|
12113
|
+
catch (error) {
|
|
12114
|
+
console.error('[AuthContext] Error checking token for refresh:', error);
|
|
12115
|
+
}
|
|
12116
|
+
};
|
|
12117
|
+
// Check immediately on mount
|
|
12118
|
+
checkAndRefresh();
|
|
12119
|
+
// Set up periodic check
|
|
12120
|
+
const intervalId = setInterval(checkAndRefresh, refreshCheckInterval);
|
|
12121
|
+
return () => {
|
|
12122
|
+
console.log('[AuthContext] Cleaning up automatic token refresh timer');
|
|
12123
|
+
clearInterval(intervalId);
|
|
12124
|
+
};
|
|
12125
|
+
}, [proxyMode, enableAutoRefresh, refreshCheckInterval, refreshThresholdPercent, token, user, refreshToken]);
|
|
11878
12126
|
const value = {
|
|
11879
12127
|
user,
|
|
11880
12128
|
token,
|
|
@@ -11892,6 +12140,7 @@ collectionId, enableContactSync, enableInteractionTracking, interactionAppId, in
|
|
|
11892
12140
|
login,
|
|
11893
12141
|
logout,
|
|
11894
12142
|
getToken,
|
|
12143
|
+
getTokenInfo,
|
|
11895
12144
|
refreshToken,
|
|
11896
12145
|
getAccount,
|
|
11897
12146
|
refreshAccount,
|
|
@@ -11909,6 +12158,14 @@ const useAuth = () => {
|
|
|
11909
12158
|
return context;
|
|
11910
12159
|
};
|
|
11911
12160
|
|
|
12161
|
+
// Helper to calculate expiration from AuthResponse
|
|
12162
|
+
const getExpirationFromResponse = (response) => {
|
|
12163
|
+
if (response.expiresAt)
|
|
12164
|
+
return response.expiresAt;
|
|
12165
|
+
if (response.expiresIn)
|
|
12166
|
+
return Date.now() + response.expiresIn;
|
|
12167
|
+
return undefined; // Will use 7-day default in tokenStorage
|
|
12168
|
+
};
|
|
11912
12169
|
// Default Smartlinks Google OAuth Client ID (public - safe to expose)
|
|
11913
12170
|
const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
|
|
11914
12171
|
// Default auth UI configuration when no clientId is provided
|
|
@@ -11952,6 +12209,28 @@ const loadGoogleIdentityServices = () => {
|
|
|
11952
12209
|
document.head.appendChild(script);
|
|
11953
12210
|
});
|
|
11954
12211
|
};
|
|
12212
|
+
// Helper to detect WebView environments (Android/iOS)
|
|
12213
|
+
const detectWebView = () => {
|
|
12214
|
+
const ua = navigator.userAgent;
|
|
12215
|
+
// Android WebView detection
|
|
12216
|
+
if (/Android/i.test(ua)) {
|
|
12217
|
+
// Modern Android WebViews include "wv" in UA string
|
|
12218
|
+
if (/\bwv\b/i.test(ua))
|
|
12219
|
+
return true;
|
|
12220
|
+
// Check for legacy Android bridge
|
|
12221
|
+
if (typeof window.Android !== 'undefined')
|
|
12222
|
+
return true;
|
|
12223
|
+
}
|
|
12224
|
+
// iOS WKWebView detection
|
|
12225
|
+
if (/iPhone|iPad|iPod/i.test(ua)) {
|
|
12226
|
+
const hasWebKitHandlers = !!window.webkit?.messageHandlers;
|
|
12227
|
+
const isSafari = !!window.safari;
|
|
12228
|
+
// WKWebView has webkit handlers but no safari object
|
|
12229
|
+
if (hasWebKitHandlers && !isSafari)
|
|
12230
|
+
return true;
|
|
12231
|
+
}
|
|
12232
|
+
return false;
|
|
12233
|
+
};
|
|
11955
12234
|
// Helper to convert generic SDK errors to user-friendly messages
|
|
11956
12235
|
const getFriendlyErrorMessage = (errorMessage) => {
|
|
11957
12236
|
// Check for common HTTP status codes in the error message
|
|
@@ -11979,7 +12258,7 @@ const getFriendlyErrorMessage = (errorMessage) => {
|
|
|
11979
12258
|
// Return original message if no pattern matches
|
|
11980
12259
|
return errorMessage;
|
|
11981
12260
|
};
|
|
11982
|
-
const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'auto', className, customization, skipConfigFetch = false, minimal = false, logger, proxyMode = false, }) => {
|
|
12261
|
+
const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'auto', className, customization, skipConfigFetch = false, minimal = false, logger, proxyMode = false, collectionId, }) => {
|
|
11983
12262
|
const [mode, setMode] = React.useState(initialMode);
|
|
11984
12263
|
const [loading, setLoading] = React.useState(false);
|
|
11985
12264
|
const [error, setError] = React.useState();
|
|
@@ -11996,6 +12275,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
11996
12275
|
const [configLoading, setConfigLoading] = React.useState(!skipConfigFetch);
|
|
11997
12276
|
const [showEmailForm, setShowEmailForm] = React.useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
|
|
11998
12277
|
const [sdkReady, setSdkReady] = React.useState(false); // Track SDK initialization state
|
|
12278
|
+
const [contactSchema, setContactSchema] = React.useState(null); // Schema for registration fields
|
|
11999
12279
|
const log = React.useMemo(() => createLoggerWrapper(logger), [logger]);
|
|
12000
12280
|
const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
|
|
12001
12281
|
const auth = useAuth();
|
|
@@ -12170,6 +12450,24 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12170
12450
|
};
|
|
12171
12451
|
fetchConfig();
|
|
12172
12452
|
}, [apiEndpoint, clientId, customization, skipConfigFetch, sdkReady, proxyMode, log]);
|
|
12453
|
+
// Fetch contact schema for registration fields when collectionId is provided
|
|
12454
|
+
React.useEffect(() => {
|
|
12455
|
+
if (!collectionId || !sdkReady)
|
|
12456
|
+
return;
|
|
12457
|
+
const fetchSchema = async () => {
|
|
12458
|
+
try {
|
|
12459
|
+
console.log('[SmartlinksAuthUI] 📋 Fetching contact schema for collection:', collectionId);
|
|
12460
|
+
const schema = await smartlinks__namespace.contact.publicGetSchema(collectionId);
|
|
12461
|
+
console.log('[SmartlinksAuthUI] ✅ Schema loaded:', schema);
|
|
12462
|
+
setContactSchema(schema);
|
|
12463
|
+
}
|
|
12464
|
+
catch (err) {
|
|
12465
|
+
console.warn('[SmartlinksAuthUI] ⚠️ Failed to fetch schema (non-fatal):', err);
|
|
12466
|
+
// Non-fatal - registration will work without schema fields
|
|
12467
|
+
}
|
|
12468
|
+
};
|
|
12469
|
+
fetchSchema();
|
|
12470
|
+
}, [collectionId, sdkReady]);
|
|
12173
12471
|
// Reset showEmailForm when mode changes away from login/register
|
|
12174
12472
|
React.useEffect(() => {
|
|
12175
12473
|
if (mode !== 'login' && mode !== 'register') {
|
|
@@ -12194,8 +12492,15 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12194
12492
|
const params = getUrlParams();
|
|
12195
12493
|
const urlMode = params.get('mode');
|
|
12196
12494
|
const token = params.get('token');
|
|
12197
|
-
|
|
12198
|
-
|
|
12495
|
+
// Check for Google OAuth redirect callback
|
|
12496
|
+
const authCode = params.get('code');
|
|
12497
|
+
const state = params.get('state');
|
|
12498
|
+
log.log('URL params detected:', { urlMode, token, authCode: !!authCode, state: !!state, hash: window.location.hash, search: window.location.search });
|
|
12499
|
+
if (authCode && state) {
|
|
12500
|
+
// Google OAuth redirect callback
|
|
12501
|
+
handleGoogleAuthCodeCallback(authCode, state);
|
|
12502
|
+
}
|
|
12503
|
+
else if (urlMode && token) {
|
|
12199
12504
|
handleURLBasedAuth(urlMode, token);
|
|
12200
12505
|
}
|
|
12201
12506
|
}, []);
|
|
@@ -12210,18 +12515,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12210
12515
|
const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
|
|
12211
12516
|
if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
|
|
12212
12517
|
// Auto-login modes: Log the user in immediately if token is provided
|
|
12213
|
-
auth.login(response.token, response.user, response.accountData, true);
|
|
12518
|
+
auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
12214
12519
|
setAuthSuccess(true);
|
|
12215
12520
|
setSuccessMessage('Email verified successfully! You are now logged in.');
|
|
12216
12521
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
12217
12522
|
// Clear the URL parameters
|
|
12218
12523
|
const cleanUrl = window.location.href.split('?')[0];
|
|
12219
12524
|
window.history.replaceState({}, document.title, cleanUrl);
|
|
12220
|
-
//
|
|
12525
|
+
// For email verification deep links, redirect immediately if configured
|
|
12526
|
+
// (user came from an email link, so redirect is expected behavior)
|
|
12221
12527
|
if (redirectUrl) {
|
|
12222
|
-
|
|
12223
|
-
window.location.href = redirectUrl;
|
|
12224
|
-
}, 2000);
|
|
12528
|
+
window.location.href = redirectUrl;
|
|
12225
12529
|
}
|
|
12226
12530
|
}
|
|
12227
12531
|
else {
|
|
@@ -12253,18 +12557,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12253
12557
|
const response = await api.verifyMagicLink(token);
|
|
12254
12558
|
// Auto-login with magic link if token is provided
|
|
12255
12559
|
if (response.token) {
|
|
12256
|
-
auth.login(response.token, response.user, response.accountData, true);
|
|
12560
|
+
auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
12257
12561
|
setAuthSuccess(true);
|
|
12258
12562
|
setSuccessMessage('Magic link verified! You are now logged in.');
|
|
12259
12563
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
12260
12564
|
// Clear the URL parameters
|
|
12261
12565
|
const cleanUrl = window.location.href.split('?')[0];
|
|
12262
12566
|
window.history.replaceState({}, document.title, cleanUrl);
|
|
12263
|
-
//
|
|
12567
|
+
// For magic link deep links, redirect immediately if configured
|
|
12568
|
+
// (user came from an email link, so redirect is expected behavior)
|
|
12264
12569
|
if (redirectUrl) {
|
|
12265
|
-
|
|
12266
|
-
window.location.href = redirectUrl;
|
|
12267
|
-
}, 2000);
|
|
12570
|
+
window.location.href = redirectUrl;
|
|
12268
12571
|
}
|
|
12269
12572
|
}
|
|
12270
12573
|
else {
|
|
@@ -12310,6 +12613,43 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12310
12613
|
setLoading(false);
|
|
12311
12614
|
}
|
|
12312
12615
|
};
|
|
12616
|
+
// Handle Google OAuth authorization code callback (from redirect flow)
|
|
12617
|
+
const handleGoogleAuthCodeCallback = async (code, stateParam) => {
|
|
12618
|
+
setLoading(true);
|
|
12619
|
+
setError(undefined);
|
|
12620
|
+
try {
|
|
12621
|
+
// Parse state to get context
|
|
12622
|
+
const state = JSON.parse(decodeURIComponent(stateParam));
|
|
12623
|
+
log.log('Google OAuth redirect callback:', { clientId: state.clientId, returnPath: state.returnPath });
|
|
12624
|
+
// Determine the redirect URI that was used (must match exactly)
|
|
12625
|
+
const redirectUri = state.redirectUri || window.location.origin + window.location.pathname;
|
|
12626
|
+
// Exchange authorization code for tokens
|
|
12627
|
+
const response = await api.loginWithGoogleCode(code, redirectUri);
|
|
12628
|
+
if (response.token) {
|
|
12629
|
+
auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
12630
|
+
setAuthSuccess(true);
|
|
12631
|
+
setSuccessMessage('Google login successful!');
|
|
12632
|
+
onAuthSuccess(response.token, response.user, response.accountData);
|
|
12633
|
+
}
|
|
12634
|
+
else {
|
|
12635
|
+
throw new Error('Authentication failed - no token received');
|
|
12636
|
+
}
|
|
12637
|
+
// Clean URL parameters
|
|
12638
|
+
const cleanUrl = window.location.origin + window.location.pathname + (state.returnPath?.includes('#') ? state.returnPath.split('?')[0] : window.location.hash.split('?')[0]);
|
|
12639
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
12640
|
+
}
|
|
12641
|
+
catch (err) {
|
|
12642
|
+
const errorMessage = err instanceof Error ? err.message : 'Google login failed';
|
|
12643
|
+
setError(getFriendlyErrorMessage(errorMessage));
|
|
12644
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
12645
|
+
// Clean URL parameters even on error
|
|
12646
|
+
const cleanUrl = window.location.origin + window.location.pathname + window.location.hash.split('?')[0];
|
|
12647
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
12648
|
+
}
|
|
12649
|
+
finally {
|
|
12650
|
+
setLoading(false);
|
|
12651
|
+
}
|
|
12652
|
+
};
|
|
12313
12653
|
const handleEmailAuth = async (data) => {
|
|
12314
12654
|
setLoading(true);
|
|
12315
12655
|
setError(undefined);
|
|
@@ -12329,7 +12669,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12329
12669
|
// Handle different verification modes
|
|
12330
12670
|
if (verificationMode === 'immediate' && response.token) {
|
|
12331
12671
|
// Immediate mode: Log in right away if token is provided (isNewUser=true for registration)
|
|
12332
|
-
auth.login(response.token, response.user, response.accountData, true);
|
|
12672
|
+
auth.login(response.token, response.user, response.accountData, true, getExpirationFromResponse(response));
|
|
12333
12673
|
setAuthSuccess(true);
|
|
12334
12674
|
const deadline = response.emailVerificationDeadline
|
|
12335
12675
|
? new Date(response.emailVerificationDeadline).toLocaleString()
|
|
@@ -12338,19 +12678,37 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12338
12678
|
if (response.token) {
|
|
12339
12679
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
12340
12680
|
}
|
|
12341
|
-
|
|
12342
|
-
setTimeout(() => {
|
|
12343
|
-
window.location.href = redirectUrl;
|
|
12344
|
-
}, 2000);
|
|
12345
|
-
}
|
|
12681
|
+
// Note: No automatic redirect - app controls navigation via onAuthSuccess callback
|
|
12346
12682
|
}
|
|
12347
12683
|
else if (verificationMode === 'verify-then-auto-login') {
|
|
12348
12684
|
// Verify-then-auto-login mode: Don't log in yet, but will auto-login after email verification
|
|
12685
|
+
// Send the verification email since backend register may not send it automatically
|
|
12686
|
+
if (response.user?.uid && data.email) {
|
|
12687
|
+
try {
|
|
12688
|
+
await api.sendEmailVerification(response.user.uid, data.email, getRedirectUrl());
|
|
12689
|
+
log.log('Verification email sent after registration');
|
|
12690
|
+
}
|
|
12691
|
+
catch (verifyError) {
|
|
12692
|
+
log.warn('Failed to send verification email after registration:', verifyError);
|
|
12693
|
+
// Don't fail the registration, just log the warning
|
|
12694
|
+
}
|
|
12695
|
+
}
|
|
12349
12696
|
setAuthSuccess(true);
|
|
12350
12697
|
setSuccessMessage('Account created! Please check your email and click the verification link to complete your registration.');
|
|
12351
12698
|
}
|
|
12352
12699
|
else {
|
|
12353
12700
|
// verify-then-manual-login mode: Traditional flow
|
|
12701
|
+
// Send the verification email since backend register may not send it automatically
|
|
12702
|
+
if (response.user?.uid && data.email) {
|
|
12703
|
+
try {
|
|
12704
|
+
await api.sendEmailVerification(response.user.uid, data.email, getRedirectUrl());
|
|
12705
|
+
log.log('Verification email sent after registration');
|
|
12706
|
+
}
|
|
12707
|
+
catch (verifyError) {
|
|
12708
|
+
log.warn('Failed to send verification email after registration:', verifyError);
|
|
12709
|
+
// Don't fail the registration, just log the warning
|
|
12710
|
+
}
|
|
12711
|
+
}
|
|
12354
12712
|
setAuthSuccess(true);
|
|
12355
12713
|
setSuccessMessage('Account created successfully! Please check your email to verify your account, then log in.');
|
|
12356
12714
|
}
|
|
@@ -12365,15 +12723,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12365
12723
|
if (response.requiresEmailVerification) {
|
|
12366
12724
|
throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
|
|
12367
12725
|
}
|
|
12368
|
-
auth.login(response.token, response.user, response.accountData, false);
|
|
12726
|
+
auth.login(response.token, response.user, response.accountData, false, getExpirationFromResponse(response));
|
|
12369
12727
|
setAuthSuccess(true);
|
|
12370
12728
|
setSuccessMessage('Login successful!');
|
|
12371
12729
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
12372
|
-
|
|
12373
|
-
setTimeout(() => {
|
|
12374
|
-
window.location.href = redirectUrl;
|
|
12375
|
-
}, 2000);
|
|
12376
|
-
}
|
|
12730
|
+
// Note: No automatic redirect - app controls navigation via onAuthSuccess callback
|
|
12377
12731
|
}
|
|
12378
12732
|
else {
|
|
12379
12733
|
throw new Error('Authentication failed - please verify your email before logging in.');
|
|
@@ -12448,11 +12802,16 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12448
12802
|
// Use custom client ID from config, or fall back to default Smartlinks client ID
|
|
12449
12803
|
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
12450
12804
|
// Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
|
|
12451
|
-
const
|
|
12805
|
+
const configuredFlow = config?.googleOAuthFlow || 'oneTap';
|
|
12806
|
+
// For oneTap, automatically use redirect flow in WebView environments
|
|
12807
|
+
const isWebView = detectWebView();
|
|
12808
|
+
const oauthFlow = (configuredFlow === 'oneTap' && isWebView) ? 'redirect' : configuredFlow;
|
|
12452
12809
|
// Log Google Auth configuration for debugging
|
|
12453
12810
|
log.log('Google Auth initiated:', {
|
|
12454
12811
|
googleClientId,
|
|
12455
|
-
|
|
12812
|
+
configuredFlow,
|
|
12813
|
+
effectiveFlow: oauthFlow,
|
|
12814
|
+
isWebView,
|
|
12456
12815
|
currentOrigin: window.location.origin,
|
|
12457
12816
|
currentHref: window.location.href,
|
|
12458
12817
|
configGoogleClientId: config?.googleClientId,
|
|
@@ -12468,7 +12827,37 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12468
12827
|
throw new Error('Google Identity Services failed to initialize');
|
|
12469
12828
|
}
|
|
12470
12829
|
log.log('Google Identity Services loaded, using flow:', oauthFlow);
|
|
12471
|
-
if (oauthFlow === '
|
|
12830
|
+
if (oauthFlow === 'redirect') {
|
|
12831
|
+
// Use OAuth2 redirect flow (works in WebViews and everywhere)
|
|
12832
|
+
if (!google.accounts.oauth2) {
|
|
12833
|
+
throw new Error('Google OAuth2 not available');
|
|
12834
|
+
}
|
|
12835
|
+
// Build the redirect URI - use current URL without query params
|
|
12836
|
+
const redirectUri = getRedirectUrl();
|
|
12837
|
+
// Build state parameter to preserve context across redirect
|
|
12838
|
+
const state = encodeURIComponent(JSON.stringify({
|
|
12839
|
+
clientId,
|
|
12840
|
+
returnPath: window.location.hash || window.location.pathname,
|
|
12841
|
+
redirectUri,
|
|
12842
|
+
}));
|
|
12843
|
+
log.log('Initializing Google OAuth2 redirect flow:', {
|
|
12844
|
+
client_id: googleClientId,
|
|
12845
|
+
scope: 'openid email profile',
|
|
12846
|
+
redirect_uri: redirectUri,
|
|
12847
|
+
state,
|
|
12848
|
+
});
|
|
12849
|
+
const client = google.accounts.oauth2.initCodeClient({
|
|
12850
|
+
client_id: googleClientId,
|
|
12851
|
+
scope: 'openid email profile',
|
|
12852
|
+
ux_mode: 'redirect',
|
|
12853
|
+
redirect_uri: redirectUri,
|
|
12854
|
+
state,
|
|
12855
|
+
});
|
|
12856
|
+
// This will navigate away from the page
|
|
12857
|
+
client.requestCode();
|
|
12858
|
+
return; // Don't set loading to false - we're navigating away
|
|
12859
|
+
}
|
|
12860
|
+
else if (oauthFlow === 'popup') {
|
|
12472
12861
|
// Use OAuth2 popup flow (works in iframes but requires popup permission)
|
|
12473
12862
|
if (!google.accounts.oauth2) {
|
|
12474
12863
|
throw new Error('Google OAuth2 not available');
|
|
@@ -12526,7 +12915,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12526
12915
|
});
|
|
12527
12916
|
if (authResponse.token) {
|
|
12528
12917
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
12529
|
-
auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
|
|
12918
|
+
auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
12530
12919
|
setAuthSuccess(true);
|
|
12531
12920
|
setSuccessMessage('Google login successful!');
|
|
12532
12921
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -12534,11 +12923,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12534
12923
|
else {
|
|
12535
12924
|
throw new Error('Authentication failed - no token received');
|
|
12536
12925
|
}
|
|
12537
|
-
|
|
12538
|
-
setTimeout(() => {
|
|
12539
|
-
window.location.href = redirectUrl;
|
|
12540
|
-
}, 2000);
|
|
12541
|
-
}
|
|
12926
|
+
// Note: No automatic redirect - app controls navigation via onAuthSuccess callback
|
|
12542
12927
|
}
|
|
12543
12928
|
catch (apiError) {
|
|
12544
12929
|
const errorMessage = apiError instanceof Error ? apiError.message : 'Google login failed';
|
|
@@ -12563,7 +12948,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12563
12948
|
client.requestAccessToken();
|
|
12564
12949
|
}
|
|
12565
12950
|
else {
|
|
12566
|
-
// Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
|
|
12951
|
+
// Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes or WebViews)
|
|
12567
12952
|
log.log('Initializing Google OneTap flow:', {
|
|
12568
12953
|
client_id: googleClientId,
|
|
12569
12954
|
origin: window.location.origin,
|
|
@@ -12576,7 +12961,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12576
12961
|
const authResponse = await api.loginWithGoogle(idToken);
|
|
12577
12962
|
if (authResponse.token) {
|
|
12578
12963
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
12579
|
-
auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser);
|
|
12964
|
+
auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
12580
12965
|
setAuthSuccess(true);
|
|
12581
12966
|
setSuccessMessage('Google login successful!');
|
|
12582
12967
|
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
@@ -12584,11 +12969,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12584
12969
|
else {
|
|
12585
12970
|
throw new Error('Authentication failed - no token received');
|
|
12586
12971
|
}
|
|
12587
|
-
|
|
12588
|
-
setTimeout(() => {
|
|
12589
|
-
window.location.href = redirectUrl;
|
|
12590
|
-
}, 2000);
|
|
12591
|
-
}
|
|
12972
|
+
// Note: No automatic redirect - app controls navigation via onAuthSuccess callback
|
|
12592
12973
|
setLoading(false);
|
|
12593
12974
|
}
|
|
12594
12975
|
catch (err) {
|
|
@@ -12600,8 +12981,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12600
12981
|
},
|
|
12601
12982
|
auto_select: false,
|
|
12602
12983
|
cancel_on_tap_outside: true,
|
|
12603
|
-
|
|
12604
|
-
// Will be needed when FedCM becomes mandatory in the future
|
|
12984
|
+
use_fedcm_for_prompt: true, // Enable FedCM for future browser compatibility
|
|
12605
12985
|
});
|
|
12606
12986
|
// Use timeout fallback instead of deprecated notification methods
|
|
12607
12987
|
// (isNotDisplayed/isSkippedMoment will stop working when FedCM becomes mandatory)
|
|
@@ -12638,7 +13018,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12638
13018
|
// Update auth context with account data if token is provided
|
|
12639
13019
|
if (response.token) {
|
|
12640
13020
|
// Phone auth can be login or signup - use isNewUser flag from backend if available
|
|
12641
|
-
auth.login(response.token, response.user, response.accountData, response.isNewUser);
|
|
13021
|
+
auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
12642
13022
|
onAuthSuccess(response.token, response.user, response.accountData);
|
|
12643
13023
|
if (redirectUrl) {
|
|
12644
13024
|
window.location.href = redirectUrl;
|
|
@@ -12811,36 +13191,39 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12811
13191
|
setShowResendVerification(false);
|
|
12812
13192
|
setShowRequestNewReset(false);
|
|
12813
13193
|
setError(undefined);
|
|
12814
|
-
}, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
|
|
13194
|
+
}, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, schema: contactSchema, registrationFieldsConfig: config?.registrationFields, additionalFields: config?.signupAdditionalFields }), emailDisplayMode === 'form' && actualProviders.length > 1 && (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders.filter((p) => p !== 'email'), providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }))] }));
|
|
12815
13195
|
})() })) })) : null }));
|
|
12816
13196
|
};
|
|
12817
13197
|
|
|
12818
|
-
const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', customization = {}, }) => {
|
|
13198
|
+
const AccountManagement = ({ apiEndpoint, clientId, collectionId, onError, className = '', customization = {}, }) => {
|
|
12819
13199
|
const auth = useAuth();
|
|
12820
13200
|
const [loading, setLoading] = React.useState(false);
|
|
12821
13201
|
const [profile, setProfile] = React.useState(null);
|
|
12822
13202
|
const [error, setError] = React.useState();
|
|
12823
13203
|
const [success, setSuccess] = React.useState();
|
|
13204
|
+
// Schema state
|
|
13205
|
+
const [schema, setSchema] = React.useState(null);
|
|
13206
|
+
const [schemaLoading, setSchemaLoading] = React.useState(false);
|
|
12824
13207
|
// Track which section is being edited
|
|
12825
13208
|
const [editingSection, setEditingSection] = React.useState(null);
|
|
12826
|
-
//
|
|
13209
|
+
// Form state for core fields
|
|
12827
13210
|
const [displayName, setDisplayName] = React.useState('');
|
|
12828
|
-
// Email change state
|
|
12829
13211
|
const [newEmail, setNewEmail] = React.useState('');
|
|
12830
13212
|
const [emailPassword, setEmailPassword] = React.useState('');
|
|
12831
|
-
// Password change state
|
|
12832
13213
|
const [currentPassword, setCurrentPassword] = React.useState('');
|
|
12833
13214
|
const [newPassword, setNewPassword] = React.useState('');
|
|
12834
13215
|
const [confirmPassword, setConfirmPassword] = React.useState('');
|
|
12835
|
-
// Phone change state (reuses existing sendPhoneCode flow)
|
|
12836
13216
|
const [newPhone, setNewPhone] = React.useState('');
|
|
12837
13217
|
const [phoneCode, setPhoneCode] = React.useState('');
|
|
12838
13218
|
const [phoneCodeSent, setPhoneCodeSent] = React.useState(false);
|
|
13219
|
+
// Custom fields form state
|
|
13220
|
+
const [customFieldValues, setCustomFieldValues] = React.useState({});
|
|
12839
13221
|
// Account deletion state
|
|
12840
13222
|
const [deletePassword, setDeletePassword] = React.useState('');
|
|
12841
13223
|
const [deleteConfirmText, setDeleteConfirmText] = React.useState('');
|
|
12842
13224
|
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
12843
|
-
const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false,
|
|
13225
|
+
const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = false, showCustomFields = true, // New: show schema-driven custom fields
|
|
13226
|
+
} = customization;
|
|
12844
13227
|
// Reinitialize Smartlinks SDK when apiEndpoint changes
|
|
12845
13228
|
React.useEffect(() => {
|
|
12846
13229
|
if (apiEndpoint) {
|
|
@@ -12851,10 +13234,32 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
|
|
|
12851
13234
|
});
|
|
12852
13235
|
}
|
|
12853
13236
|
}, [apiEndpoint]);
|
|
12854
|
-
// Load
|
|
13237
|
+
// Load schema when collectionId is available
|
|
13238
|
+
React.useEffect(() => {
|
|
13239
|
+
if (!collectionId)
|
|
13240
|
+
return;
|
|
13241
|
+
const loadSchema = async () => {
|
|
13242
|
+
setSchemaLoading(true);
|
|
13243
|
+
try {
|
|
13244
|
+
console.log('[AccountManagement] Loading schema for collection:', collectionId);
|
|
13245
|
+
const schemaResult = await smartlinks__namespace.contact.publicGetSchema(collectionId);
|
|
13246
|
+
console.log('[AccountManagement] Schema loaded:', schemaResult);
|
|
13247
|
+
setSchema(schemaResult);
|
|
13248
|
+
}
|
|
13249
|
+
catch (err) {
|
|
13250
|
+
console.warn('[AccountManagement] Failed to load schema:', err);
|
|
13251
|
+
// Non-fatal - component works without schema
|
|
13252
|
+
}
|
|
13253
|
+
finally {
|
|
13254
|
+
setSchemaLoading(false);
|
|
13255
|
+
}
|
|
13256
|
+
};
|
|
13257
|
+
loadSchema();
|
|
13258
|
+
}, [collectionId]);
|
|
13259
|
+
// Load user profile and contact data on mount
|
|
12855
13260
|
React.useEffect(() => {
|
|
12856
13261
|
loadProfile();
|
|
12857
|
-
}, [clientId]);
|
|
13262
|
+
}, [clientId, collectionId]);
|
|
12858
13263
|
const loadProfile = async () => {
|
|
12859
13264
|
if (!auth.isAuthenticated) {
|
|
12860
13265
|
setError('You must be logged in to manage your account');
|
|
@@ -12863,10 +13268,7 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
|
|
|
12863
13268
|
setLoading(true);
|
|
12864
13269
|
setError(undefined);
|
|
12865
13270
|
try {
|
|
12866
|
-
//
|
|
12867
|
-
// Endpoint: GET /api/v1/authkit/:clientId/account/profile
|
|
12868
|
-
// SDK method: smartlinks.authKit.getProfile(clientId)
|
|
12869
|
-
// Temporary mock data for UI testing
|
|
13271
|
+
// Get base profile from auth context
|
|
12870
13272
|
const profileData = {
|
|
12871
13273
|
uid: auth.user?.uid || '',
|
|
12872
13274
|
email: auth.user?.email,
|
|
@@ -12878,6 +13280,17 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
|
|
|
12878
13280
|
};
|
|
12879
13281
|
setProfile(profileData);
|
|
12880
13282
|
setDisplayName(profileData.displayName || '');
|
|
13283
|
+
// Load contact custom fields if collectionId provided
|
|
13284
|
+
if (collectionId && auth.contact) {
|
|
13285
|
+
setCustomFieldValues(auth.contact.customFields || {});
|
|
13286
|
+
}
|
|
13287
|
+
else if (collectionId) {
|
|
13288
|
+
// Try to fetch contact
|
|
13289
|
+
const contact = await auth.getContact?.();
|
|
13290
|
+
if (contact?.customFields) {
|
|
13291
|
+
setCustomFieldValues(contact.customFields);
|
|
13292
|
+
}
|
|
13293
|
+
}
|
|
12881
13294
|
}
|
|
12882
13295
|
catch (err) {
|
|
12883
13296
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
|
|
@@ -12888,28 +13301,39 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
|
|
|
12888
13301
|
setLoading(false);
|
|
12889
13302
|
}
|
|
12890
13303
|
};
|
|
13304
|
+
const cancelEdit = React.useCallback(() => {
|
|
13305
|
+
setEditingSection(null);
|
|
13306
|
+
setDisplayName(profile?.displayName || '');
|
|
13307
|
+
setNewEmail('');
|
|
13308
|
+
setEmailPassword('');
|
|
13309
|
+
setCurrentPassword('');
|
|
13310
|
+
setNewPassword('');
|
|
13311
|
+
setConfirmPassword('');
|
|
13312
|
+
setNewPhone('');
|
|
13313
|
+
setPhoneCode('');
|
|
13314
|
+
setPhoneCodeSent(false);
|
|
13315
|
+
setError(undefined);
|
|
13316
|
+
setSuccess(undefined);
|
|
13317
|
+
// Reset custom fields to original values
|
|
13318
|
+
if (auth.contact?.customFields) {
|
|
13319
|
+
setCustomFieldValues(auth.contact.customFields);
|
|
13320
|
+
}
|
|
13321
|
+
}, [profile, auth.contact]);
|
|
13322
|
+
// Get editable custom fields from schema
|
|
13323
|
+
const editableCustomFields = getEditableFields(schema).filter(f => !['email', 'displayName', 'phone', 'phoneNumber'].includes(f.key));
|
|
12891
13324
|
const handleUpdateProfile = async (e) => {
|
|
12892
13325
|
e.preventDefault();
|
|
12893
13326
|
setLoading(true);
|
|
12894
13327
|
setError(undefined);
|
|
12895
13328
|
setSuccess(undefined);
|
|
12896
13329
|
try {
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
|
|
12902
|
-
|
|
12903
|
-
|
|
12904
|
-
// const updateData: ProfileUpdateData = {
|
|
12905
|
-
// displayName: displayName || undefined,
|
|
12906
|
-
// photoURL: photoURL || undefined,
|
|
12907
|
-
// };
|
|
12908
|
-
// const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
|
|
12909
|
-
// setProfile(updatedProfile);
|
|
12910
|
-
// setSuccess('Profile updated successfully!');
|
|
12911
|
-
// setEditingSection(null);
|
|
12912
|
-
// onProfileUpdated?.(updatedProfile);
|
|
13330
|
+
await smartlinks__namespace.authKit.updateProfile(clientId, { displayName });
|
|
13331
|
+
setSuccess('Profile updated successfully!');
|
|
13332
|
+
setEditingSection(null);
|
|
13333
|
+
// Update local state
|
|
13334
|
+
if (profile) {
|
|
13335
|
+
setProfile({ ...profile, displayName });
|
|
13336
|
+
}
|
|
12913
13337
|
}
|
|
12914
13338
|
catch (err) {
|
|
12915
13339
|
const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
|
|
@@ -12920,19 +13344,29 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
|
|
|
12920
13344
|
setLoading(false);
|
|
12921
13345
|
}
|
|
12922
13346
|
};
|
|
12923
|
-
const
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
setConfirmPassword('');
|
|
12931
|
-
setNewPhone('');
|
|
12932
|
-
setPhoneCode('');
|
|
12933
|
-
setPhoneCodeSent(false);
|
|
13347
|
+
const handleUpdateCustomFields = async (e) => {
|
|
13348
|
+
e.preventDefault();
|
|
13349
|
+
if (!collectionId) {
|
|
13350
|
+
setError('Collection ID is required to update custom fields');
|
|
13351
|
+
return;
|
|
13352
|
+
}
|
|
13353
|
+
setLoading(true);
|
|
12934
13354
|
setError(undefined);
|
|
12935
13355
|
setSuccess(undefined);
|
|
13356
|
+
try {
|
|
13357
|
+
console.log('[AccountManagement] Updating custom fields:', customFieldValues);
|
|
13358
|
+
await auth.updateContactCustomFields?.(customFieldValues);
|
|
13359
|
+
setSuccess('Profile updated successfully!');
|
|
13360
|
+
setEditingSection(null);
|
|
13361
|
+
}
|
|
13362
|
+
catch (err) {
|
|
13363
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
|
|
13364
|
+
setError(errorMessage);
|
|
13365
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
13366
|
+
}
|
|
13367
|
+
finally {
|
|
13368
|
+
setLoading(false);
|
|
13369
|
+
}
|
|
12936
13370
|
};
|
|
12937
13371
|
const handleChangeEmail = async (e) => {
|
|
12938
13372
|
e.preventDefault();
|
|
@@ -12940,21 +13374,12 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
|
|
|
12940
13374
|
setError(undefined);
|
|
12941
13375
|
setSuccess(undefined);
|
|
12942
13376
|
try {
|
|
12943
|
-
|
|
12944
|
-
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
console.log('Data:', { newEmail });
|
|
12950
|
-
// Uncomment when backend is ready:
|
|
12951
|
-
// await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
|
|
12952
|
-
// setSuccess('Email changed successfully!');
|
|
12953
|
-
// setEditingSection(null);
|
|
12954
|
-
// setNewEmail('');
|
|
12955
|
-
// setEmailPassword('');
|
|
12956
|
-
// onEmailChangeRequested?.();
|
|
12957
|
-
// await loadProfile(); // Reload to show new email
|
|
13377
|
+
const redirectUrl = window.location.href;
|
|
13378
|
+
await smartlinks__namespace.authKit.changeEmail(clientId, newEmail, emailPassword, redirectUrl);
|
|
13379
|
+
setSuccess('Email change requested. Please check your new email for verification.');
|
|
13380
|
+
setEditingSection(null);
|
|
13381
|
+
setNewEmail('');
|
|
13382
|
+
setEmailPassword('');
|
|
12958
13383
|
}
|
|
12959
13384
|
catch (err) {
|
|
12960
13385
|
const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
|
|
@@ -12979,20 +13404,12 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
|
|
|
12979
13404
|
setError(undefined);
|
|
12980
13405
|
setSuccess(undefined);
|
|
12981
13406
|
try {
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
// Uncomment when backend is ready:
|
|
12989
|
-
// await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
|
|
12990
|
-
// setSuccess('Password changed successfully!');
|
|
12991
|
-
// setEditingSection(null);
|
|
12992
|
-
// setCurrentPassword('');
|
|
12993
|
-
// setNewPassword('');
|
|
12994
|
-
// setConfirmPassword('');
|
|
12995
|
-
// onPasswordChanged?.();
|
|
13407
|
+
await smartlinks__namespace.authKit.changePassword(clientId, currentPassword, newPassword);
|
|
13408
|
+
setSuccess('Password changed successfully!');
|
|
13409
|
+
setEditingSection(null);
|
|
13410
|
+
setCurrentPassword('');
|
|
13411
|
+
setNewPassword('');
|
|
13412
|
+
setConfirmPassword('');
|
|
12996
13413
|
}
|
|
12997
13414
|
catch (err) {
|
|
12998
13415
|
const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
|
|
@@ -13026,20 +13443,13 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
|
|
|
13026
13443
|
setError(undefined);
|
|
13027
13444
|
setSuccess(undefined);
|
|
13028
13445
|
try {
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
// await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
|
|
13037
|
-
// setSuccess('Phone number updated successfully!');
|
|
13038
|
-
// setEditingSection(null);
|
|
13039
|
-
// setNewPhone('');
|
|
13040
|
-
// setPhoneCode('');
|
|
13041
|
-
// setPhoneCodeSent(false);
|
|
13042
|
-
// await loadProfile();
|
|
13446
|
+
await smartlinks__namespace.authKit.updatePhone(clientId, newPhone, phoneCode);
|
|
13447
|
+
setSuccess('Phone number updated successfully!');
|
|
13448
|
+
setEditingSection(null);
|
|
13449
|
+
setNewPhone('');
|
|
13450
|
+
setPhoneCode('');
|
|
13451
|
+
setPhoneCodeSent(false);
|
|
13452
|
+
await loadProfile();
|
|
13043
13453
|
}
|
|
13044
13454
|
catch (err) {
|
|
13045
13455
|
const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
|
|
@@ -13062,19 +13472,9 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
|
|
|
13062
13472
|
setLoading(true);
|
|
13063
13473
|
setError(undefined);
|
|
13064
13474
|
try {
|
|
13065
|
-
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
// Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
|
|
13069
|
-
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
13070
|
-
console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
|
|
13071
|
-
console.log('Data: password and confirmText="DELETE" provided');
|
|
13072
|
-
console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
|
|
13073
|
-
// Uncomment when backend is ready:
|
|
13074
|
-
// await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
|
|
13075
|
-
// setSuccess('Account deleted successfully');
|
|
13076
|
-
// onAccountDeleted?.();
|
|
13077
|
-
// await auth.logout();
|
|
13475
|
+
await smartlinks__namespace.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
|
|
13476
|
+
setSuccess('Account deleted successfully');
|
|
13477
|
+
await auth.logout();
|
|
13078
13478
|
}
|
|
13079
13479
|
catch (err) {
|
|
13080
13480
|
const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
|
|
@@ -13085,120 +13485,24 @@ const AccountManagement = ({ apiEndpoint, clientId, onError, className = '', cus
|
|
|
13085
13485
|
setLoading(false);
|
|
13086
13486
|
}
|
|
13087
13487
|
};
|
|
13488
|
+
const handleCustomFieldChange = (key, value) => {
|
|
13489
|
+
setCustomFieldValues(prev => ({ ...prev, [key]: value }));
|
|
13490
|
+
};
|
|
13088
13491
|
if (!auth.isAuthenticated) {
|
|
13089
13492
|
return (jsxRuntime.jsx("div", { className: `account-management ${className}`, children: jsxRuntime.jsx("p", { className: "text-muted-foreground", children: "Please log in to manage your account" }) }));
|
|
13090
13493
|
}
|
|
13091
|
-
if (loading && !profile) {
|
|
13494
|
+
if ((loading || schemaLoading) && !profile) {
|
|
13092
13495
|
return (jsxRuntime.jsx("div", { className: `account-management ${className}`, children: jsxRuntime.jsx("p", { className: "text-muted-foreground", children: "Loading..." }) }));
|
|
13093
13496
|
}
|
|
13094
|
-
return (jsxRuntime.jsxs("div", { className: `account-management ${className}`, style: { maxWidth: '600px' }, children: [error && (jsxRuntime.jsx("div", { className: "auth-error", role: "alert", children: error })), success && (jsxRuntime.jsx("div", { className: "auth-success", role: "alert", children: success })), showProfileSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Display Name" }), jsxRuntime.jsx("div", { className: "field-value", children: profile?.displayName || 'Not set' })] }), editingSection !== 'profile' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('profile'), className: "auth-button button-secondary", children: "Change" }))] }), editingSection === 'profile' && (jsxRuntime.jsxs("form", { onSubmit: handleUpdateProfile, className: "edit-form", children: [jsxRuntime.jsx("div", { className: "form-group", children: jsxRuntime.jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" }) }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })),
|
|
13497
|
+
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] !== ''
|
|
13498
|
+
? String(customFieldValues[field.key])
|
|
13499
|
+
: 'Not set' })] }) }, field.key))), jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('customFields'), className: "auth-button button-secondary", style: { marginTop: '8px' }, children: "Edit Information" })] })) : (jsxRuntime.jsxs("form", { onSubmit: handleUpdateCustomFields, className: "edit-form", children: [editableCustomFields.map((field) => (jsxRuntime.jsx(SchemaFieldRenderer, { field: field, value: customFieldValues[field.key], onChange: handleCustomFieldChange, disabled: loading }, field.key))), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Saving...' : 'Save' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showEmailSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Email Address" }), jsxRuntime.jsxs("div", { className: "field-value", children: [profile?.email || 'Not set', profile?.emailVerified && (jsxRuntime.jsx("span", { className: "verification-badge verified", children: "Verified" })), profile?.email && !profile?.emailVerified && (jsxRuntime.jsx("span", { className: "verification-badge unverified", children: "Unverified" }))] })] }), editingSection !== 'email' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('email'), className: "auth-button button-secondary", children: "Change Email" }))] }), editingSection === 'email' && (jsxRuntime.jsxs("form", { onSubmit: handleChangeEmail, className: "edit-form", children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "newEmail", children: "New Email" }), jsxRuntime.jsx("input", { id: "newEmail", type: "email", value: newEmail, onChange: (e) => setNewEmail(e.target.value), placeholder: "new.email@example.com", className: "auth-input", required: true })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "emailPassword", children: "Confirm Password" }), jsxRuntime.jsx("input", { id: "emailPassword", type: "password", value: emailPassword, onChange: (e) => setEmailPassword(e.target.value), placeholder: "Enter your password", className: "auth-input", required: true })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Email' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPasswordSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Password" }), jsxRuntime.jsx("div", { className: "field-value", children: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] }), editingSection !== 'password' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('password'), className: "auth-button button-secondary", children: "Change Password" }))] }), editingSection === 'password' && (jsxRuntime.jsxs("form", { onSubmit: handleChangePassword, className: "edit-form", children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "currentPassword", children: "Current Password" }), jsxRuntime.jsx("input", { id: "currentPassword", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), placeholder: "Enter current password", className: "auth-input", required: true })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "newPassword", children: "New Password" }), jsxRuntime.jsx("input", { id: "newPassword", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), placeholder: "Enter new password", className: "auth-input", required: true, minLength: 6 })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "confirmPassword", children: "Confirm New Password" }), jsxRuntime.jsx("input", { id: "confirmPassword", type: "password", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), placeholder: "Confirm new password", className: "auth-input", required: true, minLength: 6 })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] })), showPhoneSection && (jsxRuntime.jsxs("section", { className: "account-section", children: [jsxRuntime.jsxs("div", { className: "account-field", children: [jsxRuntime.jsxs("div", { className: "field-info", children: [jsxRuntime.jsx("label", { className: "field-label", children: "Phone Number" }), jsxRuntime.jsx("div", { className: "field-value", children: profile?.phoneNumber || 'Not set' })] }), editingSection !== 'phone' && (jsxRuntime.jsx("button", { type: "button", onClick: () => setEditingSection('phone'), className: "auth-button button-secondary", children: "Change Phone" }))] }), editingSection === 'phone' && (jsxRuntime.jsxs("form", { onSubmit: handleUpdatePhone, className: "edit-form", children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "newPhone", children: "New Phone Number" }), jsxRuntime.jsx("input", { id: "newPhone", type: "tel", value: newPhone, onChange: (e) => setNewPhone(e.target.value), placeholder: "+1234567890", className: "auth-input", required: true })] }), !phoneCodeSent ? (jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Code' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "phoneCode", children: "Verification Code" }), jsxRuntime.jsx("input", { id: "phoneCode", type: "text", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: "Enter 6-digit code", className: "auth-input", required: true, maxLength: 6 })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Save' }), jsxRuntime.jsx("button", { type: "button", onClick: cancelEdit, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] })), showDeleteAccount && (jsxRuntime.jsxs("section", { className: "account-section danger-zone", children: [jsxRuntime.jsx("h3", { className: "section-title text-danger", children: "Danger Zone" }), !showDeleteConfirm ? (jsxRuntime.jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "auth-button button-danger", children: "Delete Account" })) : (jsxRuntime.jsxs("div", { className: "delete-confirm", children: [jsxRuntime.jsx("p", { className: "warning-text", children: "\u26A0\uFE0F This action cannot be undone. This will permanently delete your account and all associated data." }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "deletePassword", children: "Confirm Password" }), jsxRuntime.jsx("input", { id: "deletePassword", type: "password", value: deletePassword, onChange: (e) => setDeletePassword(e.target.value), placeholder: "Enter your password", className: "auth-input" })] }), jsxRuntime.jsxs("div", { className: "form-group", children: [jsxRuntime.jsx("label", { htmlFor: "deleteConfirm", children: "Type DELETE to confirm" }), jsxRuntime.jsx("input", { id: "deleteConfirm", type: "text", value: deleteConfirmText, onChange: (e) => setDeleteConfirmText(e.target.value), placeholder: "DELETE", className: "auth-input" })] }), jsxRuntime.jsxs("div", { className: "button-group", children: [jsxRuntime.jsx("button", { type: "button", onClick: handleDeleteAccount, className: "auth-button button-danger", disabled: loading, children: loading ? 'Deleting...' : 'Permanently Delete Account' }), jsxRuntime.jsx("button", { type: "button", onClick: () => {
|
|
13095
13500
|
setShowDeleteConfirm(false);
|
|
13096
13501
|
setDeletePassword('');
|
|
13097
13502
|
setDeleteConfirmText('');
|
|
13098
13503
|
}, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }));
|
|
13099
13504
|
};
|
|
13100
13505
|
|
|
13101
|
-
const SmartlinksClaimUI = (props) => {
|
|
13102
|
-
// Destructure AFTER logging raw props to debug proxyMode issue
|
|
13103
|
-
const { apiEndpoint, clientId, clientName, collectionId, productId, proofId, onClaimSuccess, onClaimError, additionalFields = [], theme = 'light', className = '', minimal = false, proxyMode = false, customization = {}, } = props;
|
|
13104
|
-
// Debug logging for proxyMode - log RAW props first
|
|
13105
|
-
console.log('[SmartlinksClaimUI] 🔍 RAW props received:', props);
|
|
13106
|
-
console.log('[SmartlinksClaimUI] 🔍 props.proxyMode value:', props.proxyMode);
|
|
13107
|
-
console.log('[SmartlinksClaimUI] 🔍 typeof props.proxyMode:', typeof props.proxyMode);
|
|
13108
|
-
console.log('[SmartlinksClaimUI] 🎯 Destructured proxyMode:', proxyMode);
|
|
13109
|
-
const auth = useAuth();
|
|
13110
|
-
const [claimStep, setClaimStep] = React.useState(auth.isAuthenticated ? 'questions' : 'auth');
|
|
13111
|
-
const [claimData, setClaimData] = React.useState({});
|
|
13112
|
-
const [error, setError] = React.useState();
|
|
13113
|
-
const [loading, setLoading] = React.useState(false);
|
|
13114
|
-
const handleAuthSuccess = (token, user, accountData) => {
|
|
13115
|
-
// Authentication successful
|
|
13116
|
-
auth.login(token, user, accountData);
|
|
13117
|
-
// If no additional questions, proceed directly to claim
|
|
13118
|
-
if (additionalFields.length === 0) {
|
|
13119
|
-
executeClaim(user);
|
|
13120
|
-
}
|
|
13121
|
-
else {
|
|
13122
|
-
setClaimStep('questions');
|
|
13123
|
-
}
|
|
13124
|
-
};
|
|
13125
|
-
const handleQuestionSubmit = async (e) => {
|
|
13126
|
-
e.preventDefault();
|
|
13127
|
-
// Validate required fields
|
|
13128
|
-
const missingFields = additionalFields
|
|
13129
|
-
.filter(field => field.required && !claimData[field.name])
|
|
13130
|
-
.map(field => field.label);
|
|
13131
|
-
if (missingFields.length > 0) {
|
|
13132
|
-
setError(`Please fill in: ${missingFields.join(', ')}`);
|
|
13133
|
-
return;
|
|
13134
|
-
}
|
|
13135
|
-
// Execute claim with collected data
|
|
13136
|
-
if (auth.user) {
|
|
13137
|
-
executeClaim(auth.user);
|
|
13138
|
-
}
|
|
13139
|
-
};
|
|
13140
|
-
const executeClaim = async (user) => {
|
|
13141
|
-
setClaimStep('claiming');
|
|
13142
|
-
setLoading(true);
|
|
13143
|
-
setError(undefined);
|
|
13144
|
-
try {
|
|
13145
|
-
// Create attestation to claim the proof
|
|
13146
|
-
const response = await smartlinks__namespace.attestation.create(collectionId, productId, proofId, {
|
|
13147
|
-
public: {
|
|
13148
|
-
claimed: true,
|
|
13149
|
-
claimedAt: new Date().toISOString(),
|
|
13150
|
-
claimedBy: user.uid,
|
|
13151
|
-
...claimData,
|
|
13152
|
-
},
|
|
13153
|
-
private: {},
|
|
13154
|
-
proof: {},
|
|
13155
|
-
});
|
|
13156
|
-
setClaimStep('success');
|
|
13157
|
-
// Call success callback
|
|
13158
|
-
onClaimSuccess({
|
|
13159
|
-
proofId,
|
|
13160
|
-
user,
|
|
13161
|
-
claimData,
|
|
13162
|
-
attestationId: response.id,
|
|
13163
|
-
});
|
|
13164
|
-
}
|
|
13165
|
-
catch (err) {
|
|
13166
|
-
console.error('Claim error:', err);
|
|
13167
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to claim proof';
|
|
13168
|
-
setError(errorMessage);
|
|
13169
|
-
onClaimError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
13170
|
-
setClaimStep(additionalFields.length > 0 ? 'questions' : 'auth');
|
|
13171
|
-
}
|
|
13172
|
-
finally {
|
|
13173
|
-
setLoading(false);
|
|
13174
|
-
}
|
|
13175
|
-
};
|
|
13176
|
-
const handleFieldChange = (fieldName, value) => {
|
|
13177
|
-
setClaimData(prev => ({
|
|
13178
|
-
...prev,
|
|
13179
|
-
[fieldName]: value,
|
|
13180
|
-
}));
|
|
13181
|
-
};
|
|
13182
|
-
// Render authentication step
|
|
13183
|
-
if (claimStep === 'auth') {
|
|
13184
|
-
console.log('[SmartlinksClaimUI] 🔑 Rendering auth step with proxyMode:', proxyMode);
|
|
13185
|
-
return (jsxRuntime.jsx("div", { className: className, children: jsxRuntime.jsx(SmartlinksAuthUI, { apiEndpoint: apiEndpoint, clientId: clientId, clientName: clientName, onAuthSuccess: handleAuthSuccess, onAuthError: onClaimError, theme: theme, minimal: minimal, proxyMode: proxyMode, customization: customization.authConfig }) }));
|
|
13186
|
-
}
|
|
13187
|
-
// Render additional questions step
|
|
13188
|
-
if (claimStep === 'questions') {
|
|
13189
|
-
return (jsxRuntime.jsxs("div", { className: `claim-questions ${className}`, children: [jsxRuntime.jsxs("div", { className: "claim-header mb-6", children: [jsxRuntime.jsx("h2", { className: "text-2xl font-bold mb-2", children: customization.claimTitle || 'Complete Your Claim' }), customization.claimDescription && (jsxRuntime.jsx("p", { className: "text-muted-foreground", children: customization.claimDescription }))] }), error && (jsxRuntime.jsx("div", { className: "claim-error bg-destructive/10 text-destructive px-4 py-3 rounded-md mb-4", children: error })), jsxRuntime.jsxs("form", { onSubmit: handleQuestionSubmit, className: "claim-form space-y-4", children: [additionalFields.map((field) => (jsxRuntime.jsxs("div", { className: "claim-field", children: [jsxRuntime.jsxs("label", { htmlFor: field.name, className: "block text-sm font-medium mb-2", children: [field.label, field.required && jsxRuntime.jsx("span", { className: "text-destructive ml-1", children: "*" })] }), field.type === 'textarea' ? (jsxRuntime.jsx("textarea", { id: field.name, name: field.name, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", rows: 4 })) : field.type === 'select' ? (jsxRuntime.jsxs("select", { id: field.name, name: field.name, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background", children: [jsxRuntime.jsx("option", { value: "", children: "Select..." }), field.options?.map((option) => (jsxRuntime.jsx("option", { value: option, children: option }, option)))] })) : (jsxRuntime.jsx("input", { id: field.name, name: field.name, type: field.type, placeholder: field.placeholder, value: claimData[field.name] || '', onChange: (e) => handleFieldChange(field.name, e.target.value), required: field.required, className: "w-full px-3 py-2 border border-input rounded-md bg-background" }))] }, field.name))), jsxRuntime.jsx("button", { type: "submit", disabled: loading, className: "claim-submit-button w-full bg-primary text-primary-foreground px-4 py-2 rounded-md font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed", children: loading ? 'Claiming...' : 'Submit Claim' })] })] }));
|
|
13190
|
-
}
|
|
13191
|
-
// Render claiming step (loading state)
|
|
13192
|
-
if (claimStep === 'claiming') {
|
|
13193
|
-
return (jsxRuntime.jsxs("div", { className: `claim-loading ${className} flex flex-col items-center justify-center py-12`, children: [jsxRuntime.jsx("div", { className: "claim-spinner w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mb-4" }), jsxRuntime.jsx("p", { className: "text-muted-foreground", children: "Claiming your product..." })] }));
|
|
13194
|
-
}
|
|
13195
|
-
// Render success step
|
|
13196
|
-
if (claimStep === 'success') {
|
|
13197
|
-
return (jsxRuntime.jsxs("div", { className: `claim-success ${className} text-center py-12`, children: [jsxRuntime.jsx("div", { className: "claim-success-icon w-16 h-16 bg-green-500 text-white rounded-full flex items-center justify-center text-3xl font-bold mx-auto mb-4", children: "\u2713" }), jsxRuntime.jsx("h2", { className: "text-2xl font-bold mb-2", children: "Claim Successful!" }), jsxRuntime.jsx("p", { className: "text-muted-foreground", children: customization.successMessage || 'Your product has been successfully claimed and registered to your account.' })] }));
|
|
13198
|
-
}
|
|
13199
|
-
return null;
|
|
13200
|
-
};
|
|
13201
|
-
|
|
13202
13506
|
const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
|
|
13203
13507
|
const { isAuthenticated, isLoading } = useAuth();
|
|
13204
13508
|
// Show loading state
|
|
@@ -13253,8 +13557,11 @@ exports.AuthProvider = AuthProvider;
|
|
|
13253
13557
|
exports.AuthUIPreview = AuthUIPreview;
|
|
13254
13558
|
exports.FirebaseAuthUI = SmartlinksAuthUI;
|
|
13255
13559
|
exports.ProtectedRoute = ProtectedRoute;
|
|
13560
|
+
exports.SchemaFieldRenderer = SchemaFieldRenderer;
|
|
13256
13561
|
exports.SmartlinksAuthUI = SmartlinksAuthUI;
|
|
13257
|
-
exports.
|
|
13562
|
+
exports.getEditableFields = getEditableFields;
|
|
13563
|
+
exports.getRegistrationFields = getRegistrationFields;
|
|
13564
|
+
exports.sortFieldsByPlacement = sortFieldsByPlacement;
|
|
13258
13565
|
exports.tokenStorage = tokenStorage;
|
|
13259
13566
|
exports.useAuth = useAuth;
|
|
13260
13567
|
//# sourceMappingURL=index.js.map
|