@slyxup/ui 0.2.15 → 0.3.1
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/package.json +6 -3
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -70
- package/src/components/AdminPanel/AdminPanel.tsx +0 -1081
- package/src/components/BillingPortal/BillingPortal.tsx +0 -184
- package/src/components/EmailVerification/EmailVerification.tsx +0 -182
- package/src/components/ForgotPassword/ForgotPassword.tsx +0 -124
- package/src/components/PricingTable/PricingTable.tsx +0 -157
- package/src/components/ResetPassword/ResetPassword.tsx +0 -122
- package/src/components/SignIn/SignIn.tsx +0 -239
- package/src/components/SignUp/SignUp.tsx +0 -211
- package/src/components/SocialButtons/SocialButtons.tsx +0 -47
- package/src/components/UserButton/UserButton.tsx +0 -101
- package/src/components/UserProfile/UserProfile.tsx +0 -1776
- package/src/icons.tsx +0 -71
- package/src/index.ts +0 -56
- package/src/lib/paddle.ts +0 -131
- package/src/styles.ts +0 -619
- package/test/components.test.tsx +0 -135
- package/test/icons.test.tsx +0 -32
- package/test/styles.test.ts +0 -47
- package/tsconfig.json +0 -1
- package/vitest.config.ts +0 -2
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { type FormEvent, useEffect, useState } from 'react';
|
|
2
|
-
import { CheckIcon, KeyholeMark } from '../../icons';
|
|
3
|
-
import { injectStyles } from '../../styles';
|
|
4
|
-
|
|
5
|
-
export interface ResetPasswordProps {
|
|
6
|
-
/** Reset token (from email link ?token=...) */
|
|
7
|
-
token: string;
|
|
8
|
-
apiUrl?: string;
|
|
9
|
-
onSuccess?: () => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Set a new password using the emailed reset token. */
|
|
13
|
-
export function ResetPassword({
|
|
14
|
-
token,
|
|
15
|
-
apiUrl,
|
|
16
|
-
onSuccess,
|
|
17
|
-
}: ResetPasswordProps) {
|
|
18
|
-
injectStyles();
|
|
19
|
-
const [password, setPassword] = useState('');
|
|
20
|
-
const [done, setDone] = useState(false);
|
|
21
|
-
const [busy, setBusy] = useState(false);
|
|
22
|
-
const [error, setError] = useState<string | null>(null);
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (error) {
|
|
26
|
-
const t = setTimeout(() => setError(null), 4000);
|
|
27
|
-
return () => clearTimeout(t);
|
|
28
|
-
}
|
|
29
|
-
}, [error]);
|
|
30
|
-
|
|
31
|
-
async function onSubmit(e: FormEvent) {
|
|
32
|
-
e.preventDefault();
|
|
33
|
-
setBusy(true);
|
|
34
|
-
setError(null);
|
|
35
|
-
try {
|
|
36
|
-
const base = (
|
|
37
|
-
apiUrl ??
|
|
38
|
-
process.env.NEXT_PUBLIC_SLYXUP_API_URL ??
|
|
39
|
-
'https://auth.slyxup.online'
|
|
40
|
-
).replace(/\/$/, '');
|
|
41
|
-
const res = await fetch(`${base}/v1/verification/password/reset`, {
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: { 'Content-Type': 'application/json' },
|
|
44
|
-
body: JSON.stringify({ token, password }),
|
|
45
|
-
});
|
|
46
|
-
if (!res.ok) {
|
|
47
|
-
const data = await res
|
|
48
|
-
.json()
|
|
49
|
-
.catch(() => ({ error: 'Invalid or expired link' }));
|
|
50
|
-
throw new Error(data.error ?? 'Invalid or expired link');
|
|
51
|
-
}
|
|
52
|
-
setDone(true);
|
|
53
|
-
onSuccess?.();
|
|
54
|
-
} catch (err) {
|
|
55
|
-
setError(err instanceof Error ? err.message : 'Something went wrong.');
|
|
56
|
-
} finally {
|
|
57
|
-
setBusy(false);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (done) {
|
|
62
|
-
return (
|
|
63
|
-
<div className="slx-card">
|
|
64
|
-
<div className="slx-success-icon">
|
|
65
|
-
<CheckIcon />
|
|
66
|
-
</div>
|
|
67
|
-
<h1 className="slx-title" style={{ textAlign: 'center' }}>
|
|
68
|
-
Password updated
|
|
69
|
-
</h1>
|
|
70
|
-
<p className="slx-subtitle" style={{ textAlign: 'center' }}>
|
|
71
|
-
Your password has been changed. Use it to sign in.
|
|
72
|
-
</p>
|
|
73
|
-
{onSuccess && (
|
|
74
|
-
<button type="button" className="slx-btn" onClick={onSuccess}>
|
|
75
|
-
Continue to sign in
|
|
76
|
-
</button>
|
|
77
|
-
)}
|
|
78
|
-
</div>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<div className={`slx-card${error ? ' slx-card-error' : ''}`}>
|
|
84
|
-
<div className="slx-mark">
|
|
85
|
-
<KeyholeMark />
|
|
86
|
-
</div>
|
|
87
|
-
<h1 className="slx-title">Choose a new password</h1>
|
|
88
|
-
<p className="slx-subtitle">
|
|
89
|
-
Pick something strong you haven't used before.
|
|
90
|
-
</p>
|
|
91
|
-
|
|
92
|
-
{error && (
|
|
93
|
-
<p className="slx-error-text" role="alert">
|
|
94
|
-
{error}
|
|
95
|
-
</p>
|
|
96
|
-
)}
|
|
97
|
-
|
|
98
|
-
<form onSubmit={onSubmit}>
|
|
99
|
-
<div className="slx-field">
|
|
100
|
-
<label className="slx-label" htmlFor="slx-reset-password">
|
|
101
|
-
New password
|
|
102
|
-
</label>
|
|
103
|
-
<input
|
|
104
|
-
id="slx-reset-password"
|
|
105
|
-
className="slx-input"
|
|
106
|
-
type="password"
|
|
107
|
-
autoComplete="new-password"
|
|
108
|
-
placeholder="At least 8 characters"
|
|
109
|
-
value={password}
|
|
110
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
111
|
-
required
|
|
112
|
-
minLength={8}
|
|
113
|
-
/>
|
|
114
|
-
</div>
|
|
115
|
-
<button className="slx-btn" type="submit" disabled={busy}>
|
|
116
|
-
{busy && <span className="slx-spinner" aria-hidden="true" />}
|
|
117
|
-
{busy ? 'Updating…' : 'Update password'}
|
|
118
|
-
</button>
|
|
119
|
-
</form>
|
|
120
|
-
</div>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { SlyxupError } from '@slyxup/core';
|
|
2
|
-
import { useAuth } from '@slyxup/react';
|
|
3
|
-
import { type FormEvent, useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { GitHubIcon, GoogleIcon, KeyholeMark } from '../../icons';
|
|
5
|
-
import { injectStyles } from '../../styles';
|
|
6
|
-
|
|
7
|
-
export interface SignInProps {
|
|
8
|
-
/** Show social buttons (default true) */
|
|
9
|
-
social?: boolean;
|
|
10
|
-
/** Called after successful sign in */
|
|
11
|
-
onSuccess?: () => void;
|
|
12
|
-
/** Switch to sign up */
|
|
13
|
-
onSignUpClick?: () => void;
|
|
14
|
-
/** Show "Forgot password?" — navigates via this callback */
|
|
15
|
-
onForgotPasswordClick?: () => void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Email/password + OAuth sign-in card. */
|
|
19
|
-
export function SignIn({
|
|
20
|
-
social = true,
|
|
21
|
-
onSuccess,
|
|
22
|
-
onSignUpClick,
|
|
23
|
-
onForgotPasswordClick,
|
|
24
|
-
}: SignInProps) {
|
|
25
|
-
injectStyles();
|
|
26
|
-
const { signIn, completeSignIn, client } = useAuth() as unknown as {
|
|
27
|
-
signIn: ReturnType<typeof useAuth>['signIn'];
|
|
28
|
-
completeSignIn: ReturnType<typeof useAuth>['completeSignIn'];
|
|
29
|
-
client: { publishableKey?: string; apiUrl: string };
|
|
30
|
-
};
|
|
31
|
-
const [email, setEmail] = useState('');
|
|
32
|
-
const [password, setPassword] = useState('');
|
|
33
|
-
const [busy, setBusy] = useState(false);
|
|
34
|
-
const [error, setError] = useState<string | null>(null);
|
|
35
|
-
const [challengeToken, setChallengeToken] = useState<string | null>(null);
|
|
36
|
-
const [tfaCode, setTfaCode] = useState('');
|
|
37
|
-
const cardRef = useRef<HTMLDivElement>(null);
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (error) {
|
|
41
|
-
const t = setTimeout(() => setError(null), 4000);
|
|
42
|
-
return () => clearTimeout(t);
|
|
43
|
-
}
|
|
44
|
-
}, [error]);
|
|
45
|
-
|
|
46
|
-
async function onSubmit(e: FormEvent) {
|
|
47
|
-
e.preventDefault();
|
|
48
|
-
setBusy(true);
|
|
49
|
-
setError(null);
|
|
50
|
-
try {
|
|
51
|
-
const res = await signIn({ email, password });
|
|
52
|
-
if (res && 'challengeToken' in res) {
|
|
53
|
-
setChallengeToken(res.challengeToken);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
onSuccess?.();
|
|
57
|
-
} catch (err) {
|
|
58
|
-
setError(
|
|
59
|
-
err instanceof SlyxupError
|
|
60
|
-
? err.message
|
|
61
|
-
: 'Something went wrong. Try again.'
|
|
62
|
-
);
|
|
63
|
-
} finally {
|
|
64
|
-
setBusy(false);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
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
|
-
|
|
85
|
-
function oauth(provider: 'google' | 'github') {
|
|
86
|
-
const redirect = encodeURIComponent(window.location.href);
|
|
87
|
-
window.location.href = `${client.apiUrl}/v1/oauth/${provider}?redirect_url=${redirect}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const missingKey =
|
|
91
|
-
!client.publishableKey ||
|
|
92
|
-
client.publishableKey === 'pk_test_missing' ||
|
|
93
|
-
client.publishableKey.includes('REPLACE');
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<div ref={cardRef} className={`slx-card${error ? ' slx-card-error' : ''}`}>
|
|
97
|
-
{missingKey && (
|
|
98
|
-
<p className="slx-setup-note">
|
|
99
|
-
<strong>Setup:</strong> Add{' '}
|
|
100
|
-
<code>NEXT_PUBLIC_SLYXUP_PUBLISHABLE_KEY</code> to{' '}
|
|
101
|
-
<code>.env.local</code> — run <code>npx @slyxup/cli keys create</code>
|
|
102
|
-
</p>
|
|
103
|
-
)}
|
|
104
|
-
<div className="slx-mark">
|
|
105
|
-
<KeyholeMark />
|
|
106
|
-
</div>
|
|
107
|
-
<h1 className="slx-title">Sign in</h1>
|
|
108
|
-
<p className="slx-subtitle">
|
|
109
|
-
Welcome back. Enter your details to continue.
|
|
110
|
-
</p>
|
|
111
|
-
|
|
112
|
-
{social && (
|
|
113
|
-
<>
|
|
114
|
-
<div className="slx-social">
|
|
115
|
-
<button
|
|
116
|
-
type="button"
|
|
117
|
-
className="slx-social-btn"
|
|
118
|
-
onClick={() => oauth('google')}
|
|
119
|
-
>
|
|
120
|
-
<GoogleIcon /> Continue with Google
|
|
121
|
-
</button>
|
|
122
|
-
<button
|
|
123
|
-
type="button"
|
|
124
|
-
className="slx-social-btn"
|
|
125
|
-
onClick={() => oauth('github')}
|
|
126
|
-
>
|
|
127
|
-
<GitHubIcon /> Continue with GitHub
|
|
128
|
-
</button>
|
|
129
|
-
</div>
|
|
130
|
-
<div className="slx-divider">or</div>
|
|
131
|
-
</>
|
|
132
|
-
)}
|
|
133
|
-
|
|
134
|
-
{error && (
|
|
135
|
-
<p className="slx-error-text" role="alert">
|
|
136
|
-
{error}
|
|
137
|
-
</p>
|
|
138
|
-
)}
|
|
139
|
-
|
|
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>
|
|
184
|
-
</label>
|
|
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
|
-
/>
|
|
221
|
-
</div>
|
|
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
|
-
)}
|
|
228
|
-
|
|
229
|
-
{onSignUpClick && (
|
|
230
|
-
<p className="slx-footer">
|
|
231
|
-
Don't have an account?{' '}
|
|
232
|
-
<button type="button" className="slx-link" onClick={onSignUpClick}>
|
|
233
|
-
Sign up
|
|
234
|
-
</button>
|
|
235
|
-
</p>
|
|
236
|
-
)}
|
|
237
|
-
</div>
|
|
238
|
-
);
|
|
239
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { SlyxupError } from '@slyxup/core';
|
|
2
|
-
import { useAuth } from '@slyxup/react';
|
|
3
|
-
import { type FormEvent, useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { GitHubIcon, GoogleIcon, KeyholeMark } from '../../icons';
|
|
5
|
-
import { injectStyles } from '../../styles';
|
|
6
|
-
|
|
7
|
-
export interface SignUpProps {
|
|
8
|
-
social?: boolean;
|
|
9
|
-
onSuccess?: () => void;
|
|
10
|
-
onSignInClick?: () => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Email/password + OAuth sign-up card. */
|
|
14
|
-
export function SignUp({
|
|
15
|
-
social = true,
|
|
16
|
-
onSuccess,
|
|
17
|
-
onSignInClick,
|
|
18
|
-
}: SignUpProps) {
|
|
19
|
-
injectStyles();
|
|
20
|
-
const { signUp, client } = useAuth() as unknown as {
|
|
21
|
-
signUp: ReturnType<typeof useAuth>['signUp'];
|
|
22
|
-
client: { publishableKey?: string; apiUrl: string };
|
|
23
|
-
};
|
|
24
|
-
const [firstName, setFirstName] = useState('');
|
|
25
|
-
const [username, setUsername] = useState('');
|
|
26
|
-
const [email, setEmail] = useState('');
|
|
27
|
-
const [password, setPassword] = useState('');
|
|
28
|
-
const [busy, setBusy] = useState(false);
|
|
29
|
-
const [error, setError] = useState<string | null>(null);
|
|
30
|
-
const [successEmail, setSuccessEmail] = useState<string | null>(null);
|
|
31
|
-
const cardRef = useRef<HTMLDivElement>(null);
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (error) {
|
|
35
|
-
const t = setTimeout(() => setError(null), 4000);
|
|
36
|
-
return () => clearTimeout(t);
|
|
37
|
-
}
|
|
38
|
-
}, [error]);
|
|
39
|
-
|
|
40
|
-
async function onSubmit(e: FormEvent) {
|
|
41
|
-
e.preventDefault();
|
|
42
|
-
setBusy(true);
|
|
43
|
-
setError(null);
|
|
44
|
-
try {
|
|
45
|
-
await signUp({
|
|
46
|
-
email,
|
|
47
|
-
password,
|
|
48
|
-
firstName: firstName || undefined,
|
|
49
|
-
username: username || undefined,
|
|
50
|
-
});
|
|
51
|
-
setSuccessEmail(email);
|
|
52
|
-
onSuccess?.();
|
|
53
|
-
} catch (err) {
|
|
54
|
-
setError(
|
|
55
|
-
err instanceof SlyxupError
|
|
56
|
-
? err.message
|
|
57
|
-
: 'Something went wrong. Try again.'
|
|
58
|
-
);
|
|
59
|
-
} finally {
|
|
60
|
-
setBusy(false);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function oauth(provider: 'google' | 'github') {
|
|
65
|
-
const redirect = encodeURIComponent(window.location.href);
|
|
66
|
-
window.location.href = `${client.apiUrl}/v1/oauth/${provider}?redirect_url=${redirect}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const missingKey =
|
|
70
|
-
!client.publishableKey ||
|
|
71
|
-
client.publishableKey === 'pk_test_missing' ||
|
|
72
|
-
client.publishableKey.includes('REPLACE');
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<div ref={cardRef} className={`slx-card${error ? ' slx-card-error' : ''}`}>
|
|
76
|
-
{missingKey && (
|
|
77
|
-
<p className="slx-setup-note">
|
|
78
|
-
<strong>Setup:</strong> Add{' '}
|
|
79
|
-
<code>NEXT_PUBLIC_SLYXUP_PUBLISHABLE_KEY</code> — run{' '}
|
|
80
|
-
<code>npx @slyxup/cli keys create</code>
|
|
81
|
-
</p>
|
|
82
|
-
)}
|
|
83
|
-
<div className="slx-mark">
|
|
84
|
-
<KeyholeMark />
|
|
85
|
-
</div>
|
|
86
|
-
<h1 className="slx-title">Create your account</h1>
|
|
87
|
-
<p className="slx-subtitle">A minute to set up. Sign in forever after.</p>
|
|
88
|
-
|
|
89
|
-
{social && (
|
|
90
|
-
<>
|
|
91
|
-
<div className="slx-social">
|
|
92
|
-
<button
|
|
93
|
-
type="button"
|
|
94
|
-
className="slx-social-btn"
|
|
95
|
-
onClick={() => oauth('google')}
|
|
96
|
-
>
|
|
97
|
-
<GoogleIcon /> Continue with Google
|
|
98
|
-
</button>
|
|
99
|
-
<button
|
|
100
|
-
type="button"
|
|
101
|
-
className="slx-social-btn"
|
|
102
|
-
onClick={() => oauth('github')}
|
|
103
|
-
>
|
|
104
|
-
<GitHubIcon /> Continue with GitHub
|
|
105
|
-
</button>
|
|
106
|
-
</div>
|
|
107
|
-
<div className="slx-divider">or</div>
|
|
108
|
-
</>
|
|
109
|
-
)}
|
|
110
|
-
|
|
111
|
-
{error && (
|
|
112
|
-
<p className="slx-error-text" role="alert">
|
|
113
|
-
{error}
|
|
114
|
-
</p>
|
|
115
|
-
)}
|
|
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
|
-
|
|
129
|
-
<form onSubmit={onSubmit}>
|
|
130
|
-
<div className="slx-field">
|
|
131
|
-
<label className="slx-label" htmlFor="slx-signup-name">
|
|
132
|
-
First name
|
|
133
|
-
</label>
|
|
134
|
-
<input
|
|
135
|
-
id="slx-signup-name"
|
|
136
|
-
className="slx-input"
|
|
137
|
-
type="text"
|
|
138
|
-
autoComplete="given-name"
|
|
139
|
-
placeholder="Ada"
|
|
140
|
-
value={firstName}
|
|
141
|
-
onChange={(e) => setFirstName(e.target.value)}
|
|
142
|
-
/>
|
|
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>
|
|
161
|
-
<div className="slx-field">
|
|
162
|
-
<label className="slx-label" htmlFor="slx-signup-email">
|
|
163
|
-
Email
|
|
164
|
-
</label>
|
|
165
|
-
<input
|
|
166
|
-
id="slx-signup-email"
|
|
167
|
-
className="slx-input"
|
|
168
|
-
type="email"
|
|
169
|
-
autoComplete="email"
|
|
170
|
-
placeholder="you@example.com"
|
|
171
|
-
value={email}
|
|
172
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
173
|
-
required
|
|
174
|
-
/>
|
|
175
|
-
</div>
|
|
176
|
-
<div className="slx-field">
|
|
177
|
-
<label className="slx-label" htmlFor="slx-signup-password">
|
|
178
|
-
Password
|
|
179
|
-
</label>
|
|
180
|
-
<input
|
|
181
|
-
id="slx-signup-password"
|
|
182
|
-
className="slx-input"
|
|
183
|
-
type="password"
|
|
184
|
-
autoComplete="new-password"
|
|
185
|
-
placeholder="At least 8 characters"
|
|
186
|
-
value={password}
|
|
187
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
188
|
-
required
|
|
189
|
-
minLength={8}
|
|
190
|
-
/>
|
|
191
|
-
<p className="slx-hint">
|
|
192
|
-
Use 8+ characters with a mix of letters and numbers.
|
|
193
|
-
</p>
|
|
194
|
-
</div>
|
|
195
|
-
<button className="slx-btn" type="submit" disabled={busy}>
|
|
196
|
-
{busy && <span className="slx-spinner" aria-hidden="true" />}
|
|
197
|
-
{busy ? 'Creating account…' : 'Create account'}
|
|
198
|
-
</button>
|
|
199
|
-
</form>
|
|
200
|
-
|
|
201
|
-
{onSignInClick && (
|
|
202
|
-
<p className="slx-footer">
|
|
203
|
-
Already have an account?{' '}
|
|
204
|
-
<button type="button" className="slx-link" onClick={onSignInClick}>
|
|
205
|
-
Sign in
|
|
206
|
-
</button>
|
|
207
|
-
</p>
|
|
208
|
-
)}
|
|
209
|
-
</div>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { useAuth } from '@slyxup/react';
|
|
2
|
-
import { GitHubIcon, GoogleIcon } from '../../icons';
|
|
3
|
-
import { injectStyles } from '../../styles';
|
|
4
|
-
|
|
5
|
-
export interface SocialButtonsProps {
|
|
6
|
-
/** Show only these providers. Default: both */
|
|
7
|
-
providers?: Array<'google' | 'github'>;
|
|
8
|
-
/** OAuth start path base. Default: `<apiUrl>/v1/oauth` from the provider client */
|
|
9
|
-
basePath?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const META = {
|
|
13
|
-
google: { label: 'Continue with Google', Icon: GoogleIcon },
|
|
14
|
-
github: { label: 'Continue with GitHub', Icon: GitHubIcon },
|
|
15
|
-
} as const;
|
|
16
|
-
|
|
17
|
-
/** Provider buttons that redirect to hosted OAuth start. */
|
|
18
|
-
export function SocialButtons({
|
|
19
|
-
providers = ['google', 'github'],
|
|
20
|
-
basePath,
|
|
21
|
-
}: SocialButtonsProps) {
|
|
22
|
-
injectStyles();
|
|
23
|
-
const { client } = useAuth() as unknown as {
|
|
24
|
-
client?: { apiUrl?: string };
|
|
25
|
-
};
|
|
26
|
-
const base = basePath ?? `${client?.apiUrl ?? ''}/v1/oauth`;
|
|
27
|
-
return (
|
|
28
|
-
<div className="slx-social">
|
|
29
|
-
{providers.map((p) => {
|
|
30
|
-
const { label, Icon } = META[p];
|
|
31
|
-
return (
|
|
32
|
-
<button
|
|
33
|
-
key={p}
|
|
34
|
-
type="button"
|
|
35
|
-
className="slx-social-btn"
|
|
36
|
-
onClick={() => {
|
|
37
|
-
const redirect = encodeURIComponent(window.location.href);
|
|
38
|
-
window.location.href = `${base}/${p}?redirect_url=${redirect}`;
|
|
39
|
-
}}
|
|
40
|
-
>
|
|
41
|
-
<Icon /> {label}
|
|
42
|
-
</button>
|
|
43
|
-
);
|
|
44
|
-
})}
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
}
|