Skip to content
124 linesCodeBlameRaw
1import { KeyRound, LogIn, Server as ServerIcon } from "lucide-react-native";
2import { useState } from "react";
3import { KeyboardAvoidingView, Platform, View } from "react-native";
4
5import { Button, Field, Screen, Text } from "@/components/ui";
6import { G1tMark } from "@/components/mark";
7import { SignInCancelled } from "@/lib/oauth";
8import { HOSTED, isHosted, parseServer } from "@/lib/server";
9import { useSession } from "@/lib/session";
10import { space } from "@/theme";
11
12/**
13 * Signing in. The usual way is **Sign in with g1t**: the site opens in the
14 * system's browser, the person approves the app, and it comes back signed
15 * in (lib/oauth.ts). **Use an access token** takes a personal token
16 * instead. **Server** points the app at a self-hosted g1t.
17 */
18export default function SignIn() {
19 const { state, signIn, signInWithToken, changeServer } = useSession();
20 const server = state.server ?? HOSTED;
21 const [mode, setMode] = useState<"oauth" | "token" | "server">("oauth");
22 const [busy, setBusy] = useState(false);
23 const [error, setError] = useState<string | null>(null);
24 const [token, setToken] = useState("");
25 const [site, setSite] = useState(server.site);
26 const [api, setApi] = useState(isHosted(server) ? "" : server.api);
27
28 const run = async (work: () => Promise<void>) => {
29 setBusy(true);
30 setError(null);
31 try {
32 await work();
33 } catch (failure) {
34 if (!(failure instanceof SignInCancelled)) setError(failure instanceof Error ? failure.message : "Sign-in failed.");
35 } finally {
36 setBusy(false);
37 }
38 };
39
40 return (
41 <Screen safe>
42 <KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : undefined} style={{ flex: 1 }}>
43 <View style={{ flex: 1, padding: space.xl, justifyContent: "center", gap: space.xl }}>
44 <View style={{ gap: space.md }}>
45 <G1tMark size={44} />
46 <Text size="title">Sign in to g1t</Text>
47 <Text tone="muted">Chat with your team and its agents, and review and merge what they make.</Text>
48 </View>
49
50 {mode === "oauth" ? (
51 <View style={{ gap: space.md }}>
52 <Button title="Sign in with g1t" icon={LogIn} busy={busy} onPress={() => run(signIn)} />
53 <Text size="small" tone="faint">
54 {new URL(server.site).host} opens in your browser to approve the app. Your password never reaches it.
55 </Text>
56 </View>
57 ) : null}
58
59 {mode === "token" ? (
60 <View style={{ gap: space.md }}>
61 <Field
62 label="Access token"
63 placeholder="g1t_…"
64 value={token}
65 onChangeText={setToken}
66 secureTextEntry
67 hint="Make one in Settings → Access tokens. For Chat, turn on “Use the website as you”."
68 />
69 <Button title="Sign in" busy={busy} disabled={!token.trim()} onPress={() => run(() => signInWithToken(token))} />
70 </View>
71 ) : null}
72
73 {mode === "server" ? (
74 <View style={{ gap: space.md }}>
75 <Field label="Site" placeholder="g1t.example.com" value={site} onChangeText={setSite} keyboardType="url" />
76 <Field
77 label="API (optional)"
78 placeholder={`api.${site ? site.replace(/^https?:\/\//, "") : "g1t.example.com"}`}
79 value={api}
80 onChangeText={setApi}
81 keyboardType="url"
82 hint="Left empty, the API is api. in front of the site's address."
83 />
84 <Button
85 title="Use this server"
86 busy={busy}
87 onPress={() =>
88 run(async () => {
89 const next = parseServer(site, api);
90 if (!next) throw new Error("That is not a web address. It looks like https://g1t.example.com.");
91 await changeServer(next);
92 setMode("oauth");
93 })
94 }
95 />
96 <Button
97 title="Use g1t.sh"
98 variant="secondary"
99 onPress={() => {
100 setSite(HOSTED.site);
101 setApi("");
102 void run(async () => {
103 await changeServer(HOSTED);
104 setMode("oauth");
105 });
106 }}
107 />
108 </View>
109 ) : null}
110
111 {error ? <Text tone="danger">{error}</Text> : null}
112
113 <View style={{ gap: space.sm }}>
114 {mode !== "oauth" ? <Button title="Sign in with g1t instead" variant="secondary" icon={LogIn} onPress={() => setMode("oauth")} /> : null}
115 {mode !== "token" ? <Button title="Use an access token" variant="secondary" icon={KeyRound} onPress={() => setMode("token")} /> : null}
116 {mode !== "server" ? (
117 <Button title={isHosted(server) ? "Server: g1t.sh" : `Server: ${new URL(server.site).host}`} variant="secondary" icon={ServerIcon} onPress={() => setMode("server")} />
118 ) : null}
119 </View>
120 </View>
121 </KeyboardAvoidingView>
122 </Screen>
123 );
124}