@proveanything/smartlinks-auth-ui 0.1.0 → 0.1.2
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 +103 -5
- package/dist/api.d.ts +8 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/components/AccountManagement.d.ts +5 -0
- package/dist/components/AccountManagement.d.ts.map +1 -0
- package/dist/components/AuthUIPreview.d.ts +2 -0
- package/dist/components/AuthUIPreview.d.ts.map +1 -1
- package/dist/components/MagicLinkForm.d.ts +10 -0
- package/dist/components/MagicLinkForm.d.ts.map +1 -0
- package/dist/components/OTPInput.d.ts +11 -0
- package/dist/components/OTPInput.d.ts.map +1 -0
- package/dist/components/PhoneAuthForm.d.ts.map +1 -1
- package/dist/components/PhoneInput.d.ts +11 -0
- package/dist/components/PhoneInput.d.ts.map +1 -0
- package/dist/components/ProviderButtons.d.ts +3 -0
- package/dist/components/ProviderButtons.d.ts.map +1 -1
- package/dist/context/AuthContext.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.css.map +1 -1
- package/dist/index.esm.js +720 -66
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +719 -64
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +78 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { useEffect, useState, createContext, useCallback, useContext } from 'react';
|
|
2
|
+
import { useEffect, useState, useRef, createContext, useCallback, useContext } from 'react';
|
|
3
3
|
import * as smartlinks from '@proveanything/smartlinks';
|
|
4
|
+
import PhoneInputComponent from 'react-phone-number-input';
|
|
5
|
+
import 'react-phone-number-input/style.css';
|
|
4
6
|
|
|
5
7
|
const AuthContainer = ({ children, theme = 'light', className = '', config, }) => {
|
|
6
8
|
// Apply CSS variables for customization
|
|
@@ -64,20 +66,159 @@ const EmailAuthForm = ({ mode, onSubmit, onModeSwitch, onForgotPassword, loading
|
|
|
64
66
|
: '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 === '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' })] })] }));
|
|
65
67
|
};
|
|
66
68
|
|
|
67
|
-
const ProviderButtons = ({ enabledProviders, onGoogleLogin, onPhoneLogin, loading, }) => {
|
|
69
|
+
const ProviderButtons = ({ enabledProviders, providerOrder, onEmailLogin, onGoogleLogin, onPhoneLogin, onMagicLinkLogin, loading, }) => {
|
|
68
70
|
if (enabledProviders.length === 0)
|
|
69
71
|
return null;
|
|
70
|
-
|
|
72
|
+
// Determine the order of providers to display
|
|
73
|
+
const orderedProviders = providerOrder && providerOrder.length > 0
|
|
74
|
+
? providerOrder.filter(p => enabledProviders.includes(p))
|
|
75
|
+
: enabledProviders;
|
|
76
|
+
// Provider button configurations
|
|
77
|
+
const providerConfigs = {
|
|
78
|
+
email: {
|
|
79
|
+
label: 'Continue with Email',
|
|
80
|
+
icon: (jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) })),
|
|
81
|
+
onClick: () => onEmailLogin?.()
|
|
82
|
+
},
|
|
83
|
+
google: {
|
|
84
|
+
label: 'Continue with Google',
|
|
85
|
+
icon: (jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [jsx("path", { d: "M19.6 10.227c0-.709-.064-1.39-.182-2.045H10v3.868h5.382a4.6 4.6 0 01-1.996 3.018v2.51h3.232c1.891-1.742 2.982-4.305 2.982-7.35z", fill: "#4285F4" }), jsx("path", { d: "M10 20c2.7 0 4.964-.895 6.618-2.423l-3.232-2.509c-.895.6-2.04.955-3.386.955-2.605 0-4.81-1.76-5.595-4.123H1.064v2.59A9.996 9.996 0 0010 20z", fill: "#34A853" }), jsx("path", { d: "M4.405 11.9c-.2-.6-.314-1.24-.314-1.9 0-.66.114-1.3.314-1.9V5.51H1.064A9.996 9.996 0 000 10c0 1.614.386 3.14 1.064 4.49l3.34-2.59z", fill: "#FBBC05" }), jsx("path", { d: "M10 3.977c1.468 0 2.786.505 3.823 1.496l2.868-2.868C14.959.99 12.695 0 10 0 6.09 0 2.71 2.24 1.064 5.51l3.34 2.59C5.19 5.736 7.395 3.977 10 3.977z", fill: "#EA4335" })] })),
|
|
86
|
+
onClick: onGoogleLogin
|
|
87
|
+
},
|
|
88
|
+
phone: {
|
|
89
|
+
label: 'Continue with Phone',
|
|
90
|
+
icon: (jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V15a2 2 0 01-2 2h-1C7.82 17 2 11.18 2 5V4z" }) })),
|
|
91
|
+
onClick: onPhoneLogin
|
|
92
|
+
},
|
|
93
|
+
'magic-link': {
|
|
94
|
+
label: 'Continue with Magic Link',
|
|
95
|
+
icon: (jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) })),
|
|
96
|
+
onClick: () => onMagicLinkLogin?.()
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
// Show divider only if email handler is not provided (meaning this is showing alternative providers, not all providers)
|
|
100
|
+
const showDivider = !onEmailLogin;
|
|
101
|
+
return (jsxs(Fragment, { children: [showDivider && (jsx("div", { className: "auth-or-divider", children: jsx("span", { children: "or continue with" }) })), jsx("div", { className: "auth-provider-buttons", children: orderedProviders.map((provider) => {
|
|
102
|
+
const config = providerConfigs[provider];
|
|
103
|
+
if (!config)
|
|
104
|
+
return null;
|
|
105
|
+
return (jsxs("button", { type: "button", className: "auth-provider-button", onClick: config.onClick, disabled: loading, children: [config.icon, config.label] }, provider));
|
|
106
|
+
}) })] }));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const OTPInput = ({ length = 6, value, onChange, disabled = false, }) => {
|
|
110
|
+
const [otp, setOtp] = useState(Array(length).fill(''));
|
|
111
|
+
const inputRefs = useRef([]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const digits = value.split('').slice(0, length);
|
|
114
|
+
const newOtp = [...digits, ...Array(Math.max(0, length - digits.length)).fill('')];
|
|
115
|
+
setOtp(newOtp);
|
|
116
|
+
}, [value, length]);
|
|
117
|
+
const handleChange = (index, digit) => {
|
|
118
|
+
if (disabled)
|
|
119
|
+
return;
|
|
120
|
+
// Only allow numbers
|
|
121
|
+
if (digit && !/^\d$/.test(digit))
|
|
122
|
+
return;
|
|
123
|
+
const newOtp = [...otp];
|
|
124
|
+
newOtp[index] = digit;
|
|
125
|
+
setOtp(newOtp);
|
|
126
|
+
// Call onChange with the full OTP value
|
|
127
|
+
onChange(newOtp.join(''));
|
|
128
|
+
// Move to next input if digit was entered
|
|
129
|
+
if (digit && index < length - 1) {
|
|
130
|
+
inputRefs.current[index + 1]?.focus();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const handleKeyDown = (index, e) => {
|
|
134
|
+
if (disabled)
|
|
135
|
+
return;
|
|
136
|
+
// Handle backspace
|
|
137
|
+
if (e.key === 'Backspace' && !otp[index] && index > 0) {
|
|
138
|
+
inputRefs.current[index - 1]?.focus();
|
|
139
|
+
}
|
|
140
|
+
// Handle arrow keys
|
|
141
|
+
if (e.key === 'ArrowLeft' && index > 0) {
|
|
142
|
+
inputRefs.current[index - 1]?.focus();
|
|
143
|
+
}
|
|
144
|
+
if (e.key === 'ArrowRight' && index < length - 1) {
|
|
145
|
+
inputRefs.current[index + 1]?.focus();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const handlePaste = (e) => {
|
|
149
|
+
if (disabled)
|
|
150
|
+
return;
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
const pastedData = e.clipboardData.getData('text/plain').slice(0, length);
|
|
153
|
+
const digits = pastedData.replace(/\D/g, '').split('');
|
|
154
|
+
const newOtp = [...Array(length).fill('')];
|
|
155
|
+
digits.forEach((digit, idx) => {
|
|
156
|
+
if (idx < length) {
|
|
157
|
+
newOtp[idx] = digit;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
setOtp(newOtp);
|
|
161
|
+
onChange(newOtp.join(''));
|
|
162
|
+
// Focus the next empty input or the last one
|
|
163
|
+
const nextEmptyIndex = newOtp.findIndex(digit => !digit);
|
|
164
|
+
const focusIndex = nextEmptyIndex === -1 ? length - 1 : nextEmptyIndex;
|
|
165
|
+
inputRefs.current[focusIndex]?.focus();
|
|
166
|
+
};
|
|
167
|
+
return (jsx("div", { className: "otp-input-container", children: otp.map((digit, index) => (jsx("input", { ref: (el) => (inputRefs.current[index] = el), type: "text", inputMode: "numeric", maxLength: 1, value: digit, onChange: (e) => handleChange(index, e.target.value), onKeyDown: (e) => handleKeyDown(index, e), onPaste: handlePaste, disabled: disabled, className: "otp-input-box", autoComplete: "off" }, index))) }));
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const PhoneInput = ({ value, onChange, disabled = false, }) => {
|
|
171
|
+
const [defaultCountry, setDefaultCountry] = useState('US');
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
// Auto-detect country based on user's location
|
|
174
|
+
const detectCountry = async () => {
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch('https://ipapi.co/json/');
|
|
177
|
+
const data = await response.json();
|
|
178
|
+
if (data.country_code) {
|
|
179
|
+
setDefaultCountry(data.country_code);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.log('Could not detect country, using default');
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
detectCountry();
|
|
187
|
+
}, []);
|
|
188
|
+
return (jsx(PhoneInputComponent, { international: true, defaultCountry: defaultCountry, value: value, onChange: (value) => onChange(value || ''), disabled: disabled, className: "phone-input-wrapper" }));
|
|
71
189
|
};
|
|
72
190
|
|
|
73
191
|
const PhoneAuthForm = ({ onSubmit, onBack, loading, error, }) => {
|
|
74
192
|
const [phoneNumber, setPhoneNumber] = useState('');
|
|
75
193
|
const [verificationCode, setVerificationCode] = useState('');
|
|
76
194
|
const [codeSent, setCodeSent] = useState(false);
|
|
195
|
+
const [resendCooldown, setResendCooldown] = useState(0);
|
|
196
|
+
// Countdown timer for resend button
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (resendCooldown > 0) {
|
|
199
|
+
const timer = setTimeout(() => {
|
|
200
|
+
setResendCooldown(resendCooldown - 1);
|
|
201
|
+
}, 1000);
|
|
202
|
+
return () => clearTimeout(timer);
|
|
203
|
+
}
|
|
204
|
+
}, [resendCooldown]);
|
|
77
205
|
const handleSendCode = async (e) => {
|
|
78
206
|
e.preventDefault();
|
|
79
207
|
await onSubmit(phoneNumber);
|
|
80
208
|
setCodeSent(true);
|
|
209
|
+
setResendCooldown(60); // 60 second cooldown
|
|
210
|
+
};
|
|
211
|
+
const handleResendCode = async () => {
|
|
212
|
+
if (resendCooldown > 0)
|
|
213
|
+
return;
|
|
214
|
+
try {
|
|
215
|
+
await onSubmit(phoneNumber);
|
|
216
|
+
setResendCooldown(60); // Reset cooldown
|
|
217
|
+
setVerificationCode(''); // Clear the code input
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
// Error will be handled by parent component
|
|
221
|
+
}
|
|
81
222
|
};
|
|
82
223
|
const handleVerifyCode = async (e) => {
|
|
83
224
|
e.preventDefault();
|
|
@@ -85,7 +226,12 @@ const PhoneAuthForm = ({ onSubmit, onBack, loading, error, }) => {
|
|
|
85
226
|
};
|
|
86
227
|
return (jsxs("form", { className: "auth-form", onSubmit: codeSent ? handleVerifyCode : handleSendCode, children: [jsxs("div", { className: "auth-form-header", children: [jsx("h2", { className: "auth-form-title", children: "Phone Authentication" }), jsx("p", { className: "auth-form-subtitle", children: codeSent
|
|
87
228
|
? 'Enter the verification code sent to your phone.'
|
|
88
|
-
: 'Enter your phone number to receive a verification code.' })] }), 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] })), !codeSent ? (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "phoneNumber", className: "auth-label", children: "Phone Number" }), jsx(
|
|
229
|
+
: 'Enter your phone number to receive a verification code.' })] }), 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] })), !codeSent ? (jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "phoneNumber", className: "auth-label", children: "Phone Number" }), jsx(PhoneInput, { value: phoneNumber, onChange: setPhoneNumber, disabled: loading })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "verificationCode", className: "auth-label", children: "Verification Code" }), jsx(OTPInput, { length: 6, value: verificationCode, onChange: setVerificationCode, disabled: loading })] }), jsx("div", { style: { marginTop: '0.5rem', textAlign: 'center' }, children: jsx("button", { type: "button", className: "auth-link", onClick: handleResendCode, disabled: loading || resendCooldown > 0, style: {
|
|
230
|
+
cursor: resendCooldown > 0 ? 'not-allowed' : 'pointer',
|
|
231
|
+
opacity: resendCooldown > 0 ? 0.5 : 1,
|
|
232
|
+
}, children: resendCooldown > 0
|
|
233
|
+
? `Resend code in ${resendCooldown}s`
|
|
234
|
+
: 'Resend code' }) })] })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsx("span", { className: "auth-spinner" })) : codeSent ? ('Verify Code') : ('Send Code') }), jsx("div", { className: "auth-divider", children: jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onBack, disabled: loading, children: "\u2190 Back to login" }) })] }));
|
|
89
235
|
};
|
|
90
236
|
|
|
91
237
|
const PasswordResetForm = ({ onSubmit, onBack, loading, error, success, token, }) => {
|
|
@@ -123,6 +269,15 @@ const PasswordResetForm = ({ onSubmit, onBack, loading, error, success, token, }
|
|
|
123
269
|
: "Enter your email address and we'll send you instructions to reset your password." })] }), (error || passwordError) && (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 || passwordError] })), token ? (jsxs(Fragment, { children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "password", className: "auth-label", children: "New Password" }), 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 })] }), jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "confirmPassword", className: "auth-label", children: "Confirm Password" }), 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 })] })] })) : (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: email, onChange: (e) => setEmail(e.target.value), required: true, disabled: loading, placeholder: "you@example.com", autoComplete: "email" })] })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading, children: loading ? (jsx("span", { className: "auth-spinner" })) : token ? ('Reset password') : ('Send reset instructions') }), jsx("div", { className: "auth-divider", children: jsx("button", { type: "button", className: "auth-link auth-link-bold", onClick: onBack, disabled: loading, children: "\u2190 Back to Sign in" }) })] }));
|
|
124
270
|
};
|
|
125
271
|
|
|
272
|
+
const MagicLinkForm = ({ onSubmit, onCancel, loading = false, error, }) => {
|
|
273
|
+
const [email, setEmail] = useState('');
|
|
274
|
+
const handleSubmit = async (e) => {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
await onSubmit(email);
|
|
277
|
+
};
|
|
278
|
+
return (jsxs("form", { onSubmit: handleSubmit, className: "auth-form", children: [jsxs("div", { className: "auth-form-group", children: [jsx("label", { htmlFor: "magic-link-email", className: "auth-label", children: "Email Address" }), jsx("input", { id: "magic-link-email", type: "email", value: email, onChange: (e) => setEmail(e.target.value), className: "auth-input", placeholder: "you@example.com", required: true, disabled: loading })] }), error && (jsx("div", { className: "auth-error-message", children: error })), jsx("button", { type: "submit", className: "auth-button auth-button-primary", disabled: loading || !email, children: loading ? (jsxs(Fragment, { children: [jsx("span", { className: "auth-spinner" }), "Sending..."] })) : ('Send Magic Link') }), jsx("button", { type: "button", onClick: onCancel, className: "auth-button auth-button-secondary", disabled: loading, children: "Cancel" })] }));
|
|
279
|
+
};
|
|
280
|
+
|
|
126
281
|
/**
|
|
127
282
|
* AuthAPI - Thin wrapper around Smartlinks SDK authKit namespace
|
|
128
283
|
* All authentication operations now use the global Smartlinks SDK
|
|
@@ -151,8 +306,8 @@ class AuthAPI {
|
|
|
151
306
|
async sendPhoneCode(phoneNumber) {
|
|
152
307
|
return smartlinks.authKit.sendPhoneCode(this.clientId, phoneNumber);
|
|
153
308
|
}
|
|
154
|
-
async verifyPhoneCode(
|
|
155
|
-
return smartlinks.authKit.verifyPhoneCode(this.clientId,
|
|
309
|
+
async verifyPhoneCode(phoneNumber, code) {
|
|
310
|
+
return smartlinks.authKit.verifyPhoneCode(this.clientId, phoneNumber, code);
|
|
156
311
|
}
|
|
157
312
|
async requestPasswordReset(email, redirectUrl) {
|
|
158
313
|
return smartlinks.authKit.requestPasswordReset(this.clientId, {
|
|
@@ -205,6 +360,15 @@ class AuthAPI {
|
|
|
205
360
|
};
|
|
206
361
|
}
|
|
207
362
|
}
|
|
363
|
+
async sendMagicLink(email, redirectUrl) {
|
|
364
|
+
return smartlinks.authKit.sendMagicLink(this.clientId, {
|
|
365
|
+
email,
|
|
366
|
+
redirectUrl
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
async verifyMagicLink(token) {
|
|
370
|
+
return smartlinks.authKit.verifyMagicLink(this.clientId, token);
|
|
371
|
+
}
|
|
208
372
|
}
|
|
209
373
|
|
|
210
374
|
const TOKEN_KEY = 'smartlinks_auth_token';
|
|
@@ -294,9 +458,10 @@ const AuthProvider = ({ children }) => {
|
|
|
294
458
|
setToken(storedToken.token);
|
|
295
459
|
setUser(storedUser);
|
|
296
460
|
setAccountData(storedAccountData);
|
|
297
|
-
// Set bearer token in global Smartlinks SDK
|
|
298
|
-
|
|
299
|
-
|
|
461
|
+
// Set bearer token in global Smartlinks SDK via auth.verifyToken
|
|
462
|
+
smartlinks.auth.verifyToken(storedToken.token).catch(err => {
|
|
463
|
+
console.warn('Failed to restore bearer token on init:', err);
|
|
464
|
+
});
|
|
300
465
|
}
|
|
301
466
|
setIsLoading(false);
|
|
302
467
|
}, []);
|
|
@@ -310,9 +475,11 @@ const AuthProvider = ({ children }) => {
|
|
|
310
475
|
setToken(authToken);
|
|
311
476
|
setUser(authUser);
|
|
312
477
|
setAccountData(authAccountData || null);
|
|
313
|
-
// Set bearer token in global Smartlinks SDK
|
|
314
|
-
//
|
|
315
|
-
smartlinks.
|
|
478
|
+
// Set bearer token in global Smartlinks SDK via auth.verifyToken
|
|
479
|
+
// This both validates the token and sets it for future API calls
|
|
480
|
+
smartlinks.auth.verifyToken(authToken).catch(err => {
|
|
481
|
+
console.warn('Failed to set bearer token on login:', err);
|
|
482
|
+
});
|
|
316
483
|
}, []);
|
|
317
484
|
const logout = useCallback(async () => {
|
|
318
485
|
// Clear local storage
|
|
@@ -321,8 +488,7 @@ const AuthProvider = ({ children }) => {
|
|
|
321
488
|
setUser(null);
|
|
322
489
|
setAccountData(null);
|
|
323
490
|
// Clear bearer token from global Smartlinks SDK
|
|
324
|
-
|
|
325
|
-
smartlinks.setBearerToken(undefined);
|
|
491
|
+
smartlinks.auth.logout();
|
|
326
492
|
}, []);
|
|
327
493
|
const getToken = useCallback(() => {
|
|
328
494
|
const storedToken = tokenStorage.getToken();
|
|
@@ -352,6 +518,269 @@ const useAuth = () => {
|
|
|
352
518
|
return context;
|
|
353
519
|
};
|
|
354
520
|
|
|
521
|
+
const AccountManagement = ({ apiEndpoint, clientId, onProfileUpdated, onEmailChangeRequested, onPasswordChanged, onAccountDeleted, onError, theme = 'light', className = '', customization = {}, }) => {
|
|
522
|
+
const auth = useAuth();
|
|
523
|
+
const [loading, setLoading] = useState(false);
|
|
524
|
+
const [profile, setProfile] = useState(null);
|
|
525
|
+
const [error, setError] = useState();
|
|
526
|
+
const [success, setSuccess] = useState();
|
|
527
|
+
// Profile form state
|
|
528
|
+
const [displayName, setDisplayName] = useState('');
|
|
529
|
+
// Email change state
|
|
530
|
+
const [newEmail, setNewEmail] = useState('');
|
|
531
|
+
const [emailPassword, setEmailPassword] = useState('');
|
|
532
|
+
// Password change state
|
|
533
|
+
const [currentPassword, setCurrentPassword] = useState('');
|
|
534
|
+
const [newPassword, setNewPassword] = useState('');
|
|
535
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
536
|
+
// Phone change state (reuses existing sendPhoneCode flow)
|
|
537
|
+
const [newPhone, setNewPhone] = useState('');
|
|
538
|
+
const [phoneCode, setPhoneCode] = useState('');
|
|
539
|
+
const [phoneCodeSent, setPhoneCodeSent] = useState(false);
|
|
540
|
+
// Account deletion state
|
|
541
|
+
const [deletePassword, setDeletePassword] = useState('');
|
|
542
|
+
const [deleteConfirmText, setDeleteConfirmText] = useState('');
|
|
543
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
544
|
+
const { showProfileSection = true, showEmailSection = true, showPasswordSection = true, showPhoneSection = true, showDeleteAccount = true, } = customization;
|
|
545
|
+
// Reinitialize Smartlinks SDK when apiEndpoint changes
|
|
546
|
+
useEffect(() => {
|
|
547
|
+
if (apiEndpoint) {
|
|
548
|
+
smartlinks.initializeApi({
|
|
549
|
+
baseURL: apiEndpoint,
|
|
550
|
+
proxyMode: false,
|
|
551
|
+
ngrokSkipBrowserWarning: true,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}, [apiEndpoint]);
|
|
555
|
+
// Load user profile on mount
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
loadProfile();
|
|
558
|
+
}, [clientId]);
|
|
559
|
+
const loadProfile = async () => {
|
|
560
|
+
if (!auth.isAuthenticated) {
|
|
561
|
+
setError('You must be logged in to manage your account');
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
setLoading(true);
|
|
565
|
+
setError(undefined);
|
|
566
|
+
try {
|
|
567
|
+
// TODO: Backend implementation required
|
|
568
|
+
// Endpoint: GET /api/v1/authkit/:clientId/account/profile
|
|
569
|
+
// SDK method: smartlinks.authKit.getProfile(clientId)
|
|
570
|
+
// Temporary mock data for UI testing
|
|
571
|
+
const profileData = {
|
|
572
|
+
uid: auth.user?.uid || '',
|
|
573
|
+
email: auth.user?.email,
|
|
574
|
+
displayName: auth.user?.displayName,
|
|
575
|
+
phoneNumber: auth.user?.phoneNumber,
|
|
576
|
+
photoURL: auth.user?.photoURL,
|
|
577
|
+
emailVerified: true,
|
|
578
|
+
accountData: auth.accountData || {},
|
|
579
|
+
};
|
|
580
|
+
setProfile(profileData);
|
|
581
|
+
setDisplayName(profileData.displayName || '');
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to load profile';
|
|
585
|
+
setError(errorMessage);
|
|
586
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
587
|
+
}
|
|
588
|
+
finally {
|
|
589
|
+
setLoading(false);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
const handleUpdateProfile = async (e) => {
|
|
593
|
+
e.preventDefault();
|
|
594
|
+
setLoading(true);
|
|
595
|
+
setError(undefined);
|
|
596
|
+
setSuccess(undefined);
|
|
597
|
+
try {
|
|
598
|
+
// TODO: Backend implementation required
|
|
599
|
+
// Endpoint: POST /api/v1/authkit/:clientId/account/update-profile
|
|
600
|
+
// SDK method: smartlinks.authKit.updateProfile(clientId, updateData)
|
|
601
|
+
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
602
|
+
console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-profile');
|
|
603
|
+
console.log('Update data:', { displayName });
|
|
604
|
+
// Uncomment when backend is ready:
|
|
605
|
+
// const updateData: ProfileUpdateData = {
|
|
606
|
+
// displayName: displayName || undefined,
|
|
607
|
+
// photoURL: photoURL || undefined,
|
|
608
|
+
// };
|
|
609
|
+
// const updatedProfile = await smartlinks.authKit.updateProfile(clientId, updateData);
|
|
610
|
+
// setProfile(updatedProfile);
|
|
611
|
+
// setSuccess('Profile updated successfully!');
|
|
612
|
+
// onProfileUpdated?.(updatedProfile);
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to update profile';
|
|
616
|
+
setError(errorMessage);
|
|
617
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
618
|
+
}
|
|
619
|
+
finally {
|
|
620
|
+
setLoading(false);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
const handleChangeEmail = async (e) => {
|
|
624
|
+
e.preventDefault();
|
|
625
|
+
setLoading(true);
|
|
626
|
+
setError(undefined);
|
|
627
|
+
setSuccess(undefined);
|
|
628
|
+
try {
|
|
629
|
+
// TODO: Backend implementation required
|
|
630
|
+
// Endpoint: POST /api/v1/authkit/:clientId/account/change-email
|
|
631
|
+
// SDK method: smartlinks.authKit.changeEmail(clientId, newEmail, password)
|
|
632
|
+
// Note: No verification flow for now - direct email update
|
|
633
|
+
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
634
|
+
console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-email');
|
|
635
|
+
console.log('Data:', { newEmail });
|
|
636
|
+
// Uncomment when backend is ready:
|
|
637
|
+
// await smartlinks.authKit.changeEmail(clientId, newEmail, emailPassword);
|
|
638
|
+
// setSuccess('Email changed successfully!');
|
|
639
|
+
// setNewEmail('');
|
|
640
|
+
// setEmailPassword('');
|
|
641
|
+
// onEmailChangeRequested?.();
|
|
642
|
+
// await loadProfile(); // Reload to show new email
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to change email';
|
|
646
|
+
setError(errorMessage);
|
|
647
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
648
|
+
}
|
|
649
|
+
finally {
|
|
650
|
+
setLoading(false);
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
const handleChangePassword = async (e) => {
|
|
654
|
+
e.preventDefault();
|
|
655
|
+
if (newPassword !== confirmPassword) {
|
|
656
|
+
setError('New passwords do not match');
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (newPassword.length < 6) {
|
|
660
|
+
setError('Password must be at least 6 characters');
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
setLoading(true);
|
|
664
|
+
setError(undefined);
|
|
665
|
+
setSuccess(undefined);
|
|
666
|
+
try {
|
|
667
|
+
// TODO: Backend implementation required
|
|
668
|
+
// Endpoint: POST /api/v1/authkit/:clientId/account/change-password
|
|
669
|
+
// SDK method: smartlinks.authKit.changePassword(clientId, currentPassword, newPassword)
|
|
670
|
+
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
671
|
+
console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/change-password');
|
|
672
|
+
console.log('Data: currentPassword and newPassword provided');
|
|
673
|
+
// Uncomment when backend is ready:
|
|
674
|
+
// await smartlinks.authKit.changePassword(clientId, currentPassword, newPassword);
|
|
675
|
+
// setSuccess('Password changed successfully!');
|
|
676
|
+
// setCurrentPassword('');
|
|
677
|
+
// setNewPassword('');
|
|
678
|
+
// setConfirmPassword('');
|
|
679
|
+
// onPasswordChanged?.();
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to change password';
|
|
683
|
+
setError(errorMessage);
|
|
684
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
685
|
+
}
|
|
686
|
+
finally {
|
|
687
|
+
setLoading(false);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const handleSendPhoneCode = async () => {
|
|
691
|
+
setLoading(true);
|
|
692
|
+
setError(undefined);
|
|
693
|
+
try {
|
|
694
|
+
await smartlinks.authKit.sendPhoneCode(clientId, newPhone);
|
|
695
|
+
setPhoneCodeSent(true);
|
|
696
|
+
setSuccess('Verification code sent to your phone');
|
|
697
|
+
}
|
|
698
|
+
catch (err) {
|
|
699
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to send verification code';
|
|
700
|
+
setError(errorMessage);
|
|
701
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
702
|
+
}
|
|
703
|
+
finally {
|
|
704
|
+
setLoading(false);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
const handleUpdatePhone = async (e) => {
|
|
708
|
+
e.preventDefault();
|
|
709
|
+
setLoading(true);
|
|
710
|
+
setError(undefined);
|
|
711
|
+
setSuccess(undefined);
|
|
712
|
+
try {
|
|
713
|
+
// TODO: Backend implementation required
|
|
714
|
+
// Endpoint: POST /api/v1/authkit/:clientId/account/update-phone
|
|
715
|
+
// SDK method: smartlinks.authKit.updatePhone(clientId, phoneNumber, verificationCode)
|
|
716
|
+
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
717
|
+
console.log('Required API endpoint: POST /api/v1/authkit/:clientId/account/update-phone');
|
|
718
|
+
console.log('Data:', { phoneNumber: newPhone, code: phoneCode });
|
|
719
|
+
// Uncomment when backend is ready:
|
|
720
|
+
// await smartlinks.authKit.updatePhone(clientId, newPhone, phoneCode);
|
|
721
|
+
// setSuccess('Phone number updated successfully!');
|
|
722
|
+
// setNewPhone('');
|
|
723
|
+
// setPhoneCode('');
|
|
724
|
+
// setPhoneCodeSent(false);
|
|
725
|
+
// await loadProfile();
|
|
726
|
+
}
|
|
727
|
+
catch (err) {
|
|
728
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to update phone number';
|
|
729
|
+
setError(errorMessage);
|
|
730
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
731
|
+
}
|
|
732
|
+
finally {
|
|
733
|
+
setLoading(false);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
const handleDeleteAccount = async () => {
|
|
737
|
+
if (deleteConfirmText !== 'DELETE') {
|
|
738
|
+
setError('Please type DELETE to confirm account deletion');
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (!deletePassword) {
|
|
742
|
+
setError('Password is required');
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
setLoading(true);
|
|
746
|
+
setError(undefined);
|
|
747
|
+
try {
|
|
748
|
+
// TODO: Backend implementation required
|
|
749
|
+
// Endpoint: DELETE /api/v1/authkit/:clientId/account/delete
|
|
750
|
+
// SDK method: smartlinks.authKit.deleteAccount(clientId, password, confirmText)
|
|
751
|
+
// Note: This performs a SOFT DELETE (marks as deleted, obfuscates email)
|
|
752
|
+
setError('Backend API not yet implemented. See console for required endpoint.');
|
|
753
|
+
console.log('Required API endpoint: DELETE /api/v1/authkit/:clientId/account/delete');
|
|
754
|
+
console.log('Data: password and confirmText="DELETE" provided');
|
|
755
|
+
console.log('Note: Backend should soft delete (mark deleted, obfuscate email, disable account)');
|
|
756
|
+
// Uncomment when backend is ready:
|
|
757
|
+
// await smartlinks.authKit.deleteAccount(clientId, deletePassword, deleteConfirmText);
|
|
758
|
+
// setSuccess('Account deleted successfully');
|
|
759
|
+
// onAccountDeleted?.();
|
|
760
|
+
// await auth.logout();
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to delete account';
|
|
764
|
+
setError(errorMessage);
|
|
765
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
766
|
+
}
|
|
767
|
+
finally {
|
|
768
|
+
setLoading(false);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
if (!auth.isAuthenticated) {
|
|
772
|
+
return (jsx(AuthContainer, { theme: theme, className: className, children: jsxs("div", { className: "auth-form", children: [jsx("h2", { className: "auth-title", children: "Account Management" }), jsx("p", { className: "text-muted-foreground", children: "Please log in to manage your account" })] }) }));
|
|
773
|
+
}
|
|
774
|
+
if (loading && !profile) {
|
|
775
|
+
return (jsx(AuthContainer, { theme: theme, className: className, children: jsx("div", { className: "auth-form", children: jsx("h2", { className: "auth-title", children: "Loading..." }) }) }));
|
|
776
|
+
}
|
|
777
|
+
return (jsx(AuthContainer, { theme: theme, className: className, children: jsxs("div", { className: "auth-form", style: { maxWidth: '600px' }, children: [jsx("h2", { className: "auth-title", children: "Account Management" }), 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: [jsx("h3", { className: "section-title", children: "Profile Information" }), jsxs("form", { onSubmit: handleUpdateProfile, className: "auth-form-fields", children: [jsxs("div", { className: "form-group", children: [jsx("label", { htmlFor: "displayName", children: "Display Name" }), jsx("input", { id: "displayName", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "Your display name", className: "auth-input" })] }), jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Updating...' : 'Update Profile' })] })] })), showEmailSection && (jsxs("section", { className: "account-section", children: [jsx("h3", { className: "section-title", children: "Email Management" }), jsxs("div", { className: "current-info", children: [jsxs("p", { children: [jsx("strong", { children: "Current Email:" }), " ", profile?.email || 'Not set'] }), jsxs("p", { children: [jsx("strong", { children: "Status:" }), ' ', jsx("span", { className: profile?.emailVerified ? 'text-success' : 'text-warning', children: profile?.emailVerified ? 'Verified' : 'Unverified' })] })] }), jsxs("form", { onSubmit: handleChangeEmail, className: "auth-form-fields", 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 })] }), jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Sending...' : 'Change Email' })] })] })), showPasswordSection && (jsxs("section", { className: "account-section", children: [jsx("h3", { className: "section-title", children: "Password Management" }), jsxs("form", { onSubmit: handleChangePassword, className: "auth-form-fields", 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 })] }), jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Changing...' : 'Change Password' })] })] })), showPhoneSection && (jsxs("section", { className: "account-section", children: [jsx("h3", { className: "section-title", children: "Phone Number" }), jsx("div", { className: "current-info", children: jsxs("p", { children: [jsx("strong", { children: "Current Phone:" }), " ", profile?.phoneNumber || 'Not set'] }) }), jsxs("form", { onSubmit: handleUpdatePhone, className: "auth-form-fields", 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 ? (jsx("button", { type: "button", onClick: handleSendPhoneCode, className: "auth-button", disabled: loading || !newPhone, children: loading ? 'Sending...' : 'Send Verification Code' })) : (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 })] }), jsx("button", { type: "submit", className: "auth-button", disabled: loading, children: loading ? 'Verifying...' : 'Verify & Update Phone' })] }))] })] })), 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: () => {
|
|
778
|
+
setShowDeleteConfirm(false);
|
|
779
|
+
setDeletePassword('');
|
|
780
|
+
setDeleteConfirmText('');
|
|
781
|
+
}, className: "auth-button button-secondary", children: "Cancel" })] })] }))] }))] }) }));
|
|
782
|
+
};
|
|
783
|
+
|
|
355
784
|
const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
|
|
356
785
|
const { isAuthenticated, isLoading } = useAuth();
|
|
357
786
|
// Show loading state
|
|
@@ -370,22 +799,46 @@ const ProtectedRoute = ({ children, fallback, redirectTo, }) => {
|
|
|
370
799
|
return jsx(Fragment, { children: children });
|
|
371
800
|
};
|
|
372
801
|
|
|
373
|
-
const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', 'phone'], theme = 'light', className, }) => {
|
|
802
|
+
const AuthUIPreview = ({ customization, enabledProviders = ['email', 'google', 'phone'], providerOrder, emailDisplayMode = 'form', theme = 'light', className, }) => {
|
|
374
803
|
const showEmail = enabledProviders.includes('email');
|
|
375
804
|
const showGoogle = enabledProviders.includes('google');
|
|
376
805
|
const showPhone = enabledProviders.includes('phone');
|
|
377
|
-
const
|
|
378
|
-
|
|
806
|
+
const showMagicLink = enabledProviders.includes('magic-link');
|
|
807
|
+
// Determine ordered providers (excluding email if in button mode)
|
|
808
|
+
const orderedProviders = providerOrder && providerOrder.length > 0
|
|
809
|
+
? providerOrder.filter(p => enabledProviders.includes(p) && p !== 'email')
|
|
810
|
+
: enabledProviders.filter(p => p !== 'email');
|
|
811
|
+
const hasOtherProviders = showGoogle || showPhone || showMagicLink;
|
|
812
|
+
// Render provider button helper
|
|
813
|
+
const renderProviderButton = (provider) => {
|
|
814
|
+
if (provider === 'google' && showGoogle) {
|
|
815
|
+
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none", children: [jsx("path", { d: "M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z", fill: "#4285F4" }), jsx("path", { d: "M9 18c2.43 0 4.467-.806 5.956-2.183l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332C2.438 15.983 5.482 18 9 18z", fill: "#34A853" }), jsx("path", { d: "M3.964 10.707c-.18-.54-.282-1.117-.282-1.707 0-.593.102-1.167.282-1.707V4.961H.957C.347 6.175 0 7.548 0 9s.348 2.825.957 4.039l3.007-2.332z", fill: "#FBBC05" }), jsx("path", { d: "M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0 5.482 0 2.438 2.017.957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z", fill: "#EA4335" })] }), jsx("span", { children: "Continue with Google" })] }, "google"));
|
|
816
|
+
}
|
|
817
|
+
if (provider === 'phone' && showPhone) {
|
|
818
|
+
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" }) }), jsx("span", { children: "Continue with Phone" })] }, "phone"));
|
|
819
|
+
}
|
|
820
|
+
if (provider === 'magic-link' && showMagicLink) {
|
|
821
|
+
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Magic Link" })] }, "magic-link"));
|
|
822
|
+
}
|
|
823
|
+
if (provider === 'email' && showEmail && emailDisplayMode === 'button') {
|
|
824
|
+
return (jsxs("button", { className: "auth-provider-button", disabled: true, children: [jsx("svg", { width: "18", height: "18", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }), jsx("span", { children: "Continue with Email" })] }, "email"));
|
|
825
|
+
}
|
|
826
|
+
return null;
|
|
827
|
+
};
|
|
828
|
+
return (jsx(AuthContainer, { theme: theme, className: className, config: customization, children: emailDisplayMode === 'button' ? (jsx("div", { className: "auth-provider-buttons", children: orderedProviders.concat(showEmail ? ['email'] : []).map(provider => renderProviderButton(provider)) })) : (
|
|
829
|
+
/* Form mode: show email form first, then other providers */
|
|
830
|
+
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)) })] }))] })) }));
|
|
379
831
|
};
|
|
380
832
|
|
|
381
|
-
|
|
382
|
-
|
|
833
|
+
// Default Smartlinks Google OAuth Client ID (public - safe to expose)
|
|
834
|
+
const DEFAULT_GOOGLE_CLIENT_ID = '696509063554-jdlbjl8vsjt7cr0vgkjkjf3ffnvi3a70.apps.googleusercontent.com';
|
|
835
|
+
const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAuthSuccess, onAuthError, enabledProviders = ['email', 'google', 'phone'], initialMode = 'login', redirectUrl, theme = 'light', className, customization, skipConfigFetch = false, }) => {
|
|
836
|
+
const [mode, setMode] = useState(initialMode);
|
|
383
837
|
const [loading, setLoading] = useState(false);
|
|
384
838
|
const [error, setError] = useState();
|
|
385
839
|
const [resetSuccess, setResetSuccess] = useState(false);
|
|
386
840
|
const [authSuccess, setAuthSuccess] = useState(false);
|
|
387
841
|
const [successMessage, setSuccessMessage] = useState();
|
|
388
|
-
const [verificationId, setVerificationId] = useState();
|
|
389
842
|
const [showResendVerification, setShowResendVerification] = useState(false);
|
|
390
843
|
const [resendEmail, setResendEmail] = useState();
|
|
391
844
|
const [showRequestNewReset, setShowRequestNewReset] = useState(false);
|
|
@@ -393,8 +846,28 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
393
846
|
const [resetToken, setResetToken] = useState(); // Store the reset token from URL
|
|
394
847
|
const [config, setConfig] = useState(null);
|
|
395
848
|
const [configLoading, setConfigLoading] = useState(!skipConfigFetch);
|
|
849
|
+
const [showEmailForm, setShowEmailForm] = useState(false); // Track if email form should be shown when emailDisplayMode is 'button'
|
|
396
850
|
const api = new AuthAPI(apiEndpoint, clientId, clientName);
|
|
397
851
|
const auth = useAuth();
|
|
852
|
+
// Reinitialize Smartlinks SDK when apiEndpoint changes (for test/dev scenarios)
|
|
853
|
+
// IMPORTANT: Preserve bearer token during reinitialization
|
|
854
|
+
useEffect(() => {
|
|
855
|
+
if (apiEndpoint) {
|
|
856
|
+
// Get current token before reinitializing
|
|
857
|
+
const currentToken = auth.getToken();
|
|
858
|
+
smartlinks.initializeApi({
|
|
859
|
+
baseURL: apiEndpoint,
|
|
860
|
+
proxyMode: false, // Direct API calls when custom endpoint is provided
|
|
861
|
+
ngrokSkipBrowserWarning: true,
|
|
862
|
+
});
|
|
863
|
+
// Restore bearer token after reinitialization using auth.verifyToken
|
|
864
|
+
if (currentToken) {
|
|
865
|
+
smartlinks.auth.verifyToken(currentToken).catch(err => {
|
|
866
|
+
console.warn('Failed to restore bearer token after reinit:', err);
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}, [apiEndpoint, auth]);
|
|
398
871
|
// Get the effective redirect URL (use prop or default to current page)
|
|
399
872
|
const getRedirectUrl = () => {
|
|
400
873
|
if (redirectUrl)
|
|
@@ -453,6 +926,12 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
453
926
|
};
|
|
454
927
|
fetchConfig();
|
|
455
928
|
}, [apiEndpoint, clientId, customization, skipConfigFetch]);
|
|
929
|
+
// Reset showEmailForm when mode changes away from login/register
|
|
930
|
+
useEffect(() => {
|
|
931
|
+
if (mode !== 'login' && mode !== 'register') {
|
|
932
|
+
setShowEmailForm(false);
|
|
933
|
+
}
|
|
934
|
+
}, [mode]);
|
|
456
935
|
// Handle URL-based auth flows (email verification, password reset)
|
|
457
936
|
useEffect(() => {
|
|
458
937
|
// Helper to get URL parameters from either hash or search
|
|
@@ -483,10 +962,10 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
483
962
|
if (urlMode === 'verifyEmail') {
|
|
484
963
|
console.log('Verifying email with token:', token);
|
|
485
964
|
const response = await api.verifyEmailWithToken(token);
|
|
486
|
-
// Get email verification mode from
|
|
487
|
-
const verificationMode = config?.emailVerification?.mode || 'verify-then-auto-login';
|
|
488
|
-
if (verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') {
|
|
489
|
-
// Auto-login modes: Log the user in immediately
|
|
965
|
+
// Get email verification mode from response or config
|
|
966
|
+
const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
|
|
967
|
+
if ((verificationMode === 'verify-then-auto-login' || verificationMode === 'immediate') && response.token) {
|
|
968
|
+
// Auto-login modes: Log the user in immediately if token is provided
|
|
490
969
|
auth.login(response.token, response.user, response.accountData);
|
|
491
970
|
setAuthSuccess(true);
|
|
492
971
|
setSuccessMessage('Email verified successfully! You are now logged in.');
|
|
@@ -502,7 +981,7 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
502
981
|
}
|
|
503
982
|
}
|
|
504
983
|
else {
|
|
505
|
-
// verify-then-manual-login mode: Show success but require manual login
|
|
984
|
+
// verify-then-manual-login mode or no token: Show success but require manual login
|
|
506
985
|
setAuthSuccess(true);
|
|
507
986
|
setSuccessMessage('Email verified successfully! Please log in with your credentials.');
|
|
508
987
|
// Clear the URL parameters
|
|
@@ -525,6 +1004,29 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
525
1004
|
const cleanUrl = window.location.href.split('?')[0];
|
|
526
1005
|
window.history.replaceState({}, document.title, cleanUrl);
|
|
527
1006
|
}
|
|
1007
|
+
else if (urlMode === 'magicLink') {
|
|
1008
|
+
console.log('Verifying magic link token:', token);
|
|
1009
|
+
const response = await api.verifyMagicLink(token);
|
|
1010
|
+
// Auto-login with magic link if token is provided
|
|
1011
|
+
if (response.token) {
|
|
1012
|
+
auth.login(response.token, response.user, response.accountData);
|
|
1013
|
+
setAuthSuccess(true);
|
|
1014
|
+
setSuccessMessage('Magic link verified! You are now logged in.');
|
|
1015
|
+
onAuthSuccess(response.token, response.user, response.accountData);
|
|
1016
|
+
// Clear the URL parameters
|
|
1017
|
+
const cleanUrl = window.location.href.split('?')[0];
|
|
1018
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
1019
|
+
// Redirect after a brief delay to show success message
|
|
1020
|
+
if (redirectUrl) {
|
|
1021
|
+
setTimeout(() => {
|
|
1022
|
+
window.location.href = redirectUrl;
|
|
1023
|
+
}, 2000);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
throw new Error('Authentication failed - no token received');
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
528
1030
|
}
|
|
529
1031
|
catch (err) {
|
|
530
1032
|
console.error('URL-based auth error:', err);
|
|
@@ -547,6 +1049,14 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
547
1049
|
const cleanUrl = window.location.href.split('?')[0];
|
|
548
1050
|
window.history.replaceState({}, document.title, cleanUrl);
|
|
549
1051
|
}
|
|
1052
|
+
else if (urlMode === 'magicLink') {
|
|
1053
|
+
// If magic link is invalid/expired
|
|
1054
|
+
setError(`${errorMessage} - Please request a new magic link below.`);
|
|
1055
|
+
setMode('magic-link');
|
|
1056
|
+
// Clear the URL parameters
|
|
1057
|
+
const cleanUrl = window.location.href.split('?')[0];
|
|
1058
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
1059
|
+
}
|
|
550
1060
|
else {
|
|
551
1061
|
setError(errorMessage);
|
|
552
1062
|
}
|
|
@@ -568,17 +1078,22 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
568
1078
|
accountData: mode === 'register' ? accountData : undefined,
|
|
569
1079
|
redirectUrl: getRedirectUrl(), // Include redirect URL for email verification
|
|
570
1080
|
});
|
|
571
|
-
// Get email verification mode from config (default: verify-then-auto-login)
|
|
572
|
-
const verificationMode = config?.emailVerification?.mode || 'verify-then-auto-login';
|
|
1081
|
+
// Get email verification mode from response or config (default: verify-then-auto-login)
|
|
1082
|
+
const verificationMode = response.emailVerificationMode || config?.emailVerification?.mode || 'verify-then-auto-login';
|
|
573
1083
|
const gracePeriodHours = config?.emailVerification?.gracePeriodHours || 24;
|
|
574
1084
|
if (mode === 'register') {
|
|
575
1085
|
// Handle different verification modes
|
|
576
|
-
if (verificationMode === 'immediate') {
|
|
577
|
-
// Immediate mode: Log in right away
|
|
1086
|
+
if (verificationMode === 'immediate' && response.token) {
|
|
1087
|
+
// Immediate mode: Log in right away if token is provided
|
|
578
1088
|
auth.login(response.token, response.user, response.accountData);
|
|
579
1089
|
setAuthSuccess(true);
|
|
580
|
-
|
|
581
|
-
|
|
1090
|
+
const deadline = response.emailVerificationDeadline
|
|
1091
|
+
? new Date(response.emailVerificationDeadline).toLocaleString()
|
|
1092
|
+
: `${gracePeriodHours} hours`;
|
|
1093
|
+
setSuccessMessage(`Account created! You're logged in now. Please verify your email by ${deadline} to keep your account active.`);
|
|
1094
|
+
if (response.token) {
|
|
1095
|
+
onAuthSuccess(response.token, response.user, response.accountData);
|
|
1096
|
+
}
|
|
582
1097
|
if (redirectUrl) {
|
|
583
1098
|
setTimeout(() => {
|
|
584
1099
|
window.location.href = redirectUrl;
|
|
@@ -597,15 +1112,27 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
597
1112
|
}
|
|
598
1113
|
}
|
|
599
1114
|
else {
|
|
600
|
-
// Login mode - always log in
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
1115
|
+
// Login mode - always log in if token is provided
|
|
1116
|
+
if (response.token) {
|
|
1117
|
+
// Check for account lock or verification requirements
|
|
1118
|
+
if (response.accountLocked) {
|
|
1119
|
+
throw new Error('Your account has been locked due to unverified email. Please check your email or request a new verification link.');
|
|
1120
|
+
}
|
|
1121
|
+
if (response.requiresEmailVerification) {
|
|
1122
|
+
throw new Error('Please verify your email before logging in. Check your inbox for the verification link.');
|
|
1123
|
+
}
|
|
1124
|
+
auth.login(response.token, response.user, response.accountData);
|
|
1125
|
+
setAuthSuccess(true);
|
|
1126
|
+
setSuccessMessage('Login successful!');
|
|
1127
|
+
onAuthSuccess(response.token, response.user, response.accountData);
|
|
1128
|
+
if (redirectUrl) {
|
|
1129
|
+
setTimeout(() => {
|
|
1130
|
+
window.location.href = redirectUrl;
|
|
1131
|
+
}, 2000);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
throw new Error('Authentication failed - please verify your email before logging in.');
|
|
609
1136
|
}
|
|
610
1137
|
}
|
|
611
1138
|
}
|
|
@@ -670,19 +1197,104 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
670
1197
|
}
|
|
671
1198
|
};
|
|
672
1199
|
const handleGoogleLogin = async () => {
|
|
1200
|
+
// Use custom client ID from config, or fall back to default Smartlinks client ID
|
|
1201
|
+
const googleClientId = config?.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
1202
|
+
// Determine OAuth flow: default to 'oneTap' for better UX, but allow 'popup' for iframe compatibility
|
|
1203
|
+
const oauthFlow = config?.googleOAuthFlow || 'oneTap';
|
|
673
1204
|
setLoading(true);
|
|
674
1205
|
setError(undefined);
|
|
675
1206
|
try {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1207
|
+
const google = window.google;
|
|
1208
|
+
if (!google) {
|
|
1209
|
+
throw new Error('Google Identity Services not loaded. Please check your internet connection.');
|
|
1210
|
+
}
|
|
1211
|
+
if (oauthFlow === 'popup') {
|
|
1212
|
+
// Use OAuth2 popup flow (works in iframes but requires popup permission)
|
|
1213
|
+
if (!google.accounts.oauth2) {
|
|
1214
|
+
throw new Error('Google OAuth2 not available');
|
|
1215
|
+
}
|
|
1216
|
+
const client = google.accounts.oauth2.initTokenClient({
|
|
1217
|
+
client_id: googleClientId,
|
|
1218
|
+
scope: 'openid email profile',
|
|
1219
|
+
callback: async (response) => {
|
|
1220
|
+
try {
|
|
1221
|
+
if (response.error) {
|
|
1222
|
+
throw new Error(response.error_description || response.error);
|
|
1223
|
+
}
|
|
1224
|
+
const accessToken = response.access_token;
|
|
1225
|
+
// Send access token to backend
|
|
1226
|
+
const authResponse = await api.loginWithGoogle(accessToken);
|
|
1227
|
+
if (authResponse.token) {
|
|
1228
|
+
auth.login(authResponse.token, authResponse.user, authResponse.accountData);
|
|
1229
|
+
setAuthSuccess(true);
|
|
1230
|
+
setSuccessMessage('Google login successful!');
|
|
1231
|
+
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
throw new Error('Authentication failed - no token received');
|
|
1235
|
+
}
|
|
1236
|
+
if (redirectUrl) {
|
|
1237
|
+
setTimeout(() => {
|
|
1238
|
+
window.location.href = redirectUrl;
|
|
1239
|
+
}, 2000);
|
|
1240
|
+
}
|
|
1241
|
+
setLoading(false);
|
|
1242
|
+
}
|
|
1243
|
+
catch (err) {
|
|
1244
|
+
const errorMessage = err instanceof Error ? err.message : 'Google login failed';
|
|
1245
|
+
setError(errorMessage);
|
|
1246
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
1247
|
+
setLoading(false);
|
|
1248
|
+
}
|
|
1249
|
+
},
|
|
1250
|
+
});
|
|
1251
|
+
client.requestAccessToken();
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
// Use One Tap / Sign-In button flow (smoother UX but doesn't work in iframes)
|
|
1255
|
+
google.accounts.id.initialize({
|
|
1256
|
+
client_id: googleClientId,
|
|
1257
|
+
callback: async (response) => {
|
|
1258
|
+
try {
|
|
1259
|
+
const idToken = response.credential;
|
|
1260
|
+
const authResponse = await api.loginWithGoogle(idToken);
|
|
1261
|
+
if (authResponse.token) {
|
|
1262
|
+
auth.login(authResponse.token, authResponse.user, authResponse.accountData);
|
|
1263
|
+
setAuthSuccess(true);
|
|
1264
|
+
setSuccessMessage('Google login successful!');
|
|
1265
|
+
onAuthSuccess(authResponse.token, authResponse.user, authResponse.accountData);
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
throw new Error('Authentication failed - no token received');
|
|
1269
|
+
}
|
|
1270
|
+
if (redirectUrl) {
|
|
1271
|
+
setTimeout(() => {
|
|
1272
|
+
window.location.href = redirectUrl;
|
|
1273
|
+
}, 2000);
|
|
1274
|
+
}
|
|
1275
|
+
setLoading(false);
|
|
1276
|
+
}
|
|
1277
|
+
catch (err) {
|
|
1278
|
+
const errorMessage = err instanceof Error ? err.message : 'Google login failed';
|
|
1279
|
+
setError(errorMessage);
|
|
1280
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
1281
|
+
setLoading(false);
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
auto_select: false,
|
|
1285
|
+
cancel_on_tap_outside: true,
|
|
1286
|
+
});
|
|
1287
|
+
google.accounts.id.prompt((notification) => {
|
|
1288
|
+
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
|
1289
|
+
setLoading(false);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
679
1293
|
}
|
|
680
1294
|
catch (err) {
|
|
681
1295
|
const errorMessage = err instanceof Error ? err.message : 'Google login failed';
|
|
682
1296
|
setError(errorMessage);
|
|
683
1297
|
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
684
|
-
}
|
|
685
|
-
finally {
|
|
686
1298
|
setLoading(false);
|
|
687
1299
|
}
|
|
688
1300
|
};
|
|
@@ -691,21 +1303,24 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
691
1303
|
setError(undefined);
|
|
692
1304
|
try {
|
|
693
1305
|
if (!verificationCode) {
|
|
694
|
-
// Send verification code
|
|
695
|
-
|
|
696
|
-
|
|
1306
|
+
// Send verification code via Twilio Verify Service
|
|
1307
|
+
await api.sendPhoneCode(phoneNumber);
|
|
1308
|
+
// Twilio Verify Service tracks the verification by phone number
|
|
1309
|
+
// No need to store verificationId
|
|
697
1310
|
}
|
|
698
1311
|
else {
|
|
699
|
-
// Verify code
|
|
700
|
-
|
|
701
|
-
|
|
1312
|
+
// Verify code - Twilio identifies the verification by phone number
|
|
1313
|
+
const response = await api.verifyPhoneCode(phoneNumber, verificationCode);
|
|
1314
|
+
// Update auth context with account data if token is provided
|
|
1315
|
+
if (response.token) {
|
|
1316
|
+
auth.login(response.token, response.user, response.accountData);
|
|
1317
|
+
onAuthSuccess(response.token, response.user, response.accountData);
|
|
1318
|
+
if (redirectUrl) {
|
|
1319
|
+
window.location.href = redirectUrl;
|
|
1320
|
+
}
|
|
702
1321
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
auth.login(response.token, response.user, response.accountData);
|
|
706
|
-
onAuthSuccess(response.token, response.user, response.accountData);
|
|
707
|
-
if (redirectUrl) {
|
|
708
|
-
window.location.href = redirectUrl;
|
|
1322
|
+
else {
|
|
1323
|
+
throw new Error('Authentication failed - no token received');
|
|
709
1324
|
}
|
|
710
1325
|
}
|
|
711
1326
|
}
|
|
@@ -743,6 +1358,23 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
743
1358
|
setLoading(false);
|
|
744
1359
|
}
|
|
745
1360
|
};
|
|
1361
|
+
const handleMagicLink = async (email) => {
|
|
1362
|
+
setLoading(true);
|
|
1363
|
+
setError(undefined);
|
|
1364
|
+
try {
|
|
1365
|
+
await api.sendMagicLink(email, getRedirectUrl());
|
|
1366
|
+
setAuthSuccess(true);
|
|
1367
|
+
setSuccessMessage('Magic link sent! Check your email to log in.');
|
|
1368
|
+
}
|
|
1369
|
+
catch (err) {
|
|
1370
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to send magic link';
|
|
1371
|
+
setError(errorMessage);
|
|
1372
|
+
onAuthError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
1373
|
+
}
|
|
1374
|
+
finally {
|
|
1375
|
+
setLoading(false);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
746
1378
|
if (configLoading) {
|
|
747
1379
|
return (jsx(AuthContainer, { theme: theme, className: className, children: jsx("div", { style: { textAlign: 'center', padding: '2rem' }, children: jsx("div", { className: "auth-spinner" }) }) }));
|
|
748
1380
|
}
|
|
@@ -755,10 +1387,11 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
755
1387
|
fontSize: '1.5rem',
|
|
756
1388
|
fontWeight: 600
|
|
757
1389
|
}, children: successMessage?.includes('verified') ? 'Email Verified!' :
|
|
758
|
-
|
|
1390
|
+
successMessage?.includes('Magic link') ? 'Check Your Email!' :
|
|
1391
|
+
mode === 'register' ? 'Account Created!' : 'Login Successful!' }), jsx("p", { style: {
|
|
759
1392
|
color: '#6B7280',
|
|
760
1393
|
fontSize: '0.875rem'
|
|
761
|
-
}, children: successMessage })] })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
|
|
1394
|
+
}, children: successMessage })] })) : mode === 'magic-link' ? (jsx(MagicLinkForm, { onSubmit: handleMagicLink, onCancel: () => setMode('login'), loading: loading, error: error })) : mode === 'phone' ? (jsx(PhoneAuthForm, { onSubmit: handlePhoneAuth, onBack: () => setMode('login'), loading: loading, error: error })) : mode === 'reset-password' ? (jsx(PasswordResetForm, { onSubmit: handlePasswordReset, onBack: () => {
|
|
762
1395
|
setMode('login');
|
|
763
1396
|
setResetSuccess(false);
|
|
764
1397
|
setResetToken(undefined); // Clear token when going back
|
|
@@ -828,13 +1461,34 @@ const SmartlinksAuthUI = ({ apiEndpoint, clientId, clientName, accountData, onAu
|
|
|
828
1461
|
fontSize: '0.875rem',
|
|
829
1462
|
fontWeight: 500,
|
|
830
1463
|
opacity: loading ? 0.6 : 1
|
|
831
|
-
}, children: "Cancel" })] })] })) : (
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1464
|
+
}, children: "Cancel" })] })] })) : (jsx(Fragment, { children: (() => {
|
|
1465
|
+
const emailDisplayMode = config?.emailDisplayMode || 'form';
|
|
1466
|
+
const providerOrder = config?.providerOrder || (config?.enabledProviders || enabledProviders);
|
|
1467
|
+
const actualProviders = config?.enabledProviders || enabledProviders;
|
|
1468
|
+
// Button mode: show provider selection first, then email form if email is selected
|
|
1469
|
+
if (emailDisplayMode === 'button' && !showEmailForm) {
|
|
1470
|
+
return (jsx(ProviderButtons, { enabledProviders: actualProviders, providerOrder: providerOrder, onEmailLogin: () => setShowEmailForm(true), onGoogleLogin: handleGoogleLogin, onPhoneLogin: () => setMode('phone'), onMagicLinkLogin: () => setMode('magic-link'), loading: loading }));
|
|
1471
|
+
}
|
|
1472
|
+
// Form mode or email button was clicked: show email form with other providers
|
|
1473
|
+
return (jsxs(Fragment, { children: [emailDisplayMode === 'button' && showEmailForm && (jsx("button", { onClick: () => setShowEmailForm(false), style: {
|
|
1474
|
+
marginBottom: '1rem',
|
|
1475
|
+
padding: '0.5rem',
|
|
1476
|
+
background: 'none',
|
|
1477
|
+
border: 'none',
|
|
1478
|
+
color: 'var(--auth-text-color, #6B7280)',
|
|
1479
|
+
cursor: 'pointer',
|
|
1480
|
+
fontSize: '0.875rem',
|
|
1481
|
+
display: 'flex',
|
|
1482
|
+
alignItems: 'center',
|
|
1483
|
+
gap: '0.25rem'
|
|
1484
|
+
}, children: "\u2190 Back to options" })), jsx(EmailAuthForm, { mode: mode, onSubmit: handleEmailAuth, onModeSwitch: () => {
|
|
1485
|
+
setMode(mode === 'login' ? 'register' : 'login');
|
|
1486
|
+
setShowResendVerification(false);
|
|
1487
|
+
setShowRequestNewReset(false);
|
|
1488
|
+
setError(undefined);
|
|
1489
|
+
}, onForgotPassword: () => setMode('reset-password'), loading: loading, error: error }), 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 }))] }));
|
|
1490
|
+
})() })) })) : null }));
|
|
837
1491
|
};
|
|
838
1492
|
|
|
839
|
-
export { AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SmartlinksAuthUI, tokenStorage, useAuth };
|
|
1493
|
+
export { AccountManagement, AuthProvider, AuthUIPreview, SmartlinksAuthUI as FirebaseAuthUI, ProtectedRoute, SmartlinksAuthUI, tokenStorage, useAuth };
|
|
840
1494
|
//# sourceMappingURL=index.esm.js.map
|