Skip to content
235 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, Code and Customize, 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/ Customize, notifications, workspaces, sign out
23 customize/ Skills, Connectors, Plugins (from More)
24 home/, notifications/, agents/ coming: they open the site
25 +native-intent.tsx g1t:// links to screens (lib/links.ts)
26 components/ the building blocks, themed (ui.tsx); chat/ and Code's
27 lib/
28 server.ts which g1t: site and API addresses
29 oauth.ts OAuth 2.1 with PKCE against the server's metadata
30 credentials.ts the server and credentials, in the phone's keystore
31 client.ts every request: refresh, errors, the site or the API
32 session.tsx who is signed in, to which workspace
33 chat.ts, code.ts the data for Chat and Code (TanStack Query)
34 live.ts the site's live sockets, with tickets, kept open
35 feed.tsx counts, banners and presence while the app is open
36 push.ts notifications on the phone
37 customize.ts Customize's listings, from @g1t/contracts' catalogs
38 theme/ packages/theme/tokens.css's colours, for React Native
39```
40
41Shared types and catalogs come from `@g1t/contracts` (by subpath, such as
42`@g1t/contracts/oauth`, so the app does not bundle every module).
43Customize's three parts are defined there too (`customize.ts`), so the
44site can move the Marketplace onto the same model.
45
46## How it talks to g1t
47
48| What | Where | How it is signed in |
49| --- | --- | --- |
50| Who you are, your workspaces | `GET {api}/user` | `Authorization: Bearer g1t_…` |
51| Code | `{api}/repos…` REST | the same |
52| 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 |
53| Notifications | `{site}/-/notify` and the feed socket `{site}/-/live` | the same, with `notifications:*` |
54| Sign-in | `{api}/.well-known/oauth-authorization-server`, `{site}/oauth/authorize`, `{api}/oauth/token` | OAuth 2.1, PKCE (S256), public client |
55
56The OAuth client is not registered anywhere: its id encodes its name and
57redirect (`encodeOAuthClient`), so `g1t for Android` with
58`g1t://oauth/callback` is the same client on every phone. Access tokens
59last 30 days and are refreshed a minute before they expire; a refresh
60token works once, so `lib/client.ts` shares one refresh between
61requests.
62
63### Chat with the app's sign-in
64
65Chat has no REST API: the app uses the site's own chat routes. The app
66asks for `chat:read`, `chat:write`, `notifications:read` and
67`notifications:write`, and the site takes a token holding them by name
68(OAuth or personal, without "Use the website as you") on these alone
69(`apps/web/app/lib/website-token.ts`, `scopedRoute` and `socketUser`):
70
71| Route | Needs |
72| --- | --- |
73| `GET`, `POST /<ws>/-/chat/api` | `chat:read`, `chat:write` |
74| `GET /<ws>/-/chat/person/<username>` | `chat:read` |
75| `GET /-/live/ticket` for `/<ws>/-/chat/live` and `/-/live` | `chat:read` |
76| `GET`, `POST /-/notify` | `notifications:read`, `notifications:write` |
77
78A full-access token does not count: it reaches Chat only with the website
79permission.
80
81### Live, and notifications
82
83- **A conversation's socket** (`lib/chat.ts`, `useChannelLive`) brings new,
84 changed and deleted messages, reactions and typing, and takes the
85 app's own typing.
86- **The feed** (`lib/feed.tsx`), the socket every tab of the site keeps,
87 is open while the app is in front: unread counts, notifications (a
88 banner, `components/chat/banner.tsx`) and presence. It tells g1t the app
89 is focused, so nothing is pushed to the phone while it is in use; in the
90 background it closes.
91- **Push** (`lib/push.ts`): the app registers its Expo push token with
92 `POST /-/notify` `{ intent: "register_device", device: { token, platform, name } }`
93 and takes it back on signing out (`unregister_device`). The notify
94 service sends through Expo's push service on the Android channel
95 `messages`, with `data` `{ href, workspace, kind, tag, channel_id, thread_root, card }`;
96 tapping one opens its thread or conversation.
97
98## Run it
99
100You need Node 22 (the repository's).
101
102### In Expo Go (quickest)
103
104Everything the app uses is in Expo Go for SDK 57, so no native build is
105needed:
106
107```sh
108npm install # at the repository's root: one workspace
109cd apps/mobile
110npx expo start --go # then scan the QR code with Expo Go
111```
112
113Install [Expo Go](https://expo.dev/go) on the phone, or press `a` for an
114Android emulator. The phone and the computer must be on the same network
115(or use `npx expo start --tunnel`).
116
117Signing in works in Go: the site sends you back to Go's own address,
118`exp://<your computer>:8081/--/oauth/callback`, which g1t accepts as an
119application's own scheme. The approval page and Connected applications
120call it **g1t in Expo Go**. Its client id encodes that address, so a new
121network makes a new client: the app keeps the id it signed in with for
122refreshing, and signing in again after a move just adds a new entry to
123Connected applications. Chat works in Go in full, live, with banners
124while the app is open. What Go can't do: notifications on an Android
125phone (Expo took push out of Go there), open from `g1t://` links (they go
126to an installed build), and show the app's own name and icon.
127
128To develop against a g1t running on your computer, choose **Server** on
129the sign-in screen and give its address on your network, such as
130`http://192.168.1.20:5173` and the API's `http://192.168.1.20:8789`
131(`localhost` on a phone is the phone). Run that g1t with its `SITE_URL`
132and `API_URL` set to those addresses too: the API's OAuth metadata
133names the sign-in page by `SITE_URL`, and the phone opens what it names.
134
135### In a development build
136
137For `g1t://` links, the app's own icon and name, and anything native Go
138lacks later (push notifications):
139
140```sh
141npm run build:android:dev # EAS builds g1t (dev) in the cloud: install it from the link
142npx expo start # then open g1t (dev) on the phone; it finds this computer
143```
144
145The development build is `g1t (dev)` (`sh.g1t.app.dev`), with
146`expo-dev-client`: it loads the app's code from `npx expo start` on your
147computer, signs in through `g1t://oauth/callback`, and gets notifications
148once Firebase is set up ([below](#push-notifications-on-android)). Build
149it again only when a native module or `app.config.ts` changes; code
150changes reload live. With Android Studio, `APP_VARIANT=development npx expo
151run:android` builds the same app on your computer instead (it makes
152`android/`, ignored by git).
153
154Checks, which CI runs from the root (`npm run typecheck`, `npm test`):
155
156```sh
157npm run typecheck -w @g1t/mobile
158npm test -w @g1t/mobile # lib/*.test.ts under Node
159```
160
161## Set up EAS and ship Android
162
163Once, by someone with access to the Expo account and Google Play:
164
1651. `npm install -g eas-cli`, then `eas login`.
1662. The EAS project exists (`35fb457b-…`, `EAS_PROJECT` in
167 `app.config.ts`); `eas whoami` should be an account with access to it.
168 Set `EAS_OWNER` in your shell if it belongs to an organization. On
169 expo.dev, the project's GitHub settings take `apps/mobile` as the base
170 directory.
1713. `eas build --profile preview --platform android`. EAS makes and keeps
172 the upload keystore the first time; say yes. The build ends with a link
173 and a QR code to install the APK.
1744. In the [Google Play Console](https://play.google.com/console), create
175 the app with the package `sh.g1t.app`, and turn on Play App Signing.
1765. `npm run build:android` makes the production App Bundle. Upload the
177 first one by hand on Play's **Internal testing** track: Google requires
178 that before its API accepts uploads.
1796. For `eas submit`, make a Google Cloud service account with access to
180 the app in the Play Console, download its JSON key, and add it to the
181 EAS project's credentials (`eas credentials`). From then on,
182 `eas submit --platform android --latest` sends a build to internal
183 testing as a draft (`eas.json`'s `submit.production`).
184
185iOS is the same with `--platform ios`, an Apple Developer account and the
186bundle id `sh.g1t.app`; EAS makes the certificates.
187
188### Push notifications on Android
189
190Expo's push service sends to Android through Firebase Cloud Messaging,
191which needs a Firebase project. Once:
192
1931. In the [Firebase console](https://console.firebase.google.com), make a
194 project, and in it an Android app for each package: `sh.g1t.app`,
195 `sh.g1t.app.preview` and `sh.g1t.app.dev`. Download
196 `google-services.json` (one file lists all three).
1972. Give it to EAS as a file variable:
198 `eas env:create --scope project --name GOOGLE_SERVICES_JSON --type file --value ./google-services.json --visibility secret --environment production --environment preview --environment development`.
199 `app.config.ts` reads it from there, or a `google-services.json` beside
200 it locally (ignored by git).
2013. In Firebase, **Project settings → Service accounts → Generate new private
202 key**, then `eas credentials` → Android → the production build's package →
203 **Google Service Account → Push Notifications (FCM V1)**, and upload that
204 key. Repeat for `sh.g1t.app.preview` and `sh.g1t.app.dev`.
2054. Build again. Pushes need a build: Expo Go on Android has none.
206
207`EXPO_ACCESS_TOKEN` on the notify Worker is needed only if the Expo
208project turns on **Enhanced push security**.
209
210### Before the first store release
211
212- **App Links**: `https://g1t.sh/.well-known/assetlinks.json` naming
213 `sh.g1t.app` and Play's signing certificate's SHA-256, then an
214 `intentFilters` entry with `autoVerify` in `app.config.ts`, so g1t.sh
215 links open the app. iOS needs `apple-app-site-association` the same way.
216- **The brand's faces**: TTF copies of Hanken Grotesk, Bricolage
217 Grotesque and IBM Plex Mono (packages/theme ships woff2), loaded with
218 `expo-font`.
219- **Agents' faces**: `components/ui.tsx`'s `Avatar` draws a letter for an
220 agent; the site's `AgentFace` should be shared and drawn here.
221- **The store listing**: screenshots, a privacy policy URL, and Play's
222 data safety form (the app keeps the server and credentials in the
223 keystore, registers a push token with the g1t it is pointed at, and
224 sends nothing anywhere else).
225
226## Notes
227
228- **React is pinned.** The site uses a newer React than React Native 0.86
229 allows, so the app has its own `react` (19.2.3) and `metro.config.js`
230 resolves every `react` import to it. `expo-doctor` reports the root's
231 copy as a duplicate; that is expected here, and the bundle has one.
232- **Icons** are drawn from the mark: `npm run icons -w @g1t/mobile`.
233- **Versions**: `version` in `app.config.ts` is what people see;
234 EAS counts build numbers itself (`appVersionSource: remote`). Over-the-air
235 updates go to builds of the same `version` (`runtimeVersion`).