Skip to content
276 linesCodeBlameRaw
1# g1t for Android and iOS
2
3The phone app: [Expo](https://expo.dev) (SDK 57, React Native 0.86) with
4Expo Router, in TypeScript. Chat and Code, signed in with g1t's
5own OAuth. Android is the first target. What it does for people is in the
6guide, [The phone app](../docs/src/content/docs/guides/mobile.md).
7
8## Layout
9
10```text
11app.config.ts name, ids and icons per build variant (APP_VARIANT)
12eas.json EAS Build profiles: development, preview, production
13metro.config.js one React for the app in the monorepo
14scripts/icons.mjs draws the icons from packages/theme/mark.svg
15src/
16 app/ the screens (Expo Router: one file, one route)
17 sign-in.tsx Sign in with g1t, an access token, or another server
18 oauth/callback.tsx where the site sends sign-in back (g1t://oauth/callback)
19 (app)/ signed in: the tabs
20 chat/ the list, a conversation, a thread, new, browse
21 code/ repositories, pull requests, issues, diffs
22 more/ notifications, workspaces, sign out
23 home/, notifications/ coming: they open the site
24 +native-intent.tsx g1t:// links to screens (lib/links.ts)
25 components/ the building blocks, themed (ui.tsx); chat/ and Code's
26 lib/
27 server.ts which g1t: site and API addresses
28 oauth.ts OAuth 2.1 with PKCE against the server's metadata
29 credentials.ts the server and credentials, in the phone's keystore
30 client.ts every request: refresh, errors, the site or the API
31 session.tsx who is signed in, to which workspace
32 chat.ts, code.ts the data for Chat and Code (TanStack Query)
33 live.ts the site's live sockets, with tickets, kept open
34 feed.tsx counts, banners and presence while the app is open
35 push.ts notifications on the phone
36 theme/ packages/theme/tokens.css's colours, for React Native
37```
38
39Shared types and catalogs come from `@g1t/contracts` (by subpath, such as
40`@g1t/contracts/oauth`, so the app does not bundle every module).
41
42## How it talks to g1t
43
44| What | Where | How it is signed in |
45| --- | --- | --- |
46| Who you are, your workspaces | `GET {api}/user` | `Authorization: Bearer g1t_…` |
47| Code | `{api}/repos…` REST | the same |
48| Chat | `{site}/<workspace>/-/chat/api` and its live socket, as the site's own pages use them | the same header, with `chat:*` ([below](#chat-with-the-apps-sign-in)); a socket gets a ticket from `{site}/-/live/ticket` first |
49| Notifications | `{site}/-/notify` and the feed socket `{site}/-/live` | the same, with `notifications:*` |
50| Sign-in | `{api}/.well-known/oauth-authorization-server`, `{site}/oauth/authorize`, `{api}/oauth/token` | OAuth 2.1, PKCE (S256), public client |
51
52The OAuth client is not registered anywhere: its id encodes its name and
53redirect (`encodeOAuthClient`), so `g1t for Android` with
54`g1t://oauth/callback` is the same client on every phone. Access tokens
55last 30 days and are refreshed a minute before they expire; a refresh
56token works once, so `lib/client.ts` shares one refresh between
57requests.
58
59### Chat with the app's sign-in
60
61Chat has no REST API: the app uses the site's own chat routes. The app
62asks for `chat:read`, `chat:write`, `notifications:read` and
63`notifications:write`, and the site takes a token holding them by name
64(OAuth or personal, without "Use the website as you") on these alone
65(`apps/web/app/lib/website-token.ts`, `scopedRoute` and `socketUser`):
66
67| Route | Needs |
68| --- | --- |
69| `GET`, `POST /<ws>/-/chat/api` | `chat:read`, `chat:write` |
70| `GET /<ws>/-/chat/person/<username>` | `chat:read` |
71| `GET /-/live/ticket` for `/<ws>/-/chat/live` and `/-/live` | `chat:read` |
72| `GET`, `POST /-/notify` | `notifications:read`, `notifications:write` |
73
74A full-access token does not count: it reaches Chat only with the website
75permission.
76
77### Live, and notifications
78
79- **A conversation's socket** (`lib/chat.ts`, `useChannelLive`) brings new,
80 changed and deleted messages, reactions and typing, and takes the
81 app's own typing.
82- **The feed** (`lib/feed.tsx`), the socket every tab of the site keeps,
83 is open while the app is in front: unread counts, notifications (a
84 banner, `components/chat/banner.tsx`) and presence. It tells g1t the app
85 is focused, so nothing is pushed to the phone while it is in use; in the
86 background it closes.
87- **Push** (`lib/push.ts`): the app registers its Expo push token with
88 `POST /-/notify` `{ intent: "register_device", device: { token, platform, name } }`
89 and takes it back on signing out (`unregister_device`). The notify
90 service sends through Expo's push service on the Android channel
91 `messages`, with `data` `{ href, workspace, kind, tag, channel_id, thread_root, card }`;
92 tapping one opens its thread or conversation.
93
94## Run it
95
96You need Node 24 (the repository's `.nvmrc`; EAS builds on 24.21.0, `eas.json`).
97
98### In Expo Go (quickest)
99
100Everything the app uses is in Expo Go for SDK 57, so no native build is
101needed:
102
103```sh
104npm install # at the repository's root: one workspace
105cd apps/mobile
106npx expo start --go # then scan the QR code with Expo Go
107```
108
109Install [Expo Go](https://expo.dev/go) on the phone, or press `a` for an
110Android emulator. The phone and the computer must be on the same network
111(or use `npx expo start --tunnel`).
112
113Signing in works in Go: the site sends you back to Go's own address,
114`exp://<your computer>:8081/--/oauth/callback`, which g1t accepts as an
115application's own scheme. The approval page and Connected applications
116call it **g1t in Expo Go**. Its client id encodes that address, so a new
117network makes a new client: the app keeps the id it signed in with for
118refreshing, and signing in again after a move just adds a new entry to
119Connected applications. Chat works in Go in full, live, with banners
120while the app is open. What Go can't do: notifications on an Android
121phone (Expo took push out of Go there), open from `g1t://` links (they go
122to an installed build), and show the app's own name and icon.
123
124To develop against a g1t running on your computer, choose **Server** on
125the sign-in screen and give its address on your network, such as
126`http://192.168.1.20:5173` and the API's `http://192.168.1.20:8789`
127(`localhost` on a phone is the phone). Run that g1t with its `SITE_URL`
128and `API_URL` set to those addresses too: the API's OAuth metadata
129names the sign-in page by `SITE_URL`, and the phone opens what it names.
130
131### In a development build
132
133For `g1t://` links, the app's own icon and name, and anything native Go
134lacks later (push notifications):
135
136```sh
137npm run build:android:dev # EAS builds g1t (dev) in the cloud: install it from the link
138npx expo start # then open g1t (dev) on the phone; it finds this computer
139```
140
141The development build is `g1t (dev)` (`sh.g1t.app.dev`), with
142`expo-dev-client`: it loads the app's code from `npx expo start` on your
143computer, signs in through `g1t://oauth/callback`, and gets notifications
144once Firebase is set up ([below](#push-notifications-on-android)). Build
145it again only when a native module or `app.config.ts` changes; code
146changes reload live. `.g1t/workflows/mobile-build.yml` builds it by hand
147(Run workflow, `development`); its default, `preview`, is what testers
148install, and main builds that again when the native side changes. The
149run's summary links the build on expo.dev, whose QR code installs it. With Android Studio, `APP_VARIANT=development npx expo
150run:android` builds the same app on your computer instead (it makes
151`android/`, ignored by git).
152
153Checks, which CI runs from the root (`npm run typecheck`, `npm test`):
154
155```sh
156npm run typecheck -w @g1t/mobile
157npm test -w @g1t/mobile # lib/*.test.ts under Node
158```
159
160## Set up EAS and ship Android
161
162Once, by someone with access to the Expo account and Google Play:
163
1641. `npm install -g eas-cli`, then `eas login`.
1652. The EAS project exists (`35fb457b-…`, `EAS_PROJECT` in
166 `app.config.ts`); `eas whoami` should be an account with access to it.
167 Set `EAS_OWNER` in your shell if it belongs to an organization. On
168 expo.dev, the project's GitHub settings take `apps/mobile` as the base
169 directory.
1703. `eas build --profile preview --platform android`. EAS makes and keeps
171 the upload keystore the first time; say yes. The build ends with a link
172 and a QR code to install the APK.
1734. In the [Google Play Console](https://play.google.com/console), create
174 the app with the package `sh.g1t.app`, and turn on Play App Signing.
1755. `npm run build:android` makes the production App Bundle. Upload the
176 first one by hand on Play's **Internal testing** track: Google requires
177 that before its API accepts uploads.
1786. For `eas submit`, make a Google Cloud service account with access to
179 the app in the Play Console, download its JSON key, and add it to the
180 EAS project's credentials (`eas credentials`). From then on,
181 `eas submit --platform android --latest` sends a build to internal
182 testing as a draft (`eas.json`'s `submit.production`).
183
184iOS is the same with `--platform ios`, an Apple Developer account and the
185bundle id `sh.g1t.app`; EAS makes the certificates.
186
187### Releasing on g1t.sh/download (until Google Play)
188
189Google Play and the App Store wait on Flagon's D-U-N-S number. Until then
190the `preview` build, `g1t (beta)`, is what people install, from
191g1t.sh/download:
192
193```sh
194# a new native side or version: bump `version` in app.config.ts, then
195git tag mobile-v0.1.0 && git push origin mobile-v0.1.0 # .g1t/workflows/mobile-release.yml
196# or by hand, from the repository's root:
197node scripts/mobile-release.mjs build # EAS builds the APK
198node scripts/mobile-release.mjs fetch # the newest finished preview build
199node scripts/mobile-release.mjs manifest
200node scripts/mobile-release.mjs publish # to g1t.sh/downloads/mobile/, latest.json last
201
202# code alone: over the air to every installed copy of this version
203cd apps/mobile && npx eas-cli update --channel preview --message "What changed"
204```
205
206The workflow needs `EXPO_TOKEN` (expo.dev → Access tokens) beside the
207deploy's Cloudflare secrets. Preview builds count their own versionCode
208(`autoIncrement`), so each installs over the last.
209
210### Push notifications on Android
211
212Expo's push service sends to Android through Firebase Cloud Messaging,
213which needs a Firebase project. Once:
214
2151. In the [Firebase console](https://console.firebase.google.com), make a
216 project, and in it an Android app for each package: `sh.g1t.app`,
217 `sh.g1t.app.preview` and `sh.g1t.app.dev`. Download
218 `google-services.json` (one file lists all three).
2192. Give it to EAS as a file variable:
220 `eas env:create --scope project --name GOOGLE_SERVICES_JSON --type file --value ./google-services.json --visibility secret --environment production --environment preview --environment development`.
221 `app.config.ts` reads it from there, or a `google-services.json` beside
222 it locally (ignored by git).
2233. In Firebase, **Project settings → Service accounts → Generate new private
224 key**, then `eas credentials` → Android → the production build's package →
225 **Google Service Account → Push Notifications (FCM V1)**, and upload that
226 key. Repeat for `sh.g1t.app.preview` and `sh.g1t.app.dev`.
2274. Build again. Pushes need a build: Expo Go on Android has none.
228
229`EXPO_ACCESS_TOKEN` on the notify Worker is needed only if the Expo
230project turns on **Enhanced push security**. Leave that off: self-hosted
231g1ts push to this same app through this same project, without g1t's
232token, and turning it on stops their notifications.
233
234### Self-hosted g1t
235
236The app works with any g1t (docs: the phone app guide's "Your own g1t",
237and self-hosting's "The phone app"):
238
239- `GET <site>/.well-known/g1t.json` (`@g1t/contracts/discovery`,
240 `apps/web/app/routes/well-known-g1t.ts`) says where the API is and
241 whether the g1t sends phones notifications (`PHONE_PUSH`). The app reads
242 it in `lib/discover.ts`, and falls back to `api.<host>` for an older g1t.
243- `g1t://connect?server=<site>` (`app/connect.tsx`) points the app at a
244 g1t after asking; a self-hosted g1t's `/download` shows it as a QR code.
245- On g1t.sh none of it shows: the sign-in screen has one button, and a
246 quiet "Using your own g1t?" at its foot.
247- Store builds open `https` only (Android blocks cleartext); development
248 builds also open `http`, for a g1t on your network.
249
250### Before the first store release
251
252- **App Links**: `https://g1t.sh/.well-known/assetlinks.json` naming
253 `sh.g1t.app` and Play's signing certificate's SHA-256, then an
254 `intentFilters` entry with `autoVerify` in `app.config.ts`, so g1t.sh
255 links open the app. iOS needs `apple-app-site-association` the same way.
256- **The brand's faces**: TTF copies of Hanken Grotesk, Bricolage
257 Grotesque and IBM Plex Mono (packages/theme ships woff2), loaded with
258 `expo-font`.
259- **The store listing**: screenshots, a privacy policy URL, and Play's
260 data safety form (the app keeps the server and credentials in the
261 keystore, registers a push token with the g1t it is pointed at, and
262 sends nothing anywhere else).
263
264## Notes
265
266- **React is pinned.** The site uses a newer React than React Native 0.86
267 allows, so the app has its own `react` (19.2.3) and `metro.config.js`
268 resolves every `react` import to it. `expo-doctor` reports the root's
269 copy as a duplicate; that is expected here, and the bundle has one.
270 The doctor has no way to allow one duplicate, so EAS builds skip it
271 (`EAS_BUILD_DISABLE_EXPO_DOCTOR_STEP` in `eas.json`); `npm run doctor`
272 still runs it.
273- **Icons** are drawn from the mark: `npm run icons -w @g1t/mobile`.
274- **Versions**: `version` in `app.config.ts` is what people see;
275 EAS counts build numbers itself (`appVersionSource: remote`). Over-the-air
276 updates go to builds of the same `version` (`runtimeVersion`).