create-m5kdev 0.33.0 → 0.34.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/module-dto-guide.mdc +1 -1
  18. package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +3 -3
  19. package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +3 -2
  20. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
  21. package/templates/minimal-app/.dockerignore.tpl +31 -0
  22. package/templates/minimal-app/.gitignore.tpl +1 -0
  23. package/templates/minimal-app/AGENTS.md.tpl +13 -10
  24. package/templates/minimal-app/CLAUDE.md.tpl +132 -131
  25. package/templates/minimal-app/README.md.tpl +50 -19
  26. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
  27. package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +51 -39
  28. package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -38
  29. package/templates/minimal-app/apps/e2e/servers.ts.tpl +136 -112
  30. package/templates/minimal-app/apps/e2e/tests/conversation.spec.ts.tpl +22 -0
  31. package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -47
  32. package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -73
  33. package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -517
  34. package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -94
  35. package/templates/minimal-app/apps/e2e/tests/files.spec.ts.tpl +35 -0
  36. package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -289
  37. package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -516
  38. package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -107
  39. package/templates/minimal-app/apps/e2e/tests/workflow.spec.ts.tpl +26 -0
  40. package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -10
  41. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  42. package/templates/minimal-app/apps/email/src/emails/trialEndingEmail.tsx.tpl +40 -0
  43. package/templates/minimal-app/apps/email/src/index.ts.tpl +7 -0
  44. package/templates/minimal-app/apps/email/src/resources.ts.tpl +72 -65
  45. package/templates/minimal-app/apps/expo/app.json.tpl +36 -36
  46. package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -8
  47. package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -16
  48. package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -31
  49. package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -151
  50. package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -53
  51. package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -8
  52. package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -71
  53. package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -19
  54. package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -85
  55. package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -40
  56. package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -75
  57. package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -193
  58. package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -34
  59. package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -37
  60. package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -10
  61. package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -22
  62. package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -9
  63. package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -11
  64. package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -20
  65. package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -11
  66. package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -1
  67. package/templates/minimal-app/apps/landing/.env.production.example.tpl +7 -0
  68. package/templates/minimal-app/apps/landing/Dockerfile.tpl +39 -0
  69. package/templates/minimal-app/apps/landing/fly.toml.tpl +20 -0
  70. package/templates/minimal-app/apps/landing/index.html.tpl +35 -0
  71. package/templates/minimal-app/apps/landing/package.json.tpl +34 -0
  72. package/templates/minimal-app/apps/landing/scripts/fly-deploy.mjs.tpl +75 -0
  73. package/templates/minimal-app/apps/landing/scripts/fly-secrets.mjs.tpl +31 -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 +54 -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 +47 -0
  95. package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl +43 -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 +54 -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 +25 -24
  114. package/templates/minimal-app/apps/shared/scripts/fly-deploy.mjs.tpl +75 -0
  115. package/templates/minimal-app/apps/shared/scripts/fly-secrets.mjs.tpl +31 -0
  116. package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -28
  117. package/templates/minimal-app/apps/shared/src/modules/conversation/conversation.constants.ts.tpl +1 -0
  118. package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
  119. package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
  120. package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -25
  121. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
  122. package/templates/minimal-app/apps/webapp/public/push-sw.js.tpl +34 -34
  123. package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +46 -44
  124. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +96 -65
  125. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +114 -93
  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 +6 -2
  142. package/templates/minimal-app/pnpm-workspace.yaml.tpl +14 -7
  143. package/templates/minimal-app/template.manifest.json +92 -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,193 +1,193 @@
