From 0f45b6b6080375d22ba4c32516c2f21f43f8e229 Mon Sep 17 00:00:00 2001 From: Real Codesiman Date: Fri, 28 Aug 2026 14:05:34 +0700 Subject: [PATCH 1/4] feat(deploy): add white-label brand system and EAS deployment pipeline Restructures assets under brands// so app.config.ts can resolve identity, icons, and EAS project per brand at build time, and wires up EAS Workflows (preview/update/release) plus deploy/white-label docs so the app can be built and shipped through EAS end-to-end. --- .eas/workflows/preview.yml | 22 ++ .eas/workflows/release.yml | 72 +++++++ .eas/workflows/update.yml | 27 +++ .env.example | 8 + .github/workflows/ci.yml | 6 + .gitignore | 8 +- CONTRIBUTING.md | 55 +++++ LICENSE | 21 ++ README.md | 16 +- app.config.test.ts | 111 ++++++++++ app.config.ts | 196 +++++++++++++++--- .../assets}/android-icon-background.png | Bin .../assets}/android-icon-foreground.png | Bin .../assets}/android-icon-monochrome.png | Bin .../_template/assets}/favicon.png | Bin .../_template/assets}/icon.png | Bin .../_template/assets}/splash-icon.png | Bin brands/_template/brand.json | 22 ++ .../assets/android-icon-background.png | Bin 0 -> 1820 bytes .../assets/android-icon-foreground.png | Bin 0 -> 11509 bytes .../assets/android-icon-monochrome.png | Bin 0 -> 8564 bytes brands/chatbotx/assets/favicon.png | Bin 0 -> 1234 bytes brands/chatbotx/assets/icon.png | Bin 0 -> 39867 bytes brands/chatbotx/assets/splash-icon.png | Bin 0 -> 5351 bytes brands/chatbotx/brand.json | 21 ++ docs/deploy.md | 129 ++++++++++++ docs/eas-updates.md | 16 +- docs/push-notifications.md | 29 ++- docs/white-label.md | 101 +++++++++ eas.json | 32 ++- jest.setup.ts | 13 +- package.json | 11 +- pnpm-lock.yaml | 160 +++++++++++--- pnpm-workspace.yaml | 1 + scripts/create-brand.ts | 44 ++++ src/api/auth-endpoints.ts | 11 +- src/api/auth-token.ts | 4 +- src/api/client.test.ts | 11 +- src/config/brand.ts | 54 +++++ src/features/auth/social-buttons.tsx | 4 +- src/features/settings/api/use-device-token.ts | 3 +- src/lib/pending-deep-link.ts | 4 +- src/theme/color-utils.ts | 20 ++ src/theme/tokens.ts | 15 +- 44 files changed, 1157 insertions(+), 90 deletions(-) create mode 100644 .eas/workflows/preview.yml create mode 100644 .eas/workflows/release.yml create mode 100644 .eas/workflows/update.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 app.config.test.ts rename {assets/images => brands/_template/assets}/android-icon-background.png (100%) rename {assets/images => brands/_template/assets}/android-icon-foreground.png (100%) rename {assets/images => brands/_template/assets}/android-icon-monochrome.png (100%) rename {assets/images => brands/_template/assets}/favicon.png (100%) rename {assets/images => brands/_template/assets}/icon.png (100%) rename {assets/images => brands/_template/assets}/splash-icon.png (100%) create mode 100644 brands/_template/brand.json create mode 100644 brands/chatbotx/assets/android-icon-background.png create mode 100644 brands/chatbotx/assets/android-icon-foreground.png create mode 100644 brands/chatbotx/assets/android-icon-monochrome.png create mode 100644 brands/chatbotx/assets/favicon.png create mode 100644 brands/chatbotx/assets/icon.png create mode 100644 brands/chatbotx/assets/splash-icon.png create mode 100644 brands/chatbotx/brand.json create mode 100644 docs/deploy.md create mode 100644 docs/white-label.md create mode 100644 scripts/create-brand.ts create mode 100644 src/config/brand.ts diff --git a/.eas/workflows/preview.yml b/.eas/workflows/preview.yml new file mode 100644 index 0000000..045d5f7 --- /dev/null +++ b/.eas/workflows/preview.yml @@ -0,0 +1,22 @@ +name: Preview build (internal, TEST distribution) + +# TEST distribution is EAS internal distribution, not a store — see docs/deploy.md. Triggers on +# `main` only; the repo's default branch should be `main` (see checklist in the deploy plan). +on: + push: + branches: ['main'] + +jobs: + build_android_preview: + name: Build Android (preview) + type: build + params: + platform: android + profile: preview + + build_ios_preview: + name: Build iOS (preview) + type: build + params: + platform: ios + profile: preview diff --git a/.eas/workflows/release.yml b/.eas/workflows/release.yml new file mode 100644 index 0000000..edd1648 --- /dev/null +++ b/.eas/workflows/release.yml @@ -0,0 +1,72 @@ +name: Release (production build + store submit) + +# Triggered by pushing a `vX.Y.Z` tag (ChatbotX's own release) or manually for a white-label +# customer build via `eas workflow:run .eas/workflows/release.yml -F brand=`. A +# non-default `brand` is expected to live in the private `chatbotx-mobile-brands` repo — see +# docs/white-label.md — cloned into `brands/` before the build so the customer's brand.json/assets +# are never committed to this (public) repo. +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + brand: + type: string + description: 'Brand id (brands//) to build and submit' + default: chatbotx + +jobs: + fetch_brand: + name: Fetch customer brand overlay + type: custom + if: ${{ inputs.brand != '' && inputs.brand != 'chatbotx' }} + steps: + - uses: eas/checkout + - name: Clone private brand overlay repo + run: | + rm -rf "brands/${{ inputs.brand }}" + git clone --depth 1 \ + "https://x-access-token:${BRANDS_REPO_TOKEN}@github.com/ChatbotXIO/chatbotx-mobile-brands.git" \ + /tmp/chatbotx-mobile-brands + cp -R "/tmp/chatbotx-mobile-brands/${{ inputs.brand }}" "brands/${{ inputs.brand }}" + + build_android_production: + name: Build Android (production) + type: build + # `after` (not `needs`) — `needs` requires the dependency to *succeed*, which would block this + # job whenever `fetch_brand` is skipped (the default chatbotx brand, nothing to clone). `after` + # only waits for it to complete, success or skip, so the `if` below is the only real gate. + after: [fetch_brand] + if: ${{ always() && (after.fetch_brand.status == 'success' || after.fetch_brand.status == 'skipped') }} + env: + BRAND: ${{ inputs.brand || 'chatbotx' }} + params: + platform: android + profile: production + + build_ios_production: + name: Build iOS (production) + type: build + after: [fetch_brand] + if: ${{ always() && (after.fetch_brand.status == 'success' || after.fetch_brand.status == 'skipped') }} + env: + BRAND: ${{ inputs.brand || 'chatbotx' }} + params: + platform: ios + profile: production + + submit_android_production: + name: Submit to Play Store + type: submit + needs: [build_android_production] + params: + build_id: ${{ needs.build_android_production.outputs.build_id }} + profile: production + + submit_ios_production: + name: Submit to App Store + type: submit + needs: [build_ios_production] + params: + build_id: ${{ needs.build_ios_production.outputs.build_id }} + profile: production diff --git a/.eas/workflows/update.yml b/.eas/workflows/update.yml new file mode 100644 index 0000000..5ea4bbc --- /dev/null +++ b/.eas/workflows/update.yml @@ -0,0 +1,27 @@ +name: OTA update + +# Automatic: every push to main publishes a JS-only OTA update to the `preview` (TEST) channel — +# the common case when no native config changed. Manual: publish to either channel on demand via +# `eas workflow:run .eas/workflows/update.yml -F channel=production`. See docs/deploy.md for the +# `runtimeVersion: appVersion` implication (a native change requires a new build, not just an OTA +# update, or the update won't be receivable by builds on a different `version`). +on: + push: + branches: ['main'] + workflow_dispatch: + inputs: + channel: + type: choice + description: 'Which channel to publish the OTA update to' + options: + - preview + - production + default: preview + +jobs: + publish_update: + name: Publish OTA update + type: update + environment: ${{ inputs.channel || 'preview' }} + params: + channel: ${{ inputs.channel || 'preview' }} diff --git a/.env.example b/.env.example index ff699c0..27bb1bf 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,11 @@ API_BASE_URL=http://localhost:3123 # PartyKit realtime server URL. Defaults to the local `partykit dev` port when unset. WS_URL=http://localhost:1999 + +# Selects brands//brand.json — app id, name, colors, icons, EAS project. `chatbotx` is the +# default brand tracked in this repo; see docs/white-label.md to add your own. +BRAND=chatbotx + +# development | preview | production — suffixes the bundle id/app name and picks the EAS Update +# channel so dev/test/prod builds can coexist on one device. See docs/deploy.md. +APP_ENV=development diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3a8157..682a50c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,3 +35,9 @@ jobs: - name: Expo config/dependency doctor run: npx expo-doctor + + - name: Verify brand mechanism (chatbotx) + run: BRAND=chatbotx pnpm prebuild:check + + - name: Verify brand mechanism (_template) + run: BRAND=_template pnpm prebuild:check diff --git a/.gitignore b/.gitignore index 641dc92..11480f5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,13 @@ yarn-error.* # local env files .env -.env*.local +.env.* +!.env.example + +# brand overlays (white-label customer brands live outside this repo — see docs/white-label.md) +brands/* +!brands/chatbotx/ +!brands/_template/ # typescript *.tsbuildinfo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1d5fd0c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Thanks for considering a contribution to ChatbotX's mobile app. + +## Getting set up + +```bash +pnpm install +cp .env.example .env +pnpm start +``` + +See the [README](README.md) for the full quick start and available scripts. + +## Before opening a PR + +```bash +pnpm typecheck +pnpm lint +pnpm format:check +pnpm test +npx expo-doctor +``` + +CI runs all of these on every push/PR — please run them locally first so review cycles aren't +spent on failures these would have caught. + +## Code style + +- TypeScript, strict mode. Avoid `any`; narrow `unknown` instead. +- Match existing patterns in the file/feature you're touching before introducing a new one. +- Keep PRs focused — unrelated refactors make review harder and should be a separate PR. + +## Commit messages + +Conventional commits: `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`, `perf:`, `ci:`. + +## Reporting bugs / requesting features + +Open a GitHub issue with: + +- What you expected vs. what happened +- Steps to reproduce (for bugs) +- Your environment (OS, Expo Go vs. dev build, physical device vs. simulator) + +## Security issues + +Please do not open a public issue for a security vulnerability. See the repository's security +policy (or contact the maintainers directly) instead. + +## Scope note + +Contributions to the core app (chat, contacts, auth, etc.) are welcome. White-label/brand-specific +work (see [docs/white-label.md](docs/white-label.md)) is maintained by the ChatbotX team for +customer builds and isn't something this repo's contribution flow covers. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6b42612 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 ChatbotX + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5192119..2cd8134 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ pnpm start ``` Then follow the Expo CLI prompts to open the app in a development build, Android emulator, iOS -simulator, or Expo Go. +simulator, or Expo Go. `.env`'s `BRAND=chatbotx` and `APP_ENV=development` defaults are enough to +run the app with zero EAS setup — see [Deploying](#deploying) once you need a real build. ## Development Commands @@ -48,8 +49,19 @@ pnpm test # jest pnpm format # prettier --write . pnpm format:check # prettier --check . pnpm generate:api # regenerate the typed API client from a running backend instance +pnpm brand:new # scaffold a new brand from brands/_template ``` +## Deploying + +TEST builds are EAS internal distribution; production builds go through EAS Build/Submit to the +App Store and Play Store. See [docs/deploy.md](docs/deploy.md) for the full flow. + +## White-label + +This app can be built for a different brand (app name, icons, colors, bundle id, EAS project) from +the same codebase — see [docs/white-label.md](docs/white-label.md). + ## Docs - [Project structure](docs/project-structure.md) @@ -57,3 +69,5 @@ pnpm generate:api # regenerate the typed API client from a running backend i - [Realtime event types](docs/realtime-events.md) - [Android push notifications](docs/push-notifications.md) - [EAS Update](docs/eas-updates.md) +- [Deploying (TEST and production)](docs/deploy.md) +- [White-label](docs/white-label.md) diff --git a/app.config.test.ts b/app.config.test.ts new file mode 100644 index 0000000..5c278df --- /dev/null +++ b/app.config.test.ts @@ -0,0 +1,111 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { loadBrand, readAppEnv, validateBrand } from './app.config'; + +const VALID_BRAND = { + id: 'acme', + displayName: 'Acme', + slug: 'acme-app', + scheme: 'acmeapp', + ios: { bundleIdentifier: 'com.acme.app' }, + android: { package: 'com.acme.app', adaptiveIconBackgroundColor: '#ffffff' }, + colors: { brand: '#3c6df0', splashBackground: '#ffffff' }, + eas: { projectId: 'abc-123' }, +}; + +describe('validateBrand', () => { + test('returns a normalized BrandConfig for a fully valid brand.json', () => { + const result = validateBrand(VALID_BRAND, 'acme'); + + expect(result).toEqual({ + id: 'acme', + displayName: 'Acme', + slug: 'acme-app', + scheme: 'acmeapp', + ios: { bundleIdentifier: 'com.acme.app' }, + android: { package: 'com.acme.app', adaptiveIconBackgroundColor: '#ffffff' }, + colors: { brand: '#3c6df0', splashBackground: '#ffffff' }, + eas: { projectId: 'abc-123', owner: undefined }, + }); + }); + + test('keeps a non-empty eas.owner', () => { + const result = validateBrand( + { ...VALID_BRAND, eas: { projectId: 'abc-123', owner: 'my-org' } }, + 'acme', + ); + expect(result.eas.owner).toBe('my-org'); + }); + + test('allows an empty eas.projectId (no EAS project configured yet)', () => { + const result = validateBrand({ ...VALID_BRAND, eas: { projectId: '' } }, 'acme'); + expect(result.eas.projectId).toBe(''); + }); + + test.each([ + ['id', { ...VALID_BRAND, id: '' }], + ['displayName', { ...VALID_BRAND, displayName: undefined }], + ['slug', { ...VALID_BRAND, slug: 123 }], + ['scheme', { ...VALID_BRAND, scheme: '' }], + ['ios.bundleIdentifier', { ...VALID_BRAND, ios: {} }], + ['android.package', { ...VALID_BRAND, android: { adaptiveIconBackgroundColor: '#fff' } }], + [ + 'android.adaptiveIconBackgroundColor', + { ...VALID_BRAND, android: { package: 'com.acme.app' } }, + ], + ['colors.brand', { ...VALID_BRAND, colors: { splashBackground: '#fff' } }], + ['colors.splashBackground', { ...VALID_BRAND, colors: { brand: '#fff' } }], + ['eas.projectId', { ...VALID_BRAND, eas: {} }], + ])('throws a message naming the missing field "%s"', (field, invalidBrand) => { + expect(() => validateBrand(invalidBrand, 'acme')).toThrow( + new RegExp(field.replace('.', '\\.')), + ); + }); + + test('throws for a non-object root', () => { + expect(() => validateBrand(null, 'acme')).toThrow(/brands\/acme\/brand\.json/); + }); +}); + +describe('loadBrand', () => { + let repoRoot: string; + + beforeEach(() => { + repoRoot = mkdtempSync(join(tmpdir(), 'load-brand-test-')); + }); + + afterEach(() => { + rmSync(repoRoot, { recursive: true, force: true }); + }); + + test('reads and validates brands//brand.json', () => { + const brandDir = join(repoRoot, 'brands', 'acme'); + mkdirSync(brandDir, { recursive: true }); + writeFileSync(join(brandDir, 'brand.json'), JSON.stringify(VALID_BRAND)); + + const result = loadBrand('acme', repoRoot); + expect(result.id).toBe('acme'); + }); + + test('throws a clear error when the brand folder does not exist', () => { + expect(() => loadBrand('missing-brand', repoRoot)).toThrow( + /no brands\/missing-brand\/brand\.json found/, + ); + }); +}); + +describe('readAppEnv', () => { + test('defaults to development when unset', () => { + expect(readAppEnv(undefined)).toBe('development'); + }); + + test.each(['development', 'preview', 'production'] as const)('accepts "%s"', (value) => { + expect(readAppEnv(value)).toBe(value); + }); + + test('throws for an invalid value', () => { + expect(() => readAppEnv('staging')).toThrow(/Invalid APP_ENV "staging"/); + }); +}); diff --git a/app.config.ts b/app.config.ts index 3593f1c..ff15625 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,4 +1,5 @@ -import { existsSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; import type { ExpoConfig } from 'expo/config'; @@ -7,29 +8,161 @@ import type { ExpoConfig } from 'expo/config'; * time and expose runtime-configurable values (like the API base URL) via `expo-constants`. * * Values consumed by the app at runtime must be read through `expo-constants` - * (see src/config/env.ts) — process.env is only available here, in the Expo config context. + * (see src/config/env.ts, src/config/brand.ts) — process.env is only available here, in the + * Expo config context. + * + * Brand-loading/validation logic lives inline here (not in a separate `config/` module) because + * Expo's config loader only Babel-transpiles this one entry file — a `require()` from inside it + * to another sibling `.ts` file fails with "Cannot find module" (no TS-stripping registered for + * transitive requires). See app.config.test.ts, which imports these exports directly under Jest + * (which *does* handle the full `.ts` require graph) to unit-test them. */ + +export interface BrandConfig { + id: string; + displayName: string; + slug: string; + scheme: string; + ios: { bundleIdentifier: string }; + android: { package: string; adaptiveIconBackgroundColor: string }; + colors: { brand: string; splashBackground: string }; + eas: { projectId: string; owner?: string }; +} + +/** `APP_ENV` selects the environment suffix applied to bundle id / display name / update channel + * — lets dev/preview/production builds coexist on one device and route to distinct EAS Update + * channels. See docs/deploy.md. */ +export type AppEnv = 'development' | 'preview' | 'production'; + +export const APP_ENV_CONFIG: Record< + AppEnv, + { idSuffix: string; nameSuffix: string; channel: string } +> = { + development: { idSuffix: '.dev', nameSuffix: ' (Dev)', channel: 'development' }, + preview: { idSuffix: '.preview', nameSuffix: ' (Test)', channel: 'preview' }, + production: { idSuffix: '', nameSuffix: '', channel: 'production' }, +}; + +export function readAppEnv(raw: string | undefined): AppEnv { + const value = raw ?? 'development'; + if (value === 'development' || value === 'preview' || value === 'production') { + return value; + } + throw new Error( + `Invalid APP_ENV "${value}" — expected "development", "preview", or "production".`, + ); +} + +/** Hand-rolled validation (matching the src/config/env.ts pattern elsewhere in the repo) rather + * than pulling in zod for one call site that only ever runs in the Node config-evaluation + * context, never bundled into the app. */ +export function validateBrand(raw: unknown, brandId: string): BrandConfig { + const fail = (field: string): never => { + throw new Error(`brands/${brandId}/brand.json is missing or has an invalid "${field}" field.`); + }; + + if (typeof raw !== 'object' || raw === null) fail('(root)'); + const b = raw as Record; + + if (typeof b.id !== 'string' || !b.id) fail('id'); + if (typeof b.displayName !== 'string' || !b.displayName) fail('displayName'); + if (typeof b.slug !== 'string' || !b.slug) fail('slug'); + if (typeof b.scheme !== 'string' || !b.scheme) fail('scheme'); + + const ios = b.ios as Record | undefined; + if (!ios || typeof ios.bundleIdentifier !== 'string' || !ios.bundleIdentifier) { + fail('ios.bundleIdentifier'); + } + + const android = b.android as Record | undefined; + if (!android || typeof android.package !== 'string' || !android.package) { + fail('android.package'); + } + if (typeof android?.adaptiveIconBackgroundColor !== 'string') { + fail('android.adaptiveIconBackgroundColor'); + } + + const colors = b.colors as Record | undefined; + if (!colors || typeof colors.brand !== 'string') fail('colors.brand'); + if (typeof colors?.splashBackground !== 'string') fail('colors.splashBackground'); + + const eas = b.eas as Record | undefined; + if (!eas || typeof eas.projectId !== 'string') fail('eas.projectId'); + + return { + id: b.id as string, + displayName: b.displayName as string, + slug: b.slug as string, + scheme: b.scheme as string, + ios: { bundleIdentifier: ios!.bundleIdentifier as string }, + android: { + package: android!.package as string, + adaptiveIconBackgroundColor: android!.adaptiveIconBackgroundColor as string, + }, + colors: { + brand: colors!.brand as string, + splashBackground: colors!.splashBackground as string, + }, + eas: { + projectId: eas!.projectId as string, + owner: typeof eas!.owner === 'string' && eas!.owner ? (eas!.owner as string) : undefined, + }, + }; +} + +/** Loads and validates `brands//brand.json` relative to the repo root. */ +export function loadBrand(brandId: string, repoRoot: string): BrandConfig { + const brandDir = join(repoRoot, 'brands', brandId); + const brandJsonPath = join(brandDir, 'brand.json'); + + if (!existsSync(brandJsonPath)) { + throw new Error( + `Unknown BRAND "${brandId}": no brands/${brandId}/brand.json found. ` + + `Run "pnpm brand:new ${brandId}" to create it, or set BRAND to an existing brand folder.`, + ); + } + + const raw: unknown = JSON.parse(readFileSync(brandJsonPath, 'utf8')); + return validateBrand(raw, brandId); +} + +const brandId = process.env.BRAND ?? 'chatbotx'; +const brand = loadBrand(brandId, __dirname); +const appEnv = readAppEnv(process.env.APP_ENV); +const envConfig = APP_ENV_CONFIG[appEnv]; +// Uses the BRAND folder name, not `brand.id` — the two commonly diverge for `_template` (folder +// `_template`, placeholder `id: "example"`) and must not be assumed identical. +const assetsDir = `./brands/${brandId}/assets`; + +// Loud stdout log: `BRAND` must be correct wherever eas-cli evaluates this file locally (e.g. to +// resolve `extra.eas.projectId` before a build upload) — a silently-wrong brand would build straight +// into the wrong customer's EAS project. See plan risk notes in docs/deploy.md. +// eslint-disable-next-line no-console -- deliberate build-time diagnostic, not app runtime code +console.log(`[app.config] brand=${brand.id} appEnv=${appEnv}`); + +const hasEasProject = brand.eas.projectId.length > 0; + const config: ExpoConfig = { - name: 'ChatbotX', - slug: 'chatbotx-mobile-app', + name: `${brand.displayName}${envConfig.nameSuffix}`, + slug: brand.slug, version: '1.0.0', orientation: 'portrait', - icon: './assets/images/icon.png', - scheme: 'chatbotxmobileapp', + icon: `${assetsDir}/icon.png`, + scheme: brand.scheme, userInterfaceStyle: 'automatic', // New Architecture is mandatory (no opt-out) as of this SDK — no `newArchEnabled` property // exists in the config schema to set. ios: { icon: './assets/expo.icon', - bundleIdentifier: 'com.chatconnectx.mobile', + bundleIdentifier: `${brand.ios.bundleIdentifier}${envConfig.idSuffix}`, }, android: { - package: 'com.chatconnectx.mobile', + package: `${brand.android.package}${envConfig.idSuffix}`, adaptiveIcon: { - backgroundColor: '#E6F4FE', - foregroundImage: './assets/images/android-icon-foreground.png', - backgroundImage: './assets/images/android-icon-background.png', - monochromeImage: './assets/images/android-icon-monochrome.png', + backgroundColor: brand.android.adaptiveIconBackgroundColor, + foregroundImage: `${assetsDir}/android-icon-foreground.png`, + backgroundImage: `${assetsDir}/android-icon-background.png`, + monochromeImage: `${assetsDir}/android-icon-monochrome.png`, }, predictiveBackGestureEnabled: false, // Edge-to-edge display is mandatory (no opt-out) as of this SDK — no `edgeToEdgeEnabled` knob @@ -37,8 +170,8 @@ const config: ExpoConfig = { // Required for Android FCM V1 push delivery — prerequisite: a Firebase project with this // package name registered, its google-services.json downloaded to the repo root (gitignored, // not committed). Conditional on the file actually existing locally: it's a per-developer/CI - // secret (see README for the EAS file-secret setup), and an unconditional reference here fails - // every clean checkout's Android build/prebuild before that secret is provisioned. + // secret (see docs/deploy.md for the EAS file-secret setup), and an unconditional reference here + // fails every clean checkout's Android build/prebuild before that secret is provisioned. googleServicesFile: existsSync('./google-services.json') ? './google-services.json' : undefined, }, plugins: [ @@ -47,8 +180,8 @@ const config: ExpoConfig = { [ 'expo-splash-screen', { - backgroundColor: '#208AEF', - image: './assets/images/splash-icon.png', + backgroundColor: brand.colors.splashBackground, + image: `${assetsDir}/splash-icon.png`, imageWidth: 76, }, ], @@ -83,7 +216,7 @@ const config: ExpoConfig = { // No dedicated notification icon asset exists yet (Android requires a flat white // silhouette, distinct from the app icon) — falls back to Expo's generated default until // one is designed. `color` matches the splash-screen brand color. - color: '#208AEF', + color: brand.colors.splashBackground, }, ], 'expo-updates', @@ -97,9 +230,15 @@ const config: ExpoConfig = { // same `version` above can receive the same update (no native code change assumed between // updates to the same version). runtimeVersion: { policy: 'appVersion' }, - updates: { - url: 'https://u.expo.dev/8cfd58a8-15f8-4f64-b16c-abf26166e80f', - }, + // A brand with no EAS project yet (e.g. a fresh open-source fork, or `_template`) omits + // `updates`/`extra.eas` entirely so `expo start`/Expo Go work with zero EAS setup. Run + // `eas init` (writes the resulting projectId into brands//brand.json, not app.json) to + // opt back in — see docs/deploy.md. + ...(hasEasProject + ? { + updates: { url: `https://u.expo.dev/${brand.eas.projectId}` }, + } + : {}), extra: { // Falls back to the backend's default local dev URL. Override via `API_BASE_URL` env var // (e.g. in `.env`, loaded by `expo start` through the standard EXPO_PUBLIC_ / dotenv support, @@ -111,11 +250,20 @@ const config: ExpoConfig = { // Mirrors the expo-localization plugin's supportsRTL above for Expo Go, which can't read the // native Info.plist/strings.xml values the plugin writes at build time. supportsRTL: true, - // Written by `eas init` — required by `getExpoPushTokenAsync` in src/lib/notifications.ts. - eas: { - projectId: '8cfd58a8-15f8-4f64-b16c-abf26166e80f', - }, + brandId: brand.id, + displayName: brand.displayName, + brandScheme: brand.scheme, + brandColor: brand.colors.brand, + appEnv, + updateChannel: envConfig.channel, + ...(hasEasProject + ? { + // Written by `eas init` — required by `getExpoPushTokenAsync` in src/lib/notifications.ts. + eas: { projectId: brand.eas.projectId }, + } + : {}), }, + ...(brand.eas.owner ? { owner: brand.eas.owner } : {}), }; export default config; diff --git a/assets/images/android-icon-background.png b/brands/_template/assets/android-icon-background.png similarity index 100% rename from assets/images/android-icon-background.png rename to brands/_template/assets/android-icon-background.png diff --git a/assets/images/android-icon-foreground.png b/brands/_template/assets/android-icon-foreground.png similarity index 100% rename from assets/images/android-icon-foreground.png rename to brands/_template/assets/android-icon-foreground.png diff --git a/assets/images/android-icon-monochrome.png b/brands/_template/assets/android-icon-monochrome.png similarity index 100% rename from assets/images/android-icon-monochrome.png rename to brands/_template/assets/android-icon-monochrome.png diff --git a/assets/images/favicon.png b/brands/_template/assets/favicon.png similarity index 100% rename from assets/images/favicon.png rename to brands/_template/assets/favicon.png diff --git a/assets/images/icon.png b/brands/_template/assets/icon.png similarity index 100% rename from assets/images/icon.png rename to brands/_template/assets/icon.png diff --git a/assets/images/splash-icon.png b/brands/_template/assets/splash-icon.png similarity index 100% rename from assets/images/splash-icon.png rename to brands/_template/assets/splash-icon.png diff --git a/brands/_template/brand.json b/brands/_template/brand.json new file mode 100644 index 0000000..70e45d8 --- /dev/null +++ b/brands/_template/brand.json @@ -0,0 +1,22 @@ +{ + "//": "Copy this folder to brands// to create a new brand. Fill in every field below, replace the assets in ./assets with your own (same filenames/sizes as brands/chatbotx/assets), then run `eas init` with BRAND= to fill in eas.projectId — see docs/white-label.md.", + "id": "example", + "displayName": "Example App", + "slug": "example-app", + "scheme": "exampleapp", + "ios": { + "bundleIdentifier": "com.example.app" + }, + "android": { + "package": "com.example.app", + "adaptiveIconBackgroundColor": "#ffffff" + }, + "colors": { + "brand": "#3c6df0", + "splashBackground": "#ffffff" + }, + "eas": { + "projectId": "", + "owner": "" + } +} diff --git a/brands/chatbotx/assets/android-icon-background.png b/brands/chatbotx/assets/android-icon-background.png new file mode 100644 index 0000000000000000000000000000000000000000..fb41a57e2858323ac57bad2c4353a8f2a3dfc0b6 GIT binary patch literal 1820 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 HnCt%!jZC}w literal 0 HcmV?d00001 diff --git a/brands/chatbotx/assets/android-icon-monochrome.png b/brands/chatbotx/assets/android-icon-monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..853559e4ddc332bee9393cb8f4d1f3a74905cbc3 GIT binary patch literal 8564 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 literal 0 HcmV?d00001 diff --git a/brands/chatbotx/assets/icon.png b/brands/chatbotx/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fcb778247b9c2fff716e11fc508048ec91ed0e89 GIT binary patch literal 39867 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 ZdCtAm5__lQNGF#7iyJoJI%DYb{{g00at8na literal 0 HcmV?d00001 diff --git a/brands/chatbotx/brand.json b/brands/chatbotx/brand.json new file mode 100644 index 0000000..43306fa --- /dev/null +++ b/brands/chatbotx/brand.json @@ -0,0 +1,21 @@ +{ + "id": "chatbotx", + "displayName": "ChatbotX", + "slug": "chatbotx-mobile-app", + "scheme": "chatbotxmobileapp", + "ios": { + "bundleIdentifier": "com.chatconnectx.mobile" + }, + "android": { + "package": "com.chatconnectx.mobile", + "adaptiveIconBackgroundColor": "#136EF1" + }, + "colors": { + "brand": "#136EF1", + "splashBackground": "#136EF1" + }, + "eas": { + "projectId": "8cfd58a8-15f8-4f64-b16c-abf26166e80f", + "owner": "" + } +} diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 0000000..3ad5d7f --- /dev/null +++ b/docs/deploy.md @@ -0,0 +1,129 @@ +# Deploying (TEST and production) + +This app builds and ships through [EAS](https://expo.dev/eas) (Expo Application Services) — EAS +Build for native binaries, EAS Update for OTA JS updates, EAS Submit for store submission. GitHub +Actions (`.github/workflows/ci.yml`) only runs lint/typecheck/test/`expo-doctor`; the actual +build/update/submit pipeline lives in [EAS Workflows](https://docs.expo.dev/eas/workflows/) +(`.eas/workflows/*.yml`), which run on Expo's own infrastructure, not GitHub's. + +See [white-label.md](white-label.md) if you're building this app for a different brand than the +default `chatbotx` one tracked in this repo. + +## Concepts + +- **`BRAND`** selects `brands//brand.json` — app id, name, colors, icons, and EAS project. + Defaults to `chatbotx`. See [white-label.md](white-label.md). +- **`APP_ENV`** (`development` | `preview` | `production`) suffixes the bundle id/app name and + picks the EAS Update channel, so dev/test/prod builds can be installed side-by-side on one + device. See the table in the root `app.config.ts`. +- **TEST = EAS internal distribution.** There is no separate "staging" app-store listing — the + `preview` build profile produces an internal-distribution build (installed via a link EAS + generates, not through TestFlight/Play internal testing) so QA can install it without a store + review cycle. + +## Prerequisites + +1. An [Expo account](https://expo.dev/signup) with access to the project's EAS organization. +2. `eas login` (or `EXPO_TOKEN` in CI — already wired into `.eas/workflows/*.yml` implicitly via + EAS's own auth, no GitHub secret needed for jobs that run natively on EAS). +3. An Apple Developer Program membership and a Google Play Console account, for anything that + submits to a store (`pnpm submit:prod` / the `release` EAS Workflow). + +### First-time EAS project setup (a fresh clone/fork) + +A brand's `eas.projectId` starts empty until you run `eas init` for it: + +```bash +BRAND=chatbotx eas init +``` + +This creates (or links) an EAS project and writes the resulting `projectId` into +`brands/chatbotx/brand.json` — **not** into `app.json`, since there is no `app.json` in this repo +(see `app.config.ts`). Until `eas.projectId` is set, `expo start`/Expo Go work fine with zero EAS +setup; `updates` and `extra.eas` are simply omitted from the resolved config. + +### EAS Environment Variables (per brand, per environment) + +`API_BASE_URL`, `WS_URL`, and the Android `google-services.json` file are **not** committed — +they're set as [EAS Environment Variables](https://docs.expo.dev/eas/environment-variables/), +scoped to the brand's EAS project and to `preview`/`production` separately: + +```bash +eas env:create --environment preview --name API_BASE_URL --value https://preview.example.com +eas env:create --environment preview --name WS_URL --value https://preview-realtime.example.com +eas env:create --environment production --name API_BASE_URL --value https://api.example.com +eas env:create --environment production --name WS_URL --value https://realtime.example.com + +# File-type secret — see docs/push-notifications.md for what this file is +eas env:create --environment preview --name GOOGLE_SERVICES_JSON --type file --value ./google-services.json +eas env:create --environment production --name GOOGLE_SERVICES_JSON --type file --value ./google-services.json +``` + +`eas.json`'s `build.preview`/`build.production` profiles reference these via `"environment": +"preview"` / `"environment": "production"` — the build worker pulls the actual values at build +time, so `preview.example.com`-style placeholders never need to touch `eas.json` itself. + +## Building + +```bash +pnpm build:dev # eas build --profile development — dev-client, internal +pnpm build:test # eas build --profile preview — internal distribution, this is "TEST" +pnpm build:prod # eas build --profile production — store-ready binary +``` + +Every build profile's `env.BRAND` defaults to `chatbotx` (`eas.json`'s `build.base`). Override it +for a customer brand: + +```bash +BRAND=acme pnpm build:test +``` + +`BRAND` must be correct **at the machine that evaluates `app.config.ts`**, which for local builds +is your own shell, and for EAS Workflow builds is the job's `env.BRAND` (see +[white-label.md](white-label.md)) — a wrong value here silently builds into the wrong brand's EAS +project. `app.config.ts` logs `[app.config] brand=... appEnv=...` on every evaluation specifically +so this is visible in build logs. + +## OTA updates + +```bash +pnpm update:test # eas update --channel preview --environment preview +pnpm update:prod # eas update --channel production --environment production +``` + +`runtimeVersion` uses the `appVersion` policy (see `app.config.ts`): an update is only offered to +installed builds that share the same `version`. If you add/upgrade a native module (anything that +changes generated `ios`/`android` code), bump `version` in `app.config.ts` and ship a new build — +an OTA update alone won't reach devices running the old native binary, and one published against a +mismatched `version` is silently never installed. Run `npx expo-doctor` (already part of CI) to +catch native/JS drift before it becomes a support ticket. + +## Submitting to the stores + +```bash +pnpm submit:prod # eas submit --profile production (ios + android) +``` + +`eas.json`'s `submit.production` has no per-brand identifiers (Apple ID / ASC app id / team id, +Play service account) checked in — either let `eas submit` prompt interactively the first time (it +offers to save credentials to your EAS project), or set them as environment variables +(`EXPO_APPLE_ID`, etc. — see `eas submit --help` for the full list) before running non-interactively +in CI. + +## Version bumping + +`eas.json`'s `"appVersionSource": "remote"` means EAS tracks the build number remotely — you don't +hand-maintain `ios.buildNumber`/`android.versionCode`. You do still bump the semantic `version` +field in `app.config.ts` yourself whenever the app should be treated as a new release train (see +the runtime-version note above). + +## Automated pipeline (EAS Workflows) + +| File | Trigger | What it does | +| ---------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `.eas/workflows/preview.yml` | push to `main` | Builds `preview` profile, both platforms — this is the TEST distribution QA installs. | +| `.eas/workflows/update.yml` | push to `main`; or manual with a `channel` input | Publishes a JS-only OTA update (defaults to the `preview` channel; pick `production` manually). | +| `.eas/workflows/release.yml` | push of a `v*` tag; or manual with a `brand` input | Builds + submits a `production` binary for both stores. A non-default `brand` pulls that customer's brand overlay from a private repo first — see [white-label.md](white-label.md). | + +CI (`.github/workflows/ci.yml`) triggers on `main` only. The repo's default branch must be `main` +for both CI and these workflows to fire on ordinary pushes. diff --git a/docs/eas-updates.md b/docs/eas-updates.md index d9ea242..10020a0 100644 --- a/docs/eas-updates.md +++ b/docs/eas-updates.md @@ -1,6 +1,16 @@ # EAS Update This app ships with `expo-updates` configured (`app.config.ts`'s `updates`/`runtimeVersion`), so -production/preview builds can receive OTA JS-bundle updates via `eas update` without an app-store -resubmission. `runtimeVersion` uses the `appVersion` policy — a build only receives updates -published against the same `version` in `app.config.ts`. +`preview`/`production` builds can receive OTA JS-bundle updates via `eas update` without an +app-store resubmission. `runtimeVersion` uses the `appVersion` policy — a build only receives +updates published against the same `version` in `app.config.ts`. + +Each `APP_ENV` maps to its own update channel (`development`/`preview`/`production` — see +`app.config.ts`'s `APP_ENV_CONFIG`), so a `preview` (TEST) build never receives a `production` +update and vice versa. See [deploy.md](deploy.md#ota-updates) for the actual publish commands +(`pnpm update:test` / `pnpm update:prod`) and the EAS Workflow that automates the common case +(`.eas/workflows/update.yml`). + +`updates.url`/`extra.eas.projectId` are per-brand (`brands//brand.json`) — a brand with no +EAS project yet has neither, and `expo-updates` is effectively inert until `eas init` is run for +that brand (see [white-label.md](white-label.md)). diff --git a/docs/push-notifications.md b/docs/push-notifications.md index 8dd8cac..6a0c5c5 100644 --- a/docs/push-notifications.md +++ b/docs/push-notifications.md @@ -1,12 +1,27 @@ -# Android push notifications (`google-services.json`) +# Push notifications (`google-services.json`) -Android FCM V1 push delivery requires a Firebase project with this app's package name -(`com.chatconnectx.mobile`) registered, and its `google-services.json` downloaded to the repo -root — the file is gitignored (per-developer/CI secret, never committed). `app.config.ts` only -references it conditionally (`existsSync` check), so a clean checkout without the file still -builds; Android push registration simply won't work until it's provisioned. +Android FCM V1 push delivery requires a Firebase project with this build's package name +registered, and its `google-services.json` downloaded to the repo root — the file is gitignored +(per-developer/CI secret, never committed). `app.config.ts` only references it conditionally +(`existsSync` check), so a clean checkout without the file still builds; Android push registration +simply won't work until it's provisioned. + +The package name is brand- and environment-specific: +`` (e.g. +`com.chatconnectx.mobile.preview` for the default brand's `preview`/TEST builds, +`com.chatconnectx.mobile` for `production` — see `app.config.ts`'s `APP_ENV_CONFIG`). **Each +`APP_ENV` suffix needs its own entry in the Firebase project** — a single `google-services.json` +can contain multiple package names, or you can register each as a separate Android app within the +same Firebase project and merge the resulting config. Skipping this means push silently fails to +register for whichever suffix wasn't added. For EAS builds, upload it as a [file-type secret](https://docs.expo.dev/eas/environment-variables/#file-environment-variables) via `eas env:create` (or the EAS dashboard) referencing the path `./google-services.json`, rather -than committing it. +than committing it — see [deploy.md](deploy.md#eas-environment-variables-per-brand-per-environment). +It's scoped per brand's EAS project, same as `API_BASE_URL`/`WS_URL`; a white-label customer needs +their own Firebase project registered with their own (brand-specific) package name — see +[white-label.md](white-label.md). + +iOS push doesn't need an equivalent file here: Expo push notifications route through APNs using +credentials EAS manages as part of the iOS build credentials, not a bundled config file. diff --git a/docs/white-label.md b/docs/white-label.md new file mode 100644 index 0000000..e9d703a --- /dev/null +++ b/docs/white-label.md @@ -0,0 +1,101 @@ +# White-label: building this app for a different brand + +The app is built once per customer ("brand") from the same codebase. A brand owns its own app +name, bundle id/package, colors, icons, and EAS project — nothing customer-specific is hardcoded +in application code. See [deploy.md](deploy.md) for the general build/update/submit flow; this doc +covers what's specific to standing up a _new_ brand. + +## Who does this + +The ChatbotX team builds and ships white-label apps for customers — customers don't get an EAS +account or build the app themselves. Each customer gets their own EAS project (in ChatbotX's +org), keeping their credentials, push certificates, and store listings isolated from every other +customer and from the open-source `chatbotx` brand. + +## 1. Scaffold the brand + +```bash +pnpm brand:new acme +``` + +This copies `brands/_template/` to `brands/acme/`, sets `id` to `acme` in the new +`brand.json`, and prints next steps. `brands/*` is gitignored except `brands/chatbotx/` and +`brands/_template/` — a new brand folder never gets committed to this (public) repo. + +## 2. Fill in `brands/acme/brand.json` + +```json +{ + "id": "acme", + "displayName": "Acme Support", + "slug": "acme-support", + "scheme": "acmesupport", + "ios": { "bundleIdentifier": "com.acme.support" }, + "android": { "package": "com.acme.support", "adaptiveIconBackgroundColor": "#0b1220" }, + "colors": { "brand": "#ff6a00", "splashBackground": "#0b1220" }, + "eas": { "projectId": "", "owner": "" } +} +``` + +- `scheme` becomes the app's custom URL scheme (`://auth-callback` for social sign-in, + `://conversations/:id` for push-notification deep links) — must be globally unique and + must be added to the backend's better-auth `trustedOrigins` for that customer's environment. +- `colors.brand` is the only color a brand needs to supply; `brandStrong` (pressed/emphasis state) + is derived automatically (see `src/theme/tokens.ts`). +- Leave `eas.projectId` empty for now — step 4 fills it in. + +## 3. Replace the assets + +Replace every file in `brands/acme/assets/` with the customer's own art, keeping the same +filenames: `icon.png`, `splash-icon.png`, `android-icon-foreground.png`, +`android-icon-background.png`, `android-icon-monochrome.png`, `favicon.png`. Match the source +files in `brands/chatbotx/assets/` for dimensions/format. + +## 4. Create the EAS project + +```bash +BRAND=acme eas init +``` + +This creates a new EAS project scoped to this brand and writes the resulting `projectId` into +`brands/acme/brand.json` (not into an `app.json` — this repo has none, see `app.config.ts`). + +## 5. Set environment variables and secrets + +Same as the default brand, scoped to `acme`'s EAS project — see +[deploy.md](deploy.md#eas-environment-variables-per-brand-per-environment). This customer's +`API_BASE_URL`/`WS_URL` (if the backend is multi-tenant, these may be identical to another +customer's; if not, they're per-customer here) and Android `google-services.json` (from a Firebase +project registered with `com.acme.support[.dev|.preview]` — see +[push-notifications.md](push-notifications.md)) all live here, never in this repo. + +## 6. Build and submit + +```bash +BRAND=acme pnpm build:test # internal TEST build for the customer/QA to try +BRAND=acme pnpm build:prod # store-ready build +BRAND=acme pnpm submit:prod # submit to that customer's App Store Connect / Play Console +``` + +Or trigger the `release` EAS Workflow manually with the `brand` input set to `acme` (see +[deploy.md](deploy.md#automated-pipeline-eas-workflows)) — this is the path used for a tagged +release when the brand isn't the default `chatbotx`. + +## Where customer brand folders actually live + +`brands/*` (other than `chatbotx` and `_template`) is gitignored in this repo on purpose — this +repo is open-source, and a customer's bundle id, EAS project id, and brand assets are not meant to +be public. The team keeps every customer's `brands//` folder in a separate private repository +(`chatbotx-mobile-brands`). The `release` EAS Workflow clones that private repo (using a +`BRANDS_REPO_TOKEN` EAS secret) into `brands//` at build time whenever `brand != chatbotx` — +see `.eas/workflows/release.yml`. Local development against a customer brand works the same way: +clone or copy that brand's folder from the private repo into `brands//` yourself before +running any `BRAND=` command. + +## Runtime (non-build-time) branding — not implemented here + +Everything above is _build-time_: baked into the native binary (icon, splash, bundle id, scheme, +EAS project). A further layer — colors/logo/display name fetched from the backend's tenant +settings at app boot and cached locally, falling back to `brand.json` — is a deliberate follow-up, +not part of this app. `brand.json` and `src/config/brand.ts` are structured so that layer can read +the same shape later without a rework. diff --git a/eas.json b/eas.json index c9b1494..a2fff78 100644 --- a/eas.json +++ b/eas.json @@ -1,36 +1,52 @@ { "cli": { - "version": ">= 13.0.0", + "version": ">= 22.6.0", "appVersionSource": "remote" }, "build": { + "base": { + "env": { + "BRAND": "chatbotx" + } + }, "development": { + "extends": "base", "developmentClient": true, "distribution": "internal", "channel": "development", + "environment": "development", "env": { - "API_BASE_URL": "http://localhost:3123", - "WS_URL": "http://localhost:1999" + "APP_ENV": "development" } }, "preview": { + "extends": "base", "distribution": "internal", "channel": "preview", + "environment": "preview", "env": { - "API_BASE_URL": "https://preview.example.com", - "WS_URL": "https://preview-realtime.example.com" + "APP_ENV": "preview" + }, + "android": { + "buildType": "apk" } }, "production": { + "extends": "base", "autoIncrement": true, "channel": "production", + "environment": "production", "env": { - "API_BASE_URL": "https://api.example.com", - "WS_URL": "https://realtime.example.com" + "APP_ENV": "production" } } }, "submit": { - "production": {} + "production": { + "ios": {}, + "android": { + "track": "internal" + } + } } } diff --git a/jest.setup.ts b/jest.setup.ts index fe72a1e..2126d7a 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -2,9 +2,10 @@ jest.mock('@react-native-async-storage/async-storage', () => require('@react-native-async-storage/async-storage/jest/async-storage-mock'), ); -// src/config/env.ts reads Expo config at MODULE LOAD and throws when `extra` is absent, so any -// test importing a module that transitively pulls in `env` (e.g. api/client.ts) fails to load -// without this. app.config.ts is not evaluated under Jest, so supply the `extra` block here. +// src/config/env.ts and src/config/brand.ts read Expo config at MODULE LOAD and throw when +// `extra` is absent, so any test importing a module that transitively pulls in either (e.g. +// api/client.ts, theme/tokens.ts) fails to load without this. app.config.ts is not evaluated +// under Jest, so supply the `extra` block here. jest.mock('expo-constants', () => ({ __esModule: true, default: { @@ -12,6 +13,12 @@ jest.mock('expo-constants', () => ({ extra: { apiBaseUrl: 'https://test.invalid', wsUrl: 'wss://test.invalid', + brandId: 'chatbotx', + displayName: 'ChatbotX', + brandScheme: 'chatbotxmobileapp', + brandColor: '#3c6df0', + appEnv: 'development', + updateChannel: 'development', }, }, }, diff --git a/package.json b/package.json index cd2e309..3865f7b 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "chatbotx-mobile-app", "main": "expo-router/entry", "version": "1.0.0", + "license": "MIT", "packageManager": "pnpm@11.15.1", "dependencies": { "@expo-google-fonts/plus-jakarta-sans": "^0.4.2", @@ -70,7 +71,15 @@ "format:check": "prettier --check .", "generate:api": "tsx scripts/generate-api-client.ts", "test": "jest", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "brand:new": "tsx scripts/create-brand.ts", + "prebuild:check": "expo prebuild --clean --no-install", + "build:dev": "eas build --profile development", + "build:test": "eas build --profile preview", + "build:prod": "eas build --profile production", + "update:test": "eas update --channel preview --environment preview", + "update:prod": "eas update --channel production --environment production", + "submit:prod": "eas submit --profile production" }, "private": true } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a89ff7..b558b39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,7 +76,7 @@ importers: version: 57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3) expo-router: specifier: ~57.0.17 - version: 57.0.17(d6f39c1ccc936eb2d412636bfc344e3d) + version: 57.0.17(2a385ddd55edfc21a7e07cfe0fd32f88) expo-secure-store: specifier: ^57.0.2 version: 57.0.2(expo@57.0.17) @@ -134,7 +134,7 @@ importers: devDependencies: '@testing-library/react-native': specifier: ^14.0.1 - version: 14.0.1(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(test-renderer@1.2.0(@types/react@19.2.18)(react@19.2.3)) + version: 14.0.1(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(test-renderer@1.2.0(@types/react@19.2.18)(react@19.2.3)) '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -152,10 +152,10 @@ importers: version: 57.0.2(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2) + version: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) jest-expo: specifier: ^57.0.5 - version: 57.0.5(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(expo@57.0.17)(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) + version: 57.0.5(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(expo@57.0.17)(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) openapi-typescript: specifier: ^7.13.0 version: 7.13.0(typescript@6.0.3) @@ -628,6 +628,10 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@egjs/hammerjs@2.0.17': resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} engines: {node: '>=0.8.0'} @@ -1187,6 +1191,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@napi-rs/wasm-runtime@1.2.3': resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} @@ -1569,6 +1576,18 @@ packages: resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} engines: {node: '>= 10'} + '@tsconfig/node10@1.0.13': + resolution: {integrity: sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -2283,6 +2302,9 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-fetch@3.2.0: resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} @@ -2410,6 +2432,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + dnssd-advertise@1.1.6: resolution: {integrity: sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==} @@ -3841,6 +3867,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} @@ -4923,6 +4952,20 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -5075,6 +5118,9 @@ packages: deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -5244,6 +5290,10 @@ packages: resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} engines: {node: '>=12'} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -5833,6 +5883,11 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + optional: true + '@egjs/hammerjs@2.0.17': dependencies: '@types/hammerjs': 2.0.46 @@ -6044,7 +6099,7 @@ snapshots: ws: 8.21.3 zod: 3.25.76 optionalDependencies: - expo-router: 57.0.17(d6f39c1ccc936eb2d412636bfc344e3d) + expo-router: 57.0.17(2a385ddd55edfc21a7e07cfe0fd32f88) react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) transitivePeerDependencies: - '@expo/dom-webview' @@ -6321,7 +6376,7 @@ snapshots: react: 19.2.3 optionalDependencies: '@expo/metro-runtime': 57.0.14(@expo/log-box@57.0.4)(expo@57.0.17)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) - expo-router: 57.0.17(d6f39c1ccc936eb2d412636bfc344e3d) + expo-router: 57.0.17(2a385ddd55edfc21a7e07cfe0fd32f88) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -6421,7 +6476,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(supports-color@10.2.2)': + '@jest/core@29.7.0(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0(supports-color@10.2.2) @@ -6435,7 +6490,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2) + jest-config: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -6610,6 +6665,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + optional: true + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -7021,7 +7082,7 @@ snapshots: '@tanstack/query-core': 5.101.4 react: 19.2.3 - '@testing-library/react-native@14.0.1(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(test-renderer@1.2.0(@types/react@19.2.18)(react@19.2.3))': + '@testing-library/react-native@14.0.1(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(test-renderer@1.2.0(@types/react@19.2.18)(react@19.2.3))': dependencies: jest-matcher-utils: 30.4.1 picocolors: 1.1.1 @@ -7031,10 +7092,22 @@ snapshots: redent: 3.0.0 test-renderer: 1.2.0(@types/react@19.2.18)(react@19.2.3) optionalDependencies: - jest: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2) + jest: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) '@tootallnate/once@2.0.1': {} + '@tsconfig/node10@1.0.13': + optional: true + + '@tsconfig/node12@1.0.11': + optional: true + + '@tsconfig/node14@1.0.3': + optional: true + + '@tsconfig/node16@1.0.4': + optional: true + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -7832,13 +7905,13 @@ snapshots: dependencies: browserslist: 4.28.8 - create-jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2): + create-jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2) + jest-config: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -7847,6 +7920,9 @@ snapshots: - supports-color - ts-node + create-require@1.1.1: + optional: true + cross-fetch@3.2.0: dependencies: node-fetch: 2.7.0 @@ -7959,6 +8035,9 @@ snapshots: diff-sequences@29.6.3: {} + diff@4.0.4: + optional: true + dnssd-advertise@1.1.6: {} doctrine@2.1.0: @@ -8561,7 +8640,7 @@ snapshots: - supports-color - typescript - expo-router@57.0.17(d6f39c1ccc936eb2d412636bfc344e3d): + expo-router@57.0.17(2a385ddd55edfc21a7e07cfe0fd32f88): dependencies: '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) '@expo/metro-runtime': 57.0.14(@expo/log-box@57.0.4)(expo@57.0.17)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) @@ -8597,7 +8676,7 @@ snapshots: standard-navigation: 0.0.5 vaul: 1.1.2(@types/react@19.2.18)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) optionalDependencies: - '@testing-library/react-native': 14.0.1(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(test-renderer@1.2.0(@types/react@19.2.18)(react@19.2.3)) + '@testing-library/react-native': 14.0.1(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(test-renderer@1.2.0(@types/react@19.2.18)(react@19.2.3)) react-dom: 19.2.3(react@19.2.3) react-native-gesture-handler: 2.32.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) react-native-reanimated: 4.5.1(react-native-worklets@0.10.1(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) @@ -9291,16 +9370,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@26.2.0)(supports-color@10.2.2): + jest-cli@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)): dependencies: - '@jest/core': 29.7.0(supports-color@10.2.2) + '@jest/core': 29.7.0(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2) + create-jest: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2) + jest-config: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.3 @@ -9310,7 +9389,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@26.2.0)(supports-color@10.2.2): + jest-config@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)): dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@jest/test-sequencer': 29.7.0 @@ -9336,6 +9415,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 26.2.0 + ts-node: 10.9.2(@types/node@26.2.0)(typescript@6.0.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -9390,7 +9470,7 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - jest-expo@57.0.5(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(expo@57.0.17)(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2): + jest-expo@57.0.5(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(expo@57.0.17)(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2): dependencies: '@jest/create-cache-key-function': 29.7.0 '@jest/globals': 29.7.0(supports-color@10.2.2) @@ -9399,7 +9479,7 @@ snapshots: jest-environment-jsdom: 29.7.0(supports-color@10.2.2) jest-snapshot: 29.7.0(supports-color@10.2.2) jest-watch-select-projects: 2.0.0 - jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)) + jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3))) json5: 2.2.3 lodash: 4.18.1 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) @@ -9599,11 +9679,11 @@ snapshots: chalk: 3.0.0 prompts: 2.4.2 - jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)): + jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3))): dependencies: ansi-escapes: 6.2.1 chalk: 4.1.2 - jest: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2) + jest: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) jest-regex-util: 29.6.3 jest-watcher: 29.7.0 slash: 5.1.0 @@ -9628,12 +9708,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2): + jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)): dependencies: - '@jest/core': 29.7.0(supports-color@10.2.2) + '@jest/core': 29.7.0(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2) + jest-cli: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -9824,6 +9904,9 @@ snapshots: dependencies: semver: 7.8.5 + make-error@1.3.6: + optional: true + makeerror@1.0.12: dependencies: tmpl: 1.0.5 @@ -11074,6 +11157,25 @@ snapshots: dependencies: typescript: 6.0.3 + ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.13 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 26.2.0 + acorn: 8.18.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 6.0.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -11240,6 +11342,9 @@ snapshots: uuid@7.0.3: {} + v8-compile-cache-lib@3.0.1: + optional: true + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -11403,6 +11508,9 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yn@3.1.1: + optional: true + yocto-queue@0.1.0: {} zod-validation-error@4.0.2(zod@3.25.76): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6436997..1663759 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ allowBuilds: + dtrace-provider: false esbuild: true unrs-resolver: true minimumReleaseAgeExclude: diff --git a/scripts/create-brand.ts b/scripts/create-brand.ts new file mode 100644 index 0000000..937698f --- /dev/null +++ b/scripts/create-brand.ts @@ -0,0 +1,44 @@ +import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +/** + * `pnpm brand:new ` — scaffolds `brands//` from `brands/_template/`, so a new white-label + * brand starts from the placeholder brand.json + stock assets instead of an empty folder. See + * docs/white-label.md for the full brand-onboarding flow. + */ +const repoRoot = join(__dirname, '..'); +const brandId = process.argv[2]; + +if (!brandId) { + console.error('Usage: pnpm brand:new '); + process.exit(1); +} + +if (!/^[a-z0-9-]+$/.test(brandId)) { + console.error(`Invalid brand id "${brandId}": use lowercase letters, numbers, and hyphens only.`); + process.exit(1); +} + +const templateDir = join(repoRoot, 'brands', '_template'); +const targetDir = join(repoRoot, 'brands', brandId); + +if (existsSync(targetDir)) { + console.error(`brands/${brandId} already exists.`); + process.exit(1); +} + +mkdirSync(targetDir, { recursive: true }); +cpSync(templateDir, targetDir, { recursive: true }); + +const brandJsonPath = join(targetDir, 'brand.json'); +const brandJson = JSON.parse(readFileSync(brandJsonPath, 'utf8')) as Record; +delete brandJson['//']; +brandJson.id = brandId; +writeFileSync(brandJsonPath, `${JSON.stringify(brandJson, null, 2)}\n`); + +console.log(`Created brands/${brandId} from brands/_template.`); +console.log( + `Next: edit brands/${brandId}/brand.json, replace brands/${brandId}/assets/*, then run`, +); +console.log(` BRAND=${brandId} eas init`); +console.log('to create the EAS project and fill in eas.projectId. See docs/white-label.md.'); diff --git a/src/api/auth-endpoints.ts b/src/api/auth-endpoints.ts index 1030fae..1b4a534 100644 --- a/src/api/auth-endpoints.ts +++ b/src/api/auth-endpoints.ts @@ -1,3 +1,4 @@ +import { brand } from '@/config/brand'; import { env } from '@/config/env'; import { setAuthToken } from './auth-token'; @@ -122,17 +123,17 @@ export async function signOut(token: string): Promise { export type SocialProvider = 'google' | 'facebook'; -/** The app's custom URL scheme (app.config.ts `scheme: 'chatbotxmobileapp'`) that the OAuth - * broker relays back to once the provider flow completes. */ -export const SOCIAL_CALLBACK_URL = 'chatbotxmobileapp://auth-callback'; +/** The app's custom URL scheme (`brands//brand.json` `scheme`, read via + * src/config/brand.ts) that the OAuth broker relays back to once the provider flow completes. */ +export const SOCIAL_CALLBACK_URL = `${brand.brandScheme}://auth-callback`; /** * Starts a social sign-in flow. Shape per better-auth's `/sign-in/social` route: request * `{ provider, callbackURL }`, response `{ redirect: boolean; url: string }`. * * The returned `url` sends the user through the provider, then the backend's fixed OAuth - * "broker" host relays back to `callbackURL`. `chatbotxmobileapp://` must be in the backend's - * better-auth `trustedOrigins`. + * "broker" host relays back to `callbackURL`. The brand's scheme (e.g. `chatbotxmobileapp://`) + * must be in the backend's better-auth `trustedOrigins` for every brand that can sign in. */ export async function startSocialSignIn(provider: SocialProvider): Promise { const response = await fetch(`${AUTH_BASE_URL}/sign-in/social`, { diff --git a/src/api/auth-token.ts b/src/api/auth-token.ts index 631fbe5..48660e7 100644 --- a/src/api/auth-token.ts +++ b/src/api/auth-token.ts @@ -1,5 +1,7 @@ import * as SecureStore from 'expo-secure-store'; +import { brand } from '@/config/brand'; + /** * Auth token storage, backed by expo-secure-store (Keychain on iOS, Keystore-backed * EncryptedSharedPreferences on Android). @@ -9,7 +11,7 @@ import * as SecureStore from 'expo-secure-store'; * `getAuthToken` currently always resolves to whatever was last persisted (or null), with no * refresh logic. */ -const AUTH_TOKEN_KEY = 'chatbotx.authToken'; +const AUTH_TOKEN_KEY = `${brand.brandId}.authToken`; export async function getAuthToken(): Promise { return SecureStore.getItemAsync(AUTH_TOKEN_KEY); diff --git a/src/api/client.test.ts b/src/api/client.test.ts index ffe859d..35d39a3 100644 --- a/src/api/client.test.ts +++ b/src/api/client.test.ts @@ -10,7 +10,16 @@ jest.mock('expo-constants', () => ({ __esModule: true, default: { expoConfig: { - extra: { apiBaseUrl: 'http://localhost:3123', wsUrl: 'ws://localhost:3123' }, + extra: { + apiBaseUrl: 'http://localhost:3123', + wsUrl: 'ws://localhost:3123', + brandId: 'chatbotx', + displayName: 'ChatbotX', + brandScheme: 'chatbotxmobileapp', + brandColor: '#3c6df0', + appEnv: 'development', + updateChannel: 'development', + }, }, }, })); diff --git a/src/config/brand.ts b/src/config/brand.ts new file mode 100644 index 0000000..ddb630d --- /dev/null +++ b/src/config/brand.ts @@ -0,0 +1,54 @@ +import Constants from 'expo-constants'; + +type AppEnv = 'development' | 'preview' | 'production'; + +interface BrandExtraConfig { + brandId: string; + displayName: string; + brandScheme: string; + brandColor: string; + appEnv: AppEnv; + updateChannel: string; +} + +function readExtra(): BrandExtraConfig { + const extra = Constants.expoConfig?.extra as Partial | undefined; + + if (!extra?.brandId) { + throw new Error( + 'Missing "extra.brandId" in Expo config. Check app.config.ts and the BRAND env var.', + ); + } + if (!extra?.displayName) { + throw new Error('Missing "extra.displayName" in Expo config. Check app.config.ts.'); + } + if (!extra?.brandScheme) { + throw new Error('Missing "extra.brandScheme" in Expo config. Check app.config.ts.'); + } + if (!extra?.brandColor) { + throw new Error('Missing "extra.brandColor" in Expo config. Check app.config.ts.'); + } + if (!extra?.appEnv) { + throw new Error( + 'Missing "extra.appEnv" in Expo config. Check app.config.ts and the APP_ENV env var.', + ); + } + if (!extra?.updateChannel) { + throw new Error('Missing "extra.updateChannel" in Expo config. Check app.config.ts.'); + } + + return { + brandId: extra.brandId, + displayName: extra.displayName, + brandScheme: extra.brandScheme, + brandColor: extra.brandColor, + appEnv: extra.appEnv, + updateChannel: extra.updateChannel, + }; +} + +/** + * Brand identity, sourced from `brands//brand.json` via app.config.ts `extra`. See + * docs/white-label.md for how a new brand is created and built. + */ +export const brand: BrandExtraConfig = readExtra(); diff --git a/src/features/auth/social-buttons.tsx b/src/features/auth/social-buttons.tsx index 157b4f6..332027d 100644 --- a/src/features/auth/social-buttons.tsx +++ b/src/features/auth/social-buttons.tsx @@ -16,7 +16,7 @@ type Provider = 'google' | 'facebook'; /** * Social sign-in buttons. Flow: `startSocialSignIn` gets the provider authorize URL from * better-auth, `WebBrowser.openAuthSessionAsync` opens it and resolves once the broker redirects - * back to our `chatbotxmobileapp://auth-callback` scheme (see auth-endpoints.ts for the + * back to our brand's `://auth-callback` (see auth-endpoints.ts for the * trustedOrigins gap that currently blocks this redirect server-side — this UI is correct but * won't complete end-to-end until that backend change lands). * @@ -29,7 +29,7 @@ type Provider = 'google' | 'facebook'; * `getSession()` call below only works if a token is ALREADY in SecureStore, which it isn't yet at * this point in the flow. So this currently cannot complete sign-in even once trustedOrigins is * fixed, UNLESS the backend's broker relay is changed to also forward a bearer token (e.g. as a - * `token` query param on the `chatbotxmobileapp://auth-callback` redirect, mirroring what + * `token` query param on the `://auth-callback` redirect, mirroring what * `set-auth-token` does for same-origin requests) — or a dedicated mobile token-exchange endpoint * is added. Neither is implemented here; both are backend-shaped decisions outside this repo. */ diff --git a/src/features/settings/api/use-device-token.ts b/src/features/settings/api/use-device-token.ts index 4959203..b784a0a 100644 --- a/src/features/settings/api/use-device-token.ts +++ b/src/features/settings/api/use-device-token.ts @@ -6,6 +6,7 @@ import { Platform } from 'react-native'; import { apiClient } from '@/api/client'; import { ApiError } from '@/api/errors'; +import { brand } from '@/config/brand'; import { getPushTokenAsync } from '@/lib/notifications'; /** @@ -15,7 +16,7 @@ import { getPushTokenAsync } from '@/lib/notifications'; * re-registration after a token rotation) always targets the token the backend actually has on * file, not whatever `getExpoPushTokenAsync` returns *now* if it has since changed. */ -const LAST_REGISTERED_TOKEN_KEY = 'chatbotx.lastRegisteredPushToken'; +const LAST_REGISTERED_TOKEN_KEY = `${brand.brandId}.lastRegisteredPushToken`; function resolvePlatform(): 'ios' | 'android' | null { if (Platform.OS === 'ios') return 'ios'; diff --git a/src/lib/pending-deep-link.ts b/src/lib/pending-deep-link.ts index b5f9ab2..8b2b1ec 100644 --- a/src/lib/pending-deep-link.ts +++ b/src/lib/pending-deep-link.ts @@ -1,7 +1,7 @@ import * as Linking from 'expo-linking'; /** - * Cold-start deep link to a conversation or contact (`chatbotxmobileapp://conversations/:id` or + * Cold-start deep link to a conversation or contact (`://conversations/:id` or * `.../contacts/:id`, e.g. from a push notification once delivery lands) arrives before the auth * bootstrap gate resolves. `(app)/_layout.tsx` redirects an unauthenticated visitor to sign-in, * which drops the original target — expo-router has no built-in "return to this deep link after @@ -19,7 +19,7 @@ let captured = false; /** Deep-link path shapes this app knows how to resolve to a route, in order of priority. Each * pattern is matched against the URL's path with the scheme/host stripped (what `Linking.parse` * gives us) — accepting both a bare `conversations/:id` / `contacts/:id` link (the natural shape - * for a push-notification deep link, e.g. `chatbotxmobileapp://conversations/12345`) and one that + * for a push-notification deep link, e.g. `://conversations/12345`) and one that * already embeds the `(app)` route-group prefix (e.g. from a universal link that mirrors the * app's own internal route paths verbatim). */ const DEEP_LINK_PATTERNS: { regex: RegExp; buildPath: (id: string) => string }[] = [ diff --git a/src/theme/color-utils.ts b/src/theme/color-utils.ts index e7ba956..20db979 100644 --- a/src/theme/color-utils.ts +++ b/src/theme/color-utils.ts @@ -35,3 +35,23 @@ export const alphaTokens = { scrim: 0.4, scrimDark: 0.6, } as const; + +/** Darkens a 6-digit `#rrggbb` hex color toward black by `amount` (0–1). Used to derive a + * `brandStrong` (pressed/emphasis) shade from a single brand color instead of hand-picking a + * second hex per brand — see src/theme/tokens.ts. */ +export function darken(hex: string, amount: number): string { + const match = HEX_PATTERN.exec(hex); + if (!match) { + throw new Error(`darken: expected a 6-digit hex color like "#3c6df0", got "${hex}"`); + } + + const intValue = parseInt(match[1], 16); + const clampedAmount = Math.min(1, Math.max(0, amount)); + const scale = (channel: number): number => Math.round(channel * (1 - clampedAmount)); + + const r = scale((intValue >> 16) & 255); + const g = scale((intValue >> 8) & 255); + const b = scale(intValue & 255); + + return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`; +} diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts index d8f2d57..21c4c15 100644 --- a/src/theme/tokens.ts +++ b/src/theme/tokens.ts @@ -8,7 +8,14 @@ * meaning (channel identity, unread state, bot state, semantic status) rather than decoration. */ -import { withAlpha } from './color-utils'; +import { brand } from '@/config/brand'; + +import { darken, withAlpha } from './color-utils'; + +/** Pressed/emphasis shade of the brand color, derived rather than hand-picked so each brand only + * needs to supply one color in brand.json. Darkened ~20% — matches the original ChatbotX + * `#3c6df0` → `#2851c9` relationship closely enough to keep existing screens' contrast intact. */ +const brandStrong = darken(brand.brandColor, 0.2); // --------------------------------------------------------------------------- // Colors @@ -28,12 +35,12 @@ const lightBase = { // (measured ~4.51:1). textTertiary: '#767063', textInverse: '#ffffff', - brand: '#3c6df0', - brandStrong: '#2851c9', + brand: brand.brandColor, + brandStrong, onBrand: '#ffffff', bubbleIn: '#ffffff', bubbleInText: '#1c1b17', - bubbleOut: '#3c6df0', + bubbleOut: brand.brandColor, bubbleOutText: '#ffffff', bubbleBot: '#f1ecff', bubbleBotText: '#2f1f66', From d47255b611637efe2143473888447e3e257ee3f1 Mon Sep 17 00:00:00 2001 From: Real Codesiman Date: Sat, 29 Aug 2026 10:54:28 +0700 Subject: [PATCH 2/4] build(deploy): add TestFlight submit profile and bump Expo SDK patches - add preview-testflight build/submit profile with auto-submit to ASC - disable export-compliance prompt via ITSAppUsesNonExemptEncryption - bump expo, expo-constants, expo-font, expo-updates to latest patches - exclude .codegraph daemon socket from EAS build uploads --- .easignore | 2 + app.config.ts | 3 + eas.json | 21 ++- package.json | 9 +- pnpm-lock.yaml | 360 ++++++++++++++++++++++---------------------- pnpm-workspace.yaml | 14 +- 6 files changed, 218 insertions(+), 191 deletions(-) create mode 100644 .easignore diff --git a/.easignore b/.easignore new file mode 100644 index 0000000..a4c8280 --- /dev/null +++ b/.easignore @@ -0,0 +1,2 @@ +# CodeGraph local index (contains a live daemon socket file that cannot be archived) +.codegraph diff --git a/app.config.ts b/app.config.ts index ff15625..ac53f2a 100644 --- a/app.config.ts +++ b/app.config.ts @@ -155,6 +155,9 @@ const config: ExpoConfig = { ios: { icon: './assets/expo.icon', bundleIdentifier: `${brand.ios.bundleIdentifier}${envConfig.idSuffix}`, + // Only OS-provided TLS/Keychain + random UUIDs — no custom crypto, so export-compliance exempt. + // Skips the App Store Connect "App Encryption Documentation" prompt on every upload. + infoPlist: { ITSAppUsesNonExemptEncryption: false }, }, android: { package: `${brand.android.package}${envConfig.idSuffix}`, diff --git a/eas.json b/eas.json index a2fff78..fede0f8 100644 --- a/eas.json +++ b/eas.json @@ -31,6 +31,11 @@ "buildType": "apk" } }, + "preview-testflight": { + "extends": "preview", + "distribution": "store", + "autoIncrement": true + }, "production": { "extends": "base", "autoIncrement": true, @@ -42,8 +47,22 @@ } }, "submit": { + "preview-testflight": { + "ios": { + "ascApiKeyPath": "./AuthKey_238L44SV57.p8", + "ascApiKeyId": "238L44SV57", + "ascApiKeyIssuerId": "a888294a-adc3-4201-a022-67fbbc4a597d", + "appleTeamId": "5H3UF98Q3H", + "ascAppId": "6806470556" + } + }, "production": { - "ios": {}, + "ios": { + "ascApiKeyPath": "./AuthKey_238L44SV57.p8", + "ascApiKeyId": "238L44SV57", + "ascApiKeyIssuerId": "a888294a-adc3-4201-a022-67fbbc4a597d", + "appleTeamId": "5H3UF98Q3H" + }, "android": { "track": "internal" } diff --git a/package.json b/package.json index 3865f7b..f880b50 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,15 @@ "@shopify/flash-list": "2.0.2", "@tanstack/react-query": "^5.101.4", "dayjs": "^1.11.23", - "expo": "~57.0.17", + "expo": "~57.0.18", "expo-camera": "~57.0.4", - "expo-constants": "~57.0.15", + "expo-constants": "~57.0.16", "expo-crypto": "~57.0.2", "expo-dev-client": "~57.0.16", "expo-device": "~57.0.1", "expo-document-picker": "~57.0.1", "expo-file-system": "~57.0.6", - "expo-font": "~57.0.1", + "expo-font": "~57.0.2", "expo-haptics": "~57.0.2", "expo-image": "~57.0.3", "expo-image-picker": "~57.0.14", @@ -32,7 +32,7 @@ "expo-splash-screen": "~57.0.8", "expo-status-bar": "~57.0.1", "expo-system-ui": "~57.0.3", - "expo-updates": "~57.0.18", + "expo-updates": "~57.0.19", "expo-web-browser": "~57.0.2", "i18next": "^26.3.6", "openapi-fetch": "^0.17.0", @@ -76,6 +76,7 @@ "prebuild:check": "expo prebuild --clean --no-install", "build:dev": "eas build --profile development", "build:test": "eas build --profile preview", + "build:testflight": "eas build --profile preview-testflight -p ios --auto-submit", "build:prod": "eas build --profile production", "update:test": "eas update --channel preview --environment preview", "update:prod": "eas update --channel production --environment production", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b558b39..19c25c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 0.4.2 '@expo/vector-icons': specifier: ^15.1.1 - version: 15.1.1(expo-font@57.0.1)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + version: 15.1.1(expo-font@57.0.2)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) '@gorhom/bottom-sheet': specifier: ^5.2.14 version: 5.2.14(c57327f6cff0368afd765304bb73382e) @@ -30,71 +30,71 @@ importers: specifier: ^1.11.23 version: 1.11.23 expo: - specifier: ~57.0.17 - version: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + specifier: ~57.0.18 + version: 57.0.18(4c67990895f3488afb1e1e80811ccb84) expo-camera: specifier: ~57.0.4 - version: 57.0.4(@types/emscripten@1.41.5)(expo@57.0.17)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + version: 57.0.4(@types/emscripten@1.41.5)(expo@57.0.18)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) expo-constants: - specifier: ~57.0.15 - version: 57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) + specifier: ~57.0.16 + version: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) expo-crypto: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.17) + version: 57.0.2(expo@57.0.18) expo-dev-client: specifier: ~57.0.16 - version: 57.0.16(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) + version: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) expo-device: specifier: ~57.0.1 - version: 57.0.1(expo@57.0.17) + version: 57.0.1(expo@57.0.18) expo-document-picker: specifier: ~57.0.1 - version: 57.0.1(expo@57.0.17) + version: 57.0.1(expo@57.0.18) expo-file-system: specifier: ~57.0.6 - version: 57.0.6(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) + version: 57.0.6(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) expo-font: - specifier: ~57.0.1 - version: 57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + specifier: ~57.0.2 + version: 57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) expo-haptics: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.17) + version: 57.0.2(expo@57.0.18) expo-image: specifier: ~57.0.3 - version: 57.0.3(expo@57.0.17)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + version: 57.0.3(expo@57.0.18)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) expo-image-picker: specifier: ~57.0.14 - version: 57.0.14(expo@57.0.17) + version: 57.0.14(expo@57.0.18) expo-linking: specifier: ~57.0.8 - version: 57.0.8(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) + version: 57.0.8(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) expo-localization: specifier: ^57.0.1 - version: 57.0.1(expo@57.0.17)(react@19.2.3) + version: 57.0.1(expo@57.0.18)(react@19.2.3) expo-notifications: specifier: ~57.0.15 - version: 57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3) + version: 57.0.15(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3) expo-router: specifier: ~57.0.17 - version: 57.0.17(2a385ddd55edfc21a7e07cfe0fd32f88) + version: 57.0.17(71af9823cfd45ac2f686cfca64abe2a5) expo-secure-store: specifier: ^57.0.2 - version: 57.0.2(expo@57.0.17) + version: 57.0.2(expo@57.0.18) expo-splash-screen: specifier: ~57.0.8 - version: 57.0.8(expo@57.0.17)(supports-color@10.2.2)(typescript@6.0.3) + version: 57.0.8(expo@57.0.18)(supports-color@10.2.2)(typescript@6.0.3) expo-status-bar: specifier: ~57.0.1 - version: 57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + version: 57.0.1(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) expo-system-ui: specifier: ~57.0.3 - version: 57.0.3(expo@57.0.17)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) + version: 57.0.3(expo@57.0.18)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) expo-updates: - specifier: ~57.0.18 - version: 57.0.18(expo-dev-client@57.0.16(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)))(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) + specifier: ~57.0.19 + version: 57.0.19(expo-dev-client@57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)))(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) expo-web-browser: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) + version: 57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) i18next: specifier: ^26.3.6 version: 26.3.6(typescript@6.0.3) @@ -155,7 +155,7 @@ importers: version: 29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)) jest-expo: specifier: ^57.0.5 - version: 57.0.5(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(expo@57.0.17)(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) + version: 57.0.5(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(expo@57.0.18)(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) openapi-typescript: specifier: ^7.13.0 version: 7.13.0(typescript@6.0.3) @@ -845,8 +845,8 @@ packages: '@expo-google-fonts/plus-jakarta-sans@0.4.2': resolution: {integrity: sha512-6LYVmVGwjQvH+uzzWlVc9+oMj4lkNQ41aymVDjO+x8aFk8kCye20wOyLomYMZaMezA++Uf1mZRCw3W3Fy/hxEA==} - '@expo/cli@57.0.19': - resolution: {integrity: sha512-HcvVmm0FNL1P3Lf23z1L/sLZtdcWfKW8P5L/doQbICWVG/PL7uI5+G2Ttxnj9QGNmpYgJ89E7+LJefS5jJMUlQ==} + '@expo/cli@57.0.20': + resolution: {integrity: sha512-ZjWz7SA5TBTyKq4+aFRUPgMFxmqHtKkDMv6d85J2UAAjdRhkNRf+B80t+rr5IFo2ywuTvyrPRBth4ei4w08olA==} hasBin: true peerDependencies: expo: '*' @@ -891,15 +891,15 @@ packages: react: '*' react-native: '*' - '@expo/env@2.4.2': - resolution: {integrity: sha512-28pqaEqwnmLduZ00Pq9HkSzE5wbj1MTwp5/n8nm8rD8MCjR9eUnVOwmNksPI3Be2ReAPO/DbPn1puy0mvoocsQ==} + '@expo/env@2.4.3': + resolution: {integrity: sha512-M1NXeZCA1mkMkYOyIe7PlyRX0/jqFtMoJgyblnlq/vpCRfmueFT7RnGSQG8uEFDF5WHOFGijAQ3fogPh3/n5Ng==} engines: {node: '>=20.12.0'} '@expo/expo-modules-macros-plugin@0.6.1': resolution: {integrity: sha512-cpsLZE4rqkc1Y3eZTkxB98jrqY1YXgetmtxFt8q89jBRmk3quRuk1BZo+VcnCSObZardjg99r1k5xijEMONFGA==} - '@expo/fingerprint@0.20.10': - resolution: {integrity: sha512-Ox/smKlpPpyTUwZAXS927xuJ8qICwNxHOtZElv0anp6xJ80FzwkfvCdC/NjCxlALS2Hp3IsmwyKd975gYgW12Q==} + '@expo/fingerprint@0.20.11': + resolution: {integrity: sha512-GC43EcjQwzpzNZISqmhvjKdi+6FTfxnC9ZxOeg6osCkYXJz94j107l0QpPLipg62khD5HAA5G5cUhevffbRC4w==} hasBin: true '@expo/image-utils@0.11.5': @@ -922,8 +922,8 @@ packages: react: '*' react-native: '*' - '@expo/metro-config@57.0.11': - resolution: {integrity: sha512-aIKcCAQALafeIf+HOoHgjj7hOb771VQRFZARDjm+GJRT74Lb3bYHjkATtS4Wv2zqVx/aFYkNqYNqPF/Py3kSlw==} + '@expo/metro-config@57.0.12': + resolution: {integrity: sha512-S62Lrq35HZqBFD55423pmWb8PjaiR/W02zQC1uECBmw1vTN8WZaFz4TJ0i21EeJzwfebMb9MLxL8JZ102Z6VbA==} peerDependencies: expo: '*' peerDependenciesMeta: @@ -2720,8 +2720,8 @@ packages: react-native-web: optional: true - expo-constants@57.0.15: - resolution: {integrity: sha512-rLME1H/K+/uFMR/jEpe/p1nribvCEZZmQegzqxu/R1HTyvYweq4E+B38zB0baie+SEwmeIPLaM4KLVMnejhm5A==} + expo-constants@57.0.16: + resolution: {integrity: sha512-HG3yJjGZo2BPTo2F1sBoxdJZRwX7d9C554EhKn8m12K62pxGolCsvPSoh/lQf95OEP9vO34wwXB2C3QFs0KG0g==} peerDependencies: expo: '*' react-native: '*' @@ -2772,8 +2772,8 @@ packages: expo: '*' react-native: '*' - expo-font@57.0.1: - resolution: {integrity: sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==} + expo-font@57.0.2: + resolution: {integrity: sha512-eWR0CAdysgVqg8VewJ8b5wKh1pa6C8YShIEU3SP0+hlSqOk4djUwuJjn3P04KHVzL3hws9FrUuPxCNmd4TGung==} peerDependencies: expo: '*' react: '*' @@ -2943,8 +2943,8 @@ packages: peerDependencies: expo: '*' - expo-updates@57.0.18: - resolution: {integrity: sha512-guTp5U6RbMOivi3GWXsflZA6M/4oTZKj1/M69sEzVW9MII/FQt+lPgbbEHwcIGrpXBr4RuJIYVrh492kkptJvQ==} + expo-updates@57.0.19: + resolution: {integrity: sha512-xX4KIUa8H2xKmiVDe/uEehD96MV6nyyF/OCrE+PBYfOEp6BbS8bYkHZfNFcoDCKMRuT6pg9JrCi8OmnoWlM/2A==} hasBin: true peerDependencies: expo: '*' @@ -2961,8 +2961,8 @@ packages: expo: '*' react-native: '*' - expo@57.0.17: - resolution: {integrity: sha512-RG9BItgEObzSafkJKHPUh5iCnaPCFXJTDwfEJe3t7uhmqipVhcsf6gAJEtME0xaGaMp4Z3C+HLbPW9DMea8G0w==} + expo@57.0.18: + resolution: {integrity: sha512-6nax9hJPhf9dWrstliXUABTJXDNIJrfJKcCtjSxuYVs2Yf127GIhKf3wsEYSFUl+LFdRbPPTNaweJePH159wZA==} hasBin: true peerDependencies: '@expo/dom-webview': '*' @@ -6036,26 +6036,26 @@ snapshots: '@expo-google-fonts/plus-jakarta-sans@0.4.2': {} - '@expo/cli@57.0.19(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.14)(expo-constants@57.0.15)(expo-font@57.0.1)(expo-router@57.0.17)(expo@57.0.17)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3)': + '@expo/cli@57.0.20(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.14)(expo-constants@57.0.16)(expo-font@57.0.2)(expo-router@57.0.17)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/config': 57.0.9(supports-color@10.2.2)(typescript@6.0.3) '@expo/config-plugins': 57.0.9(supports-color@10.2.2)(typescript@6.0.3) '@expo/devcert': 1.2.1(supports-color@10.2.2) - '@expo/env': 2.4.2(supports-color@10.2.2) + '@expo/env': 2.4.3(supports-color@10.2.2) '@expo/image-utils': 0.11.5(supports-color@10.2.2)(typescript@6.0.3) '@expo/inline-modules': 0.1.7(supports-color@10.2.2)(typescript@6.0.3) '@expo/json-file': 11.0.1 - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) '@expo/metro': 56.0.2(supports-color@10.2.2) - '@expo/metro-config': 57.0.11(expo@57.0.17)(supports-color@10.2.2)(typescript@6.0.3) + '@expo/metro-config': 57.0.12(expo@57.0.18)(supports-color@10.2.2)(typescript@6.0.3) '@expo/metro-file-map': 57.0.2(supports-color@10.2.2) '@expo/osascript': 2.7.1 '@expo/package-manager': 1.13.1 '@expo/plist': 0.8.1 '@expo/prebuild-config': 57.0.15(supports-color@10.2.2)(typescript@6.0.3) '@expo/require-utils': 57.0.5(supports-color@10.2.2)(typescript@6.0.3) - '@expo/router-server': 57.0.8(@expo/metro-runtime@57.0.14)(expo-constants@57.0.15)(expo-font@57.0.1)(expo-router@57.0.17)(expo-server@57.0.3)(expo@57.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(supports-color@10.2.2) + '@expo/router-server': 57.0.8(@expo/metro-runtime@57.0.14)(expo-constants@57.0.16)(expo-font@57.0.2)(expo-router@57.0.17)(expo-server@57.0.3)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(supports-color@10.2.2) '@expo/schema-utils': 57.0.2 '@expo/spawn-async': 1.8.0 '@expo/ws-tunnel': 2.0.0(ws@8.21.3) @@ -6072,7 +6072,7 @@ snapshots: connect: 3.7.0(supports-color@10.2.2) debug: 4.4.3(supports-color@10.2.2) dnssd-advertise: 1.1.6 - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) expo-server: 57.0.3 fetch-nodeshim: 0.4.10 getenv: 2.0.0 @@ -6099,7 +6099,7 @@ snapshots: ws: 8.21.3 zod: 3.25.76 optionalDependencies: - expo-router: 57.0.17(2a385ddd55edfc21a7e07cfe0fd32f88) + expo-router: 57.0.17(71af9823cfd45ac2f686cfca64abe2a5) react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) transitivePeerDependencies: - '@expo/dom-webview' @@ -6169,13 +6169,13 @@ snapshots: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) - '@expo/dom-webview@57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)': + '@expo/dom-webview@57.0.1(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)': dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) - '@expo/env@2.4.2(supports-color@10.2.2)': + '@expo/env@2.4.3(supports-color@10.2.2)': dependencies: chalk: 4.1.2 debug: 4.4.3(supports-color@10.2.2) @@ -6185,9 +6185,9 @@ snapshots: '@expo/expo-modules-macros-plugin@0.6.1': {} - '@expo/fingerprint@0.20.10(supports-color@10.2.2)': + '@expo/fingerprint@0.20.11(supports-color@10.2.2)': dependencies: - '@expo/env': 2.4.2(supports-color@10.2.2) + '@expo/env': 2.4.3(supports-color@10.2.2) '@expo/spawn-async': 1.8.0 arg: 5.0.2 chalk: 4.1.2 @@ -6234,22 +6234,22 @@ snapshots: - supports-color - typescript - '@expo/log-box@57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)': + '@expo/log-box@57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)': dependencies: - '@expo/dom-webview': 57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + '@expo/dom-webview': 57.0.1(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) anser: 1.4.10 - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) stacktrace-parser: 0.1.11 - '@expo/metro-config@57.0.11(expo@57.0.17)(supports-color@10.2.2)(typescript@6.0.3)': + '@expo/metro-config@57.0.12(expo@57.0.18)(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@babel/code-frame': 7.29.7 '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/generator': 7.29.8 '@expo/config': 57.0.9(supports-color@10.2.2)(typescript@6.0.3) - '@expo/env': 2.4.2(supports-color@10.2.2) + '@expo/env': 2.4.3(supports-color@10.2.2) '@expo/json-file': 11.0.1 '@expo/metro': 56.0.2(supports-color@10.2.2) '@expo/require-utils': 57.0.5(supports-color@10.2.2)(typescript@6.0.3) @@ -6269,7 +6269,7 @@ snapshots: postcss: 8.5.26 resolve-from: 5.0.0 optionalDependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) transitivePeerDependencies: - bufferutil - supports-color @@ -6287,11 +6287,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/metro-runtime@57.0.14(@expo/log-box@57.0.4)(expo@57.0.17)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)': + '@expo/metro-runtime@57.0.14(@expo/log-box@57.0.4)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)': dependencies: - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) anser: 1.4.10 - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) pretty-format: 29.7.0 react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) @@ -6366,17 +6366,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/router-server@57.0.8(@expo/metro-runtime@57.0.14)(expo-constants@57.0.15)(expo-font@57.0.1)(expo-router@57.0.17)(expo-server@57.0.3)(expo@57.0.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(supports-color@10.2.2)': + '@expo/router-server@57.0.8(@expo/metro-runtime@57.0.14)(expo-constants@57.0.16)(expo-font@57.0.2)(expo-router@57.0.17)(expo-server@57.0.3)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(supports-color@10.2.2)': dependencies: debug: 4.4.3(supports-color@10.2.2) - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) - expo-constants: 57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) - expo-font: 57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) + expo-constants: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) + expo-font: 57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) expo-server: 57.0.3 react: 19.2.3 optionalDependencies: - '@expo/metro-runtime': 57.0.14(@expo/log-box@57.0.4)(expo@57.0.17)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) - expo-router: 57.0.17(2a385ddd55edfc21a7e07cfe0fd32f88) + '@expo/metro-runtime': 57.0.14(@expo/log-box@57.0.4)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + expo-router: 57.0.17(71af9823cfd45ac2f686cfca64abe2a5) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -6391,9 +6391,9 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/ui@57.0.14(a28db61b81538200eb1a9563fb519989)': + '@expo/ui@57.0.14(7c721135c4e6c4c85c69925e87446f69)': dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) sf-symbols-typescript: 2.2.0 @@ -6406,9 +6406,9 @@ snapshots: - '@types/react' - '@types/react-dom' - '@expo/vector-icons@15.1.1(expo-font@57.0.1)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)': + '@expo/vector-icons@15.1.1(expo-font@57.0.2)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)': dependencies: - expo-font: 57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + expo-font: 57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) @@ -7625,7 +7625,7 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7(supports-color@10.2.2)) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7(supports-color@10.2.2)) - babel-preset-expo@57.0.9(@babel/core@7.29.7(supports-color@10.2.2))(@babel/runtime@7.29.7)(expo@57.0.17)(react-refresh@0.14.2)(supports-color@10.2.2): + babel-preset-expo@57.0.9(@babel/core@7.29.7(supports-color@10.2.2))(@babel/runtime@7.29.7)(expo@57.0.18)(react-refresh@0.14.2)(supports-color@10.2.2): dependencies: '@babel/generator': 7.29.8 '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2) @@ -7672,7 +7672,7 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) transitivePeerDependencies: - '@babel/core' - supports-color @@ -8456,25 +8456,25 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - expo-application@57.0.2(expo@57.0.17): + expo-application@57.0.2(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) - expo-asset@57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3): + expo-asset@57.0.15(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.11.5(supports-color@10.2.2)(typescript@6.0.3) - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) - expo-constants: 57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) + expo-constants: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) transitivePeerDependencies: - supports-color - typescript - expo-camera@57.0.4(@types/emscripten@1.41.5)(expo@57.0.17)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): + expo-camera@57.0.4(@types/emscripten@1.41.5)(expo@57.0.18)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): dependencies: barcode-detector: 3.2.2(@types/emscripten@1.41.5) - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) optionalDependencies: @@ -8482,92 +8482,92 @@ snapshots: transitivePeerDependencies: - '@types/emscripten' - expo-constants@57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2): + expo-constants@57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: - '@expo/env': 2.4.2(supports-color@10.2.2) - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + '@expo/env': 2.4.3(supports-color@10.2.2) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) transitivePeerDependencies: - supports-color - expo-crypto@57.0.2(expo@57.0.17): + expo-crypto@57.0.2(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) - expo-dev-client@57.0.16(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)): + expo-dev-client@57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) - expo-dev-launcher: 57.0.16(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) - expo-dev-menu: 57.0.16(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) - expo-dev-menu-interface: 57.0.0(expo@57.0.17) - expo-manifests: 57.0.1(expo@57.0.17) - expo-updates-interface: 57.0.1(expo@57.0.17) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) + expo-dev-launcher: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) + expo-dev-menu: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) + expo-dev-menu-interface: 57.0.0(expo@57.0.18) + expo-manifests: 57.0.1(expo@57.0.18) + expo-updates-interface: 57.0.1(expo@57.0.18) transitivePeerDependencies: - react-native - expo-dev-launcher@57.0.16(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)): + expo-dev-launcher@57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)): dependencies: '@expo/schema-utils': 57.0.2 - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) - expo-dev-menu: 57.0.16(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) - expo-manifests: 57.0.1(expo@57.0.17) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) + expo-dev-menu: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) + expo-manifests: 57.0.1(expo@57.0.18) react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) - expo-dev-menu-interface@57.0.0(expo@57.0.17): + expo-dev-menu-interface@57.0.0(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) - expo-dev-menu@57.0.16(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)): + expo-dev-menu@57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) - expo-dev-menu-interface: 57.0.0(expo@57.0.17) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) + expo-dev-menu-interface: 57.0.0(expo@57.0.18) react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) - expo-device@57.0.1(expo@57.0.17): + expo-device@57.0.1(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) ua-parser-js: 0.7.41 - expo-document-picker@57.0.1(expo@57.0.17): + expo-document-picker@57.0.1(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) expo-eas-client@57.0.2: {} - expo-file-system@57.0.6(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)): + expo-file-system@57.0.6(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) - expo-font@57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): + expo-font@57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) fontfaceobserver: 2.3.0 react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) - expo-glass-effect@57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): + expo-glass-effect@57.0.1(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) - expo-haptics@57.0.2(expo@57.0.17): + expo-haptics@57.0.2(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) - expo-image-loader@57.0.1(expo@57.0.17): + expo-image-loader@57.0.1(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) - expo-image-picker@57.0.14(expo@57.0.17): + expo-image-picker@57.0.14(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) - expo-image-loader: 57.0.1(expo@57.0.17) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) + expo-image-loader: 57.0.1(expo@57.0.18) - expo-image@57.0.3(expo@57.0.17)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): + expo-image@57.0.3(expo@57.0.18)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) sf-symbols-typescript: 2.2.0 @@ -8576,14 +8576,14 @@ snapshots: expo-json-utils@57.0.1: {} - expo-keep-awake@57.0.1(expo@57.0.17)(react@19.2.3): + expo-keep-awake@57.0.1(expo@57.0.18)(react@19.2.3): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react: 19.2.3 - expo-linking@57.0.8(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2): + expo-linking@57.0.8(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2): dependencies: - expo-constants: 57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) + expo-constants: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) invariant: 2.2.4 react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) @@ -8591,15 +8591,15 @@ snapshots: - expo - supports-color - expo-localization@57.0.1(expo@57.0.17)(react@19.2.3): + expo-localization@57.0.1(expo@57.0.18)(react@19.2.3): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react: 19.2.3 rtl-detect: 1.1.2 - expo-manifests@57.0.1(expo@57.0.17): + expo-manifests@57.0.1(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) expo-json-utils: 57.0.1 expo-modules-autolinking@57.0.12(supports-color@10.2.2)(typescript@6.0.3): @@ -8626,26 +8626,26 @@ snapshots: dependencies: react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) - expo-notifications@57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3): + expo-notifications@57.0.15(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.11.5(supports-color@10.2.2)(typescript@6.0.3) abort-controller: 3.0.0 badgin: 1.2.3 - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) - expo-application: 57.0.2(expo@57.0.17) - expo-constants: 57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) + expo-application: 57.0.2(expo@57.0.18) + expo-constants: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) transitivePeerDependencies: - supports-color - typescript - expo-router@57.0.17(2a385ddd55edfc21a7e07cfe0fd32f88): + expo-router@57.0.17(71af9823cfd45ac2f686cfca64abe2a5): dependencies: - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) - '@expo/metro-runtime': 57.0.14(@expo/log-box@57.0.4)(expo@57.0.17)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + '@expo/metro-runtime': 57.0.14(@expo/log-box@57.0.4)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) '@expo/schema-utils': 57.0.2 - '@expo/ui': 57.0.14(a28db61b81538200eb1a9563fb519989) + '@expo/ui': 57.0.14(7c721135c4e6c4c85c69925e87446f69) '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.3) '@radix-ui/react-tabs': 1.1.21(@types/react@19.2.18)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-native-masked-view/masked-view': 0.3.2(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) @@ -8653,12 +8653,12 @@ snapshots: color: 4.2.3 debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) - expo-constants: 57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) - expo-glass-effect: 57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) - expo-linking: 57.0.8(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) + expo-constants: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) + expo-glass-effect: 57.0.1(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + expo-linking: 57.0.8(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2) expo-server: 57.0.3 - expo-symbols: 57.0.2(expo-font@57.0.1)(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + expo-symbols: 57.0.2(expo-font@57.0.2)(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) fast-deep-equal: 3.1.3 invariant: 2.2.4 nanoid: 3.3.18 @@ -8689,55 +8689,55 @@ snapshots: - react-native-worklets - supports-color - expo-secure-store@57.0.2(expo@57.0.17): + expo-secure-store@57.0.2(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) expo-server@57.0.3: {} - expo-splash-screen@57.0.8(expo@57.0.17)(supports-color@10.2.2)(typescript@6.0.3): + expo-splash-screen@57.0.8(expo@57.0.18)(supports-color@10.2.2)(typescript@6.0.3): dependencies: '@expo/config-plugins': 57.0.9(supports-color@10.2.2)(typescript@6.0.3) '@expo/image-utils': 0.11.5(supports-color@10.2.2)(typescript@6.0.3) - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) xml2js: 0.6.0 transitivePeerDependencies: - supports-color - typescript - expo-status-bar@57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): + expo-status-bar@57.0.1(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) expo-structured-headers@57.0.0: {} - expo-symbols@57.0.2(expo-font@57.0.1)(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): + expo-symbols@57.0.2(expo-font@57.0.2)(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3): dependencies: '@expo-google-fonts/material-symbols': 0.4.44 - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) - expo-font: 57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) + expo-font: 57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) sf-symbols-typescript: 2.2.0 - expo-system-ui@57.0.3(expo@57.0.17)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2): + expo-system-ui@57.0.3(expo@57.0.18)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: '@react-native/normalize-colors': 0.86.3 debug: 4.4.3(supports-color@10.2.2) - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) optionalDependencies: react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) transitivePeerDependencies: - supports-color - expo-updates-interface@57.0.1(expo@57.0.17): + expo-updates-interface@57.0.1(expo@57.0.18): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) - expo-updates@57.0.18(expo-dev-client@57.0.16(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)))(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2): + expo-updates@57.0.19(expo-dev-client@57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)))(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2): dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/plist': 0.8.1 @@ -8745,11 +8745,11 @@ snapshots: arg: 4.1.3 chalk: 4.1.2 debug: 4.4.3(supports-color@10.2.2) - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) expo-eas-client: 57.0.2 - expo-manifests: 57.0.1(expo@57.0.17) + expo-manifests: 57.0.1(expo@57.0.18) expo-structured-headers: 57.0.0 - expo-updates-interface: 57.0.1(expo@57.0.17) + expo-updates-interface: 57.0.1(expo@57.0.18) getenv: 2.0.0 glob: 13.0.6 ignore: 5.3.2 @@ -8758,34 +8758,34 @@ snapshots: react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) resolve-from: 5.0.0 optionalDependencies: - expo-dev-client: 57.0.16(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) + expo-dev-client: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) transitivePeerDependencies: - supports-color - expo-web-browser@57.0.2(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)): + expo-web-browser@57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)): dependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) react-native: 0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2) - expo@57.0.17(4c67990895f3488afb1e1e80811ccb84): + expo@57.0.18(4c67990895f3488afb1e1e80811ccb84): dependencies: '@babel/runtime': 7.29.7 - '@expo/cli': 57.0.19(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.14)(expo-constants@57.0.15)(expo-font@57.0.1)(expo-router@57.0.17)(expo@57.0.17)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3) + '@expo/cli': 57.0.20(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.14)(expo-constants@57.0.16)(expo-font@57.0.2)(expo-router@57.0.17)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3) '@expo/config': 57.0.9(supports-color@10.2.2)(typescript@6.0.3) '@expo/config-plugins': 57.0.9(supports-color@10.2.2)(typescript@6.0.3) '@expo/devtools': 57.0.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) - '@expo/fingerprint': 0.20.10(supports-color@10.2.2) + '@expo/fingerprint': 0.20.11(supports-color@10.2.2) '@expo/local-build-cache-provider': 57.0.8(supports-color@10.2.2)(typescript@6.0.3) - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) '@expo/metro': 56.0.2(supports-color@10.2.2) - '@expo/metro-config': 57.0.11(expo@57.0.17)(supports-color@10.2.2)(typescript@6.0.3) + '@expo/metro-config': 57.0.12(expo@57.0.18)(supports-color@10.2.2)(typescript@6.0.3) '@ungap/structured-clone': 1.3.3 - babel-preset-expo: 57.0.9(@babel/core@7.29.7(supports-color@10.2.2))(@babel/runtime@7.29.7)(expo@57.0.17)(react-refresh@0.14.2)(supports-color@10.2.2) - expo-asset: 57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3) - expo-constants: 57.0.15(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) - expo-file-system: 57.0.6(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) - expo-font: 57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) - expo-keep-awake: 57.0.1(expo@57.0.17)(react@19.2.3) + babel-preset-expo: 57.0.9(@babel/core@7.29.7(supports-color@10.2.2))(@babel/runtime@7.29.7)(expo@57.0.18)(react-refresh@0.14.2)(supports-color@10.2.2) + expo-asset: 57.0.15(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2)(typescript@6.0.3) + expo-constants: 57.0.16(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(supports-color@10.2.2) + expo-file-system: 57.0.6(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2)) + expo-font: 57.0.2(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + expo-keep-awake: 57.0.1(expo@57.0.18)(react@19.2.3) expo-modules-autolinking: 57.0.12(supports-color@10.2.2)(typescript@6.0.3) expo-modules-core: 57.0.14(react-native-worklets@0.10.1(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) pretty-format: 29.7.0 @@ -8794,8 +8794,8 @@ snapshots: react-refresh: 0.14.2 whatwg-url-minimum: 0.1.2 optionalDependencies: - '@expo/dom-webview': 57.0.1(expo@57.0.17)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) - '@expo/metro-runtime': 57.0.14(@expo/log-box@57.0.4)(expo@57.0.17)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + '@expo/dom-webview': 57.0.1(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) + '@expo/metro-runtime': 57.0.14(@expo/log-box@57.0.4)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3) react-dom: 19.2.3(react@19.2.3) react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) transitivePeerDependencies: @@ -9470,7 +9470,7 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - jest-expo@57.0.5(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(expo@57.0.17)(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2): + jest-expo@57.0.5(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(expo@57.0.18)(jest@29.7.0(@types/node@26.2.0)(supports-color@10.2.2)(ts-node@10.9.2(@types/node@26.2.0)(typescript@6.0.3)))(react-native@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(@react-native/jest-preset@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2))(@react-native/metro-config@0.86.3(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2))(@types/react@19.2.18)(react@19.2.3)(supports-color@10.2.2))(react@19.2.3)(supports-color@10.2.2): dependencies: '@jest/create-cache-key-function': 29.7.0 '@jest/globals': 29.7.0(supports-color@10.2.2) @@ -9487,7 +9487,7 @@ snapshots: server-only: 0.0.1 stacktrace-js: 2.0.2 optionalDependencies: - expo: 57.0.17(4c67990895f3488afb1e1e80811ccb84) + expo: 57.0.18(4c67990895f3488afb1e1e80811ccb84) transitivePeerDependencies: - '@babel/core' - bufferutil diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1663759..e6a069a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,27 +3,29 @@ allowBuilds: esbuild: true unrs-resolver: true minimumReleaseAgeExclude: - - '@expo/cli@57.0.17' - - '@expo/fingerprint@0.20.9' + - '@expo/cli@57.0.17 || 57.0.20' + - '@expo/fingerprint@0.20.9 || 0.20.11' - '@expo/local-build-cache-provider@57.0.7' - - '@expo/metro-config@57.0.9' + - '@expo/metro-config@57.0.9 || 57.0.12' - '@expo/metro-runtime@57.0.12' - '@expo/prebuild-config@57.0.13' - '@expo/router-server@57.0.7' - '@expo/ui@57.0.12' - expo-asset@57.0.13 - - expo-constants@57.0.13 + - expo-constants@57.0.13 || 57.0.16 - expo-file-system@57.0.5 || 57.0.6 - expo-linking@57.0.7 - expo-modules-core@57.0.12 - expo-modules-jsi@57.0.5 - expo-router@57.0.15 - - expo@57.0.15 + - expo@57.0.15 || 57.0.18 - expo-camera@57.0.4 - expo-image-picker@57.0.12 - - expo-updates@57.0.16 + - expo-updates@57.0.16 || 57.0.19 - expo-dev-client@57.0.14 - expo-dev-launcher@57.0.14 - expo-dev-menu@57.0.14 - expo-notifications@57.0.13 - expo-haptics@57.0.2 + - '@expo/env@2.4.3' + - expo-font@57.0.2 From 2ccd6d7b15619d3dc484c0fead10921ae60546e6 Mon Sep 17 00:00:00 2001 From: Real Codesiman Date: Sat, 29 Aug 2026 14:34:12 +0700 Subject: [PATCH 3/4] refactor(brand): harden brand config validation and derive dark-mode brand colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validate brand.json fields with a shared requireString helper (hex-color checks, optional eas.owner) instead of ad-hoc per-field checks - Trim BrandExtraConfig to what src/ actually consumes (brandId, brandScheme, brandColor) — displayName/appEnv/updateChannel come from expo-constants and eas.json instead - Add lighten() alongside darken() and derive dark-scheme brand/brandStrong/ bubbleOut shades from the single brand.json color instead of hardcoding them - Harden the release workflow's brand-overlay clone step (fail fast on a missing token/brand.json, clean up /tmp) and restructure eas.json submit profiles to share config via "base" + extends - Make create-brand.ts clean up a half-scaffolded folder on failure --- .eas/workflows/release.yml | 6 ++- app.config.test.ts | 27 ++++++++-- app.config.ts | 94 ++++++++++++++++++----------------- eas.json | 16 +++--- jest.setup.ts | 3 -- scripts/create-brand.ts | 23 +++++---- src/api/client.test.ts | 3 -- src/config/brand.test.ts | 38 ++++++++++++++ src/config/brand.ts | 55 ++++++++------------ src/theme/color-utils.test.ts | 41 ++++++++++++++- src/theme/color-utils.ts | 62 +++++++++++++---------- src/theme/tokens.test.ts | 13 +++++ src/theme/tokens.ts | 31 ++++++++---- 13 files changed, 266 insertions(+), 146 deletions(-) create mode 100644 src/config/brand.test.ts diff --git a/.eas/workflows/release.yml b/.eas/workflows/release.yml index edd1648..caad9ba 100644 --- a/.eas/workflows/release.yml +++ b/.eas/workflows/release.yml @@ -24,10 +24,14 @@ jobs: - uses: eas/checkout - name: Clone private brand overlay repo run: | - rm -rf "brands/${{ inputs.brand }}" + set -euo pipefail + : "${BRANDS_REPO_TOKEN:?BRANDS_REPO_TOKEN secret is not set}" + rm -rf "brands/${{ inputs.brand }}" /tmp/chatbotx-mobile-brands git clone --depth 1 \ "https://x-access-token:${BRANDS_REPO_TOKEN}@github.com/ChatbotXIO/chatbotx-mobile-brands.git" \ /tmp/chatbotx-mobile-brands + test -f "/tmp/chatbotx-mobile-brands/${{ inputs.brand }}/brand.json" \ + || { echo "No brand.json for brand '${{ inputs.brand }}' in chatbotx-mobile-brands"; exit 1; } cp -R "/tmp/chatbotx-mobile-brands/${{ inputs.brand }}" "brands/${{ inputs.brand }}" build_android_production: diff --git a/app.config.test.ts b/app.config.test.ts index 5c278df..3516d4c 100644 --- a/app.config.test.ts +++ b/app.config.test.ts @@ -39,6 +39,22 @@ describe('validateBrand', () => { expect(result.eas.owner).toBe('my-org'); }); + test('treats an empty eas.owner as absent', () => { + const result = validateBrand( + { ...VALID_BRAND, eas: { projectId: 'abc-123', owner: '' } }, + 'acme', + ); + expect(result.eas.owner).toBeUndefined(); + }); + + test('accepts uppercase hex colors', () => { + const result = validateBrand( + { ...VALID_BRAND, colors: { brand: '#136EF1', splashBackground: '#FFFFFF' } }, + 'acme', + ); + expect(result.colors.brand).toBe('#136EF1'); + }); + test('allows an empty eas.projectId (no EAS project configured yet)', () => { const result = validateBrand({ ...VALID_BRAND, eas: { projectId: '' } }, 'acme'); expect(result.eas.projectId).toBe(''); @@ -50,13 +66,18 @@ describe('validateBrand', () => { ['slug', { ...VALID_BRAND, slug: 123 }], ['scheme', { ...VALID_BRAND, scheme: '' }], ['ios.bundleIdentifier', { ...VALID_BRAND, ios: {} }], - ['android.package', { ...VALID_BRAND, android: { adaptiveIconBackgroundColor: '#fff' } }], + ['android.package', { ...VALID_BRAND, android: { adaptiveIconBackgroundColor: '#ffffff' } }], [ 'android.adaptiveIconBackgroundColor', { ...VALID_BRAND, android: { package: 'com.acme.app' } }, ], - ['colors.brand', { ...VALID_BRAND, colors: { splashBackground: '#fff' } }], - ['colors.splashBackground', { ...VALID_BRAND, colors: { brand: '#fff' } }], + ['colors.brand', { ...VALID_BRAND, colors: { splashBackground: '#ffffff' } }], + ['colors.brand', { ...VALID_BRAND, colors: { ...VALID_BRAND.colors, brand: '#fff' } }], + [ + 'android.adaptiveIconBackgroundColor', + { ...VALID_BRAND, android: { ...VALID_BRAND.android, adaptiveIconBackgroundColor: 'blue' } }, + ], + ['colors.splashBackground', { ...VALID_BRAND, colors: { brand: '#ffffff' } }], ['eas.projectId', { ...VALID_BRAND, eas: {} }], ])('throws a message naming the missing field "%s"', (field, invalidBrand) => { expect(() => validateBrand(invalidBrand, 'acme')).toThrow( diff --git a/app.config.ts b/app.config.ts index ac53f2a..8bf07a5 100644 --- a/app.config.ts +++ b/app.config.ts @@ -43,16 +43,22 @@ export const APP_ENV_CONFIG: Record< production: { idSuffix: '', nameSuffix: '', channel: 'production' }, }; +const APP_ENVS = Object.keys(APP_ENV_CONFIG) as AppEnv[]; + +function isAppEnv(value: string): value is AppEnv { + return (APP_ENVS as string[]).includes(value); +} + export function readAppEnv(raw: string | undefined): AppEnv { const value = raw ?? 'development'; - if (value === 'development' || value === 'preview' || value === 'production') { - return value; - } + if (isAppEnv(value)) return value; throw new Error( - `Invalid APP_ENV "${value}" — expected "development", "preview", or "production".`, + `Invalid APP_ENV "${value}" — expected ${APP_ENVS.map((e) => `"${e}"`).join(', ')}.`, ); } +const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/; + /** Hand-rolled validation (matching the src/config/env.ts pattern elsewhere in the repo) rather * than pulling in zod for one call site that only ever runs in the Node config-evaluation * context, never bundled into the app. */ @@ -61,51 +67,52 @@ export function validateBrand(raw: unknown, brandId: string): BrandConfig { throw new Error(`brands/${brandId}/brand.json is missing or has an invalid "${field}" field.`); }; - if (typeof raw !== 'object' || raw === null) fail('(root)'); - const b = raw as Record; - - if (typeof b.id !== 'string' || !b.id) fail('id'); - if (typeof b.displayName !== 'string' || !b.displayName) fail('displayName'); - if (typeof b.slug !== 'string' || !b.slug) fail('slug'); - if (typeof b.scheme !== 'string' || !b.scheme) fail('scheme'); + const asRecord = (value: unknown, field: string): Record => + typeof value === 'object' && value !== null ? (value as Record) : fail(field); - const ios = b.ios as Record | undefined; - if (!ios || typeof ios.bundleIdentifier !== 'string' || !ios.bundleIdentifier) { - fail('ios.bundleIdentifier'); - } - - const android = b.android as Record | undefined; - if (!android || typeof android.package !== 'string' || !android.package) { - fail('android.package'); - } - if (typeof android?.adaptiveIconBackgroundColor !== 'string') { - fail('android.adaptiveIconBackgroundColor'); - } - - const colors = b.colors as Record | undefined; - if (!colors || typeof colors.brand !== 'string') fail('colors.brand'); - if (typeof colors?.splashBackground !== 'string') fail('colors.splashBackground'); + /** Reads `path` (e.g. "ios.bundleIdentifier") off the raw object as a string. Empty strings are + * rejected unless `allowEmpty`; `optional` also accepts an absent key (returned as ''); `hex` + * additionally requires a 6-digit `#rrggbb` color so a bad value fails here, naming the file, + * instead of at module load deep inside src/theme/tokens.ts. */ + const requireString = ( + path: string, + { + allowEmpty = false, + optional = false, + hex = false, + }: { allowEmpty?: boolean; optional?: boolean; hex?: boolean } = {}, + ): string => { + const value = path.split('.').reduce((obj, key) => asRecord(obj, path)[key], raw); + if (value === undefined && optional) return ''; + if (typeof value !== 'string') return fail(path); + if (!allowEmpty && !value) return fail(path); + if (hex && !HEX_COLOR_PATTERN.test(value)) return fail(path); + return value; + }; - const eas = b.eas as Record | undefined; - if (!eas || typeof eas.projectId !== 'string') fail('eas.projectId'); + asRecord(raw, '(root)'); + const owner = requireString('eas.owner', { optional: true, allowEmpty: true }); return { - id: b.id as string, - displayName: b.displayName as string, - slug: b.slug as string, - scheme: b.scheme as string, - ios: { bundleIdentifier: ios!.bundleIdentifier as string }, + id: requireString('id'), + displayName: requireString('displayName'), + slug: requireString('slug'), + scheme: requireString('scheme'), + ios: { bundleIdentifier: requireString('ios.bundleIdentifier') }, android: { - package: android!.package as string, - adaptiveIconBackgroundColor: android!.adaptiveIconBackgroundColor as string, + package: requireString('android.package'), + adaptiveIconBackgroundColor: requireString('android.adaptiveIconBackgroundColor', { + hex: true, + }), }, colors: { - brand: colors!.brand as string, - splashBackground: colors!.splashBackground as string, + brand: requireString('colors.brand', { hex: true }), + splashBackground: requireString('colors.splashBackground', { hex: true }), }, + // `owner` is optional; an absent or empty value means "use the logged-in EAS account". eas: { - projectId: eas!.projectId as string, - owner: typeof eas!.owner === 'string' && eas!.owner ? (eas!.owner as string) : undefined, + projectId: requireString('eas.projectId', { allowEmpty: true }), + owner: owner || undefined, }, }; } @@ -218,8 +225,8 @@ const config: ExpoConfig = { { // No dedicated notification icon asset exists yet (Android requires a flat white // silhouette, distinct from the app icon) — falls back to Expo's generated default until - // one is designed. `color` matches the splash-screen brand color. - color: brand.colors.splashBackground, + // one is designed. `color` tints that icon with the brand color. + color: brand.colors.brand, }, ], 'expo-updates', @@ -254,11 +261,8 @@ const config: ExpoConfig = { // native Info.plist/strings.xml values the plugin writes at build time. supportsRTL: true, brandId: brand.id, - displayName: brand.displayName, brandScheme: brand.scheme, brandColor: brand.colors.brand, - appEnv, - updateChannel: envConfig.channel, ...(hasEasProject ? { // Written by `eas init` — required by `getExpoPushTokenAsync` in src/lib/notifications.ts. diff --git a/eas.json b/eas.json index fede0f8..22c3c02 100644 --- a/eas.json +++ b/eas.json @@ -47,22 +47,22 @@ } }, "submit": { - "preview-testflight": { + "base": { "ios": { "ascApiKeyPath": "./AuthKey_238L44SV57.p8", "ascApiKeyId": "238L44SV57", "ascApiKeyIssuerId": "a888294a-adc3-4201-a022-67fbbc4a597d", - "appleTeamId": "5H3UF98Q3H", + "appleTeamId": "5H3UF98Q3H" + } + }, + "preview-testflight": { + "extends": "base", + "ios": { "ascAppId": "6806470556" } }, "production": { - "ios": { - "ascApiKeyPath": "./AuthKey_238L44SV57.p8", - "ascApiKeyId": "238L44SV57", - "ascApiKeyIssuerId": "a888294a-adc3-4201-a022-67fbbc4a597d", - "appleTeamId": "5H3UF98Q3H" - }, + "extends": "base", "android": { "track": "internal" } diff --git a/jest.setup.ts b/jest.setup.ts index 2126d7a..f1aacc5 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -14,11 +14,8 @@ jest.mock('expo-constants', () => ({ apiBaseUrl: 'https://test.invalid', wsUrl: 'wss://test.invalid', brandId: 'chatbotx', - displayName: 'ChatbotX', brandScheme: 'chatbotxmobileapp', brandColor: '#3c6df0', - appEnv: 'development', - updateChannel: 'development', }, }, }, diff --git a/scripts/create-brand.ts b/scripts/create-brand.ts index 937698f..d6f2753 100644 --- a/scripts/create-brand.ts +++ b/scripts/create-brand.ts @@ -1,4 +1,4 @@ -import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { cpSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; /** @@ -27,14 +27,19 @@ if (existsSync(targetDir)) { process.exit(1); } -mkdirSync(targetDir, { recursive: true }); -cpSync(templateDir, targetDir, { recursive: true }); - -const brandJsonPath = join(targetDir, 'brand.json'); -const brandJson = JSON.parse(readFileSync(brandJsonPath, 'utf8')) as Record; -delete brandJson['//']; -brandJson.id = brandId; -writeFileSync(brandJsonPath, `${JSON.stringify(brandJson, null, 2)}\n`); +try { + cpSync(templateDir, targetDir, { recursive: true }); + + const brandJsonPath = join(targetDir, 'brand.json'); + const { '//': _templateNote, ...brandJson } = JSON.parse( + readFileSync(brandJsonPath, 'utf8'), + ) as Record; + writeFileSync(brandJsonPath, `${JSON.stringify({ ...brandJson, id: brandId }, null, 2)}\n`); +} catch (error) { + // Don't leave a half-scaffolded folder behind — it would trip the "already exists" guard on retry. + rmSync(targetDir, { recursive: true, force: true }); + throw error; +} console.log(`Created brands/${brandId} from brands/_template.`); console.log( diff --git a/src/api/client.test.ts b/src/api/client.test.ts index 35d39a3..45c16eb 100644 --- a/src/api/client.test.ts +++ b/src/api/client.test.ts @@ -14,11 +14,8 @@ jest.mock('expo-constants', () => ({ apiBaseUrl: 'http://localhost:3123', wsUrl: 'ws://localhost:3123', brandId: 'chatbotx', - displayName: 'ChatbotX', brandScheme: 'chatbotxmobileapp', brandColor: '#3c6df0', - appEnv: 'development', - updateChannel: 'development', }, }, }, diff --git a/src/config/brand.test.ts b/src/config/brand.test.ts new file mode 100644 index 0000000..85ce527 --- /dev/null +++ b/src/config/brand.test.ts @@ -0,0 +1,38 @@ +const VALID_EXTRA = { + brandId: 'chatbotx', + brandScheme: 'chatbotxmobileapp', + brandColor: '#3c6df0', +}; + +function loadBrandWith(extra: Record | undefined): () => typeof import('./brand') { + jest.resetModules(); + jest.doMock('expo-constants', () => ({ + __esModule: true, + default: { expoConfig: extra === undefined ? undefined : { extra } }, + })); + // eslint-disable-next-line @typescript-eslint/no-require-imports -- fresh module load per mock + return () => require('./brand') as typeof import('./brand'); +} + +describe('brand', () => { + afterEach(() => { + jest.dontMock('expo-constants'); + }); + + it('exposes the brand fields from Expo config extra', () => { + const { brand } = loadBrandWith(VALID_EXTRA)(); + expect(brand).toEqual(VALID_EXTRA); + }); + + it.each(['brandId', 'brandScheme', 'brandColor'] as const)( + 'throws at load when "%s" is missing', + (field) => { + const { [field]: _omitted, ...rest } = VALID_EXTRA; + expect(loadBrandWith(rest)).toThrow(new RegExp(`extra\\.${field}`)); + }, + ); + + it('throws at load when expoConfig is absent entirely', () => { + expect(loadBrandWith(undefined)).toThrow(/extra\.brandId/); + }); +}); diff --git a/src/config/brand.ts b/src/config/brand.ts index ddb630d..73d09ca 100644 --- a/src/config/brand.ts +++ b/src/config/brand.ts @@ -1,54 +1,39 @@ import Constants from 'expo-constants'; -type AppEnv = 'development' | 'preview' | 'production'; - interface BrandExtraConfig { brandId: string; - displayName: string; brandScheme: string; brandColor: string; - appEnv: AppEnv; - updateChannel: string; } +const REQUIRED_FIELDS: readonly (keyof BrandExtraConfig)[] = [ + 'brandId', + 'brandScheme', + 'brandColor', +]; + function readExtra(): BrandExtraConfig { - const extra = Constants.expoConfig?.extra as Partial | undefined; + const extra = (Constants.expoConfig?.extra ?? {}) as Partial; - if (!extra?.brandId) { - throw new Error( - 'Missing "extra.brandId" in Expo config. Check app.config.ts and the BRAND env var.', - ); - } - if (!extra?.displayName) { - throw new Error('Missing "extra.displayName" in Expo config. Check app.config.ts.'); - } - if (!extra?.brandScheme) { - throw new Error('Missing "extra.brandScheme" in Expo config. Check app.config.ts.'); - } - if (!extra?.brandColor) { - throw new Error('Missing "extra.brandColor" in Expo config. Check app.config.ts.'); - } - if (!extra?.appEnv) { - throw new Error( - 'Missing "extra.appEnv" in Expo config. Check app.config.ts and the APP_ENV env var.', - ); - } - if (!extra?.updateChannel) { - throw new Error('Missing "extra.updateChannel" in Expo config. Check app.config.ts.'); + for (const field of REQUIRED_FIELDS) { + if (!extra[field]) { + throw new Error( + `Missing "extra.${field}" in Expo config. Check app.config.ts and the BRAND env var.`, + ); + } } return { - brandId: extra.brandId, - displayName: extra.displayName, - brandScheme: extra.brandScheme, - brandColor: extra.brandColor, - appEnv: extra.appEnv, - updateChannel: extra.updateChannel, + brandId: extra.brandId!, + brandScheme: extra.brandScheme!, + brandColor: extra.brandColor!, }; } /** - * Brand identity, sourced from `brands//brand.json` via app.config.ts `extra`. See - * docs/white-label.md for how a new brand is created and built. + * Brand identity the app reads at runtime, sourced from `brands//brand.json` via + * app.config.ts `extra`. Only what src/ actually consumes is exposed here — the display name is + * available as `Constants.expoConfig.name`, and the environment/update channel are build-time + * concerns owned by eas.json. See docs/white-label.md for how a new brand is created and built. */ export const brand: BrandExtraConfig = readExtra(); diff --git a/src/theme/color-utils.test.ts b/src/theme/color-utils.test.ts index 6e895b4..d9d1e06 100644 --- a/src/theme/color-utils.test.ts +++ b/src/theme/color-utils.test.ts @@ -1,4 +1,4 @@ -import { alphaTokens, withAlpha } from '@/theme/color-utils'; +import { alphaTokens, darken, lighten, withAlpha } from '@/theme/color-utils'; describe('withAlpha', () => { it('converts a 6-digit hex color into an rgba string', () => { @@ -48,3 +48,42 @@ describe('alphaTokens', () => { }); }); }); + +describe('darken', () => { + it('scales every channel toward black by the given amount', () => { + expect(darken('#3c6df0', 0.2)).toBe('#3057c0'); + }); + + it('returns the input unchanged at 0 and black at 1', () => { + expect(darken('#3c6df0', 0)).toBe('#3c6df0'); + expect(darken('#3c6df0', 1)).toBe('#000000'); + }); + + it('clamps out-of-range amounts', () => { + expect(darken('#3c6df0', -1)).toBe('#3c6df0'); + expect(darken('#3c6df0', 5)).toBe('#000000'); + }); + + it('always emits two hex digits per channel', () => { + expect(darken('#0a0a0a', 0.5)).toBe('#050505'); + }); + + it('throws on an invalid hex color', () => { + expect(() => darken('#fff', 0.2)).toThrow(/darken/); + }); +}); + +describe('lighten', () => { + it('blends every channel toward white by the given amount', () => { + expect(lighten('#3c6df0', 0.5)).toBe('#9eb6f8'); + }); + + it('returns the input unchanged at 0 and white at 1', () => { + expect(lighten('#3c6df0', 0)).toBe('#3c6df0'); + expect(lighten('#3c6df0', 1)).toBe('#ffffff'); + }); + + it('throws on an invalid hex color', () => { + expect(() => lighten('blue', 0.2)).toThrow(/lighten/); + }); +}); diff --git a/src/theme/color-utils.ts b/src/theme/color-utils.ts index 20db979..2657eb4 100644 --- a/src/theme/color-utils.ts +++ b/src/theme/color-utils.ts @@ -6,22 +6,37 @@ const HEX_PATTERN = /^#([0-9a-fA-F]{6})$/; -/** Parses a 6-digit `#rrggbb` hex string and returns an `rgba(r, g, b, alpha)` string. Throws on - * anything else (3-digit shorthand, 8-digit hex-with-alpha, named colors) — callers only ever - * feed this our own token hex values, so a loud failure beats silently returning a wrong color. */ -export function withAlpha(hex: string, alpha: number): string { +type Rgb = readonly [r: number, g: number, b: number]; + +/** Parses a 6-digit `#rrggbb` hex string into channel values. Throws on anything else (3-digit + * shorthand, 8-digit hex-with-alpha, named colors) — callers only ever feed this our own token / + * brand.json hex values, so a loud failure beats silently returning a wrong color. */ +function parseHex(hex: string, caller: string): Rgb { const match = HEX_PATTERN.exec(hex); if (!match) { - throw new Error(`withAlpha: expected a 6-digit hex color like "#3c6df0", got "${hex}"`); + throw new Error(`${caller}: expected a 6-digit hex color like "#3c6df0", got "${hex}"`); } - const intValue = parseInt(match[1], 16); - const r = (intValue >> 16) & 255; - const g = (intValue >> 8) & 255; - const b = intValue & 255; - const clampedAlpha = Math.min(1, Math.max(0, alpha)); + return [(intValue >> 16) & 255, (intValue >> 8) & 255, intValue & 255]; +} + +const clamp01 = (value: number): number => Math.min(1, Math.max(0, value)); + +const toHex = ([r, g, b]: Rgb): string => + `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`; + +/** Linearly blends every channel of `hex` toward `target` (0 = unchanged, 1 = fully `target`). */ +function mixToward(hex: string, target: number, amount: number, caller: string): string { + const t = clamp01(amount); + const blend = (channel: number): number => Math.round(channel + (target - channel) * t); + const [r, g, b] = parseHex(hex, caller); + return toHex([blend(r), blend(g), blend(b)]); +} - return `rgba(${r}, ${g}, ${b}, ${clampedAlpha})`; +/** Returns an `rgba(r, g, b, alpha)` string for a 6-digit `#rrggbb` hex color. */ +export function withAlpha(hex: string, alpha: number): string { + const [r, g, b] = parseHex(hex, 'withAlpha'); + return `rgba(${r}, ${g}, ${b}, ${clamp01(alpha)})`; } /** Shared alpha values for overlay-style states (hover/press/soft fills/scrims), so call sites @@ -36,22 +51,15 @@ export const alphaTokens = { scrimDark: 0.6, } as const; -/** Darkens a 6-digit `#rrggbb` hex color toward black by `amount` (0–1). Used to derive a - * `brandStrong` (pressed/emphasis) shade from a single brand color instead of hand-picking a - * second hex per brand — see src/theme/tokens.ts. */ +/** Darkens a 6-digit `#rrggbb` hex color toward black by `amount` (0–1). Used to derive the + * light-scheme `brandStrong` (pressed/emphasis) shade from the single brand color in brand.json + * instead of hand-picking a second hex per brand — see src/theme/tokens.ts. */ export function darken(hex: string, amount: number): string { - const match = HEX_PATTERN.exec(hex); - if (!match) { - throw new Error(`darken: expected a 6-digit hex color like "#3c6df0", got "${hex}"`); - } - - const intValue = parseInt(match[1], 16); - const clampedAmount = Math.min(1, Math.max(0, amount)); - const scale = (channel: number): number => Math.round(channel * (1 - clampedAmount)); - - const r = scale((intValue >> 16) & 255); - const g = scale((intValue >> 8) & 255); - const b = scale(intValue & 255); + return mixToward(hex, 0, amount, 'darken'); +} - return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`; +/** Lightens a 6-digit `#rrggbb` hex color toward white by `amount` (0–1). Used to derive the + * dark-scheme brand shades (which need to sit on near-black surfaces) from the same brand color. */ +export function lighten(hex: string, amount: number): string { + return mixToward(hex, 255, amount, 'lighten'); } diff --git a/src/theme/tokens.test.ts b/src/theme/tokens.test.ts index ae31759..363c8fb 100644 --- a/src/theme/tokens.test.ts +++ b/src/theme/tokens.test.ts @@ -1,4 +1,5 @@ import { colorTokens, radiusTokens, spacingTokens, typographyTokens } from '@/theme/tokens'; +import { darken, lighten } from '@/theme/color-utils'; const NEW_SEMANTIC_COLOR_KEYS = [ 'bg', @@ -157,3 +158,15 @@ describe('typographyTokens', () => { expect(typographyTokens.label.letterSpacing).toBeGreaterThan(0); }); }); + +describe('brand-derived color tokens', () => { + // jest.setup.ts mocks `extra.brandColor` as '#3c6df0'. + it('derives light and dark brand shades from the configured brand color', () => { + expect(colorTokens.light.brand).toBe('#3c6df0'); + expect(colorTokens.light.bubbleOut).toBe('#3c6df0'); + expect(colorTokens.light.brandStrong).toBe(darken('#3c6df0', 0.2)); + expect(colorTokens.dark.brand).toBe(lighten('#3c6df0', 0.35)); + expect(colorTokens.dark.brandStrong).toBe(lighten('#3c6df0', 0.55)); + expect(colorTokens.dark.bubbleOut).toBe(darken('#3c6df0', 0.1)); + }); +}); diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts index 21c4c15..c509c03 100644 --- a/src/theme/tokens.ts +++ b/src/theme/tokens.ts @@ -10,12 +10,21 @@ import { brand } from '@/config/brand'; -import { darken, withAlpha } from './color-utils'; +import { darken, lighten, withAlpha } from './color-utils'; -/** Pressed/emphasis shade of the brand color, derived rather than hand-picked so each brand only - * needs to supply one color in brand.json. Darkened ~20% — matches the original ChatbotX - * `#3c6df0` → `#2851c9` relationship closely enough to keep existing screens' contrast intact. */ -const brandStrong = darken(brand.brandColor, 0.2); +/** Brand shades derived from the single `colors.brand` in brand.json rather than hand-picked per + * brand and per scheme. The light-scheme `brandStrong` (pressed/emphasis) is darkened ~20%; the + * dark scheme lifts the color toward white so it reads on near-black surfaces (`brand` ≈ the + * original ChatbotX `#7d9eff`, `brandStrong` ≈ `#a4bcff`) while `bubbleOut` stays close to the + * base color so outgoing bubbles keep enough contrast against white bubble text. */ +const brandShades = { + light: { brand: brand.brandColor, brandStrong: darken(brand.brandColor, 0.2) }, + dark: { + brand: lighten(brand.brandColor, 0.35), + brandStrong: lighten(brand.brandColor, 0.55), + bubbleOut: darken(brand.brandColor, 0.1), + }, +} as const; // --------------------------------------------------------------------------- // Colors @@ -35,12 +44,12 @@ const lightBase = { // (measured ~4.51:1). textTertiary: '#767063', textInverse: '#ffffff', - brand: brand.brandColor, - brandStrong, + brand: brandShades.light.brand, + brandStrong: brandShades.light.brandStrong, onBrand: '#ffffff', bubbleIn: '#ffffff', bubbleInText: '#1c1b17', - bubbleOut: brand.brandColor, + bubbleOut: brandShades.light.brand, bubbleOutText: '#ffffff', bubbleBot: '#f1ecff', bubbleBotText: '#2f1f66', @@ -62,12 +71,12 @@ const darkBase = { textSecondary: '#b9b3a2', textTertiary: '#847d6b', textInverse: '#1c1b17', - brand: '#7d9eff', - brandStrong: '#a4bcff', + brand: brandShades.dark.brand, + brandStrong: brandShades.dark.brandStrong, onBrand: '#0e152b', bubbleIn: '#28261f', bubbleInText: '#f4f2ec', - bubbleOut: '#3c5fd9', + bubbleOut: brandShades.dark.bubbleOut, bubbleOutText: '#ffffff', bubbleBot: '#2a2145', bubbleBotText: '#e4d9ff', From aa4c51bd6d6409f610c0d52c2dd93adf0459fac1 Mon Sep 17 00:00:00 2001 From: Real Codesiman Date: Sat, 29 Aug 2026 15:06:59 +0700 Subject: [PATCH 4/4] chore(ci): silence test log noise and bump deprecated GitHub Actions - jest.setup.ts: filter RNGH's false-positive "callbacks are worklets and some are not" console.error (inconsistent worklet transform under Jest against RNGH's own precompiled ReanimatedSwipeable.js, not our code) - app.config.ts: skip the brand/appEnv console.log under Jest, keep it for real expo prebuild/EAS runs - ci.yml: bump actions/checkout, actions/setup-node, pnpm/action-setup to their Node-24-compatible majors, clearing the "Node.js 20 is deprecated" annotation --- .github/workflows/ci.yml | 6 +++--- app.config.ts | 9 ++++++--- jest.setup.ts | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 682a50c..24e799a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,11 +9,11 @@ jobs: ci: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7 with: node-version: 22 cache: pnpm diff --git a/app.config.ts b/app.config.ts index 8bf07a5..97368a7 100644 --- a/app.config.ts +++ b/app.config.ts @@ -143,9 +143,12 @@ const assetsDir = `./brands/${brandId}/assets`; // Loud stdout log: `BRAND` must be correct wherever eas-cli evaluates this file locally (e.g. to // resolve `extra.eas.projectId` before a build upload) — a silently-wrong brand would build straight -// into the wrong customer's EAS project. See plan risk notes in docs/deploy.md. -// eslint-disable-next-line no-console -- deliberate build-time diagnostic, not app runtime code -console.log(`[app.config] brand=${brand.id} appEnv=${appEnv}`); +// into the wrong customer's EAS project. See plan risk notes in docs/deploy.md. Skipped under Jest +// (app.config.ts is exercised directly by app.config.test.ts) since it's pure noise there. +if (!process.env.JEST_WORKER_ID) { + // eslint-disable-next-line no-console -- deliberate build-time diagnostic, not app runtime code + console.log(`[app.config] brand=${brand.id} appEnv=${appEnv}`); +} const hasEasProject = brand.eas.projectId.length > 0; diff --git a/jest.setup.ts b/jest.setup.ts index f1aacc5..d655fa5 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -83,3 +83,21 @@ jest.mock('expo-document-picker', () => ({ __esModule: true, getDocumentAsync: jest.fn(), })); + +// react-native-gesture-handler's ReanimatedSwipeable (used by SwipeableRow) composes internal +// Tap/Pan gestures whose callbacks are worklet-ized by Reanimated's babel plugin inconsistently +// under Jest's transform — some callbacks read as worklets, some don't — which trips RNGH's own +// `checkGestureCallbacksForWorklets` dev-mode check (react-native-gesture-handler/src/handlers/ +// gestures/GestureDetector/utils.ts). It's a false positive against RNGH's own compiled +// ReanimatedSwipeable.js, not anything in our code, and the gestures work correctly at runtime. +// Filter only this exact message so real console.error calls still fail tests as expected. +const RNGH_WORKLET_MIXED_CALLBACKS_MESSAGE = + "Some of the callbacks in the gesture are worklets and some are not. Either make sure that all calbacks are marked as 'worklet' if you wish to run them on the UI thread or use '.runOnJS(true)' modifier on the gesture explicitly to run all callbacks on the JS thread."; + +const originalConsoleError = console.error.bind(console); +jest.spyOn(console, 'error').mockImplementation((...args: unknown[]) => { + if (typeof args[0] === 'string' && args[0].includes(RNGH_WORKLET_MIXED_CALLBACKS_MESSAGE)) { + return; + } + originalConsoleError(...args); +});