diff options
| author | MohamedBassem <me@mbassem.com> | 2024-03-11 12:24:51 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-03-11 12:33:31 +0000 |
| commit | c87db85815d84ddf907d0a1d26226a2ab911181b (patch) | |
| tree | c909031a7c9b525871fc8938e98c38da4741bde9 /packages/mobile | |
| parent | 999ed977a588b2c3b2055f18db4218d77882a1a1 (diff) | |
| download | karakeep-c87db85815d84ddf907d0a1d26226a2ab911181b.tar.zst | |
mobile: An ugly yet functional signin workflow
Diffstat (limited to 'packages/mobile')
| -rw-r--r-- | packages/mobile/app/_layout.tsx | 4 | ||||
| -rw-r--r-- | packages/mobile/app/dashboard.tsx | 35 | ||||
| -rw-r--r-- | packages/mobile/app/index.tsx | 7 | ||||
| -rw-r--r-- | packages/mobile/app/signin.tsx | 83 | ||||
| -rw-r--r-- | packages/mobile/lib/providers.tsx | 5 | ||||
| -rw-r--r-- | packages/mobile/lib/settings.ts | 28 | ||||
| -rw-r--r-- | packages/mobile/lib/storage-state.ts | 50 |
7 files changed, 182 insertions, 30 deletions
diff --git a/packages/mobile/app/_layout.tsx b/packages/mobile/app/_layout.tsx index b37585e2..d3cbbee1 100644 --- a/packages/mobile/app/_layout.tsx +++ b/packages/mobile/app/_layout.tsx @@ -11,9 +11,7 @@ import { Providers } from "@/lib/providers"; export default function RootLayout() { const router = useRouter(); - const { hasShareIntent, shareIntent, resetShareIntent } = useShareIntent({ - debug: true, - }); + const { hasShareIntent, shareIntent, resetShareIntent } = useShareIntent(); useEffect(() => { if (hasShareIntent) { diff --git a/packages/mobile/app/dashboard.tsx b/packages/mobile/app/dashboard.tsx new file mode 100644 index 00000000..8be57615 --- /dev/null +++ b/packages/mobile/app/dashboard.tsx @@ -0,0 +1,35 @@ +import { useRouter } from "expo-router"; +import { useEffect } from "react"; +import { Text, View } from "react-native"; + +import Logo from "@/components/Logo"; +import { Button } from "@/components/ui/Button"; +import useAppSettings from "@/lib/settings"; +import { api } from "@/lib/trpc"; + +export default function Main() { + const router = useRouter(); + const { settings, setSettings, isLoading } = useAppSettings(); + + useEffect(() => { + if (!isLoading && !settings.apiKey) { + router.replace("signin"); + } + }, [settings, isLoading]); + + const onLogout = () => { + setSettings({ ...settings, apiKey: undefined }); + }; + + const { data } = api.users.whoami.useQuery(); + + return ( + <View className="flex h-full items-center justify-center gap-4 px-4"> + <Logo /> + <Text className="justify-center"> + Logged in as: {isLoading ? "Loading ..." : data?.email} + </Text> + <Button label="Log Out" onPress={onLogout} /> + </View> + ); +} diff --git a/packages/mobile/app/index.tsx b/packages/mobile/app/index.tsx index e352ba54..557417d7 100644 --- a/packages/mobile/app/index.tsx +++ b/packages/mobile/app/index.tsx @@ -3,10 +3,9 @@ import { View } from "react-native"; export default function App() { return ( - <View className="flex-1 items-center justify-center bg-white"> - <Link href="/signin" className=""> - Signin - </Link> + <View className="flex-1 items-center justify-center gap-4 bg-white"> + <Link href="signin">Signin</Link> + <Link href="dashboard">Dashboard</Link> </View> ); } diff --git a/packages/mobile/app/signin.tsx b/packages/mobile/app/signin.tsx index 95a1cf48..a89b0087 100644 --- a/packages/mobile/app/signin.tsx +++ b/packages/mobile/app/signin.tsx @@ -1,24 +1,101 @@ +import { useRouter } from "expo-router"; +import { useEffect, useState } from "react"; import { View, Text } from "react-native"; import Logo from "@/components/Logo"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; +import useAppSettings from "@/lib/settings"; +import { api } from "@/lib/trpc"; export default function Signin() { + const router = useRouter(); + + const { settings, setSettings } = useAppSettings(); + + const [error, setError] = useState<string | undefined>(); + + const { mutate: login, isPending } = api.apiKeys.exchange.useMutation({ + onSuccess: (resp) => { + setSettings({ ...settings, apiKey: resp.key }); + router.replace("dashboard"); + }, + onError: (e) => { + if (e.data?.code === "UNAUTHORIZED") { + setError("Wrong username or password"); + } else { + setError(`${e.message}`); + } + }, + }); + + const [formData, setFormData] = useState<{ + email: string; + password: string; + }>({ + email: "", + password: "", + }); + + useEffect(() => { + if (settings.apiKey) { + router.navigate("dashboard"); + } + }, [settings]); + + const onSignin = () => { + const randStr = (Math.random() + 1).toString(36).substring(5); + login({ ...formData, keyName: `Mobile App: (${randStr})` }); + }; + return ( <View className="flex h-full flex-col justify-center gap-2 px-4"> <View className="items-center"> <Logo /> </View> + {error && ( + <Text className="w-full text-center text-red-500">{error}</Text> + )} + <View className="gap-2"> + <Text className="font-bold">Server Address</Text> + <Input + className="w-full" + placeholder="Server Address" + value={settings.address} + autoCapitalize="none" + keyboardType="url" + onEndEditing={(e) => + setSettings({ ...settings, address: e.nativeEvent.text }) + } + /> + </View> <View className="gap-2"> <Text className="font-bold">Email</Text> - <Input className="w-full" placeholder="Email" /> + <Input + className="w-full" + placeholder="Email" + keyboardType="email-address" + autoCapitalize="none" + value={formData.email} + onChangeText={(e) => setFormData((s) => ({ ...s, email: e }))} + /> </View> <View className="gap-2"> <Text className="font-bold">Password</Text> - <Input className="w-full" placeholder="Password" secureTextEntry /> + <Input + className="w-full" + placeholder="Password" + secureTextEntry + value={formData.password} + onChangeText={(e) => setFormData((s) => ({ ...s, password: e }))} + /> </View> - <Button className="w-full" label="Sign In" /> + <Button + className="w-full" + label="Sign In" + onPress={onSignin} + disabled={isPending} + /> </View> ); } diff --git a/packages/mobile/lib/providers.tsx b/packages/mobile/lib/providers.tsx index d5638da8..394bad28 100644 --- a/packages/mobile/lib/providers.tsx +++ b/packages/mobile/lib/providers.tsx @@ -14,7 +14,10 @@ function getTRPCClient(address: string) { async headers() { const settings = await getAppSettings(); return { - Authorization: settings ? `Bearer ${settings.apiKey}` : undefined, + Authorization: + settings && settings.apiKey + ? `Bearer ${settings.apiKey}` + : undefined, }; }, transformer: superjson, diff --git a/packages/mobile/lib/settings.ts b/packages/mobile/lib/settings.ts index 85296cfa..21f40528 100644 --- a/packages/mobile/lib/settings.ts +++ b/packages/mobile/lib/settings.ts @@ -1,33 +1,23 @@ import * as SecureStore from "expo-secure-store"; -import { useEffect, useState } from "react"; + +import { useStorageState } from "./storage-state"; const SETTING_NAME = "settings"; export type Settings = { - apiKey: string; + apiKey?: string; address: string; }; export default function useAppSettings() { - const [settings, setSettings] = useState<Settings>({ - apiKey: "", - address: "", - }); - - useEffect(() => { - SecureStore.setItemAsync(SETTING_NAME, JSON.stringify(settings)); - }, [settings]); + let [[isLoading, settings], setSettings] = + useStorageState<Settings>(SETTING_NAME); - useEffect(() => { - SecureStore.getItemAsync(SETTING_NAME).then((val) => { - if (!val) { - return; - } - setSettings(JSON.parse(val)); - }); - }, []); + settings ||= { + address: "https://demo.hoarder.app", + }; - return { settings, setSettings }; + return { settings, setSettings, isLoading }; } export async function getAppSettings() { diff --git a/packages/mobile/lib/storage-state.ts b/packages/mobile/lib/storage-state.ts new file mode 100644 index 00000000..09917c79 --- /dev/null +++ b/packages/mobile/lib/storage-state.ts @@ -0,0 +1,50 @@ +import * as SecureStore from "expo-secure-store"; +import * as React from "react"; + +type UseStateHook<T> = [[boolean, T | null], (value: T | null) => void]; + +function useAsyncState<T>( + initialValue: [boolean, T | null] = [true, null], +): UseStateHook<T> { + return React.useReducer( + ( + state: [boolean, T | null], + action: T | null = null, + ): [boolean, T | null] => [false, action], + initialValue, + ) as UseStateHook<T>; +} + +export async function setStorageItemAsync(key: string, value: string | null) { + if (value == null) { + await SecureStore.deleteItemAsync(key); + } else { + await SecureStore.setItemAsync(key, value); + } +} + +export function useStorageState<T>(key: string): UseStateHook<T> { + // Public + const [state, setState] = useAsyncState<T>(); + + // Get + React.useEffect(() => { + SecureStore.getItemAsync(key).then((value) => { + if (!value) { + return null; + } + setState(JSON.parse(value)); + }); + }, [key]); + + // Set + const setValue = React.useCallback( + (value: T | null) => { + setState(value); + setStorageItemAsync(key, JSON.stringify(value)); + }, + [key], + ); + + return [state, setValue]; +} |
