@ramonclaudio/create-vexpo 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/dist/index.js +8 -7
- package/dist/templates/default/.eas/workflows/asc-events.yml +9 -6
- package/dist/templates/default/.eas/workflows/deploy-production.yml +28 -15
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +3 -2
- package/dist/templates/default/.eas/workflows/pr-preview.yml +12 -21
- package/dist/templates/default/.eas/workflows/release.yml +3 -7
- package/dist/templates/default/.eas/workflows/rollback.yml +54 -28
- package/dist/templates/default/.eas/workflows/rollout.yml +27 -33
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +1 -5
- package/dist/templates/default/.eas/workflows/testflight.yml +3 -7
- package/dist/templates/default/.github/workflows/check.yml +20 -12
- package/dist/templates/default/.maestro/launch.yaml +19 -10
- package/dist/templates/default/AGENTS.md +25 -8
- package/dist/templates/default/DESIGN.md +14 -10
- package/dist/templates/default/README.md +83 -78
- package/dist/templates/default/SETUP.md +159 -152
- package/dist/templates/default/__tests__/convex/_auth-harness.test.ts +112 -0
- package/dist/templates/default/__tests__/convex/_harness.ts +132 -0
- package/dist/templates/default/__tests__/convex/appAttest.test.ts +172 -0
- package/dist/templates/default/__tests__/convex/appAttestStore.test.ts +48 -0
- package/dist/templates/default/__tests__/convex/pushTokens-remove.test.ts +106 -0
- package/dist/templates/default/__tests__/convex/pushTokens-upsert.test.ts +146 -0
- package/dist/templates/default/__tests__/convex/users-deleteAccount.test.ts +140 -0
- package/dist/templates/default/__tests__/convex/users-deleteAvatar.test.ts +104 -0
- package/dist/templates/default/__tests__/convex/users-getMe.test.ts +98 -0
- package/dist/templates/default/__tests__/convex/users-getUser.test.ts +120 -0
- package/dist/templates/default/__tests__/convex/users-hardDeleteExpired.test.ts +67 -0
- package/dist/templates/default/__tests__/convex/users-restoreAccount.test.ts +96 -0
- package/dist/templates/default/__tests__/convex/users-updateAvatar.test.ts +92 -0
- package/dist/templates/default/__tests__/convex/users-updateProfile.test.ts +126 -0
- package/dist/templates/default/__tests__/convex/webhook.test.ts +31 -0
- package/dist/templates/default/__tests__/lib/deep-link.test.ts +51 -6
- package/dist/templates/default/__tests__/lib/schemas.test.ts +205 -0
- package/dist/templates/default/__tests__/lib/text-style.test.ts +31 -0
- package/dist/templates/default/_env.example +7 -7
- package/dist/templates/default/_gitattributes +1 -1
- package/dist/templates/default/_gitignore +17 -2
- package/dist/templates/default/_npmrc +7 -0
- package/dist/templates/default/_oxlintrc.json +1 -1
- package/dist/templates/default/app-store/accessibility.config.json +20 -0
- package/dist/templates/default/app-store/privacy.config.json +27 -0
- package/dist/templates/default/app.config.ts +105 -33
- package/dist/templates/default/app.json +1 -9
- package/dist/templates/default/convex/_generated/api.d.ts +12 -0
- package/dist/templates/default/convex/admin.ts +0 -13
- package/dist/templates/default/convex/appAttest.ts +467 -0
- package/dist/templates/default/convex/appAttestStore.ts +141 -0
- package/dist/templates/default/convex/apple.ts +53 -0
- package/dist/templates/default/convex/auth.ts +6 -45
- package/dist/templates/default/convex/constants.ts +2 -7
- package/dist/templates/default/convex/crons.ts +12 -5
- package/dist/templates/default/convex/email.ts +4 -24
- package/dist/templates/default/convex/env.ts +0 -4
- package/dist/templates/default/convex/errors.ts +0 -7
- package/dist/templates/default/convex/functions.ts +0 -26
- package/dist/templates/default/convex/http.ts +3 -5
- package/dist/templates/default/convex/log.ts +2 -25
- package/dist/templates/default/convex/pushSender.ts +145 -0
- package/dist/templates/default/convex/pushTokens.ts +110 -13
- package/dist/templates/default/convex/rateLimit.ts +8 -39
- package/dist/templates/default/convex/schema.ts +48 -5
- package/dist/templates/default/convex/tsconfig.json +1 -0
- package/dist/templates/default/convex/users.ts +143 -61
- package/dist/templates/default/convex/validators.ts +1 -38
- package/dist/templates/default/convex/webhook.ts +1 -31
- package/dist/templates/default/convex.json +1 -2
- package/dist/templates/default/metro.config.js +9 -1
- package/dist/templates/default/package.json +67 -70
- package/dist/templates/default/plugins/README.md +5 -1
- package/dist/templates/default/scripts/README.md +9 -9
- package/dist/templates/default/scripts/_run.mjs +3 -20
- package/dist/templates/default/scripts/clean.ts +81 -69
- package/dist/templates/default/scripts/gen-update-cert.mjs +98 -0
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home)/index.tsx +21 -6
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home,search)/_layout.tsx +9 -8
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(search)/index.tsx +26 -24
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/_layout.tsx +3 -4
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/_layout.tsx +10 -6
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/index.tsx +81 -51
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/preferences.tsx +72 -12
- package/dist/templates/default/src/app/(app)/_layout.tsx +147 -0
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/_layout.tsx +4 -5
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/forgot-password.tsx +15 -9
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/reset-password.tsx +88 -14
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-in.tsx +65 -35
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-up.tsx +131 -196
- package/dist/templates/default/src/app/(app)/debug.tsx +479 -0
- package/dist/templates/default/{app → src/app}/(app)/help.tsx +76 -64
- package/dist/templates/default/{app → src/app}/(app)/linked.tsx +21 -27
- package/dist/templates/default/{app → src/app}/(app)/privacy.tsx +35 -8
- package/dist/templates/default/src/app/(app)/profile/change-password.tsx +264 -0
- package/dist/templates/default/{app/(app)/profile.tsx → src/app/(app)/profile/index.tsx} +179 -255
- package/dist/templates/default/src/app/(app)/restore-account.tsx +192 -0
- package/dist/templates/default/src/app/(app)/sessions.tsx +287 -0
- package/dist/templates/default/src/app/(app)/welcome.tsx +194 -0
- package/dist/templates/default/src/app/+native-intent.tsx +25 -0
- package/dist/templates/default/src/app/+not-found.tsx +43 -0
- package/dist/templates/default/{app → src/app}/_layout.tsx +28 -37
- package/dist/templates/default/src/components/auth/apple-button.tsx +51 -0
- package/dist/templates/default/{components → src/components}/auth/otp-verification.tsx +79 -58
- package/dist/templates/default/{components → src/components}/auth/password-field.tsx +74 -18
- package/dist/templates/default/src/components/auth/segmented-toggle.tsx +71 -0
- package/dist/templates/default/src/components/ui/content-unavailable.tsx +81 -0
- package/dist/templates/default/src/components/ui/convex-error.tsx +21 -0
- package/dist/templates/default/src/components/ui/error-boundary.tsx +89 -0
- package/dist/templates/default/{components → src/components}/ui/loading-screen.tsx +5 -4
- package/dist/templates/default/{components → src/components}/ui/material.tsx +50 -17
- package/dist/templates/default/src/components/ui/offline-banner.tsx +59 -0
- package/dist/templates/default/{components → src/components}/ui/prominent-button.tsx +8 -11
- package/dist/templates/default/{components → src/components}/ui/skeleton.tsx +31 -13
- package/dist/templates/default/src/components/ui/status-text.tsx +64 -0
- package/dist/templates/default/src/components/ui/update-banner.tsx +85 -0
- package/dist/templates/default/{constants → src/constants}/layout.ts +0 -6
- package/dist/templates/default/{constants → src/constants}/theme.ts +49 -64
- package/dist/templates/default/{constants → src/constants}/ui.ts +13 -4
- package/dist/templates/default/src/hooks/use-debounce.ts +12 -0
- package/dist/templates/default/src/hooks/use-deep-link.ts +51 -0
- package/dist/templates/default/src/hooks/use-delete-account.ts +35 -0
- package/dist/templates/default/src/hooks/use-motion-screen-options.ts +13 -0
- package/dist/templates/default/src/hooks/use-network.ts +34 -0
- package/dist/templates/default/{hooks → src/hooks}/use-notifications.ts +39 -30
- package/dist/templates/default/src/hooks/use-reduce-transparency.ts +30 -0
- package/dist/templates/default/{hooks → src/hooks}/use-theme.ts +0 -5
- package/dist/templates/default/src/lib/appAttest.ts +78 -0
- package/dist/templates/default/src/lib/assets.ts +9 -0
- package/dist/templates/default/src/lib/deep-link.ts +82 -0
- package/dist/templates/default/{lib → src/lib}/dev-menu.ts +0 -4
- package/dist/templates/default/{lib → src/lib}/device.ts +1 -13
- package/dist/templates/default/{lib → src/lib}/dynamic-font.ts +13 -10
- package/dist/templates/default/src/lib/dynamic-symbol-size.ts +33 -0
- package/dist/templates/default/src/lib/masks.ts +21 -0
- package/dist/templates/default/src/lib/native-state.ts +20 -0
- package/dist/templates/default/{lib → src/lib}/notifications.ts +7 -45
- package/dist/templates/default/{lib → src/lib}/preferences.ts +0 -2
- package/dist/templates/default/{lib → src/lib}/schemas.ts +19 -16
- package/dist/templates/default/src/lib/text-style.ts +20 -0
- package/dist/templates/default/{lib → src/lib}/updates.ts +0 -7
- package/dist/templates/default/store.config.json +1 -1
- package/dist/templates/default/tsconfig.json +3 -1
- package/dist/templates/default/vitest.config.ts +8 -1
- package/package.json +5 -5
- package/dist/templates/default/app/(app)/_layout.tsx +0 -73
- package/dist/templates/default/app/(app)/debug.tsx +0 -389
- package/dist/templates/default/app/(app)/sessions.tsx +0 -191
- package/dist/templates/default/app/(app)/welcome.tsx +0 -140
- package/dist/templates/default/app/+native-intent.tsx +0 -14
- package/dist/templates/default/app/+not-found.tsx +0 -51
- package/dist/templates/default/bun.lock +0 -1860
- package/dist/templates/default/components/auth/segmented-toggle.tsx +0 -47
- package/dist/templates/default/components/ui/convex-error.tsx +0 -32
- package/dist/templates/default/components/ui/error-boundary.tsx +0 -57
- package/dist/templates/default/components/ui/offline-banner.tsx +0 -58
- package/dist/templates/default/components/ui/status-text.tsx +0 -49
- package/dist/templates/default/components/ui/update-banner.tsx +0 -82
- package/dist/templates/default/fingerprint.config.js +0 -9
- package/dist/templates/default/hooks/use-debounce.ts +0 -20
- package/dist/templates/default/hooks/use-deep-link.ts +0 -43
- package/dist/templates/default/hooks/use-network.ts +0 -11
- package/dist/templates/default/lib/assets.ts +0 -17
- package/dist/templates/default/lib/deep-link.ts +0 -71
- package/dist/templates/default/patches/PR-368.patch +0 -91
- package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-navigation-tracking.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-onboarding.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-reduced-motion.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-updates.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/a11y.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/app.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/auth-client.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/convex-auth.tsx +0 -0
- /package/dist/templates/default/{lib → src/lib}/env.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/haptics.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/storage.ts +0 -0
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vexpo clean script.
|
|
3
3
|
*
|
|
4
|
-
* Wipes every regenerable cache and build artifact:
|
|
5
|
-
* - Project artifacts: node_modules, bun.lock, ios/, .expo/, dist/, convex/_generated/, tsconfig.tsbuildinfo, coverage/, .vitest-cache/, expo-env.d.ts, bun-error.*, *.log
|
|
6
|
-
* - .eas/ per-project state (keeps .eas/workflows/)
|
|
7
|
-
* - .DS_Store files repo-wide
|
|
8
|
-
* - $TMPDIR caches: metro-*, haste-map-*, react-*, node-compile-cache, expo-*, RN*
|
|
9
|
-
* - System caches: ~/Library/Caches/CocoaPods, ~/.expo
|
|
10
|
-
* - Xcode build outputs: ~/Library/Developer/Xcode/DerivedData/<project>-*
|
|
11
|
-
*
|
|
12
4
|
* Never wiped (user data / secrets):
|
|
13
5
|
* - .env / .env.* (auth values)
|
|
14
6
|
* - .p8 / .p12 / AuthKey_* / SubscriptionKey_* (Apple keys)
|
|
@@ -16,16 +8,12 @@
|
|
|
16
8
|
* - .vexpo-manual-setup/ / .rebrand-backup/
|
|
17
9
|
* - .setup-state.json (opt-in via --state)
|
|
18
10
|
*
|
|
19
|
-
*
|
|
11
|
+
* Kept by default so reinstall is deterministic (opt in via --all):
|
|
12
|
+
* - package-lock.json / bun.lock / yarn.lock (lockfile; `npm ci` or
|
|
13
|
+
* `<pm> install --frozen-lockfile` when present)
|
|
14
|
+
* - convex/_generated/ (regenerated via `npx convex codegen` after --all)
|
|
20
15
|
*
|
|
21
16
|
* Uses macOS `trash` for every delete so anything wiped is recoverable.
|
|
22
|
-
*
|
|
23
|
-
* Usage:
|
|
24
|
-
* bun run clean full wipe + install
|
|
25
|
-
* bun run clean --metro just Metro/Haste/Babel caches (fast, no reinstall)
|
|
26
|
-
* bun run clean --state also wipe .setup-state.json (next setup re-probes everything)
|
|
27
|
-
* bun run clean --no-install wipe everything but skip the reinstall
|
|
28
|
-
* bun run clean --help
|
|
29
17
|
*/
|
|
30
18
|
|
|
31
19
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
@@ -107,13 +95,7 @@ async function fileExists(p: string): Promise<boolean> {
|
|
|
107
95
|
|
|
108
96
|
type PM = "bun" | "pnpm" | "yarn" | "npm";
|
|
109
97
|
|
|
110
|
-
|
|
111
|
-
* Capture which PM ran this script BEFORE any wipes. Two signals:
|
|
112
|
-
* 1. `npm_execpath`. every modern PM (npm/bun/pnpm/yarn) sets this to its
|
|
113
|
-
* own binary path when running scripts. Most reliable.
|
|
114
|
-
* 2. Lockfile presence. fallback when running outside `<pm> run` (e.g.
|
|
115
|
-
* direct `node scripts/clean.ts`). Read while the lockfile still exists.
|
|
116
|
-
*/
|
|
98
|
+
// Capture which PM ran this script BEFORE any wipes: --all trashes the lockfile.
|
|
117
99
|
async function detectPackageManager(): Promise<PM> {
|
|
118
100
|
const execpath = (process.env.npm_execpath ?? "").toLowerCase();
|
|
119
101
|
if (execpath.includes("bun")) return "bun";
|
|
@@ -126,15 +108,16 @@ async function detectPackageManager(): Promise<PM> {
|
|
|
126
108
|
return "npm";
|
|
127
109
|
}
|
|
128
110
|
|
|
129
|
-
function installCmdFor(pm: PM): string {
|
|
130
|
-
return `${pm} install`;
|
|
111
|
+
function installCmdFor(pm: PM, frozen: boolean): string {
|
|
112
|
+
if (!frozen) return `${pm} install`;
|
|
113
|
+
// npm uses `ci` for frozen installs, every other PM has `--frozen-lockfile`.
|
|
114
|
+
if (pm === "npm") return "npm ci";
|
|
115
|
+
return `${pm} install --frozen-lockfile`;
|
|
131
116
|
}
|
|
132
117
|
|
|
133
118
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
134
119
|
process.chdir(REPO_ROOT);
|
|
135
120
|
|
|
136
|
-
// ─── Output ──────────────────────────────────────────────────────────────────
|
|
137
|
-
|
|
138
121
|
const RESET = "\x1b[0m";
|
|
139
122
|
const BOLD = "\x1b[1m";
|
|
140
123
|
const DIM = "\x1b[2m";
|
|
@@ -164,27 +147,32 @@ function section(title: string): void {
|
|
|
164
147
|
line(`\n${BOLD}${VIOLET}${title}${RESET} ${DIM}${fill}${RESET}`);
|
|
165
148
|
}
|
|
166
149
|
|
|
167
|
-
// ─── Args ────────────────────────────────────────────────────────────────────
|
|
168
|
-
|
|
169
150
|
const HELP = `${BOLD}vexpo clean${RESET}
|
|
170
151
|
|
|
171
152
|
${BOLD}Usage:${RESET}
|
|
172
|
-
${DIM}
|
|
173
|
-
${DIM}
|
|
174
|
-
${DIM}
|
|
175
|
-
${DIM}
|
|
176
|
-
${DIM}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
expo-env.d.ts, bun-error.*,
|
|
181
|
-
all .DS_Store files, $TMPDIR
|
|
182
|
-
~/Library/Caches/CocoaPods, ~/.expo,
|
|
183
|
-
subfolder for this project.
|
|
184
|
-
|
|
153
|
+
${DIM}npm run clean${RESET} wipe caches, keep lockfile, frozen install
|
|
154
|
+
${DIM}npm run clean --all${RESET} also wipe lockfile + convex/_generated
|
|
155
|
+
${DIM}npm run clean --metro${RESET} just Metro/Haste/Babel caches
|
|
156
|
+
${DIM}npm run clean --state${RESET} also wipe .setup-state.json
|
|
157
|
+
${DIM}npm run clean --no-install${RESET} wipe everything but skip reinstall
|
|
158
|
+
${DIM}npm run clean --help${RESET}
|
|
159
|
+
|
|
160
|
+
The default wipe removes node_modules, ios/, .expo/, dist/,
|
|
161
|
+
tsbuildinfo, coverage/, .vitest-cache/, expo-env.d.ts, bun-error.*,
|
|
162
|
+
*.log, .eas/ (except workflows/), all .DS_Store files, $TMPDIR
|
|
163
|
+
Metro/Haste/React/expo/RN caches, ~/Library/Caches/CocoaPods, ~/.expo,
|
|
164
|
+
and the Xcode DerivedData subfolder for this project. The lockfile
|
|
165
|
+
and convex/_generated/ are kept so reinstall is deterministic
|
|
166
|
+
(${DIM}npm ci${RESET}). Never touches .env files,
|
|
167
|
+
Apple keys, store.config.json, .vexpo-manual-setup/, or .rebrand-backup/.
|
|
168
|
+
|
|
169
|
+
${BOLD}--all${RESET} additionally wipes the lockfile and convex/_generated/.
|
|
170
|
+
Reinstall resolves transitives fresh and ${DIM}npx convex codegen${RESET} runs
|
|
171
|
+
after install to rebuild the Convex bindings. Use when the lockfile
|
|
172
|
+
is suspect or you want a true clean-slate reinstall.
|
|
185
173
|
|
|
186
174
|
${BOLD}--state${RESET} additionally wipes .setup-state.json so the next
|
|
187
|
-
${DIM}
|
|
175
|
+
${DIM}npm run setup${RESET} re-probes every phase against external services
|
|
188
176
|
(slower, but the cure when state has drifted from reality).
|
|
189
177
|
|
|
190
178
|
Bundlers (Metro, expo CLI, react-native start, Watchman) are stopped
|
|
@@ -193,13 +181,20 @@ held open. ${BOLD}convex dev${RESET} is left alone (it's your data layer, not a
|
|
|
193
181
|
bundler); restart it manually if it misbehaves after a full wipe.
|
|
194
182
|
`;
|
|
195
183
|
|
|
196
|
-
let args: {
|
|
184
|
+
let args: {
|
|
185
|
+
metro?: boolean;
|
|
186
|
+
state?: boolean;
|
|
187
|
+
all?: boolean;
|
|
188
|
+
"no-install"?: boolean;
|
|
189
|
+
help?: boolean;
|
|
190
|
+
};
|
|
197
191
|
try {
|
|
198
192
|
args = parseArgs({
|
|
199
193
|
args: process.argv.slice(2),
|
|
200
194
|
options: {
|
|
201
195
|
metro: { type: "boolean", default: false },
|
|
202
196
|
state: { type: "boolean", default: false },
|
|
197
|
+
all: { type: "boolean", default: false },
|
|
203
198
|
"no-install": { type: "boolean", default: false },
|
|
204
199
|
help: { type: "boolean", short: "h", default: false },
|
|
205
200
|
},
|
|
@@ -215,8 +210,6 @@ if (args.help) {
|
|
|
215
210
|
process.exit(0);
|
|
216
211
|
}
|
|
217
212
|
|
|
218
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
219
|
-
|
|
220
213
|
async function pathExists(p: string): Promise<boolean> {
|
|
221
214
|
try {
|
|
222
215
|
await stat(p);
|
|
@@ -246,7 +239,6 @@ async function trashPaths(paths: string[]): Promise<void> {
|
|
|
246
239
|
|
|
247
240
|
async function expandGlob(dir: string, pattern: string): Promise<string[]> {
|
|
248
241
|
if (!(await pathExists(dir))) return [];
|
|
249
|
-
// Convert simple glob (only * supported) to regex. Sufficient for our patterns.
|
|
250
242
|
const re = new RegExp(
|
|
251
243
|
"^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$",
|
|
252
244
|
);
|
|
@@ -254,8 +246,6 @@ async function expandGlob(dir: string, pattern: string): Promise<string[]> {
|
|
|
254
246
|
return entries.filter((e) => re.test(e)).map((e) => `${dir}/${e}`);
|
|
255
247
|
}
|
|
256
248
|
|
|
257
|
-
// ─── Targets ─────────────────────────────────────────────────────────────────
|
|
258
|
-
|
|
259
249
|
const REPO = REPO_ROOT;
|
|
260
250
|
const TMPDIR = process.env.TMPDIR?.replace(/\/$/, "") ?? "/tmp";
|
|
261
251
|
const HOME = homedir();
|
|
@@ -273,37 +263,32 @@ async function readPkgName(): Promise<string> {
|
|
|
273
263
|
|
|
274
264
|
const PROJECT_TARGETS = [
|
|
275
265
|
"node_modules",
|
|
276
|
-
"bun.lock",
|
|
277
266
|
"ios",
|
|
278
267
|
".expo",
|
|
279
268
|
"dist",
|
|
280
|
-
"convex/_generated",
|
|
281
269
|
"tsconfig.tsbuildinfo",
|
|
282
270
|
"coverage",
|
|
283
271
|
".vitest-cache",
|
|
284
272
|
"expo-env.d.ts",
|
|
285
273
|
];
|
|
286
274
|
|
|
287
|
-
//
|
|
288
|
-
//
|
|
275
|
+
// Wiped only with --all. Default leaves these alone: the lockfile stays the
|
|
276
|
+
// source of truth (frozen install) and convex/_generated needs a deployment
|
|
277
|
+
// round-trip via `npx convex codegen` to come back.
|
|
278
|
+
const PROJECT_TARGETS_ALL = ["bun.lock", "package-lock.json", "convex/_generated"];
|
|
279
|
+
|
|
289
280
|
const PROJECT_GLOBS = ["bun-error.*", "*.log"];
|
|
290
281
|
|
|
291
282
|
const TMP_GLOBS = ["metro-*", "haste-map-*", "react-*", "node-compile-cache", "expo-*", "RN*"];
|
|
292
283
|
|
|
293
|
-
// ─── Steps ───────────────────────────────────────────────────────────────────
|
|
294
|
-
|
|
295
284
|
/**
|
|
296
285
|
* Stop bundlers before wiping their caches. macOS `trash` silently skips
|
|
297
286
|
* files held open by a running process, so caches survive the wipe and the
|
|
298
287
|
* bundler restarts onto stale state. Killing first prevents that.
|
|
299
|
-
*
|
|
300
|
-
* Bundlers are killed automatically. `convex dev` is the user's data layer,
|
|
301
|
-
* not a bundler. left alone, with a warning.
|
|
302
288
|
*/
|
|
303
289
|
async function stepStopBundlers(): Promise<void> {
|
|
304
290
|
section("Stop bundlers");
|
|
305
291
|
|
|
306
|
-
// Patterns are pgrep -f extended regex over the full command line.
|
|
307
292
|
// Order: kill the parent CLI first so it can tear down its child Metro.
|
|
308
293
|
const targets: { pattern: string; name: string }[] = [
|
|
309
294
|
{ pattern: "node .*\\.bin/expo (run:|start)", name: "expo CLI" },
|
|
@@ -321,7 +306,6 @@ async function stepStopBundlers(): Promise<void> {
|
|
|
321
306
|
killed += pids.length;
|
|
322
307
|
}
|
|
323
308
|
|
|
324
|
-
// Watchman has its own clean shutdown. No-op if not installed (exit 127).
|
|
325
309
|
const wm = await spawn(["watchman", "shutdown-server"], {
|
|
326
310
|
stdio: ["ignore", "ignore", "ignore"],
|
|
327
311
|
}).exited;
|
|
@@ -361,9 +345,10 @@ async function stepMetroCachesOnly(): Promise<void> {
|
|
|
361
345
|
ok(`trashed ${matches.length} cache director${matches.length === 1 ? "y" : "ies"}`);
|
|
362
346
|
}
|
|
363
347
|
|
|
364
|
-
async function stepProjectArtifacts(): Promise<void> {
|
|
348
|
+
async function stepProjectArtifacts(all: boolean): Promise<void> {
|
|
365
349
|
section("Project artifacts");
|
|
366
|
-
const
|
|
350
|
+
const names = all ? [...PROJECT_TARGETS, ...PROJECT_TARGETS_ALL] : PROJECT_TARGETS;
|
|
351
|
+
const targets = names.map((t) => `${REPO}/${t}`);
|
|
367
352
|
for (const pattern of PROJECT_GLOBS) {
|
|
368
353
|
targets.push(...(await expandGlob(REPO, pattern)));
|
|
369
354
|
}
|
|
@@ -477,8 +462,6 @@ async function stepExpoCache(): Promise<void> {
|
|
|
477
462
|
nop("~/.expo not present");
|
|
478
463
|
return;
|
|
479
464
|
}
|
|
480
|
-
// .expo holds the user-level Expo cache (devices.json, telemetry, sdk
|
|
481
|
-
// metadata). Safe to wipe; Expo regenerates on next CLI invocation.
|
|
482
465
|
await trashPaths([path]);
|
|
483
466
|
ok("trashed ~/.expo");
|
|
484
467
|
}
|
|
@@ -491,19 +474,46 @@ async function stepSetupState(): Promise<void> {
|
|
|
491
474
|
return;
|
|
492
475
|
}
|
|
493
476
|
await trashPaths([path]);
|
|
494
|
-
ok("trashed .setup-state.json (next `
|
|
477
|
+
ok("trashed .setup-state.json (next `npm run setup` re-probes every phase)");
|
|
495
478
|
}
|
|
496
479
|
|
|
497
480
|
async function stepInstall(pm: PM): Promise<void> {
|
|
498
481
|
section("Reinstall");
|
|
499
|
-
|
|
482
|
+
// Frozen install when a lockfile is on disk: deterministic, no transitive drift.
|
|
483
|
+
// After --all the lockfile is gone and bun resolves fresh.
|
|
484
|
+
const frozen = await pathExists(`${REPO}/${lockfileFor(pm)}`);
|
|
485
|
+
const cmd = installCmdFor(pm, frozen).split(" ");
|
|
500
486
|
const proc = spawn(cmd, { stdio: ["inherit", "inherit", "inherit"] });
|
|
501
487
|
const code = await proc.exited;
|
|
502
488
|
if (code !== 0) throw new Error(`${cmd.join(" ")} exited with code ${code}`);
|
|
503
489
|
ok(cmd.join(" "));
|
|
504
490
|
}
|
|
505
491
|
|
|
506
|
-
|
|
492
|
+
function lockfileFor(pm: PM): string {
|
|
493
|
+
if (pm === "bun") return "bun.lock";
|
|
494
|
+
if (pm === "pnpm") return "pnpm-lock.yaml";
|
|
495
|
+
if (pm === "yarn") return "yarn.lock";
|
|
496
|
+
return "package-lock.json";
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function stepConvexCodegen(): Promise<void> {
|
|
500
|
+
section("Convex codegen");
|
|
501
|
+
if (await pathExists(`${REPO}/convex/_generated`)) {
|
|
502
|
+
nop("convex/_generated/ present (skipped)");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// No bun.lock or convex/_generated on disk after --all. `convex codegen`
|
|
506
|
+
// talks to the deployment to rebuild the TypeScript bindings; skip and warn
|
|
507
|
+
// if the env isn't wired so this never blocks a clean.
|
|
508
|
+
const cmd = ["npx", "convex", "codegen"];
|
|
509
|
+
const proc = spawn(cmd, { stdio: ["inherit", "inherit", "inherit"] });
|
|
510
|
+
const code = await proc.exited;
|
|
511
|
+
if (code !== 0) {
|
|
512
|
+
bad(`${cmd.join(" ")} exited with code ${code} (run it manually once Convex is reachable)`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
ok(cmd.join(" "));
|
|
516
|
+
}
|
|
507
517
|
|
|
508
518
|
// Wrapped in an async IIFE so the file works under both ESM (top-level await
|
|
509
519
|
// supported) and CJS-via-tsx (no top-level await).
|
|
@@ -514,11 +524,12 @@ void (async () => {
|
|
|
514
524
|
await stepStopBundlers();
|
|
515
525
|
await stepMetroCachesOnly();
|
|
516
526
|
} else {
|
|
517
|
-
// Capture PM BEFORE any wipes;
|
|
527
|
+
// Capture PM BEFORE any wipes; --all trashes the lockfile.
|
|
518
528
|
const pm = await detectPackageManager();
|
|
519
529
|
const pkgName = await readPkgName();
|
|
530
|
+
const all = args.all === true;
|
|
520
531
|
await stepStopBundlers();
|
|
521
|
-
await stepProjectArtifacts();
|
|
532
|
+
await stepProjectArtifacts(all);
|
|
522
533
|
await stepEasState();
|
|
523
534
|
await stepDsStores();
|
|
524
535
|
await stepTmpdirCaches();
|
|
@@ -528,6 +539,7 @@ void (async () => {
|
|
|
528
539
|
if (args.state) await stepSetupState();
|
|
529
540
|
if (!args["no-install"]) {
|
|
530
541
|
await stepInstall(pm);
|
|
542
|
+
if (all) await stepConvexCodegen();
|
|
531
543
|
} else {
|
|
532
544
|
yep(`--no-install passed; skipping ${pm} install`);
|
|
533
545
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-shot setup for OTA update code signing.
|
|
4
|
+
*
|
|
5
|
+
* npm run updates:gen-cert -- --name "Acme Inc."
|
|
6
|
+
*
|
|
7
|
+
* Wraps `npx expo-updates codesigning:generate` with vexpo's conventions:
|
|
8
|
+
* - cert goes to `./certs/certificate.pem` (committed; verified on-device)
|
|
9
|
+
* - private key goes to `../keys/private-key.pem` (NOT committed; lives
|
|
10
|
+
* as an EAS file-type env var in CI)
|
|
11
|
+
* - validity duration: 10 years (long enough to outlive most apps)
|
|
12
|
+
*
|
|
13
|
+
* Once the cert exists, `app.config.ts` automatically wires the
|
|
14
|
+
* `codeSigningCertificate` / `codeSigningMetadata` block. No manual edits.
|
|
15
|
+
*
|
|
16
|
+
* Two more steps, printed at the end, finish CI wiring:
|
|
17
|
+
* 1. `eas env:create --environment production --visibility secret \
|
|
18
|
+
* --type file --name EAS_UPDATE_PRIVATE_KEY \
|
|
19
|
+
* --value <path-to-private-key.pem>`
|
|
20
|
+
* 2. Confirm `.eas/workflows/deploy-production.yml`'s `update_ios` job
|
|
21
|
+
* passes `private_key_path: "$EAS_UPDATE_PRIVATE_KEY"` (ships wired).
|
|
22
|
+
*
|
|
23
|
+
* After that every OTA bundle is signed locally during `eas update` and
|
|
24
|
+
* verified on-device before install. A compromised EAS account or CDN
|
|
25
|
+
* cannot ship arbitrary JS.
|
|
26
|
+
*
|
|
27
|
+
* https://docs.expo.dev/eas-update/code-signing/
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { existsSync } from "node:fs";
|
|
31
|
+
import { resolve, dirname } from "node:path";
|
|
32
|
+
import { spawnSync } from "node:child_process";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
import { createInterface } from "node:readline/promises";
|
|
35
|
+
|
|
36
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const PROJECT = resolve(HERE, "..");
|
|
38
|
+
const CERT = resolve(PROJECT, "certs", "certificate.pem");
|
|
39
|
+
const KEY = resolve(PROJECT, "..", "keys", "private-key.pem");
|
|
40
|
+
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
const flagIndex = args.indexOf("--name");
|
|
43
|
+
const flagValue = flagIndex >= 0 ? args[flagIndex + 1] : undefined;
|
|
44
|
+
|
|
45
|
+
if (existsSync(CERT)) {
|
|
46
|
+
console.error(`Certificate already exists at ${CERT}`);
|
|
47
|
+
console.error("Delete it (and the matching private key) before regenerating.");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const commonName = await resolveCommonName(flagValue);
|
|
52
|
+
|
|
53
|
+
const result = spawnSync(
|
|
54
|
+
"npx",
|
|
55
|
+
[
|
|
56
|
+
"expo-updates",
|
|
57
|
+
"codesigning:generate",
|
|
58
|
+
"--certificate-output-directory",
|
|
59
|
+
"certs",
|
|
60
|
+
"--key-output-directory",
|
|
61
|
+
"../keys",
|
|
62
|
+
"--certificate-validity-duration-years",
|
|
63
|
+
"10",
|
|
64
|
+
"--certificate-common-name",
|
|
65
|
+
commonName,
|
|
66
|
+
],
|
|
67
|
+
{ cwd: PROJECT, stdio: "inherit" },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (result.status !== 0) {
|
|
71
|
+
process.exit(result.status ?? 1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log("\n--- Next steps ---");
|
|
75
|
+
console.log(`1. Commit ${CERT.replace(`${PROJECT}/`, "")} (it's a public cert).`);
|
|
76
|
+
console.log(`2. Upload the private key to EAS as a file-type secret:`);
|
|
77
|
+
console.log(` eas env:create --environment production --visibility secret \\`);
|
|
78
|
+
console.log(` --type file --name EAS_UPDATE_PRIVATE_KEY --value ${KEY}`);
|
|
79
|
+
console.log(`3. Keep ${KEY} off committed surface. The .gitignore already covers it.`);
|
|
80
|
+
console.log(
|
|
81
|
+
`4. The next \`expo prebuild\` picks up the cert automatically. Run \`npm run prebuild\`.`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
async function resolveCommonName(provided) {
|
|
85
|
+
if (provided && provided.trim().length > 0) return provided.trim();
|
|
86
|
+
if (!process.stdin.isTTY) {
|
|
87
|
+
console.error("Provide --name '<Organization Name>' when running non-interactively.");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
91
|
+
const answer = (await rl.question("Certificate common name (organization): ")).trim();
|
|
92
|
+
rl.close();
|
|
93
|
+
if (answer.length === 0) {
|
|
94
|
+
console.error("Common name is required.");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
return answer;
|
|
98
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { Host, ScrollView, VStack, Text
|
|
2
|
-
import {
|
|
1
|
+
import { Host, ScrollView, VStack, Text } from "@expo/ui/swift-ui";
|
|
2
|
+
import {
|
|
3
|
+
foregroundStyle,
|
|
4
|
+
kerning,
|
|
5
|
+
padding,
|
|
6
|
+
frame,
|
|
7
|
+
refreshable,
|
|
8
|
+
tint,
|
|
9
|
+
} from "@expo/ui/swift-ui/modifiers";
|
|
3
10
|
|
|
11
|
+
import { ContentUnavailable } from "@/components/ui/content-unavailable";
|
|
4
12
|
import { authClient } from "@/lib/auth-client";
|
|
5
13
|
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
6
14
|
import { useColors } from "@/hooks/use-theme";
|
|
@@ -8,14 +16,18 @@ import { useColors } from "@/hooks/use-theme";
|
|
|
8
16
|
export default function HomeScreen() {
|
|
9
17
|
const dfont = useDynamicFont();
|
|
10
18
|
const colors = useColors();
|
|
11
|
-
const { data: session } = authClient.useSession();
|
|
19
|
+
const { data: session, refetch } = authClient.useSession();
|
|
12
20
|
|
|
13
21
|
const name = session?.user?.name?.split(" ")[0] ?? "there";
|
|
14
22
|
const now = new Date();
|
|
15
23
|
|
|
24
|
+
const onRefresh = async () => {
|
|
25
|
+
await refetch?.();
|
|
26
|
+
};
|
|
27
|
+
|
|
16
28
|
return (
|
|
17
|
-
<Host style={{ flex: 1 }}>
|
|
18
|
-
<ScrollView modifiers={[tint(colors.primary as string)]}>
|
|
29
|
+
<Host testID="home-screen" style={{ flex: 1 }}>
|
|
30
|
+
<ScrollView modifiers={[tint(colors.primary as string), refreshable(onRefresh)]}>
|
|
19
31
|
<VStack
|
|
20
32
|
spacing={24}
|
|
21
33
|
alignment="leading"
|
|
@@ -27,18 +39,21 @@ export default function HomeScreen() {
|
|
|
27
39
|
modifiers={[frame({ maxWidth: Infinity, alignment: "leading" })]}
|
|
28
40
|
>
|
|
29
41
|
<Text
|
|
42
|
+
testID="home-date"
|
|
30
43
|
modifiers={[dfont({ size: 14 }), foregroundStyle(colors.mutedForeground as string)]}
|
|
31
44
|
>
|
|
32
45
|
<Text date={now} dateStyle="date" />
|
|
33
46
|
</Text>
|
|
34
47
|
<Text
|
|
48
|
+
testID="home-greeting"
|
|
35
49
|
modifiers={[dfont({ size: 32, weight: "bold", design: "rounded" }), kerning(-0.5)]}
|
|
36
50
|
>
|
|
37
51
|
Hey, {name}
|
|
38
52
|
</Text>
|
|
39
53
|
</VStack>
|
|
40
54
|
|
|
41
|
-
<
|
|
55
|
+
<ContentUnavailable
|
|
56
|
+
testID="home-empty"
|
|
42
57
|
title="Nothing here yet"
|
|
43
58
|
systemImage="square.dashed"
|
|
44
59
|
description="Home screen is ready to build."
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Stack
|
|
1
|
+
import { Stack } from "expo-router";
|
|
2
2
|
|
|
3
3
|
import { useColors } from "@/hooks/use-theme";
|
|
4
|
-
import {
|
|
4
|
+
import { useMotionScreenOptions } from "@/hooks/use-motion-screen-options";
|
|
5
5
|
import { HeaderTint } from "@/constants/theme";
|
|
6
6
|
import { FontFamily } from "@/constants/layout";
|
|
7
7
|
|
|
@@ -10,15 +10,18 @@ export const unstable_settings = {
|
|
|
10
10
|
search: { anchor: "index" },
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// `segment` is the active group name in the comma-shared array, e.g.
|
|
14
|
+
// `(home)` or `(search)`. Per the SDK 56 shared-routes docs, this is the
|
|
15
|
+
// canonical entry point — no `useSegments()` cast, no magic index.
|
|
16
|
+
export default function SharedLayout({ segment }: { segment: string }) {
|
|
14
17
|
const colors = useColors();
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const isSearch = segments[2] === "(search)";
|
|
18
|
+
const motion = useMotionScreenOptions("default");
|
|
19
|
+
const isSearch = segment === "(search)";
|
|
18
20
|
|
|
19
21
|
return (
|
|
20
22
|
<Stack
|
|
21
23
|
screenOptions={{
|
|
24
|
+
...motion,
|
|
22
25
|
headerTintColor: HeaderTint as string,
|
|
23
26
|
headerBlurEffect: "none",
|
|
24
27
|
headerShadowVisible: false,
|
|
@@ -26,8 +29,6 @@ export default function SharedLayout() {
|
|
|
26
29
|
headerLargeStyle: { backgroundColor: "transparent" },
|
|
27
30
|
headerTitleStyle: { fontFamily: FontFamily.semiBold },
|
|
28
31
|
headerLargeTitleStyle: { fontFamily: FontFamily.bold },
|
|
29
|
-
animation: reduceMotion ? "fade" : "default",
|
|
30
|
-
animationDuration: reduceMotion ? 150 : undefined,
|
|
31
32
|
}}
|
|
32
33
|
>
|
|
33
34
|
<Stack.Screen
|
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
2
|
import { router, Stack } from "expo-router";
|
|
3
|
+
import { Host, ScrollView, Button, Text, VStack, HStack, Spacer, Image } from "@expo/ui/swift-ui";
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
ScrollView,
|
|
6
|
-
Button,
|
|
7
|
-
Text,
|
|
8
|
-
VStack,
|
|
9
|
-
HStack,
|
|
10
|
-
Spacer,
|
|
11
|
-
Image,
|
|
12
|
-
ContentUnavailableView,
|
|
13
|
-
} from "@expo/ui/swift-ui";
|
|
14
|
-
import {
|
|
5
|
+
accessibilityHidden,
|
|
15
6
|
background,
|
|
16
7
|
buttonStyle,
|
|
17
8
|
clipShape,
|
|
@@ -24,10 +15,12 @@ import {
|
|
|
24
15
|
import type { SFSymbol } from "sf-symbols-typescript";
|
|
25
16
|
|
|
26
17
|
import { useDynamicFont } from "@/lib/dynamic-font";
|
|
18
|
+
import { useSymbolSize } from "@/lib/dynamic-symbol-size";
|
|
27
19
|
import { useColors } from "@/hooks/use-theme";
|
|
28
20
|
import { useDebounce } from "@/hooks/use-debounce";
|
|
29
21
|
import { useDebugEnabled } from "@/lib/preferences";
|
|
30
22
|
import { haptics } from "@/lib/haptics";
|
|
23
|
+
import { ContentUnavailable } from "@/components/ui/content-unavailable";
|
|
31
24
|
|
|
32
25
|
type Destination = {
|
|
33
26
|
title: string;
|
|
@@ -114,12 +107,13 @@ function score(d: Destination, query: string): number {
|
|
|
114
107
|
if (title.startsWith(q)) return 80;
|
|
115
108
|
if (title.includes(q)) return 60;
|
|
116
109
|
if (d.subtitle.toLowerCase().includes(q)) return 40;
|
|
117
|
-
if (d.keywords.includes(q)) return 20;
|
|
110
|
+
if (d.keywords.toLowerCase().includes(q)) return 20;
|
|
118
111
|
return 0;
|
|
119
112
|
}
|
|
120
113
|
|
|
121
114
|
export default function SearchScreen() {
|
|
122
115
|
const dfont = useDynamicFont();
|
|
116
|
+
const symbolSize = useSymbolSize();
|
|
123
117
|
const colors = useColors();
|
|
124
118
|
const [raw, setRaw] = useState("");
|
|
125
119
|
const query = useDebounce(raw, DEBOUNCE_MS);
|
|
@@ -133,11 +127,11 @@ export default function SearchScreen() {
|
|
|
133
127
|
const results = useMemo(() => {
|
|
134
128
|
if (query.trim().length === 0) return destinations;
|
|
135
129
|
const trimmed = query.trim();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
130
|
+
const scored = destinations.map((d) => ({ d, s: score(d, trimmed) })).filter(({ s }) => s > 0);
|
|
131
|
+
// `.toSorted` is ES2023 and not in Hermes V1 (default in SDK 56); `.sort`
|
|
132
|
+
// mutates in place, and `.filter` above already returned a fresh array.
|
|
133
|
+
scored.sort((a, b) => b.s - a.s);
|
|
134
|
+
return scored.map(({ d }) => d);
|
|
141
135
|
}, [query, destinations]);
|
|
142
136
|
|
|
143
137
|
const open = (href: Destination["href"]) => {
|
|
@@ -155,10 +149,10 @@ export default function SearchScreen() {
|
|
|
155
149
|
<>
|
|
156
150
|
<Stack.SearchBar
|
|
157
151
|
placement="automatic"
|
|
158
|
-
placeholder="Search
|
|
152
|
+
placeholder="Search screens"
|
|
159
153
|
onChangeText={(e) => setRaw(e.nativeEvent.text)}
|
|
160
154
|
/>
|
|
161
|
-
<Host style={{ flex: 1, backgroundColor: colors.background }}>
|
|
155
|
+
<Host testID="search-screen" style={{ flex: 1, backgroundColor: colors.background }}>
|
|
162
156
|
<ScrollView
|
|
163
157
|
modifiers={[scrollDismissesKeyboard("interactively"), tint(colors.primary as string)]}
|
|
164
158
|
>
|
|
@@ -168,7 +162,8 @@ export default function SearchScreen() {
|
|
|
168
162
|
modifiers={[padding({ horizontal: 24, top: 16, bottom: 40 })]}
|
|
169
163
|
>
|
|
170
164
|
{results.length === 0 ? (
|
|
171
|
-
<
|
|
165
|
+
<ContentUnavailable
|
|
166
|
+
testID="search-empty"
|
|
172
167
|
title="No results"
|
|
173
168
|
systemImage="magnifyingglass"
|
|
174
169
|
description={`Nothing matches "${query.trim()}"`}
|
|
@@ -181,9 +176,10 @@ export default function SearchScreen() {
|
|
|
181
176
|
{results.map((d) => (
|
|
182
177
|
<Button
|
|
183
178
|
key={d.href as string}
|
|
179
|
+
testID={`search-result-${d.title.toLowerCase().replace(/\s+/g, "-")}`}
|
|
184
180
|
modifiers={[
|
|
185
181
|
buttonStyle("plain"),
|
|
186
|
-
frame({ maxWidth:
|
|
182
|
+
frame({ maxWidth: Infinity }),
|
|
187
183
|
background(colors.muted as string),
|
|
188
184
|
clipShape("capsule"),
|
|
189
185
|
]}
|
|
@@ -193,11 +189,16 @@ export default function SearchScreen() {
|
|
|
193
189
|
spacing={14}
|
|
194
190
|
alignment="center"
|
|
195
191
|
modifiers={[
|
|
196
|
-
frame({ maxWidth:
|
|
192
|
+
frame({ maxWidth: Infinity }),
|
|
197
193
|
padding({ horizontal: 16, vertical: 12 }),
|
|
198
194
|
]}
|
|
199
195
|
>
|
|
200
|
-
<Image
|
|
196
|
+
<Image
|
|
197
|
+
systemName={d.icon}
|
|
198
|
+
size={symbolSize(20)}
|
|
199
|
+
color={colors.foreground as string}
|
|
200
|
+
modifiers={[accessibilityHidden(true)]}
|
|
201
|
+
/>
|
|
201
202
|
<VStack alignment="leading" spacing={2}>
|
|
202
203
|
<Text
|
|
203
204
|
modifiers={[
|
|
@@ -219,8 +220,9 @@ export default function SearchScreen() {
|
|
|
219
220
|
<Spacer />
|
|
220
221
|
<Image
|
|
221
222
|
systemName="chevron.right"
|
|
222
|
-
size={13}
|
|
223
|
+
size={symbolSize(13)}
|
|
223
224
|
color={colors.mutedForeground as string}
|
|
225
|
+
modifiers={[accessibilityHidden(true)]}
|
|
224
226
|
/>
|
|
225
227
|
</HStack>
|
|
226
228
|
</Button>
|
|
@@ -6,7 +6,7 @@ import { haptics } from "@/lib/haptics";
|
|
|
6
6
|
import { LoadingScreen } from "@/components/ui/loading-screen";
|
|
7
7
|
|
|
8
8
|
export function SuspenseFallback() {
|
|
9
|
-
return <LoadingScreen />;
|
|
9
|
+
return <LoadingScreen testID="tabs-loading" />;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export default function TabLayout() {
|
|
@@ -47,12 +47,11 @@ export default function TabLayout() {
|
|
|
47
47
|
>
|
|
48
48
|
<NativeTabs.Trigger.Icon sf={{ default: "house", selected: "house.fill" }} />
|
|
49
49
|
<NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
|
|
50
|
-
<NativeTabs.Trigger.Badge hidden />
|
|
51
50
|
</NativeTabs.Trigger>
|
|
52
51
|
|
|
53
52
|
<NativeTabs.Trigger
|
|
54
53
|
name="settings"
|
|
55
|
-
contentStyle={{ backgroundColor: colors.background
|
|
54
|
+
contentStyle={{ backgroundColor: colors.background }}
|
|
56
55
|
listeners={{
|
|
57
56
|
tabPress: () => haptics.light(),
|
|
58
57
|
}}
|
|
@@ -64,7 +63,7 @@ export default function TabLayout() {
|
|
|
64
63
|
<NativeTabs.Trigger
|
|
65
64
|
name="(search)"
|
|
66
65
|
role="search"
|
|
67
|
-
contentStyle={{ backgroundColor: colors.background
|
|
66
|
+
contentStyle={{ backgroundColor: colors.background }}
|
|
68
67
|
listeners={{
|
|
69
68
|
tabPress: () => haptics.light(),
|
|
70
69
|
}}
|