@indietabletop/appkit 3.6.0-1 → 3.6.0-3
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/lib/AppConfig/AppConfig.tsx +54 -0
- package/lib/HistoryState.ts +21 -0
- package/lib/Letterhead/style.css.ts +2 -0
- package/lib/LetterheadForm/index.tsx +53 -0
- package/lib/LetterheadForm/style.css.ts +8 -0
- package/lib/QRCode/QRCode.stories.tsx +47 -0
- package/lib/QRCode/QRCode.tsx +49 -0
- package/lib/QRCode/style.css.ts +19 -0
- package/lib/ShareButton/ShareButton.tsx +141 -0
- package/lib/SubscribeCard/LetterheadInfoCard.tsx +23 -0
- package/lib/SubscribeCard/SubscribeByEmailCard.stories.tsx +83 -0
- package/lib/SubscribeCard/SubscribeByEmailCard.tsx +177 -0
- package/lib/SubscribeCard/SubscribeCard.stories.tsx +27 -23
- package/lib/SubscribeCard/SubscribeCard.tsx +22 -17
- package/lib/Title/index.tsx +7 -2
- package/lib/account/AccountIssueView.tsx +40 -0
- package/lib/account/AlreadyLoggedInView.tsx +44 -0
- package/lib/account/CurrentUserFetcher.stories.tsx +339 -0
- package/lib/account/CurrentUserFetcher.tsx +119 -0
- package/lib/account/FailureFallbackView.tsx +36 -0
- package/lib/account/JoinPage.stories.tsx +270 -0
- package/lib/account/JoinPage.tsx +288 -0
- package/lib/account/LoadingView.tsx +14 -0
- package/lib/account/LoginPage.stories.tsx +318 -0
- package/lib/account/LoginPage.tsx +138 -0
- package/lib/account/LoginView.tsx +136 -0
- package/lib/account/NoConnectionView.tsx +34 -0
- package/lib/account/PasswordResetPage.stories.tsx +250 -0
- package/lib/account/PasswordResetPage.tsx +291 -0
- package/lib/account/UserMismatchView.tsx +61 -0
- package/lib/account/VerifyPage.tsx +217 -0
- package/lib/account/style.css.ts +57 -0
- package/lib/account/types.ts +9 -0
- package/lib/account/useCurrentUserResult.tsx +38 -0
- package/lib/class-names.ts +1 -1
- package/lib/client.ts +54 -7
- package/lib/globals.css.ts +5 -0
- package/lib/index.ts +11 -1
- package/lib/useEnsureValue.ts +31 -0
- package/package.json +3 -2
- package/lib/ClientContext/ClientContext.tsx +0 -25
- package/lib/LoginPage/LoginPage.stories.tsx +0 -107
- package/lib/LoginPage/LoginPage.tsx +0 -204
- package/lib/LoginPage/style.css.ts +0 -17
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { Story } from "@storybook/addon-docs/blocks";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { http, HttpResponse } from "msw";
|
|
4
|
+
import { fn } from "storybook/test";
|
|
5
|
+
import { sleep } from "../sleep.ts";
|
|
6
|
+
import type { CurrentUser, SessionInfo } from "../types.ts";
|
|
7
|
+
import { JoinCard } from "./JoinPage.tsx";
|
|
8
|
+
|
|
9
|
+
function createMocks(options?: { responseSpeed?: number }) {
|
|
10
|
+
const simulateNetwork = () => sleep(options?.responseSpeed ?? 2000);
|
|
11
|
+
|
|
12
|
+
const john: CurrentUser = {
|
|
13
|
+
id: "john",
|
|
14
|
+
email: "john@example.com",
|
|
15
|
+
isVerified: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const sessionInfo: SessionInfo = {
|
|
19
|
+
createdTs: 123,
|
|
20
|
+
expiresTs: 123,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
data: { john },
|
|
25
|
+
handlers: {
|
|
26
|
+
refreshTokens: {
|
|
27
|
+
failed: () => {
|
|
28
|
+
return http.post(
|
|
29
|
+
"http://mock.api/v1/sessions/access-tokens",
|
|
30
|
+
async () => {
|
|
31
|
+
await simulateNetwork();
|
|
32
|
+
return HttpResponse.text("Refresh token expired or missing", {
|
|
33
|
+
status: 401,
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
getCurrentUser: {
|
|
41
|
+
success: (currentUser: CurrentUser) => {
|
|
42
|
+
return http.get("http://mock.api/v1/users/me", async () => {
|
|
43
|
+
await simulateNetwork();
|
|
44
|
+
return HttpResponse.json(currentUser);
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
noConnection: () => {
|
|
49
|
+
return http.get("http://mock.api/v1/users/me", async () => {
|
|
50
|
+
return HttpResponse.error();
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
unknownFailure: () => {
|
|
55
|
+
return http.get("http://mock.api/v1/users/me", async () => {
|
|
56
|
+
await simulateNetwork();
|
|
57
|
+
return HttpResponse.text("Internal server error", { status: 500 });
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Auth cookies no longer valid to make this request.
|
|
63
|
+
*/
|
|
64
|
+
notAuthenticated: () => {
|
|
65
|
+
return http.get("http://mock.api/v1/users/me", async () => {
|
|
66
|
+
await simulateNetwork();
|
|
67
|
+
return HttpResponse.text("Not authenticated", { status: 401 });
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
join: {
|
|
73
|
+
success(currentUser: CurrentUser) {
|
|
74
|
+
return http.post("http://mock.api/v1/users", async () => {
|
|
75
|
+
await simulateNetwork();
|
|
76
|
+
|
|
77
|
+
return HttpResponse.json({
|
|
78
|
+
currentUser,
|
|
79
|
+
sessionInfo,
|
|
80
|
+
tokenId: "1",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
emailTaken() {
|
|
86
|
+
return http.post("http://mock.api/v1/users", async () => {
|
|
87
|
+
await simulateNetwork();
|
|
88
|
+
|
|
89
|
+
return HttpResponse.text("Duplicate unique key", { status: 409 });
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
unknownFailure() {
|
|
94
|
+
return http.post("http://mock.api/v1/users", async () => {
|
|
95
|
+
await simulateNetwork();
|
|
96
|
+
return HttpResponse.text("Internal server error", {
|
|
97
|
+
status: 500,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
verify: {
|
|
103
|
+
success() {
|
|
104
|
+
return http.put(
|
|
105
|
+
"http://mock.api/v1/user-verification-tokens/:id",
|
|
106
|
+
async () => {
|
|
107
|
+
await simulateNetwork();
|
|
108
|
+
return HttpResponse.json({ message: "OK" });
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
notFoundOrExpired() {
|
|
113
|
+
return http.put(
|
|
114
|
+
"http://mock.api/v1/user-verification-tokens/:id",
|
|
115
|
+
async () => {
|
|
116
|
+
await simulateNetwork();
|
|
117
|
+
return HttpResponse.text("Not found", { status: 404 });
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
unknownFailure() {
|
|
122
|
+
return http.put(
|
|
123
|
+
"http://mock.api/v1/user-verification-tokens/:id",
|
|
124
|
+
async () => {
|
|
125
|
+
await simulateNetwork();
|
|
126
|
+
return HttpResponse.text("Internal server error", {
|
|
127
|
+
status: 500,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { data, handlers } = createMocks({ responseSpeed: 700 });
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Allows the user to reset their password.
|
|
141
|
+
*/
|
|
142
|
+
const meta = {
|
|
143
|
+
title: "Account/Join Page",
|
|
144
|
+
component: JoinCard,
|
|
145
|
+
tags: ["autodocs"],
|
|
146
|
+
args: {
|
|
147
|
+
onLogout: fn(),
|
|
148
|
+
},
|
|
149
|
+
parameters: {
|
|
150
|
+
msw: {
|
|
151
|
+
handlers: {
|
|
152
|
+
refresh: handlers.refreshTokens.failed(),
|
|
153
|
+
currentUser: handlers.getCurrentUser.notAuthenticated(),
|
|
154
|
+
join: handlers.join.success(data.john),
|
|
155
|
+
verify: handlers.verify.success(),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
} satisfies Meta<typeof JoinCard>;
|
|
160
|
+
|
|
161
|
+
export default meta;
|
|
162
|
+
|
|
163
|
+
type Story = StoryObj<typeof meta>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The default case in which all steps of the flow succeed.
|
|
167
|
+
*/
|
|
168
|
+
export const Success: Story = {
|
|
169
|
+
play: async ({ canvas, userEvent }) => {
|
|
170
|
+
userEvent.type(canvas.getByTestId("email-input"), "foo@bar.com");
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* In this case, the initial step fails because the email address is associated
|
|
176
|
+
* with an existing user.
|
|
177
|
+
*/
|
|
178
|
+
export const EmailTakenOnJoin: Story = {
|
|
179
|
+
parameters: {
|
|
180
|
+
msw: {
|
|
181
|
+
handlers: {
|
|
182
|
+
join: handlers.join.emailTaken(),
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* In this case, the initial step fails for a reason that doesn't have any
|
|
190
|
+
* special handling in this location. E.g. network error, server error, etc.
|
|
191
|
+
*/
|
|
192
|
+
export const UnknownFailureOnJoin: Story = {
|
|
193
|
+
parameters: {
|
|
194
|
+
msw: {
|
|
195
|
+
handlers: {
|
|
196
|
+
join: handlers.join.unknownFailure(),
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* In this case, the verify step fails because the token has expired,
|
|
204
|
+
* or is incorrect.
|
|
205
|
+
*/
|
|
206
|
+
export const NotFoundOrExpiredOnVerify: Story = {
|
|
207
|
+
parameters: {
|
|
208
|
+
msw: {
|
|
209
|
+
handlers: {
|
|
210
|
+
verify: handlers.verify.notFoundOrExpired(),
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* In this case, the verify step fails for a reason that doesn't have any
|
|
218
|
+
* special handling in this location. E.g. network error, server error, etc.
|
|
219
|
+
*/
|
|
220
|
+
export const UnknownFailureOnVerify: Story = {
|
|
221
|
+
parameters: {
|
|
222
|
+
msw: {
|
|
223
|
+
handlers: {
|
|
224
|
+
verify: handlers.verify.unknownFailure(),
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The proactive user-session check returns a user account.
|
|
232
|
+
*
|
|
233
|
+
* The user can continue to use the app, or log out.
|
|
234
|
+
*/
|
|
235
|
+
export const AlreadyLoggedIn: Story = {
|
|
236
|
+
parameters: {
|
|
237
|
+
msw: {
|
|
238
|
+
handlers: {
|
|
239
|
+
currentUser: handlers.getCurrentUser.success(data.john),
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The proactive user session check has failed due to connection issues.
|
|
247
|
+
*/
|
|
248
|
+
export const NoConnection: Story = {
|
|
249
|
+
parameters: {
|
|
250
|
+
msw: {
|
|
251
|
+
handlers: {
|
|
252
|
+
currentUser: handlers.getCurrentUser.noConnection(),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The proactive user session check has failed due to an error that doesn't
|
|
260
|
+
* carry any special meaning.
|
|
261
|
+
*/
|
|
262
|
+
export const UnknownFailure: Story = {
|
|
263
|
+
parameters: {
|
|
264
|
+
msw: {
|
|
265
|
+
handlers: {
|
|
266
|
+
currentUser: handlers.getCurrentUser.unknownFailure(),
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { Form, useStoreState } from "@ariakit/react";
|
|
2
|
+
import { type Dispatch, type SetStateAction, useState } from "react";
|
|
3
|
+
import { Link } from "wouter";
|
|
4
|
+
import { useAppConfig, useClient } from "../AppConfig/AppConfig.tsx";
|
|
5
|
+
import { cx } from "../class-names.ts";
|
|
6
|
+
import { interactiveText } from "../common.css.ts";
|
|
7
|
+
import { getSubmitFailureMessage } from "../failureMessages.ts";
|
|
8
|
+
import {
|
|
9
|
+
Letterhead,
|
|
10
|
+
LetterheadHeading,
|
|
11
|
+
LetterheadParagraph,
|
|
12
|
+
LetterheadSubmitButton,
|
|
13
|
+
} from "../Letterhead/index.tsx";
|
|
14
|
+
import { button } from "../Letterhead/style.css.ts";
|
|
15
|
+
import {
|
|
16
|
+
InputsStack,
|
|
17
|
+
LetterheadCheckboxField,
|
|
18
|
+
LetterheadFormActions,
|
|
19
|
+
LetterheadHeader,
|
|
20
|
+
LetterheadSubmitError,
|
|
21
|
+
LetterheadTextField,
|
|
22
|
+
} from "../LetterheadForm/index.tsx";
|
|
23
|
+
import { useForm } from "../use-form.ts";
|
|
24
|
+
import { validEmail, validPassword } from "../validations.ts";
|
|
25
|
+
import { AlreadyLoggedInView } from "./AlreadyLoggedInView.tsx";
|
|
26
|
+
import { FailureFallbackView } from "./FailureFallbackView.tsx";
|
|
27
|
+
import { LoadingView } from "./LoadingView.tsx";
|
|
28
|
+
import { NoConnectionView } from "./NoConnectionView.tsx";
|
|
29
|
+
import type { DefaultFormValues, EventHandlerWithReload } from "./types.ts";
|
|
30
|
+
import { useCurrentUserResult } from "./useCurrentUserResult.tsx";
|
|
31
|
+
|
|
32
|
+
type SetStep = Dispatch<SetStateAction<JoinStep>>;
|
|
33
|
+
|
|
34
|
+
function InitialStep(props: {
|
|
35
|
+
setStep: SetStep;
|
|
36
|
+
defaultValues?: DefaultFormValues;
|
|
37
|
+
}) {
|
|
38
|
+
const { client, placeholders, hrefs } = useAppConfig();
|
|
39
|
+
const { defaultValues, setStep } = props;
|
|
40
|
+
|
|
41
|
+
const { form, submitName } = useForm({
|
|
42
|
+
defaultValues: {
|
|
43
|
+
email: defaultValues?.email ?? "",
|
|
44
|
+
password: "",
|
|
45
|
+
acceptedTos: false,
|
|
46
|
+
subscribedToNewsletter: false,
|
|
47
|
+
},
|
|
48
|
+
validate: {
|
|
49
|
+
email: validEmail,
|
|
50
|
+
password: validPassword,
|
|
51
|
+
},
|
|
52
|
+
async onSubmit({ values }) {
|
|
53
|
+
const op = await client.join(values);
|
|
54
|
+
|
|
55
|
+
return op.mapFailure((failure) => {
|
|
56
|
+
return getSubmitFailureMessage(failure, {
|
|
57
|
+
409: "🙀 This email address is already in use. Please choose a different one, or log in.",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
onSuccess(response, form) {
|
|
62
|
+
setStep({
|
|
63
|
+
type: "SUBMIT_CODE",
|
|
64
|
+
tokenId: response.tokenId,
|
|
65
|
+
email: form.values.email,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const emailValue = useStoreState(form, (s) => s.values.email);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Letterhead>
|
|
74
|
+
<LetterheadHeader>
|
|
75
|
+
<LetterheadHeading>Join Indie Tabletop Club</LetterheadHeading>
|
|
76
|
+
<LetterheadParagraph>
|
|
77
|
+
Enter your email and choose a strong password. We will send you a
|
|
78
|
+
one-time code to verify your account.
|
|
79
|
+
</LetterheadParagraph>
|
|
80
|
+
</LetterheadHeader>
|
|
81
|
+
|
|
82
|
+
<Form store={form} resetOnSubmit={false}>
|
|
83
|
+
<InputsStack>
|
|
84
|
+
<LetterheadTextField
|
|
85
|
+
name={form.names.email}
|
|
86
|
+
label="Email"
|
|
87
|
+
type="email"
|
|
88
|
+
placeholder={placeholders.email}
|
|
89
|
+
autoComplete="username"
|
|
90
|
+
data-testid="email-input"
|
|
91
|
+
required
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<LetterheadTextField
|
|
95
|
+
name={form.names.password}
|
|
96
|
+
label="Password"
|
|
97
|
+
type="password"
|
|
98
|
+
placeholder="Choose a strong password"
|
|
99
|
+
hint="Must be at least 8 characters"
|
|
100
|
+
autoComplete="new-password"
|
|
101
|
+
data-testid="new-password-input"
|
|
102
|
+
required
|
|
103
|
+
/>
|
|
104
|
+
|
|
105
|
+
<LetterheadCheckboxField
|
|
106
|
+
name={form.names.subscribedToNewsletter}
|
|
107
|
+
label={
|
|
108
|
+
<>
|
|
109
|
+
Subscribe to <em>The Changelog</em>, an at-most-once-a-month
|
|
110
|
+
newsletter about updates and new releases. You can unsubscribe
|
|
111
|
+
any time with one click.
|
|
112
|
+
</>
|
|
113
|
+
}
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
<LetterheadCheckboxField
|
|
117
|
+
name={form.names.acceptedTos}
|
|
118
|
+
required
|
|
119
|
+
label={
|
|
120
|
+
<>
|
|
121
|
+
I accept the{" "}
|
|
122
|
+
<a
|
|
123
|
+
target="_blank"
|
|
124
|
+
rel="noreferrer noopener"
|
|
125
|
+
href={hrefs.terms()}
|
|
126
|
+
className={interactiveText}
|
|
127
|
+
>
|
|
128
|
+
Terms of Service
|
|
129
|
+
</a>
|
|
130
|
+
.
|
|
131
|
+
</>
|
|
132
|
+
}
|
|
133
|
+
/>
|
|
134
|
+
</InputsStack>
|
|
135
|
+
|
|
136
|
+
<LetterheadFormActions>
|
|
137
|
+
<LetterheadSubmitError name={submitName} />
|
|
138
|
+
<LetterheadSubmitButton>Join</LetterheadSubmitButton>
|
|
139
|
+
|
|
140
|
+
<LetterheadParagraph align="start">
|
|
141
|
+
{"Have an existing account? "}
|
|
142
|
+
<Link
|
|
143
|
+
{...cx(interactiveText)}
|
|
144
|
+
href={hrefs.login()}
|
|
145
|
+
state={{ emailValue }}
|
|
146
|
+
>
|
|
147
|
+
Log in
|
|
148
|
+
</Link>
|
|
149
|
+
{"."}
|
|
150
|
+
</LetterheadParagraph>
|
|
151
|
+
</LetterheadFormActions>
|
|
152
|
+
</Form>
|
|
153
|
+
</Letterhead>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function SubmitCodeStep(props: {
|
|
158
|
+
tokenId: string;
|
|
159
|
+
setStep: SetStep;
|
|
160
|
+
email: string;
|
|
161
|
+
}) {
|
|
162
|
+
const { tokenId, email, setStep } = props;
|
|
163
|
+
const client = useClient();
|
|
164
|
+
|
|
165
|
+
const { form, submitName } = useForm({
|
|
166
|
+
defaultValues: {
|
|
167
|
+
code: "",
|
|
168
|
+
},
|
|
169
|
+
async onSubmit({ values }) {
|
|
170
|
+
const op = await client.verifyUser({ ...values, tokenId });
|
|
171
|
+
|
|
172
|
+
return op.mapFailure((failure) => {
|
|
173
|
+
return getSubmitFailureMessage(failure, {
|
|
174
|
+
404: "🚫 This code is incorrect or expired. Please try again.",
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
onSuccess() {
|
|
179
|
+
setStep({ type: "SUCCESS" });
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<Form store={form} resetOnSubmit={false}>
|
|
185
|
+
<Letterhead>
|
|
186
|
+
<LetterheadHeader>
|
|
187
|
+
<LetterheadHeading>Verify account</LetterheadHeading>
|
|
188
|
+
<LetterheadParagraph>
|
|
189
|
+
We've sent a one-time code to <em>{email}</em>. Please, enter the
|
|
190
|
+
code in the field below to verify your account.
|
|
191
|
+
</LetterheadParagraph>
|
|
192
|
+
</LetterheadHeader>
|
|
193
|
+
|
|
194
|
+
<LetterheadTextField
|
|
195
|
+
name={form.names.code}
|
|
196
|
+
label="Code"
|
|
197
|
+
placeholder="E.g. 123123"
|
|
198
|
+
autoComplete="one-time-code"
|
|
199
|
+
required
|
|
200
|
+
/>
|
|
201
|
+
|
|
202
|
+
<LetterheadFormActions>
|
|
203
|
+
<LetterheadSubmitError name={submitName} />
|
|
204
|
+
<LetterheadSubmitButton>Verify</LetterheadSubmitButton>
|
|
205
|
+
</LetterheadFormActions>
|
|
206
|
+
</Letterhead>
|
|
207
|
+
</Form>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function SuccessStep() {
|
|
212
|
+
const { hrefs } = useAppConfig();
|
|
213
|
+
return (
|
|
214
|
+
<Letterhead>
|
|
215
|
+
<LetterheadHeader>
|
|
216
|
+
<LetterheadHeading>Success!</LetterheadHeading>
|
|
217
|
+
<LetterheadParagraph>
|
|
218
|
+
Your Indie Tabletop Club account has been verified, yay!
|
|
219
|
+
</LetterheadParagraph>
|
|
220
|
+
</LetterheadHeader>
|
|
221
|
+
|
|
222
|
+
<Link href={hrefs.dashboard()} {...cx(button())}>
|
|
223
|
+
Go to dashboard
|
|
224
|
+
</Link>
|
|
225
|
+
</Letterhead>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
type JoinStep =
|
|
230
|
+
| { type: "INITIAL" }
|
|
231
|
+
| { type: "SUBMIT_CODE"; tokenId: string; email: string }
|
|
232
|
+
| { type: "SUCCESS" };
|
|
233
|
+
|
|
234
|
+
function JoinFlow(props: { defaultValues?: DefaultFormValues }) {
|
|
235
|
+
const [step, setStep] = useState<JoinStep>({ type: "INITIAL" });
|
|
236
|
+
|
|
237
|
+
switch (step.type) {
|
|
238
|
+
case "INITIAL": {
|
|
239
|
+
return <InitialStep {...props} setStep={setStep} />;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
case "SUBMIT_CODE": {
|
|
243
|
+
return <SubmitCodeStep {...step} setStep={setStep} />;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case "SUCCESS": {
|
|
247
|
+
return <SuccessStep />;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function JoinCard(props: {
|
|
253
|
+
onLogout: EventHandlerWithReload;
|
|
254
|
+
defaultValues?: DefaultFormValues;
|
|
255
|
+
}) {
|
|
256
|
+
const { result, latestAttemptTs, reload } = useCurrentUserResult();
|
|
257
|
+
|
|
258
|
+
return result.unpack(
|
|
259
|
+
(currentUser) => {
|
|
260
|
+
return (
|
|
261
|
+
<AlreadyLoggedInView
|
|
262
|
+
{...props}
|
|
263
|
+
currentUser={currentUser}
|
|
264
|
+
reload={reload}
|
|
265
|
+
/>
|
|
266
|
+
);
|
|
267
|
+
},
|
|
268
|
+
(failure) => {
|
|
269
|
+
if (failure.type === "API_ERROR" && failure.code === 401) {
|
|
270
|
+
return <JoinFlow {...props} />;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (failure.type === "NETWORK_ERROR") {
|
|
274
|
+
return (
|
|
275
|
+
<NoConnectionView
|
|
276
|
+
latestAttemptTs={latestAttemptTs}
|
|
277
|
+
onRetry={() => reload()}
|
|
278
|
+
/>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return <FailureFallbackView />;
|
|
283
|
+
},
|
|
284
|
+
() => {
|
|
285
|
+
return <LoadingView />;
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { cx } from "../class-names.ts";
|
|
2
|
+
import { Letterhead } from "../Letterhead/index.tsx";
|
|
3
|
+
import { LoadingIndicator } from "../LoadingIndicator.tsx";
|
|
4
|
+
import { loadingView } from "./style.css.ts";
|
|
5
|
+
|
|
6
|
+
export function LoadingView() {
|
|
7
|
+
return (
|
|
8
|
+
<Letterhead>
|
|
9
|
+
<div {...cx(loadingView.container)}>
|
|
10
|
+
<LoadingIndicator />
|
|
11
|
+
</div>
|
|
12
|
+
</Letterhead>
|
|
13
|
+
);
|
|
14
|
+
}
|