diff --git a/.emergent/emergent.yml b/.emergent/emergent.yml deleted file mode 100644 index 678cefe9..00000000 --- a/.emergent/emergent.yml +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env_image_name": "expo_mongo_base_image_cloud_arm:release-18022026-2", - "job_id": "35614e0d-446a-45a3-8537-f9b837a8b4e1", - "created_at": "2026-02-20T11:39:55.272020+00:00Z" -} diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml new file mode 100644 index 00000000..cc1670f5 --- /dev/null +++ b/.github/workflows/mobile.yml @@ -0,0 +1,46 @@ +name: Mobile + +on: + pull_request: + paths: + - "apps/mobile/**" + - "packages/core/**" + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + - ".npmrc" + - ".github/workflows/mobile.yml" + push: + branches: [main] + paths: + - "apps/mobile/**" + - "packages/core/**" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + +concurrency: + group: mobile-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + # Typecheck and test the phone app and the shared package it uses. + - run: pnpm turbo run typecheck test --filter "@outhere/mobile..." + - name: Web build (local data) + working-directory: apps/mobile + run: pnpm exec expo export --platform web --output-dir /tmp/web + - name: Web build (Supabase mode) + working-directory: apps/mobile + env: + EXPO_PUBLIC_SUPABASE_URL: https://example.supabase.co + EXPO_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder + run: pnpm exec expo export --platform web --clear --output-dir /tmp/web-remote diff --git a/.github/workflows/supabase.yml b/.github/workflows/supabase.yml new file mode 100644 index 00000000..4a0e96d8 --- /dev/null +++ b/.github/workflows/supabase.yml @@ -0,0 +1,51 @@ +name: Database + +on: + pull_request: + paths: ["supabase/**", ".github/workflows/supabase.yml"] + push: + branches: [main] + paths: ["supabase/**"] + +jobs: + migrations: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:17 + env: + POSTGRES_PASSWORD: postgres + ports: ["5432:5432"] + options: >- + --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10 + env: + PGHOST: localhost + PGUSER: postgres + PGPASSWORD: postgres + steps: + - uses: actions/checkout@v4 + - name: Migrate, seed and test + run: ./supabase/tests/run.sh + - name: Types are up to date + run: | + createdb oh_types + sed "/^create role/d" supabase/tests/shim.sql | psql -d oh_types -q > /dev/null + for f in supabase/migrations/*.sql; do psql -d oh_types -q -v ON_ERROR_STOP=1 -f "$f" > /dev/null; done + python3 supabase/scripts/gen_types.py oh_types /tmp/database.ts + diff -u supabase/types/database.ts /tmp/database.ts + + functions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + # Run from the functions folder so Deno reads its own deno.json and + # leaves the pnpm workspace at the root alone. + - name: Typecheck Edge Functions + working-directory: supabase/functions + run: deno check story-media/index.ts + - name: Test the R2 helper + working-directory: supabase/functions + run: deno test --allow-env _shared/r2_test.ts diff --git a/Docs/Prd.md b/Docs/Prd.md index 6df8bf95..cecbcb70 100644 --- a/Docs/Prd.md +++ b/Docs/Prd.md @@ -2,8 +2,8 @@ ## Status - This document defines product behaviour, rules, and UX requirements. -- Engineering execution order lives in docs/BUILD_PLAN.md. -- Codex tasks live in docs/TASKS/. +- Engineering execution order lives in Docs/ROADMAP.md (phases) and Docs/build_plan.md (original tasks). +- Task specs live in Docs/Task/. ## Non-negotiables - City-first identity (no borough dependency). @@ -14,8 +14,8 @@ - Tabs: Home, Move, Outside/Arena, Clubs (Crews), Profile. ## Current repo notes -- Expo Router tabs live in frontend/app/(tabs)/ -- API client in frontend/src/services/api.ts defines current endpoints. +- Expo Router tabs live in apps/mobile/src/app/(tabs)/ +- API client in apps/mobile/src/services/api.js defines current endpoints. ## Product Requirements Document (PRD) diff --git a/Docs/ROADMAP.md b/Docs/ROADMAP.md new file mode 100644 index 00000000..5a7f00df --- /dev/null +++ b/Docs/ROADMAP.md @@ -0,0 +1,70 @@ +# OutHere Roadmap + +The app's screens and navigation are largely built, but much of what they show is hardcoded, there are no real accounts, and the backend trusts whatever the phone sends. This roadmap replaces what sits underneath the screens, one phase per pull request, without redesigning them. + +Each phase starts only once the previous phase's "done when" check passes. + +## Phase 0: Clean foundation (done) + +- One app: `apps/mobile`. The old Emergent `frontend/` app and generated root clutter are removed. +- The Anything (create.xyz) runtime is gone: no fetch override, launcher menu, remote error reporting or sandbox messaging. +- Step sync posts the real count (it used to post 0 every 30 seconds) and uses the local calendar day. +- The DEV BYPASS button only exists in development builds; a failed signup no longer onboards a local-only account. +- The committed Mapbox token is removed; route planning falls back to OSRM without one. +- App identity: name `OutHere`, scheme `outhere`, bundle ID `com.outhere.app`, real permission strings. Native projects are generated by `expo prebuild` / EAS instead of committed. +- iOS maps use Apple Maps (Google Maps needs an SDK and key we don't ship). +- The backend installs from `requirements.txt` and its smoke tests live in `backend/tests`. + +**Done when:** the app bundles for iOS, Android and web with only OutHere's own config. + +## Phase 0.5: The whole app working on the phone (done) + +The server decision is parked so the product can be built and felt first. + +- `src/data/engine.js` owns all data in AsyncStorage, simulates a seeded world of 24 players and 5 crews, and applies the rules for XP, OUT, streaks, battles, crews, events, drops, rewards and memberships. +- Every screen reads through React Query hooks (`src/data/hooks.js`) and is built from one design kit (`src/components/ui`). All mock arrays, fake fallbacks, `/api/*` fetches and dead buttons are gone. +- Membership limits are enforced and open an upgrade sheet. Payments are not live, so switching plan is free in this build. +- The old `services/api.js` client and the unused Mapbox and RevenueCat packages are removed, so the app runs in Expo Go. + +**Done when:** every tab and detail screen works end to end offline. A server later replaces `engine.js` behind the same functions. + +## Phase 1: Accounts and real data + +- A dedicated Supabase project: real tables, Row Level Security, Storage for crew logos. +- Sign in with Apple, Google or an email code. The random `device_id` is retired. +- Step ingest goes through a server function that rejects future dates, caps backdating at one day and caps daily steps. +- `src/services/api.js` moves onto Supabase. + +**Done when:** two test users cannot read or change each other's data. + +## Phase 2: Core loop for real + +- Home, Move, Crew and Arena on live data; runs saved to the server. +- Battles and the XP/OUT ledger become database functions with unique keys (no double payouts), and battles complete on a schedule. +- Crew chat and feed in realtime. Every mock array and fake fallback is removed, with proper empty and error states. + +**Done when:** none of the five main tabs shows hardcoded data. + +## Phase 3: Memberships and payments + +- RevenueCat in-app purchases for Pro and Black, with a 14-day Pro trial. +- A store webhook sets the membership tier; gating is enforced on the server. +- Black application and invite flow behind the 14-day streak. + +**Done when:** a sandbox purchase unlocks Pro end to end. + +## Phase 4: Events, drops, rewards and social + +- Events and drops run from a simple admin, with RSVP capacity enforced. +- Rewards catalogue and OUT redemption with weekly limits. +- Friends, user search, news feed and push notifications. + +**Done when:** spec tasks 001 to 008 in `Docs/Task` all pass their checks. + +## Phase 5: Launch readiness + +- Apple Health and Google Fit, background step tracking. +- Crash reporting, analytics, anti-cheat caps and rate limits. +- EAS builds, TestFlight beta, store listings and a privacy policy. + +**Done when:** a TestFlight beta is live with real users in London. diff --git a/Docs/Task.zip b/Docs/Task.zip deleted file mode 100644 index 28fb09cc..00000000 Binary files a/Docs/Task.zip and /dev/null differ diff --git a/Docs/build_plan.md b/Docs/build_plan.md index 5a0e84ee..a68957aa 100644 --- a/Docs/build_plan.md +++ b/Docs/build_plan.md @@ -1,5 +1,8 @@ -# docs/BUILD_PLAN.md -# OutHere Build Plan (Single Source of Execution Truth) +# Docs/build_plan.md +# OutHere Build Plan + +> Tasks 001 to 008 below are largely built as a demo. The current plan for making them real +> (accounts, real data, payments, launch) lives in [ROADMAP.md](ROADMAP.md). ## Goal Deliver OutHere in stable, shippable increments without breaking existing tracking, UI, or API. @@ -38,10 +41,10 @@ This plan is deliberately strict because it prevents the agent from “helpfully --- ## Repo structure assumptions (for agent context) -- Frontend lives in: `frontend/` +- Frontend lives in: `apps/mobile/` (the old `frontend/` app was removed in phase 0) - Backend lives in: `backend/` -- Tabs and navigation live in: `frontend/app/(tabs)/` -- API client lives in: `frontend/src/services/api.ts` +- Tabs and navigation live in: `apps/mobile/src/app/(tabs)/` +- API client lives in: `apps/mobile/src/services/api.js` If the repo differs, tasks must be adjusted to actual paths without inventing new architecture. --- @@ -49,15 +52,15 @@ If the repo differs, tasks must be adjusted to actual paths without inventing ne ## Working Method (How to Run Codex) For each task: 1. Ensure the following files exist and are current: - - `docs/PRD.md` - - `docs/BUILD_PLAN.md` (this file) - - `docs/TASKS/00X_*.md` (the task you are running) + - `Docs/Prd.md` + - `Docs/build_plan.md` (this file) + - `Docs/Task/00X_*.md` (the task you are running) 2. Create a branch named: - `task/00X-short-name` 3. Run Codex with instructions: - - Read `docs/PRD.md` - - Read `docs/BUILD_PLAN.md` - - Read the target task file in `docs/TASKS/` + - Read `Docs/Prd.md` + - Read `Docs/build_plan.md` + - Read the target task file in `Docs/Task/` - Implement **only** that task file - Output **git diff patch only** 4. Apply patch locally. @@ -98,10 +101,10 @@ If a task introduces regressions: # Execution Tasks (These are the ONLY tasks to run) These tasks must be completed in order. -They are stored as markdown files in `docs/TASKS/` and numbered 001–008. +They are stored as markdown files in `Docs/Task/` and numbered 001–008. ## TASK-001 - Arena Tab (Replace Challenges) + Hub Layout -**Task file:** `docs/TASKS/001_ARENA_TAB.md` +**Task file:** `Docs/Task/001_ARENA_TAB.md` Purpose: - Establish the new product centre: Arena as the hub. @@ -119,7 +122,7 @@ Smoke test focus: --- ## TASK-002 - Backend: 1v1 Battles (Create, List, Detail, Complete) -**Task file:** `docs/TASKS/002_BATTLES_1V1_BACKEND.md` +**Task file:** `Docs/Task/002_BATTLES_1V1_BACKEND.md` Purpose: - Create the first “competitive retention loop”. @@ -140,7 +143,7 @@ Smoke test focus: --- ## TASK-003 - Frontend: 1v1 Battles UI (Create + List + Detail) -**Task file:** `docs/TASKS/003_BATTLES_1V1_FRONTEND.md` +**Task file:** `Docs/Task/003_BATTLES_1V1_FRONTEND.md` Purpose: - Make battles visible and usable from Arena. @@ -157,7 +160,7 @@ Smoke test focus: --- ## TASK-004 - XP + OUT Ledgers (Server Source of Truth) + Client UI -**Task file:** `docs/TASKS/004_XP_OUT_LEDGER.md` +**Task file:** `Docs/Task/004_XP_OUT_LEDGER.md` Purpose: - Establish the platform economy correctly from day one. @@ -179,7 +182,7 @@ Smoke test focus: --- ## TASK-005 - Memberships: Free, Pro, Black + Feature Gating + Black Eligibility (14-day) -**Task file:** `docs/TASKS/005_MEMBERSHIPS_GATING.md` +**Task file:** `Docs/Task/005_MEMBERSHIPS_GATING.md` Purpose: - Add monetisation and status access control without breaking the UX. @@ -200,7 +203,7 @@ Smoke test focus: --- ## TASK-006 - Crews: Ownership, Roles, Logo Upload (Pro/Black) + No Custom Colours -**Task file:** `docs/TASKS/006_CREW_OWNERSHIP_LOGOS.md` +**Task file:** `Docs/Task/006_CREW_OWNERSHIP_LOGOS.md` Purpose: - Turn crews into identity assets and community engines, without design chaos. @@ -223,7 +226,7 @@ Smoke test focus: --- ## TASK-007 - Drops + Rewards Catalogue + OUT Redemption -**Task file:** `docs/TASKS/007_REWARDS_DROPS_SHOP.md` +**Task file:** `Docs/Task/007_REWARDS_DROPS_SHOP.md` Purpose: - Introduce the commerce loop (drops + redemption) without building a full e-commerce platform prematurely. @@ -246,7 +249,7 @@ Smoke test focus: --- ## TASK-008 - Events: List, RSVP, Check-in, Rewards -**Task file:** `docs/TASKS/008_EVENTS_RSVP_CHECKIN.md` +**Task file:** `Docs/Task/008_EVENTS_RSVP_CHECKIN.md` Purpose: - Make OutHere physical and sponsor-ready, linking digital status to real-world presence. diff --git a/README.md b/README.md index 0ee90ede..07dc91cc 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ pnpm workspaces with Turborepo. - `apps/web` - the website at weouthere.app - `apps/app` - the member app: clubs, battles, league, Club HQ, and the public club pages (`/`, `/join/`, `/@`) - `apps/admin` - Outhere HQ, the staff app -- `apps/mobile` - Expo phone app (primary client). It still installs with npm from its own folder: one dependency is a GitHub tarball, and its patch-package patches and npm overrides move to `pnpm-workspace.yaml` when it joins. +- `apps/mobile` - Expo phone app (primary client). Run it with `pnpm --filter @outhere/mobile start`. Its package pins and patches live in `pnpm-workspace.yaml`, and it shares rules with `packages/core`. +- `supabase` - the database: migrations, seed data, tests and Edge Functions (see [supabase/README.md](supabase/README.md)) - `packages/core` - shared types, rules and data access (example data and Supabase) - `packages/ui` - shared React components and the stylesheet - `backend` - API/backend services @@ -76,15 +77,14 @@ Bottom tabs: ### Install ```bash -cd apps/mobile -npm install +corepack enable +pnpm install # from the repo root ``` ### Run mobile app ```bash -cd apps/mobile -npx expo start +pnpm --filter @outhere/mobile start ``` Useful options: diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts index 6e9dbf85..714bd303 100644 --- a/apps/admin/vite.config.ts +++ b/apps/admin/vite.config.ts @@ -3,6 +3,9 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [react()], + // The workspace installs a flat node_modules for the phone app, which hoists + // its older React to the root. Resolve React from this package everywhere. + resolve: { dedupe: ["react", "react-dom"] }, // Share the member app's images (/img/...) and favicon instead of copying them. publicDir: "../app/public", server: { port: 5174 }, diff --git a/apps/app/vite.config.ts b/apps/app/vite.config.ts index 62d1dfda..28dafe6e 100644 --- a/apps/app/vite.config.ts +++ b/apps/app/vite.config.ts @@ -3,13 +3,17 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [reactRouter()], - resolve: { tsconfigPaths: true }, + // The workspace installs a flat node_modules for the phone app, which hoists + // its older React to the root. Resolve React from this package everywhere. + resolve: { tsconfigPaths: true, dedupe: ["react", "react-dom", "react-router"] }, server: { port: 5173 }, // weouthere.app is the website, which passes every path it doesn't own to // this app. A folder name the website will never use keeps the two apart. build: { assetsDir: "_app" }, ssr: { // Workspace packages ship TypeScript source, so Vite compiles them. - noExternal: ["@outhere/core", "@outhere/ui"], + // React and React Router are bundled too, so the server render and + // react-dom always share this app's copy of React. + noExternal: ["@outhere/core", "@outhere/ui", "react", "react-dom", "react-router"], }, }); diff --git a/apps/mobile/.env.example b/apps/mobile/.env.example index 5b997e33..3dd84afd 100644 --- a/apps/mobile/.env.example +++ b/apps/mobile/.env.example @@ -1,3 +1,13 @@ -# OUT HERE Mobile — Environment Variables -# The backend URL for API calls. When running the backend locally: +# Outhere mobile environment variables. Copy to .env and fill in. + +# Backend API base URL (the FastAPI server in /backend while it is still in use). EXPO_PUBLIC_BACKEND_URL=http://localhost:8000 + +# Mapbox public token for walking route planning. Optional: without it the +# Move tab falls back to the public OSRM router. +EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN= + +# Outhere's own Supabase project (never the other app's). Leave blank to run +# on local demo data on the phone. +EXPO_PUBLIC_SUPABASE_URL= +EXPO_PUBLIC_SUPABASE_ANON_KEY= diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore index 5873d9ab..baaa7a08 100644 --- a/apps/mobile/.gitignore +++ b/apps/mobile/.gitignore @@ -3,4 +3,7 @@ # The following patterns were generated by expo-cli expo-env.d.ts -# @end expo-cli \ No newline at end of file +# @end expo-cli +# Native projects are generated by `npx expo prebuild` / EAS from app.json +/ios +/android diff --git a/apps/mobile/App.web.tsx b/apps/mobile/App.web.tsx index 4736c3ff..509c73ac 100755 --- a/apps/mobile/App.web.tsx +++ b/apps/mobile/App.web.tsx @@ -1,115 +1,28 @@ -import { usePathname, useRouter } from 'expo-router'; import { App } from 'expo-router/build/qualified-entry'; -import React, { memo, useEffect, useState } from 'react'; -import { ErrorBoundaryWrapper } from './__create/SharedErrorBoundary'; -import './src/__create/polyfills'; +import React from 'react'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { Toaster } from 'sonner-native'; import { AlertModal } from './polyfills/web/alerts.web'; import './global.css'; -const GlobalErrorReporter = () => { - useEffect(() => { - if (typeof window === 'undefined') { - return; - } - const errorHandler = (event: ErrorEvent) => { - if (typeof event.preventDefault === 'function') event.preventDefault(); - console.error(event.error); - }; - // unhandled promises happen all the time, so we just log them - const unhandledRejectionHandler = (event: PromiseRejectionEvent) => { - if (typeof event.preventDefault === 'function') event.preventDefault(); - console.error('Unhandled promise rejection:', event.reason); - }; - window.addEventListener('error', errorHandler); - window.addEventListener('unhandledrejection', unhandledRejectionHandler); - return () => { - window.removeEventListener('error', errorHandler); - window.removeEventListener('unhandledrejection', unhandledRejectionHandler); - }; - }, []); - return null; -}; - -const Wrapper = memo(() => { - return ( - - - - - - - - ); -}); -const healthyResponse = { - type: 'sandbox:mobile:healthcheck:response', - healthy: true, -}; - -const useHandshakeParent = () => { - useEffect(() => { - const handleMessage = (event: MessageEvent) => { - if (event.data.type === 'sandbox:mobile:healthcheck') { - window.parent.postMessage(healthyResponse, '*'); - } - }; - window.addEventListener('message', handleMessage); - // Immediately respond to the parent window with a healthy response in - // case we missed the healthcheck message - window.parent.postMessage(healthyResponse, '*'); - return () => { - window.removeEventListener('message', handleMessage); - }; - }, []); -}; - -const CreateApp = () => { - const router = useRouter(); - const pathname = usePathname(); - useHandshakeParent(); - - useEffect(() => { - const handleMessage = (event: MessageEvent) => { - if (event.data.type === 'sandbox:navigation' && event.data.pathname !== pathname) { - router.push(event.data.pathname); - } - }; - - window.addEventListener('message', handleMessage); - window.parent.postMessage({ type: 'sandbox:mobile:ready' }, '*'); - return () => { - window.removeEventListener('message', handleMessage); - }; - }, [router, pathname]); - - useEffect(() => { - window.parent.postMessage( - { - type: 'sandbox:mobile:navigation', - pathname, - }, - '*' - ); - }, [pathname]); - - return ( - <> - - - - ); -}; - -export default CreateApp; +const WebApp = () => ( + <> + + + + + + +); + +export default WebApp; diff --git a/apps/mobile/NATIVE_TARGETS.md b/apps/mobile/NATIVE_TARGETS.md new file mode 100644 index 00000000..80b92e27 --- /dev/null +++ b/apps/mobile/NATIVE_TARGETS.md @@ -0,0 +1,94 @@ +# Native targets: watch, widgets and Live Activity + +Settings > Watch, widgets and lock screen shows previews of these +(`src/app/previews.jsx`, built from `src/components/previews/`). The real +versions are native iOS code that sits beside the Expo app. None of it is in +the app yet, and no packages for it are installed. + +## How they get into this Expo app + +1. Add the config plugin `@bacons/apple-targets`. Each target gets a folder, + `apps/mobile/targets//`, holding an `expo-target.config.js` and its + Swift sources. +2. `npx expo prebuild -p ios` turns those folders into Xcode targets. The `ios` + folder stays generated, not committed. +3. Build and sign with EAS (`eas build -p ios`). Each target needs its own + bundle id under `com.outhere.app` (for example `com.outhere.app.widgets`) + and its entitlements; EAS sets up the extra credentials in the same flow. +4. A small native module (Expo Modules API) lets the JavaScript side do what it + can't do alone: write to the App Group, reload widgets, start, update and end + the Live Activity, and talk to the watch. + +`src/components/previews/data.js` gathers exactly the data each one shows, from +the same hooks the app uses, so the snapshot the native side reads can be built +from it. + +## 1. Apple Watch app + +What it needs: + +- A watchOS app target in SwiftUI (`type: "watch"` in apple-targets). +- Watch Connectivity (`WCSession`) to the phone: application context for the + latest numbers, messages for taps (start, emotes, answer a challenger, check + in, finish). +- HealthKit on the watch: an `HKWorkoutSession` so a run keeps recording with + the screen off, with distance, steps and heart rate. Core Location on the + watch for runs without the phone. Haptics for splits and turns. +- A watch face complication (WidgetKit on watchOS) for steps, streak and who's + out. + +Data from the app: steps today and the goal, streak, how many are out in your +city; the chosen route (name, distance, path and turns); the live run if it +started on the phone; the live battle (rival, both scores, end time) and your +emote tray; the club v club fixture (both clubs and scores, squad out, end +time, your part in the average); a mystery challenger (level band, stake, +expiry); the next club run (club, crest, place, time, check in open); and the +saved run (distance, OUT, XP, streak, where the battle stands). Back to the +app: runs recorded on the watch, answers and check ins, which go through the +same engine calls as the phone. + +## 2. Home screen widgets + +What it needs: + +- A WidgetKit extension (`type: "widget"`) with the large and small widgets + from the prototype, and lock screen accessory widgets. +- An App Group (for example `group.com.outhere.app`) shared by the app and the + extension. Widgets can't run the app's JavaScript, so the app writes a small + JSON snapshot there and calls `WidgetCenter.reloadAllTimelines()` when it + changes. iOS limits refreshes, so write on real changes: steps every few + hundred, the battle score, a new day. +- `outhere://` links so a tap opens the right screen. + +Data from the app: steps today and the goal, streak, the battle rival and your +lead, how many are out now, the crew name with this week's km and goal, and +the next event (title, start time, whether you're going). + +## 3. Live Activity and Dynamic Island + +What it needs: + +- ActivityKit. The Live Activity and Dynamic Island views live in the widget + extension above; an `ActivityAttributes` type is shared by the app and the + extension. +- `NSSupportsLiveActivities` set to true in `ios.infoPlist` in `app.json`. +- The app starts the activity when a run starts, updates it every few seconds + and on pause or resume, and ends it on finish. Pause and Finish on the lock + screen are App Intents (`LiveActivityIntent`, iOS 17 and later). +- Runs with the phone locked need background location: `UIBackgroundModes` + with `location`, and the matching permission text. The app only asks for + location while in use today. +- For the battle on the lock screen while the app is closed, updates come by + push (ActivityKit push tokens through APNs), which needs a server. + +Data from the app: running or paused (blue dot or red dot), distance, when the +run started and total paused time (so the system ticks the clock itself), +pace, steps, OUT so far, the route name or goal and the distance left. For the +battle version: rival, both scores and the end time. The club nudge under it is +an ordinary push notification. + +## Suggested order + +Widgets and the App Group first (smallest, and the snapshot is reused), then +the Live Activity in the same extension, then the watch app. Android +equivalents (Glance widgets, a Wear OS app) are not covered here. diff --git a/apps/mobile/__create/DeviceErrorBoundary.ios.tsx b/apps/mobile/__create/DeviceErrorBoundary.ios.tsx deleted file mode 100755 index 778b14ee..00000000 --- a/apps/mobile/__create/DeviceErrorBoundary.ios.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { type ReactNode, useCallback, useEffect, useState } from 'react'; -import { SharedErrorBoundary, Button } from './SharedErrorBoundary'; -import * as Updates from 'expo-updates'; -import { SplashScreen } from 'expo-router/build/exports'; -import { DevSettings, LogBox, Platform, View } from 'react-native'; -import { isErrorLike, serializeError } from 'serialize-error'; -import { reportErrorToRemote } from './report-error-to-remote'; -import { errorFixEmitter, ErrorFixEvents } from '@anythingai/app/utils'; - -type ErrorBoundaryState = { - hasError: boolean; - error: unknown | null; - sentLogs: boolean; -}; - -const DeviceErrorBoundary = ({ - sentLogs, - error, -}: { - sentLogs: boolean; - error: unknown | null; -}) => { - const isAnythingApp = process.env.EXPO_PUBLIC_IS_ANYTHING_APP === 'true'; - useEffect(() => { - SplashScreen.hideAsync().catch(() => { }); - }, []); - const handleFixClick = useCallback(() => { - const seraizliedError = serializeError(error); - const displayableError = isErrorLike(seraizliedError) - ? `${seraizliedError.message}\n\n${seraizliedError.stack}` - : JSON.stringify(seraizliedError, null, 2); - errorFixEmitter.emit(ErrorFixEvents.ERROR_FIX_SUBMITTED, { - error: displayableError, - }); - }, [error]); - const handleReload = useCallback(async () => { - if (Platform.OS === 'web') { - window.location.reload(); - return; - } - - Updates.reloadAsync().catch((error) => { - // no-op, we don't want to show an error here - }); - }, []); - return ( - - - {isAnythingApp && ( - - )} - {!isAnythingApp && ( - - )} - - - ); -}; - -export class DeviceErrorBoundaryWrapper extends React.Component< - { - children: ReactNode; - }, - ErrorBoundaryState -> { - state: ErrorBoundaryState = { - hasError: false, - error: null, - sentLogs: false, - }; - - static getDerivedStateFromError(error: unknown): ErrorBoundaryState { - return { hasError: true, error, sentLogs: false }; - } - - componentDidCatch(error: unknown, errorInfo: React.ErrorInfo): void { - this.setState({ error }); - reportErrorToRemote({ error }) - .then(({ success, error: fetchError }) => { - this.setState({ hasError: true, sentLogs: success }); - }) - .catch((reportError) => { - this.setState({ hasError: true, sentLogs: false }); - }); - } - - render() { - if (this.state.hasError) { - return ; - } - return this.props.children; - } -} diff --git a/apps/mobile/__create/DeviceErrorBoundary.tsx b/apps/mobile/__create/DeviceErrorBoundary.tsx deleted file mode 100755 index 024b8b69..00000000 --- a/apps/mobile/__create/DeviceErrorBoundary.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { type ReactNode, useCallback, useEffect, useState } from 'react'; -import { SharedErrorBoundary, Button } from './SharedErrorBoundary'; -import * as Updates from 'expo-updates'; -import { SplashScreen } from 'expo-router/build/exports'; -import { DevSettings, LogBox, Platform, View } from 'react-native'; -import { serializeError } from 'serialize-error'; -import { reportErrorToRemote } from './report-error-to-remote'; - -type ErrorBoundaryState = { hasError: boolean; error: unknown | null; sentLogs: boolean }; - -const DeviceErrorBoundary = ({ - sentLogs, -}: { - sentLogs: boolean; -}) => { - useEffect(() => { - SplashScreen.hideAsync().catch(() => { }); - }, []); - const handleReload = useCallback(async () => { - if (Platform.OS === 'web') { - window.location.reload(); - return; - } - - Updates.reloadAsync().catch((error) => { - // no-op, we don't want to show an error here - }); - }, []); - return ( - - - - - - ); -}; - -export class DeviceErrorBoundaryWrapper extends React.Component< - { - children: ReactNode; - }, - ErrorBoundaryState -> { - state: ErrorBoundaryState = { hasError: false, error: null, sentLogs: false }; - - static getDerivedStateFromError(error: unknown): ErrorBoundaryState { - return { hasError: true, error, sentLogs: false }; - } - componentDidCatch(error: unknown, errorInfo: React.ErrorInfo): void { - this.setState({ error }); - reportErrorToRemote({ error }) - .then(({ success, error: fetchError }) => { - this.setState({ hasError: true, sentLogs: success }); - }) - .catch((reportError) => { - this.setState({ hasError: true, sentLogs: false }); - }); - } - - render() { - if (this.state.hasError) { - return ; - } - return this.props.children; - } -} \ No newline at end of file diff --git a/apps/mobile/__create/SharedErrorBoundary.tsx b/apps/mobile/__create/SharedErrorBoundary.tsx deleted file mode 100755 index 2ae3333c..00000000 --- a/apps/mobile/__create/SharedErrorBoundary.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import React, { - type ReactNode, - useEffect, - useState, - useCallback, - useRef, -} from 'react'; -import { Animated, Text, View } from 'react-native'; -import { isErrorLike, serializeError } from 'serialize-error'; - -export function SharedErrorBoundary({ - isOpen, - children, - description, -}: { - isOpen: boolean; - children?: ReactNode; - description?: string; -}): React.ReactElement { - const animation = useRef(new Animated.Value(isOpen ? 1 : 0)).current; - const [contentHeight, setContentHeight] = useState(0); - - useEffect(() => { - Animated.timing(animation, { - toValue: isOpen ? 1 : 0, - duration: 250, - useNativeDriver: true, - }).start(); - }, [isOpen, animation]); - - const translateY = animation.interpolate({ - inputRange: [0, 1], - // fallback 100 if height not measured yet so it starts off-screen - outputRange: [Math.max(contentHeight + 34, 100), 0], - }); - - const opacity = animation; - - return ( - - setContentHeight(e.nativeEvent.layout.height)} - style={{ - backgroundColor: '#18191B', - borderRadius: 8, - padding: 16, - maxWidth: 448, - width: '100%', - marginHorizontal: 16, - boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.15)', - }} - > - - - - - ⚠ - - - - - - - - App Error Detected - - - {description ?? - 'It looks like an error occurred while trying to use your app.'} - - - {children} - - - - - ); -} -export function Button({ - color = 'primary', - onPress = () => {}, - children, -}: { - color?: 'primary' | 'secondary'; - onPress?: () => void; - children: string; -}): React.ReactElement { - return ( - - - {children} - - - ); -} - -function InternalErrorBoundary({ - error: errorArg = null, -}: { - error: unknown | null; -}) { - const [isOpen, setIsOpen] = useState(true); - const handleCopyError = useCallback(() => { - const serializedError = serializeError(errorArg); - const text = isErrorLike(serializedError) - ? `${serializedError.message}\n\n${serializedError.stack}` - : JSON.stringify(serializedError, null, 2); - navigator.clipboard.writeText(text); - setIsOpen(false); - }, [errorArg]); - - const postCountRef = useRef(0); - const lastPostTimeRef = useRef(0); - const lastErrorKeyRef = useRef(null); - const MAX_ERROR_POSTS_PER_ERROR = 5; - const THROTTLE_MS = 1000; - - useEffect(() => { - const serialized = serializeError(errorArg); - const errorKey = JSON.stringify(serialized); - - if (errorKey !== lastErrorKeyRef.current) { - lastErrorKeyRef.current = errorKey; - postCountRef.current = 0; - } - - if (postCountRef.current >= MAX_ERROR_POSTS_PER_ERROR) { - return; - } - - const now = Date.now(); - const timeSinceLastPost = now - lastPostTimeRef.current; - - const post = () => { - if (postCountRef.current >= MAX_ERROR_POSTS_PER_ERROR) { - return; - } - postCountRef.current += 1; - lastPostTimeRef.current = Date.now(); - window.parent.postMessage( - { type: 'sandbox:error:detected', error: serialized }, - '*' - ); - }; - - if (timeSinceLastPost < THROTTLE_MS) { - const timer = setTimeout(post, THROTTLE_MS - timeSinceLastPost); - return () => clearTimeout(timer); - } - - post(); - }, [errorArg]); - - function isInIframe() { - try { - return window.parent !== window; - } catch { - return true; - } - } - return ( - - {!isInIframe() && ( - - - - )} - - ); -} - -type ErrorBoundaryState = { hasError: boolean; error: unknown | null }; - -export class ErrorBoundaryWrapper extends React.Component< - { - children: ReactNode; - }, - ErrorBoundaryState -> { - state: ErrorBoundaryState = { hasError: false, error: null }; - - static getDerivedStateFromError(error: unknown): ErrorBoundaryState { - return { hasError: true, error }; - } - - componentDidCatch(error: unknown, info: unknown) { - console.warn(error, info); - } - - render() { - if (this.state.hasError) { - return ; - } - return this.props.children; - } -} diff --git a/apps/mobile/__create/consoleToParent.ts b/apps/mobile/__create/consoleToParent.ts deleted file mode 100755 index dbef01d2..00000000 --- a/apps/mobile/__create/consoleToParent.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Patch console so each call is echoed to the parent window. - * Must be imported first in the app entry point. - */ - -const IGNORE_LIST = [ - /^Running application "main"/, - /props\.pointerEvents is deprecated\. Use style\.pointerEvents/, - /"shadow.*" style props are deprecated\. Use "boxShadow"/, -]; - -function serialize(value: unknown) { - return JSON.stringify(value, (_k, v) => { - if (v instanceof Date) { - return { __t: 'Date', v: v.toISOString() }; - } - if (v instanceof Error) { - return { - __t: 'Error', - v: { name: v.name, message: v.message, stack: v.stack }, - }; - } - return v; - }); -} - -if (typeof window !== 'undefined') { - for (const level of ['log', 'info', 'warn', 'error', 'debug', 'table', 'trace'] as const) { - const orig = console[level]?.bind(console); - console[level] = (...args: unknown[]) => { - orig?.(...args); - // Ignore messages that match the ignore list - if (IGNORE_LIST.some((regex) => typeof args[0] === 'string' && regex.test(args[0]))) { - return; - } - try { - window.parent.postMessage( - { - type: 'sandbox:mobile:console-write', - __expoConsole: true, - level, - args: args.map(serialize), - }, - '*' - ); - } catch { - /* ignore errors so logging never breaks the app */ - } - }; - } -} diff --git a/apps/mobile/__create/handle-resolve-request-error.js b/apps/mobile/__create/handle-resolve-request-error.js deleted file mode 100755 index db7b5ca1..00000000 --- a/apps/mobile/__create/handle-resolve-request-error.js +++ /dev/null @@ -1,63 +0,0 @@ -const crypto = require('node:crypto'); -const fs = require('node:fs'); -const path = require('node:path'); -const { reportErrorToRemote } = require('./report-error-to-remote'); - -const VIRTUAL_ROOT = path.join(__dirname, '../.metro-virtual'); -const VIRTUAL_ROOT_UNRESOLVED = path.join(VIRTUAL_ROOT, 'unresolved'); - -const handleResolveRequestError = ({ error, context, moduleName, platform }) => { - const errorMessage = `Unable to resolve module '${moduleName}' from '${context.originModulePath}'`; - const syntheticError = new Error(errorMessage); - syntheticError.stack = error.stack; - reportErrorToRemote({ error: syntheticError }).catch((reportError) => { - // no-op - }); - if (process.env.NODE_ENV === 'production') throw error; - if (platform === 'android') throw error; - if (!__DEV__ && process.env.EXPO_PUBLIC_CREATE_ENV !== 'DEVELOPMENT') throw error; - - // Build a deterministic virtual file path for this failed request - const key = `${moduleName}|${context.originModulePath}|${platform}`; - const hash = crypto.createHash('sha256').update(key).digest('hex').slice(0, 16); - - fs.mkdirSync(VIRTUAL_ROOT_UNRESOLVED, { recursive: true }); - const vfile = path.join(VIRTUAL_ROOT_UNRESOLVED, `throw-${hash}.js`); - - // Serialize a safe payload for the client - const payload = { - moduleName, - from: context.originModulePath, - platform, - originalMessage: String(error?.message ? error.message : 'Unknown resolve error'), - }; - - const code = [ - '// Auto generated by custom Metro resolver', - '(function(){', - ` var info = ${JSON.stringify(payload)};`, - " var msg = 'Unable to resolve \"' + info.moduleName + '\" from \"' + info.from + '\"';", - " msg += '\\n\\n' + info.originalMessage;", - ' var e = new Error(msg);', - " e.name = 'ModuleResolveError';", - " e.code = 'MODULE_RESOLVE_FAILED';", - ' throw e;', - '})();', - 'export {};', // keep ESM shape harmlessly - '', - ].join('\n'); - - fs.writeFileSync(vfile, code, 'utf8'); - - // Tell Metro to load our thrower as a real source file - return { - filePath: vfile, - type: 'sourceFile', - }; -}; - -module.exports = { - handleResolveRequestError, - VIRTUAL_ROOT, - VIRTUAL_ROOT_UNRESOLVED, -}; diff --git a/apps/mobile/__create/report-error-to-remote.js b/apps/mobile/__create/report-error-to-remote.js deleted file mode 100755 index 6407d81c..00000000 --- a/apps/mobile/__create/report-error-to-remote.js +++ /dev/null @@ -1,39 +0,0 @@ -import { serializeError } from 'serialize-error'; - -export const reportErrorToRemote = async ({ error }) => { - if ( - !process.env.EXPO_PUBLIC_LOGS_ENDPOINT || - !process.env.EXPO_PUBLIC_PROJECT_GROUP_ID || - !process.env.EXPO_PUBLIC_CREATE_TEMP_API_KEY - ) { - console.debug( - 'reportErrorToRemote: Missing environment variables for logging endpoint, project group ID, or API key.', - error - ); - return { success: false }; - } - try { - await fetch(process.env.EXPO_PUBLIC_LOGS_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${process.env.EXPO_PUBLIC_CREATE_TEMP_API_KEY}`, - }, - body: JSON.stringify({ - projectGroupId: process.env.EXPO_PUBLIC_PROJECT_GROUP_ID, - logs: [ - { - message: JSON.stringify(serializeError(error)), - timestamp: new Date().toISOString(), - level: 'error', - source: 'BUILDER', - devServerId: process.env.EXPO_PUBLIC_DEV_SERVER_ID, - }, - ], - }), - }); - } catch (fetchError) { - return { success: false, error: fetchError }; - } - return { success: true }; -}; diff --git a/apps/mobile/__create/reset.css b/apps/mobile/__create/reset.css deleted file mode 100755 index a96d9e4d..00000000 --- a/apps/mobile/__create/reset.css +++ /dev/null @@ -1,34 +0,0 @@ -html, body, #__next { - -webkit-overflow-scrolling: touch; -} -#__next { - display: flex; - flex-direction: column; - height: 100%; -} -html { - scroll-behavior: smooth; - -webkit-text-size-adjust: 100%; -} -body { - /* Allows you to scroll below the viewport; default value is visible */ - overflow-y: auto; - overscroll-behavior-y: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -ms-overflow-style: scrollbar; -} -input:focus, -textarea:focus, -button:focus, -select:focus { - outline: none; /* removes the glow */ - box-shadow: none; /* Chrome fallback */ -} - -/* Firefox also adds padding on some controls */ -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; -} diff --git a/apps/mobile/app.json b/apps/mobile/app.json index 9d060546..b8d21932 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -1,28 +1,37 @@ { "expo": { - "name": "Anything mobile app", + "name": "Outhere", + "slug": "outhere", + "scheme": "outhere", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/images/icon.png", - "userInterfaceStyle": "automatic", + "userInterfaceStyle": "dark", "newArchEnabled": true, "ios": { "supportsTablet": true, "infoPlist": { - "ITSAppUsesNonExemptEncryption": false + "ITSAppUsesNonExemptEncryption": false, + "NSMotionUsageDescription": "Outhere counts your steps to track your daily movement, streaks and battles.", + "NSLocationWhenInUseUsageDescription": "Outhere uses your location to map your runs and walks and to check you in at events.", + "NSCameraUsageDescription": "Outhere uses your camera so you can take a photo for your crew logo.", + "NSPhotoLibraryUsageDescription": "Outhere uses your photos so you can choose a crew logo.", + "NSContactsUsageDescription": "Outhere checks which of your contacts are already on Outhere so you can add them. Nothing is stored." }, - "bundleIdentifier": "xyz.create.CreateExpoEnvironment" + "bundleIdentifier": "com.outhere.app" }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive-icon.png", - "backgroundColor": "#ffffff" + "backgroundColor": "#07090c" }, "permissions": [ - "android.permission.RECORD_AUDIO", - "android.permission.MODIFY_AUDIO_SETTINGS" + "android.permission.ACTIVITY_RECOGNITION", + "android.permission.ACCESS_FINE_LOCATION", + "android.permission.ACCESS_COARSE_LOCATION", + "android.permission.READ_CONTACTS" ], - "package": "xyz.create.CreateExpoEnvironment" + "package": "com.outhere.app" }, "plugins": [ [ @@ -35,8 +44,9 @@ "expo-splash-screen", { "image": "./assets/images/splash-icon.png", - "imageWidth": 200, - "resizeMode": "contain" + "imageWidth": 160, + "resizeMode": "contain", + "backgroundColor": "#07090c" } ], "expo-audio", @@ -58,7 +68,14 @@ "expo-font", "expo-secure-store", "expo-web-browser", - "expo-asset" + "expo-asset", + [ + "expo-image-picker", + { + "photosPermission": "Outhere uses your photos for stories and your profile picture.", + "cameraPermission": "Outhere uses the camera for stories and your profile picture." + } + ] ], "web": { "bundler": "metro", diff --git a/apps/mobile/assets/images/adaptive-icon.png b/apps/mobile/assets/images/adaptive-icon.png index 27d44bd3..9a504a71 100755 Binary files a/apps/mobile/assets/images/adaptive-icon.png and b/apps/mobile/assets/images/adaptive-icon.png differ diff --git a/apps/mobile/assets/images/crew/bars.jpg b/apps/mobile/assets/images/crew/bars.jpg new file mode 100644 index 00000000..de7925f2 Binary files /dev/null and b/apps/mobile/assets/images/crew/bars.jpg differ diff --git a/apps/mobile/assets/images/crew/coastal.jpg b/apps/mobile/assets/images/crew/coastal.jpg new file mode 100644 index 00000000..a95b37fb Binary files /dev/null and b/apps/mobile/assets/images/crew/coastal.jpg differ diff --git a/apps/mobile/assets/images/crew/owl.jpg b/apps/mobile/assets/images/crew/owl.jpg new file mode 100644 index 00000000..e3b6e0cd Binary files /dev/null and b/apps/mobile/assets/images/crew/owl.jpg differ diff --git a/apps/mobile/assets/images/crew/sunlit.jpg b/apps/mobile/assets/images/crew/sunlit.jpg new file mode 100644 index 00000000..6532457e Binary files /dev/null and b/apps/mobile/assets/images/crew/sunlit.jpg differ diff --git a/apps/mobile/assets/images/crew/trail.jpg b/apps/mobile/assets/images/crew/trail.jpg new file mode 100644 index 00000000..2165fe8f Binary files /dev/null and b/apps/mobile/assets/images/crew/trail.jpg differ diff --git a/apps/mobile/assets/images/favicon.png b/apps/mobile/assets/images/favicon.png index 096dd764..4ebc75ad 100755 Binary files a/apps/mobile/assets/images/favicon.png and b/apps/mobile/assets/images/favicon.png differ diff --git a/apps/mobile/assets/images/icon.png b/apps/mobile/assets/images/icon.png index 38fe5b49..c4fa238a 100755 Binary files a/apps/mobile/assets/images/icon.png and b/apps/mobile/assets/images/icon.png differ diff --git a/apps/mobile/assets/images/move/bars.jpg b/apps/mobile/assets/images/move/bars.jpg new file mode 100644 index 00000000..7535239f Binary files /dev/null and b/apps/mobile/assets/images/move/bars.jpg differ diff --git a/apps/mobile/assets/images/move/gpslost.jpg b/apps/mobile/assets/images/move/gpslost.jpg new file mode 100644 index 00000000..19d43587 Binary files /dev/null and b/apps/mobile/assets/images/move/gpslost.jpg differ diff --git a/apps/mobile/assets/images/move/map-blue.jpg b/apps/mobile/assets/images/move/map-blue.jpg new file mode 100644 index 00000000..cbb8d21b Binary files /dev/null and b/apps/mobile/assets/images/move/map-blue.jpg differ diff --git a/apps/mobile/assets/images/move/map-grey.jpg b/apps/mobile/assets/images/move/map-grey.jpg new file mode 100644 index 00000000..9c376175 Binary files /dev/null and b/apps/mobile/assets/images/move/map-grey.jpg differ diff --git a/apps/mobile/assets/images/oh/badge.jpg b/apps/mobile/assets/images/oh/badge.jpg new file mode 100644 index 00000000..249bc28e Binary files /dev/null and b/apps/mobile/assets/images/oh/badge.jpg differ diff --git a/apps/mobile/assets/images/oh/badges/bars.jpg b/apps/mobile/assets/images/oh/badges/bars.jpg new file mode 100644 index 00000000..de7925f2 Binary files /dev/null and b/apps/mobile/assets/images/oh/badges/bars.jpg differ diff --git a/apps/mobile/assets/images/oh/badges/card.jpg b/apps/mobile/assets/images/oh/badges/card.jpg new file mode 100644 index 00000000..b82ac1c0 Binary files /dev/null and b/apps/mobile/assets/images/oh/badges/card.jpg differ diff --git a/apps/mobile/assets/images/oh/badges/owl.jpg b/apps/mobile/assets/images/oh/badges/owl.jpg new file mode 100644 index 00000000..e3b6e0cd Binary files /dev/null and b/apps/mobile/assets/images/oh/badges/owl.jpg differ diff --git a/apps/mobile/assets/images/oh/badges/stack.jpg b/apps/mobile/assets/images/oh/badges/stack.jpg new file mode 100644 index 00000000..9c2ce9d3 Binary files /dev/null and b/apps/mobile/assets/images/oh/badges/stack.jpg differ diff --git a/apps/mobile/assets/images/oh/bars.jpg b/apps/mobile/assets/images/oh/bars.jpg new file mode 100644 index 00000000..8c1f7a62 Binary files /dev/null and b/apps/mobile/assets/images/oh/bars.jpg differ diff --git a/apps/mobile/assets/images/oh/battle.jpg b/apps/mobile/assets/images/oh/battle.jpg new file mode 100644 index 00000000..8066f2a5 Binary files /dev/null and b/apps/mobile/assets/images/oh/battle.jpg differ diff --git a/apps/mobile/assets/images/oh/bell.jpg b/apps/mobile/assets/images/oh/bell.jpg new file mode 100644 index 00000000..33199ae0 Binary files /dev/null and b/apps/mobile/assets/images/oh/bell.jpg differ diff --git a/apps/mobile/assets/images/oh/birmingham.jpg b/apps/mobile/assets/images/oh/birmingham.jpg new file mode 100644 index 00000000..44a827e9 Binary files /dev/null and b/apps/mobile/assets/images/oh/birmingham.jpg differ diff --git a/apps/mobile/assets/images/oh/blackcard.jpg b/apps/mobile/assets/images/oh/blackcard.jpg new file mode 100644 index 00000000..7cf35401 Binary files /dev/null and b/apps/mobile/assets/images/oh/blackcard.jpg differ diff --git a/apps/mobile/assets/images/oh/blackhero.jpg b/apps/mobile/assets/images/oh/blackhero.jpg new file mode 100644 index 00000000..c14d510f Binary files /dev/null and b/apps/mobile/assets/images/oh/blackhero.jpg differ diff --git a/apps/mobile/assets/images/oh/bolt.jpg b/apps/mobile/assets/images/oh/bolt.jpg new file mode 100644 index 00000000..25970be5 Binary files /dev/null and b/apps/mobile/assets/images/oh/bolt.jpg differ diff --git a/apps/mobile/assets/images/oh/cap.jpg b/apps/mobile/assets/images/oh/cap.jpg new file mode 100644 index 00000000..4c4b9da2 Binary files /dev/null and b/apps/mobile/assets/images/oh/cap.jpg differ diff --git a/apps/mobile/assets/images/oh/card.jpg b/apps/mobile/assets/images/oh/card.jpg new file mode 100644 index 00000000..ffe2f13e Binary files /dev/null and b/apps/mobile/assets/images/oh/card.jpg differ diff --git a/apps/mobile/assets/images/oh/challenger.jpg b/apps/mobile/assets/images/oh/challenger.jpg new file mode 100644 index 00000000..2d619a3b Binary files /dev/null and b/apps/mobile/assets/images/oh/challenger.jpg differ diff --git a/apps/mobile/assets/images/oh/chatsnap.jpg b/apps/mobile/assets/images/oh/chatsnap.jpg new file mode 100644 index 00000000..30471c09 Binary files /dev/null and b/apps/mobile/assets/images/oh/chatsnap.jpg differ diff --git a/apps/mobile/assets/images/oh/city.jpg b/apps/mobile/assets/images/oh/city.jpg new file mode 100644 index 00000000..7841a0ae Binary files /dev/null and b/apps/mobile/assets/images/oh/city.jpg differ diff --git a/apps/mobile/assets/images/oh/clubfind.jpg b/apps/mobile/assets/images/oh/clubfind.jpg new file mode 100644 index 00000000..c2e86530 Binary files /dev/null and b/apps/mobile/assets/images/oh/clubfind.jpg differ diff --git a/apps/mobile/assets/images/oh/clubhero.jpg b/apps/mobile/assets/images/oh/clubhero.jpg new file mode 100644 index 00000000..d8d0bca8 Binary files /dev/null and b/apps/mobile/assets/images/oh/clubhero.jpg differ diff --git a/apps/mobile/assets/images/oh/clubhero2.jpg b/apps/mobile/assets/images/oh/clubhero2.jpg new file mode 100644 index 00000000..6245f298 Binary files /dev/null and b/apps/mobile/assets/images/oh/clubhero2.jpg differ diff --git a/apps/mobile/assets/images/oh/clubrun.jpg b/apps/mobile/assets/images/oh/clubrun.jpg new file mode 100644 index 00000000..61a81d88 Binary files /dev/null and b/apps/mobile/assets/images/oh/clubrun.jpg differ diff --git a/apps/mobile/assets/images/oh/coin.jpg b/apps/mobile/assets/images/oh/coin.jpg new file mode 100644 index 00000000..f6dc1661 Binary files /dev/null and b/apps/mobile/assets/images/oh/coin.jpg differ diff --git a/apps/mobile/assets/images/oh/crest.jpg b/apps/mobile/assets/images/oh/crest.jpg new file mode 100644 index 00000000..4fc41bad Binary files /dev/null and b/apps/mobile/assets/images/oh/crest.jpg differ diff --git a/apps/mobile/assets/images/oh/crew.jpg b/apps/mobile/assets/images/oh/crew.jpg new file mode 100644 index 00000000..7d85e83b Binary files /dev/null and b/apps/mobile/assets/images/oh/crew.jpg differ diff --git a/apps/mobile/assets/images/oh/crown.jpg b/apps/mobile/assets/images/oh/crown.jpg new file mode 100644 index 00000000..02212395 Binary files /dev/null and b/apps/mobile/assets/images/oh/crown.jpg differ diff --git a/apps/mobile/assets/images/oh/derby.jpg b/apps/mobile/assets/images/oh/derby.jpg new file mode 100644 index 00000000..a6bf66c1 Binary files /dev/null and b/apps/mobile/assets/images/oh/derby.jpg differ diff --git a/apps/mobile/assets/images/oh/dial.jpg b/apps/mobile/assets/images/oh/dial.jpg new file mode 100644 index 00000000..0845cc05 Binary files /dev/null and b/apps/mobile/assets/images/oh/dial.jpg differ diff --git a/apps/mobile/assets/images/oh/emote-bolt.jpg b/apps/mobile/assets/images/oh/emote-bolt.jpg new file mode 100644 index 00000000..05c0bdc0 Binary files /dev/null and b/apps/mobile/assets/images/oh/emote-bolt.jpg differ diff --git a/apps/mobile/assets/images/oh/emote-crown.jpg b/apps/mobile/assets/images/oh/emote-crown.jpg new file mode 100644 index 00000000..d2fd98a7 Binary files /dev/null and b/apps/mobile/assets/images/oh/emote-crown.jpg differ diff --git a/apps/mobile/assets/images/oh/emote-fist.jpg b/apps/mobile/assets/images/oh/emote-fist.jpg new file mode 100644 index 00000000..d44c4f6d Binary files /dev/null and b/apps/mobile/assets/images/oh/emote-fist.jpg differ diff --git a/apps/mobile/assets/images/oh/emote-flame.jpg b/apps/mobile/assets/images/oh/emote-flame.jpg new file mode 100644 index 00000000..7beb6893 Binary files /dev/null and b/apps/mobile/assets/images/oh/emote-flame.jpg differ diff --git a/apps/mobile/assets/images/oh/emote-snail.jpg b/apps/mobile/assets/images/oh/emote-snail.jpg new file mode 100644 index 00000000..b94aab5e Binary files /dev/null and b/apps/mobile/assets/images/oh/emote-snail.jpg differ diff --git a/apps/mobile/assets/images/oh/emote-watch.jpg b/apps/mobile/assets/images/oh/emote-watch.jpg new file mode 100644 index 00000000..14a8a496 Binary files /dev/null and b/apps/mobile/assets/images/oh/emote-watch.jpg differ diff --git a/apps/mobile/assets/images/oh/emote-wings.jpg b/apps/mobile/assets/images/oh/emote-wings.jpg new file mode 100644 index 00000000..fd2e28bc Binary files /dev/null and b/apps/mobile/assets/images/oh/emote-wings.jpg differ diff --git a/apps/mobile/assets/images/oh/event.jpg b/apps/mobile/assets/images/oh/event.jpg new file mode 100644 index 00000000..b6e2faf8 Binary files /dev/null and b/apps/mobile/assets/images/oh/event.jpg differ diff --git a/apps/mobile/assets/images/oh/fighterA.jpg b/apps/mobile/assets/images/oh/fighterA.jpg new file mode 100644 index 00000000..c3241c2c Binary files /dev/null and b/apps/mobile/assets/images/oh/fighterA.jpg differ diff --git a/apps/mobile/assets/images/oh/fighterB.jpg b/apps/mobile/assets/images/oh/fighterB.jpg new file mode 100644 index 00000000..b04644b4 Binary files /dev/null and b/apps/mobile/assets/images/oh/fighterB.jpg differ diff --git a/apps/mobile/assets/images/oh/flame.jpg b/apps/mobile/assets/images/oh/flame.jpg new file mode 100644 index 00000000..8c86b954 Binary files /dev/null and b/apps/mobile/assets/images/oh/flame.jpg differ diff --git a/apps/mobile/assets/images/oh/founders.jpg b/apps/mobile/assets/images/oh/founders.jpg new file mode 100644 index 00000000..94768580 Binary files /dev/null and b/apps/mobile/assets/images/oh/founders.jpg differ diff --git a/apps/mobile/assets/images/oh/founding.jpg b/apps/mobile/assets/images/oh/founding.jpg new file mode 100644 index 00000000..bf999cb9 Binary files /dev/null and b/apps/mobile/assets/images/oh/founding.jpg differ diff --git a/apps/mobile/assets/images/oh/friends.jpg b/apps/mobile/assets/images/oh/friends.jpg new file mode 100644 index 00000000..1b85ee81 Binary files /dev/null and b/apps/mobile/assets/images/oh/friends.jpg differ diff --git a/apps/mobile/assets/images/oh/gift.jpg b/apps/mobile/assets/images/oh/gift.jpg new file mode 100644 index 00000000..7dd5b280 Binary files /dev/null and b/apps/mobile/assets/images/oh/gift.jpg differ diff --git a/apps/mobile/assets/images/oh/goal11.jpg b/apps/mobile/assets/images/oh/goal11.jpg new file mode 100644 index 00000000..3d4da381 Binary files /dev/null and b/apps/mobile/assets/images/oh/goal11.jpg differ diff --git a/apps/mobile/assets/images/oh/gpslost.jpg b/apps/mobile/assets/images/oh/gpslost.jpg new file mode 100644 index 00000000..92bec3ea Binary files /dev/null and b/apps/mobile/assets/images/oh/gpslost.jpg differ diff --git a/apps/mobile/assets/images/oh/hero.jpg b/apps/mobile/assets/images/oh/hero.jpg new file mode 100644 index 00000000..5b95fd43 Binary files /dev/null and b/apps/mobile/assets/images/oh/hero.jpg differ diff --git a/apps/mobile/assets/images/oh/kemi.jpg b/apps/mobile/assets/images/oh/kemi.jpg new file mode 100644 index 00000000..6507783e Binary files /dev/null and b/apps/mobile/assets/images/oh/kemi.jpg differ diff --git a/apps/mobile/assets/images/oh/kit.jpg b/apps/mobile/assets/images/oh/kit.jpg new file mode 100644 index 00000000..d0736df6 Binary files /dev/null and b/apps/mobile/assets/images/oh/kit.jpg differ diff --git a/apps/mobile/assets/images/oh/laurel.jpg b/apps/mobile/assets/images/oh/laurel.jpg new file mode 100644 index 00000000..0776547c Binary files /dev/null and b/apps/mobile/assets/images/oh/laurel.jpg differ diff --git a/apps/mobile/assets/images/oh/lock.jpg b/apps/mobile/assets/images/oh/lock.jpg new file mode 100644 index 00000000..50dec2ad Binary files /dev/null and b/apps/mobile/assets/images/oh/lock.jpg differ diff --git a/apps/mobile/assets/images/oh/manchester.jpg b/apps/mobile/assets/images/oh/manchester.jpg new file mode 100644 index 00000000..981ac233 Binary files /dev/null and b/apps/mobile/assets/images/oh/manchester.jpg differ diff --git a/apps/mobile/assets/images/oh/map.jpg b/apps/mobile/assets/images/oh/map.jpg new file mode 100644 index 00000000..63391f6c Binary files /dev/null and b/apps/mobile/assets/images/oh/map.jpg differ diff --git a/apps/mobile/assets/images/oh/marsh.jpg b/apps/mobile/assets/images/oh/marsh.jpg new file mode 100644 index 00000000..f8b8dd8c Binary files /dev/null and b/apps/mobile/assets/images/oh/marsh.jpg differ diff --git a/apps/mobile/assets/images/oh/medal.jpg b/apps/mobile/assets/images/oh/medal.jpg new file mode 100644 index 00000000..3842a1f1 Binary files /dev/null and b/apps/mobile/assets/images/oh/medal.jpg differ diff --git a/apps/mobile/assets/images/oh/nav-city.jpg b/apps/mobile/assets/images/oh/nav-city.jpg new file mode 100644 index 00000000..dce532d9 Binary files /dev/null and b/apps/mobile/assets/images/oh/nav-city.jpg differ diff --git a/apps/mobile/assets/images/oh/pacers.jpg b/apps/mobile/assets/images/oh/pacers.jpg new file mode 100644 index 00000000..eb41ab24 Binary files /dev/null and b/apps/mobile/assets/images/oh/pacers.jpg differ diff --git a/apps/mobile/assets/images/oh/parcel.jpg b/apps/mobile/assets/images/oh/parcel.jpg new file mode 100644 index 00000000..c9f99e83 Binary files /dev/null and b/apps/mobile/assets/images/oh/parcel.jpg differ diff --git a/apps/mobile/assets/images/oh/phone.jpg b/apps/mobile/assets/images/oh/phone.jpg new file mode 100644 index 00000000..6447da5e Binary files /dev/null and b/apps/mobile/assets/images/oh/phone.jpg differ diff --git a/apps/mobile/assets/images/oh/pin.jpg b/apps/mobile/assets/images/oh/pin.jpg new file mode 100644 index 00000000..b25de9ba Binary files /dev/null and b/apps/mobile/assets/images/oh/pin.jpg differ diff --git a/apps/mobile/assets/images/oh/podium.jpg b/apps/mobile/assets/images/oh/podium.jpg new file mode 100644 index 00000000..7ebbee5c Binary files /dev/null and b/apps/mobile/assets/images/oh/podium.jpg differ diff --git a/apps/mobile/assets/images/oh/procard.jpg b/apps/mobile/assets/images/oh/procard.jpg new file mode 100644 index 00000000..37bf7848 Binary files /dev/null and b/apps/mobile/assets/images/oh/procard.jpg differ diff --git a/apps/mobile/assets/images/oh/rival-amara.jpg b/apps/mobile/assets/images/oh/rival-amara.jpg new file mode 100644 index 00000000..9bbb8cdc Binary files /dev/null and b/apps/mobile/assets/images/oh/rival-amara.jpg differ diff --git a/apps/mobile/assets/images/oh/rival-hannah.jpg b/apps/mobile/assets/images/oh/rival-hannah.jpg new file mode 100644 index 00000000..3ceedb4b Binary files /dev/null and b/apps/mobile/assets/images/oh/rival-hannah.jpg differ diff --git a/apps/mobile/assets/images/oh/rival-mei.jpg b/apps/mobile/assets/images/oh/rival-mei.jpg new file mode 100644 index 00000000..0d6b10be Binary files /dev/null and b/apps/mobile/assets/images/oh/rival-mei.jpg differ diff --git a/apps/mobile/assets/images/oh/rival-tobi.jpg b/apps/mobile/assets/images/oh/rival-tobi.jpg new file mode 100644 index 00000000..30d9c7c3 Binary files /dev/null and b/apps/mobile/assets/images/oh/rival-tobi.jpg differ diff --git a/apps/mobile/assets/images/oh/rival-zain.jpg b/apps/mobile/assets/images/oh/rival-zain.jpg new file mode 100644 index 00000000..a2199177 Binary files /dev/null and b/apps/mobile/assets/images/oh/rival-zain.jpg differ diff --git a/apps/mobile/assets/images/oh/selfie.jpg b/apps/mobile/assets/images/oh/selfie.jpg new file mode 100644 index 00000000..df37fc83 Binary files /dev/null and b/apps/mobile/assets/images/oh/selfie.jpg differ diff --git a/apps/mobile/assets/images/oh/shield.jpg b/apps/mobile/assets/images/oh/shield.jpg new file mode 100644 index 00000000..9bfed7b0 Binary files /dev/null and b/apps/mobile/assets/images/oh/shield.jpg differ diff --git a/apps/mobile/assets/images/oh/shoe.jpg b/apps/mobile/assets/images/oh/shoe.jpg new file mode 100644 index 00000000..fb130b7d Binary files /dev/null and b/apps/mobile/assets/images/oh/shoe.jpg differ diff --git a/apps/mobile/assets/images/oh/shoebox.jpg b/apps/mobile/assets/images/oh/shoebox.jpg new file mode 100644 index 00000000..8fe8fafa Binary files /dev/null and b/apps/mobile/assets/images/oh/shoebox.jpg differ diff --git a/apps/mobile/assets/images/oh/signin.jpg b/apps/mobile/assets/images/oh/signin.jpg new file mode 100644 index 00000000..3a392f14 Binary files /dev/null and b/apps/mobile/assets/images/oh/signin.jpg differ diff --git a/apps/mobile/assets/images/oh/stack.jpg b/apps/mobile/assets/images/oh/stack.jpg new file mode 100644 index 00000000..59ab22d8 Binary files /dev/null and b/apps/mobile/assets/images/oh/stack.jpg differ diff --git a/apps/mobile/assets/images/oh/stickers.jpg b/apps/mobile/assets/images/oh/stickers.jpg new file mode 100644 index 00000000..97db9495 Binary files /dev/null and b/apps/mobile/assets/images/oh/stickers.jpg differ diff --git a/apps/mobile/assets/images/oh/teal.jpg b/apps/mobile/assets/images/oh/teal.jpg new file mode 100644 index 00000000..dc864300 Binary files /dev/null and b/apps/mobile/assets/images/oh/teal.jpg differ diff --git a/apps/mobile/assets/images/oh/tealjacket.jpg b/apps/mobile/assets/images/oh/tealjacket.jpg new file mode 100644 index 00000000..73e90439 Binary files /dev/null and b/apps/mobile/assets/images/oh/tealjacket.jpg differ diff --git a/apps/mobile/assets/images/oh/tee.jpg b/apps/mobile/assets/images/oh/tee.jpg new file mode 100644 index 00000000..2d1984af Binary files /dev/null and b/apps/mobile/assets/images/oh/tee.jpg differ diff --git a/apps/mobile/assets/images/oh/ticket.jpg b/apps/mobile/assets/images/oh/ticket.jpg new file mode 100644 index 00000000..43db2330 Binary files /dev/null and b/apps/mobile/assets/images/oh/ticket.jpg differ diff --git a/apps/mobile/assets/images/oh/token.jpg b/apps/mobile/assets/images/oh/token.jpg new file mode 100644 index 00000000..1e417502 Binary files /dev/null and b/apps/mobile/assets/images/oh/token.jpg differ diff --git a/apps/mobile/assets/images/oh/trail.jpg b/apps/mobile/assets/images/oh/trail.jpg new file mode 100644 index 00000000..0aa4305b Binary files /dev/null and b/apps/mobile/assets/images/oh/trail.jpg differ diff --git a/apps/mobile/assets/images/oh/trophy.jpg b/apps/mobile/assets/images/oh/trophy.jpg new file mode 100644 index 00000000..6c6b292e Binary files /dev/null and b/apps/mobile/assets/images/oh/trophy.jpg differ diff --git a/apps/mobile/assets/images/oh/ukmap.jpg b/apps/mobile/assets/images/oh/ukmap.jpg new file mode 100644 index 00000000..ddb6da30 Binary files /dev/null and b/apps/mobile/assets/images/oh/ukmap.jpg differ diff --git a/apps/mobile/assets/images/oh/verified.jpg b/apps/mobile/assets/images/oh/verified.jpg new file mode 100644 index 00000000..3cb0cc01 Binary files /dev/null and b/apps/mobile/assets/images/oh/verified.jpg differ diff --git a/apps/mobile/assets/images/oh/walk.jpg b/apps/mobile/assets/images/oh/walk.jpg new file mode 100644 index 00000000..3aad79c0 Binary files /dev/null and b/apps/mobile/assets/images/oh/walk.jpg differ diff --git a/apps/mobile/assets/images/oh/watch.jpg b/apps/mobile/assets/images/oh/watch.jpg new file mode 100644 index 00000000..83066303 Binary files /dev/null and b/apps/mobile/assets/images/oh/watch.jpg differ diff --git a/apps/mobile/assets/images/oh/welcome11.jpg b/apps/mobile/assets/images/oh/welcome11.jpg new file mode 100644 index 00000000..ef78aa06 Binary files /dev/null and b/apps/mobile/assets/images/oh/welcome11.jpg differ diff --git a/apps/mobile/assets/images/pack/sample-respect-clap.jpg b/apps/mobile/assets/images/pack/sample-respect-clap.jpg new file mode 100644 index 00000000..c471af1b Binary files /dev/null and b/apps/mobile/assets/images/pack/sample-respect-clap.jpg differ diff --git a/apps/mobile/assets/images/pack/sample-respect-fist.jpg b/apps/mobile/assets/images/pack/sample-respect-fist.jpg new file mode 100644 index 00000000..02c79f49 Binary files /dev/null and b/apps/mobile/assets/images/pack/sample-respect-fist.jpg differ diff --git a/apps/mobile/assets/images/pack/sample-respect-heart.jpg b/apps/mobile/assets/images/pack/sample-respect-heart.jpg new file mode 100644 index 00000000..3517b311 Binary files /dev/null and b/apps/mobile/assets/images/pack/sample-respect-heart.jpg differ diff --git a/apps/mobile/assets/images/pack/sample-respect.jpg b/apps/mobile/assets/images/pack/sample-respect.jpg new file mode 100644 index 00000000..1583a22a Binary files /dev/null and b/apps/mobile/assets/images/pack/sample-respect.jpg differ diff --git a/apps/mobile/assets/images/pack/sample-rooftop.jpg b/apps/mobile/assets/images/pack/sample-rooftop.jpg new file mode 100644 index 00000000..fa0cff34 Binary files /dev/null and b/apps/mobile/assets/images/pack/sample-rooftop.jpg differ diff --git a/apps/mobile/assets/images/pack/sample-stride.jpg b/apps/mobile/assets/images/pack/sample-stride.jpg new file mode 100644 index 00000000..e9f3b1a1 Binary files /dev/null and b/apps/mobile/assets/images/pack/sample-stride.jpg differ diff --git a/apps/mobile/assets/images/pack/sample-win.jpg b/apps/mobile/assets/images/pack/sample-win.jpg new file mode 100644 index 00000000..28f6e5fd Binary files /dev/null and b/apps/mobile/assets/images/pack/sample-win.jpg differ diff --git a/apps/mobile/assets/images/pack/selfie-ref.jpg b/apps/mobile/assets/images/pack/selfie-ref.jpg new file mode 100644 index 00000000..402bdaee Binary files /dev/null and b/apps/mobile/assets/images/pack/selfie-ref.jpg differ diff --git a/apps/mobile/assets/images/pack/silhouette-female.jpg b/apps/mobile/assets/images/pack/silhouette-female.jpg new file mode 100644 index 00000000..ff691172 Binary files /dev/null and b/apps/mobile/assets/images/pack/silhouette-female.jpg differ diff --git a/apps/mobile/assets/images/pack/silhouette-male.jpg b/apps/mobile/assets/images/pack/silhouette-male.jpg new file mode 100644 index 00000000..f445e317 Binary files /dev/null and b/apps/mobile/assets/images/pack/silhouette-male.jpg differ diff --git a/apps/mobile/src/__create/placeholder.svg b/apps/mobile/assets/images/placeholder.svg old mode 100755 new mode 100644 similarity index 100% rename from apps/mobile/src/__create/placeholder.svg rename to apps/mobile/assets/images/placeholder.svg diff --git a/apps/mobile/assets/images/splash-icon.png b/apps/mobile/assets/images/splash-icon.png index 27d44bd3..9713a583 100755 Binary files a/apps/mobile/assets/images/splash-icon.png and b/apps/mobile/assets/images/splash-icon.png differ diff --git a/apps/mobile/dist-ios-check/_expo/static/js/ios/index-cbd7b3bb6d5c97673ea1d8e0162d3a3a.hbc b/apps/mobile/dist-ios-check/_expo/static/js/ios/index-cbd7b3bb6d5c97673ea1d8e0162d3a3a.hbc deleted file mode 100644 index 4502217f..00000000 Binary files a/apps/mobile/dist-ios-check/_expo/static/js/ios/index-cbd7b3bb6d5c97673ea1d8e0162d3a3a.hbc and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/017bc6ba3fc25503e5eb5e53826d48a8 b/apps/mobile/dist-ios-check/assets/017bc6ba3fc25503e5eb5e53826d48a8 deleted file mode 100644 index f81adec0..00000000 Binary files a/apps/mobile/dist-ios-check/assets/017bc6ba3fc25503e5eb5e53826d48a8 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/0747a1317bbe9c6fc340b889ef8ab3ae b/apps/mobile/dist-ios-check/assets/0747a1317bbe9c6fc340b889ef8ab3ae deleted file mode 100644 index 34396d47..00000000 Binary files a/apps/mobile/dist-ios-check/assets/0747a1317bbe9c6fc340b889ef8ab3ae and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/0a328cd9c1afd0afe8e3b1ec5165b1b4 b/apps/mobile/dist-ios-check/assets/0a328cd9c1afd0afe8e3b1ec5165b1b4 deleted file mode 100644 index 7c1fd849..00000000 Binary files a/apps/mobile/dist-ios-check/assets/0a328cd9c1afd0afe8e3b1ec5165b1b4 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/0ea69b5077e7c4696db85dbcba75b0e1 b/apps/mobile/dist-ios-check/assets/0ea69b5077e7c4696db85dbcba75b0e1 deleted file mode 100644 index 2320fd74..00000000 Binary files a/apps/mobile/dist-ios-check/assets/0ea69b5077e7c4696db85dbcba75b0e1 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/1190ab078c57159f4245a328118fcd9a b/apps/mobile/dist-ios-check/assets/1190ab078c57159f4245a328118fcd9a deleted file mode 100644 index 12981d86..00000000 Binary files a/apps/mobile/dist-ios-check/assets/1190ab078c57159f4245a328118fcd9a and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/140c53a7643ea949007aa9a282153849 b/apps/mobile/dist-ios-check/assets/140c53a7643ea949007aa9a282153849 deleted file mode 100644 index 6868f7bb..00000000 Binary files a/apps/mobile/dist-ios-check/assets/140c53a7643ea949007aa9a282153849 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/166afe3a6c4c45803490e21b596041bf b/apps/mobile/dist-ios-check/assets/166afe3a6c4c45803490e21b596041bf deleted file mode 100644 index 606d7064..00000000 Binary files a/apps/mobile/dist-ios-check/assets/166afe3a6c4c45803490e21b596041bf and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/1681f34aaca71b8dfb70756bca331eb2 b/apps/mobile/dist-ios-check/assets/1681f34aaca71b8dfb70756bca331eb2 deleted file mode 100644 index e2b5fbb0..00000000 Binary files a/apps/mobile/dist-ios-check/assets/1681f34aaca71b8dfb70756bca331eb2 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/19eeb73b9593a38f8e9f418337fc7d10 b/apps/mobile/dist-ios-check/assets/19eeb73b9593a38f8e9f418337fc7d10 deleted file mode 100644 index f8441bc4..00000000 Binary files a/apps/mobile/dist-ios-check/assets/19eeb73b9593a38f8e9f418337fc7d10 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/1f77739ca9ff2188b539c36f30ffa2be b/apps/mobile/dist-ios-check/assets/1f77739ca9ff2188b539c36f30ffa2be deleted file mode 100644 index d1ac9ba1..00000000 Binary files a/apps/mobile/dist-ios-check/assets/1f77739ca9ff2188b539c36f30ffa2be and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/20e71bdf79e3a97bf55fd9e164041578 b/apps/mobile/dist-ios-check/assets/20e71bdf79e3a97bf55fd9e164041578 deleted file mode 100644 index ca8d666e..00000000 Binary files a/apps/mobile/dist-ios-check/assets/20e71bdf79e3a97bf55fd9e164041578 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/28cb2ca20c6bc7d40ef0804a2b053509 b/apps/mobile/dist-ios-check/assets/28cb2ca20c6bc7d40ef0804a2b053509 deleted file mode 100644 index eb4f127c..00000000 Binary files a/apps/mobile/dist-ios-check/assets/28cb2ca20c6bc7d40ef0804a2b053509 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/2d0a9133e39524f138be6d4db9f4851f b/apps/mobile/dist-ios-check/assets/2d0a9133e39524f138be6d4db9f4851f deleted file mode 100644 index b26634e5..00000000 Binary files a/apps/mobile/dist-ios-check/assets/2d0a9133e39524f138be6d4db9f4851f and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/31765c9a79280d99d4e13f87fee923ff b/apps/mobile/dist-ios-check/assets/31765c9a79280d99d4e13f87fee923ff deleted file mode 100644 index 59c5735d..00000000 --- a/apps/mobile/dist-ios-check/assets/31765c9a79280d99d4e13f87fee923ff +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/apps/mobile/dist-ios-check/assets/31b5ffea3daddc69dd01a1f3d6cf63c5 b/apps/mobile/dist-ios-check/assets/31b5ffea3daddc69dd01a1f3d6cf63c5 deleted file mode 100644 index 76d91cb9..00000000 Binary files a/apps/mobile/dist-ios-check/assets/31b5ffea3daddc69dd01a1f3d6cf63c5 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/32459ccc3954e6b3b04d878e569436b8 b/apps/mobile/dist-ios-check/assets/32459ccc3954e6b3b04d878e569436b8 deleted file mode 100644 index 46160776..00000000 Binary files a/apps/mobile/dist-ios-check/assets/32459ccc3954e6b3b04d878e569436b8 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/370dd5af19f8364907b6e2c41f45dbbf b/apps/mobile/dist-ios-check/assets/370dd5af19f8364907b6e2c41f45dbbf deleted file mode 100644 index 7f9b53c1..00000000 Binary files a/apps/mobile/dist-ios-check/assets/370dd5af19f8364907b6e2c41f45dbbf and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/3b89dd103490708d19a95adcae52210e b/apps/mobile/dist-ios-check/assets/3b89dd103490708d19a95adcae52210e deleted file mode 100644 index fc567cd2..00000000 Binary files a/apps/mobile/dist-ios-check/assets/3b89dd103490708d19a95adcae52210e and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/3cd68ccdb8938e3711da2e8831b85493 b/apps/mobile/dist-ios-check/assets/3cd68ccdb8938e3711da2e8831b85493 deleted file mode 100644 index d9af7b68..00000000 Binary files a/apps/mobile/dist-ios-check/assets/3cd68ccdb8938e3711da2e8831b85493 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/3f78af31cca60105799838a1a7a59fbd b/apps/mobile/dist-ios-check/assets/3f78af31cca60105799838a1a7a59fbd deleted file mode 100644 index 53c6483a..00000000 Binary files a/apps/mobile/dist-ios-check/assets/3f78af31cca60105799838a1a7a59fbd and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/3fc49f4d75a5a92eea5f3b1f9a8b12e4 b/apps/mobile/dist-ios-check/assets/3fc49f4d75a5a92eea5f3b1f9a8b12e4 deleted file mode 100644 index b589a4a9..00000000 Binary files a/apps/mobile/dist-ios-check/assets/3fc49f4d75a5a92eea5f3b1f9a8b12e4 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/412dd9275b6b48ad28f5e3d81bb1f626 b/apps/mobile/dist-ios-check/assets/412dd9275b6b48ad28f5e3d81bb1f626 deleted file mode 100644 index 8e3297fc..00000000 Binary files a/apps/mobile/dist-ios-check/assets/412dd9275b6b48ad28f5e3d81bb1f626 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/4e85bc9ebe07e0340c9c4fc2f6c38908 b/apps/mobile/dist-ios-check/assets/4e85bc9ebe07e0340c9c4fc2f6c38908 deleted file mode 100644 index 9d09b0fe..00000000 Binary files a/apps/mobile/dist-ios-check/assets/4e85bc9ebe07e0340c9c4fc2f6c38908 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/56c8d80832e37783f12c05db7c8849e2 b/apps/mobile/dist-ios-check/assets/56c8d80832e37783f12c05db7c8849e2 deleted file mode 100644 index 08362f34..00000000 Binary files a/apps/mobile/dist-ios-check/assets/56c8d80832e37783f12c05db7c8849e2 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/605ed7926cf39a2ad5ec2d1f9d391d3d b/apps/mobile/dist-ios-check/assets/605ed7926cf39a2ad5ec2d1f9d391d3d deleted file mode 100644 index f33e8162..00000000 Binary files a/apps/mobile/dist-ios-check/assets/605ed7926cf39a2ad5ec2d1f9d391d3d and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/61ca7e64b7d605716c57706cef640b9a b/apps/mobile/dist-ios-check/assets/61ca7e64b7d605716c57706cef640b9a deleted file mode 100644 index 01a7b636..00000000 Binary files a/apps/mobile/dist-ios-check/assets/61ca7e64b7d605716c57706cef640b9a and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/6a5a5ec704600ad5cf355bcd746719de b/apps/mobile/dist-ios-check/assets/6a5a5ec704600ad5cf355bcd746719de deleted file mode 100644 index 9695442f..00000000 Binary files a/apps/mobile/dist-ios-check/assets/6a5a5ec704600ad5cf355bcd746719de and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/6e435534bd35da5fef04168860a9b8fa b/apps/mobile/dist-ios-check/assets/6e435534bd35da5fef04168860a9b8fa deleted file mode 100644 index bba7dcf6..00000000 Binary files a/apps/mobile/dist-ios-check/assets/6e435534bd35da5fef04168860a9b8fa and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/7811e294b4c9f572643e835b85f9f696 b/apps/mobile/dist-ios-check/assets/7811e294b4c9f572643e835b85f9f696 deleted file mode 100644 index 3c6728c5..00000000 Binary files a/apps/mobile/dist-ios-check/assets/7811e294b4c9f572643e835b85f9f696 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/78c625386b4d0690b421eb0fc78f7b9c b/apps/mobile/dist-ios-check/assets/78c625386b4d0690b421eb0fc78f7b9c deleted file mode 100644 index b4f5127f..00000000 Binary files a/apps/mobile/dist-ios-check/assets/78c625386b4d0690b421eb0fc78f7b9c and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/7d40544b395c5949f4646f5e150fe020 b/apps/mobile/dist-ios-check/assets/7d40544b395c5949f4646f5e150fe020 deleted file mode 100644 index 8ed2ec42..00000000 Binary files a/apps/mobile/dist-ios-check/assets/7d40544b395c5949f4646f5e150fe020 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/7e5438b17fb356848b9a56b511361fdf b/apps/mobile/dist-ios-check/assets/7e5438b17fb356848b9a56b511361fdf deleted file mode 100644 index be4a4c1c..00000000 Binary files a/apps/mobile/dist-ios-check/assets/7e5438b17fb356848b9a56b511361fdf and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/871378c6eab492a3e689a9385dc45a12 b/apps/mobile/dist-ios-check/assets/871378c6eab492a3e689a9385dc45a12 deleted file mode 100644 index 713a1a9f..00000000 Binary files a/apps/mobile/dist-ios-check/assets/871378c6eab492a3e689a9385dc45a12 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/90d899b9019774650c9bdf0685729e5d b/apps/mobile/dist-ios-check/assets/90d899b9019774650c9bdf0685729e5d deleted file mode 100644 index 952d89d0..00000000 Binary files a/apps/mobile/dist-ios-check/assets/90d899b9019774650c9bdf0685729e5d and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/9c7a702c163c6e28d619603bbca7a036 b/apps/mobile/dist-ios-check/assets/9c7a702c163c6e28d619603bbca7a036 deleted file mode 100644 index 2fdd5a3e..00000000 Binary files a/apps/mobile/dist-ios-check/assets/9c7a702c163c6e28d619603bbca7a036 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/a132ecc4ba5c1517ff83c0fb321bc7fc b/apps/mobile/dist-ios-check/assets/a132ecc4ba5c1517ff83c0fb321bc7fc deleted file mode 100644 index 63de0e30..00000000 Binary files a/apps/mobile/dist-ios-check/assets/a132ecc4ba5c1517ff83c0fb321bc7fc and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/a8b98e516b7e5a7770f0bfec609f2d8e b/apps/mobile/dist-ios-check/assets/a8b98e516b7e5a7770f0bfec609f2d8e deleted file mode 100644 index 368b33d0..00000000 Binary files a/apps/mobile/dist-ios-check/assets/a8b98e516b7e5a7770f0bfec609f2d8e and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/ab19f4cbc543357183a20571f68380a3 b/apps/mobile/dist-ios-check/assets/ab19f4cbc543357183a20571f68380a3 deleted file mode 100644 index 828e6322..00000000 Binary files a/apps/mobile/dist-ios-check/assets/ab19f4cbc543357183a20571f68380a3 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/adec7d6f310bc577f05e8fe06a5daccf b/apps/mobile/dist-ios-check/assets/adec7d6f310bc577f05e8fe06a5daccf deleted file mode 100644 index e7e2ecfa..00000000 Binary files a/apps/mobile/dist-ios-check/assets/adec7d6f310bc577f05e8fe06a5daccf and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/aff2c65b39a296d4f7e96d0f58169170 b/apps/mobile/dist-ios-check/assets/aff2c65b39a296d4f7e96d0f58169170 deleted file mode 100644 index c852b6ba..00000000 Binary files a/apps/mobile/dist-ios-check/assets/aff2c65b39a296d4f7e96d0f58169170 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/b06871f281fee6b241d60582ae9369b9 b/apps/mobile/dist-ios-check/assets/b06871f281fee6b241d60582ae9369b9 deleted file mode 100644 index 35acda2f..00000000 Binary files a/apps/mobile/dist-ios-check/assets/b06871f281fee6b241d60582ae9369b9 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/b456537461dae2b56a15c3c88d85d343 b/apps/mobile/dist-ios-check/assets/b456537461dae2b56a15c3c88d85d343 deleted file mode 100644 index 42d79fd1..00000000 Binary files a/apps/mobile/dist-ios-check/assets/b456537461dae2b56a15c3c88d85d343 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/b49ae8ab2dbccb02c4d11caaacf09eab b/apps/mobile/dist-ios-check/assets/b49ae8ab2dbccb02c4d11caaacf09eab deleted file mode 100644 index 96e2e81a..00000000 Binary files a/apps/mobile/dist-ios-check/assets/b49ae8ab2dbccb02c4d11caaacf09eab and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/b49d35b0fe9bfb9a3f6f248a17852e5c b/apps/mobile/dist-ios-check/assets/b49d35b0fe9bfb9a3f6f248a17852e5c deleted file mode 100644 index 5c7116a7..00000000 Binary files a/apps/mobile/dist-ios-check/assets/b49d35b0fe9bfb9a3f6f248a17852e5c and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/b4eb097d35f44ed943676fd56f6bdc51 b/apps/mobile/dist-ios-check/assets/b4eb097d35f44ed943676fd56f6bdc51 deleted file mode 100644 index f9d33a73..00000000 Binary files a/apps/mobile/dist-ios-check/assets/b4eb097d35f44ed943676fd56f6bdc51 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/ca4b48e04dc1ce10bfbddb262c8b835f b/apps/mobile/dist-ios-check/assets/ca4b48e04dc1ce10bfbddb262c8b835f deleted file mode 100644 index 38e346c0..00000000 Binary files a/apps/mobile/dist-ios-check/assets/ca4b48e04dc1ce10bfbddb262c8b835f and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/d1ea1496f9057eb392d5bbf3732a61b7 b/apps/mobile/dist-ios-check/assets/d1ea1496f9057eb392d5bbf3732a61b7 deleted file mode 100644 index 13dfd103..00000000 Binary files a/apps/mobile/dist-ios-check/assets/d1ea1496f9057eb392d5bbf3732a61b7 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/d2285965fe34b05465047401b8595dd0 b/apps/mobile/dist-ios-check/assets/d2285965fe34b05465047401b8595dd0 deleted file mode 100644 index 6ecb6868..00000000 Binary files a/apps/mobile/dist-ios-check/assets/d2285965fe34b05465047401b8595dd0 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/d62ddc38b69aff346c20a28774933d6a b/apps/mobile/dist-ios-check/assets/d62ddc38b69aff346c20a28774933d6a deleted file mode 100644 index 4a8433c8..00000000 Binary files a/apps/mobile/dist-ios-check/assets/d62ddc38b69aff346c20a28774933d6a and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/d84e297c3b3e49a614248143d53e40ca b/apps/mobile/dist-ios-check/assets/d84e297c3b3e49a614248143d53e40ca deleted file mode 100644 index 4e3e31b8..00000000 Binary files a/apps/mobile/dist-ios-check/assets/d84e297c3b3e49a614248143d53e40ca and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/d8b800c443b8972542883e0b9de2bdc6 b/apps/mobile/dist-ios-check/assets/d8b800c443b8972542883e0b9de2bdc6 deleted file mode 100644 index 2a5bc8e7..00000000 Binary files a/apps/mobile/dist-ios-check/assets/d8b800c443b8972542883e0b9de2bdc6 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/d8e7601e3df962f83c62371ac14964d8 b/apps/mobile/dist-ios-check/assets/d8e7601e3df962f83c62371ac14964d8 deleted file mode 100644 index 5f44c546..00000000 Binary files a/apps/mobile/dist-ios-check/assets/d8e7601e3df962f83c62371ac14964d8 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/dad2fa9f4394a630f0f9a0d6dabd44bc b/apps/mobile/dist-ios-check/assets/dad2fa9f4394a630f0f9a0d6dabd44bc deleted file mode 100644 index 9dfd92ed..00000000 Binary files a/apps/mobile/dist-ios-check/assets/dad2fa9f4394a630f0f9a0d6dabd44bc and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/df525526b33c51f294cb7dbb0661f4ab b/apps/mobile/dist-ios-check/assets/df525526b33c51f294cb7dbb0661f4ab deleted file mode 100644 index 707e8b85..00000000 Binary files a/apps/mobile/dist-ios-check/assets/df525526b33c51f294cb7dbb0661f4ab and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/e20945d7c929279ef7a6f1db184a4470 b/apps/mobile/dist-ios-check/assets/e20945d7c929279ef7a6f1db184a4470 deleted file mode 100644 index 6cce217d..00000000 Binary files a/apps/mobile/dist-ios-check/assets/e20945d7c929279ef7a6f1db184a4470 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/e20c2fe873237189a9b2bc2768c022e0 b/apps/mobile/dist-ios-check/assets/e20c2fe873237189a9b2bc2768c022e0 deleted file mode 100644 index a4b37ab7..00000000 Binary files a/apps/mobile/dist-ios-check/assets/e20c2fe873237189a9b2bc2768c022e0 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/e8cb3fb92dc534dcf53d8564624f0b36 b/apps/mobile/dist-ios-check/assets/e8cb3fb92dc534dcf53d8564624f0b36 deleted file mode 100644 index 5e4c785f..00000000 Binary files a/apps/mobile/dist-ios-check/assets/e8cb3fb92dc534dcf53d8564624f0b36 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/eaf316bee4eabbb6ff76a664e853b4ba b/apps/mobile/dist-ios-check/assets/eaf316bee4eabbb6ff76a664e853b4ba deleted file mode 100644 index bbe1e7c5..00000000 Binary files a/apps/mobile/dist-ios-check/assets/eaf316bee4eabbb6ff76a664e853b4ba and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/f3a81967828232c893d547162e922764 b/apps/mobile/dist-ios-check/assets/f3a81967828232c893d547162e922764 deleted file mode 100644 index a22662ef..00000000 Binary files a/apps/mobile/dist-ios-check/assets/f3a81967828232c893d547162e922764 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/f4a3a28bbf9b5d6c54568042e5cf773e b/apps/mobile/dist-ios-check/assets/f4a3a28bbf9b5d6c54568042e5cf773e deleted file mode 100644 index a8efddf5..00000000 Binary files a/apps/mobile/dist-ios-check/assets/f4a3a28bbf9b5d6c54568042e5cf773e and /dev/null differ diff --git a/apps/mobile/dist-ios-check/assets/f86a4012c286f0a10bc6e9f91b7b9f05 b/apps/mobile/dist-ios-check/assets/f86a4012c286f0a10bc6e9f91b7b9f05 deleted file mode 100644 index 1584de20..00000000 Binary files a/apps/mobile/dist-ios-check/assets/f86a4012c286f0a10bc6e9f91b7b9f05 and /dev/null differ diff --git a/apps/mobile/dist-ios-check/canvaskit.wasm b/apps/mobile/dist-ios-check/canvaskit.wasm deleted file mode 100755 index 6b12b963..00000000 Binary files a/apps/mobile/dist-ios-check/canvaskit.wasm and /dev/null differ diff --git a/apps/mobile/dist-ios-check/metadata.json b/apps/mobile/dist-ios-check/metadata.json deleted file mode 100644 index 99a9aa47..00000000 --- a/apps/mobile/dist-ios-check/metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"version":0,"bundler":"metro","fileMetadata":{"ios":{"bundle":"_expo/static/js/ios/index-cbd7b3bb6d5c97673ea1d8e0162d3a3a.hbc","assets":[{"path":"assets/7d40544b395c5949f4646f5e150fe020","ext":"png"},{"path":"assets/a132ecc4ba5c1517ff83c0fb321bc7fc","ext":"png"},{"path":"assets/0ea69b5077e7c4696db85dbcba75b0e1","ext":"png"},{"path":"assets/2d0a9133e39524f138be6d4db9f4851f","ext":"png"},{"path":"assets/0a328cd9c1afd0afe8e3b1ec5165b1b4","ext":"png"},{"path":"assets/3cd68ccdb8938e3711da2e8831b85493","ext":"png"},{"path":"assets/61ca7e64b7d605716c57706cef640b9a","ext":"png"},{"path":"assets/aff2c65b39a296d4f7e96d0f58169170","ext":"png"},{"path":"assets/d8e7601e3df962f83c62371ac14964d8","ext":"png"},{"path":"assets/3cd68ccdb8938e3711da2e8831b85493","ext":"png"},{"path":"assets/0747a1317bbe9c6fc340b889ef8ab3ae","ext":"png"},{"path":"assets/d84e297c3b3e49a614248143d53e40ca","ext":"png"},{"path":"assets/1190ab078c57159f4245a328118fcd9a","ext":"png"},{"path":"assets/78c625386b4d0690b421eb0fc78f7b9c","ext":"png"},{"path":"assets/0747a1317bbe9c6fc340b889ef8ab3ae","ext":"png"},{"path":"assets/dad2fa9f4394a630f0f9a0d6dabd44bc","ext":"png"},{"path":"assets/f3a81967828232c893d547162e922764","ext":"png"},{"path":"assets/d62ddc38b69aff346c20a28774933d6a","ext":"png"},{"path":"assets/f3a81967828232c893d547162e922764","ext":"png"},{"path":"assets/d1ea1496f9057eb392d5bbf3732a61b7","ext":"png"},{"path":"assets/19eeb73b9593a38f8e9f418337fc7d10","ext":"png"},{"path":"assets/ab19f4cbc543357183a20571f68380a3","ext":"png"},{"path":"assets/d8b800c443b8972542883e0b9de2bdc6","ext":"png"},{"path":"assets/412dd9275b6b48ad28f5e3d81bb1f626","ext":"png"},{"path":"assets/017bc6ba3fc25503e5eb5e53826d48a8","ext":"png"},{"path":"assets/20e71bdf79e3a97bf55fd9e164041578","ext":"png"},{"path":"assets/3f78af31cca60105799838a1a7a59fbd","ext":"ttf"},{"path":"assets/31b5ffea3daddc69dd01a1f3d6cf63c5","ext":"ttf"},{"path":"assets/140c53a7643ea949007aa9a282153849","ext":"ttf"},{"path":"assets/ca4b48e04dc1ce10bfbddb262c8b835f","ext":"ttf"},{"path":"assets/b49ae8ab2dbccb02c4d11caaacf09eab","ext":"ttf"},{"path":"assets/b06871f281fee6b241d60582ae9369b9","ext":"ttf"},{"path":"assets/1f77739ca9ff2188b539c36f30ffa2be","ext":"ttf"},{"path":"assets/605ed7926cf39a2ad5ec2d1f9d391d3d","ext":"ttf"},{"path":"assets/3b89dd103490708d19a95adcae52210e","ext":"ttf"},{"path":"assets/370dd5af19f8364907b6e2c41f45dbbf","ext":"ttf"},{"path":"assets/adec7d6f310bc577f05e8fe06a5daccf","ext":"ttf"},{"path":"assets/56c8d80832e37783f12c05db7c8849e2","ext":"ttf"},{"path":"assets/e20945d7c929279ef7a6f1db184a4470","ext":"ttf"},{"path":"assets/b4eb097d35f44ed943676fd56f6bdc51","ext":"ttf"},{"path":"assets/6e435534bd35da5fef04168860a9b8fa","ext":"ttf"},{"path":"assets/4e85bc9ebe07e0340c9c4fc2f6c38908","ext":"ttf"},{"path":"assets/871378c6eab492a3e689a9385dc45a12","ext":"ttf"},{"path":"assets/d2285965fe34b05465047401b8595dd0","ext":"ttf"},{"path":"assets/1681f34aaca71b8dfb70756bca331eb2","ext":"ttf"},{"path":"assets/31765c9a79280d99d4e13f87fee923ff","ext":"svg"},{"path":"assets/a8b98e516b7e5a7770f0bfec609f2d8e","ext":"png"},{"path":"assets/3fc49f4d75a5a92eea5f3b1f9a8b12e4","ext":"png"},{"path":"assets/6a5a5ec704600ad5cf355bcd746719de","ext":"png"},{"path":"assets/e20c2fe873237189a9b2bc2768c022e0","ext":"png"},{"path":"assets/7811e294b4c9f572643e835b85f9f696","ext":"png"},{"path":"assets/eaf316bee4eabbb6ff76a664e853b4ba","ext":"png"},{"path":"assets/166afe3a6c4c45803490e21b596041bf","ext":"png"},{"path":"assets/b49d35b0fe9bfb9a3f6f248a17852e5c","ext":"png"},{"path":"assets/28cb2ca20c6bc7d40ef0804a2b053509","ext":"png"},{"path":"assets/f86a4012c286f0a10bc6e9f91b7b9f05","ext":"png"},{"path":"assets/e8cb3fb92dc534dcf53d8564624f0b36","ext":"png"},{"path":"assets/9c7a702c163c6e28d619603bbca7a036","ext":"png"},{"path":"assets/f4a3a28bbf9b5d6c54568042e5cf773e","ext":"ttf"},{"path":"assets/32459ccc3954e6b3b04d878e569436b8","ext":"ttf"},{"path":"assets/df525526b33c51f294cb7dbb0661f4ab","ext":"png"},{"path":"assets/90d899b9019774650c9bdf0685729e5d","ext":"png"},{"path":"assets/7e5438b17fb356848b9a56b511361fdf","ext":"png"},{"path":"assets/b456537461dae2b56a15c3c88d85d343","ext":"png"},{"path":"assets/df525526b33c51f294cb7dbb0661f4ab","ext":"png"}]}}} \ No newline at end of file diff --git a/apps/mobile/global.css b/apps/mobile/global.css index 83666a3e..34a9f7e7 100755 --- a/apps/mobile/global.css +++ b/apps/mobile/global.css @@ -3,3 +3,11 @@ body { font-family: "Inter", sans-serif !important; } + +/* Skia draws into a that flexes to fill its view. A canvas keeps a + minimum height of 150px (its default size) as a flex item, so a small + BlendImage (under 150px) grew past its box. Let it shrink to the view. */ +canvas { + min-height: 0; + min-width: 0; +} diff --git a/apps/mobile/index.tsx b/apps/mobile/index.tsx index 3a5e3427..616780e5 100755 --- a/apps/mobile/index.tsx +++ b/apps/mobile/index.tsx @@ -1,35 +1,8 @@ -import ExceptionsManager from 'react-native/Libraries/Core/ExceptionsManager'; - -if (__DEV__) { - ExceptionsManager.handleException = (error, isFatal) => { - // no-op - }; -} - import 'react-native-url-polyfill/auto'; -import './src/__create/polyfills'; global.Buffer = require('buffer').Buffer; import '@expo/metro-runtime'; -import { AppRegistry, LogBox } from 'react-native'; -import { DeviceErrorBoundaryWrapper } from './__create/DeviceErrorBoundary'; -import AnythingMenu from './src/__create/anything-menu'; import { renderRootComponent } from 'expo-router/build/renderRootComponent'; -import App from './entrypoint' - +import App from './entrypoint'; -if (__DEV__ || process.env.EXPO_PUBLIC_CREATE_ENV === 'DEVELOPMENT') { - LogBox.ignoreAllLogs(); - LogBox.uninstall(); - AppRegistry.setWrapperComponentProvider(() => ({ children }) => { - return ( - <> - - {children} - - - - ); - }); -} renderRootComponent(App); diff --git a/apps/mobile/index.web.tsx b/apps/mobile/index.web.tsx index b6e7749b..a97a545a 100755 --- a/apps/mobile/index.web.tsx +++ b/apps/mobile/index.web.tsx @@ -1,127 +1,18 @@ import '@expo/metro-runtime'; -import { toPng } from 'html-to-image'; -import { serializeError } from 'serialize-error'; -import React, { useEffect } from 'react'; -import './__create/consoleToParent'; -import { renderRootComponent } from 'expo-router/build/renderRootComponent'; - import { LoadSkiaWeb } from '@shopify/react-native-skia/lib/module/web'; -import './__create/reset.css'; -import CreateApp from './App'; -async function inlineGoogleFonts(): Promise { - // Find all elements that load Google Fonts CSS - const links = Array.from(document.querySelectorAll( - 'link[rel="stylesheet"][href*="fonts.googleapis.com"]' - )); - - for (const link of links) { - try { - const href = link.href; - const res = await fetch(href); - let cssText = await res.text(); - - // Ensure font URLs are absolute - cssText = cssText.replace(/url\(([^)]+)\)/g, (match, url) => { - const clean = url.replace(/["']/g, ""); - if (clean.startsWith("http")) { - return `url(${clean})`; - } - return `url(${new URL(clean, href).toString()})`; - }); - - // Inject - - -
- - - - diff --git a/apps/mobile/types/ambient-modules.d.ts b/apps/mobile/types/ambient-modules.d.ts deleted file mode 100644 index 06ad5d69..00000000 --- a/apps/mobile/types/ambient-modules.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -declare module "@anythingai/app/utils" { - export const errorFixEmitter: { - emit: (event: string, payload?: unknown) => void; - }; - - export const ErrorFixEvents: { - ERROR_FIX_SUBMITTED: string; - }; -} - -declare module "@anythingai/app/screens/launcher-menu" { - import type { ComponentType } from "react"; - const LauncherMenuContainer: ComponentType; - export default LauncherMenuContainer; -} - -declare module "react-native-web-refresh-control" { - import type { ComponentType } from "react"; - import type { RefreshControlProps } from "react-native"; - - export const RefreshControl: ComponentType; - const DefaultExport: ComponentType; - export default DefaultExport; -} diff --git a/apps/mobile/types/modules.d.ts b/apps/mobile/types/modules.d.ts new file mode 100644 index 00000000..87a1be79 --- /dev/null +++ b/apps/mobile/types/modules.d.ts @@ -0,0 +1 @@ +declare module 'react-native-web-refresh-control'; diff --git a/apps/mobile/vercel.json b/apps/mobile/vercel.json new file mode 100644 index 00000000..e7563d96 --- /dev/null +++ b/apps/mobile/vercel.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "installCommand": "cd ../.. && corepack enable && pnpm install --frozen-lockfile", + "buildCommand": "npx expo export --platform web --output-dir dist", + "outputDirectory": "dist", + "framework": null, + "cleanUrls": true, + "rewrites": [{ "source": "/((?!_expo/|assets/|canvaskit.wasm|favicon.ico).*)", "destination": "/index.html" }], + "headers": [ + { "source": "/canvaskit.wasm", "headers": [{ "key": "Content-Type", "value": "application/wasm" }, { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }] }, + { "source": "/_expo/(.*)", "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }] } + ] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 3261b714..876bbdd0 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,8 +1,12 @@ import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; +// The workspace installs a flat node_modules for the phone app, which hoists +// its older React to the root. Resolve React from this package and bundle it +// into the server build, so the site and react-dom always share one copy. export default defineConfig({ plugins: [react()], + resolve: { dedupe: ['react', 'react-dom'] }, build: { target: 'es2020', assetsInlineLimit: 0 }, - ssr: { noExternal: [/^@radix-ui\//, '@outhere/core'] }, + ssr: { noExternal: [/^@radix-ui\//, '@outhere/core', 'react', 'react-dom'] }, }); diff --git a/backend/.env.example b/backend/.env.example index 881853bf..70a6d886 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,5 +1,9 @@ -# OUT HERE Backend Configuration -# Copy this file to .env and fill in the values. +# OutHere backend configuration. Copy to .env and fill in. +# Storage is picked in this order (see SUPABASE.md): +# 1. SUPABASE_DB_URL set -> Supabase Postgres +# 2. MONGO_URL set -> MongoDB +# 3. neither -> in-memory mock (local dev only, data is lost on restart) -SUPABASE_URL=your_supabase_url_here -SUPABASE_KEY=your_supabase_service_role_key_here +SUPABASE_DB_URL=postgresql://postgres:@db..supabase.co:5432/postgres +# MONGO_URL=mongodb://localhost:27017 +DB_NAME=outere_db diff --git a/backend/requirements.txt b/backend/requirements.txt index 850b0d7f..d4fd2b6c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,29 +1,12 @@ fastapi==0.110.1 uvicorn==0.25.0 -boto3>=1.34.129 -requests-oauthlib>=2.0.0 -cryptography>=42.0.8 -python-dotenv>=1.0.1 -pymongo==4.5.0 pydantic>=2.6.4 -email-validator>=2.2.0 -pyjwt>=2.10.1 -bcrypt==4.1.3 -passlib>=1.7.4 -tzdata>=2024.2 +python-dotenv>=1.0.1 +psycopg[binary]>=3.2.0 motor==3.3.1 +pymongo==4.5.0 mongomock_motor==0.0.36 -psycopg[binary]>=3.2.0 -pytest>=8.0.0 -black>=24.1.1 -isort>=5.13.2 -flake8>=7.0.0 -mypy>=1.8.0 -python-jose>=3.3.0 + +# Tests (backend/tests) requests>=2.31.0 -pandas>=2.2.0 -numpy>=1.26.0 -python-multipart>=0.0.9 -jq>=1.6.0 -typer>=0.9.0 -emergentintegrations==0.1.0 +pytest>=8.0.0 diff --git a/scenario_test.py b/backend/tests/api_scenario_test.py similarity index 99% rename from scenario_test.py rename to backend/tests/api_scenario_test.py index fbb808a4..c1c19b56 100644 --- a/scenario_test.py +++ b/backend/tests/api_scenario_test.py @@ -13,7 +13,7 @@ from dotenv import load_dotenv # Load environment variables -load_dotenv('/app/frontend/.env') +load_dotenv() # Get backend URL from frontend environment BACKEND_URL = os.getenv('EXPO_PUBLIC_BACKEND_URL', 'http://localhost:8001') diff --git a/backend_test.py b/backend/tests/api_smoke_test.py similarity index 99% rename from backend_test.py rename to backend/tests/api_smoke_test.py index 69338077..f13c8845 100644 --- a/backend_test.py +++ b/backend/tests/api_smoke_test.py @@ -13,7 +13,7 @@ from dotenv import load_dotenv # Load environment variables -load_dotenv('/app/frontend/.env') +load_dotenv() # Get backend URL from frontend environment BACKEND_URL = os.getenv('EXPO_PUBLIC_BACKEND_URL', 'http://localhost:8001') diff --git a/backend_stress_test.py b/backend/tests/api_stress_test.py similarity index 99% rename from backend_stress_test.py rename to backend/tests/api_stress_test.py index 926acc1b..b40fd3c1 100644 --- a/backend_stress_test.py +++ b/backend/tests/api_stress_test.py @@ -15,7 +15,7 @@ import threading # Load environment variables -load_dotenv('/app/frontend/.env') +load_dotenv() # Get backend URL from frontend environment BACKEND_URL = os.getenv('EXPO_PUBLIC_BACKEND_URL', 'http://localhost:8001') diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index f610ec0d..00000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,39 +0,0 @@ -# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files - -# dependencies -node_modules/ - -# Expo -.expo/ -dist/ -web-build/ -expo-env.d.ts - -# Native -.kotlin/ -*.orig.* -*.jks -*.p8 -*.p12 -*.key -*.mobileprovision - -# Metro -.metro-health-check* - -# debug -npm-debug.* -yarn-debug.* -yarn-error.* - -# macOS -.DS_Store -*.pem - -# local env files -.env*.local - -# typescript -*.tsbuildinfo - -app-example diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 48dd63ff..00000000 --- a/frontend/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Welcome to your Expo app 👋 - -This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). - -## Get started - -1. Install dependencies - - ```bash - npm install - ``` - -2. Start the app - - ```bash - npx expo start - ``` - -In the output, you'll find options to open the app in a - -- [development build](https://docs.expo.dev/develop/development-builds/introduction/) -- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) -- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) -- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo - -You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). - -## Get a fresh project - -When you're ready, run: - -```bash -npm run reset-project -``` - -This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. - -## Learn more - -To learn more about developing your project with Expo, look at the following resources: - -- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). -- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. - -## Join the community - -Join our community of developers creating universal apps. - -- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. -- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. diff --git a/frontend/app.json b/frontend/app.json deleted file mode 100644 index 35af9594..00000000 --- a/frontend/app.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "expo": { - "name": "OUT 'ERE", - "slug": "outere", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/images/icon.png", - "scheme": "outere", - "userInterfaceStyle": "dark", - "newArchEnabled": true, - "ios": { - "supportsTablet": true, - "infoPlist": { - "NSMotionUsageDescription": "Track your steps and movement", - "NSLocationWhenInUseUsageDescription": "Show local leaderboards near you" - } - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/images/adaptive-icon.png", - "backgroundColor": "#0A0A0A" - }, - "edgeToEdgeEnabled": true, - "permissions": [ - "android.permission.ACTIVITY_RECOGNITION", - "android.permission.ACCESS_FINE_LOCATION", - "android.permission.ACCESS_COARSE_LOCATION" - ] - }, - "web": { - "bundler": "metro", - "output": "static", - "favicon": "./assets/images/favicon.png" - }, - "plugins": [ - "expo-router", - [ - "expo-splash-screen", - { - "image": "./assets/images/splash-icon.png", - "imageWidth": 200, - "resizeMode": "contain", - "backgroundColor": "#0A0A0A" - } - ], - [ - "expo-sensors", - { - "motionPermission": "Track your steps to compete on leaderboards" - } - ], - [ - "expo-location", - { - "locationAlwaysAndWhenInUsePermission": "Show local leaderboards near you" - } - ] - ], - "experiments": { - "typedRoutes": true - } - } -} diff --git a/frontend/app/(tabs)/_layout.tsx b/frontend/app/(tabs)/_layout.tsx deleted file mode 100644 index 670d66ad..00000000 --- a/frontend/app/(tabs)/_layout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useEffect } from 'react'; -import { Tabs } from 'expo-router'; -import { House, Users, Trophy, BarChart3, UserRound } from 'lucide-react-native'; -import { useUserStore } from '../../src/store/userStore'; -import { useThemeStore } from '../../src/store/themeStore'; -import { CurvedBottomTabs } from '../../src/components/base/curved-bottom-tabs'; - -export default function TabLayout() { - const { initializeDevice } = useUserStore(); - const { colors } = useThemeStore(); - - useEffect(() => { - initializeDevice(); - }, [initializeDevice]); - - return ( - ( - - )} - screenOptions={{ - headerShown: false, - }} - > - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - - ); -} diff --git a/frontend/app/(tabs)/groups.tsx b/frontend/app/(tabs)/groups.tsx deleted file mode 100644 index e4414516..00000000 --- a/frontend/app/(tabs)/groups.tsx +++ /dev/null @@ -1,356 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - Pressable, - RefreshControl, - TextInput, - Alert, - Modal, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { Check, ChevronRight, LogIn, Plus, UsersRound } from 'lucide-react-native'; -import { useRouter } from 'expo-router'; -import { FONTS, SPACING, BORDER_RADIUS, AVATAR_COLORS } from '../../src/constants/theme'; -import { useUserStore } from '../../src/store/userStore'; -import { useThemeStore } from '../../src/store/themeStore'; -import { groupsAPI } from '../../src/services/api'; - -interface Group { - id: string; - name: string; - description: string; - members: string[]; - avatar_color: string; - invite_code: string; - creator_device_id: string; -} - -export default function GroupsScreen() { - const router = useRouter(); - const { colors } = useThemeStore(); - const { deviceId } = useUserStore(); - const [groups, setGroups] = useState([]); - const [refreshing, setRefreshing] = useState(false); - const [showCreateModal, setShowCreateModal] = useState(false); - const [showJoinModal, setShowJoinModal] = useState(false); - const [newGroupName, setNewGroupName] = useState(''); - const [newGroupDesc, setNewGroupDesc] = useState(''); - const [selectedColor, setSelectedColor] = useState(AVATAR_COLORS[0]); - const [inviteCode, setInviteCode] = useState(''); - - const loadGroups = useCallback(async () => { - if (!deviceId) return; - try { - const data = await groupsAPI.getUserGroups(deviceId); - setGroups(data); - } catch (error) { - console.error('Error loading groups:', error); - } - }, [deviceId]); - - useEffect(() => { - loadGroups(); - }, [loadGroups]); - - const onRefresh = useCallback(async () => { - setRefreshing(true); - await loadGroups(); - setRefreshing(false); - }, [loadGroups]); - - const handleCreateGroup = async () => { - if (!newGroupName.trim() || !deviceId) { - Alert.alert('Error', 'Please enter a group name'); - return; - } - try { - const newGroup = await groupsAPI.create({ - name: newGroupName.trim(), - description: newGroupDesc.trim(), - creator_device_id: deviceId, - avatar_color: selectedColor, - }); - setGroups([...groups, newGroup]); - setShowCreateModal(false); - setNewGroupName(''); - setNewGroupDesc(''); - Alert.alert('Success', `Group created! Invite code: ${newGroup.invite_code}`); - } catch (error) { - console.error('Error creating group:', error); - Alert.alert('Error', 'Failed to create group'); - } - }; - - const handleJoinGroup = async () => { - if (!inviteCode.trim() || !deviceId) { - Alert.alert('Error', 'Please enter an invite code'); - return; - } - try { - const result = await groupsAPI.joinByCode(inviteCode.trim(), deviceId); - if (result.group) { - setGroups([...groups.filter(g => g.id !== result.group.id), result.group]); - } - setShowJoinModal(false); - setInviteCode(''); - Alert.alert('Success', result.message); - } catch (error: any) { - Alert.alert('Error', error.response?.data?.detail || 'Invalid invite code'); - } - }; - - const navigateToGroup = (group: Group) => { - router.push(`/group/${group.id}`); - }; - - return ( - - {/* Header */} - - SQUADS - - setShowJoinModal(true)} - > - - - setShowCreateModal(true)} - > - - - - - - - } - > - {groups.length === 0 ? ( - - - No Squads Yet - - Create a squad or join one with an invite code - - - setShowCreateModal(true)} - > - - Create Squad - - setShowJoinModal(true)} - > - - Join Squad - - - - ) : ( - groups.map((group) => ( - navigateToGroup(group)} - > - - {group.name.charAt(0).toUpperCase()} - - - {group.name} - - {group.members?.length || 0} members - - - - - )) - )} - - - {/* Create Group Modal */} - - - - Create Squad - - SQUAD NAME - - - DESCRIPTION (OPTIONAL) - - - SQUAD COLOR - - {AVATAR_COLORS.map((color) => ( - setSelectedColor(color)} - > - {selectedColor === color && } - - ))} - - - - setShowCreateModal(false)} - > - Cancel - - - Create - - - - - - - {/* Join Group Modal */} - - - - Join Squad - - INVITE CODE - setInviteCode(text.toUpperCase())} - placeholder="Enter 8-digit code" - placeholderTextColor={colors.textMuted} - autoCapitalize="characters" - maxLength={8} - /> - - - setShowJoinModal(false)} - > - Cancel - - - Join - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { flex: 1 }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: SPACING.md, - }, - title: { fontSize: FONTS.xxl, fontFamily: FONTS.black, fontWeight: '800', letterSpacing: 1, lineHeight: FONTS.xxl + 5 }, - headerButtons: { flexDirection: 'row', gap: SPACING.sm }, - headerButton: { - width: 40, - height: 40, - borderRadius: 20, - alignItems: 'center', - justifyContent: 'center', - }, - scrollView: { flex: 1 }, - scrollContent: { padding: SPACING.md, paddingTop: 0 }, - emptyContainer: { alignItems: 'center', paddingVertical: SPACING.xxl }, - emptyTitle: { fontSize: FONTS.xl, fontFamily: FONTS.bold, fontWeight: '700', marginTop: SPACING.md }, - emptySubtitle: { fontSize: FONTS.md, fontFamily: FONTS.regular, marginTop: SPACING.sm, textAlign: 'center', lineHeight: FONTS.md + 5 }, - emptyButtons: { flexDirection: 'row', gap: SPACING.md, marginTop: SPACING.lg }, - emptyButton: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.sm, - paddingHorizontal: SPACING.lg, - paddingVertical: SPACING.md, - borderRadius: BORDER_RADIUS.md, - }, - emptyButtonText: { fontSize: FONTS.md, fontFamily: FONTS.bold, fontWeight: '600' }, - groupCard: { - flexDirection: 'row', - alignItems: 'center', - padding: SPACING.md, - borderRadius: BORDER_RADIUS.md, - marginBottom: SPACING.sm, - borderWidth: 0.8, - }, - groupAvatar: { - width: 50, - height: 50, - borderRadius: 25, - alignItems: 'center', - justifyContent: 'center', - }, - groupAvatarText: { fontSize: FONTS.xl, fontWeight: '800', color: '#FFFFFF' }, - groupInfo: { flex: 1, marginLeft: SPACING.md }, - groupName: { fontSize: FONTS.md, fontFamily: FONTS.bold, fontWeight: '600', lineHeight: FONTS.md + 4 }, - groupMembers: { fontSize: FONTS.sm, fontFamily: FONTS.regular, marginTop: 2 }, - modalOverlay: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.8)', - justifyContent: 'center', - padding: SPACING.md, - }, - modalContent: { borderRadius: BORDER_RADIUS.lg, padding: SPACING.lg }, - modalTitle: { fontSize: FONTS.xl, fontFamily: FONTS.bold, fontWeight: '700', marginBottom: SPACING.lg, textAlign: 'center', lineHeight: FONTS.xl + 4 }, - inputLabel: { fontSize: FONTS.xs, fontFamily: FONTS.bold, fontWeight: '600', letterSpacing: 0.6, marginBottom: SPACING.xs }, - textInput: { - borderRadius: BORDER_RADIUS.md, - padding: SPACING.md + 1, - fontSize: FONTS.md, - marginBottom: SPACING.md, - borderWidth: 0, - }, - codeInput: { textAlign: 'center', fontSize: FONTS.xl, letterSpacing: 4 }, - colorPicker: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING.sm, marginBottom: SPACING.lg }, - colorOption: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' }, - colorOptionSelected: { borderWidth: 2, borderColor: '#FFFFFF' }, - modalButtons: { flexDirection: 'row', gap: SPACING.md }, - modalButton: { flex: 1, padding: SPACING.md, borderRadius: BORDER_RADIUS.md, alignItems: 'center' }, - modalButtonText: { fontSize: FONTS.md, fontFamily: FONTS.bold, fontWeight: '600' }, -}); diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx deleted file mode 100644 index 2615870e..00000000 --- a/frontend/app/(tabs)/index.tsx +++ /dev/null @@ -1,554 +0,0 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - Pressable, - RefreshControl, - Platform, - Alert, - TextInput, - KeyboardAvoidingView, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { AtSign, ArrowRight, Plus, Flame, TrendingUp, Navigation, Flag } from 'lucide-react-native'; -import { FONTS, SPACING, BORDER_RADIUS } from '../../src/constants/theme'; -import { useUserStore } from '../../src/store/userStore'; -import { useThemeStore } from '../../src/store/themeStore'; -import { usePedometer } from '../../src/hooks/usePedometer'; -import { StepCounter } from '../../src/components/StepCounter'; -import { StreakBadge } from '../../src/components/StreakBadge'; -import { OutsideNowBanner } from '../../src/components/OutsideNowBanner'; -import { userAPI, stepsAPI, challengesAPI } from '../../src/services/api'; - -export default function HomeScreen() { - const { colors } = useThemeStore(); - const { - user, - deviceId, - setUser, - todayStats, - updateTodayStats, - isOnboarded, - setOnboarded, - } = useUserStore(); - - const { - currentSteps, - distance, - isTracking, - startTracking, - simulateSteps, - syncSteps, - } = usePedometer(); - - const [refreshing, setRefreshing] = useState(false); - const [challenges, setChallenges] = useState([]); - const [showOnboarding, setShowOnboarding] = useState(false); - const [username, setUsername] = useState(''); - - // Load user data - const loadUserData = useCallback(async () => { - if (!deviceId) return; - - try { - const userData = await userAPI.get(deviceId); - setUser(userData); - setOnboarded(true); - - // Load today's steps - const todayData = await stepsAPI.getToday(deviceId); - updateTodayStats(todayData); - - // Load challenges - const challengeData = await challengesAPI.getAll(userData.city); - setChallenges(challengeData); - } catch (error: any) { - if (error.response?.status === 404) { - setShowOnboarding(true); - } - console.log('User not found, showing onboarding'); - } - }, [deviceId, setUser, setOnboarded, updateTodayStats]); - - useEffect(() => { - if (deviceId && !isOnboarded) { - loadUserData(); - } else if (deviceId && isOnboarded) { - loadUserData(); - } - }, [deviceId, isOnboarded, loadUserData]); - - // Start tracking when user is loaded - useEffect(() => { - if (user && !isTracking) { - startTracking(); - } - }, [user, isTracking, startTracking]); - - const onRefresh = useCallback(async () => { - setRefreshing(true); - await loadUserData(); - await syncSteps(); - setRefreshing(false); - }, [loadUserData, syncSteps]); - - const handleCreateUser = async () => { - if (!deviceId || !username.trim()) { - Alert.alert('Error', 'Please enter a username'); - return; - } - - try { - const newUser = await userAPI.create({ - device_id: deviceId, - username: username.trim(), - city: 'London', - borough: 'Central', - }); - setUser(newUser); - setOnboarded(true); - setShowOnboarding(false); - } catch (error) { - console.error('Error creating user:', error); - Alert.alert('Error', 'Failed to create user'); - } - }; - - // Simulate steps for demo (web only) - const handleSimulateSteps = () => { - const randomSteps = Math.floor(Math.random() * 500) + 100; - simulateSteps(randomSteps); - }; - - // Onboarding screen - if (showOnboarding || !isOnboarded) { - return ( - - - - - OUT 'ERE - WE OUTSIDE. - - - - Welcome to the movement - - Track your steps, compete with your city, stay outside. - - - - CHOOSE YOUR TAG - - - - - - - - LET'S GO - - - - - - - ); - } - - return ( - - - } - showsVerticalScrollIndicator={false} - > - {/* Header */} - - - WE OUTSIDE - @{user?.username || 'Loading...'} - - - - - {/* Outside Now Banner */} - - - {/* Step Counter */} - - - - - {/* Tracking Status */} - - - - - {isTracking ? 'Tracking Active' : 'Tracking Paused'} - - - - {Platform.OS === 'web' && ( - - - Add Steps (Demo) - - )} - - - {/* Quick Stats */} - - - - - {user?.outside_score?.toLocaleString() || 0} - - Outside Score - - - - - - {user?.total_steps?.toLocaleString() || 0} - - Total Steps - - - - - - {user?.total_distance?.toFixed(1) || 0} - - KM Total - - - - {/* Challenges Preview */} - - - Active Challenges - - See All - - - - {challenges.slice(0, 2).map((challenge, index) => ( - - - - - {challenge.title} - - {challenge.description} - - - - - +{challenge.reward_points} - pts - - - ))} - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - padding: SPACING.md, - paddingBottom: SPACING.xxl, - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: SPACING.lg, - }, - greeting: { - fontSize: FONTS.xs, - fontFamily: FONTS.bold, - fontWeight: '700', - letterSpacing: 1.5, - }, - username: { - fontSize: FONTS.xxl, - fontFamily: FONTS.black, - fontWeight: '800', - marginTop: 2, - lineHeight: FONTS.xxl + 6, - }, - stepCounterContainer: { - alignItems: 'center', - marginVertical: SPACING.lg, - }, - trackingStatus: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingVertical: SPACING.md, - }, - statusIndicator: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.sm, - }, - statusDot: { - width: 8, - height: 8, - borderRadius: 4, - }, - statusText: { - fontSize: FONTS.sm, - fontFamily: FONTS.regular, - lineHeight: FONTS.sm + 4, - }, - simulateButton: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - paddingHorizontal: SPACING.md + 1, - paddingVertical: SPACING.sm + 1, - borderRadius: BORDER_RADIUS.md, - borderWidth: 0, - }, - simulateText: { - fontSize: FONTS.sm, - fontFamily: FONTS.bold, - fontWeight: '600', - }, - quickStats: { - flexDirection: 'row', - borderRadius: BORDER_RADIUS.lg, - padding: SPACING.lg, - marginVertical: SPACING.md, - borderWidth: 0.8, - }, - quickStatItem: { - flex: 1, - alignItems: 'center', - }, - quickStatValue: { - fontSize: FONTS.xl, - fontFamily: FONTS.black, - fontWeight: '800', - marginTop: SPACING.sm, - lineHeight: FONTS.xl + 5, - }, - quickStatLabel: { - fontSize: FONTS.xs, - fontFamily: FONTS.regular, - marginTop: 2, - letterSpacing: 0.3, - }, - quickStatDivider: { - width: 1, - marginHorizontal: SPACING.md, - }, - challengesSection: { - marginTop: SPACING.lg, - }, - sectionHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: SPACING.md, - }, - sectionTitle: { - fontSize: FONTS.lg, - fontFamily: FONTS.bold, - fontWeight: '700', - lineHeight: FONTS.lg + 5, - }, - seeAllText: { - fontSize: FONTS.sm, - fontFamily: FONTS.bold, - fontWeight: '600', - }, - challengeCard: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - borderRadius: BORDER_RADIUS.md, - padding: SPACING.md, - marginBottom: SPACING.sm, - borderWidth: 0.8, - }, - challengeInfo: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - gap: SPACING.md, - }, - challengeTextContainer: { - flex: 1, - }, - challengeTitle: { - fontSize: FONTS.md, - fontFamily: FONTS.bold, - fontWeight: '600', - lineHeight: FONTS.md + 4, - }, - challengeDescription: { - fontSize: FONTS.sm, - fontFamily: FONTS.regular, - marginTop: 2, - lineHeight: FONTS.sm + 4, - }, - challengeReward: { - alignItems: 'center', - }, - rewardPoints: { - fontSize: FONTS.lg, - fontFamily: FONTS.black, - fontWeight: '800', - lineHeight: FONTS.lg + 3, - }, - rewardLabel: { - fontSize: FONTS.xs, - fontFamily: FONTS.regular, - }, - // Onboarding styles - onboardingContainer: { - flex: 1, - justifyContent: 'center', - padding: SPACING.lg, - }, - logoContainer: { - alignItems: 'center', - marginBottom: SPACING.xxl, - }, - logoText: { - fontSize: FONTS.hero, - fontFamily: FONTS.black, - fontWeight: '900', - letterSpacing: 1.2, - lineHeight: FONTS.hero + 6, - }, - tagline: { - fontSize: FONTS.lg, - fontFamily: FONTS.bold, - fontWeight: '700', - letterSpacing: 2, - marginTop: SPACING.xs, - }, - onboardingContent: { - alignItems: 'center', - }, - welcomeTitle: { - fontSize: FONTS.xxl, - fontFamily: FONTS.bold, - fontWeight: '700', - textAlign: 'center', - marginBottom: SPACING.sm, - lineHeight: FONTS.xxl + 6, - }, - welcomeSubtitle: { - fontSize: FONTS.md, - fontFamily: FONTS.regular, - textAlign: 'center', - marginBottom: SPACING.xl, - lineHeight: FONTS.md + 6, - }, - inputContainer: { - width: '100%', - marginBottom: SPACING.lg, - }, - inputLabel: { - fontSize: FONTS.xs, - fontFamily: FONTS.bold, - fontWeight: '700', - letterSpacing: 1, - marginBottom: SPACING.sm, - }, - textInputWrapper: { - flexDirection: 'row', - alignItems: 'center', - borderRadius: BORDER_RADIUS.md, - paddingHorizontal: SPACING.md + 1, - paddingVertical: SPACING.md + 1, - borderWidth: 0, - }, - textInput: { - flex: 1, - marginLeft: SPACING.sm, - }, - textInputReal: { - flex: 1, - marginLeft: SPACING.sm, - fontSize: FONTS.md, - }, - textInputText: { - fontSize: FONTS.md, - }, - textInputPlaceholder: {}, - keyboardAvoid: { - flex: 1, - }, - startButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: SPACING.xl, - paddingVertical: SPACING.md, - borderRadius: BORDER_RADIUS.md, - gap: SPACING.sm, - width: '100%', - }, - startButtonDisabled: {}, - startButtonText: { - fontSize: FONTS.md, - fontFamily: FONTS.bold, - fontWeight: '700', - letterSpacing: 1, - }, -}); diff --git a/frontend/app/(tabs)/leaderboard.tsx b/frontend/app/(tabs)/leaderboard.tsx deleted file mode 100644 index 764fcc40..00000000 --- a/frontend/app/(tabs)/leaderboard.tsx +++ /dev/null @@ -1,362 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - Pressable, - RefreshControl, - ActivityIndicator, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { Building2, Globe, MapPin, Trophy } from 'lucide-react-native'; -import { COLORS, FONTS, SPACING, BORDER_RADIUS } from '../../src/constants/theme'; -import { useUserStore } from '../../src/store/userStore'; -import { LeaderboardItem } from '../../src/components/LeaderboardItem'; -import { leaderboardAPI } from '../../src/services/api'; - -type Period = 'daily' | 'weekly' | 'alltime'; -type Filter = 'global' | 'city' | 'borough'; - -export default function LeaderboardScreen() { - const { user } = useUserStore(); - const [period, setPeriod] = useState('daily'); - const [filter, setFilter] = useState('global'); - const [leaderboard, setLeaderboard] = useState([]); - const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); - - const loadLeaderboard = useCallback(async () => { - try { - setLoading(true); - const city = filter === 'city' || filter === 'borough' ? user?.city : undefined; - const borough = filter === 'borough' ? user?.borough : undefined; - const data = await leaderboardAPI.get(period, city, borough); - setLeaderboard(data); - } catch (error) { - console.error('Error loading leaderboard:', error); - } finally { - setLoading(false); - } - }, [period, filter, user]); - - useEffect(() => { - loadLeaderboard(); - }, [loadLeaderboard]); - - const onRefresh = useCallback(async () => { - setRefreshing(true); - await loadLeaderboard(); - setRefreshing(false); - }, [loadLeaderboard]); - - const PeriodButton = ({ value, label }: { value: Period; label: string }) => ( - setPeriod(value)} - > - - {label} - - - ); - - const FilterButton = ({ value, label, icon }: { value: Filter; label: string; icon: React.ReactNode }) => ( - setFilter(value)} - > - {icon} - - {label} - - - ); - - // Find current user's rank - const userRank = leaderboard.findIndex( - (entry) => entry.user_id === user?.id - ) + 1; - - return ( - - {/* Header */} - - LEADERBOARD - - {filter === 'global' ? 'Global' : filter === 'city' ? user?.city : user?.borough} Rankings - - - - {/* Period Selector */} - - - - - - - {/* Filter Selector */} - - } - /> - } - /> - } - /> - - - {/* User's Position Card */} - {user && userRank > 0 && ( - - - YOUR POSITION - - - #{userRank} - - - - - {leaderboard[userRank - 1]?.steps?.toLocaleString() || 0} - - steps - - - )} - - {/* Leaderboard List */} - - } - showsVerticalScrollIndicator={false} - > - {loading ? ( - - - Loading rankings... - - ) : leaderboard.length === 0 ? ( - - - No Rankings Yet - - Be the first to get outside and set the pace! - - - ) : ( - leaderboard.map((entry, index) => ( - - )) - )} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: COLORS.background, - }, - header: { - padding: SPACING.md, - paddingBottom: SPACING.sm, - }, - title: { - fontSize: FONTS.xxl, - fontFamily: FONTS.black, - fontWeight: '800', - color: COLORS.textPrimary, - letterSpacing: 1, - lineHeight: FONTS.xxl + 5, - }, - subtitle: { - fontSize: FONTS.sm, - fontFamily: FONTS.regular, - color: COLORS.textSecondary, - marginTop: 2, - lineHeight: FONTS.sm + 3, - }, - periodContainer: { - flexDirection: 'row', - paddingHorizontal: SPACING.md, - gap: SPACING.sm, - marginBottom: SPACING.md, - }, - periodButton: { - flex: 1, - paddingVertical: SPACING.sm + 1, - paddingHorizontal: SPACING.md + 1, - borderRadius: BORDER_RADIUS.md, - backgroundColor: COLORS.backgroundSecondary, - alignItems: 'center', - borderWidth: 0, - borderColor: COLORS.border, - }, - periodButtonActive: { - backgroundColor: COLORS.primary, - borderColor: COLORS.primary, - }, - periodButtonText: { - fontSize: FONTS.sm, - fontFamily: FONTS.bold, - fontWeight: '600', - color: COLORS.textSecondary, - }, - periodButtonTextActive: { - color: COLORS.textPrimary, - }, - filterContainer: { - flexDirection: 'row', - paddingHorizontal: SPACING.md, - gap: SPACING.sm, - marginBottom: SPACING.md, - }, - filterButton: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING.sm, - gap: SPACING.xs, - borderRadius: BORDER_RADIUS.sm, - backgroundColor: COLORS.backgroundSecondary, - }, - filterButtonActive: { - backgroundColor: 'rgba(255, 107, 53, 0.15)', - }, - filterButtonText: { - fontSize: FONTS.xs, - fontFamily: FONTS.bold, - fontWeight: '600', - color: COLORS.textMuted, - }, - filterButtonTextActive: { - color: COLORS.primary, - }, - userPositionCard: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - backgroundColor: COLORS.backgroundSecondary, - marginHorizontal: SPACING.md, - marginBottom: SPACING.md, - padding: SPACING.md, - borderRadius: BORDER_RADIUS.lg, - borderWidth: 1, - borderColor: COLORS.primary, - }, - userPositionLeft: { - gap: SPACING.xs, - }, - userPositionLabel: { - fontSize: FONTS.xs, - fontFamily: FONTS.bold, - fontWeight: '600', - color: COLORS.textMuted, - letterSpacing: 0.6, - }, - userPositionRank: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.sm, - }, - userPositionNumber: { - fontSize: FONTS.xxxl, - fontFamily: FONTS.black, - fontWeight: '800', - color: COLORS.primary, - lineHeight: FONTS.xxxl + 5, - }, - userPositionRight: { - alignItems: 'flex-end', - }, - userPositionSteps: { - fontSize: FONTS.xl, - fontFamily: FONTS.bold, - fontWeight: '700', - color: COLORS.textPrimary, - }, - userPositionStepsLabel: { - fontSize: FONTS.xs, - fontFamily: FONTS.regular, - color: COLORS.textSecondary, - }, - listContainer: { - flex: 1, - }, - listContent: { - padding: SPACING.md, - paddingTop: 0, - }, - loadingContainer: { - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING.xxl, - }, - loadingText: { - marginTop: SPACING.md, - fontSize: FONTS.md, - fontFamily: FONTS.regular, - color: COLORS.textSecondary, - }, - emptyContainer: { - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING.xxl, - }, - emptyTitle: { - fontSize: FONTS.xl, - fontFamily: FONTS.bold, - fontWeight: '700', - color: COLORS.textPrimary, - marginTop: SPACING.md, - }, - emptySubtitle: { - fontSize: FONTS.md, - fontFamily: FONTS.regular, - color: COLORS.textSecondary, - marginTop: SPACING.sm, - textAlign: 'center', - }, -}); diff --git a/frontend/app/(tabs)/profile.tsx b/frontend/app/(tabs)/profile.tsx deleted file mode 100644 index 3557371d..00000000 --- a/frontend/app/(tabs)/profile.tsx +++ /dev/null @@ -1,703 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - Pressable, - Alert, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { - Calendar, - Check, - ChevronRight, - Flag, - Heart, - Info, - MapPin, - Palette, - Pencil, - RotateCcw, - Share2, -} from 'lucide-react-native'; -import { FONTS, SPACING, BORDER_RADIUS, AVATAR_COLORS, CITIES, THEMES } from '../../src/constants/theme'; -import { useUserStore } from '../../src/store/userStore'; -import { useThemeStore, ThemeKey } from '../../src/store/themeStore'; -import { userAPI } from '../../src/services/api'; -import { StreakBadge } from '../../src/components/StreakBadge'; - -export default function ProfileScreen() { - const { user, deviceId, setUser, clearUser, setOnboarded } = useUserStore(); - const { colors, currentTheme, setTheme } = useThemeStore(); - const [showCityPicker, setShowCityPicker] = useState(false); - const [showColorPicker, setShowColorPicker] = useState(false); - const [showThemePicker, setShowThemePicker] = useState(false); - - const handleUpdateCity = async (city: string, borough: string) => { - if (!deviceId) return; - try { - const updated = await userAPI.update(deviceId, { city, borough }); - setUser(updated); - setShowCityPicker(false); - } catch (error) { - console.error('Error updating city:', error); - } - }; - - const handleUpdateColor = async (color: string) => { - if (!deviceId) return; - try { - const updated = await userAPI.update(deviceId, { avatar_color: color }); - setUser(updated); - setShowColorPicker(false); - } catch (error) { - console.error('Error updating color:', error); - } - }; - - const handleUpdateGoal = async (type: 'daily' | 'weekly') => { - const options = type === 'daily' - ? ['5,000', '7,500', '10,000', '12,500', '15,000'] - : ['35,000', '50,000', '70,000', '100,000']; - - Alert.alert( - `Set ${type === 'daily' ? 'Daily' : 'Weekly'} Goal`, - 'Choose your step goal', - [ - ...options.map(opt => ({ - text: opt, - onPress: async () => { - const value = parseInt(opt.replace(',', '')); - try { - const updated = await userAPI.update(deviceId!, - type === 'daily' ? { daily_goal: value } : { weekly_goal: value } - ); - setUser(updated); - } catch (error) { - console.error('Error updating goal:', error); - } - }, - })), - { text: 'Cancel', style: 'cancel' }, - ] - ); - }; - - const handleThemeChange = async (theme: ThemeKey) => { - await setTheme(theme); - setShowThemePicker(false); - }; - - const handleLogout = () => { - Alert.alert( - 'Reset Profile', - 'This will clear all your local data. Your stats are saved on the server.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Reset', - style: 'destructive', - onPress: () => { - clearUser(); - setOnboarded(false); - }, - }, - ] - ); - }; - - const MenuItem = ({ - icon, - label, - value, - onPress, - showArrow = true, - }: { - icon: React.ReactNode; - label: string; - value?: string; - onPress?: () => void; - showArrow?: boolean; - }) => ( - - - {icon} - {label} - - - {value && {value}} - {showArrow && ( - - )} - - - ); - - const dynamicStyles = { - container: { backgroundColor: colors.background }, - card: { - backgroundColor: colors.backgroundSecondary, - borderColor: colors.border, - }, - text: { color: colors.textPrimary }, - textMuted: { color: colors.textMuted }, - textSecondary: { color: colors.textSecondary }, - primary: { color: colors.primary }, - }; - - return ( - - - {/* Header */} - - PROFILE - - - {/* Profile Card */} - - setShowColorPicker(true)} - > - - {user?.username?.charAt(0).toUpperCase() || '?'} - - - - - - - @{user?.username || 'Loading...'} - - {user?.city}, {user?.borough} - - - - - - - - {/* Color Picker Modal */} - {showColorPicker && ( - - - Choose Your Color - - {AVATAR_COLORS.map((color) => ( - handleUpdateColor(color)} - > - {user?.avatar_color === color && ( - - )} - - ))} - - setShowColorPicker(false)} - > - Close - - - - )} - - {/* Quick Stats */} - - - - {user?.outside_score?.toLocaleString() || 0} - - Outside Score - - - - - {user?.total_steps?.toLocaleString() || 0} - - Total Steps - - - - - {user?.longest_streak || 0} - - Best Streak - - - - {/* Theme Settings */} - - Appearance - - - } - label="App Theme" - value={THEMES[currentTheme].name} - onPress={() => setShowThemePicker(true)} - /> - - - - {/* Theme Picker Modal */} - {showThemePicker && ( - - - Choose Theme - - {(Object.keys(THEMES) as ThemeKey[]).map((themeKey) => ( - handleThemeChange(themeKey)} - > - - - - - - {THEMES[themeKey].name} - - {currentTheme === themeKey && ( - - - - )} - - ))} - - setShowThemePicker(false)} - > - Done - - - - )} - - {/* Settings */} - - Settings - - - } - label="Location" - value={`${user?.city}, ${user?.borough}`} - onPress={() => setShowCityPicker(true)} - /> - } - label="Daily Goal" - value={`${(user?.daily_goal || 10000).toLocaleString()} steps`} - onPress={() => handleUpdateGoal('daily')} - /> - } - label="Weekly Goal" - value={`${(user?.weekly_goal || 70000).toLocaleString()} steps`} - onPress={() => handleUpdateGoal('weekly')} - /> - - - - {/* City Picker Modal */} - {showCityPicker && ( - - - Select Your City - - {CITIES.map((city) => ( - - {city.name} - {city.boroughs.map((borough) => ( - handleUpdateCity(city.name, borough)} - > - - {borough} - - {user?.city === city.name && - user?.borough === borough && ( - - )} - - ))} - - ))} - - setShowCityPicker(false)} - > - Close - - - - )} - - {/* About Section */} - - About - - - } - label="App Version" - value="1.0.0" - showArrow={false} - /> - } - label="Rate OUT 'ERE" - onPress={() => {}} - /> - } - label="Share with Friends" - onPress={() => {}} - /> - - - - {/* Danger Zone */} - - - - Reset Profile - - - - {/* Branding */} - - OUT 'ERE - WE OUTSIDE. - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - padding: SPACING.md, - paddingBottom: SPACING.xxl, - }, - header: { - marginBottom: SPACING.lg, - }, - title: { - fontSize: FONTS.xxl, - fontFamily: FONTS.black, - fontWeight: '800', - letterSpacing: 1, - lineHeight: FONTS.xxl + 5, - }, - profileCard: { - borderRadius: BORDER_RADIUS.lg, - padding: SPACING.lg, - alignItems: 'center', - marginBottom: SPACING.lg, - borderWidth: 0.8, - }, - avatar: { - width: 80, - height: 80, - borderRadius: 40, - alignItems: 'center', - justifyContent: 'center', - marginBottom: SPACING.md, - }, - avatarText: { - fontSize: FONTS.xxxl, - fontWeight: '800', - color: '#FFFFFF', - }, - avatarEditBadge: { - position: 'absolute', - bottom: 0, - right: 0, - width: 24, - height: 24, - borderRadius: 12, - alignItems: 'center', - justifyContent: 'center', - borderWidth: 1, - }, - username: { - fontSize: FONTS.xl, - fontFamily: FONTS.bold, - fontWeight: '700', - lineHeight: FONTS.xl + 4, - }, - location: { - fontSize: FONTS.sm, - fontFamily: FONTS.regular, - marginTop: 2, - lineHeight: FONTS.sm + 3, - }, - profileStats: { - marginTop: SPACING.md, - }, - quickStatsCard: { - flexDirection: 'row', - borderRadius: BORDER_RADIUS.lg, - padding: SPACING.lg, - marginBottom: SPACING.lg, - borderWidth: 0.8, - }, - quickStatItem: { - flex: 1, - alignItems: 'center', - }, - quickStatValue: { - fontSize: FONTS.xl, - fontFamily: FONTS.black, - fontWeight: '800', - }, - quickStatLabel: { - fontSize: FONTS.xs, - fontFamily: FONTS.regular, - marginTop: 2, - }, - quickStatDivider: { - width: 1, - }, - settingsSection: { - marginBottom: SPACING.lg, - }, - sectionTitle: { - fontSize: FONTS.sm, - fontFamily: FONTS.bold, - fontWeight: '700', - letterSpacing: 0.6, - marginBottom: SPACING.sm, - textTransform: 'uppercase', - }, - menuGroup: { - borderRadius: BORDER_RADIUS.md, - overflow: 'hidden', - borderWidth: 0.8, - }, - menuItem: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - padding: SPACING.md, - borderBottomWidth: 0.8, - }, - menuItemLeft: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.md, - }, - menuItemLabel: { - fontSize: FONTS.md, - fontFamily: FONTS.regular, - lineHeight: FONTS.md + 4, - }, - menuItemRight: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING.sm, - }, - menuItemValue: { - fontSize: FONTS.sm, - fontFamily: FONTS.regular, - lineHeight: FONTS.sm + 3, - }, - dangerSection: { - marginBottom: SPACING.lg, - }, - logoutButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: SPACING.sm, - backgroundColor: 'rgba(244, 67, 54, 0.1)', - padding: SPACING.md + 1, - borderRadius: BORDER_RADIUS.md, - borderWidth: 0, - }, - logoutText: { - fontSize: FONTS.md, - fontFamily: FONTS.bold, - fontWeight: '600', - }, - branding: { - alignItems: 'center', - paddingVertical: SPACING.xl, - }, - brandingText: { - fontSize: FONTS.xl, - fontFamily: FONTS.black, - fontWeight: '900', - letterSpacing: 1, - }, - brandingSubtext: { - fontSize: FONTS.xs, - fontWeight: '600', - letterSpacing: 2, - }, - pickerOverlay: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.8)', - justifyContent: 'center', - padding: SPACING.md, - zIndex: 100, - }, - pickerContent: { - borderRadius: BORDER_RADIUS.lg, - padding: SPACING.lg, - maxHeight: '80%', - }, - pickerTitle: { - fontSize: FONTS.lg, - fontWeight: '700', - marginBottom: SPACING.md, - textAlign: 'center', - lineHeight: FONTS.lg + 4, - }, - colorGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: SPACING.md, - justifyContent: 'center', - marginBottom: SPACING.lg, - }, - colorOption: { - width: 48, - height: 48, - borderRadius: 24, - alignItems: 'center', - justifyContent: 'center', - }, - colorOptionSelected: { - borderWidth: 2, - borderColor: '#FFFFFF', - }, - themeGrid: { - flexDirection: 'row', - gap: SPACING.md, - marginBottom: SPACING.lg, - }, - themeOption: { - flex: 1, - borderRadius: BORDER_RADIUS.md, - padding: SPACING.md, - alignItems: 'center', - }, - themePreview: { - width: '100%', - height: 60, - borderRadius: BORDER_RADIUS.sm, - marginBottom: SPACING.sm, - padding: SPACING.sm, - alignItems: 'center', - justifyContent: 'center', - }, - themeAccentDot: { - width: 24, - height: 24, - borderRadius: 12, - marginBottom: 4, - }, - themeAccentLine: { - width: 40, - height: 4, - borderRadius: 2, - }, - themeLabel: { - fontSize: FONTS.sm, - fontWeight: '600', - textAlign: 'center', - }, - themeCheck: { - position: 'absolute', - top: 8, - right: 8, - width: 24, - height: 24, - borderRadius: 12, - alignItems: 'center', - justifyContent: 'center', - }, - cityList: { - maxHeight: 400, - }, - cityName: { - fontSize: FONTS.md, - fontWeight: '700', - paddingVertical: SPACING.sm, - borderBottomWidth: 0.8, - }, - boroughOption: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingVertical: SPACING.sm, - paddingLeft: SPACING.md, - }, - boroughText: { - fontSize: FONTS.md, - lineHeight: FONTS.md + 3, - }, - pickerClose: { - padding: SPACING.md, - borderRadius: BORDER_RADIUS.md, - alignItems: 'center', - }, - pickerCloseText: { - fontSize: FONTS.md, - fontWeight: '600', - }, -}); diff --git a/frontend/app/(tabs)/stats.tsx b/frontend/app/(tabs)/stats.tsx deleted file mode 100644 index 00a9cddd..00000000 --- a/frontend/app/(tabs)/stats.tsx +++ /dev/null @@ -1,478 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - RefreshControl, - Dimensions, -} from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import { BarChart3, Clock3, Flame, Footprints, Navigation, TrendingUp } from 'lucide-react-native'; -import { BarChart } from 'react-native-gifted-charts'; -import { COLORS, FONTS, SPACING, BORDER_RADIUS } from '../../src/constants/theme'; -import { useUserStore } from '../../src/store/userStore'; -import { StreakBadge } from '../../src/components/StreakBadge'; -import { stepsAPI, statsAPI } from '../../src/services/api'; - -const { width } = Dimensions.get('window'); - -export default function StatsScreen() { - const { user, deviceId } = useUserStore(); - const [refreshing, setRefreshing] = useState(false); - const [weeklyData, setWeeklyData] = useState([]); - const [weeklySummary, setWeeklySummary] = useState(null); - const [userStats, setUserStats] = useState(null); - - const loadStats = useCallback(async () => { - if (!deviceId) return; - - try { - // Load step history - const history = await stepsAPI.getHistory(deviceId, 7); - - // Format for chart - const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - const chartData = history.map((record: any) => { - const date = new Date(record.date); - return { - value: record.steps, - label: days[date.getDay()], - frontColor: COLORS.primary, - }; - }); - setWeeklyData(chartData); - - // Load weekly summary - const summary = await stepsAPI.getWeeklySummary(deviceId); - setWeeklySummary(summary); - - // Load user stats - const stats = await statsAPI.getUserStats(deviceId); - setUserStats(stats); - } catch (error) { - console.error('Error loading stats:', error); - } - }, [deviceId]); - - useEffect(() => { - loadStats(); - }, [loadStats]); - - const onRefresh = useCallback(async () => { - setRefreshing(true); - await loadStats(); - setRefreshing(false); - }, [loadStats]); - - const StatCard = ({ - icon, - value, - label, - color = COLORS.primary, - }: { - icon: React.ReactNode; - value: string | number; - label: string; - color?: string; - }) => ( - - {icon} - {value} - {label} - - ); - - return ( - - - } - showsVerticalScrollIndicator={false} - > - {/* Header */} - - YOUR STATS - Track your progress - - - {/* Streak Section */} - - - - - Longest Streak - - {user?.longest_streak || 0} days - - - - - Days Active - - {weeklySummary?.days_active || 0} / 7 - - - - - - {/* Weekly Chart */} - - This Week - - {weeklyData.length > 0 ? ( - d.value), 10000)} - barBorderRadius={4} - frontColor={COLORS.primary} - backgroundColor={COLORS.backgroundSecondary} - isAnimated - /> - ) : ( - - - No data yet - - )} - - - - {/* Weekly Summary */} - - Weekly Summary - - } - value={weeklySummary?.total_steps?.toLocaleString() || 0} - label="Total Steps" - /> - } - value={`${weeklySummary?.total_distance?.toFixed(1) || 0} km`} - label="Distance" - color={COLORS.primaryLight} - /> - } - value={weeklySummary?.total_active_minutes || 0} - label="Active Min" - color={COLORS.success} - /> - } - value={Math.round(weeklySummary?.avg_steps || 0).toLocaleString()} - label="Daily Avg" - color={COLORS.warning} - /> - - - - {/* All Time Stats */} - - All Time - - - - - {user?.outside_score?.toLocaleString() || 0} - - Outside Score - - - - - {user?.total_steps?.toLocaleString() || 0} - - Total Steps - - - - - {user?.total_distance?.toFixed(1) || 0} km - - Total Distance - - - - - {/* Goals Progress */} - - Goals - - - Daily Goal - - {((userStats?.weekly?.total_steps / 7 / (user?.daily_goal || 10000)) * 100).toFixed(0)}% avg - - - - - - - Target: {(user?.daily_goal || 10000).toLocaleString()} steps/day - - - - - - Weekly Goal - - {((weeklySummary?.total_steps / (user?.weekly_goal || 70000)) * 100).toFixed(0)}% - - - - - - - Target: {(user?.weekly_goal || 70000).toLocaleString()} steps/week - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: COLORS.background, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - padding: SPACING.md, - paddingBottom: SPACING.xxl, - }, - header: { - marginBottom: SPACING.lg, - }, - title: { - fontSize: FONTS.xxl, - fontFamily: FONTS.black, - fontWeight: '800', - color: COLORS.textPrimary, - letterSpacing: 1, - lineHeight: FONTS.xxl + 5, - }, - subtitle: { - fontSize: FONTS.sm, - fontFamily: FONTS.regular, - color: COLORS.textSecondary, - marginTop: 2, - lineHeight: FONTS.sm + 3, - }, - streakSection: { - backgroundColor: COLORS.backgroundSecondary, - borderRadius: BORDER_RADIUS.lg, - padding: SPACING.lg, - marginBottom: SPACING.lg, - borderWidth: 0.8, - borderColor: COLORS.border, - }, - streakInfo: { - flexDirection: 'row', - marginTop: SPACING.lg, - paddingTop: SPACING.md, - borderTopWidth: 0.8, - borderTopColor: COLORS.border, - }, - streakInfoItem: { - flex: 1, - alignItems: 'center', - }, - streakInfoLabel: { - fontSize: FONTS.xs, - fontFamily: FONTS.regular, - color: COLORS.textMuted, - marginBottom: SPACING.xs, - }, - streakInfoValue: { - fontSize: FONTS.lg, - fontFamily: FONTS.bold, - fontWeight: '700', - color: COLORS.textPrimary, - }, - streakInfoDivider: { - width: 1, - backgroundColor: COLORS.border, - }, - chartSection: { - marginBottom: SPACING.lg, - }, - sectionTitle: { - fontSize: FONTS.lg, - fontFamily: FONTS.bold, - fontWeight: '700', - color: COLORS.textPrimary, - marginBottom: SPACING.md, - lineHeight: FONTS.lg + 4, - }, - chartContainer: { - backgroundColor: COLORS.backgroundSecondary, - borderRadius: BORDER_RADIUS.lg, - padding: SPACING.md, - borderWidth: 0.8, - borderColor: COLORS.border, - overflow: 'hidden', - }, - noDataContainer: { - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING.xxl, - }, - noDataText: { - marginTop: SPACING.md, - fontSize: FONTS.md, - fontFamily: FONTS.regular, - color: COLORS.textMuted, - }, - summarySection: { - marginBottom: SPACING.lg, - }, - summaryGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: SPACING.sm, - }, - statCard: { - flex: 1, - minWidth: (width - SPACING.md * 3 - SPACING.sm) / 2, - backgroundColor: COLORS.backgroundSecondary, - borderRadius: BORDER_RADIUS.md, - padding: SPACING.md, - alignItems: 'center', - borderWidth: 0.8, - borderColor: COLORS.border, - }, - statValue: { - fontSize: FONTS.xl, - fontFamily: FONTS.black, - fontWeight: '800', - color: COLORS.textPrimary, - marginTop: SPACING.sm, - lineHeight: FONTS.xl + 4, - }, - statLabel: { - fontSize: FONTS.xs, - fontFamily: FONTS.regular, - color: COLORS.textMuted, - marginTop: 2, - }, - allTimeSection: { - marginBottom: SPACING.lg, - }, - allTimeGrid: { - flexDirection: 'row', - gap: SPACING.sm, - }, - allTimeCard: { - flex: 1, - backgroundColor: COLORS.backgroundSecondary, - borderRadius: BORDER_RADIUS.md, - padding: SPACING.md, - alignItems: 'center', - borderWidth: 0.8, - borderColor: COLORS.border, - }, - allTimeValue: { - fontSize: FONTS.lg, - fontFamily: FONTS.bold, - fontWeight: '800', - color: COLORS.textPrimary, - marginTop: SPACING.sm, - lineHeight: FONTS.lg + 3, - }, - allTimeLabel: { - fontSize: FONTS.xs, - fontFamily: FONTS.regular, - color: COLORS.textMuted, - marginTop: 2, - textAlign: 'center', - }, - goalsSection: { - marginBottom: SPACING.lg, - }, - goalCard: { - backgroundColor: COLORS.backgroundSecondary, - borderRadius: BORDER_RADIUS.md, - padding: SPACING.md, - marginBottom: SPACING.sm, - borderWidth: 0.8, - borderColor: COLORS.border, - }, - goalHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: SPACING.sm, - }, - goalTitle: { - fontSize: FONTS.md, - fontFamily: FONTS.bold, - fontWeight: '600', - color: COLORS.textPrimary, - }, - goalProgress: { - fontSize: FONTS.sm, - fontFamily: FONTS.bold, - fontWeight: '700', - color: COLORS.primary, - }, - goalBarBackground: { - height: 8, - backgroundColor: COLORS.backgroundTertiary, - borderRadius: 4, - overflow: 'hidden', - }, - goalBarFill: { - height: '100%', - backgroundColor: COLORS.primary, - borderRadius: 4, - }, - goalTarget: { - fontSize: FONTS.xs, - fontFamily: FONTS.regular, - color: COLORS.textMuted, - marginTop: SPACING.sm, - }, -}); diff --git a/frontend/app/+html.tsx b/frontend/app/+html.tsx deleted file mode 100644 index eb746628..00000000 --- a/frontend/app/+html.tsx +++ /dev/null @@ -1,44 +0,0 @@ -// @ts-nocheck -import { ScrollViewStyleReset } from "expo-router/html"; -import type { PropsWithChildren } from "react"; - -export default function Root({ children }: PropsWithChildren) { - return ( - - - - - - {/* - Disable body scrolling on web to make ScrollView components work correctly. - If you want to enable scrolling, remove `ScrollViewStyleReset` and - set `overflow: auto` on the body style below. - */} - -