Skip to content

Commit

The phone app signs in from Expo Go and stays signed in there. Go sends sign-in back to its own exp:// address, which follows the computer's network address, and a client id encodes its redirect, so a refresh after the address changed named another client and was refused. Credentials now keep the client id they were issued to and refresh with it; Go's client is named "g1t in Expo Go" on the approval page and in Connected applications. The README leads with running in Expo Go, and says what a development build adds and how to point the phone at a g1t on your computer.

Claude-Session:
https://claude.ai/code/session_01MQE64a4WNN14F8GELCiuc9
Claudeagent and Claudeagentcommitted Parentdd35dfeBrowse files
3 files+62−180/3 viewed
+40−9
7979
8080 ## Run it
8181
82−You need Node 22 (the repository's), and for Android, Android Studio with
83−an emulator or a phone with USB debugging.
82+You need Node 22 (the repository's).
8483
84+### In Expo Go (quickest)
85+
86+Everything the app uses is in Expo Go for SDK 57, so no native build is
87+needed:
88+
8589 ```sh
8690 npm install # at the repository's root: one workspace
8791 cd apps/mobile
88−npx expo run:android # builds a development app and starts Metro
92+npx expo start # then scan the QR code with Expo Go
8993 ```
9094
91−`expo run:android` makes the native project (`android/`, ignored by git)
92−and installs `g1t (dev)`. After the first build, `npx expo start` and
93−the installed app are enough until a native module changes.
95+Install [Expo Go](https://expo.dev/go) on the phone, or press `a` for an
96+Android emulator. The phone and the computer must be on the same network
97+(or use `npx expo start --tunnel`).
9498
95−Expo Go works for screens that need nothing native beyond what Go has,
96−and sign-in comes back to Go's own `exp://` address (allowed as a
97−redirect). `g1t://` links need a development build.
99+Signing in works in Go: the site sends you back to Go's own address,
100+`exp://<your computer>:8081/--/oauth/callback`, which g1t accepts as an
101+application's own scheme. The approval page and Connected applications
102+call it **g1t in Expo Go**. Its client id encodes that address, so a new
103+network makes a new client: the app keeps the id it signed in with for
104+refreshing, and signing in again after a move just adds a new entry to
105+Connected applications. What Go can't do: open from `g1t://` links (they
106+go to an installed build) and show the app's own name and icon.
107+
108+To develop against a g1t running on your computer, choose **Server** on
109+the sign-in screen and give its address on your network, such as
110+`http://192.168.1.20:5173` and the API's `http://192.168.1.20:8789`
111+(`localhost` on a phone is the phone). Run that g1t with its `SITE_URL`
112+and `API_URL` set to those addresses too: the API's OAuth metadata
113+names the sign-in page by `SITE_URL`, and the phone opens what it names.
114+
115+### In a development build
116+
117+For `g1t://` links, the app's own icon and name, and anything native Go
118+lacks later (push notifications):
119+
120+```sh
121+npx expo run:android # builds g1t (dev) and starts Metro
122+```
123+
124+`expo run:android` makes the native project (`android/`, ignored by git)
125+and installs `g1t (dev)`, which signs in through `g1t://oauth/callback`.
126+After the first build, `npx expo start` and the installed app are enough
127+until a native module changes. `eas build --profile development` makes
128+the same build in the cloud.
98129
99130 Checks, which CI runs from the root (`npm run typecheck`, `npm test`):
100131
+6−0
2323 expiresAt: number;
2424 /** The scopes the person granted, or `*` for full access. */
2525 scope: string;
26+ /**
27+ * The client id it was issued to: a refresh must name the same one.
28+ * Kept, because in Expo Go the id follows the computer's address,
29+ * which changes between sessions (lib/oauth.ts).
30+ */
31+ clientId?: string;
2632 }
2733 | { kind: "token"; accessToken: string };
2834
+16−9
11 import { encodeOAuthClient } from "@g1t/contracts/oauth";
2+import Constants, { ExecutionEnvironment } from "expo-constants";
23 import * as AuthSession from "expo-auth-session";
34 import * as WebBrowser from "expo-web-browser";
45 import { Platform } from "react-native";
4849 "artifacts:read",
4950 ];
5051
52+/** Whether the app is running inside Expo Go, while developing. */
53+export const IN_EXPO_GO = Constants.executionEnvironment === ExecutionEnvironment.StoreClient;
54+
5155 /** The name shown on the approval page and in Connected applications. */
52−const CLIENT_NAME = Platform.OS === "ios" ? "g1t for iOS" : "g1t for Android";
56+const CLIENT_NAME = IN_EXPO_GO ? "g1t in Expo Go" : Platform.OS === "ios" ? "g1t for iOS" : "g1t for Android";
5357
5458 /**
5559 * Where the site sends the person back: `g1t://oauth/callback` in a build
56− * of the app, and Expo Go's own address while developing in it.
60+ * of the app, and Expo Go's own address while developing in it
61+ * (`exp://192.168.1.20:8081/--/oauth/callback`), which g1t accepts as an
62+ * application's own scheme. Since the client id encodes it, Go's id
63+ * changes with the computer's address; credentials keep the id they were
64+ * issued to, for refreshing.
5765 */
5866 export function redirectUri(): string {
5967 return AuthSession.makeRedirectUri({ scheme: "g1t", path: OAUTH_CALLBACK_PATH.slice(1) });
131139 },
132140 discovery(meta),
133141 );
134− return fromTokenResponse(tokens);
142+ return fromTokenResponse(tokens, id);
135143 }
136144
137−function fromTokenResponse(tokens: AuthSession.TokenResponse): Credentials {
145+function fromTokenResponse(tokens: AuthSession.TokenResponse, clientId: string): Credentials {
138146 if (!tokens.refreshToken) throw new Error("g1t did not return a refresh token.");
139147 return {
140148 kind: "oauth",
142150 refreshToken: tokens.refreshToken,
143151 expiresAt: (tokens.issuedAt + (tokens.expiresIn ?? 30 * 24 * 3600)) * 1000,
144152 scope: tokens.scope ?? "",
153+ clientId,
145154 };
146155 }
147156
159168 */
160169 export async function refresh(server: Server, credentials: Extract<Credentials, { kind: "oauth" }>): Promise<Credentials> {
161170 const meta = await metadata(server);
171+ const id = credentials.clientId ?? clientId(redirectUri());
162172 try {
163− const tokens = await AuthSession.refreshAsync(
164− { clientId: clientId(redirectUri()), refreshToken: credentials.refreshToken },
165− discovery(meta),
166− );
167− return fromTokenResponse(tokens);
173+ const tokens = await AuthSession.refreshAsync({ clientId: id, refreshToken: credentials.refreshToken }, discovery(meta));
174+ return fromTokenResponse(tokens, id);
168175 } catch (error) {
169176 // invalid_grant: used, expired or revoked in Connected applications.
170177 if (error instanceof AuthSession.TokenError) throw new SignedOut();