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