1
- import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
2
- import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
3
- import { USER_LOCALE_HEADER } from "@m5kdev/commons/modules/auth/auth.constants";
4
- import { resolveAppLocale } from "@m5kdev/commons/modules/auth/auth.locale";
5
- import { APP_LOCALE_CONFIG } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
6
- import { Link, useLocalSearchParams, useRouter } from "expo-router";
7
- import { Button } from "heroui-native";
8
- import { useEffect, useMemo, useState } from "react";
9
- import { Text, View } from "react-native";
10
- import { AuthScaffold } from "../components/AuthScaffold";
11
- import { FormTextField } from "../components/FormTextField";
12
- import { StatusText } from "../components/StatusText";
13
- import { isWaitlistProfile } from "../config";
14
- import { trpcClient } from "../lib/trpc";
15
-
16
- function singleParam(value: string | string[] | undefined) {
17
- return Array.isArray(value) ? value[0] : value;
18
- }
19
-
20
- export default function SignupScreen() {
21
- const authClient = useAuthClient();
22
- const { registerSession } = useSession();
23
- const router = useRouter();
24
- const params = useLocalSearchParams<{
25
- code?: string | string[];
26
- email?: string | string[];
27
- invitation?: string | string[];
28
- }>();
29
- const code = singleParam(params.code);
30
- const invitation = singleParam(params.invitation);
31
- const lockedEmail = singleParam(params.email);
32
- const isPublicWaitlist = isWaitlistProfile && !code && !invitation;
33
- const [email, setEmail] = useState(lockedEmail ?? "");
34
- const [password, setPassword] = useState("");
35
- const [status, setStatus] = useState<string | null>(null);
36
- const [statusTone, setStatusTone] = useState<"error" | "success">("success");
37
- const [isBusy, setIsBusy] = useState(false);
38
- const [waitlistCodeStatus, setWaitlistCodeStatus] = useState<string | null>(null);
39
- const [isValidatingWaitlistCode, setIsValidatingWaitlistCode] = useState(false);
40
- const [waitlistCodeError, setWaitlistCodeError] = useState(false);
41
-
42
- const waitlistStatusText = useMemo(() => {
43
- if (!code) return null;
44
- if (isValidatingWaitlistCode) return "Validating the invitation code...";
45
- if (waitlistCodeError) return "An error occurred while validating the invitation code.";
46
- if (waitlistCodeStatus === "VALID") return "Invitation code is valid. You can proceed.";
47
- if (waitlistCodeStatus === "EXPIRED") return "Invitation code has expired.";
48
- if (waitlistCodeStatus === "NOT_FOUND") return "Invitation code not found.";
49
- if (waitlistCodeStatus) return "Invalid invitation code.";
50
- return null;
51
- }, [code, isValidatingWaitlistCode, waitlistCodeError, waitlistCodeStatus]);
52
-
53
- useEffect(() => {
54
- if (lockedEmail) setEmail(lockedEmail);
55
- }, [lockedEmail]);
56
-
57
- useEffect(() => {
58
- let cancelled = false;
59
- if (!code) {
60
- setWaitlistCodeStatus(null);
61
- setWaitlistCodeError(false);
62
- return;
63
- }
64
-
65
- setIsValidatingWaitlistCode(true);
66
- setWaitlistCodeError(false);
67
- trpcClient.auth.validateWaitlistCode
68
- .query({ code })
69
- .then((result) => {
70
- if (!cancelled) setWaitlistCodeStatus(result.status);
71
- })
72
- .catch(() => {
73
- if (!cancelled) setWaitlistCodeError(true);
74
- })
75
- .finally(() => {
76
- if (!cancelled) setIsValidatingWaitlistCode(false);
77
- });
78
-
79
- return () => {
80
- cancelled = true;
81
- };
82
- }, [code]);
83
-
84
- const submitWaitlist = async () => {
85
- setIsBusy(true);
86
- setStatus(null);
87
- try {
88
- await trpcClient.auth.joinWaitlist.mutate({ email });
89
- setStatusTone("success");
90
- setStatus("Waitlist confirmation email sent");
91
- } catch (error) {
92
- setStatusTone("error");
93
- setStatus(error instanceof Error ? error.message : "Failed to join waitlist");
94
- } finally {
95
- setIsBusy(false);
96
- }
97
- };
98
-
99
- const submitSignup = async () => {
100
- setIsBusy(true);
101
- setStatus(null);
102
- try {
103
- const deviceLocale = Intl.DateTimeFormat().resolvedOptions().locale;
104
- const userLocale = resolveAppLocale(deviceLocale, APP_LOCALE_CONFIG);
105
- const result = await authClient.signUp.email(
106
- {
107
- name: email,
108
- email,
109
- password,
110
- },
111
- {
112
- headers: {
113
- "Waitlist-Invitation-Code": code ?? "",
114
- "Organization-Invitation-Code": invitation ?? "",
115
- [USER_LOCALE_HEADER]: userLocale,
116
- },
117
- }
118
- );
119
- if (result.error) {
120
- setStatusTone("error");
121
- setStatus(result.error.message ?? "Signup failed");
122
- return;
123
- }
124
- if (isWaitlistProfile || invitation) {
125
- const loginResult = await authClient.signIn.email({ email, password });
126
- if (loginResult.error || !loginResult.data?.user) {
127
- setStatusTone("error");
128
- setStatus(loginResult.error?.message ?? "Authentication failed");
129
- return;
130
- }
131
- registerSession(() => router.replace("/posts"));
132
- return;
133
- }
134
- setStatusTone("success");
135
- setStatus("Verification email sent");
136
- } catch (error) {
137
- setStatusTone("error");
138
- setStatus(error instanceof Error ? error.message : "Signup failed");
139
- } finally {
140
- setIsBusy(false);
141
- }
142
- };
143
-
144
- return (
145
- <AuthScaffold
146
- title={isPublicWaitlist ? "Join the waitlist" : "Sign up"}
147
- description={
148
- isPublicWaitlist
149
- ? "Request access to the waitlist test profile."
150
- : "Create a test account for the Expo app."
151
- }
152
- >
153
- <View className="gap-4">
154
- <FormTextField
155
- label="Email"
156
- testID={isPublicWaitlist ? "waitlist-email" : "signup-email"}
157
- value={email}
158
- onChangeText={setEmail}
159
- keyboardType="email-address"
160
- editable={!lockedEmail}
161
- />
162
- {isPublicWaitlist ? null : (
163
- <FormTextField
164
- label="Password"
165
- testID="signup-password"
166
- value={password}
167
- onChangeText={setPassword}
168
- secureTextEntry
169
- />
170
- )}
171
- <StatusText
172
- testID="waitlist-code-status"
173
- tone={waitlistCodeStatus === "VALID" ? "success" : "error"}
174
- >
175
- {waitlistStatusText}
176
- </StatusText>
177
- <StatusText testID="auth-status" tone={statusTone}>
178
- {status}
179
- </StatusText>
180
- <Button
181
- testID={isPublicWaitlist ? "waitlist-submit" : "signup-submit"}
182
- onPress={isPublicWaitlist ? submitWaitlist : submitSignup}
183
- isDisabled={isBusy}
184
- >
185
- {isPublicWaitlist ? "Join the waitlist" : "Sign up"}
186
- </Button>
187
- <Link href="/login" asChild>
188
- <Text className="text-center text-primary underline">Already have an account?</Text>
189
- </Link>
190
- </View>
191
- </AuthScaffold>
192
- );
193
- }
1
+ import { USER_LOCALE_HEADER } from "@m5kdev/commons/modules/auth/auth.constants";
2
+ import { resolveAppLocale } from "@m5kdev/commons/modules/auth/auth.locale";
3
+ import { useAuthClient } from "@m5kdev/frontend/modules/auth/hooks/useAuthClient";
4
+ import { useSession } from "@m5kdev/frontend/modules/auth/hooks/useSession";
5
+ import { APP_LOCALE_CONFIG } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
6
+ import { Link, useLocalSearchParams, useRouter } from "expo-router";
7
+ import { Button } from "heroui-native";
8
+ import { useEffect, useMemo, useState } from "react";
9
+ import { Text, View } from "react-native";
10
+ import { AuthScaffold } from "../components/AuthScaffold";
11
+ import { FormTextField } from "../components/FormTextField";
12
+ import { StatusText } from "../components/StatusText";
13
+ import { isWaitlistProfile } from "../config";
14
+ import { trpcClient } from "../lib/trpc";
15
+
16
+ function singleParam(value: string | string[] | undefined) {
17
+ return Array.isArray(value) ? value[0] : value;
18
+ }
19
+
20
+ export default function SignupScreen() {
21
+ const authClient = useAuthClient();
22
+ const { registerSession } = useSession();
23
+ const router = useRouter();
24
+ const params = useLocalSearchParams<{
25
+ code?: string | string[];
26
+ email?: string | string[];
27
+ invitation?: string | string[];
28
+ }>();
29
+ const code = singleParam(params.code);
30
+ const invitation = singleParam(params.invitation);
31
+ const lockedEmail = singleParam(params.email);
32
+ const isPublicWaitlist = isWaitlistProfile && !code && !invitation;
33
+ const [email, setEmail] = useState(lockedEmail ?? "");
34
+ const [password, setPassword] = useState("");
35
+ const [status, setStatus] = useState<string | null>(null);
36
+ const [statusTone, setStatusTone] = useState<"error" | "success">("success");
37
+ const [isBusy, setIsBusy] = useState(false);
38
+ const [waitlistCodeStatus, setWaitlistCodeStatus] = useState<string | null>(null);
39
+ const [isValidatingWaitlistCode, setIsValidatingWaitlistCode] = useState(false);
40
+ const [waitlistCodeError, setWaitlistCodeError] = useState(false);
41
+
42
+ const waitlistStatusText = useMemo(() => {
43
+ if (!code) return null;
44
+ if (isValidatingWaitlistCode) return "Validating the invitation code...";
45
+ if (waitlistCodeError) return "An error occurred while validating the invitation code.";
46
+ if (waitlistCodeStatus === "VALID") return "Invitation code is valid. You can proceed.";
47
+ if (waitlistCodeStatus === "EXPIRED") return "Invitation code has expired.";
48
+ if (waitlistCodeStatus === "NOT_FOUND") return "Invitation code not found.";
49
+ if (waitlistCodeStatus) return "Invalid invitation code.";
50
+ return null;
51
+ }, [code, isValidatingWaitlistCode, waitlistCodeError, waitlistCodeStatus]);
52
+
53
+ useEffect(() => {
54
+ if (lockedEmail) setEmail(lockedEmail);
55
+ }, [lockedEmail]);
56
+
57
+ useEffect(() => {
58
+ let cancelled = false;
59
+ if (!code) {
60
+ setWaitlistCodeStatus(null);
61
+ setWaitlistCodeError(false);
62
+ return;
63
+ }
64
+
65
+ setIsValidatingWaitlistCode(true);
66
+ setWaitlistCodeError(false);
67
+ trpcClient.auth.validateWaitlistCode
68
+ .query({ code })
69
+ .then((result) => {
70
+ if (!cancelled) setWaitlistCodeStatus(result.status);
71
+ })
72
+ .catch(() => {
73
+ if (!cancelled) setWaitlistCodeError(true);
74
+ })
75
+ .finally(() => {
76
+ if (!cancelled) setIsValidatingWaitlistCode(false);
77
+ });
78
+
79
+ return () => {
80
+ cancelled = true;
81
+ };
82
+ }, [code]);
83
+
84
+ const submitWaitlist = async () => {
85
+ setIsBusy(true);
86
+ setStatus(null);
87
+ try {
88
+ await trpcClient.auth.joinWaitlist.mutate({ email });
89
+ setStatusTone("success");
90
+ setStatus("Waitlist confirmation email sent");
91
+ } catch (error) {
92
+ setStatusTone("error");
93
+ setStatus(error instanceof Error ? error.message : "Failed to join waitlist");
94
+ } finally {
95
+ setIsBusy(false);
96
+ }
97
+ };
98
+
99
+ const submitSignup = async () => {
100
+ setIsBusy(true);
101
+ setStatus(null);
102
+ try {
103
+ const deviceLocale = Intl.DateTimeFormat().resolvedOptions().locale;
104
+ const userLocale = resolveAppLocale(deviceLocale, APP_LOCALE_CONFIG);
105
+ const result = await authClient.signUp.email(
106
+ {
107
+ name: email,
108
+ email,
109
+ password,
110
+ },
111
+ {
112
+ headers: {
113
+ "Waitlist-Invitation-Code": code ?? "",
114
+ "Organization-Invitation-Code": invitation ?? "",
115
+ [USER_LOCALE_HEADER]: userLocale,
116
+ },
117
+ }
118
+ );
119
+ if (result.error) {
120
+ setStatusTone("error");
121
+ setStatus(result.error.message ?? "Signup failed");
122
+ return;
123
+ }
124
+ if (isWaitlistProfile || invitation) {
125
+ const loginResult = await authClient.signIn.email({ email, password });
126
+ if (loginResult.error || !loginResult.data?.user) {
127
+ setStatusTone("error");
128
+ setStatus(loginResult.error?.message ?? "Authentication failed");
129
+ return;
130
+ }
131
+ registerSession(() => router.replace("/posts"));
132
+ return;
133
+ }
134
+ setStatusTone("success");
135
+ setStatus("Verification email sent");
136
+ } catch (error) {
137
+ setStatusTone("error");
138
+ setStatus(error instanceof Error ? error.message : "Signup failed");
139
+ } finally {
140
+ setIsBusy(false);
141
+ }
142
+ };
143
+
144
+ return (
145
+ <AuthScaffold
146
+ title={isPublicWaitlist ? "Join the waitlist" : "Sign up"}
147
+ description={
148
+ isPublicWaitlist
149
+ ? "Request access to the waitlist test profile."
150
+ : "Create a test account for the Expo app."
151
+ }
152
+ >
153
+ <View className="gap-4">
154
+ <FormTextField
155
+ label="Email"
156
+ testID={isPublicWaitlist ? "waitlist-email" : "signup-email"}
157
+ value={email}
158
+ onChangeText={setEmail}
159
+ keyboardType="email-address"
160
+ editable={!lockedEmail}
161
+ />
162
+ {isPublicWaitlist ? null : (
163
+ <FormTextField
164
+ label="Password"
165
+ testID="signup-password"
166
+ value={password}
167
+ onChangeText={setPassword}
168
+ secureTextEntry
169
+ />
170
+ )}
171
+ <StatusText
172
+ testID="waitlist-code-status"
173
+ tone={waitlistCodeStatus === "VALID" ? "success" : "error"}
174
+ >
175
+ {waitlistStatusText}
176
+ </StatusText>
177
+ <StatusText testID="auth-status" tone={statusTone}>
178
+ {status}
179
+ </StatusText>
180
+ <Button
181
+ testID={isPublicWaitlist ? "waitlist-submit" : "signup-submit"}
182
+ onPress={isPublicWaitlist ? submitWaitlist : submitSignup}
183
+ isDisabled={isBusy}
184
+ >
185
+ {isPublicWaitlist ? "Join the waitlist" : "Sign up"}
186
+ </Button>
187
+ <Link href="/login" asChild>
188
+ <Text className="text-center text-primary underline">Already have an account?</Text>
189
+ </Link>
190
+ </View>
191
+ </AuthScaffold>
192
+ );
193
+ }
@@ -1,34 +1,34 @@
1
- import { Card, Typography } from "heroui-native";
2
- import type { ReactNode } from "react";
3
- import { ScrollView, Text, View } from "react-native";
4
- import { appTitle } from "../config";
5
-
6
- export function AuthScaffold({
7
- children,
8
- description,
9
- title,
10
- }: {
11
- children: ReactNode;
12
- description?: string;
13
- title: string;
14
- }) {
15
- return (
16
- <ScrollView className="flex-1 bg-background" contentContainerClassName="flex-grow px-5 py-8">
17
- <View className="flex-1 justify-center gap-5">
18
- <View className="gap-2">
19
- <Text className="text-center text-xs font-semibold uppercase tracking-widest text-default-500">
20
- Expo auth fixture
21
- </Text>
22
- <Text className="text-center text-3xl font-semibold text-foreground">{appTitle}</Text>
23
- <Typography.Paragraph className="text-center text-default-600">
24
- {description ?? "Native auth screens backed by @m5kdev/frontend."}
25
- </Typography.Paragraph>
26
- </View>
27
- <Card className="gap-5 p-5">
28
- <Text className="text-xl font-semibold text-foreground">{title}</Text>
29
- {children}
30
- </Card>
31
- </View>
32
- </ScrollView>
33
- );
34
- }
1
+ import { Card, Typography } from "heroui-native";
2
+ import type { ReactNode } from "react";
3
+ import { ScrollView, Text, View } from "react-native";
4
+ import { appTitle } from "../config";
5
+
6
+ export function AuthScaffold({
7
+ children,
8
+ description,
9
+ title,
10
+ }: {
11
+ children: ReactNode;
12
+ description?: string;
13
+ title: string;
14
+ }) {
15
+ return (
16
+ <ScrollView className="flex-1 bg-background" contentContainerClassName="flex-grow px-5 py-8">
17
+ <View className="flex-1 justify-center gap-5">
18
+ <View className="gap-2">
19
+ <Text className="text-center text-xs font-semibold uppercase tracking-widest text-default-500">
20
+ Expo auth fixture
21
+ </Text>
22
+ <Text className="text-center text-3xl font-semibold text-foreground">{appTitle}</Text>
23
+ <Typography.Paragraph className="text-center text-default-600">
24
+ {description ?? "Native auth screens backed by @m5kdev/frontend."}
25
+ </Typography.Paragraph>
26
+ </View>
27
+ <Card className="gap-5 p-5">
28
+ <Text className="text-xl font-semibold text-foreground">{title}</Text>
29
+ {children}
30
+ </Card>
31
+ </View>
32
+ </ScrollView>
33
+ );
34
+ }
@@ -1,37 +1,37 @@
1
- import { Input, Label, TextField } from "heroui-native";
2
- import type { TextInputProps } from "react-native";
3
-
4
- export function FormTextField({
5
- label,
6
- testID,
7
- value,
8
- onChangeText,
9
- secureTextEntry,
10
- keyboardType,
11
- autoCapitalize = "none",
12
- editable = true,
13
- }: {
14
- label: string;
15
- testID: string;
16
- value: string;
17
- onChangeText: (value: string) => void;
18
- secureTextEntry?: boolean;
19
- keyboardType?: TextInputProps["keyboardType"];
20
- autoCapitalize?: TextInputProps["autoCapitalize"];
21
- editable?: boolean;
22
- }) {
23
- return (
24
- <TextField isRequired isDisabled={!editable}>
25
- <Label>{label}</Label>
26
- <Input
27
- testID={testID}
28
- value={value}
29
- onChangeText={onChangeText}
30
- secureTextEntry={secureTextEntry}
31
- keyboardType={keyboardType}
32
- autoCapitalize={autoCapitalize}
33
- editable={editable}
34
- />
35
- </TextField>
36
- );
37
- }
1
+ import { Input, Label, TextField } from "heroui-native";
2
+ import type { TextInputProps } from "react-native";
3
+
4
+ export function FormTextField({
5
+ label,
6
+ testID,
7
+ value,
8
+ onChangeText,
9
+ secureTextEntry,
10
+ keyboardType,
11
+ autoCapitalize = "none",
12
+ editable = true,
13
+ }: {
14
+ label: string;
15
+ testID: string;
16
+ value: string;
17
+ onChangeText: (value: string) => void;
18
+ secureTextEntry?: boolean;
19
+ keyboardType?: TextInputProps["keyboardType"];
20
+ autoCapitalize?: TextInputProps["autoCapitalize"];
21
+ editable?: boolean;
22
+ }) {
23
+ return (
24
+ <TextField isRequired isDisabled={!editable}>
25
+ <Label>{label}</Label>
26
+ <Input
27
+ testID={testID}
28
+ value={value}
29
+ onChangeText={onChangeText}
30
+ secureTextEntry={secureTextEntry}
31
+ keyboardType={keyboardType}
32
+ autoCapitalize={autoCapitalize}
33
+ editable={editable}
34
+ />
35
+ </TextField>
36
+ );
37
+ }
@@ -1,10 +1,10 @@
1
- import { Typography } from "heroui-native";
2
- import { View } from "react-native";
3
-
4
- export function LoadingScreen() {
5
- return (
6
- <View className="flex-1 items-center justify-center bg-background px-6">
7
- <Typography.Paragraph testID="loading-state">Loading auth session...</Typography.Paragraph>
8
- </View>
9
- );
10
- }
1
+ import { Typography } from "heroui-native";
2
+ import { View } from "react-native";
3
+
4
+ export function LoadingScreen() {
5
+ return (
6
+ <View className="flex-1 items-center justify-center bg-background px-6">
7
+ <Typography.Paragraph testID="loading-state">Loading auth session...</Typography.Paragraph>
8
+ </View>
9
+ );
10
+ }
@@ -1,22 +1,22 @@
1
- import { Text } from "react-native";
2
-
3
- export function StatusText({
4
- children,
5
- tone = "default",
6
- testID,
7
- }: {
8
- children: string | null;
9
- tone?: "default" | "error" | "success";
10
- testID?: string;
11
- }) {
12
- if (!children) return null;
13
-
14
- const toneClass =
15
- tone === "error" ? "text-danger" : tone === "success" ? "text-success" : "text-default-600";
16
-
17
- return (
18
- <Text testID={testID} className={`text-sm ${toneClass}`}>
19
- {children}
20
- </Text>
21
- );
22
- }
1
+ import { Text } from "react-native";
2
+
3
+ export function StatusText({
4
+ children,
5
+ tone = "default",
6
+ testID,
7
+ }: {
8
+ children: string | null;
9
+ tone?: "default" | "error" | "success";
10
+ testID?: string;
11
+ }) {
12
+ if (!children) return null;
13
+
14
+ const toneClass =
15
+ tone === "error" ? "text-danger" : tone === "success" ? "text-success" : "text-default-600";
16
+
17
+ return (
18
+ <Text testID={testID} className={`text-sm ${toneClass}`}>
19
+ {children}
20
+ </Text>
21
+ );
22
+ }
@@ -1,9 +1,9 @@
1
- import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
2
-
3
- export const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL ?? "http://127.0.0.1:8080";
4
-
5
- export const appUrl = process.env.EXPO_PUBLIC_APP_URL ?? "http://127.0.0.1:8081";
6
-
7
- export const isWaitlistProfile = process.env.EXPO_PUBLIC_ENABLE_WAITLIST === "true";
8
-
9
- export const appTitle = APP_NAME;
1
+ import { APP_NAME } from "{{PACKAGE_SCOPE}}/shared/modules/app/app.constants";
2
+
3
+ export const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL ?? "http://127.0.0.1:8080";
4
+
5
+ export const appUrl = process.env.EXPO_PUBLIC_APP_URL ?? "http://127.0.0.1:8081";
6
+
7
+ export const isWaitlistProfile = process.env.EXPO_PUBLIC_ENABLE_WAITLIST === "true";
8
+
9
+ export const appTitle = APP_NAME;
@@ -1,11 +1,11 @@
1
- @import "tailwindcss";
2
- @import "uniwind";
3
-
4
- @import "heroui-native/styles";
5
-
6
- /* End-user path: heroui-native lives in the project's node_modules, one
7
- level up from this src/global.css file. */
8
- /* biome-ignore lint/suspicious/noUnknownAtRules: Tailwind v4 source directive. */
9
- @source '../node_modules/heroui-native/lib';
10
- /* biome-ignore lint/suspicious/noUnknownAtRules: Tailwind v4 source directive for workspace package classes. */
11
- @source '../../../../packages/frontend/dist/src/**/*.{js,ts,jsx,tsx}';
1
+ @import "tailwindcss";
2
+ @import "uniwind";
3
+
4
+ @import "heroui-native/styles";
5
+
6
+ /* End-user path: heroui-native lives in the project's node_modules, one
7
+ level up from this src/global.css file. */
8
+ /* biome-ignore lint/suspicious/noUnknownAtRules: Tailwind v4 source directive. */
9
+ @source "../node_modules/heroui-native/lib";
10
+ /* biome-ignore lint/suspicious/noUnknownAtRules: Tailwind v4 source directive for workspace package classes. */
11
+ @source "../../../../packages/frontend/dist/src/**/*.{js,ts,jsx,tsx}";