create-m5kdev 0.33.0 → 0.35.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.
Files changed (147) hide show
  1. package/dist/src/catalog.cjs +21 -1
  2. package/dist/src/catalog.cjs.map +1 -1
  3. package/dist/src/catalog.d.cts +3 -1
  4. package/dist/src/create.cjs +6 -2
  5. package/dist/src/create.cjs.map +1 -1
  6. package/dist/src/doctor.cjs.map +1 -1
  7. package/dist/src/fs.cjs +10 -5
  8. package/dist/src/fs.cjs.map +1 -1
  9. package/dist/src/prompts.cjs +32 -0
  10. package/dist/src/prompts.cjs.map +1 -1
  11. package/dist/src/prompts.d.cts +7 -1
  12. package/dist/src/template.cjs +23 -4
  13. package/dist/src/template.cjs.map +1 -1
  14. package/dist/src/template.d.cts +17 -3
  15. package/dist/src/types.d.cts +11 -4
  16. package/package.json +4 -1
  17. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +1 -0
  18. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +1 -1
  19. package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +3 -3
  20. package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +3 -2
  21. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
  22. package/templates/minimal-app/.cursor/rules/server-event-emit.mdc +41 -0
  23. package/templates/minimal-app/.dockerignore.tpl +31 -0
  24. package/templates/minimal-app/.gitignore.tpl +1 -0
  25. package/templates/minimal-app/AGENTS.md.tpl +14 -10
  26. package/templates/minimal-app/CLAUDE.md.tpl +133 -131
  27. package/templates/minimal-app/README.md.tpl +51 -19
  28. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
  29. package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +51 -39
  30. package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -38
  31. package/templates/minimal-app/apps/e2e/servers.ts.tpl +136 -112
  32. package/templates/minimal-app/apps/e2e/tests/conversation.spec.ts.tpl +22 -0
  33. package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -47
  34. package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -73
  35. package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -517
  36. package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -94
  37. package/templates/minimal-app/apps/e2e/tests/files.spec.ts.tpl +35 -0
  38. package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -289
  39. package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -516
  40. package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -107
  41. package/templates/minimal-app/apps/e2e/tests/workflow.spec.ts.tpl +26 -0
  42. package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -10
  43. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  44. package/templates/minimal-app/apps/email/src/emails/trialEndingEmail.tsx.tpl +40 -0
  45. package/templates/minimal-app/apps/email/src/index.ts.tpl +7 -0
  46. package/templates/minimal-app/apps/email/src/resources.ts.tpl +72 -65
  47. package/templates/minimal-app/apps/expo/app.json.tpl +36 -36
  48. package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -8
  49. package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -16
  50. package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -31
  51. package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -151
  52. package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -53
  53. package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -8
  54. package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -71
  55. package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -19
  56. package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -85
  57. package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -40
  58. package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -75
  59. package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -193
  60. package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -34
  61. package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -37
  62. package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -10
  63. package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -22
  64. package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -9
  65. package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -11
  66. package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -20
  67. package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -11
  68. package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -1
  69. package/templates/minimal-app/apps/landing/.env.production.example.tpl +7 -0
  70. package/templates/minimal-app/apps/landing/Dockerfile.tpl +39 -0
  71. package/templates/minimal-app/apps/landing/fly.toml.tpl +20 -0
  72. package/templates/minimal-app/apps/landing/index.html.tpl +35 -0
  73. package/templates/minimal-app/apps/landing/package.json.tpl +34 -0
  74. package/templates/minimal-app/apps/landing/src/App.tsx.tpl +12 -0
  75. package/templates/minimal-app/apps/landing/src/LandingPage.tsx.tpl +37 -0
  76. package/templates/minimal-app/apps/landing/src/index.css.tpl +24 -0
  77. package/templates/minimal-app/apps/landing/src/main.tsx.tpl +16 -0
  78. package/templates/minimal-app/apps/landing/src/vite-env.d.ts.tpl +11 -0
  79. package/templates/minimal-app/apps/landing/tsconfig.json.tpl +12 -0
  80. package/templates/minimal-app/apps/landing/vite.config.ts.tpl +25 -0
  81. package/templates/minimal-app/apps/server/AGENTS.md.tpl +55 -46
  82. package/templates/minimal-app/apps/server/{drizzle/seed.e2e.ts.tpl → db.e2e.ts.tpl} +57 -40
  83. package/templates/minimal-app/apps/server/{drizzle/seed.ts.tpl → db.ts.tpl} +27 -20
  84. package/templates/minimal-app/apps/server/drizzle/0003_silly_the_liberteens.sql +27 -0
  85. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +56 -166
  86. package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +1796 -1910
  87. package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +1818 -1932
  88. package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +2137 -0
  89. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +8 -1
  90. package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +3 -33
  91. package/templates/minimal-app/apps/server/package.json.tpl +5 -3
  92. package/templates/minimal-app/apps/server/src/app.ts.tpl +36 -0
  93. package/templates/minimal-app/apps/server/src/modules/conversation/conversation.mastra.ts.tpl +26 -0
  94. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl +51 -0
  95. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl +51 -0
  96. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.trpc.ts.tpl +16 -0
  97. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +28 -28
  98. package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +28 -28
  99. package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +57 -54
  100. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -67
  101. package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +116 -116
  102. package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -37
  103. package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -279
  104. package/templates/minimal-app/apps/server/src/schema.ts.tpl +31 -20
  105. package/templates/minimal-app/apps/server/src/types.ts.tpl +12 -12
  106. package/templates/minimal-app/apps/server/tsconfig.json.tpl +1 -1
  107. package/templates/minimal-app/apps/server/tsup.config.ts.tpl +23 -9
  108. package/templates/minimal-app/apps/shared/.env.example.tpl +5 -0
  109. package/templates/minimal-app/apps/shared/.env.production.example.tpl +23 -0
  110. package/templates/minimal-app/apps/shared/.env.tpl +5 -0
  111. package/templates/minimal-app/apps/shared/Dockerfile.tpl +48 -0
  112. package/templates/minimal-app/apps/shared/fly.toml.tpl +24 -0
  113. package/templates/minimal-app/apps/shared/package.json.tpl +26 -24
  114. package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -28
  115. package/templates/minimal-app/apps/shared/src/modules/conversation/conversation.constants.ts.tpl +1 -0
  116. package/templates/minimal-app/apps/shared/src/modules/demo-workflow/demo-workflow.constants.ts.tpl +1 -0
  117. package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
  118. package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
  119. package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -25
  120. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
  121. package/templates/minimal-app/apps/webapp/public/push-sw.js.tpl +34 -34
  122. package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +49 -44
  123. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +96 -65
  124. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +114 -93
  125. package/templates/minimal-app/apps/webapp/src/StarterServerEventProvider.tsx.tpl +38 -0
  126. package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -15
  127. package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
  128. package/templates/minimal-app/apps/webapp/src/modules/conversation/ConversationRoute.tsx.tpl +36 -0
  129. package/templates/minimal-app/apps/webapp/src/modules/files/FilesRoute.tsx.tpl +88 -0
  130. package/templates/minimal-app/apps/webapp/src/modules/files/hooks/useFilesRoute.ts.tpl +50 -0
  131. package/templates/minimal-app/apps/webapp/src/modules/workflows/WorkflowsRoute.tsx.tpl +97 -0
  132. package/templates/minimal-app/apps/webapp/src/modules/workflows/hooks/useWorkflowsRoute.ts.tpl +64 -0
  133. package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
  134. package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -10
  135. package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
  136. package/templates/minimal-app/apps/webapp/translations/en/app.json.tpl +7 -7
  137. package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +193 -135
  138. package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
  139. package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
  140. package/templates/minimal-app/biome.json.tpl +17 -12
  141. package/templates/minimal-app/package.json.tpl +7 -2
  142. package/templates/minimal-app/pnpm-workspace.yaml.tpl +14 -7
  143. package/templates/minimal-app/template.manifest.json +93 -2
  144. package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +0 -26
  145. package/templates/minimal-app/apps/server/drizzle/guard.ts.tpl +0 -36
  146. package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +0 -40
  147. package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +0 -16
