Skip to content

Commit

Permalink
auth
Browse files Browse the repository at this point in the history
  • Loading branch information
nikgraf committed Jun 1, 2024
1 parent 78a44e0 commit 65b0dd9
Show file tree
Hide file tree
Showing 19 changed files with 1,063 additions and 108 deletions.
20 changes: 11 additions & 9 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"@expo/metro-runtime": "~3.2.1",
"@expo/vector-icons": "^14.0.0",
"@react-native/assets-registry": "0.75.0-main",
"@tanstack/react-query": "^5.40.0",
"@trpc/client": "^11.0.0-rc.382",
"babel-preset-expo": "~11.0.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand All @@ -29,17 +31,18 @@
"lucide-react-native": "^0.381.0",
"nativewind": "^4.0.1",
"position-strings": "^2.0.1",
"react": "18.3.1",
"react-dom": "^18.2.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.1",
"react-native-libsodium": "^1.3.1",
"react-native-reanimated": "~3.11.0",
"react-native-safe-area-context": "4.10.3",
"react-native-opaque": "^0.3.1",
"react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "3.31.1",
"react-native-svg": "15.3.0",
"react-native-svg": "15.2.0",
"react-native-web": "~0.19.6",
"secsync-react-yjs": "^0.4.0",
"secsync": "^0.4.0",
"secsync-react-yjs": "^0.4.0",
"tailwind-merge": "^2.3.0",
"tailwindcss": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
Expand All @@ -49,14 +52,13 @@
"devDependencies": {
"@babel/core": "^7.24.3",
"@babel/runtime": "^7.24.1",
"@types/react": "~18.3.3",
"babel-plugin-transform-vite-meta-env": "^1.0.3",
"@types/react": "~18.2.79",
"eslint": "^9.3.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.5",
"prettier": "3.2.5",
"prettier-plugin-tailwindcss": "^0.6.0",
"typescript": "~5.4.5"
"typescript": "~5.3.3"
},
"resolutions": {
"@effect/schema": "=0.64.16"
Expand Down
102 changes: 87 additions & 15 deletions apps/app/src/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { Theme, ThemeProvider } from "@react-navigation/native";
import {
MutationCache,
QueryCache,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import * as React from "react";
import { useState } from "react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { NAV_THEME } from "~/lib/constants";
import { useColorScheme } from "~/lib/useColorScheme";
import "../global.css";
import useLoadingLibsodium from "../hooks/useLoadingLibsodium";
import { trpc } from "../utils/trpc";

const LIGHT_THEME: Theme = {
dark: false,
Expand All @@ -17,29 +26,92 @@ const DARK_THEME: Theme = {
colors: NAV_THEME.dark,
};

// TODO PROD API URL
const apiUrl = "http://localhost:3030/api";

export default function Layout() {
const { isDarkColorScheme } = useColorScheme();
const isLoadingComplete = useLoadingLibsodium();

const [queryClient] = useState(
() =>
new QueryClient({
queryCache: new QueryCache({
// TODO
// onError: (error) => {
// if (
// error instanceof TRPCClientError &&
// error.data?.code === "UNAUTHORIZED" &&
// window.location.pathname !== "/login"
// ) {
// removeLocalDb();
// queryClient.clear();
// router.navigate({
// to: "/login",
// search: { redirect: window.location.pathname },
// });
// }
// },
}),
mutationCache: new MutationCache({
// TODO
// onError: (error) => {
// if (
// error instanceof TRPCClientError &&
// error.data?.code === "UNAUTHORIZED" &&
// window.location.pathname !== "/login"
// ) {
// removeLocalDb();
// queryClient.clear();
// router.navigate({
// to: "/login",
// search: { redirect: window.location.pathname },
// });
// }
// },
}),
})
);
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: apiUrl,
fetch(url, options) {
return fetch(url, {
...options,
credentials: "include",
});
},
}),
],
})
);

if (!isLoadingComplete) {
return null;
}

return (
<SafeAreaProvider>
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
<Stack>
<Stack.Screen
name="index"
options={{
// Hide the header for all other routes.
title: "Lists",
}}
/>
</Stack>
{/* Default Portal Host (one per app) */}
</ThemeProvider>
</SafeAreaProvider>
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<SafeAreaProvider>
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
<Stack>
<Stack.Screen
name="index"
options={{
// Hide the header for all other routes.
title: "Lists",
}}
/>
</Stack>
{/* Default Portal Host (one per app) */}
{/* <PortalHost /> */}
</ThemeProvider>
</SafeAreaProvider>
</QueryClientProvider>
</trpc.Provider>
);
}
14 changes: 11 additions & 3 deletions apps/app/src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ import * as React from "react";
import { Text } from "react-native";

