Skip to content

Commit 577b35b

Browse files
authored
Merge branch 'main' into fix/windows-path-refresh
2 parents bd5ad7d + 2daff8c commit 577b35b

44 files changed

Lines changed: 1803 additions & 742 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/test-t3-mobile/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ Run Metro from `apps/mobile`.
8080
APP_VARIANT=development vp exec expo start \
8181
--dev-client \
8282
--scheme t3code-dev \
83-
--clear \
8483
--lan \
8584
--port <metro-port>
8685
```
@@ -179,6 +178,7 @@ Keep local verification focused. Do not turn this workflow into a full repositor
179178
## Troubleshoot predictable failures
180179

181180
- **Old UI or an old error appears:** verify Metro's worktree, variant, URL, and port before diagnosing the app.
181+
- **Metro serves stale or invalid transforms after those checks:** stop the owned Metro process and run `vp run dev:client:reset` once on the standard port. For a custom port, add `--clear` to the complete explicit `expo start` command above.
182182
- **The environment remains empty:** verify the platform-specific HTTP origin, use a fresh token, and confirm project seeding used the identical base directory.
183183
- **A second client cannot pair:** pairing tokens are single-use; issue another token.
184184
- **The pairing form opens but does not connect:** confirm the deep link uses the existing `connections/new` route, includes `autoConnect=1`, and carries a freshly minted encoded `pairingUrl`.

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real
104104
## Verifying
105105

106106
- Smallest proof that the change works. `vp test run <files>` for the tests you touched, targeted lint and typecheck for the scope you changed.
107+
- Test meaningful logic or observable behavior. Do not render components to static markup to assert props or attributes, or add tests that merely assert callback wiring or mirror the implementation.
107108
- **Do not run repo-wide checks.** No `vp check`, no `vp run -r test`, no `vp run -r typecheck` unless I ask. CI owns the full suite.
108109
- Backend behavior changes ship with focused tests for that behavior.
109110
- The server is event-sourced and its async flows emit typed receipts. Wait on receipts and worker drains, never on sleeps or polling. A test that needs a timeout to pass is wrong.

‎apps/mobile/README.md‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ Start Metro for the dev client:
2828
vp run dev:client
2929
```
3030

31+
Metro keeps its transform cache between ordinary starts. If the cache itself is causing stale or
32+
invalid output, clear it for one development-client start:
33+
34+
```bash
35+
vp run dev:client:reset
36+
```
37+
38+
Run that reset once after installing or changing the Uniwind dependency patch. Cached transforms
39+
can otherwise reference its previous pnpm package path. Ordinary Metro starts still keep the cache.
40+
41+
Component edits use Fast Refresh. Connection-runtime edits replace the active Effect layer through
42+
a stable atom runtime, preserving navigation and existing atom subscribers. Replaced registries
43+
and managed runtimes dispose their resources; the app does not force a JavaScript reload. The Uniwind patch
44+
skips global style invalidation when generated styles and themes are unchanged, while real style
45+
changes still refresh. See [mobile development lifecycle](../../docs/internals/mobile-development.md)
46+
for the lifetime boundaries.
47+
3148
Build and run the local iOS dev client:
3249