@@ -1,71 +1,71 @@
1
- import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
- import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
- import { Link, useRouter } from "expo-router";
4
- import { Button } from "heroui-native";
5
- import { useState } from "react";
6
- import { Text, View } from "react-native";
7
- import { AuthScaffold } from "../components/AuthScaffold";
8
- import { FormTextField } from "../components/FormTextField";
9
- import { StatusText } from "../components/StatusText";
10
-
11
- export default function LoginScreen() {
12
- const authClient = useAuthClient();
13
- const { registerSession } = useSession();
14
- const router = useRouter();
15
- const [email, setEmail] = useState("");
16
- const [password, setPassword] = useState("");
17
- const [status, setStatus] = useState<string | null>(null);
18
- const [isBusy, setIsBusy] = useState(false);
19
-
20
- const submit = async () => {
21
- setIsBusy(true);
22
- setStatus(null);
23
- try {
24
- const result = await authClient.signIn.email({ email, password });
25
- if (result.error || !result.data?.user) {
26
- setStatus(result.error?.message ?? "Authentication failed");
27
- return;
28
- }
29
- registerSession(() => router.replace("/posts"));
30
- } catch (error) {
31
- setStatus(error instanceof Error ? error.message : "Authentication failed");
32
- } finally {
33
- setIsBusy(false);
34
- }
35
- };
36
-
37
- return (
38
- <AuthScaffold title="Login" description="Sign in to the protected Expo test app.">
39
- <View className="gap-4">
40
- <FormTextField
41
- label="Email"
42
- testID="login-email"
43
- value={email}
44
- onChangeText={setEmail}
45
- keyboardType="email-address"
46
- />
47
- <FormTextField
48
- label="Password"
49
- testID="login-password"
50
- value={password}
51
- onChangeText={setPassword}
52
- secureTextEntry
53
- />
54
- <StatusText testID="auth-error" tone="error">
55
- {status}
56
- </StatusText>
57
- <Button testID="login-submit" onPress={submit} isDisabled={isBusy}>
58
- Login
59
- </Button>
60
- <View className="flex-row justify-between">
61
- <Link href="/signup" asChild>
62
- <Text className="text-primary underline">Sign up</Text>
63
- </Link>
64
- <Link href="/forgot-password" asChild>
65
- <Text className="text-primary underline">Forgot password</Text>
66
- </Link>
67
- </View>
68
- </View>
69
- </AuthScaffold>
70
- );
71
- }
1
+ import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
+ import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
+ import { Link, useRouter } from "expo-router";
4
+ import { Button } from "heroui-native";
5
+ import { useState } from "react";
6
+ import { Text, View } from "react-native";
7
+ import { AuthScaffold } from "../components/AuthScaffold";
8
+ import { FormTextField } from "../components/FormTextField";
9
+ import { StatusText } from "../components/StatusText";
10
+
11
+ export default function LoginScreen() {
12
+ const authClient = useAuthClient();
13
+ const { registerSession } = useSession();
14
+ const router = useRouter();
15
+ const [email, setEmail] = useState("");
16
+ const [password, setPassword] = useState("");
17
+ const [status, setStatus] = useState<string | null>(null);
18
+ const [isBusy, setIsBusy] = useState(false);
19
+
20
+ const submit = async () => {
21
+ setIsBusy(true);
22
+ setStatus(null);
23
+ try {
24
+ const result = await authClient.signIn.email({ email, password });
25
+ if (result.error || !result.data?.user) {
26
+ setStatus(result.error?.message ?? "Authentication failed");
27
+ return;
28
+ }
29
+ registerSession(() => router.replace("/posts"));
30
+ } catch (error) {
31
+ setStatus(error instanceof Error ? error.message : "Authentication failed");
32
+ } finally {
33
+ setIsBusy(false);
34
+ }
35
+ };
36
+
37
+ return (
38
+ <AuthScaffold title="Login" description="Sign in to the protected Expo test app.">
39
+ <View className="gap-4">
40
+ <FormTextField
41
+ label="Email"
42
+ testID="login-email"
43
+ value={email}
44
+ onChangeText={setEmail}
45
+ keyboardType="email-address"
46
+ />
47
+ <FormTextField
48
+ label="Password"
49
+ testID="login-password"
50
+ value={password}
51
+ onChangeText={setPassword}
52
+ secureTextEntry
53
+ />
54
+ <StatusText testID="auth-error" tone="error">
55
+ {status}
56
+ </StatusText>
57
+ <Button testID="login-submit" onPress={submit} isDisabled={isBusy}>
58
+ Login
59
+ </Button>
60
+ <View className="flex-row justify-between">
61
+ <Link href="/signup" asChild>
62
+ <Text className="text-primary underline">Sign up</Text>
63
+ </Link>
64
+ <Link href="/forgot-password" asChild>
65
+ <Text className="text-primary underline">Forgot password</Text>
66
+ </Link>
67
+ </View>
68
+ </View>
69
+ </AuthScaffold>
70
+ );
71
+ }
@@ -1,19 +1,19 @@
1
- import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
- import { Redirect } from "expo-router";
3
- import { useEffect, useState } from "react";
4
- import { LoadingScreen } from "../components/LoadingScreen";
5
-
6
- export default function LogoutScreen() {
7
- const authClient = useAuthClient();
8
- const [done, setDone] = useState(false);
9
-
10
- useEffect(() => {
11
- authClient.signOut().finally(() => setDone(true));
12
- }, [authClient]);
13
-
14
- if (!done) {
15
- return <LoadingScreen />;
16
- }
17
-
18
- return <Redirect href="/login" />;
19
- }
1
+ import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
+ import { Redirect } from "expo-router";
3
+ import { useEffect, useState } from "react";
4
+ import { LoadingScreen } from "../components/LoadingScreen";
5
+
6
+ export default function LogoutScreen() {
7
+ const authClient = useAuthClient();
8
+ const [done, setDone] = useState(false);
9
+
10
+ useEffect(() => {
11
+ authClient.signOut().finally(() => setDone(true));
12
+ }, [authClient]);
13
+
14
+ if (!done) {
15
+ return <LoadingScreen />;
16
+ }
17
+
18
+ return <Redirect href="/login" />;
19
+ }
@@ -1,85 +1,85 @@
1
- import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
- import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
- import { type Href, Redirect, useLocalSearchParams, useRouter } from "expo-router";
4
- import { useEffect, useMemo, useState } from "react";
5
- import { LoadingScreen } from "../../components/LoadingScreen";
6
- import { trpcClient } from "../../lib/trpc";
7
-
8
- function singleParam(value: string | string[] | undefined) {
9
- return Array.isArray(value) ? value[0] : value;
10
- }
11
-
12
- export default function AcceptInvitationScreen() {
13
- const authClient = useAuthClient();
14
- const { data: session, registerSession } = useSession();
15
- const router = useRouter();
16
- const params = useLocalSearchParams<{ id?: string | string[] }>();
17
- const invitationId = singleParam(params.id);
18
- const [errorPath, setErrorPath] = useState<string | null>(null);
19
-
20
- const signupPath = useMemo(() => {
21
- if (!invitationId) return null;
22
- return trpcClient.auth.readInvitation
23
- .query({ id: invitationId })
24
- .then((invitation) => {
25
- const search = new URLSearchParams({
26
- invitation: invitationId,
27
- email: invitation.email,
28
- });
29
- return `/signup?${search.toString()}`;
30
- })
31
- .catch(() => "/signup");
32
- }, [invitationId]);
33
-
34
- useEffect(() => {
35
- let cancelled = false;
36
-
37
- if (!invitationId) {
38
- setErrorPath("/signup");
39
- return;
40
- }
41
-
42
- if (!session) {
43
- signupPath?.then((path) => {
44
- if (!cancelled) router.replace(path as Href);
45
- });
46
- return;
47
- }
48
-
49
- authClient.organization
50
- .acceptInvitation({ invitationId })
51
- .then(async ({ data, error }) => {
52
- if (error) {
53
- throw new Error(error.message ?? "Unable to accept invitation");
54
- }
55
-
56
- const organizationId =
57
- (data as { invitation?: { organizationId?: string } | null } | null)?.invitation
58
- ?.organizationId ?? null;
59
-
60
- if (organizationId) {
61
- const activeResult = await authClient.organization.setActive({ organizationId });
62
- if (activeResult.error) {
63
- throw new Error(activeResult.error.message ?? "Unable to activate organization");
64
- }
65
- }
66
-
67
- if (!cancelled) {
68
- registerSession(() => router.replace("/posts"));
69
- }
70
- })
71
- .catch(() => {
72
- if (!cancelled) setErrorPath("/signup");
73
- });
74
-
75
- return () => {
76
- cancelled = true;
77
- };
78
- }, [authClient, invitationId, registerSession, router, session, signupPath]);
79
-
80
- if (errorPath) {
81
- return <Redirect href={errorPath as Href} />;
82
- }
83
-
84
- return <LoadingScreen />;
85
- }
1
+ import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
+ import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
+ import { type Href, Redirect, useLocalSearchParams, useRouter } from "expo-router";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { LoadingScreen } from "../../components/LoadingScreen";
6
+ import { trpcClient } from "../../lib/trpc";
7
+
8
+ function singleParam(value: string | string[] | undefined) {
9
+ return Array.isArray(value) ? value[0] : value;
10
+ }
11
+
12
+ export default function AcceptInvitationScreen() {
13
+ const authClient = useAuthClient();
14
+ const { data: session, registerSession } = useSession();
15
+ const router = useRouter();
16
+ const params = useLocalSearchParams<{ id?: string | string[] }>();
17
+ const invitationId = singleParam(params.id);
18
+ const [errorPath, setErrorPath] = useState<string | null>(null);
19
+
20
+ const signupPath = useMemo(() => {
21
+ if (!invitationId) return null;
22
+ return trpcClient.auth.readInvitation
23
+ .query({ id: invitationId })
24
+ .then((invitation) => {
25
+ const search = new URLSearchParams({
26
+ invitation: invitationId,
27
+ email: invitation.email,
28
+ });
29
+ return `/signup?${search.toString()}`;
30
+ })
31
+ .catch(() => "/signup");
32
+ }, [invitationId]);
33
+
34
+ useEffect(() => {
35
+ let cancelled = false;
36
+
37
+ if (!invitationId) {
38
+ setErrorPath("/signup");
39
+ return;
40
+ }
41
+
42
+ if (!session) {
43
+ signupPath?.then((path) => {
44
+ if (!cancelled) router.replace(path as Href);
45
+ });
46
+ return;
47
+ }
48
+
49
+ authClient.organization
50
+ .acceptInvitation({ invitationId })
51
+ .then(async ({ data, error }) => {
52
+ if (error) {
53
+ throw new Error(error.message ?? "Unable to accept invitation");
54
+ }
55
+
56
+ const organizationId =
57
+ (data as { invitation?: { organizationId?: string } | null } | null)?.invitation
58
+ ?.organizationId ?? null;
59
+
60
+ if (organizationId) {
61
+ const activeResult = await authClient.organization.setActive({ organizationId });
62
+ if (activeResult.error) {
63
+ throw new Error(activeResult.error.message ?? "Unable to activate organization");
64
+ }
65
+ }
66
+
67
+ if (!cancelled) {
68
+ registerSession(() => router.replace("/posts"));
69
+ }
70
+ })
71
+ .catch(() => {
72
+ if (!cancelled) setErrorPath("/signup");
73
+ });
74
+
75
+ return () => {
76
+ cancelled = true;
77
+ };
78
+ }, [authClient, invitationId, registerSession, router, session, signupPath]);
79
+
80
+ if (errorPath) {
81
+ return <Redirect href={errorPath as Href} />;
82
+ }
83
+
84
+ return <LoadingScreen />;
85
+ }
@@ -1,40 +1,40 @@
1
- import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
2
- import { Link, Redirect } from "expo-router";
3
- import { Button, Card, Typography } from "heroui-native";
4
- import { ScrollView, Text, View } from "react-native";
5
-
6
- export default function PostsScreen() {
7
- const { data: session } = useSession();
8
-
9
- if (!session) {
10
- return <Redirect href="/login" />;
11
- }
12
-
13
- return (
14
- <ScrollView className="flex-1 bg-background" contentContainerClassName="px-5 py-8">
15
- <View className="gap-5">
16
- <View className="gap-2">
17
- <Text className="text-3xl font-semibold text-foreground">Editorial posts</Text>
18
- <Typography.Paragraph className="text-default-600">
19
- A small blog that protects auth changes across web and Expo.
20
- </Typography.Paragraph>
21
- <Text testID="session-email" className="text-sm text-default-600">
22
- {session.user.email}
23
- </Text>
24
- </View>
25
- <Card className="gap-2 p-5">
26
- <Text className="text-lg font-semibold text-foreground">Native fixture post</Text>
27
- <Typography.Paragraph>
28
- This protected screen is rendered by Expo Router and authenticated through
29
- @m5kdev/frontend.
30
- </Typography.Paragraph>
31
- </Card>
32
- <Link href="/logout" asChild>
33
- <Button testID="logout-submit" variant="outline">
34
- Logout
35
- </Button>
36
- </Link>
37
- </View>
38
- </ScrollView>
39
- );
40
- }
1
+ import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
2
+ import { Link, Redirect } from "expo-router";
3
+ import { Button, Card, Typography } from "heroui-native";
4
+ import { ScrollView, Text, View } from "react-native";
5
+
6
+ export default function PostsScreen() {
7
+ const { data: session } = useSession();
8
+
9
+ if (!session) {
10
+ return <Redirect href="/login" />;
11
+ }
12
+
13
+ return (
14
+ <ScrollView className="flex-1 bg-background" contentContainerClassName="px-5 py-8">
15
+ <View className="gap-5">
16
+ <View className="gap-2">
17
+ <Text className="text-3xl font-semibold text-foreground">Editorial posts</Text>
18
+ <Typography.Paragraph className="text-default-600">
19
+ A small blog that protects auth changes across web and Expo.
20
+ </Typography.Paragraph>
21
+ <Text testID="session-email" className="text-sm text-default-600">
22
+ {session.user.email}
23
+ </Text>
24
+ </View>
25
+ <Card className="gap-2 p-5">
26
+ <Text className="text-lg font-semibold text-foreground">Native fixture post</Text>
27
+ <Typography.Paragraph>
28
+ This protected screen is rendered by Expo Router and authenticated through
29
+ @m5kdev/frontend.
30
+ </Typography.Paragraph>
31
+ </Card>
32
+ <Link href="/logout" asChild>
33
+ <Button testID="logout-submit" variant="outline">
34
+ Logout
35
+ </Button>
36
+ </Link>
37
+ </View>
38
+ </ScrollView>
39
+ );
40
+ }
@@ -1,75 +1,75 @@
1
- import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
- import { useLocalSearchParams } from "expo-router";
3
- import { Button } from "heroui-native";
4
- import { useState } from "react";
5
- import { View } from "react-native";
6
- import { AuthScaffold } from "../components/AuthScaffold";
7
- import { FormTextField } from "../components/FormTextField";
8
- import { StatusText } from "../components/StatusText";
9
-
10
- function singleParam(value: string | string[] | undefined) {
11
- return Array.isArray(value) ? value[0] : value;
12
- }
13
-
14
- export default function ResetPasswordScreen() {
15
- const authClient = useAuthClient();
16
- const params = useLocalSearchParams<{ token?: string | string[] }>();
17
- const token = singleParam(params.token);
18
- const [newPassword, setNewPassword] = useState("");
19
- const [confirmPassword, setConfirmPassword] = useState("");
20
- const [status, setStatus] = useState<string | null>(null);
21
- const [tone, setTone] = useState<"error" | "success">("success");
22
- const [isBusy, setIsBusy] = useState(false);
23
-
24
- const submit = async () => {
25
- setStatus(null);
26
- if (!token) {
27
- setTone("error");
28
- setStatus("Reset token is required");
29
- return;
30
- }
31
- if (newPassword !== confirmPassword) {
32
- setTone("error");
33
- setStatus("Passwords do not match");
34
- return;
35
- }
36
- setIsBusy(true);
37
- try {
38
- await authClient.resetPassword({ newPassword, token });
39
- setTone("success");
40
- setStatus("Password reset successfully");
41
- } catch (error) {
42
- setTone("error");
43
- setStatus(error instanceof Error ? error.message : "Password reset failed");
44
- } finally {
45
- setIsBusy(false);
46
- }
47
- };
48
-
49
- return (
50
- <AuthScaffold title="Set a new password">
51
- <View className="gap-4">
52
- <FormTextField
53
- label="New password"
54
- testID="reset-password-new"
55
- value={newPassword}
56
- onChangeText={setNewPassword}
57
- secureTextEntry
58
- />
59
- <FormTextField
60
- label="Confirm password"
61
- testID="reset-password-confirm"
62
- value={confirmPassword}
63
- onChangeText={setConfirmPassword}
64
- secureTextEntry
65
- />
66
- <StatusText testID="auth-status" tone={tone}>
67
- {status}
68
- </StatusText>
69
- <Button testID="reset-password-submit" onPress={submit} isDisabled={isBusy}>
70
- Reset password
71
- </Button>
72
- </View>
73
- </AuthScaffold>
74
- );
75
- }
1
+ import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
+ import { useLocalSearchParams } from "expo-router";
3
+ import { Button } from "heroui-native";
4
+ import { useState } from "react";
5
+ import { View } from "react-native";
6
+ import { AuthScaffold } from "../components/AuthScaffold";
7
+ import { FormTextField } from "../components/FormTextField";
8
+ import { StatusText } from "../components/StatusText";
9
+
10
+ function singleParam(value: string | string[] | undefined) {
11
+ return Array.isArray(value) ? value[0] : value;
12
+ }
13
+
14
+ export default function ResetPasswordScreen() {
15
+ const authClient = useAuthClient();
16
+ const params = useLocalSearchParams<{ token?: string | string[] }>();
17
+ const token = singleParam(params.token);
18
+ const [newPassword, setNewPassword] = useState("");
19
+ const [confirmPassword, setConfirmPassword] = useState("");
20
+ const [status, setStatus] = useState<string | null>(null);
21
+ const [tone, setTone] = useState<"error" | "success">("success");
22
+ const [isBusy, setIsBusy] = useState(false);
23
+
24
+ const submit = async () => {
25
+ setStatus(null);
26
+ if (!token) {
27
+ setTone("error");
28
+ setStatus("Reset token is required");
29
+ return;
30
+ }
31
+ if (newPassword !== confirmPassword) {
32
+ setTone("error");
33
+ setStatus("Passwords do not match");
34
+ return;
35
+ }
36
+ setIsBusy(true);
37
+ try {
38
+ await authClient.resetPassword({ newPassword, token });
39
+ setTone("success");
40
+ setStatus("Password reset successfully");
41
+ } catch (error) {
42
+ setTone("error");
43
+ setStatus(error instanceof Error ? error.message : "Password reset failed");
44
+ } finally {
45
+ setIsBusy(false);
46
+ }
47
+ };
48
+
49
+ return (
50
+ <AuthScaffold title="Set a new password">
51
+ <View className="gap-4">
52
+ <FormTextField
53
+ label="New password"
54
+ testID="reset-password-new"
55
+ value={newPassword}
56
+ onChangeText={setNewPassword}
57
+ secureTextEntry
58
+ />
59
+ <FormTextField
60
+ label="Confirm password"
61
+ testID="reset-password-confirm"
62
+ value={confirmPassword}
63
+ onChangeText={setConfirmPassword}
64
+ secureTextEntry
65
+ />
66
+ <StatusText testID="auth-status" tone={tone}>
67
+ {status}
68
+ </StatusText>
69
+ <Button testID="reset-password-submit" onPress={submit} isDisabled={isBusy}>
70
+ Reset password
71
+ </Button>
72
+ </View>
73
+ </AuthScaffold>
74
+ );
75
+ }