@indietabletop/appkit 3.6.0-2 → 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 +17 -16
- 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,250 @@
|
|
|
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 { sleep } from "../sleep.ts";
|
|
5
|
+
import type { CurrentUser, SessionInfo } from "../types.ts";
|
|
6
|
+
import { PasswordResetCard } from "./PasswordResetPage.tsx";
|
|
7
|
+
|
|
8
|
+
function createMocks(options?: { responseSpeed?: number }) {
|
|
9
|
+
const simulateNetwork = () => sleep(options?.responseSpeed ?? 2000);
|
|
10
|
+
|
|
11
|
+
const john: CurrentUser = {
|
|
12
|
+
id: "john",
|
|
13
|
+
email: "john@example.com",
|
|
14
|
+
isVerified: true,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const sessionInfo: SessionInfo = {
|
|
18
|
+
createdTs: 123,
|
|
19
|
+
expiresTs: 123,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
data: { john },
|
|
24
|
+
handlers: {
|
|
25
|
+
requestPasswordReset: {
|
|
26
|
+
success: () => {
|
|
27
|
+
return http.post(
|
|
28
|
+
"http://mock.api/v1/password-reset-tokens",
|
|
29
|
+
async () => {
|
|
30
|
+
await simulateNetwork();
|
|
31
|
+
return HttpResponse.json({ message: "OK", tokenId: "1" });
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
userNotFound: () => {
|
|
36
|
+
return http.post(
|
|
37
|
+
"http://mock.api/v1/password-reset-tokens",
|
|
38
|
+
async () => {
|
|
39
|
+
await simulateNetwork();
|
|
40
|
+
return HttpResponse.text("User not found", { status: 404 });
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
unknownFailure: () => {
|
|
45
|
+
return http.post(
|
|
46
|
+
"http://mock.api/v1/password-reset-tokens",
|
|
47
|
+
async () => {
|
|
48
|
+
await simulateNetwork();
|
|
49
|
+
return HttpResponse.text("Internal server error", {
|
|
50
|
+
status: 500,
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
checkPasswordResetCode: {
|
|
58
|
+
success() {
|
|
59
|
+
return http.get(
|
|
60
|
+
"http://mock.api/v1/password-reset-tokens/:id",
|
|
61
|
+
async () => {
|
|
62
|
+
await simulateNetwork();
|
|
63
|
+
return HttpResponse.json({ message: "OK" });
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
notFoundOrExpired() {
|
|
68
|
+
return http.get(
|
|
69
|
+
"http://mock.api/v1/password-reset-tokens/:id",
|
|
70
|
+
async () => {
|
|
71
|
+
await simulateNetwork();
|
|
72
|
+
return HttpResponse.text("Not found", { status: 404 });
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
unknownFailure() {
|
|
77
|
+
return http.get(
|
|
78
|
+
"http://mock.api/v1/password-reset-tokens/:id",
|
|
79
|
+
async () => {
|
|
80
|
+
await simulateNetwork();
|
|
81
|
+
return HttpResponse.text("Internal server error", {
|
|
82
|
+
status: 500,
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
setNewPassword: {
|
|
90
|
+
success() {
|
|
91
|
+
return http.put(
|
|
92
|
+
"http://mock.api/v1/password-reset-tokens/:id",
|
|
93
|
+
async () => {
|
|
94
|
+
await simulateNetwork();
|
|
95
|
+
return HttpResponse.json({ message: "OK" });
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
notFoundOrExpired() {
|
|
100
|
+
return http.put(
|
|
101
|
+
"http://mock.api/v1/password-reset-tokens/:id",
|
|
102
|
+
async () => {
|
|
103
|
+
await simulateNetwork();
|
|
104
|
+
return HttpResponse.text("Not found", { status: 404 });
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
unknownFailure() {
|
|
109
|
+
return http.put(
|
|
110
|
+
"http://mock.api/v1/password-reset-tokens/:id",
|
|
111
|
+
async () => {
|
|
112
|
+
await simulateNetwork();
|
|
113
|
+
return HttpResponse.text("Internal server error", {
|
|
114
|
+
status: 500,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
createNewSession: {
|
|
122
|
+
success: (currentUser: CurrentUser) => {
|
|
123
|
+
return http.post("http://mock.api/v1/sessions", async () => {
|
|
124
|
+
await simulateNetwork();
|
|
125
|
+
return HttpResponse.json({ currentUser, sessionInfo });
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { data, handlers } = createMocks({ responseSpeed: 700 });
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Allows the user to reset their password.
|
|
137
|
+
*/
|
|
138
|
+
const meta = {
|
|
139
|
+
title: "Account/Password Reset Page",
|
|
140
|
+
component: PasswordResetCard,
|
|
141
|
+
tags: ["autodocs"],
|
|
142
|
+
args: {},
|
|
143
|
+
parameters: {
|
|
144
|
+
msw: {
|
|
145
|
+
handlers: {
|
|
146
|
+
request: handlers.requestPasswordReset.success(),
|
|
147
|
+
check: handlers.checkPasswordResetCode.success(),
|
|
148
|
+
set: handlers.setNewPassword.success(),
|
|
149
|
+
login: handlers.createNewSession.success(data.john),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
} satisfies Meta<typeof PasswordResetCard>;
|
|
154
|
+
|
|
155
|
+
export default meta;
|
|
156
|
+
|
|
157
|
+
type Story = StoryObj<typeof meta>;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The default case in which all steps of the flow succeed.
|
|
161
|
+
*/
|
|
162
|
+
export const Success: Story = {};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* In this case, the initial step fails because the email address was not
|
|
166
|
+
* found in our database.
|
|
167
|
+
*/
|
|
168
|
+
export const UserNotFoundOnRequestCode: Story = {
|
|
169
|
+
parameters: {
|
|
170
|
+
msw: {
|
|
171
|
+
handlers: {
|
|
172
|
+
request: handlers.requestPasswordReset.userNotFound(),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* In this case, the initial step fails for a reason that doesn't have any
|
|
180
|
+
* special handling in this location. E.g. network error, server error, etc.
|
|
181
|
+
*/
|
|
182
|
+
export const UnknownFailureOnRequestCode: Story = {
|
|
183
|
+
parameters: {
|
|
184
|
+
msw: {
|
|
185
|
+
handlers: {
|
|
186
|
+
request: handlers.requestPasswordReset.unknownFailure(),
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* In this case, the check code step fails because the token has expired,
|
|
194
|
+
* or is incorrect.
|
|
195
|
+
*/
|
|
196
|
+
export const NotFoundOrExpiredOnCheckCode: Story = {
|
|
197
|
+
parameters: {
|
|
198
|
+
msw: {
|
|
199
|
+
handlers: {
|
|
200
|
+
check: handlers.checkPasswordResetCode.notFoundOrExpired(),
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* In this case, the check step fails for a reason that doesn't have any
|
|
208
|
+
* special handling in this location. E.g. network error, server error, etc.
|
|
209
|
+
*/
|
|
210
|
+
export const UnknownFailureOnCheckCode: Story = {
|
|
211
|
+
parameters: {
|
|
212
|
+
msw: {
|
|
213
|
+
handlers: {
|
|
214
|
+
check: handlers.checkPasswordResetCode.unknownFailure(),
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* In this case, the final set password step fails because the token has
|
|
222
|
+
* expired.
|
|
223
|
+
*
|
|
224
|
+
* Note that technically the token could also be incorrect/not found, but
|
|
225
|
+
* because the user has gotten through the check step, that would require
|
|
226
|
+
* some very strange set of circumstances.
|
|
227
|
+
*/
|
|
228
|
+
export const NotFoundOrExpiredOnSetPassword: Story = {
|
|
229
|
+
parameters: {
|
|
230
|
+
msw: {
|
|
231
|
+
handlers: {
|
|
232
|
+
set: handlers.setNewPassword.notFoundOrExpired(),
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* In this case, the set password step fails for a reason that doesn't have any
|
|
240
|
+
* special handling in this location. E.g. network error, server error, etc.
|
|
241
|
+
*/
|
|
242
|
+
export const UnknownFailureOnSetPassword: Story = {
|
|
243
|
+
parameters: {
|
|
244
|
+
msw: {
|
|
245
|
+
handlers: {
|
|
246
|
+
set: handlers.setNewPassword.unknownFailure(),
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
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
|
+
LetterheadFormActions,
|
|
17
|
+
LetterheadHeader,
|
|
18
|
+
LetterheadSubmitError,
|
|
19
|
+
LetterheadTextField,
|
|
20
|
+
} from "../LetterheadForm/index.tsx";
|
|
21
|
+
import { useForm } from "../use-form.ts";
|
|
22
|
+
import { validEmail, validPassword } from "../validations.ts";
|
|
23
|
+
import type { DefaultFormValues } from "./types.ts";
|
|
24
|
+
|
|
25
|
+
type SetStep = Dispatch<SetStateAction<ResetPasswordStep>>;
|
|
26
|
+
|
|
27
|
+
function RequestPasswordResetStep(props: {
|
|
28
|
+
defaultValues?: DefaultFormValues;
|
|
29
|
+
setStep: SetStep;
|
|
30
|
+
}) {
|
|
31
|
+
const { defaultValues } = props;
|
|
32
|
+
const { client, placeholders, hrefs } = useAppConfig();
|
|
33
|
+
|
|
34
|
+
const { form, submitName } = useForm({
|
|
35
|
+
defaultValues: {
|
|
36
|
+
email: defaultValues?.email ?? "",
|
|
37
|
+
},
|
|
38
|
+
validate: {
|
|
39
|
+
email: validEmail,
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async onSubmit({ values }) {
|
|
43
|
+
const op = await client.requestPasswordReset(values);
|
|
44
|
+
|
|
45
|
+
return op.mapFailure((failure) => {
|
|
46
|
+
return getSubmitFailureMessage(failure, {
|
|
47
|
+
404: "🤔 Couldn't find a user with this email address.",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
onSuccess(response, { values }) {
|
|
53
|
+
props.setStep({
|
|
54
|
+
type: "SUBMIT_CODE",
|
|
55
|
+
tokenId: response.tokenId,
|
|
56
|
+
email: values.email,
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const emailValue = useStoreState(form, (s) => s.values.email);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Letterhead>
|
|
65
|
+
<LetterheadHeader>
|
|
66
|
+
<LetterheadHeading>Password reset</LetterheadHeading>
|
|
67
|
+
<LetterheadParagraph>
|
|
68
|
+
Enter your Indie Tabletop Club account email to begin password reset.
|
|
69
|
+
We will send you a one-time code to verify your access.
|
|
70
|
+
</LetterheadParagraph>
|
|
71
|
+
</LetterheadHeader>
|
|
72
|
+
|
|
73
|
+
<Form store={form} resetOnSubmit={false}>
|
|
74
|
+
<LetterheadTextField
|
|
75
|
+
name={form.names.email}
|
|
76
|
+
label="Email"
|
|
77
|
+
type="email"
|
|
78
|
+
placeholder={placeholders.email}
|
|
79
|
+
required
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<LetterheadFormActions>
|
|
83
|
+
<LetterheadSubmitError name={submitName} />
|
|
84
|
+
<LetterheadSubmitButton>Continue</LetterheadSubmitButton>
|
|
85
|
+
|
|
86
|
+
<LetterheadParagraph align="start">
|
|
87
|
+
{"Remembered your password? "}
|
|
88
|
+
<Link
|
|
89
|
+
href={hrefs.login()}
|
|
90
|
+
className={interactiveText}
|
|
91
|
+
state={{ emailValue }}
|
|
92
|
+
>
|
|
93
|
+
Log in
|
|
94
|
+
</Link>
|
|
95
|
+
{"."}
|
|
96
|
+
</LetterheadParagraph>
|
|
97
|
+
</LetterheadFormActions>
|
|
98
|
+
</Form>
|
|
99
|
+
</Letterhead>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function SubmitCodeStep(props: {
|
|
104
|
+
tokenId: string;
|
|
105
|
+
email: string;
|
|
106
|
+
setStep: SetStep;
|
|
107
|
+
}) {
|
|
108
|
+
const client = useClient();
|
|
109
|
+
const { form, submitName } = useForm({
|
|
110
|
+
defaultValues: {
|
|
111
|
+
code: "",
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async onSubmit({ values }) {
|
|
115
|
+
const op = await client.checkPasswordResetCode({
|
|
116
|
+
...values,
|
|
117
|
+
tokenId: props.tokenId,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return op.mapFailure((failure) => {
|
|
121
|
+
return getSubmitFailureMessage(failure, {
|
|
122
|
+
404: "🚫 This code is incorrect or expired. Please try again.",
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
onSuccess(_, { values }) {
|
|
128
|
+
props.setStep({
|
|
129
|
+
type: "SET_NEW_PASSWORD",
|
|
130
|
+
tokenId: props.tokenId,
|
|
131
|
+
code: values.code,
|
|
132
|
+
email: props.email,
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Letterhead>
|
|
139
|
+
<LetterheadHeader>
|
|
140
|
+
<LetterheadHeading>Submit code</LetterheadHeading>
|
|
141
|
+
<LetterheadParagraph>
|
|
142
|
+
We've sent a one-time code to the email address you have provided.
|
|
143
|
+
Please, enter the code in the field below to continue.
|
|
144
|
+
</LetterheadParagraph>
|
|
145
|
+
</LetterheadHeader>
|
|
146
|
+
|
|
147
|
+
<Form store={form} resetOnSubmit={false}>
|
|
148
|
+
<LetterheadTextField
|
|
149
|
+
name={form.names.code}
|
|
150
|
+
label="Code"
|
|
151
|
+
placeholder="E.g. 123123"
|
|
152
|
+
autoComplete="one-time-code"
|
|
153
|
+
required
|
|
154
|
+
/>
|
|
155
|
+
|
|
156
|
+
<LetterheadFormActions>
|
|
157
|
+
<LetterheadSubmitError name={submitName} />
|
|
158
|
+
<LetterheadSubmitButton>Verify code</LetterheadSubmitButton>
|
|
159
|
+
</LetterheadFormActions>
|
|
160
|
+
</Form>
|
|
161
|
+
</Letterhead>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function SetNewPasswordStep(props: {
|
|
166
|
+
tokenId: string;
|
|
167
|
+
code: string;
|
|
168
|
+
email: string;
|
|
169
|
+
setStep: SetStep;
|
|
170
|
+
}) {
|
|
171
|
+
const client = useClient();
|
|
172
|
+
|
|
173
|
+
const { form, submitName } = useForm({
|
|
174
|
+
defaultValues: { password: "" },
|
|
175
|
+
validate: { password: validPassword },
|
|
176
|
+
async onSubmit({ values }) {
|
|
177
|
+
const res = await client.setNewPassword({
|
|
178
|
+
tokenId: props.tokenId,
|
|
179
|
+
code: props.code,
|
|
180
|
+
password: values.password,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (res.isFailure) {
|
|
184
|
+
return res.mapFailure((failure) => {
|
|
185
|
+
return getSubmitFailureMessage(failure, {
|
|
186
|
+
404: "⏱️ One-time code has expired. Please restart the password reset process.",
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Login attempt with new credentials. Must be performed in onSubmit so
|
|
192
|
+
// that errors are correctly propagated.
|
|
193
|
+
const loginOp = await client.login({
|
|
194
|
+
email: props.email,
|
|
195
|
+
password: values.password,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return loginOp.mapFailure(getSubmitFailureMessage);
|
|
199
|
+
},
|
|
200
|
+
onSuccess() {
|
|
201
|
+
props.setStep({ type: "SUCCESS" });
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<Letterhead>
|
|
207
|
+
<LetterheadHeader>
|
|
208
|
+
<LetterheadHeading>Choose New Password</LetterheadHeading>
|
|
209
|
+
<LetterheadParagraph>
|
|
210
|
+
Please choose a new password. Make it at least 8 characters long.
|
|
211
|
+
</LetterheadParagraph>
|
|
212
|
+
</LetterheadHeader>
|
|
213
|
+
|
|
214
|
+
<Form store={form} resetOnSubmit={false}>
|
|
215
|
+
<LetterheadTextField
|
|
216
|
+
name={form.names.password}
|
|
217
|
+
label="New Password"
|
|
218
|
+
type="password"
|
|
219
|
+
placeholder="Enter a new password"
|
|
220
|
+
required
|
|
221
|
+
/>
|
|
222
|
+
|
|
223
|
+
<LetterheadFormActions>
|
|
224
|
+
<LetterheadSubmitError name={submitName} />
|
|
225
|
+
<LetterheadSubmitButton>Save & Login</LetterheadSubmitButton>
|
|
226
|
+
</LetterheadFormActions>
|
|
227
|
+
</Form>
|
|
228
|
+
</Letterhead>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function SuccessStep() {
|
|
233
|
+
const { hrefs } = useAppConfig();
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<Letterhead>
|
|
237
|
+
<LetterheadHeader>
|
|
238
|
+
<LetterheadHeading>Success!</LetterheadHeading>
|
|
239
|
+
<LetterheadParagraph>
|
|
240
|
+
Your password has been successfully reset and you've been
|
|
241
|
+
automatically logged in. Yay!
|
|
242
|
+
</LetterheadParagraph>
|
|
243
|
+
</LetterheadHeader>
|
|
244
|
+
|
|
245
|
+
<LetterheadFormActions>
|
|
246
|
+
<Link href={hrefs.dashboard()} {...cx(button())}>
|
|
247
|
+
Go to dashboard
|
|
248
|
+
</Link>
|
|
249
|
+
</LetterheadFormActions>
|
|
250
|
+
</Letterhead>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
type ResetPasswordStep =
|
|
255
|
+
| { type: "REQUEST_PASSWORD_RESET" }
|
|
256
|
+
| { type: "SUBMIT_CODE"; tokenId: string; email: string }
|
|
257
|
+
| { type: "SET_NEW_PASSWORD"; tokenId: string; code: string; email: string }
|
|
258
|
+
| { type: "SUCCESS" };
|
|
259
|
+
|
|
260
|
+
export function PasswordResetCard(props: {
|
|
261
|
+
/**
|
|
262
|
+
* Default values for the initial request password reset step.
|
|
263
|
+
*
|
|
264
|
+
* You might want to provide a value for history state or query param, so that
|
|
265
|
+
* if a user jumps between login and password reset, their email address
|
|
266
|
+
* is maintained between the two locations.
|
|
267
|
+
*/
|
|
268
|
+
defaultValues?: DefaultFormValues;
|
|
269
|
+
}) {
|
|
270
|
+
const [step, setStep] = useState<ResetPasswordStep>({
|
|
271
|
+
type: "REQUEST_PASSWORD_RESET",
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
switch (step.type) {
|
|
275
|
+
case "REQUEST_PASSWORD_RESET": {
|
|
276
|
+
return <RequestPasswordResetStep {...props} setStep={setStep} />;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case "SUBMIT_CODE": {
|
|
280
|
+
return <SubmitCodeStep {...step} setStep={setStep} />;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
case "SET_NEW_PASSWORD": {
|
|
284
|
+
return <SetNewPasswordStep {...step} setStep={setStep} />;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case "SUCCESS": {
|
|
288
|
+
return <SuccessStep />;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Button } from "@ariakit/react";
|
|
2
|
+
import { cx } from "../class-names.ts";
|
|
3
|
+
import {
|
|
4
|
+
Letterhead,
|
|
5
|
+
LetterheadHeading,
|
|
6
|
+
LetterheadParagraph,
|
|
7
|
+
} from "../Letterhead/index.tsx";
|
|
8
|
+
import { LetterheadHeader } from "../LetterheadForm/index.tsx";
|
|
9
|
+
import type { CurrentUser } from "../types.ts";
|
|
10
|
+
import { accountPicker } from "./style.css.ts";
|
|
11
|
+
import type { EventHandler, EventHandlerWithReload } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
export function UserMismatchView(props: {
|
|
14
|
+
serverUser: CurrentUser;
|
|
15
|
+
localUser: CurrentUser;
|
|
16
|
+
onClearLocalContent: EventHandler;
|
|
17
|
+
onServerLogout: EventHandlerWithReload;
|
|
18
|
+
reload: () => void;
|
|
19
|
+
}) {
|
|
20
|
+
const { localUser, serverUser, onClearLocalContent, onServerLogout, reload } =
|
|
21
|
+
props;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Letterhead>
|
|
25
|
+
<LetterheadHeader>
|
|
26
|
+
<LetterheadHeading>User mismatch</LetterheadHeading>
|
|
27
|
+
|
|
28
|
+
<LetterheadParagraph>
|
|
29
|
+
You are logged into Indie Tabletop Club as <em>{serverUser.email}</em>
|
|
30
|
+
, but locally stored data belong to <em>{localUser.email}</em>.
|
|
31
|
+
</LetterheadParagraph>
|
|
32
|
+
|
|
33
|
+
<LetterheadParagraph>
|
|
34
|
+
Which account do you want to use?
|
|
35
|
+
</LetterheadParagraph>
|
|
36
|
+
</LetterheadHeader>
|
|
37
|
+
|
|
38
|
+
<div {...cx(accountPicker.container)}>
|
|
39
|
+
<Button
|
|
40
|
+
{...cx(accountPicker.button)}
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={() => onClearLocalContent()}
|
|
43
|
+
>
|
|
44
|
+
<div {...cx(accountPicker.buttonLabel)}>{serverUser.email}</div>
|
|
45
|
+
<div>Local data will be deleted.</div>
|
|
46
|
+
</Button>
|
|
47
|
+
|
|
48
|
+
<div {...cx(accountPicker.divider)} />
|
|
49
|
+
|
|
50
|
+
<Button
|
|
51
|
+
{...cx(accountPicker.button)}
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => onServerLogout({ reload })}
|
|
54
|
+
>
|
|
55
|
+
<div {...cx(accountPicker.buttonLabel)}>{localUser.email}</div>
|
|
56
|
+
<div>You will be asked to log in again.</div>
|
|
57
|
+
</Button>
|
|
58
|
+
</div>
|
|
59
|
+
</Letterhead>
|
|
60
|
+
);
|
|
61
|
+
}
|