Skip to content

Commit a682347

Browse files
authored
Merge pull request #37 from pylon-code/feat/mobile-connect-config
fix(mobile): Pylon Connect reaches mobile builds
2 parents 1da5852 + 5f899cf commit a682347

7 files changed

Lines changed: 200 additions & 20 deletions

File tree

.env.example

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
1-
# Optional: T3 Connect source builds
2-
# Leave these unset to disable optional T3 Connect features in local source builds.
1+
# Optional: Pylon Connect source builds
2+
# Leave these unset to disable optional Pylon Connect features in local source builds.
33
# Release builds inject their public values at build time. Do not add server-side
44
# secrets to this file.
55

6-
# Get these from the Clerk Dashboard under API keys, JWT templates, and OAuth applications.
7-
# T3CODE_CLERK_PUBLISHABLE_KEY=pk_test_...
8-
# T3CODE_CLERK_JWT_TEMPLATE=t3-relay
9-
# T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=oauthapp_...
6+
# These are Pylon's own public identifiers, not secrets: they ship inside every
7+
# released web, desktop, and mobile artifact. Copy this file to .env to enable
8+
# Pylon Connect in a source build. Mobile builds read the same values from the
9+
# EAS environment; CI keeps that mirror in sync from the GitHub `production`
10+
# environment, which is the source of truth.
11+
# T3CODE_CLERK_PUBLISHABLE_KEY=pk_live_Y2xlcmsucHlsb24tY29kZS5jb20k
12+
# T3CODE_CLERK_JWT_TEMPLATE=pylon-relay
13+
# T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=LL3XOy2zs9YyBSHm
14+
# T3CODE_RELAY_URL=https://relay.pylon-code.com
1015

1116
# Optional: signed macOS passkey builds. The RP domain defaults to the Frontend API
1217
# hostname encoded in T3CODE_CLERK_PUBLISHABLE_KEY. Set the override only when Clerk
1318
# returns a different RP ID or when multiple domains must be entitled.
1419
# T3CODE_APPLE_TEAM_ID=ABC1234567
15-
# T3CODE_MACOS_PROVISIONING_PROFILE=/absolute/path/to/t3code.provisionprofile
20+
# T3CODE_MACOS_PROVISIONING_PROFILE=/absolute/path/to/pylon.provisionprofile
1621
# T3CODE_CLERK_PASSKEY_RP_DOMAINS=example.clerk.accounts.dev,clerk.example.com
1722

18-
# Get this from your relay deployment. `infra/relay` deploys update it automatically.
19-
# T3CODE_RELAY_URL=https://relay.example.com
20-
2123
# Optional: hosted app origin used by the CLI's out-of-band OAuth flow.
22-
# Defaults to https://app.t3.codes; override to test against a staging deployment.
23-
# T3CODE_HOSTED_APP_URL=https://nightly.app.t3.codes
24+
# Defaults to https://app.pylon-code.com; override to test against a staging deployment.
25+
# T3CODE_HOSTED_APP_URL=https://nightly.pylon-code.com
2426

2527
# Public, ingest-only mobile OpenTelemetry configuration.
2628
# T3CODE_MOBILE_OTLP_TRACES_URL=https://api.axiom.co/v1/traces
27-
# T3CODE_MOBILE_OTLP_TRACES_DATASET=t3-code-mobile-traces-dev
29+
# T3CODE_MOBILE_OTLP_TRACES_DATASET=pylon-mobile-traces-dev
2830
# T3CODE_MOBILE_OTLP_TRACES_TOKEN=xaat-...

.github/workflows/mobile-eas-preview.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ jobs:
2121
APP_VARIANT: preview
2222
NODE_OPTIONS: --max-old-space-size=8192
2323
MOBILE_VERSION_POLICY: fingerprint
24+
# app.config.ts reads these to build `extra.eas.projectId`, and eas-cli
25+
# resolves the project from it. They otherwise live only in a gitignored
26+
# .env.local, so without them here every eas command fails with "EAS
27+
# project not configured".
28+
PYLON_EAS_PROJECT_ID: ${{ vars.PYLON_EAS_PROJECT_ID }}
29+
PYLON_EAS_OWNER: ${{ vars.PYLON_EAS_OWNER }}
2430
steps:
2531
- id: expo-token
2632
name: Check for EXPO_TOKEN
@@ -88,6 +94,13 @@ jobs:
8894
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
8995
run: eas env:pull preview --non-interactive
9096

