Skip to content
61 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The phone app's scaffolding, in apps/mobile: Expo SDK 57 with Expo Router, Android first, with Chat, Code and Customize. It signs in with g1t's own OAuth 2.1 (PKCE, a client id that encodes "g1t for Android" and g1t://oauth/callback, so nothing is registered), keeps the server and credentials in the phone's keystore, refreshes the 30-day token a minute early and shares the single-use refresh between requests. A personal access token is the other way in, and a self-hosted g1t the other server.1import * as SecureStore from "expo-secure-store";
2
3import { HOSTED, type Server } from "./server";
4
5/**
6 * What the app keeps between launches, in the phone's keystore (Android
7 * Keystore, the iOS keychain): the server, and how it is signed in.
8 *
9 * Two ways to be signed in:
10 * - `oauth`: the app's own sign-in (lib/oauth.ts). An access token that
11 * lasts 30 days and a refresh token that works once and returns the
12 * next pair, as every OAuth client of g1t's gets.
13 * - `token`: a personal access token pasted in, for a g1t whose OAuth
14 * tokens do not yet open Chat (see the README) or for testing. One with
15 * "Use the website as you" turned on opens everything the app shows.
16 */
17export type Credentials =
18 | {
19 kind: "oauth";
20 accessToken: string;
21 refreshToken: string;
22 /** Epoch milliseconds. */
23 expiresAt: number;
24 /** The scopes the person granted, or `*` for full access. */
25 scope: string;
26 }
27 | { kind: "token"; accessToken: string };
28
29const SERVER_KEY = "g1t.server";
30const CREDENTIALS_KEY = "g1t.credentials";
31
32async function readJson<T>(key: string): Promise<T | null> {
33 try {
34 const text = await SecureStore.getItemAsync(key);
35 return text ? (JSON.parse(text) as T) : null;
36 } catch {
37 // Unreadable (a restore to another phone, a keystore reset): start over.
38 return null;
39 }
40}
41
42export async function loadServer(): Promise<Server> {
43 const saved = await readJson<Server>(SERVER_KEY);
44 return saved?.site && saved?.api ? saved : HOSTED;
45}
46
47export async function saveServer(server: Server): Promise<void> {
48 await SecureStore.setItemAsync(SERVER_KEY, JSON.stringify(server));
49}
50
51export async function loadCredentials(): Promise<Credentials | null> {
52 return readJson<Credentials>(CREDENTIALS_KEY);
53}
54
55export async function saveCredentials(credentials: Credentials): Promise<void> {
56 await SecureStore.setItemAsync(CREDENTIALS_KEY, JSON.stringify(credentials));
57}
58
59export async function clearCredentials(): Promise<void> {
60 await SecureStore.deleteItemAsync(CREDENTIALS_KEY);
61}