@proveanything/smartlinks-auth-ui 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts.map +1 -1
- package/dist/components/EmailAuthForm.d.ts +1 -0
- package/dist/components/EmailAuthForm.d.ts.map +1 -1
- package/dist/components/PasswordResetForm.d.ts +1 -0
- package/dist/components/PasswordResetForm.d.ts.map +1 -1
- package/dist/components/SmartlinksAuthUI.d.ts.map +1 -1
- package/dist/index.esm.js +277 -80
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +277 -80
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/errorHandling.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -278,7 +278,7 @@ const AuthModeToggle = ({ mode, onModeChange, disabled = false, }) => {
|
|
|
278
278
|
return (jsxRuntime.jsxs("div", { className: "auth-mode-toggle", role: "tablist", "aria-label": "Authentication mode", children: [jsxRuntime.jsx("button", { type: "button", role: "tab", "aria-selected": mode === 'login', className: `auth-mode-toggle-button ${mode === 'login' ? 'auth-mode-toggle-button--active' : ''}`, onClick: () => onModeChange('login'), disabled: disabled, children: "Sign In" }), jsxRuntime.jsx("button", { type: "button", role: "tab", "aria-selected": mode === 'register', className: `auth-mode-toggle-button ${mode === 'register' ? 'auth-mode-toggle-button--active' : ''}`, onClick: () => onModeChange('register'), disabled: disabled, children: "Create Account" })] }));
|
|
279
279
|
};
|
|
280
280
|
|
|
281
|
-
const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, signupProminence = 'minimal', schema, registrationFieldsConfig = [], additionalFields = [], }) => {
|
|
281
|
+
const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading, error, signupProminence = 'minimal', signupRedirectUrl, schema, registrationFieldsConfig = [], additionalFields = [], }) => {
|
|
282
282
|
const [formData, setFormData] = React.useState({
|
|
283
283
|
email: '',
|
|
284
284
|
password: '',
|
|
@@ -334,7 +334,7 @@ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading
|
|
|
334
334
|
if (newMode !== mode) {
|
|
335
335
|
onModeSwitch();
|
|
336
336
|
}
|
|
337
|
-
}, disabled: loading })), jsxRuntime.jsxs("div", { className: "auth-form-header", children: [jsxRuntime.jsx("h2", { className: "auth-form-title", children: title }), jsxRuntime.jsx("p", { className: "auth-form-subtitle", children: subtitle })] }), 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 Smith" })] })), 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') }), signupProminence !== 'balanced' && (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' })] }))] }));
|
|
337
|
+
}, disabled: loading })), jsxRuntime.jsxs("div", { className: "auth-form-header", children: [jsxRuntime.jsx("h2", { className: "auth-form-title", children: title }), jsxRuntime.jsx("p", { className: "auth-form-subtitle", children: subtitle })] }), 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 Smith" })] })), 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') }), signupProminence !== 'balanced' && signupProminence !== 'none' && (jsxRuntime.jsxs("div", { className: "auth-divider", children: [jsxRuntime.jsx("span", { children: mode === 'login' ? "Don't have an account?" : 'Already have an account?' }), signupRedirectUrl && mode === 'login' ? (jsxRuntime.jsx("a", { href: signupRedirectUrl, target: "_top", className: "auth-link auth-link-bold", children: "Sign up" })) : (jsxRuntime.jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onModeSwitch, disabled: loading, children: mode === 'login' ? 'Sign up' : 'Sign in' }))] }))] }));
|
|
338
338
|
};
|
|
339
339
|
|
|
340
340
|
const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, loading, }) => {
|
|
@@ -10831,7 +10831,7 @@ const PhoneAuthForm = ({ onSubmit, onBack, loading, error, }) => {
|
|
|
10831
10831
|
: 'Resend code' }) })] })), jsxRuntime.jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsxRuntime.jsx("span", { className: "auth-spinner" })) : codeSent ? ('Verify Code') : ('Send Code') }), jsxRuntime.jsx("div", { className: "auth-divider", children: jsxRuntime.jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onBack, disabled: loading, children: "\u2190 Back to login" }) })] }));
|
|
10832
10832
|
};
|
|
10833
10833
|
|
|
10834
|
-
const PasswordResetForm = ({ onSubmit, onBack, loading, error, success, successMessage, token, }) => {
|
|
10834
|
+
const PasswordResetForm = ({ onSubmit, onBack, loading, error, success, successMessage, token, resetEmail, }) => {
|
|
10835
10835
|
const [email, setEmail] = React.useState('');
|
|
10836
10836
|
const [password, setPassword] = React.useState('');
|
|
10837
10837
|
const [confirmPassword, setConfirmPassword] = React.useState('');
|
|
@@ -10856,14 +10856,14 @@ const PasswordResetForm = ({ onSubmit, onBack, loading, error, success, successM
|
|
|
10856
10856
|
await onSubmit(email);
|
|
10857
10857
|
}
|
|
10858
10858
|
};
|
|
10859
|
-
if (success) {
|
|
10859
|
+
if (success && !error) {
|
|
10860
10860
|
return (jsxRuntime.jsxs("div", { className: "auth-form", children: [jsxRuntime.jsxs("div", { className: "auth-form-header", children: [jsxRuntime.jsx("div", { style: { textAlign: 'center', marginBottom: '1rem' }, children: jsxRuntime.jsxs("svg", { width: "64", height: "64", viewBox: "0 0 64 64", fill: "none", style: { margin: '0 auto' }, children: [jsxRuntime.jsx("circle", { cx: "32", cy: "32", r: "32", fill: "#10b981", fillOpacity: "0.1" }), jsxRuntime.jsx("path", { d: "M20 32l8 8 16-16", stroke: "#10b981", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round" })] }) }), jsxRuntime.jsx("h2", { className: "auth-form-title", children: token ? 'Password reset!' : 'Check your email' }), jsxRuntime.jsx("p", { className: "auth-form-subtitle", children: token
|
|
10861
10861
|
? (successMessage || 'Your password has been successfully reset. You can now sign in with your new password.')
|
|
10862
10862
|
: (successMessage || `We've sent password reset instructions to ${email}`) })] }), jsxRuntime.jsx("button", { type: "button", className: "auth-button auth-button-primary", onClick: onBack, children: "Back to Sign in" })] }));
|
|
10863
10863
|
}
|
|
10864
10864
|
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: token ? 'Set new password' : 'Reset your password' }), jsxRuntime.jsx("p", { className: "auth-form-subtitle", children: token
|
|
10865
10865
|
? 'Enter your new password below.'
|
|
10866
|
-
: "Enter your email address and we'll send you instructions to reset your password." })] }), (error || passwordError) && (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 || passwordError] })), token ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "password", className: "auth-label", children: "New Password" }), jsxRuntime.jsx("input", { type: "password", id: "password", className: "auth-input", value: password, onChange: (e) => setPassword(e.target.value), required: true, disabled: loading, placeholder: "Enter new password", autoComplete: "new-password", minLength: 6 })] }), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "confirmPassword", className: "auth-label", children: "Confirm Password" }), jsxRuntime.jsx("input", { type: "password", id: "confirmPassword", className: "auth-input", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), required: true, disabled: loading, placeholder: "Confirm new password", autoComplete: "new-password", minLength: 6 })] })] })) : (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: email, onChange: (e) => setEmail(e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] })), jsxRuntime.jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsxRuntime.jsx("span", { className: "auth-spinner" })) : token ? ('Reset password') : ('Send reset instructions') }), jsxRuntime.jsx("div", { className: "auth-divider", children: jsxRuntime.jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onBack, disabled: loading, children: "\u2190 Back to Sign in" }) })] }));
|
|
10866
|
+
: "Enter your email address and we'll send you instructions to reset your password." })] }), (error || passwordError) && (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 || passwordError] })), token ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [resetEmail && (jsxRuntime.jsx("input", { type: "email", value: resetEmail, readOnly: true, autoComplete: "username", style: { position: 'absolute', opacity: 0, height: 0, width: 0, overflow: 'hidden', pointerEvents: 'none' }, tabIndex: -1, "aria-hidden": "true" })), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "password", className: "auth-label", children: "New Password" }), jsxRuntime.jsx("input", { type: "password", id: "password", className: "auth-input", value: password, onChange: (e) => setPassword(e.target.value), required: true, disabled: loading, placeholder: "Enter new password", autoComplete: "new-password", minLength: 6 })] }), jsxRuntime.jsxs("div", { className: "auth-form-group", children: [jsxRuntime.jsx("label", { htmlFor: "confirmPassword", className: "auth-label", children: "Confirm Password" }), jsxRuntime.jsx("input", { type: "password", id: "confirmPassword", className: "auth-input", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), required: true, disabled: loading, placeholder: "Confirm new password", autoComplete: "new-password", minLength: 6 })] })] })) : (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: email, onChange: (e) => setEmail(e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] })), jsxRuntime.jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsxRuntime.jsx("span", { className: "auth-spinner" })) : token ? ('Reset password') : ('Send reset instructions') }), jsxRuntime.jsx("div", { className: "auth-divider", children: jsxRuntime.jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onBack, disabled: loading, children: "\u2190 Back to Sign in" }) })] }));
|
|
10867
10867
|
};
|
|
10868
10868
|
|
|
10869
10869
|
const MagicLinkForm = ({ onSubmit, onCancel, loading = false, error, }) => {
|
|
@@ -10969,14 +10969,46 @@ class AuthAPI {
|
|
|
10969
10969
|
return smartlinks__namespace.authKit.verifyPhoneCode(this.clientId, phoneNumber, code);
|
|
10970
10970
|
}
|
|
10971
10971
|
async requestPasswordReset(email, redirectUrl) {
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10972
|
+
console.log('[AuthKit:API] requestPasswordReset called', { clientId: this.clientId, email, redirectUrl });
|
|
10973
|
+
try {
|
|
10974
|
+
const result = await smartlinks__namespace.authKit.requestPasswordReset(this.clientId, {
|
|
10975
|
+
email,
|
|
10976
|
+
redirectUrl,
|
|
10977
|
+
clientName: this.clientName
|
|
10978
|
+
});
|
|
10979
|
+
console.log('[AuthKit:API] requestPasswordReset resolved:', JSON.stringify(result));
|
|
10980
|
+
return result;
|
|
10981
|
+
}
|
|
10982
|
+
catch (error) {
|
|
10983
|
+
const err = error;
|
|
10984
|
+
console.error('[AuthKit:API] requestPasswordReset threw:', {
|
|
10985
|
+
type: typeof error,
|
|
10986
|
+
message: err?.message,
|
|
10987
|
+
statusCode: err?.statusCode ?? err?.response?.status,
|
|
10988
|
+
errorCode: err?.errorCode ?? err?.details?.errorCode,
|
|
10989
|
+
details: err?.details ?? err?.response?.data,
|
|
10990
|
+
keys: err ? Object.keys(err) : [],
|
|
10991
|
+
});
|
|
10992
|
+
throw error;
|
|
10993
|
+
}
|
|
10977
10994
|
}
|
|
10978
10995
|
async verifyResetToken(token) {
|
|
10979
|
-
|
|
10996
|
+
console.log('[AuthKit:API] verifyResetToken called');
|
|
10997
|
+
try {
|
|
10998
|
+
const result = await smartlinks__namespace.authKit.verifyResetToken(this.clientId, token);
|
|
10999
|
+
console.log('[AuthKit:API] verifyResetToken resolved:', JSON.stringify(result));
|
|
11000
|
+
return result;
|
|
11001
|
+
}
|
|
11002
|
+
catch (error) {
|
|
11003
|
+
const err = error;
|
|
11004
|
+
console.error('[AuthKit:API] verifyResetToken threw:', {
|
|
11005
|
+
type: typeof error,
|
|
11006
|
+
message: err?.message,
|
|
11007
|
+
statusCode: err?.statusCode ?? err?.response?.status,
|
|
11008
|
+
errorCode: err?.errorCode ?? err?.details?.errorCode,
|
|
11009
|
+
});
|
|
11010
|
+
throw error;
|
|
11011
|
+
}
|
|
10980
11012
|
}
|
|
10981
11013
|
async completePasswordReset(token, newPassword) {
|
|
10982
11014
|
return smartlinks__namespace.authKit.completePasswordReset(this.clientId, token, newPassword);
|
|
@@ -11012,7 +11044,6 @@ class AuthAPI {
|
|
|
11012
11044
|
this.log.warn('Failed to fetch UI config, using defaults:', error);
|
|
11013
11045
|
return {
|
|
11014
11046
|
branding: {
|
|
11015
|
-
title: 'Smartlinks Auth',
|
|
11016
11047
|
subtitle: 'Sign in to your account',
|
|
11017
11048
|
primaryColor: '#3B82F6',
|
|
11018
11049
|
secondaryColor: '#1D4ED8',
|
|
@@ -12408,6 +12439,10 @@ function getFriendlyErrorMessage(error) {
|
|
|
12408
12439
|
if (isApiErrorLike(error)) {
|
|
12409
12440
|
// First, check for specific error code (most precise)
|
|
12410
12441
|
const errorCode = error.errorCode || error.details?.errorCode || error.details?.error;
|
|
12442
|
+
// For rate-limit errors, prefer the backend's message as it includes specific wait times
|
|
12443
|
+
if (errorCode === 'RATE_LIMIT_EXCEEDED' && error.message && error.message !== '[object Object]') {
|
|
12444
|
+
return error.message;
|
|
12445
|
+
}
|
|
12411
12446
|
if (errorCode && ERROR_CODE_MESSAGES[errorCode]) {
|
|
12412
12447
|
return ERROR_CODE_MESSAGES[errorCode];
|
|
12413
12448
|
}
|
|
@@ -12415,6 +12450,10 @@ function getFriendlyErrorMessage(error) {
|
|
|
12415
12450
|
if (error.statusCode >= 500) {
|
|
12416
12451
|
return 'Server error. Please try again later.';
|
|
12417
12452
|
}
|
|
12453
|
+
// For 429 status, prefer backend message (may include wait time)
|
|
12454
|
+
if (error.statusCode === 429 && error.message && error.message !== '[object Object]') {
|
|
12455
|
+
return error.message;
|
|
12456
|
+
}
|
|
12418
12457
|
if (STATUS_MESSAGES[error.statusCode]) {
|
|
12419
12458
|
return STATUS_MESSAGES[error.statusCode];
|
|
12420
12459
|
}
|
|
@@ -12541,7 +12580,7 @@ function requiresEmailVerification(error) {
|
|
|
12541
12580
|
}
|
|
12542
12581
|
|
|
12543
12582
|
// VERSION: Update this when making changes to help identify which version is running
|
|
12544
|
-
const AUTH_UI_VERSION = '
|
|
12583
|
+
const AUTH_UI_VERSION = '46';
|
|
12545
12584
|
const LOG_PREFIX = `[SmartlinksAuthUI:v${AUTH_UI_VERSION}]`;
|
|
12546
12585
|
// Helper to check for URL auth params synchronously (runs during initialization)
|
|
12547
12586
|
// This prevents the form from flashing before detecting deep-link flows
|
|
@@ -12565,6 +12604,24 @@ const getExpirationFromResponse = (response) => {
|
|
|
12565
12604
|
return Date.now() + response.expiresIn;
|
|
12566
12605
|
return undefined; // Will use 7-day default in tokenStorage
|
|
12567
12606
|
};
|
|
12607
|
+
const getActionResultErrorMessage = (result) => {
|
|
12608
|
+
if (!result || typeof result !== 'object')
|
|
12609
|
+
return null;
|
|
12610
|
+
const candidate = result;
|
|
12611
|
+
const numericStatusCode = typeof candidate.statusCode === 'string'
|
|
12612
|
+
? Number(candidate.statusCode)
|
|
12613
|
+
: candidate.statusCode;
|
|
12614
|
+
const hasFailureStatus = typeof numericStatusCode === 'number' && !Number.isNaN(numericStatusCode) && numericStatusCode >= 400;
|
|
12615
|
+
const isFailure = candidate.success === false || candidate.ok === false || !!candidate.errorCode || hasFailureStatus;
|
|
12616
|
+
if (!isFailure)
|
|
12617
|
+
return null;
|
|
12618
|
+
return getFriendlyErrorMessage({
|
|
12619
|
+
statusCode: hasFailureStatus ? numericStatusCode : 400,
|
|
12620
|
+
errorCode: candidate.errorCode,
|
|
12621
|
+
message: candidate.message || 'Request failed. Please try again.',
|
|
12622
|
+
details: candidate.details,
|
|
12623
|
+
});
|
|
12624
|
+
};
|
|
12568
12625
|
// Default Smartlinks Google OAuth Client ID (public - safe to expose)
|
|
12569
12626
|
const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
|
|
12570
12627
|
// Default Google OAuth proxy URL (hosted on our whitelisted domain)
|
|
@@ -12656,6 +12713,43 @@ const getNativeBridge = () => {
|
|
|
12656
12713
|
return native;
|
|
12657
12714
|
return null;
|
|
12658
12715
|
};
|
|
12716
|
+
// Helper to register an AuthKit.onAuthResult handler for a specific callbackId
|
|
12717
|
+
// The Android native app calls: window.AuthKit.onAuthResult(resultJson)
|
|
12718
|
+
const registerAuthKitCallback = (callbackId, onResult, timeoutMs = 5000, onTimeout) => {
|
|
12719
|
+
const bridge = window.AuthKit;
|
|
12720
|
+
if (!bridge) {
|
|
12721
|
+
console.warn('[AuthKit] No AuthKit bridge found, cannot register callback');
|
|
12722
|
+
onTimeout?.();
|
|
12723
|
+
return () => { };
|
|
12724
|
+
}
|
|
12725
|
+
// Store the previous handler so we can restore on cleanup
|
|
12726
|
+
const previousHandler = bridge.onAuthResult;
|
|
12727
|
+
const timeout = setTimeout(() => {
|
|
12728
|
+
// Restore previous handler on timeout
|
|
12729
|
+
bridge.onAuthResult = previousHandler;
|
|
12730
|
+
onTimeout?.();
|
|
12731
|
+
}, timeoutMs);
|
|
12732
|
+
bridge.onAuthResult = (resultOrJson) => {
|
|
12733
|
+
const result = typeof resultOrJson === 'string' ? JSON.parse(resultOrJson) : resultOrJson;
|
|
12734
|
+
// Match by callbackId if present, otherwise accept any result
|
|
12735
|
+
if (result.callbackId && result.callbackId !== callbackId) {
|
|
12736
|
+
// Not for us — pass through to previous handler if any
|
|
12737
|
+
if (typeof previousHandler === 'function') {
|
|
12738
|
+
previousHandler(resultOrJson);
|
|
12739
|
+
}
|
|
12740
|
+
return;
|
|
12741
|
+
}
|
|
12742
|
+
clearTimeout(timeout);
|
|
12743
|
+
// Restore previous handler
|
|
12744
|
+
bridge.onAuthResult = previousHandler;
|
|
12745
|
+
onResult(result);
|
|
12746
|
+
};
|
|
12747
|
+
// Return cleanup function
|
|
12748
|
+
return () => {
|
|
12749
|
+
clearTimeout(timeout);
|
|
12750
|
+
bridge.onAuthResult = previousHandler;
|
|
12751
|
+
};
|
|
12752
|
+
};
|
|
12659
12753
|
// Sign out from Google on the native side (clears cached Google account)
|
|
12660
12754
|
// This is fire-and-forget with a timeout - gracefully degrades if not supported
|
|
12661
12755
|
const signOutGoogleNative = async () => {
|
|
@@ -12664,21 +12758,7 @@ const signOutGoogleNative = async () => {
|
|
|
12664
12758
|
return;
|
|
12665
12759
|
const callbackId = `google_signout_${Date.now()}`;
|
|
12666
12760
|
return new Promise((resolve) => {
|
|
12667
|
-
|
|
12668
|
-
// Store original callback to restore later
|
|
12669
|
-
const originalCallback = window.smartlinksNativeCallback;
|
|
12670
|
-
window.smartlinksNativeCallback = (result) => {
|
|
12671
|
-
if (result.callbackId === callbackId) {
|
|
12672
|
-
clearTimeout(timeout);
|
|
12673
|
-
// Restore original callback
|
|
12674
|
-
window.smartlinksNativeCallback = originalCallback;
|
|
12675
|
-
resolve();
|
|
12676
|
-
}
|
|
12677
|
-
else if (originalCallback) {
|
|
12678
|
-
// Pass through to original callback for other messages
|
|
12679
|
-
originalCallback(result);
|
|
12680
|
-
}
|
|
12681
|
-
};
|
|
12761
|
+
registerAuthKitCallback(callbackId, () => resolve(), 3000, () => resolve());
|
|
12682
12762
|
const payload = JSON.stringify({
|
|
12683
12763
|
type: 'GOOGLE_SIGN_OUT',
|
|
12684
12764
|
callbackId,
|
|
@@ -12692,44 +12772,32 @@ const checkSilentGoogleSignIn = async (clientId, googleClientId) => {
|
|
|
12692
12772
|
return null;
|
|
12693
12773
|
const callbackId = `google_check_${Date.now()}`;
|
|
12694
12774
|
return new Promise((resolve) => {
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
|
|
12701
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
12704
|
-
resolve({
|
|
12705
|
-
isSignedIn: result.isSignedIn || false,
|
|
12706
|
-
idToken: result.idToken,
|
|
12707
|
-
email: result.email,
|
|
12708
|
-
name: result.name,
|
|
12709
|
-
picture: result.picture,
|
|
12710
|
-
});
|
|
12711
|
-
}
|
|
12712
|
-
else {
|
|
12713
|
-
resolve(null);
|
|
12714
|
-
}
|
|
12775
|
+
registerAuthKitCallback(callbackId, (result) => {
|
|
12776
|
+
if (result.success) {
|
|
12777
|
+
resolve({
|
|
12778
|
+
isSignedIn: result.isSignedIn || false,
|
|
12779
|
+
idToken: result.idToken,
|
|
12780
|
+
email: result.email,
|
|
12781
|
+
name: result.name,
|
|
12782
|
+
picture: result.picture,
|
|
12783
|
+
});
|
|
12715
12784
|
}
|
|
12716
|
-
else
|
|
12717
|
-
|
|
12718
|
-
originalCallback(result);
|
|
12785
|
+
else {
|
|
12786
|
+
resolve(null);
|
|
12719
12787
|
}
|
|
12720
|
-
};
|
|
12788
|
+
}, 5000, () => resolve(null));
|
|
12721
12789
|
const payload = JSON.stringify({
|
|
12722
12790
|
type: 'GOOGLE_CHECK_SIGN_IN',
|
|
12723
12791
|
clientId,
|
|
12724
12792
|
googleClientId,
|
|
12725
|
-
serverClientId: googleClientId,
|
|
12793
|
+
serverClientId: googleClientId,
|
|
12726
12794
|
callbackId,
|
|
12727
12795
|
});
|
|
12728
12796
|
nativeBridge.checkGoogleSignIn(payload);
|
|
12729
12797
|
});
|
|
12730
12798
|
};
|
|
12731
12799
|
// getFriendlyErrorMessage is now imported from ../utils/errorHandling
|
|
12732
|
-
const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, onRedirect, enabledProviders = ['email', 'google', 'phone'], initialMode, signupProminence, redirectUrl, theme = '
|
|
12800
|
+
const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, onRedirect, enabledProviders = ['email', 'google', 'phone'], initialMode, signupProminence, redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, minimal = false, logger, proxyMode = false, collectionId, disableConfigCache = false, enableSilentGoogleSignIn = false, }) => {
|
|
12733
12801
|
// Resolve signup prominence from props, customization, config, or default
|
|
12734
12802
|
const resolvedSignupProminence = signupProminence || customization?.signupProminence || 'minimal';
|
|
12735
12803
|
// Determine initial mode based on signupProminence setting
|
|
@@ -12766,6 +12834,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
12766
12834
|
const [contactSchema, setContactSchema] = React.useState(null); // Schema for registration fields
|
|
12767
12835
|
const [silentSignInChecked, setSilentSignInChecked] = React.useState(false); // Track if silent sign-in has been checked
|
|
12768
12836
|
const [googleFallbackToPopup, setGoogleFallbackToPopup] = React.useState(false); // Show popup fallback when FedCM is blocked/dismissed
|
|
12837
|
+
const [googleNativeTimedOut, setGoogleNativeTimedOut] = React.useState(false); // Native bridge callback timed out
|
|
12769
12838
|
const log = React.useMemo(() => createLoggerWrapper(logger), [logger]);
|
|
12770
12839
|
const api = new AuthAPI(apiEndpoint, clientId, clientName, logger);
|
|
12771
12840
|
const auth = useAuth();
|
|
@@ -13042,6 +13111,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13042
13111
|
if (urlMode === 'verifyEmail') {
|
|
13043
13112
|
log.log('Verifying email with token:', token);
|
|
13044
13113
|
const response = await api.verifyEmailWithToken(token);
|
|
13114
|
+
log.log('Email verification response:', { hasToken: !!response.token, hasUser: !!response.user, emailVerificationMode: response.emailVerificationMode, isNewUser: response.isNewUser });
|
|
13045
13115
|
// Get email verification mode from response or config
|
|
13046
13116
|
const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-auto-login';
|
|
13047
13117
|
if ((verificationMode === 'verify-auto-login' || verificationMode === 'immediate') && response.token) {
|
|
@@ -13086,9 +13156,9 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13086
13156
|
log.log('Verifying reset token:', token);
|
|
13087
13157
|
// Verify token is valid, then show password reset form
|
|
13088
13158
|
const verifyResult = await api.verifyResetToken(token);
|
|
13089
|
-
//
|
|
13090
|
-
if (!verifyResult.valid) {
|
|
13091
|
-
throw new Error(verifyResult
|
|
13159
|
+
// Guard against undefined result (proxy mode may resolve without data)
|
|
13160
|
+
if (!verifyResult || !verifyResult.valid) {
|
|
13161
|
+
throw new Error(verifyResult?.message || 'Invalid or expired token');
|
|
13092
13162
|
}
|
|
13093
13163
|
setUrlAuthProcessing(false); // Clear processing state - showing reset form
|
|
13094
13164
|
setResetToken(token); // Store token for use in password reset
|
|
@@ -13103,6 +13173,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13103
13173
|
else if (urlMode === 'magicLink') {
|
|
13104
13174
|
log.log('Verifying magic link token:', token);
|
|
13105
13175
|
const response = await api.verifyMagicLink(token);
|
|
13176
|
+
log.log('Magic link verification response:', { hasToken: !!response.token, hasUser: !!response.user, isNewUser: response.isNewUser });
|
|
13106
13177
|
// Auto-login with magic link if token is provided
|
|
13107
13178
|
if (response.token) {
|
|
13108
13179
|
// Always await - auth.login now waits for parent ack automatically in iframe mode
|
|
@@ -13178,6 +13249,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13178
13249
|
const redirectUri = state.redirectUri || window.location.origin + window.location.pathname;
|
|
13179
13250
|
// Exchange authorization code for tokens
|
|
13180
13251
|
const response = await api.loginWithGoogleCode(code, redirectUri);
|
|
13252
|
+
log.log('Google OAuth code exchange response:', { hasToken: !!response.token, hasUser: !!response.user, isNewUser: response.isNewUser });
|
|
13181
13253
|
if (response.token) {
|
|
13182
13254
|
// Await login to ensure token is persisted before any navigation
|
|
13183
13255
|
await auth.login(response.token, response.user, response.accountData, response.isNewUser, getExpirationFromResponse(response));
|
|
@@ -13208,13 +13280,15 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13208
13280
|
setError(undefined);
|
|
13209
13281
|
setAuthSuccess(false);
|
|
13210
13282
|
try {
|
|
13283
|
+
log.log('Submitting email auth:', { mode, email: data.email });
|
|
13211
13284
|
const response = mode === 'login'
|
|
13212
13285
|
? await api.login(data.email, data.password)
|
|
13213
13286
|
: await api.register({
|
|
13214
13287
|
...data,
|
|
13215
13288
|
accountData: mode === 'register' ? accountData : undefined,
|
|
13216
|
-
redirectUrl: getRedirectUrl(),
|
|
13289
|
+
redirectUrl: getRedirectUrl(),
|
|
13217
13290
|
});
|
|
13291
|
+
log.log('Email auth response:', { mode, hasToken: !!response?.token, hasUser: !!response?.user, isNewUser: response?.isNewUser, requiresEmailVerification: response?.requiresEmailVerification, accountLocked: response?.accountLocked, emailVerificationMode: response?.emailVerificationMode });
|
|
13218
13292
|
// Defensive check: validate response before accessing properties
|
|
13219
13293
|
// SDK should throw on 401/error responses, but handle edge cases gracefully
|
|
13220
13294
|
if (!response) {
|
|
@@ -13362,17 +13436,46 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13362
13436
|
return;
|
|
13363
13437
|
setLoading(true);
|
|
13364
13438
|
setError(undefined);
|
|
13439
|
+
setAuthSuccess(false);
|
|
13440
|
+
setResetSuccess(false);
|
|
13441
|
+
setSuccessMessage(undefined);
|
|
13442
|
+
setShowResendVerification(false);
|
|
13365
13443
|
try {
|
|
13366
13444
|
const result = await api.requestPasswordReset(resetRequestEmail, getRedirectUrl());
|
|
13367
|
-
|
|
13445
|
+
const resultError = getActionResultErrorMessage(result);
|
|
13446
|
+
if (resultError) {
|
|
13447
|
+
setAuthSuccess(false);
|
|
13448
|
+
setResetSuccess(false);
|
|
13449
|
+
setSuccessMessage(undefined);
|
|
13450
|
+
setError(resultError);
|
|
13451
|
+
setShowRequestNewReset(true);
|
|
13452
|
+
return;
|
|
13453
|
+
}
|
|
13454
|
+
const resultAny = result;
|
|
13455
|
+
const isConfirmedSuccess = result && (resultAny.success === true ||
|
|
13456
|
+
resultAny.ok === true ||
|
|
13457
|
+
(resultAny.message && !resultAny.errorCode && !resultAny.statusCode));
|
|
13458
|
+
if (!isConfirmedSuccess) {
|
|
13459
|
+
setAuthSuccess(false);
|
|
13460
|
+
setResetSuccess(false);
|
|
13461
|
+
setSuccessMessage(undefined);
|
|
13462
|
+
setError(resultAny?.message || 'Unable to send password reset email. Please try again.');
|
|
13463
|
+
setShowRequestNewReset(true);
|
|
13464
|
+
return;
|
|
13465
|
+
}
|
|
13466
|
+
setAuthSuccess(false);
|
|
13467
|
+
setMode('reset-password');
|
|
13368
13468
|
setResetSuccess(true);
|
|
13369
|
-
// Use backend message if available, otherwise default
|
|
13370
13469
|
setSuccessMessage(result?.message || 'Password reset email sent! Please check your inbox.');
|
|
13371
13470
|
setShowRequestNewReset(false);
|
|
13372
13471
|
setResetRequestEmail('');
|
|
13373
13472
|
}
|
|
13374
13473
|
catch (err) {
|
|
13474
|
+
setAuthSuccess(false);
|
|
13475
|
+
setResetSuccess(false);
|
|
13476
|
+
setSuccessMessage(undefined);
|
|
13375
13477
|
setError(getFriendlyErrorMessage(err));
|
|
13478
|
+
setShowRequestNewReset(true);
|
|
13376
13479
|
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
13377
13480
|
}
|
|
13378
13481
|
finally {
|
|
@@ -13427,21 +13530,20 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13427
13530
|
if (nativeBridge) {
|
|
13428
13531
|
log.log('Using native bridge for Google Sign-In');
|
|
13429
13532
|
const callbackId = `google_auth_${Date.now()}`;
|
|
13430
|
-
|
|
13431
|
-
|
|
13432
|
-
|
|
13433
|
-
log.log('Ignoring stale native callback:', result.callbackId);
|
|
13434
|
-
return;
|
|
13435
|
-
}
|
|
13436
|
-
log.log('Native callback received:', {
|
|
13533
|
+
// Register callback via AuthKit.onAuthResult (the native app calls this)
|
|
13534
|
+
const cleanup = registerAuthKitCallback(callbackId, async (result) => {
|
|
13535
|
+
log.log('Native Google auth result received:', {
|
|
13437
13536
|
success: result.success,
|
|
13438
13537
|
hasIdToken: !!result.idToken,
|
|
13439
13538
|
email: result.email,
|
|
13440
13539
|
error: result.error,
|
|
13540
|
+
callbackId: result.callbackId,
|
|
13441
13541
|
});
|
|
13542
|
+
setGoogleNativeTimedOut(false);
|
|
13442
13543
|
try {
|
|
13443
13544
|
if (result.success && result.idToken) {
|
|
13444
13545
|
const authResponse = await api.loginWithGoogle(result.idToken);
|
|
13546
|
+
log.log('Native Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
13445
13547
|
if (authResponse.token) {
|
|
13446
13548
|
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
13447
13549
|
setAuthSuccess(true);
|
|
@@ -13464,7 +13566,12 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13464
13566
|
finally {
|
|
13465
13567
|
setLoading(false);
|
|
13466
13568
|
}
|
|
13467
|
-
}
|
|
13569
|
+
}, 30000, // 30s timeout for interactive Google Sign-In
|
|
13570
|
+
() => {
|
|
13571
|
+
log.log('Native Google Sign-In timed out after 30s');
|
|
13572
|
+
setLoading(false);
|
|
13573
|
+
setGoogleNativeTimedOut(true);
|
|
13574
|
+
});
|
|
13468
13575
|
const payloadObj = {
|
|
13469
13576
|
type: 'GOOGLE_SIGN_IN',
|
|
13470
13577
|
clientId,
|
|
@@ -13476,12 +13583,13 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13476
13583
|
requestServerAuthCode: false,
|
|
13477
13584
|
};
|
|
13478
13585
|
const payload = JSON.stringify(payloadObj);
|
|
13479
|
-
log.log('Invoking native signInWithGoogle');
|
|
13586
|
+
log.log('Invoking native signInWithGoogle with callbackId:', callbackId);
|
|
13480
13587
|
try {
|
|
13481
13588
|
nativeBridge.signInWithGoogle(payload);
|
|
13482
13589
|
}
|
|
13483
13590
|
catch (invokeError) {
|
|
13484
13591
|
console.error(`${LOG_PREFIX} Exception invoking signInWithGoogle:`, invokeError);
|
|
13592
|
+
cleanup(); // Clean up the callback registration
|
|
13485
13593
|
throw invokeError;
|
|
13486
13594
|
}
|
|
13487
13595
|
// Don't set loading to false - waiting for native callback
|
|
@@ -13526,6 +13634,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13526
13634
|
clearInterval(checkClosed);
|
|
13527
13635
|
try {
|
|
13528
13636
|
if (event.data.success && event.data.token) {
|
|
13637
|
+
log.log('Google proxy result received:', { success: true, hasUser: !!event.data.user, isNewUser: event.data.isNewUser });
|
|
13529
13638
|
const { token, user, accountData, isNewUser, expiresAt, expiresIn } = event.data;
|
|
13530
13639
|
const expiration = expiresAt || (expiresIn ? Date.now() + expiresIn : undefined);
|
|
13531
13640
|
await auth.login(token, user, accountData, isNewUser, expiration);
|
|
@@ -13653,6 +13762,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13653
13762
|
googleUserInfo: userInfo,
|
|
13654
13763
|
tokenType: 'access_token',
|
|
13655
13764
|
});
|
|
13765
|
+
log.log('Popup Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
13656
13766
|
if (authResponse.token) {
|
|
13657
13767
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
13658
13768
|
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
@@ -13692,12 +13802,17 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13692
13802
|
client_id: googleClientId,
|
|
13693
13803
|
origin: window.location.origin,
|
|
13694
13804
|
});
|
|
13805
|
+
// Track whether the credential callback has fired (user selected an account)
|
|
13806
|
+
// so we don't show the "blocked" fallback when the prompt closes after a successful selection
|
|
13807
|
+
let credentialCallbackFired = false;
|
|
13695
13808
|
google.accounts.id.initialize({
|
|
13696
13809
|
client_id: googleClientId,
|
|
13697
13810
|
callback: async (response) => {
|
|
13811
|
+
credentialCallbackFired = true;
|
|
13698
13812
|
try {
|
|
13699
13813
|
const idToken = response.credential;
|
|
13700
13814
|
const authResponse = await api.loginWithGoogle(idToken);
|
|
13815
|
+
log.log('OneTap Google login response:', { hasToken: !!authResponse.token, hasUser: !!authResponse.user, isNewUser: authResponse.isNewUser });
|
|
13701
13816
|
if (authResponse.token) {
|
|
13702
13817
|
// Google OAuth can be login or signup - use isNewUser flag from backend if available
|
|
13703
13818
|
await auth.login(authResponse.token, authResponse.user, authResponse.accountData, authResponse.isNewUser, getExpirationFromResponse(authResponse));
|
|
@@ -13721,23 +13836,29 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13721
13836
|
cancel_on_tap_outside: true,
|
|
13722
13837
|
use_fedcm_for_prompt: true, // Enable FedCM for future browser compatibility
|
|
13723
13838
|
});
|
|
13724
|
-
// Use timeout fallback — if no prompt interaction after
|
|
13839
|
+
// Use timeout fallback — if no prompt interaction after 15s, assume FedCM was blocked
|
|
13840
|
+
// (15s gives users enough time to choose a Google account from the picker)
|
|
13725
13841
|
const promptTimeout = setTimeout(() => {
|
|
13842
|
+
if (credentialCallbackFired)
|
|
13843
|
+
return; // Auth is already in progress, don't show fallback
|
|
13726
13844
|
log.log('Google OneTap prompt timed out — FedCM may be blocked or unavailable');
|
|
13727
13845
|
setGoogleFallbackToPopup(true);
|
|
13728
13846
|
setLoading(false);
|
|
13729
|
-
},
|
|
13847
|
+
}, 15000);
|
|
13730
13848
|
google.accounts.id.prompt((notification) => {
|
|
13731
13849
|
clearTimeout(promptTimeout);
|
|
13850
|
+
// If the credential callback already fired, the user successfully selected an account
|
|
13851
|
+
// The prompt closing with 'dismissed' is expected — don't show fallback
|
|
13852
|
+
if (credentialCallbackFired) {
|
|
13853
|
+
log.log('Google OneTap prompt closed after credential selection — ignoring');
|
|
13854
|
+
return;
|
|
13855
|
+
}
|
|
13732
13856
|
// Check for FedCM/OneTap dismissal or blocking
|
|
13733
|
-
// notification may have getMomentType(), getDismissedReason(), getSkippedReason()
|
|
13734
13857
|
const momentType = notification?.getMomentType?.();
|
|
13735
13858
|
const dismissedReason = notification?.getDismissedReason?.();
|
|
13736
13859
|
const skippedReason = notification?.getSkippedReason?.();
|
|
13737
13860
|
log.log('Google OneTap prompt notification:', { momentType, dismissedReason, skippedReason });
|
|
13738
13861
|
if (momentType === 'skipped' || momentType === 'dismissed') {
|
|
13739
|
-
// User dismissed the prompt, or browser blocked it (FedCM disabled)
|
|
13740
|
-
// Offer popup flow as alternative
|
|
13741
13862
|
log.log('Google OneTap was dismissed/skipped, offering popup fallback');
|
|
13742
13863
|
setGoogleFallbackToPopup(true);
|
|
13743
13864
|
}
|
|
@@ -13764,6 +13885,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13764
13885
|
else {
|
|
13765
13886
|
// Verify code - Twilio identifies the verification by phone number
|
|
13766
13887
|
const response = await api.verifyPhoneCode(phoneNumber, verificationCode);
|
|
13888
|
+
log.log('Phone verification response:', { hasToken: !!response?.token, hasUser: !!response?.user, isNewUser: response?.isNewUser });
|
|
13767
13889
|
// Defensive validation: API may return undefined or error object on failure
|
|
13768
13890
|
if (!response) {
|
|
13769
13891
|
throw new Error('Verification failed - please check your code and try again');
|
|
@@ -13798,6 +13920,9 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13798
13920
|
const handlePasswordReset = async (emailOrPassword, confirmPassword) => {
|
|
13799
13921
|
setLoading(true);
|
|
13800
13922
|
setError(undefined);
|
|
13923
|
+
setAuthSuccess(false);
|
|
13924
|
+
setResetSuccess(false);
|
|
13925
|
+
setSuccessMessage(undefined);
|
|
13801
13926
|
const effectiveRedirectUrl = getRedirectUrl();
|
|
13802
13927
|
try {
|
|
13803
13928
|
if (resetToken && confirmPassword) {
|
|
@@ -13806,6 +13931,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13806
13931
|
// Auto-login with the new password if we have the email
|
|
13807
13932
|
if (resetEmail) {
|
|
13808
13933
|
try {
|
|
13934
|
+
log.log('Auto-login after password reset for:', resetEmail);
|
|
13809
13935
|
const loginResponse = await api.login(resetEmail, emailOrPassword);
|
|
13810
13936
|
if (loginResponse.token) {
|
|
13811
13937
|
await auth.login(loginResponse.token, loginResponse.user, loginResponse.accountData, false);
|
|
@@ -13818,9 +13944,12 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13818
13944
|
}
|
|
13819
13945
|
}
|
|
13820
13946
|
catch (loginErr) {
|
|
13821
|
-
|
|
13947
|
+
log.warn('Auto-login after password reset failed, requiring manual login:', loginErr);
|
|
13822
13948
|
}
|
|
13823
13949
|
}
|
|
13950
|
+
else {
|
|
13951
|
+
log.warn('No resetEmail available for auto-login after password reset');
|
|
13952
|
+
}
|
|
13824
13953
|
// Fallback: show success but require manual login
|
|
13825
13954
|
setResetSuccess(true);
|
|
13826
13955
|
setSuccessMessage('Password reset successful! Please sign in with your new password.');
|
|
@@ -13830,14 +13959,35 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13830
13959
|
else {
|
|
13831
13960
|
// Request password reset email
|
|
13832
13961
|
const result = await api.requestPasswordReset(emailOrPassword, effectiveRedirectUrl);
|
|
13962
|
+
const resultError = getActionResultErrorMessage(result);
|
|
13963
|
+
if (resultError) {
|
|
13964
|
+
setAuthSuccess(false);
|
|
13965
|
+
setResetSuccess(false);
|
|
13966
|
+
setSuccessMessage(undefined);
|
|
13967
|
+
setError(resultError);
|
|
13968
|
+
return;
|
|
13969
|
+
}
|
|
13970
|
+
const resultAny = result;
|
|
13971
|
+
const isConfirmedSuccess = result && (resultAny.success === true ||
|
|
13972
|
+
resultAny.ok === true ||
|
|
13973
|
+
(resultAny.message && !resultAny.errorCode && !resultAny.statusCode));
|
|
13974
|
+
if (!isConfirmedSuccess) {
|
|
13975
|
+
setAuthSuccess(false);
|
|
13976
|
+
setResetSuccess(false);
|
|
13977
|
+
setSuccessMessage(undefined);
|
|
13978
|
+
setError(resultAny?.message || 'Unable to send password reset email. Please try again.');
|
|
13979
|
+
return;
|
|
13980
|
+
}
|
|
13833
13981
|
setResetSuccess(true);
|
|
13834
|
-
// Use backend message if available
|
|
13835
13982
|
if (result?.message) {
|
|
13836
13983
|
setSuccessMessage(result.message);
|
|
13837
13984
|
}
|
|
13838
13985
|
}
|
|
13839
13986
|
}
|
|
13840
13987
|
catch (err) {
|
|
13988
|
+
setAuthSuccess(false);
|
|
13989
|
+
setResetSuccess(false);
|
|
13990
|
+
setSuccessMessage(undefined);
|
|
13841
13991
|
setError(getFriendlyErrorMessage(err));
|
|
13842
13992
|
onAuthError?.(err instanceof Error ? err : new Error(getFriendlyErrorMessage(err)));
|
|
13843
13993
|
}
|
|
@@ -13901,7 +14051,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13901
14051
|
setResetToken(undefined); // Clear token when going back
|
|
13902
14052
|
setResetEmail(undefined); // Clear email when going back
|
|
13903
14053
|
setSuccessMessage(undefined); // Clear success message
|
|
13904
|
-
}, loading: loading, error: error, success: resetSuccess, successMessage: successMessage, token: resetToken })) : (mode === 'login' || mode === 'register') ? (jsxRuntime.jsx(jsxRuntime.Fragment, { children: showResendVerification ? (jsxRuntime.jsxs("div", { style: {
|
|
14054
|
+
}, loading: loading, error: error, success: resetSuccess, successMessage: successMessage, token: resetToken, resetEmail: resetEmail })) : (mode === 'login' || mode === 'register') ? (jsxRuntime.jsx(jsxRuntime.Fragment, { children: showResendVerification ? (jsxRuntime.jsxs("div", { style: {
|
|
13905
14055
|
marginTop: '1rem',
|
|
13906
14056
|
padding: '1.5rem',
|
|
13907
14057
|
backgroundColor: config?.branding?.inheritHostStyles
|
|
@@ -13999,7 +14149,16 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
13999
14149
|
color: config?.branding?.inheritHostStyles
|
|
14000
14150
|
? 'hsl(var(--foreground, 215 25% 15%))'
|
|
14001
14151
|
: (resolvedTheme === 'dark' ? '#f1f5f9' : '#374151')
|
|
14002
|
-
}, children: "Password Reset Link Expired" }), jsxRuntime.jsx("
|
|
14152
|
+
}, children: "Password Reset Link Expired" }), error && (jsxRuntime.jsx("div", { style: {
|
|
14153
|
+
marginBottom: '1rem',
|
|
14154
|
+
padding: '0.75rem',
|
|
14155
|
+
backgroundColor: resolvedTheme === 'dark' ? 'rgba(239, 68, 68, 0.15)' : 'rgba(239, 68, 68, 0.1)',
|
|
14156
|
+
border: `1px solid ${resolvedTheme === 'dark' ? 'rgba(239, 68, 68, 0.3)' : 'rgba(239, 68, 68, 0.2)'}`,
|
|
14157
|
+
borderRadius: '0.375rem',
|
|
14158
|
+
color: resolvedTheme === 'dark' ? '#fca5a5' : '#dc2626',
|
|
14159
|
+
fontSize: '0.875rem',
|
|
14160
|
+
lineHeight: '1.5',
|
|
14161
|
+
}, children: error })), jsxRuntime.jsx("p", { style: {
|
|
14003
14162
|
marginBottom: '1rem',
|
|
14004
14163
|
fontSize: '0.875rem',
|
|
14005
14164
|
color: config?.branding?.inheritHostStyles
|
|
@@ -14098,10 +14257,48 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14098
14257
|
backgroundColor: 'transparent',
|
|
14099
14258
|
border: 'none',
|
|
14100
14259
|
cursor: 'pointer',
|
|
14101
|
-
}, children: "Dismiss" })] })), ((
|
|
14260
|
+
}, children: "Dismiss" })] })), googleNativeTimedOut && (jsxRuntime.jsxs("div", { style: {
|
|
14261
|
+
marginBottom: '1rem',
|
|
14262
|
+
padding: '0.75rem 1rem',
|
|
14263
|
+
borderRadius: '0.5rem',
|
|
14264
|
+
backgroundColor: resolvedTheme === 'dark' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(245, 158, 11, 0.05)',
|
|
14265
|
+
border: `1px solid ${resolvedTheme === 'dark' ? 'rgba(245, 158, 11, 0.3)' : 'rgba(245, 158, 11, 0.2)'}`,
|
|
14266
|
+
}, children: [jsxRuntime.jsx("p", { style: {
|
|
14267
|
+
fontSize: '0.8125rem',
|
|
14268
|
+
color: resolvedTheme === 'dark' ? '#fbbf24' : '#92400e',
|
|
14269
|
+
marginBottom: '0.5rem',
|
|
14270
|
+
lineHeight: 1.4,
|
|
14271
|
+
}, children: 'Google sign-in didn\'t respond. You can try again or use another sign-in method.' }), jsxRuntime.jsx("button", { onClick: () => {
|
|
14272
|
+
setGoogleNativeTimedOut(false);
|
|
14273
|
+
handleGoogleLogin();
|
|
14274
|
+
}, style: {
|
|
14275
|
+
width: '100%',
|
|
14276
|
+
padding: '0.5rem 1rem',
|
|
14277
|
+
fontSize: '0.875rem',
|
|
14278
|
+
fontWeight: 500,
|
|
14279
|
+
color: '#fff',
|
|
14280
|
+
backgroundColor: '#4285F4',
|
|
14281
|
+
border: 'none',
|
|
14282
|
+
borderRadius: '0.375rem',
|
|
14283
|
+
cursor: 'pointer',
|
|
14284
|
+
}, children: 'Retry Google Sign-In' }), jsxRuntime.jsx("button", { onClick: () => setGoogleNativeTimedOut(false), style: {
|
|
14285
|
+
marginTop: '0.375rem',
|
|
14286
|
+
width: '100%',
|
|
14287
|
+
padding: '0.25rem',
|
|
14288
|
+
fontSize: '0.75rem',
|
|
14289
|
+
color: resolvedTheme === 'dark' ? '#64748b' : '#9ca3af',
|
|
14290
|
+
backgroundColor: 'transparent',
|
|
14291
|
+
border: 'none',
|
|
14292
|
+
cursor: 'pointer',
|
|
14293
|
+
}, children: 'Dismiss' })] })), (() => {
|
|
14102
14294
|
const emailDisplayMode = config?.emailDisplayMode || 'form';
|
|
14103
14295
|
const providerOrder = config?.providerOrder || (config?.enabledProviders || enabledProviders);
|
|
14104
14296
|
const actualProviders = config?.enabledProviders || enabledProviders;
|
|
14297
|
+
const hasEmailProvider = actualProviders.includes('email');
|
|
14298
|
+
// If email provider is not enabled, only show provider buttons (no email/password form)
|
|
14299
|
+
if (!hasEmailProvider) {
|
|
14300
|
+
return (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
|
|
14301
|
+
}
|
|
14105
14302
|
// Button mode: show provider selection first, then email form if email is selected
|
|
14106
14303
|
if (emailDisplayMode === 'button' && !showEmailForm) {
|
|
14107
14304
|
return (jsxRuntime.jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
|
|
@@ -14123,7 +14320,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
14123
14320
|
setShowResendVerification(false);
|
|
14124
14321
|
setShowRequestNewReset(false);
|
|
14125
14322
|
setError(undefined);
|
|
14126
|
-
}, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, signupProminence: resolvedSignupProminence, 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 }))] }));
|
|
14323
|
+
}, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error, signupProminence: resolvedSignupProminence, signupRedirectUrl: config?.signupRedirectUrl || customization?.signupRedirectUrl, 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 }))] }));
|
|
14127
14324
|
})()] })) })) : null }));
|
|
14128
14325
|
};
|
|
14129
14326
|
|