| 1 | # g1t for Android and iOS |
| 2 | |
| 3 | The phone app: [Expo](https://expo.dev) (SDK 57, React Native 0.86) with |
| 4 | Expo Router, in TypeScript. Chat, Code and Customize, signed in with g1t's |
| 5 | own OAuth. Android is the first target. What it does for people is in the |
| 6 | guide, [The phone app](../docs/src/content/docs/guides/mobile.md). |
| 7 | |
| 8 | ## Layout |
| 9 | |
| 10 | ```text |
| 11 | app.config.ts name, ids and icons per build variant (APP_VARIANT) |
| 12 | eas.json EAS Build profiles: development, preview, production |
| 13 | metro.config.js one React for the app in the monorepo |
| 14 | scripts/icons.mjs draws the icons from packages/theme/mark.svg |
| 15 | src/ |
| 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 | |
| 41 | Shared types and catalogs come from `@g1t/contracts` (by subpath, such as |
| 42 | `@g1t/contracts/oauth`, so the app does not bundle every module). |
| 43 | Customize's three parts are defined there too (`customize.ts`), so the |
| 44 | site 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 | |
| 56 | The OAuth client is not registered anywhere: its id encodes its name and |
| 57 | redirect (`encodeOAuthClient`), so `g1t for Android` with |
| 58 | `g1t://oauth/callback` is the same client on every phone. Access tokens |
| 59 | last 30 days and are refreshed a minute before they expire; a refresh |
| 60 | token works once, so `lib/client.ts` shares one refresh between |
| 61 | requests. |
| 62 | |
| 63 | ### Chat with the app's sign-in |
| 64 | |
| 65 | Chat has no REST API: the app uses the site's own chat routes. The app |
| 66 | asks 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 | |
| 78 | A full-access token does not count: it reaches Chat only with the website |
| 79 | permission. |
| 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 | |
| 100 | You need Node 22 (the repository's). |
| 101 | |
| 102 | ### In Expo Go (quickest) |
| 103 | |
| 104 | Everything the app uses is in Expo Go for SDK 57, so no native build is |
| 105 | needed: |
| 106 | |
| 107 | ```sh |
| 108 | npm install # at the repository's root: one workspace |
| 109 | cd apps/mobile |
| 110 | npx expo start --go # then scan the QR code with Expo Go |
| 111 | ``` |
| 112 | |
| 113 | Install [Expo Go](https://expo.dev/go) on the phone, or press `a` for an |
| 114 | Android emulator. The phone and the computer must be on the same network |
| 115 | (or use `npx expo start --tunnel`). |
| 116 | |
| 117 | Signing 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 |
| 119 | application's own scheme. The approval page and Connected applications |
| 120 | call it **g1t in Expo Go**. Its client id encodes that address, so a new |
| 121 | network makes a new client: the app keeps the id it signed in with for |
| 122 | refreshing, and signing in again after a move just adds a new entry to |
| 123 | Connected applications. Chat works in Go in full, live, with banners |
| 124 | while the app is open. What Go can't do: notifications on an Android |
| 125 | phone (Expo took push out of Go there), open from `g1t://` links (they go |
| 126 | to an installed build), and show the app's own name and icon. |
| 127 | |
| 128 | To develop against a g1t running on your computer, choose **Server** on |
| 129 | the 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` |
| 132 | and `API_URL` set to those addresses too: the API's OAuth metadata |
| 133 | names the sign-in page by `SITE_URL`, and the phone opens what it names. |
| 134 | |
| 135 | ### In a development build |
| 136 | |
| 137 | For `g1t://` links, the app's own icon and name, and anything native Go |
| 138 | lacks later (push notifications): |
| 139 | |
| 140 | ```sh |
| 141 | npm run build:android:dev # EAS builds g1t (dev) in the cloud: install it from the link |
| 142 | npx expo start # then open g1t (dev) on the phone; it finds this computer |
| 143 | ``` |
| 144 | |
| 145 | The 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 |
| 147 | computer, signs in through `g1t://oauth/callback`, and gets notifications |
| 148 | once Firebase is set up ([below](#push-notifications-on-android)). Build |
| 149 | it again only when a native module or `app.config.ts` changes; code |
| 150 | changes reload live. With Android Studio, `APP_VARIANT=development npx expo |
| 151 | run:android` builds the same app on your computer instead (it makes |
| 152 | `android/`, ignored by git). |
| 153 | |
| 154 | Checks, which CI runs from the root (`npm run typecheck`, `npm test`): |
| 155 | |
| 156 | ```sh |
| 157 | npm run typecheck -w @g1t/mobile |
| 158 | npm test -w @g1t/mobile # lib/*.test.ts under Node |
| 159 | ``` |
| 160 | |
| 161 | ## Set up EAS and ship Android |
| 162 | |
| 163 | Once, by someone with access to the Expo account and Google Play: |
| 164 | |
| 165 | 1. `npm install -g eas-cli`, then `eas login`. |
| 166 | 2. 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. |
| 171 | 3. `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. |
| 174 | 4. 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. |
| 176 | 5. `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. |
| 179 | 6. 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 | |
| 185 | iOS is the same with `--platform ios`, an Apple Developer account and the |
| 186 | bundle id `sh.g1t.app`; EAS makes the certificates. |
| 187 | |
| 188 | ### Push notifications on Android |
| 189 | |
| 190 | Expo's push service sends to Android through Firebase Cloud Messaging, |
| 191 | which needs a Firebase project. Once: |
| 192 | |
| 193 | 1. 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). |
| 197 | 2. 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). |
| 201 | 3. 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`. |
| 205 | 4. 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 |
| 208 | project turns on **Enhanced push security**. Leave that off: self-hosted |
| 209 | g1ts push to this same app through this same project, without g1t's |
| 210 | token, and turning it on stops their notifications. |
| 211 | |
| 212 | ### Self-hosted g1t |
| 213 | |
| 214 | The app works with any g1t (docs: the phone app guide's "Your own g1t", |
| 215 | and self-hosting's "The phone app"): |
| 216 | |
| 217 | - `GET <site>/.well-known/g1t.json` (`@g1t/contracts/discovery`, |
| 218 | `apps/web/app/routes/well-known-g1t.ts`) says where the API is and |
| 219 | whether the g1t sends phones notifications (`PHONE_PUSH`). The app reads |
| 220 | it in `lib/discover.ts`, and falls back to `api.<host>` for an older g1t. |
| 221 | - `g1t://connect?server=<site>` (`app/connect.tsx`) points the app at a |
| 222 | g1t after asking; a self-hosted g1t's `/download` shows it as a QR code. |
| 223 | - On g1t.sh none of it shows: the sign-in screen has one button, and a |
| 224 | quiet "Using your own g1t?" at its foot. |
| 225 | - Store builds open `https` only (Android blocks cleartext); development |
| 226 | builds also open `http`, for a g1t on your network. |
| 227 | |
| 228 | ### Before the first store release |
| 229 | |
| 230 | - **App Links**: `https://g1t.sh/.well-known/assetlinks.json` naming |
| 231 | `sh.g1t.app` and Play's signing certificate's SHA-256, then an |
| 232 | `intentFilters` entry with `autoVerify` in `app.config.ts`, so g1t.sh |
| 233 | links open the app. iOS needs `apple-app-site-association` the same way. |
| 234 | - **The brand's faces**: TTF copies of Hanken Grotesk, Bricolage |
| 235 | Grotesque and IBM Plex Mono (packages/theme ships woff2), loaded with |
| 236 | `expo-font`. |
| 237 | - **Agents' faces**: `components/ui.tsx`'s `Avatar` draws a letter for an |
| 238 | agent; the site's `AgentFace` should be shared and drawn here. |
| 239 | - **The store listing**: screenshots, a privacy policy URL, and Play's |
| 240 | data safety form (the app keeps the server and credentials in the |
| 241 | keystore, registers a push token with the g1t it is pointed at, and |
| 242 | sends nothing anywhere else). |
| 243 | |
| 244 | ## Notes |
| 245 | |
| 246 | - **React is pinned.** The site uses a newer React than React Native 0.86 |
| 247 | allows, so the app has its own `react` (19.2.3) and `metro.config.js` |
| 248 | resolves every `react` import to it. `expo-doctor` reports the root's |
| 249 | copy as a duplicate; that is expected here, and the bundle has one. |
| 250 | - **Icons** are drawn from the mark: `npm run icons -w @g1t/mobile`. |
| 251 | - **Versions**: `version` in `app.config.ts` is what people see; |
| 252 | EAS counts build numbers itself (`appVersionSource: remote`). Over-the-air |
| 253 | updates go to builds of the same `version` (`runtimeVersion`). |