@slyxup/ui 0.2.9 → 0.2.11

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.
@@ -23,14 +23,17 @@ export function SignIn({
23
23
  onForgotPasswordClick,
24
24
  }: SignInProps) {
25
25
  injectStyles();
26
- const { signIn, client } = useAuth() as unknown as {
26
+ const { signIn, completeSignIn, client } = useAuth() as unknown as {
27
27
  signIn: ReturnType<typeof useAuth>['signIn'];
28
+ completeSignIn: ReturnType<typeof useAuth>['completeSignIn'];
28
29
  client: { publishableKey?: string; apiUrl: string };
29
30
  };
30
31
  const [email, setEmail] = useState('');
31
32
  const [password, setPassword] = useState('');
32
33
  const [busy, setBusy] = useState(false);
33
34
  const [error, setError] = useState<string | null>(null);
35
+ const [challengeToken, setChallengeToken] = useState<string | null>(null);
36
+ const [tfaCode, setTfaCode] = useState('');
34
37
  const cardRef = useRef<HTMLDivElement>(null);
35
38
 
36
39
  useEffect(() => {
@@ -45,7 +48,11 @@ export function SignIn({
45
48
  setBusy(true);
46
49
  setError(null);
47
50
  try {
48
- await signIn({ email, password });
51
+ const res = await signIn({ email, password });
52
+ if (res && 'challengeToken' in res) {
53
+ setChallengeToken(res.challengeToken);
54
+ return;
55
+ }
49
56
  onSuccess?.();
50
57
  } catch (err) {
51
58
  setError(
@@ -58,6 +65,23 @@ export function SignIn({
58
65
  }
59
66
  }
60
67
 
68
+ async function onSubmit2FA(e: FormEvent) {
69
+ e.preventDefault();
70
+ if (!challengeToken) return;
71
+ setBusy(true);
72
+ setError(null);
73
+ try {
74
+ await completeSignIn({ challengeToken, code: tfaCode.trim() });
75
+ onSuccess?.();
76
+ } catch (err) {
77
+ setError(
78
+ err instanceof SlyxupError ? err.message : 'Invalid code. Try again.'
79
+ );
80
+ } finally {
81
+ setBusy(false);
82
+ }
83
+ }
84
+
61
85
  function oauth(provider: 'google' | 'github') {
62
86
  const redirect = encodeURIComponent(window.location.href);
63
87
  window.location.href = `${client.apiUrl}/v1/oauth/${provider}?redirect_url=${redirect}`;
@@ -113,53 +137,94 @@ export function SignIn({
113
137
  </p>
114
138
  )}
115
139
 
116
- <form onSubmit={onSubmit} noValidate={false}>
117
- <div className="slx-field">
118
- <label className="slx-label" htmlFor="slx-signin-email">
119
- Email
120
- </label>
121
- <input
122
- id="slx-signin-email"
123
- className="slx-input"
124
- type="email"
125
- autoComplete="email"
126
- placeholder="you@example.com"
127
- value={email}
128
- onChange={(e) => setEmail(e.target.value)}
129
- required
130
- />
131
- </div>
132
- <div className="slx-field">
133
- <div className="slx-row">
134
- <label className="slx-label" htmlFor="slx-signin-password">
135
- Password
140
+ {challengeToken ? (
141
+ <form onSubmit={onSubmit2FA} noValidate={false}>
142
+ <div className="slx-field">
143
+ <label className="slx-label" htmlFor="slx-signin-2fa">
144
+ Authenticator code
145
+ </label>
146
+ <input
147
+ id="slx-signin-2fa"
148
+ className="slx-input"
149
+ type="text"
150
+ inputMode="numeric"
151
+ autoComplete="one-time-code"
152
+ maxLength={6}
153
+ pattern="[0-9]*"
154
+ placeholder="000000"
155
+ value={tfaCode}
156
+ onChange={(e) => setTfaCode(e.target.value.replace(/\D/g, ''))}
157
+ required
158
+ />
159
+ <p className="slx-hint">
160
+ Enter the 6-digit code from your authenticator app.
161
+ </p>
162
+ </div>
163
+ <button className="slx-btn" type="submit" disabled={busy}>
164
+ {busy && <span className="slx-spinner" aria-hidden="true" />}
165
+ {busy ? 'Verifying…' : 'Verify code'}
166
+ </button>
167
+ <button
168
+ type="button"
169
+ className="slx-link"
170
+ style={{ marginTop: 8 }}
171
+ onClick={() => {
172
+ setChallengeToken(null);
173
+ setTfaCode('');
174
+ }}
175
+ >
176
+ ← Back to sign in
177
+ </button>
178
+ </form>
179
+ ) : (
180
+ <form onSubmit={onSubmit} noValidate={false}>
181
+ <div className="slx-field">
182
+ <label className="slx-label" htmlFor="slx-signin-email">
183
+ Email <span className="slx-hint">or username</span>
136
184
  </label>
137
- {onForgotPasswordClick && (
138
- <button
139
- type="button"
140
- className="slx-link slx-forgot"
141
- onClick={onForgotPasswordClick}
142
- >
143
- Forgot password?
144
- </button>
145
- )}
185
+ <input
186
+ id="slx-signin-email"
187
+ className="slx-input"
188
+ type="text"
189
+ autoComplete="username"
190
+ placeholder="you@example.com or yourname"
191
+ value={email}
192
+ onChange={(e) => setEmail(e.target.value)}
193
+ required
194
+ />
195
+ </div>
196
+ <div className="slx-field">
197
+ <div className="slx-row">
198
+ <label className="slx-label" htmlFor="slx-signin-password">
199
+ Password
200
+ </label>
201
+ {onForgotPasswordClick && (
202
+ <button
203
+ type="button"
204
+ className="slx-link slx-forgot"
205
+ onClick={onForgotPasswordClick}
206
+ >
207
+ Forgot password?
208
+ </button>
209
+ )}
210
+ </div>
211
+ <input
212
+ id="slx-signin-password"
213
+ className="slx-input"
214
+ type="password"
215
+ autoComplete="current-password"
216
+ placeholder="••••••••"
217
+ value={password}
218
+ onChange={(e) => setPassword(e.target.value)}
219
+ required
220
+ />
146
221
  </div>
147
- <input
148
- id="slx-signin-password"
149
- className="slx-input"
150
- type="password"
151
- autoComplete="current-password"
152
- placeholder="••••••••"
153
- value={password}
154
- onChange={(e) => setPassword(e.target.value)}
155
- required
156
- />
157
- </div>
158
- <button className="slx-btn" type="submit" disabled={busy}>
159
- {busy && <span className="slx-spinner" aria-hidden="true" />}
160
- {busy ? 'Signing in…' : 'Sign in'}
161
- </button>
162
- </form>
222
+ <button className="slx-btn" type="submit" disabled={busy}>
223
+ {busy && <span className="slx-spinner" aria-hidden="true" />}
224
+ {busy ? 'Signing in…' : 'Sign in'}
225
+ </button>
226
+ </form>
227
+ )}
163
228
 
164
229
  {onSignUpClick && (
165
230
  <p className="slx-footer">
@@ -22,10 +22,12 @@ export function SignUp({
22
22
  client: { publishableKey?: string; apiUrl: string };
23
23
  };
24
24
  const [firstName, setFirstName] = useState('');
25
+ const [username, setUsername] = useState('');
25
26
  const [email, setEmail] = useState('');
26
27
  const [password, setPassword] = useState('');
27
28
  const [busy, setBusy] = useState(false);
28
29
  const [error, setError] = useState<string | null>(null);
30
+ const [successEmail, setSuccessEmail] = useState<string | null>(null);
29
31
  const cardRef = useRef<HTMLDivElement>(null);
30
32
 
31
33
  useEffect(() => {
@@ -40,7 +42,13 @@ export function SignUp({
40
42
  setBusy(true);
41
43
  setError(null);
42
44
  try {
43
- await signUp({ email, password, firstName: firstName || undefined });
45
+ await signUp({
46
+ email,
47
+ password,
48
+ firstName: firstName || undefined,
49
+ username: username || undefined,
50
+ });
51
+ setSuccessEmail(email);
44
52
  onSuccess?.();
45
53
  } catch (err) {
46
54
  setError(
@@ -106,6 +114,18 @@ export function SignUp({
106
114
  </p>
107
115
  )}
108
116
 
117
+ {successEmail && (
118
+ <p
119
+ className="slx-success-text"
120
+ aria-live="polite"
121
+ style={{ color: 'var(--slx-success)' }}
122
+ >
123
+ Account created! We sent a verification link to{' '}
124
+ <strong>{successEmail}</strong>. Check your inbox to verify and sign
125
+ in.
126
+ </p>
127
+ )}
128
+
109
129
  <form onSubmit={onSubmit}>
110
130
  <div className="slx-field">
111
131
  <label className="slx-label" htmlFor="slx-signup-name">
@@ -121,6 +141,23 @@ export function SignUp({
121
141
  onChange={(e) => setFirstName(e.target.value)}
122
142
  />
123
143
  </div>
144
+ <div className="slx-field">
145
+ <label className="slx-label" htmlFor="slx-signup-username">
146
+ Username
147
+ </label>
148
+ <input
149
+ id="slx-signup-username"
150
+ className="slx-input"
151
+ type="text"
152
+ autoComplete="username"
153
+ placeholder="ada"
154
+ value={username}
155
+ onChange={(e) => setUsername(e.target.value)}
156
+ />
157
+ <p className="slx-hint">
158
+ Optional — lets you sign in with a username instead of your email.
159
+ </p>
160
+ </div>
124
161
  <div className="slx-field">
125
162
  <label className="slx-label" htmlFor="slx-signup-email">
126
163
  Email