@indietabletop/appkit 5.0.0 → 5.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.
@@ -1,6 +1,6 @@
1
1
  import { Form, useStoreState } from "@ariakit/react";
2
2
  import { type Dispatch, type SetStateAction, useState } from "react";
3
- import { Link } from "wouter";
3
+ import { Link, useLocation } from "wouter";
4
4
  import { useAppConfig, useClient } from "../AppConfig/AppConfig.tsx";
5
5
  import { cx } from "../class-names.ts";
6
6
  import { interactiveText } from "../common.css.ts";
@@ -28,13 +28,16 @@ import { LoadingView } from "./LoadingView.tsx";
28
28
  import { NoConnectionView } from "./NoConnectionView.tsx";
29
29
  import type { DefaultFormValues } from "./types.ts";
30
30
  import { useFetchCurrentUser } from "./useFetchCurrentUser.tsx";
31
+ import { useRedirectPath } from "./useRedirectPath.ts";
31
32
 
32
33
  type SetStep = Dispatch<SetStateAction<JoinStep>>;
33
34
 
34
- function InitialStep(props: {
35
+ type InitialStepProps = {
35
36
  setStep: SetStep;
36
37
  defaultValues?: DefaultFormValues;
37
- }) {
38
+ };
39
+
40
+ function InitialStep(props: InitialStepProps) {
38
41
  const { client, placeholders, hrefs } = useAppConfig();
39
42
  const { defaultValues, setStep } = props;
40
43
 
@@ -152,13 +155,22 @@ function InitialStep(props: {
152
155
  );
153
156
  }
154
157
 
155
- function SubmitCodeStep(props: {
158
+ type SubmitCodeStepProps = {
156
159
  tokenId: string;
157
160
  setStep: SetStep;
158
161
  email: string;
159
- }) {
162
+ };
163
+
164
+ function SubmitCodeStep(props: SubmitCodeStepProps) {
160
165
  const { tokenId, email, setStep } = props;
166
+ const [_, navigate] = useLocation();
161
167
  const client = useClient();
168
+ const redirectPath = useRedirectPath();
169
+
170
+ // Use redirect path if found in search params, otherwise go the success step.
171
+ const onSuccess = redirectPath
172
+ ? () => navigate(redirectPath)
173
+ : () => setStep({ type: "SUCCESS" });
162
174
 
163
175
  const { form, submitName } = useForm({
164
176
  defaultValues: {
@@ -173,9 +185,7 @@ function SubmitCodeStep(props: {
173
185
  });
174
186
  });
175
187
  },
176
- onSuccess() {
177
- setStep({ type: "SUCCESS" });
178
- },
188
+ onSuccess,
179
189
  });
180
190
 
181
191
  return (
@@ -229,7 +239,11 @@ type JoinStep =
229
239
  | { type: "SUBMIT_CODE"; tokenId: string; email: string }
230
240
  | { type: "SUCCESS" };
231
241
 
232
- function JoinFlow(props: { defaultValues?: DefaultFormValues }) {
242
+ type JoinFlowProps = {
243
+ defaultValues?: DefaultFormValues;
244
+ };
245
+
246
+ function JoinFlow(props: JoinFlowProps) {
233
247
  const [step, setStep] = useState<JoinStep>({ type: "INITIAL" });
234
248
 
235
249
  switch (step.type) {
@@ -253,6 +267,12 @@ export type JoinCardProps = {
253
267
 
254
268
  /**
255
269
  * Allows the user to join Indie Tabletop Club.
270
+ *
271
+ * Will automatically use the `redirectTo` query param value as the redirect
272
+ * location once the user creates and verifies their account.
273
+ *
274
+ * Otherwise a success screen will be shown and the user will be directed to
275
+ * the dashboard.
256
276
  */
257
277
  export function JoinCard(props: JoinCardProps) {
258
278
  const { result, latestAttemptTs, reload } = useFetchCurrentUser();
@@ -33,14 +33,21 @@ export type LoginCardProps = {
33
33
  /**
34
34
  * Called when the login action succeeds.
35
35
  *
36
- * Typically you want to redirect the user at this point, and possibly
37
- * run additional side-effects.
36
+ * If no handler is provided, the component will will navigate to the
37
+ * dashboard (as determined by `appConfig.hrefs.dashboard()` return value),
38
+ * or a location provided in `redirectTo` query param if it is a valid
39
+ * local path.
38
40
  */
39
- onLogin: AuthEventHandler;
41
+ onLogin?: AuthEventHandler;
40
42
  };
41
43
 
42
44
  /**
43
45
  * Allows the user to log into Indie Tabletop Club.
46
+ *
47
+ * Will automatically use the `redirectTo` query param value as the redirect
48
+ * location after successful login.
49
+ *
50
+ * Otherwise, the user will be redirected to the app's dashboard.
44
51
  */
45
52
  export function LoginCard(props: LoginCardProps) {
46
53
  const currentUser = useCurrentUser();
@@ -1,6 +1,6 @@
1
1
  import { Button, Form, useStoreState } from "@ariakit/react";
2
2
  import type { ReactNode } from "react";
3
- import { Link } from "wouter";
3
+ import { Link, useLocation } from "wouter";
4
4
  import { useAppConfig } from "../AppConfig/AppConfig.tsx";
5
5
  import { interactiveText } from "../common.css.ts";
6
6
  import { getSubmitFailureMessage } from "../failureMessages.ts";
@@ -22,10 +22,11 @@ import type { CurrentUser } from "../types.ts";
22
22
  import { useForm } from "../use-form.ts";
23
23
  import { validEmail } from "../validations.ts";
24
24
  import type { AuthEventHandler, DefaultFormValues } from "./types.ts";
25
+ import { useRedirectPath } from "./useRedirectPath.ts";
25
26
 
26
27
  export function LoginView(props: {
27
28
  defaultValues?: DefaultFormValues;
28
- onLogin: AuthEventHandler;
29
+ onLogin?: AuthEventHandler;
29
30
  currentUser: CurrentUser | null;
30
31
  description: ReactNode;
31
32
  reload: () => void;
@@ -33,8 +34,11 @@ export function LoginView(props: {
33
34
  const { currentUser, defaultValues, description, onLogin, reload } = props;
34
35
  const { placeholders, client, hrefs } = useAppConfig();
35
36
  const { logout } = useAppActions();
37
+
36
38
  const localUserPresent = !!currentUser?.email;
37
39
  const defaultEmailValue = currentUser?.email ?? defaultValues?.email ?? "";
40
+ const redirectPath = useRedirectPath() ?? hrefs.dashboard();
41
+ const [_, navigate] = useLocation();
38
42
 
39
43
  const { form, submitName } = useForm({
40
44
  defaultValues: { email: defaultEmailValue, password: "" },
@@ -50,7 +54,15 @@ export function LoginView(props: {
50
54
  });
51
55
  },
52
56
  onSuccess() {
53
- onLogin();
57
+ if (onLogin) {
58
+ onLogin();
59
+ }
60
+
61
+ // If a custom onLogin handler is not provided, will automatically
62
+ // navigate to the appropriate redirect path.
63
+ else {
64
+ navigate(redirectPath);
65
+ }
54
66
  },
55
67
  });
56
68
 
@@ -0,0 +1,21 @@
1
+ import { useSearchParams } from "wouter";
2
+
3
+ function isValidRedirect(value: string | null | undefined): value is string {
4
+ return !!(value && /^[~/]/.test(value));
5
+ }
6
+
7
+ /**
8
+ * Gets redirect path from query params and validates it.
9
+ *
10
+ * Returns `null` if no path is found, or it is invalid.
11
+ */
12
+ export function useRedirectPath() {
13
+ const [params] = useSearchParams();
14
+ const redirectTo = params.get("redirectTo");
15
+
16
+ if (isValidRedirect(redirectTo)) {
17
+ return redirectTo;
18
+ }
19
+
20
+ return null;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indietabletop/appkit",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "A collection of modules used in apps built by Indie Tabletop Club",
5
5
  "private": false,
6
6
  "type": "module",