diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fda39d..16c6660 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,10 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 + cache: npm - name: Install dependencies - run: npm install + run: npm ci - name: Typecheck run: npm run typecheck diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index 3d3d141..11668df 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -2,6 +2,12 @@ name: Deploy Demo on: workflow_dispatch: + inputs: + alias: + description: EAS Hosting alias to deploy + required: true + default: demo + type: string jobs: deploy-demo: @@ -16,9 +22,10 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 + cache: npm - name: Install dependencies - run: npm install + run: npm ci - name: Typecheck run: npm run typecheck @@ -29,15 +36,15 @@ jobs: - name: Web export run: npm run build:web env: - EXPO_PUBLIC_AUTH_REDIRECT_URL: https://arcade-radar--demo.expo.app/ + EXPO_PUBLIC_AUTH_REDIRECT_URL: https://arcade-radar--${{ inputs.alias }}.expo.app/ EXPO_PUBLIC_SUPABASE_KEY: ${{ secrets.EXPO_PUBLIC_SUPABASE_KEY }} EXPO_PUBLIC_SUPABASE_URL: ${{ secrets.EXPO_PUBLIC_SUPABASE_URL }} - - name: Deploy demo alias - run: npx eas-cli@latest deploy --alias demo --non-interactive + - name: Deploy alias + run: npx eas-cli@latest deploy --alias "${{ inputs.alias }}" --non-interactive env: EAS_NO_VCS: 1 - EXPO_PUBLIC_AUTH_REDIRECT_URL: https://arcade-radar--demo.expo.app/ + EXPO_PUBLIC_AUTH_REDIRECT_URL: https://arcade-radar--${{ inputs.alias }}.expo.app/ EXPO_PUBLIC_SUPABASE_KEY: ${{ secrets.EXPO_PUBLIC_SUPABASE_KEY }} EXPO_PUBLIC_SUPABASE_URL: ${{ secrets.EXPO_PUBLIC_SUPABASE_URL }} - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} \ No newline at end of file + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} diff --git a/.gitignore b/.gitignore index 94d0c96..6348658 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,3 @@ yarn-error.* expo-env.d.ts # @end expo-cli - -package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index f694cb3..bb1d58c 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ Required GitHub repository secrets: - `EXPO_PUBLIC_SUPABASE_KEY` - `EXPO_TOKEN` for the manual EAS demo deploy workflow -The CI workflow uses `npm install` instead of `npm ci` because this project -currently ignores `package-lock.json`. +The workflows use `npm ci`, so keep `package-lock.json` committed whenever +dependencies change. ## Demo Deployment @@ -99,6 +99,9 @@ For mixer demos, use the stable EAS alias instead of one-off deploy URLs: npx eas-cli@latest deploy --alias demo ``` +The GitHub `Deploy Demo` workflow also accepts an alias input. Leave it as +`demo` for the mixer, or use another alias such as `staging` later. + Use this URL for QR codes and Supabase Auth redirects: ```text diff --git a/src/app/demo.tsx b/src/app/demo.tsx index ca6098d..33da1bc 100644 --- a/src/app/demo.tsx +++ b/src/app/demo.tsx @@ -10,6 +10,10 @@ import { import { SafeAreaView } from "react-native-safe-area-context"; import { theme } from "@/constants/theme"; +import { + buildDemoSearchParams, + featuredDemoSearches, +} from "@/lib/demo"; import { env } from "@/lib/env"; const demoUrl = env.authRedirectUrl || "https://arcade-radar--demo.expo.app/"; @@ -30,24 +34,6 @@ const talkingPoints = [ "The map stack avoids Google Maps Platform costs.", ]; -const featuredDemoSearches = [ - { - game: "Street Fighter III: 3rd Strike", - location: "75201", - title: "Fighting game run", - }, - { - game: "Marvel vs. Capcom 2", - location: "75080", - title: "Rare cabinet search", - }, - { - game: "DanceDanceRevolution", - location: "76051", - title: "Rhythm game search", - }, -]; - export default function DemoScreen() { const { width } = useWindowDimensions(); const isWideLayout = width >= 1100; @@ -89,10 +75,7 @@ export default function DemoScreen() { key={search.title} href={{ pathname: "/", - params: { - game: search.game, - location: search.location, - }, + params: buildDemoSearchParams(search), }} asChild > diff --git a/src/lib/demo.test.ts b/src/lib/demo.test.ts new file mode 100644 index 0000000..8cb6247 --- /dev/null +++ b/src/lib/demo.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildDemoSearchParams, + buildExpoAliasUrl, + defaultDemoAlias, + featuredDemoSearches, +} from './demo'; + +describe('demo deployment helpers', () => { + it('builds stable EAS alias URLs', () => { + expect(buildExpoAliasUrl('demo')).toBe( + 'https://arcade-radar--demo.expo.app/', + ); + expect(buildExpoAliasUrl(' staging ')).toBe( + 'https://arcade-radar--staging.expo.app/', + ); + expect(buildExpoAliasUrl('')).toBe( + `https://arcade-radar--${defaultDemoAlias}.expo.app/`, + ); + }); +}); + +describe('featuredDemoSearches', () => { + it('contains QR-demo-ready searches with location and game params', () => { + expect(featuredDemoSearches.length).toBeGreaterThanOrEqual(3); + + for (const search of featuredDemoSearches) { + expect(search.title).not.toHaveLength(0); + expect(search.game).not.toHaveLength(0); + expect(search.location).toMatch(/^\d{5}$/); + expect(buildDemoSearchParams(search)).toEqual({ + game: search.game, + location: search.location, + }); + } + }); +}); diff --git a/src/lib/demo.ts b/src/lib/demo.ts new file mode 100644 index 0000000..7e8411f --- /dev/null +++ b/src/lib/demo.ts @@ -0,0 +1,38 @@ +export interface FeaturedDemoSearch { + game: string; + location: string; + title: string; +} + +export const defaultDemoAlias = 'demo'; + +export const featuredDemoSearches: FeaturedDemoSearch[] = [ + { + game: 'Street Fighter III: 3rd Strike', + location: '75201', + title: 'Fighting game run', + }, + { + game: 'Marvel vs. Capcom 2', + location: '75080', + title: 'Rare cabinet search', + }, + { + game: 'DanceDanceRevolution', + location: '76051', + title: 'Rhythm game search', + }, +]; + +export function buildExpoAliasUrl(alias: string): string { + const normalizedAlias = alias.trim() || defaultDemoAlias; + + return `https://arcade-radar--${normalizedAlias}.expo.app/`; +} + +export function buildDemoSearchParams(search: FeaturedDemoSearch) { + return { + game: search.game, + location: search.location, + }; +} diff --git a/src/lib/scout.test.ts b/src/lib/scout.test.ts new file mode 100644 index 0000000..dbcea99 --- /dev/null +++ b/src/lib/scout.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { getScoutErrorMessage } from './scout'; + +describe('getScoutErrorMessage', () => { + it('returns plain string errors', () => { + expect(getScoutErrorMessage('Already submitted')).toBe('Already submitted'); + }); + + it('prefers structured error messages', () => { + expect(getScoutErrorMessage({ message: 'Authentication required' })).toBe( + 'Authentication required', + ); + }); + + it('falls back to details when message is missing', () => { + expect(getScoutErrorMessage({ details: 'Duplicate pending report' })).toBe( + 'Duplicate pending report', + ); + }); + + it('serializes unknown objects as a last resort', () => { + expect(getScoutErrorMessage({ code: 'P0001' })).toBe('{"code":"P0001"}'); + }); +});