3350
```bash
@@ -89,7 +106,9 @@ The native lint task runs SwiftLint for Swift plus ktlint and detekt for Kotlin.
89106

90107
## EAS Builds
91108

92-
CI uses Expo fingerprinting with the `preview:dev` profile to reuse an existing compatible build when possible, or start a new internal EAS build when native runtime inputs change. Production and default local builds continue to use the `appVersion` runtime policy.
109+
Preview and production variants use Expo fingerprinting so OTA updates only reach binaries with matching native dependencies, config plugins, and patches. CI uses the `preview:dev` profile to reuse a compatible native build when possible.
110+
111+
The development variant uses `appVersion` to avoid recalculating the native fingerprint for each Metro launch manifest. `MOBILE_VERSION_POLICY` can override either default. If you distribute a custom Release build with the development identity and publish OTA updates to it, set `MOBILE_VERSION_POLICY=fingerprint` for both its build and updates. Changing the runtime policy requires a native rebuild for OTA matching; an existing dev client can still load local Metro bundles.
93112

94113
For preview or production EAS environments, set `T3CODE_CLERK_PUBLISHABLE_KEY`,
95114
`T3CODE_CLERK_JWT_TEMPLATE`, and `T3CODE_RELAY_URL`

‎apps/mobile/app.config.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Object.assign(process.env, repoEnv);
1010

1111
const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT);
1212
const isIosPersonalTeamBuild = repoEnv.T3CODE_IOS_PERSONAL_TEAM === "1";
13+
const runtimeVersionPolicy =
14+
process.env.MOBILE_VERSION_POLICY ??
15+
(APP_VARIANT === "development" ? "appVersion" : "fingerprint");
1316

1417
const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim();
1518
const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/;
@@ -166,11 +169,10 @@ const config: ExpoConfig = {
166169
scheme: variant.scheme,
167170
version: "1.0.4",
168171
runtimeVersion: {
169-
// Fingerprint (not appVersion) so an OTA only reaches binaries whose native
170-
// project — native deps, config plugins, AND patches/ — matches the update.
171-
// With appVersion, every 0.1.0 build shares a runtime version, so a JS update
172-
// could land on a binary missing the native changes it needs and crash.
173-
policy: process.env.MOBILE_VERSION_POLICY ?? "fingerprint",
172+
// Development manifests resolve on every launch, so avoid fingerprint's
173+
// expensive native-project calculation there. Preview and production stay
174+
// fingerprinted so OTAs only reach binaries with matching native projects.
175+
policy: runtimeVersionPolicy,
174176
},
175177
orientation: "portrait",
176178
icon: variant.assets.appIcon,

‎apps/mobile/package.json‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
"private": true,
55
"main": "index.ts",
66
"scripts": {
7-
"dev": "expo start --clear",
8-
"dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear --lan",
9-
"dev:client:preview": "eas env:exec preview 'EXPO_NO_DOTENV=1 APP_VARIANT=preview expo start --dev-client --scheme t3code-preview --clear --lan'",
7+
"dev": "expo start",
8+
"dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --lan",
9+
"dev:client:reset": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear --lan",
10+
"dev:client:preview": "eas env:exec preview 'EXPO_NO_DOTENV=1 APP_VARIANT=preview expo start --dev-client --scheme t3code-preview --lan'",
1011
"start": "expo start",
1112
"start:dev": "APP_VARIANT=development expo start",
1213
"start:preview": "APP_VARIANT=preview expo start",
1314
"start:prod": "APP_VARIANT=production expo start",
14-
"showcase": "APP_VARIANT=production EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code --clear",
15+
"showcase": "APP_VARIANT=production EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code",
1516
"screenshots": "node ../../scripts/mobile-showcase.ts",
1617
"android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android",
1718
"android:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && REACT_NATIVE_PACKAGER_HOSTNAME=localhost expo run:android",
@@ -120,7 +121,7 @@
120121
"react-native-worklets": "0.10.1",
121122
"shiki": "4.2.0",
122123
"tailwind-merge": "^3.5.0",
123-
"uniwind": "^1.6.2"
124+
"uniwind": "1.11.0"
124125
},
125126
"devDependencies": {
126127
"@effect/vitest": "catalog:",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { SymbolView as ExpoSymbolView, type SymbolViewProps } from "expo-symbols";
2+
import { withUniwind } from "uniwind";
3+
4+
export type { SFSymbol } from "expo-symbols";
5+
export type AppSymbolName = SymbolViewProps["name"];
6+
7+
/**
8+
* Keep the iOS implementation isolated from the Android Tabler fallback so
9+
* Metro does not initialize the icon package when iOS renders SF Symbols.
10+
*/
11+
function AppSymbolView(props: SymbolViewProps) {
12+
return <ExpoSymbolView {...props} />;
13+
}
14+
15+
export const SymbolView = withUniwind(AppSymbolView);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Tabler 3.44 exports per-icon runtime modules but points their declarations at missing files.
2+
declare module "@tabler/icons-react-native/Icon*" {
3+
import type { Icon } from "@tabler/icons-react-native";
4+
5+
const icon: Icon;
6+
export default icon;
7+
}

‎apps/mobile/src/components/AppSymbol.tsx‎

Lines changed: 84 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,87 @@
1-
import {
2-
IconAdjustmentsHorizontal,
3-
IconAlertCircle,
4-
IconAlertTriangle,
5-
IconApps,
6-
IconArchive,
7-
IconArrowBackUp,
8-
IconArrowDownCircle,
9-
IconArrowRightCircle,
10-
IconArrowUp,
11-
IconArrowUpCircle,
12-
IconArrowUpRight,
13-
IconArrowUpRightCircle,
14-
IconArrowsMaximize,
15-
IconBellRinging,
16-
IconBolt,
17-
IconBox,
18-
IconCamera,
19-
IconChartBar,
20-
IconCheck,
21-
IconChevronDown,
22-
IconCode,
23-
IconChevronLeft,
24-
IconChevronRight,
25-
IconChevronUp,
26-
IconCircleCheck,
27-
IconCircleXFilled,
28-
IconClock,
29-
IconCopy,
30-
IconDeviceDesktop,
31-
IconDots,
32-
IconDotsCircleHorizontal,
33-
IconEdit,
34-
IconExternalLink,
35-
IconEye,
36-
IconFileText,
37-
IconFilter,
38-
IconFolder,
39-
IconFolderOpen,
40-
IconFolderPlus,
41-
IconGitBranch,
42-
IconHammer,
43-
IconGitMerge,
44-
IconGitPullRequest,
45-
IconInfoCircle,
46-
IconKeyboard,
47-
IconKeyboardHide,
48-
IconLayoutColumns,
49-
IconLayoutSidebar,
50-
IconLetterSpacing,
51-
IconLink,
52-
IconMessage,
53-
IconMinus,
54-
IconMoon,
55-
IconNetwork,
56-
IconPalette,
57-
IconPin,
58-
IconPinnedOff,
59-
IconPlayerPlay,
60-
IconPlayerStopFilled,
61-
IconPlus,
62-
IconQrcode,
63-
IconRefresh,
64-
IconSearch,
65-
IconServer,
66-
IconSettings,
67-
IconSparkles,
68-
IconSun,
69-
IconLayoutSidebarRight,
70-
IconTerminal2,
71-
IconTextDecrease,
72-
IconTextIncrease,
73-
IconTool,
74-
IconTrash,
75-
IconTypography,
76-
IconUserCircle,
77-
IconWifiOff,
78-
IconWorld,
79-
IconX,
80-
type Icon,
81-
} from "@tabler/icons-react-native";
82-
import { Platform } from "react-native";
83-
import { SymbolView as ExpoSymbolView, type SFSymbol, type SymbolViewProps } from "expo-symbols";
1+
import type { Icon } from "@tabler/icons-react-native/types";
2+
/*
3+
* Keep these as per-icon exports. Importing the package root eagerly registers
4+
* the entire Tabler icon set in Metro.
5+
*/
6+
import IconAdjustmentsHorizontal from "@tabler/icons-react-native/IconAdjustmentsHorizontal";
7+
import IconAlertCircle from "@tabler/icons-react-native/IconAlertCircle";
8+
import IconAlertTriangle from "@tabler/icons-react-native/IconAlertTriangle";
9+
import IconApps from "@tabler/icons-react-native/IconApps";
10+
import IconArchive from "@tabler/icons-react-native/IconArchive";
11+
import IconArrowBackUp from "@tabler/icons-react-native/IconArrowBackUp";
12+
import IconArrowDownCircle from "@tabler/icons-react-native/IconArrowDownCircle";
13+
import IconArrowRightCircle from "@tabler/icons-react-native/IconArrowRightCircle";
14+
import IconArrowUp from "@tabler/icons-react-native/IconArrowUp";
15+
import IconArrowUpCircle from "@tabler/icons-react-native/IconArrowUpCircle";
16+
import IconArrowUpRight from "@tabler/icons-react-native/IconArrowUpRight";
17+
import IconArrowUpRightCircle from "@tabler/icons-react-native/IconArrowUpRightCircle";
18+
import IconArrowsMaximize from "@tabler/icons-react-native/IconArrowsMaximize";
19+
import IconBellRinging from "@tabler/icons-react-native/IconBellRinging";
20+
import IconBolt from "@tabler/icons-react-native/IconBolt";
21+
import IconBox from "@tabler/icons-react-native/IconBox";
22+
import IconCamera from "@tabler/icons-react-native/IconCamera";
23+
import IconChartBar from "@tabler/icons-react-native/IconChartBar";
24+
import IconCheck from "@tabler/icons-react-native/IconCheck";
25+
import IconChevronDown from "@tabler/icons-react-native/IconChevronDown";
26+
import IconChevronLeft from "@tabler/icons-react-native/IconChevronLeft";
27+
import IconChevronRight from "@tabler/icons-react-native/IconChevronRight";
28+
import IconChevronUp from "@tabler/icons-react-native/IconChevronUp";
29+
import IconCircleCheck from "@tabler/icons-react-native/IconCircleCheck";
30+
import IconCircleXFilled from "@tabler/icons-react-native/IconCircleXFilled";
31+
import IconClock from "@tabler/icons-react-native/IconClock";
32+
import IconCode from "@tabler/icons-react-native/IconCode";
33+
import IconCopy from "@tabler/icons-react-native/IconCopy";
34+
import IconDeviceDesktop from "@tabler/icons-react-native/IconDeviceDesktop";
35+
import IconDots from "@tabler/icons-react-native/IconDots";
36+
import IconDotsCircleHorizontal from "@tabler/icons-react-native/IconDotsCircleHorizontal";
37+
import IconEdit from "@tabler/icons-react-native/IconEdit";
38+
import IconExternalLink from "@tabler/icons-react-native/IconExternalLink";
39+
import IconEye from "@tabler/icons-react-native/IconEye";
40+
import IconFileText from "@tabler/icons-react-native/IconFileText";
41+
import IconFilter from "@tabler/icons-react-native/IconFilter";
42+
import IconFolder from "@tabler/icons-react-native/IconFolder";
43+
import IconFolderOpen from "@tabler/icons-react-native/IconFolderOpen";
44+
import IconFolderPlus from "@tabler/icons-react-native/IconFolderPlus";
45+
import IconGitBranch from "@tabler/icons-react-native/IconGitBranch";
46+
import IconGitMerge from "@tabler/icons-react-native/IconGitMerge";
47+
import IconGitPullRequest from "@tabler/icons-react-native/IconGitPullRequest";
48+
import IconHammer from "@tabler/icons-react-native/IconHammer";
49+
import IconInfoCircle from "@tabler/icons-react-native/IconInfoCircle";
50+
import IconKeyboard from "@tabler/icons-react-native/IconKeyboard";
51+
import IconKeyboardHide from "@tabler/icons-react-native/IconKeyboardHide";
52+
import IconLayoutColumns from "@tabler/icons-react-native/IconLayoutColumns";
53+
import IconLayoutSidebar from "@tabler/icons-react-native/IconLayoutSidebar";
54+
import IconLayoutSidebarRight from "@tabler/icons-react-native/IconLayoutSidebarRight";
55+
import IconLetterSpacing from "@tabler/icons-react-native/IconLetterSpacing";
56+
import IconLink from "@tabler/icons-react-native/IconLink";
57+
import IconMessage from "@tabler/icons-react-native/IconMessage";
58+
import IconMinus from "@tabler/icons-react-native/IconMinus";
59+
import IconMoon from "@tabler/icons-react-native/IconMoon";
60+
import IconNetwork from "@tabler/icons-react-native/IconNetwork";
61+
import IconPalette from "@tabler/icons-react-native/IconPalette";
62+
import IconPin from "@tabler/icons-react-native/IconPin";
63+
import IconPinnedOff from "@tabler/icons-react-native/IconPinnedOff";
64+
import IconPlayerPlay from "@tabler/icons-react-native/IconPlayerPlay";
65+
import IconPlayerStopFilled from "@tabler/icons-react-native/IconPlayerStopFilled";
66+
import IconPlus from "@tabler/icons-react-native/IconPlus";
67+
import IconQrcode from "@tabler/icons-react-native/IconQrcode";
68+
import IconRefresh from "@tabler/icons-react-native/IconRefresh";
69+
import IconSearch from "@tabler/icons-react-native/IconSearch";
70+
import IconServer from "@tabler/icons-react-native/IconServer";
71+
import IconSettings from "@tabler/icons-react-native/IconSettings";
72+
import IconSparkles from "@tabler/icons-react-native/IconSparkles";
73+
import IconSun from "@tabler/icons-react-native/IconSun";
74+
import IconTerminal2 from "@tabler/icons-react-native/IconTerminal2";
75+
import IconTextDecrease from "@tabler/icons-react-native/IconTextDecrease";
76+
import IconTextIncrease from "@tabler/icons-react-native/IconTextIncrease";
77+
import IconTool from "@tabler/icons-react-native/IconTool";
78+
import IconTrash from "@tabler/icons-react-native/IconTrash";
79+
import IconTypography from "@tabler/icons-react-native/IconTypography";
80+
import IconUserCircle from "@tabler/icons-react-native/IconUserCircle";
81+
import IconWifiOff from "@tabler/icons-react-native/IconWifiOff";
82+
import IconWorld from "@tabler/icons-react-native/IconWorld";
83+
import IconX from "@tabler/icons-react-native/IconX";
84+
import type { SFSymbol, SymbolViewProps } from "expo-symbols";
8485
import { withUniwind } from "uniwind";
8586

8687
const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
@@ -192,10 +193,6 @@ export type { SFSymbol } from "expo-symbols";
192193
export type AppSymbolName = SymbolViewProps["name"];
193194

194195
function AppSymbolView(props: SymbolViewProps) {
195-
if (Platform.OS !== "android") {
196-
return <ExpoSymbolView {...props} />;
197-
}
198-
199196
const materialName = typeof props.name === "string" ? undefined : props.name.android;
200197
const sfSymbol = typeof props.name === "string" ? props.name : props.name.ios;
201198
const AndroidIcon =

0 commit comments

Comments
 (0)