97+
# The preview environment is a mirror; the production workflow refreshes it
98+
# from the GitHub `production` environment. Fail here rather than hand a
99+
# reviewer a build whose Connect surfaces are silently missing.
100+
- name: Verify Connect config reaches the app manifest
101+
if: steps.expo-token.outputs.present == 'true'
102+
run: node scripts/verify-mobile-connect-config.ts
103+
91104
- name: Deploy with fingerprint check
92105
if: steps.expo-token.outputs.present == 'true'
93106
# Pinned to a release rather than @main: this is the only action reference

.github/workflows/mobile-eas-production.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,24 @@ jobs:
3636
runs-on: blacksmith-8vcpu-ubuntu-2404
3737
permissions:
3838
contents: read
39+
# Pylon Connect's public config lives in this environment, the same source
40+
# `relay_public_config` in release.yml feeds to desktop, CLI, and hosted web.
41+
# Mobile reads it from here too, so the four surfaces cannot drift apart.
42+
environment: production
3943
env:
4044
APP_VARIANT: production
4145
NODE_OPTIONS: --max-old-space-size=8192
46+
CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
47+
CLERK_JWT_TEMPLATE: ${{ vars.CLERK_JWT_TEMPLATE }}
48+
CLERK_CLI_OAUTH_CLIENT_ID: ${{ vars.CLERK_CLI_OAUTH_CLIENT_ID }}
49+
RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }}
50+
RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }}
51+
# app.config.ts reads these to build `extra.eas.projectId`, and eas-cli
52+
# resolves the project from it. They otherwise live only in a gitignored
53+
# .env.local, so without them here every eas command fails with "EAS
54+
# project not configured".
55+
PYLON_EAS_PROJECT_ID: ${{ vars.PYLON_EAS_PROJECT_ID }}
56+
PYLON_EAS_OWNER: ${{ vars.PYLON_EAS_OWNER }}
4257
steps:
4358
- id: expo-token
4459
name: Check for EXPO_TOKEN
@@ -91,13 +106,60 @@ jobs:
91106
# ignored build script (no allowBuilds config outside the repo).
92107
packager: npm
93108

