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,16 +1,16 @@
1
- const { getDefaultConfig } = require("expo/metro-config");
2
- const { withUniwindConfig } = require("uniwind/metro");
3
- const path = require("node:path");
4
-
5
- const config = getDefaultConfig(__dirname);
6
- const workspaceRoot = path.resolve(__dirname, "../../..");
7
-
8
- config.watchFolders = [workspaceRoot];
9
- config.resolver.nodeModulesPaths = [
10
- path.resolve(__dirname, "node_modules"),
11
- path.resolve(workspaceRoot, "node_modules"),
12
- ];
13
-
14
- module.exports = withUniwindConfig(config, {
15
- cssEntryFile: "./src/global.css",
16
- });
1
+ const { getDefaultConfig } = require("expo/metro-config");
2
+ const { withUniwindConfig } = require("uniwind/metro");
3
+ const path = require("node:path");
4
+
5
+ const config = getDefaultConfig(__dirname);
6
+ const workspaceRoot = path.resolve(__dirname, "../../..");
7
+
8
+ config.watchFolders = [workspaceRoot];
9
+ config.resolver.nodeModulesPaths = [
10
+ path.resolve(__dirname, "node_modules"),
11
+ path.resolve(workspaceRoot, "node_modules"),
12
+ ];
13
+
14
+ module.exports = withUniwindConfig(config, {
15
+ cssEntryFile: "./src/global.css",
16
+ });
@@ -1,31 +1,31 @@
1
- import { AppConfigProvider } from "@m5kdev/frontend/modules/app/components/AppConfigProvider";
2
- import { AuthProvider } from "@m5kdev/frontend/modules/auth/components/AuthProvider";
3
- import { Stack } from "expo-router";
4
- import { HeroUINativeProvider } from "heroui-native";
5
- import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
6
- import type { JSX } from "react";
7
- import { GestureHandlerRootView } from "react-native-gesture-handler";
8
- import { LoadingScreen } from "../components/LoadingScreen";
9
- import { appUrl, serverUrl } from "../config";
10
-
11
- import "../global.css";
12
-
13
- export default function RootLayout(): JSX.Element {
14
- return (
15
- <GestureHandlerRootView style={{ flex: 1 }}>
16
- <HeroUINativeProvider>
17
- <AppConfigProvider
18
- config={{
19
- appName: APP_NAME,
20
- appUrl,
21
- serverUrl,
22
- }}
23
- >
24
- <AuthProvider loader={<LoadingScreen />}>
25
- <Stack screenOptions={{ headerShown: false }} />
26
- </AuthProvider>
27
- </AppConfigProvider>
28
- </HeroUINativeProvider>
29
- </GestureHandlerRootView>
30
- );
31
- }
1
+ import { AppConfigProvider } from "@m5kdev/frontend/modules/app/components/AppConfigProvider";
2
+ import { AuthProvider } from "@m5kdev/frontend/modules/auth/components/AuthProvider";
3
+ import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
4
+ import { Stack } from "expo-router";
5
+ import { HeroUINativeProvider } from "heroui-native";
6
+ import type { JSX } from "react";
7
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
8
+ import { LoadingScreen } from "../components/LoadingScreen";
9
+ import { appUrl, serverUrl } from "../config";
10
+
11
+ import "../global.css";
12
+
13
+ export default function RootLayout(): JSX.Element {
14
+ return (
15
+ <GestureHandlerRootView style={{ flex: 1 }}>
16
+ <HeroUINativeProvider>
17
+ <AppConfigProvider
18
+ config={{
19
+ appName: APP_NAME,
20
+ appUrl,
21
+ serverUrl,
22
+ }}
23
+ >
24
+ <AuthProvider loader={<LoadingScreen />}>
25
+ <Stack screenOptions={{ headerShown: false }} />
26
+ </AuthProvider>
27
+ </AppConfigProvider>
28
+ </HeroUINativeProvider>
29
+ </GestureHandlerRootView>
30
+ );
31
+ }
@@ -1,151 +1,151 @@
1
- import { useAppConfig } from "@m5kdev/frontend/modules/app/hooks/useAppConfig";
2
- import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
- import { Button } from "heroui-native";
4
- import { useEffect, 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
- import { trpcClient } from "../lib/trpc";
10
-
11
- export default function ClaimAccountScreen() {
12
- const { serverUrl } = useAppConfig();
13
- const { data: session, registerSession } = useSession();
14
- const [email, setEmail] = useState("");
15
- const [password, setPassword] = useState("");
16
- const [status, setStatus] = useState<string | null>("Loading claim status...");
17
- const [statusTone, setStatusTone] = useState<"default" | "error" | "success">("default");
18
- const [hasClaim, setHasClaim] = useState(false);
19
- const [isClaimed, setIsClaimed] = useState(false);
20
- const [isBusy, setIsBusy] = useState(false);
21
-
22
- useEffect(() => {
23
- if (session?.user.email) {
24
- setEmail(session.user.email);
25
- }
26
- }, [session?.user.email]);
27
-
28
- useEffect(() => {
29
- let cancelled = false;
30
-
31
- if (isClaimed) {
32
- return;
33
- }
34
-
35
- if (!session?.user) {
36
- setStatusTone("error");
37
- setStatus("You need to sign in with your magic link before claiming this account.");
38
- setHasClaim(false);
39
- return;
40
- }
41
-
42
- trpcClient.auth.getMyAccountClaimStatus
43
- .query()
44
- .then((claim) => {
45
- if (cancelled) return;
46
- setHasClaim(Boolean(claim));
47
- setStatusTone(claim ? "success" : "error");
48
- setStatus(claim ? "Claim your account" : "No pending account claim was found.");
49
- if (claim?.claimedEmail) {
50
- setEmail(claim.claimedEmail);
51
- }
52
- })
53
- .catch((error) => {
54
- if (cancelled) return;
55
- setHasClaim(false);
56
- setStatusTone("error");
57
- setStatus(error instanceof Error ? error.message : "Unable to load claim status");
58
- });
59
-
60
- return () => {
61
- cancelled = true;
62
- };
63
- }, [isClaimed, session?.user]);
64
-
65
- const saveEmail = async () => {
66
- setIsBusy(true);
67
- setStatus(null);
68
- try {
69
- await trpcClient.auth.setMyAccountClaimEmail.mutate({ email });
70
- registerSession();
71
- setStatusTone("success");
72
- setStatus("Email updated");
73
- } catch (error) {
74
- setStatusTone("error");
75
- setStatus(error instanceof Error ? error.message : "Unable to update email");
76
- } finally {
77
- setIsBusy(false);
78
- }
79
- };
80
-
81
- const setPasswordAndClaim = async () => {
82
- setIsBusy(true);
83
- setStatus(null);
84
- try {
85
- const response = await fetch(`${serverUrl}/api/auth/set-password`, {
86
- method: "POST",
87
- credentials: "include",
88
- headers: {
89
- "Content-Type": "application/json",
90
- },
91
- body: JSON.stringify({
92
- newPassword: password,
93
- }),
94
- });
95
- const payload = (await response.json().catch(() => ({}))) as { message?: string };
96
- if (!response.ok) {
97
- throw new Error(payload.message ?? "Unable to set password");
98
- }
99
-
100
- await trpcClient.auth.acceptMyAccountClaim.mutate();
101
- setIsClaimed(true);
102
- registerSession();
103
- setStatusTone("success");
104
- setStatus("Password set. Account claimed.");
105
- } catch (error) {
106
- setStatusTone("error");
107
- setStatus(error instanceof Error ? error.message : "Unable to claim account");
108
- } finally {
109
- setIsBusy(false);
110
- }
111
- };
112
-
113
- return (
114
- <AuthScaffold
115
- title="Claim your account"
116
- description="Set permanent credentials for a provisioned account."
117
- >
118
- <View className="gap-4">
119
- <FormTextField
120
- label="Email"
121
- testID="claim-email"
122
- value={email}
123
- onChangeText={setEmail}
124
- keyboardType="email-address"
125
- editable={hasClaim && !isBusy}
126
- />
127
- <Button testID="claim-email-submit" onPress={saveEmail} isDisabled={!hasClaim || isBusy}>
128
- Save Email
129
- </Button>
130
- <FormTextField
131
- label="Set password"
132
- testID="claim-password"
133
- value={password}
134
- onChangeText={setPassword}
135
- secureTextEntry
136
- editable={hasClaim && !isBusy}
137
- />
138
- <Button
139
- testID="claim-password-submit"
140
- onPress={setPasswordAndClaim}
141
- isDisabled={!hasClaim || !password || isBusy}
142
- >
143
- Set Password and Claim
144
- </Button>
145
- <StatusText testID="claim-status" tone={statusTone}>
146
- {status}
147
- </StatusText>
148
- </View>
149
- </AuthScaffold>
150
- );
151
- }
1
+ import { useAppConfig } from "@m5kdev/frontend/modules/app/hooks/useAppConfig";
2
+ import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
+ import { Button } from "heroui-native";
4
+ import { useEffect, 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
+ import { trpcClient } from "../lib/trpc";
10
+
11
+ export default function ClaimAccountScreen() {
12
+ const { serverUrl } = useAppConfig();
13
+ const { data: session, registerSession } = useSession();
14
+ const [email, setEmail] = useState("");
15
+ const [password, setPassword] = useState("");
16
+ const [status, setStatus] = useState<string | null>("Loading claim status...");
17
+ const [statusTone, setStatusTone] = useState<"default" | "error" | "success">("default");
18
+ const [hasClaim, setHasClaim] = useState(false);
19
+ const [isClaimed, setIsClaimed] = useState(false);
20
+ const [isBusy, setIsBusy] = useState(false);
21
+
22
+ useEffect(() => {
23
+ if (session?.user.email) {
24
+ setEmail(session.user.email);
25
+ }
26
+ }, [session?.user.email]);
27
+
28
+ useEffect(() => {
29
+ let cancelled = false;
30
+
31
+ if (isClaimed) {
32
+ return;
33
+ }
34
+
35
+ if (!session?.user) {
36
+ setStatusTone("error");
37
+ setStatus("You need to sign in with your magic link before claiming this account.");
38
+ setHasClaim(false);
39
+ return;
40
+ }
41
+
42
+ trpcClient.auth.getMyAccountClaimStatus
43
+ .query()
44
+ .then((claim) => {
45
+ if (cancelled) return;
46
+ setHasClaim(Boolean(claim));
47
+ setStatusTone(claim ? "success" : "error");
48
+ setStatus(claim ? "Claim your account" : "No pending account claim was found.");
49
+ if (claim?.claimedEmail) {
50
+ setEmail(claim.claimedEmail);
51
+ }
52
+ })
53
+ .catch((error) => {
54
+ if (cancelled) return;
55
+ setHasClaim(false);
56
+ setStatusTone("error");
57
+ setStatus(error instanceof Error ? error.message : "Unable to load claim status");
58
+ });
59
+
60
+ return () => {
61
+ cancelled = true;
62
+ };
63
+ }, [isClaimed, session?.user]);
64
+
65
+ const saveEmail = async () => {
66
+ setIsBusy(true);
67
+ setStatus(null);
68
+ try {
69
+ await trpcClient.auth.setMyAccountClaimEmail.mutate({ email });
70
+ registerSession();
71
+ setStatusTone("success");
72
+ setStatus("Email updated");
73
+ } catch (error) {
74
+ setStatusTone("error");
75
+ setStatus(error instanceof Error ? error.message : "Unable to update email");
76
+ } finally {
77
+ setIsBusy(false);
78
+ }
79
+ };
80
+
81
+ const setPasswordAndClaim = async () => {
82
+ setIsBusy(true);
83
+ setStatus(null);
84
+ try {
85
+ const response = await fetch(`${serverUrl}/api/auth/set-password`, {
86
+ method: "POST",
87
+ credentials: "include",
88
+ headers: {
89
+ "Content-Type": "application/json",
90
+ },
91
+ body: JSON.stringify({
92
+ newPassword: password,
93
+ }),
94
+ });
95
+ const payload = (await response.json().catch(() => ({}))) as { message?: string };
96
+ if (!response.ok) {
97
+ throw new Error(payload.message ?? "Unable to set password");
98
+ }
99
+
100
+ await trpcClient.auth.acceptMyAccountClaim.mutate();
101
+ setIsClaimed(true);
102
+ registerSession();
103
+ setStatusTone("success");
104
+ setStatus("Password set. Account claimed.");
105
+ } catch (error) {
106
+ setStatusTone("error");
107
+ setStatus(error instanceof Error ? error.message : "Unable to claim account");
108
+ } finally {
109
+ setIsBusy(false);
110
+ }
111
+ };
112
+
113
+ return (
114
+ <AuthScaffold
115
+ title="Claim your account"
116
+ description="Set permanent credentials for a provisioned account."
117
+ >
118
+ <View className="gap-4">
119
+ <FormTextField
120
+ label="Email"
121
+ testID="claim-email"
122
+ value={email}
123
+ onChangeText={setEmail}
124
+ keyboardType="email-address"
125
+ editable={hasClaim && !isBusy}
126
+ />
127
+ <Button testID="claim-email-submit" onPress={saveEmail} isDisabled={!hasClaim || isBusy}>
128
+ Save Email
129
+ </Button>
130
+ <FormTextField
131
+ label="Set password"
132
+ testID="claim-password"
133
+ value={password}
134
+ onChangeText={setPassword}
135
+ secureTextEntry
136
+ editable={hasClaim && !isBusy}
137
+ />
138
+ <Button
139
+ testID="claim-password-submit"
140
+ onPress={setPasswordAndClaim}
141
+ isDisabled={!hasClaim || !password || isBusy}
142
+ >
143
+ Set Password and Claim
144
+ </Button>
145
+ <StatusText testID="claim-status" tone={statusTone}>
146
+ {status}
147
+ </StatusText>
148
+ </View>
149
+ </AuthScaffold>
150
+ );
151
+ }
@@ -1,53 +1,53 @@
1
- import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
- import { Button } from "heroui-native";
3
- import { useState } from "react";
4
- import { View } from "react-native";
5
- import { AuthScaffold } from "../components/AuthScaffold";
6
- import { FormTextField } from "../components/FormTextField";
7
- import { StatusText } from "../components/StatusText";
8
-
9
- export default function ForgotPasswordScreen() {
10
- const authClient = useAuthClient();
11
- const [email, setEmail] = useState("");
12
- const [status, setStatus] = useState<string | null>(null);
13
- const [tone, setTone] = useState<"error" | "success">("success");
14
- const [isBusy, setIsBusy] = useState(false);
15
-
16
- const submit = async () => {
17
- setIsBusy(true);
18
- setStatus(null);
19
- try {
20
- await authClient.requestPasswordReset({
21
- email,
22
- redirectTo: "/reset-password",
23
- });
24
- setTone("success");
25
- setStatus("Password reset email sent");
26
- } catch (error) {
27
- setTone("error");
28
- setStatus(error instanceof Error ? error.message : "Failed to request password reset");
29
- } finally {
30
- setIsBusy(false);
31
- }
32
- };
33
-
34
- return (
35
- <AuthScaffold title="Reset password">
36
- <View className="gap-4">
37
- <FormTextField
38
- label="Email"
39
- testID="forgot-password-email"
40
- value={email}
41
- onChangeText={setEmail}
42
- keyboardType="email-address"
43
- />
44
- <StatusText testID="auth-status" tone={tone}>
45
- {status}
46
- </StatusText>
47
- <Button testID="forgot-password-submit" onPress={submit} isDisabled={isBusy}>
48
- Reset password
49
- </Button>
50
- </View>
51
- </AuthScaffold>
52
- );
53
- }
1
+ import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
+ import { Button } from "heroui-native";
3
+ import { useState } from "react";
4
+ import { View } from "react-native";
5
+ import { AuthScaffold } from "../components/AuthScaffold";
6
+ import { FormTextField } from "../components/FormTextField";
7
+ import { StatusText } from "../components/StatusText";
8
+
9
+ export default function ForgotPasswordScreen() {
10
+ const authClient = useAuthClient();
11
+ const [email, setEmail] = useState("");
12
+ const [status, setStatus] = useState<string | null>(null);
13
+ const [tone, setTone] = useState<"error" | "success">("success");
14
+ const [isBusy, setIsBusy] = useState(false);
15
+
16
+ const submit = async () => {
17
+ setIsBusy(true);
18
+ setStatus(null);
19
+ try {
20
+ await authClient.requestPasswordReset({
21
+ email,
22
+ redirectTo: "/reset-password",
23
+ });
24
+ setTone("success");
25
+ setStatus("Password reset email sent");
26
+ } catch (error) {
27
+ setTone("error");
28
+ setStatus(error instanceof Error ? error.message : "Failed to request password reset");
29
+ } finally {
30
+ setIsBusy(false);
31
+ }
32
+ };
33
+
34
+ return (
35
+ <AuthScaffold title="Reset password">
36
+ <View className="gap-4">
37
+ <FormTextField
38
+ label="Email"
39
+ testID="forgot-password-email"
40
+ value={email}
41
+ onChangeText={setEmail}
42
+ keyboardType="email-address"
43
+ />
44
+ <StatusText testID="auth-status" tone={tone}>
45
+ {status}
46
+ </StatusText>
47
+ <Button testID="forgot-password-submit" onPress={submit} isDisabled={isBusy}>
48
+ Reset password
49
+ </Button>
50
+ </View>
51
+ </AuthScaffold>
52
+ );
53
+ }
@@ -1,8 +1,8 @@
1
- import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
2
- import { Redirect } from "expo-router";
3
-
4
- export default function IndexScreen() {
5
- const { data: session } = useSession();
6
-
7
- return <Redirect href={session ? "/posts" : "/login"} />;
8
- }
1
+ import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
2
+ import { Redirect } from "expo-router";
3
+
4
+ export default function IndexScreen() {
5
+ const { data: session } = useSession();
6
+
7
+ return <Redirect href={session ? "/posts" : "/login"} />;
8
+ }