From 5fe330cd79be034e41f9c5181455e50e8c2fc5c1 Mon Sep 17 00:00:00 2001
From: MohamedBassem <me@mbassem.com>
Date: Sun, 22 Sep 2024 02:17:43 +0100
Subject: feature(mobile): Allow users to login with API keys in the mobile app

---
 apps/mobile/app/signin.tsx | 188 +++++++++++++++++++++++++++------------------
 1 file changed, 115 insertions(+), 73 deletions(-)

(limited to 'apps/mobile/app/signin.tsx')

diff --git a/apps/mobile/app/signin.tsx b/apps/mobile/app/signin.tsx
index 6190c258..df0c2e5f 100644
--- a/apps/mobile/app/signin.tsx
+++ b/apps/mobile/app/signin.tsx
@@ -8,37 +8,63 @@ import {
   TouchableWithoutFeedback,
   View,
 } from "react-native";
-import { Redirect, useRouter } from "expo-router";
+import { Redirect } from "expo-router";
 import Logo from "@/components/Logo";
 import { TailwindResolver } from "@/components/TailwindResolver";
-import { Button, buttonVariants } from "@/components/ui/Button";
+import { Button } from "@/components/ui/Button";
 import { Input } from "@/components/ui/Input";
 import useAppSettings from "@/lib/settings";
 import { api } from "@/lib/trpc";
-import { cn } from "@/lib/utils";
-import { Bug } from "lucide-react-native";
+
+enum LoginType {
+  Password,
+  ApiKey,
+}
 
 export default function Signin() {
-  const router = useRouter();
   const { settings, setSettings } = useAppSettings();
-  const [serverAddress, setServerAddress] = useState(settings.address);
 
   const [error, setError] = useState<string | undefined>();
-
-  const { mutate: login, isPending } = api.apiKeys.exchange.useMutation({
-    onSuccess: (resp) => {
-      setSettings({ ...settings, apiKey: resp.key, apiKeyId: resp.id });
-    },
-    onError: (e) => {
-      if (e.data?.code === "UNAUTHORIZED") {
-        setError("Wrong username or password");
+  const [loginType, setLoginType] = useState<LoginType>(LoginType.Password);
+  const toggleLoginType = () => {
+    setLoginType((prev) => {
+      if (prev === LoginType.Password) {
+        return LoginType.ApiKey;
       } else {
-        setError(`${e.message}`);
+        return LoginType.Password;
       }
-    },
-  });
+    });
+  };
+
+  const { mutate: login, isPending: userNamePasswordRequestIsPending } =
+    api.apiKeys.exchange.useMutation({
+      onSuccess: (resp) => {
+        setSettings({ ...settings, apiKey: resp.key, apiKeyId: resp.id });
+      },
+      onError: (e) => {
+        if (e.data?.code === "UNAUTHORIZED") {
+          setError("Wrong username or password");
+        } else {
+          setError(`${e.message}`);
+        }
+      },
+    });
+
+  const { mutate: validateApiKey, isPending: apiKeyValueRequestIsPending } =
+    api.apiKeys.validate.useMutation({
+      onSuccess: () => {
+        setSettings({ ...settings, apiKey: apiFormData.apiKey });
+      },
+      onError: (e) => {
+        if (e.data?.code === "UNAUTHORIZED") {
+          setError("Invalid API key");
+        } else {
+          setError(`${e.message}`);
+        }
+      },
+    });
 
-  const [formData, setFormData] = useState<{
+  const [usernameFormData, setUserNameFormData] = useState<{
     email: string;
     password: string;
   }>({
@@ -46,13 +72,23 @@ export default function Signin() {
     password: "",
   });
 
+  const [apiFormData, setApiFormData] = useState<{
+    apiKey: string;
+  }>({
+    apiKey: "",
+  });
+
   if (settings.apiKey) {
     return <Redirect href="dashboard" />;
   }
 
   const onSignin = () => {
-    const randStr = (Math.random() + 1).toString(36).substring(5);
-    login({ ...formData, keyName: `Mobile App: (${randStr})` });
+    if (loginType === LoginType.Password) {
+      const randStr = (Math.random() + 1).toString(36).substring(5);
+      login({ ...usernameFormData, keyName: `Mobile App: (${randStr})` });
+    } else if (loginType === LoginType.ApiKey) {
+      validateApiKey({ apiKey: apiFormData.apiKey });
+    }
   };
 
   return (
@@ -76,66 +112,72 @@ export default function Signin() {
           {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={serverAddress}
-              autoCapitalize="none"
-              keyboardType="url"
-              onChangeText={(e) => {
-                setServerAddress(e);
-                setSettings({ ...settings, address: e.replace(/\/$/, "") });
-              }}
-            />
-          </View>
-          <View className="gap-2">
-            <Text className="font-bold">Email</Text>
-            <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
-              value={formData.password}
-              autoCapitalize="none"
-              textContentType="password"
-              onChangeText={(e) => setFormData((s) => ({ ...s, password: e }))}
-            />
-          </View>
+          {loginType === LoginType.Password && (
+            <>
+              <View className="gap-2">
+                <Text className="font-bold">Email</Text>
+                <Input
+                  className="w-full"
+                  placeholder="Email"
+                  keyboardType="email-address"
+                  autoCapitalize="none"
+                  value={usernameFormData.email}
+                  onChangeText={(e) =>
+                    setUserNameFormData((s) => ({ ...s, email: e }))
+                  }
+                />
+              </View>
+              <View className="gap-2">
+                <Text className="font-bold">Password</Text>
+                <Input
+                  className="w-full"
+                  placeholder="Password"
+                  secureTextEntry
+                  value={usernameFormData.password}
+                  autoCapitalize="none"
+                  textContentType="password"
+                  onChangeText={(e) =>
+                    setUserNameFormData((s) => ({ ...s, password: e }))
+                  }
+                />
+              </View>
+            </>
+          )}
+
+          {loginType === LoginType.ApiKey && (
+            <View className="gap-2">
+              <Text className="font-bold">API Key</Text>
+              <Input
+                className="w-full"
+                placeholder="API Key"
+                secureTextEntry
+                value={apiFormData.apiKey}
+                autoCapitalize="none"
+                textContentType="password"
+                onChangeText={(e) =>
+                  setApiFormData((s) => ({ ...s, apiKey: e }))
+                }
+              />
+            </View>
+          )}
+
           <View className="flex flex-row items-center justify-between gap-2">
             <Button
               className="flex-1"
               label="Sign In"
               onPress={onSignin}
-              disabled={isPending}
+              disabled={
+                userNamePasswordRequestIsPending || apiKeyValueRequestIsPending
+              }
             />
-            <Pressable
-              className={cn(
-                buttonVariants({ variant: "default" }),
-                !settings.address && "bg-gray-500",
-              )}
-              onPress={() => router.push("/test-connection")}
-              disabled={!settings.address}
-            >
-              <TailwindResolver
-                comp={(styles) => (
-                  <Bug size={20} color={styles?.color?.toString()} />
-                )}
-                className="text-background"
-              />
-            </Pressable>
           </View>
+          <Pressable onPress={toggleLoginType}>
+            <Text className="mt-2 text-center text-gray-500">
+              {loginType === LoginType.Password
+                ? "Use API key instead?"
+                : "Use password instead?"}
+            </Text>
+          </Pressable>
         </View>
       </TouchableWithoutFeedback>
     </KeyboardAvoidingView>
-- 
cgit v1.3-1-g0d28

