@indietabletop/appkit 4.0.0 → 4.1.0
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/ModernIDB/ModernIDB.ts +2 -2
- package/lib/ModernIDB/types.ts +4 -0
- package/lib/SubscribeCard/SubscribeByEmailCard.stories.tsx +0 -6
- package/lib/account/AccountIssueView.tsx +11 -7
- package/lib/account/AlreadyLoggedInView.tsx +8 -5
- package/lib/account/CurrentUserFetcher.stories.tsx +0 -28
- package/lib/account/CurrentUserFetcher.tsx +11 -32
- package/lib/account/JoinCard.stories.tsx +0 -2
- package/lib/account/JoinCard.tsx +4 -11
- package/lib/account/LoginCard.stories.tsx +0 -23
- package/lib/account/LoginCard.tsx +11 -59
- package/lib/account/LoginView.tsx +10 -7
- package/lib/account/UserMismatchView.tsx +8 -7
- package/lib/account/VerifyPage.tsx +10 -7
- package/lib/account/types.ts +5 -3
- package/lib/account/{useCurrentUserResult.tsx → useFetchCurrentUser.tsx} +1 -1
- package/lib/async-op.ts +32 -0
- package/lib/client.ts +213 -43
- package/lib/createSafeStorage.ts +91 -0
- package/lib/index.ts +5 -0
- package/lib/store/index.tsx +227 -0
- package/lib/store/store.ts +453 -0
- package/lib/store/types.ts +45 -0
- package/lib/store/utils.ts +46 -0
- package/lib/types/spacegits.ts +108 -0
- package/package.json +6 -3
|
@@ -5,6 +5,7 @@ import { ModernIDBError } from "./ModernIDBError.ts";
|
|
|
5
5
|
import { ObjectStore } from "./ObjectStore.ts";
|
|
6
6
|
import type {
|
|
7
7
|
BlockingHandler,
|
|
8
|
+
ModernIDBIndexes,
|
|
8
9
|
ModernIDBSchema,
|
|
9
10
|
ModernIDBState,
|
|
10
11
|
OpenRequestHandlers,
|
|
@@ -36,7 +37,7 @@ type DatabaseProps = {
|
|
|
36
37
|
|
|
37
38
|
export class ModernIDB<
|
|
38
39
|
Schema extends ModernIDBSchema,
|
|
39
|
-
IndexNames extends
|
|
40
|
+
IndexNames extends ModernIDBIndexes<Schema>,
|
|
40
41
|
> {
|
|
41
42
|
readonly name: string;
|
|
42
43
|
readonly version: number;
|
|
@@ -110,7 +111,6 @@ export class ModernIDB<
|
|
|
110
111
|
if (this.state !== "closed") {
|
|
111
112
|
throw new ModernIDBError(
|
|
112
113
|
"InvalidConnectionStateError",
|
|
113
|
-
|
|
114
114
|
`Cannot open connection to database '${this.name}'. Instance must ` +
|
|
115
115
|
`be in a 'closed' state in order to be opened, but current ` +
|
|
116
116
|
`state is '${this.state}'.`,
|
package/lib/ModernIDB/types.ts
CHANGED
|
@@ -13,6 +13,10 @@ export type ModernIDBSchema = {
|
|
|
13
13
|
[key: string]: unknown;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export type ModernIDBIndexes<Schema extends ModernIDBSchema> = {
|
|
17
|
+
[K in keyof Schema]?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
16
20
|
export type VersionChangeHandler<
|
|
17
21
|
Schema extends ModernIDBSchema,
|
|
18
22
|
IndexNames extends {
|
|
@@ -4,12 +4,6 @@ import { LetterheadParagraph } from "../Letterhead/index.tsx";
|
|
|
4
4
|
import { sleep } from "../sleep.ts";
|
|
5
5
|
import { SubscribeByEmailCard } from "./SubscribeByEmailCard.tsx";
|
|
6
6
|
|
|
7
|
-
const pledge = {
|
|
8
|
-
id: "1",
|
|
9
|
-
email: "test@example.com",
|
|
10
|
-
contactSubscribed: false,
|
|
11
|
-
};
|
|
12
|
-
|
|
13
7
|
const handlers = {
|
|
14
8
|
subscribe() {
|
|
15
9
|
return http.post(
|
|
@@ -6,13 +6,11 @@ import {
|
|
|
6
6
|
LetterheadHeading,
|
|
7
7
|
LetterheadParagraph,
|
|
8
8
|
} from "../Letterhead/index.tsx";
|
|
9
|
-
import
|
|
9
|
+
import { useAppActions } from "../store/index.tsx";
|
|
10
10
|
|
|
11
|
-
export function AccountIssueView(props: {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}) {
|
|
15
|
-
const { onLogout, reload } = props;
|
|
11
|
+
export function AccountIssueView(props: { reload: () => void }) {
|
|
12
|
+
const { reload } = props;
|
|
13
|
+
const { logout } = useAppActions();
|
|
16
14
|
|
|
17
15
|
return (
|
|
18
16
|
<Letterhead>
|
|
@@ -25,7 +23,13 @@ export function AccountIssueView(props: {
|
|
|
25
23
|
|
|
26
24
|
<LetterheadParagraph>
|
|
27
25
|
{"You can try "}
|
|
28
|
-
<Button
|
|
26
|
+
<Button
|
|
27
|
+
{...cx(interactiveText)}
|
|
28
|
+
onClick={async () => {
|
|
29
|
+
await logout();
|
|
30
|
+
reload();
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
29
33
|
logging out
|
|
30
34
|
</Button>
|
|
31
35
|
{" and in again. "}
|
|
@@ -7,16 +7,16 @@ import {
|
|
|
7
7
|
LetterheadHeading,
|
|
8
8
|
LetterheadParagraph,
|
|
9
9
|
} from "../Letterhead/index.tsx";
|
|
10
|
+
import { useAppActions } from "../store/index.tsx";
|
|
10
11
|
import type { CurrentUser } from "../types.ts";
|
|
11
|
-
import type { EventHandlerWithReload } from "./types.ts";
|
|
12
12
|
|
|
13
13
|
export function AlreadyLoggedInView(props: {
|
|
14
14
|
currentUser: CurrentUser;
|
|
15
|
-
onLogout: EventHandlerWithReload;
|
|
16
15
|
reload: () => void;
|
|
17
16
|
}) {
|
|
17
|
+
const { currentUser, reload } = props;
|
|
18
18
|
const { hrefs } = useAppConfig();
|
|
19
|
-
const {
|
|
19
|
+
const { logout } = useAppActions();
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
22
|
<Letterhead>
|
|
@@ -30,10 +30,13 @@ export function AlreadyLoggedInView(props: {
|
|
|
30
30
|
<Link className={interactiveText} href={hrefs.dashboard()}>
|
|
31
31
|
Continue
|
|
32
32
|
</Link>
|
|
33
|
-
{` as current user, or `}
|
|
33
|
+
{` as the current user, or `}
|
|
34
34
|
<Button
|
|
35
35
|
className={interactiveText}
|
|
36
|
-
onClick={() =>
|
|
36
|
+
onClick={async () => {
|
|
37
|
+
await logout();
|
|
38
|
+
reload();
|
|
39
|
+
}}
|
|
37
40
|
>
|
|
38
41
|
log out
|
|
39
42
|
</Button>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Story } from "@storybook/addon-docs/blocks";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
3
|
import { http, HttpResponse } from "msw";
|
|
4
|
-
import { fn } from "storybook/test";
|
|
5
4
|
import { sleep } from "../sleep.ts";
|
|
6
5
|
import type { CurrentUser, SessionInfo } from "../types.ts";
|
|
7
6
|
import { CurrentUserFetcher } from "./CurrentUserFetcher.tsx";
|
|
@@ -168,11 +167,6 @@ const meta = {
|
|
|
168
167
|
component: CurrentUserFetcher,
|
|
169
168
|
tags: ["autodocs"],
|
|
170
169
|
args: {
|
|
171
|
-
localUser: null,
|
|
172
|
-
onLogout: fn(),
|
|
173
|
-
onClearLocalContent: fn(),
|
|
174
|
-
onLogin: fn(),
|
|
175
|
-
onServerLogout: fn(),
|
|
176
170
|
children: (
|
|
177
171
|
<div
|
|
178
172
|
style={{
|
|
@@ -207,9 +201,6 @@ type Story = StoryObj<typeof meta>;
|
|
|
207
201
|
* the local user (determined by the user id).
|
|
208
202
|
*/
|
|
209
203
|
export const Default: Story = {
|
|
210
|
-
args: {
|
|
211
|
-
localUser: data.john,
|
|
212
|
-
},
|
|
213
204
|
parameters: {
|
|
214
205
|
msw: {
|
|
215
206
|
handlers: {
|
|
@@ -230,9 +221,6 @@ export const NoLocalUser: Story = {};
|
|
|
230
221
|
* error 401. The user should supply their credentials again.
|
|
231
222
|
*/
|
|
232
223
|
export const SessionExpired: Story = {
|
|
233
|
-
args: {
|
|
234
|
-
localUser: data.john,
|
|
235
|
-
},
|
|
236
224
|
parameters: {
|
|
237
225
|
msw: {
|
|
238
226
|
handlers: {
|
|
@@ -254,10 +242,6 @@ export const SessionExpired: Story = {
|
|
|
254
242
|
* if not handled correctly.
|
|
255
243
|
*/
|
|
256
244
|
export const UserMismatch: Story = {
|
|
257
|
-
args: {
|
|
258
|
-
localUser: data.john,
|
|
259
|
-
},
|
|
260
|
-
|
|
261
245
|
parameters: {
|
|
262
246
|
msw: {
|
|
263
247
|
handlers: {
|
|
@@ -270,10 +254,6 @@ export const UserMismatch: Story = {
|
|
|
270
254
|
/**
|
|
271
255
|
*/
|
|
272
256
|
export const UserUnverified: Story = {
|
|
273
|
-
args: {
|
|
274
|
-
localUser: data.vernon,
|
|
275
|
-
},
|
|
276
|
-
|
|
277
257
|
parameters: {
|
|
278
258
|
msw: {
|
|
279
259
|
handlers: {
|
|
@@ -293,10 +273,6 @@ export const UserUnverified: Story = {
|
|
|
293
273
|
* This can happen if users delete their accounts but
|
|
294
274
|
*/
|
|
295
275
|
export const UserNotFound: Story = {
|
|
296
|
-
args: {
|
|
297
|
-
localUser: data.john,
|
|
298
|
-
},
|
|
299
|
-
|
|
300
276
|
parameters: {
|
|
301
277
|
msw: {
|
|
302
278
|
handlers: {
|
|
@@ -311,10 +287,6 @@ export const UserNotFound: Story = {
|
|
|
311
287
|
* carry any special meaning.
|
|
312
288
|
*/
|
|
313
289
|
export const UnknownFailure: Story = {
|
|
314
|
-
args: {
|
|
315
|
-
localUser: data.john,
|
|
316
|
-
},
|
|
317
|
-
|
|
318
290
|
parameters: {
|
|
319
291
|
msw: {
|
|
320
292
|
handlers: {
|
|
@@ -1,30 +1,18 @@
|
|
|
1
1
|
import { useState, type ReactNode } from "react";
|
|
2
2
|
import { ModalDialog } from "../ModalDialog/index.tsx";
|
|
3
|
-
import
|
|
3
|
+
import { useCurrentUser } from "../store/index.tsx";
|
|
4
4
|
import { AccountIssueView } from "./AccountIssueView.tsx";
|
|
5
5
|
import { LoginView } from "./LoginView.tsx";
|
|
6
|
-
import
|
|
7
|
-
import { useCurrentUserResult } from "./useCurrentUserResult.tsx";
|
|
6
|
+
import { useFetchCurrentUser } from "./useFetchCurrentUser.tsx";
|
|
8
7
|
import { UserMismatchView } from "./UserMismatchView.tsx";
|
|
9
8
|
import { VerifyAccountView } from "./VerifyPage.tsx";
|
|
10
9
|
|
|
11
10
|
type CurrentUserFetcherProps = {
|
|
12
|
-
/**
|
|
13
|
-
* Current user as stored in persistent storage.
|
|
14
|
-
*
|
|
15
|
-
* If this property is set, the component will attempt to fetch latest data
|
|
16
|
-
* from the server and store it (via ITC client).
|
|
17
|
-
*/
|
|
18
|
-
localUser: CurrentUser | null;
|
|
19
|
-
onLogin: EventHandlerWithReload;
|
|
20
|
-
onClearLocalContent: EventHandler;
|
|
21
|
-
onLogout: EventHandlerWithReload;
|
|
22
|
-
onServerLogout: EventHandlerWithReload;
|
|
23
11
|
children: ReactNode;
|
|
24
12
|
};
|
|
25
13
|
|
|
26
14
|
/**
|
|
27
|
-
* Fetches fresh current user data if local
|
|
15
|
+
* Fetches fresh current user data if a local user exists in the app store.
|
|
28
16
|
*
|
|
29
17
|
* This component uses the Indie Tabletop Client under the hood, so if new
|
|
30
18
|
* data is successfully fetched, the onCurrentUser callback will be invoked,
|
|
@@ -34,22 +22,17 @@ type CurrentUserFetcherProps = {
|
|
|
34
22
|
* that we could run into: expired session, user mismatch and account deletion.
|
|
35
23
|
*
|
|
36
24
|
* All other errors are ignored. This allows users to use the app in offline
|
|
37
|
-
*
|
|
25
|
+
* mode, and doesn't interrupt their session if some unexpected error happens,
|
|
38
26
|
* which they cannot do anything about anyways.
|
|
39
27
|
*/
|
|
40
28
|
export function CurrentUserFetcher(props: CurrentUserFetcherProps) {
|
|
41
|
-
const {
|
|
42
|
-
|
|
43
|
-
children,
|
|
44
|
-
onLogin,
|
|
45
|
-
onLogout,
|
|
46
|
-
onClearLocalContent,
|
|
47
|
-
onServerLogout,
|
|
48
|
-
} = props;
|
|
29
|
+
const { children } = props;
|
|
30
|
+
const localUser = useCurrentUser();
|
|
49
31
|
const [isOpen, setOpen] = useState(true);
|
|
50
32
|
|
|
51
|
-
const { result, reload } =
|
|
52
|
-
// We only want to fetch the current user if they exist in
|
|
33
|
+
const { result, reload } = useFetchCurrentUser({
|
|
34
|
+
// We only want to fetch the current user if they exist in the store, i.e.
|
|
35
|
+
// they have logged into the app previously.
|
|
53
36
|
performFetch: !!localUser,
|
|
54
37
|
});
|
|
55
38
|
|
|
@@ -62,8 +45,7 @@ export function CurrentUserFetcher(props: CurrentUserFetcherProps) {
|
|
|
62
45
|
<ModalDialog size="large" open>
|
|
63
46
|
<LoginView
|
|
64
47
|
currentUser={localUser}
|
|
65
|
-
onLogin={
|
|
66
|
-
onLogout={onLogout}
|
|
48
|
+
onLogin={() => reload()}
|
|
67
49
|
description={undefined}
|
|
68
50
|
reload={reload}
|
|
69
51
|
/>
|
|
@@ -80,7 +62,7 @@ export function CurrentUserFetcher(props: CurrentUserFetcherProps) {
|
|
|
80
62
|
return (
|
|
81
63
|
<>
|
|
82
64
|
<ModalDialog size="large" open>
|
|
83
|
-
<AccountIssueView
|
|
65
|
+
<AccountIssueView reload={reload} />
|
|
84
66
|
</ModalDialog>
|
|
85
67
|
|
|
86
68
|
{children}
|
|
@@ -101,8 +83,6 @@ export function CurrentUserFetcher(props: CurrentUserFetcherProps) {
|
|
|
101
83
|
<UserMismatchView
|
|
102
84
|
serverUser={result.value}
|
|
103
85
|
localUser={localUser}
|
|
104
|
-
onClearLocalContent={onClearLocalContent}
|
|
105
|
-
onServerLogout={onServerLogout}
|
|
106
86
|
reload={reload}
|
|
107
87
|
/>
|
|
108
88
|
</ModalDialog>
|
|
@@ -119,7 +99,6 @@ export function CurrentUserFetcher(props: CurrentUserFetcherProps) {
|
|
|
119
99
|
<VerifyAccountView
|
|
120
100
|
currentUser={serverUser}
|
|
121
101
|
onClose={() => setOpen(false)}
|
|
122
|
-
onLogout={onLogout}
|
|
123
102
|
reload={reload}
|
|
124
103
|
/>
|
|
125
104
|
</ModalDialog>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Story } from "@storybook/addon-docs/blocks";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
3
|
import { http, HttpResponse } from "msw";
|
|
4
|
-
import { fn } from "storybook/test";
|
|
5
4
|
import { sleep } from "../sleep.ts";
|
|
6
5
|
import type { CurrentUser, SessionInfo } from "../types.ts";
|
|
7
6
|
import { JoinCard } from "./JoinCard.tsx";
|
|
@@ -142,7 +141,6 @@ const meta = {
|
|
|
142
141
|
tags: ["autodocs"],
|
|
143
142
|
args: {
|
|
144
143
|
defaultValues: {},
|
|
145
|
-
onLogout: fn(),
|
|
146
144
|
},
|
|
147
145
|
parameters: {
|
|
148
146
|
msw: {
|
package/lib/account/JoinCard.tsx
CHANGED
|
@@ -26,8 +26,8 @@ import { AlreadyLoggedInView } from "./AlreadyLoggedInView.tsx";
|
|
|
26
26
|
import { FailureFallbackView } from "./FailureFallbackView.tsx";
|
|
27
27
|
import { LoadingView } from "./LoadingView.tsx";
|
|
28
28
|
import { NoConnectionView } from "./NoConnectionView.tsx";
|
|
29
|
-
import type { DefaultFormValues
|
|
30
|
-
import {
|
|
29
|
+
import type { DefaultFormValues } from "./types.ts";
|
|
30
|
+
import { useFetchCurrentUser } from "./useFetchCurrentUser.tsx";
|
|
31
31
|
|
|
32
32
|
type SetStep = Dispatch<SetStateAction<JoinStep>>;
|
|
33
33
|
|
|
@@ -248,7 +248,6 @@ function JoinFlow(props: { defaultValues?: DefaultFormValues }) {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
export type JoinCardProps = {
|
|
251
|
-
onLogout: EventHandlerWithReload;
|
|
252
251
|
defaultValues?: DefaultFormValues;
|
|
253
252
|
};
|
|
254
253
|
|
|
@@ -256,17 +255,11 @@ export type JoinCardProps = {
|
|
|
256
255
|
* Allows the user to join Indie Tabletop Club.
|
|
257
256
|
*/
|
|
258
257
|
export function JoinCard(props: JoinCardProps) {
|
|
259
|
-
const { result, latestAttemptTs, reload } =
|
|
258
|
+
const { result, latestAttemptTs, reload } = useFetchCurrentUser();
|
|
260
259
|
|
|
261
260
|
return result.unpack(
|
|
262
261
|
(currentUser) => {
|
|
263
|
-
return
|
|
264
|
-
<AlreadyLoggedInView
|
|
265
|
-
{...props}
|
|
266
|
-
currentUser={currentUser}
|
|
267
|
-
reload={reload}
|
|
268
|
-
/>
|
|
269
|
-
);
|
|
262
|
+
return <AlreadyLoggedInView currentUser={currentUser} reload={reload} />;
|
|
270
263
|
},
|
|
271
264
|
(failure) => {
|
|
272
265
|
if (failure.type === "API_ERROR" && failure.code === 401) {
|
|
@@ -128,13 +128,9 @@ const meta = {
|
|
|
128
128
|
component: LoginCard,
|
|
129
129
|
tags: ["autodocs"],
|
|
130
130
|
args: {
|
|
131
|
-
currentUser: null,
|
|
132
131
|
description: "Log in to Indie Tabletop Club to enable backup & sync.",
|
|
133
132
|
defaultValues: {},
|
|
134
133
|
onLogin: fn(),
|
|
135
|
-
onLogout: fn(),
|
|
136
|
-
onClearLocalContent: fn(),
|
|
137
|
-
onServerLogout: fn(),
|
|
138
134
|
},
|
|
139
135
|
parameters: {
|
|
140
136
|
msw: {
|
|
@@ -169,9 +165,6 @@ export const Default: Story = {
|
|
|
169
165
|
* attempting to create a new session.
|
|
170
166
|
*/
|
|
171
167
|
export const InvalidCredentialsOnSubmit: Story = {
|
|
172
|
-
args: {
|
|
173
|
-
currentUser: null,
|
|
174
|
-
},
|
|
175
168
|
parameters: {
|
|
176
169
|
msw: {
|
|
177
170
|
handlers: {
|
|
@@ -187,9 +180,6 @@ export const InvalidCredentialsOnSubmit: Story = {
|
|
|
187
180
|
* email is used that is not currently in the database.
|
|
188
181
|
*/
|
|
189
182
|
export const UserNotFoundOnSubmit: Story = {
|
|
190
|
-
args: {
|
|
191
|
-
currentUser: null,
|
|
192
|
-
},
|
|
193
183
|
parameters: {
|
|
194
184
|
msw: {
|
|
195
185
|
handlers: {
|
|
@@ -205,9 +195,6 @@ export const UserNotFoundOnSubmit: Story = {
|
|
|
205
195
|
* that doesn't have a specific meaning in the context of the login page.
|
|
206
196
|
*/
|
|
207
197
|
export const UnknownFailureOnSubmit: Story = {
|
|
208
|
-
args: {
|
|
209
|
-
currentUser: null,
|
|
210
|
-
},
|
|
211
198
|
parameters: {
|
|
212
199
|
msw: {
|
|
213
200
|
handlers: {
|
|
@@ -222,9 +209,6 @@ export const UnknownFailureOnSubmit: Story = {
|
|
|
222
209
|
* A case when user is stored locally, but their server session has expired.
|
|
223
210
|
*/
|
|
224
211
|
export const ReauthenticateSession: Story = {
|
|
225
|
-
args: {
|
|
226
|
-
currentUser: data.john,
|
|
227
|
-
},
|
|
228
212
|
parameters: {
|
|
229
213
|
msw: {
|
|
230
214
|
handlers: {
|
|
@@ -242,9 +226,6 @@ export const ReauthenticateSession: Story = {
|
|
|
242
226
|
* The user is directed to app without any further steps.
|
|
243
227
|
*/
|
|
244
228
|
export const AlreadyLoggedIn: Story = {
|
|
245
|
-
args: {
|
|
246
|
-
currentUser: data.john,
|
|
247
|
-
},
|
|
248
229
|
parameters: {
|
|
249
230
|
msw: {
|
|
250
231
|
handlers: {
|
|
@@ -260,10 +241,6 @@ export const AlreadyLoggedIn: Story = {
|
|
|
260
241
|
* different credentials.
|
|
261
242
|
*/
|
|
262
243
|
export const UserMismatch: Story = {
|
|
263
|
-
args: {
|
|
264
|
-
currentUser: data.john,
|
|
265
|
-
},
|
|
266
|
-
|
|
267
244
|
parameters: {
|
|
268
245
|
msw: {
|
|
269
246
|
handlers: {
|
|
@@ -1,29 +1,16 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import
|
|
2
|
+
import { useCurrentUser } from "../store/index.tsx";
|
|
3
3
|
import { AccountIssueView } from "./AccountIssueView.tsx";
|
|
4
4
|
import { AlreadyLoggedInView } from "./AlreadyLoggedInView.tsx";
|
|
5
5
|
import { FailureFallbackView } from "./FailureFallbackView.tsx";
|
|
6
6
|
import { LoadingView } from "./LoadingView.tsx";
|
|
7
7
|
import { LoginView } from "./LoginView.tsx";
|
|
8
8
|
import { NoConnectionView } from "./NoConnectionView.tsx";
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
EventHandler,
|
|
12
|
-
EventHandlerWithReload,
|
|
13
|
-
} from "./types.ts";
|
|
14
|
-
import { useCurrentUserResult } from "./useCurrentUserResult.tsx";
|
|
9
|
+
import type { AuthEventHandler, DefaultFormValues } from "./types.ts";
|
|
10
|
+
import { useFetchCurrentUser } from "./useFetchCurrentUser.tsx";
|
|
15
11
|
import { UserMismatchView } from "./UserMismatchView.tsx";
|
|
16
12
|
|
|
17
13
|
export type LoginCardProps = {
|
|
18
|
-
/**
|
|
19
|
-
* Any user data that might currently be stored in persistent storage like
|
|
20
|
-
* `localStorage` or IndexedDB.
|
|
21
|
-
*
|
|
22
|
-
* If the app contains any local data, it is important that this value is
|
|
23
|
-
* provided so that we don't run into strange user mismatch issues!
|
|
24
|
-
*/
|
|
25
|
-
currentUser: CurrentUser | null;
|
|
26
|
-
|
|
27
14
|
/**
|
|
28
15
|
* A description that will appear in the default login case (i.e. when an
|
|
29
16
|
* unauthenticated user is prompted to log in).
|
|
@@ -48,53 +35,22 @@ export type LoginCardProps = {
|
|
|
48
35
|
*
|
|
49
36
|
* Typically you want to redirect the user at this point, and possibly
|
|
50
37
|
* run additional side-effects.
|
|
51
|
-
*
|
|
52
|
-
* Note that it is not necessary to save the new current user in any way,
|
|
53
|
-
* as they will already be saved via the client onCurrentUser handler.
|
|
54
|
-
*/
|
|
55
|
-
onLogin: EventHandler;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Called when the user indicates that they would like to log out.
|
|
59
|
-
*
|
|
60
|
-
* Typically, you might want to clear all local data, perform
|
|
61
|
-
* a server logout, and redirect to some sensible new location.
|
|
62
|
-
*/
|
|
63
|
-
onLogout: EventHandlerWithReload;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Called when there is a mismatch between local user data and data returned
|
|
67
|
-
* from the server and the user chooses to use the server account.
|
|
68
|
-
*
|
|
69
|
-
* Local content should be cleared in response to this action, but **not**
|
|
70
|
-
* server logout (as the user is choosing to continue as the user that is
|
|
71
|
-
* currently referenced in the cookie).
|
|
72
|
-
*/
|
|
73
|
-
onClearLocalContent: EventHandler;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Called when there is a mismatch between local user data and data returned
|
|
77
|
-
* from the server and the user chooses to use the local account.
|
|
78
|
-
*
|
|
79
|
-
* A server logout should be performed in response to this event, but local
|
|
80
|
-
* data should not be touched.
|
|
81
38
|
*/
|
|
82
|
-
|
|
39
|
+
onLogin: AuthEventHandler;
|
|
83
40
|
};
|
|
84
41
|
|
|
85
42
|
/**
|
|
86
43
|
* Allows the user to log into Indie Tabletop Club.
|
|
87
44
|
*/
|
|
88
45
|
export function LoginCard(props: LoginCardProps) {
|
|
89
|
-
const
|
|
90
|
-
const { result, latestAttemptTs, reload } =
|
|
46
|
+
const currentUser = useCurrentUser();
|
|
47
|
+
const { result, latestAttemptTs, reload } = useFetchCurrentUser();
|
|
91
48
|
|
|
92
49
|
return result.unpack(
|
|
93
50
|
(serverUser) => {
|
|
94
51
|
if (currentUser && currentUser.id !== serverUser.id) {
|
|
95
52
|
return (
|
|
96
53
|
<UserMismatchView
|
|
97
|
-
{...props}
|
|
98
54
|
serverUser={serverUser}
|
|
99
55
|
localUser={currentUser}
|
|
100
56
|
reload={reload}
|
|
@@ -102,23 +58,19 @@ export function LoginCard(props: LoginCardProps) {
|
|
|
102
58
|
);
|
|
103
59
|
}
|
|
104
60
|
|
|
105
|
-
return
|
|
106
|
-
<AlreadyLoggedInView
|
|
107
|
-
{...props}
|
|
108
|
-
currentUser={serverUser}
|
|
109
|
-
reload={reload}
|
|
110
|
-
/>
|
|
111
|
-
);
|
|
61
|
+
return <AlreadyLoggedInView currentUser={serverUser} reload={reload} />;
|
|
112
62
|
},
|
|
113
63
|
|
|
114
64
|
(failure) => {
|
|
115
65
|
if (failure.type === "API_ERROR") {
|
|
116
66
|
if (failure.code === 401) {
|
|
117
|
-
return
|
|
67
|
+
return (
|
|
68
|
+
<LoginView {...props} currentUser={currentUser} reload={reload} />
|
|
69
|
+
);
|
|
118
70
|
}
|
|
119
71
|
|
|
120
72
|
if (failure.code === 404) {
|
|
121
|
-
return <AccountIssueView
|
|
73
|
+
return <AccountIssueView reload={reload} />;
|
|
122
74
|
}
|
|
123
75
|
}
|
|
124
76
|
|
|
@@ -17,22 +17,22 @@ import {
|
|
|
17
17
|
LetterheadSubmitError,
|
|
18
18
|
LetterheadTextField,
|
|
19
19
|
} from "../LetterheadForm/index.tsx";
|
|
20
|
+
import { useAppActions } from "../store/index.tsx";
|
|
20
21
|
import type { CurrentUser } from "../types.ts";
|
|
21
22
|
import { useForm } from "../use-form.ts";
|
|
22
23
|
import { validEmail } from "../validations.ts";
|
|
23
|
-
import type {
|
|
24
|
+
import type { AuthEventHandler, DefaultFormValues } from "./types.ts";
|
|
24
25
|
|
|
25
26
|
export function LoginView(props: {
|
|
26
27
|
defaultValues?: DefaultFormValues;
|
|
27
|
-
onLogin:
|
|
28
|
-
onLogout: EventHandlerWithReload;
|
|
28
|
+
onLogin: AuthEventHandler;
|
|
29
29
|
currentUser: CurrentUser | null;
|
|
30
30
|
description: ReactNode;
|
|
31
31
|
reload: () => void;
|
|
32
32
|
}) {
|
|
33
|
+
const { currentUser, defaultValues, description, onLogin, reload } = props;
|
|
33
34
|
const { placeholders, client, hrefs } = useAppConfig();
|
|
34
|
-
const {
|
|
35
|
-
props;
|
|
35
|
+
const { logout } = useAppActions();
|
|
36
36
|
const localUserPresent = !!currentUser?.email;
|
|
37
37
|
const defaultEmailValue = currentUser?.email ?? defaultValues?.email ?? "";
|
|
38
38
|
|
|
@@ -50,7 +50,7 @@ export function LoginView(props: {
|
|
|
50
50
|
});
|
|
51
51
|
},
|
|
52
52
|
onSuccess() {
|
|
53
|
-
onLogin(
|
|
53
|
+
onLogin();
|
|
54
54
|
},
|
|
55
55
|
});
|
|
56
56
|
|
|
@@ -72,7 +72,10 @@ export function LoginView(props: {
|
|
|
72
72
|
{"To use a different account, please "}
|
|
73
73
|
<Button
|
|
74
74
|
className={interactiveText}
|
|
75
|
-
onClick={() =>
|
|
75
|
+
onClick={async () => {
|
|
76
|
+
await logout();
|
|
77
|
+
reload();
|
|
78
|
+
}}
|
|
76
79
|
>
|
|
77
80
|
log out
|
|
78
81
|
</Button>
|
|
@@ -6,19 +6,17 @@ import {
|
|
|
6
6
|
LetterheadParagraph,
|
|
7
7
|
} from "../Letterhead/index.tsx";
|
|
8
8
|
import { LetterheadHeader } from "../LetterheadForm/index.tsx";
|
|
9
|
+
import { useAppActions } from "../store/index.tsx";
|
|
9
10
|
import type { CurrentUser } from "../types.ts";
|
|
10
11
|
import { accountPicker } from "./style.css.ts";
|
|
11
|
-
import type { EventHandler, EventHandlerWithReload } from "./types.ts";
|
|
12
12
|
|
|
13
13
|
export function UserMismatchView(props: {
|
|
14
14
|
serverUser: CurrentUser;
|
|
15
15
|
localUser: CurrentUser;
|
|
16
|
-
onClearLocalContent: EventHandler;
|
|
17
|
-
onServerLogout: EventHandlerWithReload;
|
|
18
16
|
reload: () => void;
|
|
19
17
|
}) {
|
|
20
|
-
const { localUser, serverUser,
|
|
21
|
-
|
|
18
|
+
const { localUser, serverUser, reload } = props;
|
|
19
|
+
const { clientLogout, serverLogout } = useAppActions();
|
|
22
20
|
|
|
23
21
|
return (
|
|
24
22
|
<Letterhead>
|
|
@@ -39,7 +37,7 @@ export function UserMismatchView(props: {
|
|
|
39
37
|
<Button
|
|
40
38
|
{...cx(accountPicker.button)}
|
|
41
39
|
type="button"
|
|
42
|
-
onClick={() =>
|
|
40
|
+
onClick={async () => await clientLogout({ serverUser })}
|
|
43
41
|
>
|
|
44
42
|
<div {...cx(accountPicker.buttonLabel)}>{serverUser.email}</div>
|
|
45
43
|
<div>Local data will be deleted.</div>
|
|
@@ -50,7 +48,10 @@ export function UserMismatchView(props: {
|
|
|
50
48
|
<Button
|
|
51
49
|
{...cx(accountPicker.button)}
|
|
52
50
|
type="button"
|
|
53
|
-
onClick={() =>
|
|
51
|
+
onClick={async () => {
|
|
52
|
+
await serverLogout();
|
|
53
|
+
reload();
|
|
54
|
+
}}
|
|
54
55
|
>
|
|
55
56
|
<div {...cx(accountPicker.buttonLabel)}>{localUser.email}</div>
|
|
56
57
|
<div>You will be asked to log in again.</div>
|