109+
# EAS build servers read their own environment store, not this checkout —
110+
# a repo-root .env is gitignored and never reaches them. So the GitHub
111+
# values are mirrored into EAS here, keeping one source of truth instead
112+
# of a hand-maintained expo.dev copy that silently drifts.
113+
- name: Sync Connect config to the EAS environment
114+
if: steps.expo-token.outputs.present == 'true'
115+
working-directory: apps/mobile
116+
env:
117+
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
118+
run: |
119+
set -euo pipefail
120+
121+
relay_domain="${RELAY_DOMAIN:-}"
122+
if [[ -z "$relay_domain" && -n "${RELAY_API_ZONE_NAME:-}" ]]; then
123+
relay_domain="relay.$RELAY_API_ZONE_NAME"
124+
fi
125+
# Partial configuration is treated as none, matching `relay_public_config`
126+
# in release.yml: half-configured Connect fails at runtime in the user's
127+
# app rather than here, where the cause is still visible.
128+
missing=()
129+
for name in relay_domain CLERK_PUBLISHABLE_KEY CLERK_JWT_TEMPLATE CLERK_CLI_OAUTH_CLIENT_ID; do
130+
if [[ -z "${!name:-}" ]]; then
131+
missing+=("$name")
132+
fi
133+
done
134+
if (( ${#missing[@]} > 0 )); then
135+
printf 'Connect is not configured (missing: %s); leaving the EAS environment untouched.\n' "${missing[*]}" >&2
136+
exit 0
137+
fi
138+
139+
sync() {
140+
echo "Syncing $1 to the EAS production, preview, and development environments."
141+
eas env:create \
142+
--name "$1" --value "$2" \
143+
--environment production --environment preview --environment development \
144+
--visibility plaintext --type string --scope project \
145+
--force --non-interactive
146+
}
147+
sync T3CODE_CLERK_PUBLISHABLE_KEY "$CLERK_PUBLISHABLE_KEY"
148+
sync T3CODE_CLERK_JWT_TEMPLATE "$CLERK_JWT_TEMPLATE"
149+
sync T3CODE_CLERK_CLI_OAUTH_CLIENT_ID "$CLERK_CLI_OAUTH_CLIENT_ID"
150+
sync T3CODE_RELAY_URL "https://$relay_domain"
151+
94152
- name: Pull production environment variables
95153
if: steps.expo-token.outputs.present == 'true'
96154
working-directory: apps/mobile
97155
env:
98156
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
99157
run: eas env:pull production --non-interactive
100158

159+
- name: Verify Connect config reaches the app manifest
160+
if: steps.expo-token.outputs.present == 'true'
161+
run: node scripts/verify-mobile-connect-config.ts
162+
101163
- name: Build and submit
102164
if: steps.expo-token.outputs.present == 'true' && inputs.mode == 'build'
103165
working-directory: apps/mobile

apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ function ConfiguredConnectOnboardingRouteScreen() {
116116
) : (
117117
<View collapsable={false} className="rounded-[24px] bg-card p-5">
118118
<Text className="text-sm leading-normal text-foreground-muted">
119-
Sign in to your T3 account to set up Pylon Connect.
119+
Sign in to your Pylon account to set up Pylon Connect.
120120
</Text>
121121
</View>
122122
)}

apps/mobile/src/features/settings/SettingsRouteScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ function ConfiguredSettingsRouteScreen() {
452452
<SettingsSection title="Account">
453453
<SettingsRow
454454
icon="person.crop.circle"
455-
label="T3 Account"
455+
label="Pylon Account"
456456
value={accountLabel}
457457
onPress={openAccount}
458458
/>

docs/internals/t3-connect.md

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,17 @@ identifiers, not secrets.
3939
Web, desktop, mobile, and bundled server builds statically inject the values they consume during
4040
their build step. A built artifact does not need an environment file at runtime. CI release builds
4141
should set `T3CODE_CLERK_PUBLISHABLE_KEY`, `T3CODE_CLERK_JWT_TEMPLATE`,
42-
`T3CODE_CLERK_CLI_OAUTH_CLIENT_ID`, and `T3CODE_RELAY_URL` before building. EAS preview and
43-
production builds only need the Clerk publishable key, JWT template name, and relay URL in their EAS
44-
environment.
42+
`T3CODE_CLERK_CLI_OAUTH_CLIENT_ID`, and `T3CODE_RELAY_URL` before building.
43+
44+
Mobile is the exception, because EAS build servers read their own environment store rather than the
45+
checkout: a repository-root `.env` is gitignored and never reaches them. The GitHub `production`
46+
environment stays the single source of truth, and `mobile-eas-production.yml` mirrors those values
47+
into the EAS `production`, `preview`, and `development` environments before it builds. Both mobile
48+
workflows then run `scripts/verify-mobile-connect-config.ts`, which resolves the public app manifest
49+
and fails the job when `clerk.publishableKey`, `clerk.jwtTemplate`, or `relay.url` is absent. That
50+
check exists because the failure is otherwise invisible: `hasCloudPublicConfig()` omits every Connect
51+
surface with no error and no empty state, so a misconfigured build looks like an app that simply
52+
never had the feature.
4553

4654
When any client-facing public value is absent, cloud UI is omitted. The `t3 connect` command group is
4755
always registered: when the CLI public values are absent, `makeCli` in `apps/server/src/bin.ts`
@@ -236,8 +244,40 @@ codesign --verify --deep --strict "/Applications/Pylon (Alpha).app"
236244
codesign -d --entitlements :- "/Applications/Pylon (Alpha).app"
237245
```
238246

239-
The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth
240-
flow uses a custom redirect URI, add that exact URI to the same allowlist.
247+
## Mobile Native Redirect Allowlist
248+
249+
Mobile does **not** use `allowed_origins`. That field covers browser-like stacks — Electron and
250+
browser extensions — which is why the desktop entries above live there. Clerk's native
251+
authentication view (`AuthView` from `@clerk/expo/native`) is validated against a separate
252+
**Redirect URLs** resource, reachable in the Dashboard under **Native applications > Allowlist for
253+
mobile SSO redirect**. Patching `allowed_origins` does not affect it.
254+
255+
The view derives its redirect from the **iOS bundle identifier**, not from the app's URL scheme, so
256+
each variant needs its own entry:
257+
258+
```text
259+
com.pylon.code://callback
260+
com.pylon.code.preview://callback
261+
com.pylon.code.dev://callback
262+
```
263+
264+
The Backend API is additive, so adding one entry cannot disturb the others:
265+
266+
```sh
267+
curl -X POST https://api.clerk.com/v1/redirect_urls \
268+
-H "Authorization: Bearer $CLERK_SECRET_KEY" \
269+
-H "Content-Type: application/json" \
270+
-d '{"url":"com.pylon.code.preview://callback"}'
271+
```
272+
273+
`GET /v1/redirect_urls` lists the current entries and `DELETE /v1/redirect_urls/<id>` removes one.
274+
275+
A missing entry fails at the end of the sign-in flow, not at launch: Clerk renders "The current
276+
redirect url passed in the sign in or sign up request does not match an authorized redirect URI for
277+
this instance" and names the rejected URI. Read that URI off the error rather than deriving it — it
278+
is the exact string the allowlist needs. Note that these are bundle identifiers
279+
(`com.pylon.code.preview`), while the desktop entries above are URL schemes (`pylon-code`); the two
280+
namespaces are easy to confuse.
241281

242282
## Sign-in Surfaces
243283

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// @effect-diagnostics nodeBuiltinImport:off - Build bootstrap runs before an Effect runtime exists.
2+
import * as NodeChildProcess from "node:child_process";
3+
import * as NodePath from "node:path";
4+
import * as NodeURL from "node:url";
5+
6+
// Pylon Connect is gated on `hasCloudPublicConfig()` in
7+
// apps/mobile/src/features/cloud/publicConfig.ts, which omits every Connect
8+
// surface when any of these three is absent. That failure is invisible in the
9+
// app: no error, no empty state, the section simply does not render. Asserting
10+
// against the resolved manifest here turns a silently Connect-dark build into a
11+
// failed job, where the cause is still visible.
12+
const REQUIRED = [
13+
["clerk.publishableKey", (extra: Extra) => extra?.clerk?.publishableKey],
14+
["clerk.jwtTemplate", (extra: Extra) => extra?.clerk?.jwtTemplate],
15+
["relay.url", (extra: Extra) => extra?.relay?.url],
16+
] as const;
17+
18+
interface Extra {
19+
readonly clerk?: { readonly publishableKey?: unknown; readonly jwtTemplate?: unknown };
20+
readonly relay?: { readonly url?: unknown };
21+
}
22+
23+
const REPO_ROOT = NodePath.dirname(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)));
24+
const MOBILE_ROOT = NodePath.join(REPO_ROOT, "apps", "mobile");
25+
26+
// The workspace-local binary, not `pnpm exec`: `pnpm exec` reinstalls the
27+
// workspace and re-runs the CLI under its own Node, which can be older than the
28+
// version whose type stripping app.config.ts needs.
29+
const EXPO_BIN = NodePath.join(MOBILE_ROOT, "node_modules", ".bin", "expo");
30+
31+
function readPublicManifest(): { readonly extra?: Extra } {
32+
const stdout = NodeChildProcess.execFileSync(EXPO_BIN, ["config", "--type", "public", "--json"], {
33+
cwd: MOBILE_ROOT,
34+
encoding: "utf8",
35+
maxBuffer: 32 * 1024 * 1024,
36+
});
37+
// `expo config` can emit progress lines before the document.
38+
const start = stdout.indexOf("{");
39+
if (start === -1) {
40+
throw new Error(`expo config produced no JSON document:\n${stdout}`);
41+
}
42+
return JSON.parse(stdout.slice(start));
43+
}
44+
45+
const extra = readPublicManifest().extra;
46+
const missing = REQUIRED.filter(([, read]) => {
47+
const value = read(extra ?? {});
48+
return typeof value !== "string" || value.trim() === "";
49+
}).map(([name]) => name);
50+
51+
if (missing.length > 0) {
52+
process.stderr.write(
53+
`Pylon Connect config is missing from the app manifest: ${missing.join(", ")}.\n` +
54+
"Mobile reads these from the EAS environment. Confirm the GitHub production " +
55+
"environment defines CLERK_PUBLISHABLE_KEY, CLERK_JWT_TEMPLATE, and " +
56+
"RELAY_API_ZONE_NAME (or RELAY_DOMAIN), and that the sync step ran before this one.\n" +
57+
"Building now would ship an app with every Connect surface silently omitted.\n",
58+
);
59+
process.exit(1);
60+
}
61+
62+
const variant = process.env.APP_VARIANT?.trim() || "production";
63+
process.stdout.write(`Pylon Connect config present in the ${variant} app manifest.\n`);

0 commit comments

Comments
 (0)