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.
- package/dist/src/catalog.cjs +21 -1
- package/dist/src/catalog.cjs.map +1 -1
- package/dist/src/catalog.d.cts +3 -1
- package/dist/src/create.cjs +6 -2
- package/dist/src/create.cjs.map +1 -1
- package/dist/src/doctor.cjs.map +1 -1
- package/dist/src/fs.cjs +10 -5
- package/dist/src/fs.cjs.map +1 -1
- package/dist/src/prompts.cjs +32 -0
- package/dist/src/prompts.cjs.map +1 -1
- package/dist/src/prompts.d.cts +7 -1
- package/dist/src/template.cjs +23 -4
- package/dist/src/template.cjs.map +1 -1
- package/dist/src/template.d.cts +17 -3
- package/dist/src/types.d.cts +11 -4
- package/package.json +4 -1
- package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +1 -1
- package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +3 -3
- package/templates/minimal-app/.cursor/rules/module-module-guide.mdc +3 -2
- package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +1 -1
- package/templates/minimal-app/.dockerignore.tpl +31 -0
- package/templates/minimal-app/.gitignore.tpl +1 -0
- package/templates/minimal-app/AGENTS.md.tpl +13 -10
- package/templates/minimal-app/CLAUDE.md.tpl +132 -131
- package/templates/minimal-app/README.md.tpl +50 -19
- package/templates/minimal-app/apps/e2e/package.json.tpl +23 -23
- package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +51 -39
- package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -38
- package/templates/minimal-app/apps/e2e/servers.ts.tpl +136 -112
- package/templates/minimal-app/apps/e2e/tests/conversation.spec.ts.tpl +22 -0
- package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -47
- package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -73
- package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -517
- package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -94
- package/templates/minimal-app/apps/e2e/tests/files.spec.ts.tpl +35 -0
- package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -289
- package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -516
- package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -107
- package/templates/minimal-app/apps/e2e/tests/workflow.spec.ts.tpl +26 -0
- package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -10
- package/templates/minimal-app/apps/email/package.json.tpl +34 -34
- package/templates/minimal-app/apps/email/src/emails/trialEndingEmail.tsx.tpl +40 -0
- package/templates/minimal-app/apps/email/src/index.ts.tpl +7 -0
- package/templates/minimal-app/apps/email/src/resources.ts.tpl +72 -65
- package/templates/minimal-app/apps/expo/app.json.tpl +36 -36
- package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -8
- package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -16
- package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -31
- package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -151
- package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -53
- package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -8
- package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -71
- package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -19
- package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -85
- package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -40
- package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -75
- package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -193
- package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -34
- package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -37
- package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -10
- package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -22
- package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -9
- package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -11
- package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -20
- package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -11
- package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -1
- package/templates/minimal-app/apps/landing/.env.production.example.tpl +7 -0
- package/templates/minimal-app/apps/landing/Dockerfile.tpl +39 -0
- package/templates/minimal-app/apps/landing/fly.toml.tpl +20 -0
- package/templates/minimal-app/apps/landing/index.html.tpl +35 -0
- package/templates/minimal-app/apps/landing/package.json.tpl +34 -0
- package/templates/minimal-app/apps/landing/scripts/fly-deploy.mjs.tpl +75 -0
- package/templates/minimal-app/apps/landing/scripts/fly-secrets.mjs.tpl +31 -0
- package/templates/minimal-app/apps/landing/src/App.tsx.tpl +12 -0
- package/templates/minimal-app/apps/landing/src/LandingPage.tsx.tpl +37 -0
- package/templates/minimal-app/apps/landing/src/index.css.tpl +24 -0
- package/templates/minimal-app/apps/landing/src/main.tsx.tpl +16 -0
- package/templates/minimal-app/apps/landing/src/vite-env.d.ts.tpl +11 -0
- package/templates/minimal-app/apps/landing/tsconfig.json.tpl +12 -0
- package/templates/minimal-app/apps/landing/vite.config.ts.tpl +25 -0
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +54 -46
- package/templates/minimal-app/apps/server/{drizzle/seed.e2e.ts.tpl → db.e2e.ts.tpl} +57 -40
- package/templates/minimal-app/apps/server/{drizzle/seed.ts.tpl → db.ts.tpl} +27 -20
- package/templates/minimal-app/apps/server/drizzle/0003_silly_the_liberteens.sql +27 -0
- package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +56 -166
- package/templates/minimal-app/apps/server/drizzle/meta/0001_snapshot.json.tpl +1796 -1910
- package/templates/minimal-app/apps/server/drizzle/meta/0002_snapshot.json.tpl +1818 -1932
- package/templates/minimal-app/apps/server/drizzle/meta/0003_snapshot.json.tpl +2137 -0
- package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +8 -1
- package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +3 -33
- package/templates/minimal-app/apps/server/package.json.tpl +5 -3
- package/templates/minimal-app/apps/server/src/app.ts.tpl +36 -0
- package/templates/minimal-app/apps/server/src/modules/conversation/conversation.mastra.ts.tpl +26 -0
- package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.module.ts.tpl +47 -0
- package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.service.ts.tpl +43 -0
- package/templates/minimal-app/apps/server/src/modules/demo-workflow/demo-workflow.trpc.ts.tpl +16 -0
- package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +28 -28
- package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +28 -28
- package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +54 -54
- package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -67
- package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +116 -116
- package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -37
- package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -279
- package/templates/minimal-app/apps/server/src/schema.ts.tpl +31 -20
- package/templates/minimal-app/apps/server/src/types.ts.tpl +12 -12
- package/templates/minimal-app/apps/server/tsconfig.json.tpl +1 -1
- package/templates/minimal-app/apps/server/tsup.config.ts.tpl +23 -9
- package/templates/minimal-app/apps/shared/.env.example.tpl +5 -0
- package/templates/minimal-app/apps/shared/.env.production.example.tpl +23 -0
- package/templates/minimal-app/apps/shared/.env.tpl +5 -0
- package/templates/minimal-app/apps/shared/Dockerfile.tpl +48 -0
- package/templates/minimal-app/apps/shared/fly.toml.tpl +24 -0
- package/templates/minimal-app/apps/shared/package.json.tpl +25 -24
- package/templates/minimal-app/apps/shared/scripts/fly-deploy.mjs.tpl +75 -0
- package/templates/minimal-app/apps/shared/scripts/fly-secrets.mjs.tpl +31 -0
- package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -28
- package/templates/minimal-app/apps/shared/src/modules/conversation/conversation.constants.ts.tpl +1 -0
- package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
- package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
- package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -25
- package/templates/minimal-app/apps/webapp/package.json.tpl +53 -53
- package/templates/minimal-app/apps/webapp/public/push-sw.js.tpl +34 -34
- package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +46 -44
- package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +96 -65
- package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +114 -93
- package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -15
- package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
- package/templates/minimal-app/apps/webapp/src/modules/conversation/ConversationRoute.tsx.tpl +36 -0
- package/templates/minimal-app/apps/webapp/src/modules/files/FilesRoute.tsx.tpl +88 -0
- package/templates/minimal-app/apps/webapp/src/modules/files/hooks/useFilesRoute.ts.tpl +50 -0
- package/templates/minimal-app/apps/webapp/src/modules/workflows/WorkflowsRoute.tsx.tpl +97 -0
- package/templates/minimal-app/apps/webapp/src/modules/workflows/hooks/useWorkflowsRoute.ts.tpl +64 -0
- package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
- package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -10
- package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
- package/templates/minimal-app/apps/webapp/translations/en/app.json.tpl +7 -7
- package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +193 -135
- package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
- package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
- package/templates/minimal-app/biome.json.tpl +17 -12
- package/templates/minimal-app/package.json.tpl +6 -2
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +14 -7
- package/templates/minimal-app/template.manifest.json +92 -2
- package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +0 -26
- package/templates/minimal-app/apps/server/drizzle/guard.ts.tpl +0 -36
- package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +0 -40
- 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
|
+
}
|