const Lists: React.FC = () => {
const workouts: { id: string; startedAt: string }[] = [];

return (
<Text>
Hello WOrld <Link href="/list/wow">LIST A</Link>
Hello WOrld{" "}
<Link
href={{
pathname: "/list/[listId]",
params: { listId: "wow" },
}}
>
LIST A
</Link>
<Link href="/login">Login</Link>
<Link href="/register">Register</Link>
</Text>
);
};
Expand Down
48 changes: 48 additions & 0 deletions apps/app/src/app/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { View } from "react-native";
import { AuthForm } from "src/components/authForm";
import { Text } from "~/components/ui/text";
import { AlertCircle } from "~/lib/icons/AlertCircle";
import { useLogin } from "../hooks/useLogin";

const Login = () => {
const { login, isPending } = useLogin();
const [error, setError] = useState<string | null>(null);
const { redirect } = useLocalSearchParams<{ redirect?: string }>();

return (
<View className="max-w-md mr-auto ml-auto">
<AuthForm
onSubmit={async ({ password, username }) => {
const sessionKey = await login({
userIdentifier: username,
password,
});
if (!sessionKey) {
setError("Failed to login");
return;
}
if (redirect) {
router.navigate(redirect);
return;
}
router.navigate("/");
}}
children={<Text>Login</Text>}
isPending={isPending}
/>

{error && (
<View className="mt-4">
<AlertCircle className="h-4 w-4" />
{/* TODO proper styling */}
<Text>Error</Text>
<Text>Failed to log in</Text>
</View>
)}
</View>
);
};

export default Login;
47 changes: 47 additions & 0 deletions apps/app/src/app/register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { View } from "react-native";
import { Text } from "~/components/ui/text";
import { AlertCircle } from "~/lib/icons/AlertCircle";
import { AuthForm } from "../components/authForm";
import { useRegisterAndLogin } from "../hooks/useRegisterAndLogin";

const Register = () => {
const { registerAndLogin, isPending } = useRegisterAndLogin();
const { redirect } = useLocalSearchParams<{ redirect?: string }>();
const [error, setError] = useState<string | null>(null);

return (
<View className="max-w-md mr-auto ml-auto">
<AuthForm
onSubmit={async ({ password, username }) => {
const sessionKey = await registerAndLogin({
userIdentifier: username,
password,
});
if (!sessionKey) {
setError("Failed to register");
return;
}
if (redirect) {
router.navigate(redirect);
return;
}
router.navigate("/");
}}
children={<Text>Register</Text>}
isPending={isPending}
/>
{error && (
<View className="mt-4">
<AlertCircle className="h-4 w-4" />
{/* TODO proper styling */}
<Text>Error</Text>
<Text>Failed to register</Text>
</View>
)}
</View>
);
};

export default Register;
57 changes: 57 additions & 0 deletions apps/app/src/components/authForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useState } from "react";
import { View } from "react-native";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Text } from "~/components/ui/text";

type Props = {
onSubmit: (params: { username: string; password: string }) => void;
isPending: boolean;
children: React.ReactNode;
};

export const AuthForm = ({ onSubmit, isPending, children }: Props) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");

return (
<View>
<Text className="text-xl text-center font-semibold mb-8 mt-12">
{children}
</Text>

<View className="gap-4 flex flex-col">
<Input
// required TODO
className="border border-slate-300 p-2 rounded"
placeholder="Username"
autoComplete="off"
value={username}
onChangeText={(value) => {
setUsername(value);
}}
/>

<Input
// required
className="border border-slate-300 p-2 rounded"
placeholder="Password"
autoComplete="off"
value={password}
onChangeText={(value) => {
setPassword(value);
}}
/>

<Button
disabled={isPending}
onPress={() => {
onSubmit({ username, password });
}}
>
{children}
</Button>
</View>
</View>
);
};
24 changes: 24 additions & 0 deletions apps/app/src/hooks/useInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useRef } from "react";

type IntervalFunc = () => unknown | void;

// Inspired by https://overreacted.io/making-setinterval-declarative-with-react-hooks/
export const useInterval = (callback: IntervalFunc, delay: number | null) => {
const savedCallback = useRef<IntervalFunc | null>(null);

useEffect(() => {
if (delay === null) return;
savedCallback.current = callback;
});

useEffect(() => {
if (delay === null) return;
function tick() {
if (savedCallback.current !== null) {
savedCallback.current();
}
}
const id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
};
Loading

0 comments on commit 65b0dd9

Please sign in to comment.