aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-11 12:24:51 +0000
committerMohamedBassem <me@mbassem.com>2024-03-11 12:33:31 +0000
commitc87db85815d84ddf907d0a1d26226a2ab911181b (patch)
treec909031a7c9b525871fc8938e98c38da4741bde9 /packages
parent999ed977a588b2c3b2055f18db4218d77882a1a1 (diff)
downloadkarakeep-c87db85815d84ddf907d0a1d26226a2ab911181b.tar.zst
mobile: An ugly yet functional signin workflow
Diffstat (limited to 'packages')
-rw-r--r--packages/mobile/app/_layout.tsx4
-rw-r--r--packages/mobile/app/dashboard.tsx35
-rw-r--r--packages/mobile/app/index.tsx7
-rw-r--r--packages/mobile/app/signin.tsx83
-rw-r--r--packages/mobile/lib/providers.tsx5
-rw-r--r--packages/mobile/lib/settings.ts28
-rw-r--r--packages/mobile/lib/storage-state.ts50
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];
+}