aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/mobile/app/_layout.tsx10
-rw-r--r--apps/mobile/app/dashboard/_layout.tsx2
-rw-r--r--apps/mobile/app/index.tsx2
-rw-r--r--apps/mobile/app/server-address.tsx87
-rw-r--r--apps/mobile/app/signin.tsx188
5 files changed, 214 insertions, 75 deletions
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx
index 677a0638..5eadad80 100644
--- a/apps/mobile/app/_layout.tsx
+++ b/apps/mobile/app/_layout.tsx
@@ -52,6 +52,16 @@ export default function RootLayout() {
}}
>
<Stack.Screen name="index" />
+ <Stack.Screen
+ name="signin"
+ options={{
+ headerShown: true,
+ headerBackVisible: true,
+ headerBackTitle: "Back",
+ title: "",
+ }}
+ />
+ <Stack.Screen name="server-address" />
<Stack.Screen name="sharing" />
<Stack.Screen
name="test-connection"
diff --git a/apps/mobile/app/dashboard/_layout.tsx b/apps/mobile/app/dashboard/_layout.tsx
index 22d1ed07..a1a25398 100644
--- a/apps/mobile/app/dashboard/_layout.tsx
+++ b/apps/mobile/app/dashboard/_layout.tsx
@@ -19,7 +19,7 @@ export default function Dashboard() {
const isLoggedIn = useIsLoggedIn();
useEffect(() => {
if (isLoggedIn !== undefined && !isLoggedIn) {
- return router.replace("signin");
+ return router.replace("server-address");
}
}, [isLoggedIn]);
diff --git a/apps/mobile/app/index.tsx b/apps/mobile/app/index.tsx
index dbbea97e..702269a5 100644
--- a/apps/mobile/app/index.tsx
+++ b/apps/mobile/app/index.tsx
@@ -11,6 +11,6 @@ export default function App() {
} else if (isLoggedIn) {
return <Redirect href="dashboard" />;
} else {
- return <Redirect href="signin" />;
+ return <Redirect href="server-address" />;
}
}
diff --git a/apps/mobile/app/server-address.tsx b/apps/mobile/app/server-address.tsx
new file mode 100644
index 00000000..c34806b3
--- /dev/null
+++ b/apps/mobile/app/server-address.tsx
@@ -0,0 +1,87 @@
+import { useState } from "react";
+import {
+ Keyboard,
+ KeyboardAvoidingView,
+ Platform,
+ Pressable,
+ Text,
+ TouchableWithoutFeedback,
+ View,
+} from "react-native";
+import { Redirect, useRouter } from "expo-router";
+import Logo from "@/components/Logo";
+import { TailwindResolver } from "@/components/TailwindResolver";
+import { Button, buttonVariants } from "@/components/ui/Button";
+import { Input } from "@/components/ui/Input";
+import useAppSettings from "@/lib/settings";
+import { cn } from "@/lib/utils";
+import { Bug } from "lucide-react-native";
+
+export default function ServerAddress() {
+ const router = useRouter();
+ const { settings, setSettings } = useAppSettings();
+ const [serverAddress, setServerAddress] = useState(settings.address);
+
+ if (settings.apiKey) {
+ return <Redirect href="dashboard" />;
+ }
+
+ return (
+ <KeyboardAvoidingView
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
+ >
+ <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
+ <View className="flex h-full flex-col justify-center gap-2 px-4">
+ <View className="items-center">
+ <TailwindResolver
+ className="color-foreground"
+ comp={(styles) => (
+ <Logo
+ height={150}
+ width={200}
+ fill={styles?.color?.toString()}
+ />
+ )}
+ />
+ </View>
+ <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="flex flex-row items-center justify-between gap-2">
+ <Button
+ className="flex-1"
+ label="Next"
+ onPress={() => router.push("/signin")}
+ />
+ <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>
+ </View>
+ </TouchableWithoutFeedback>
+ </KeyboardAvoidingView>
+ );
+}
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>