diff --git a/.env b/.env.example similarity index 79% rename from .env rename to .env.example index f1b951e..3764d04 100644 --- a/.env +++ b/.env.example @@ -6,5 +6,5 @@ EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET= EXPO_PUBLIC_FIREBASE_APP_ID= EXPO_PUBLIC_GRAPHQL_ENDPOINT= EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT= -EXPO_PUBLIC_MANAGER_DASHBOARD_URL= -EXPO_PUBLIC_REFERRER_ENDPOINT= \ No newline at end of file +EXPO_PUBLIC_COMMUNITY_DASHBOARD_URL= +EXPO_PUBLIC_REFERRER_ENDPOINT= diff --git a/.github/workflows/release-android.yml b/.github/workflows/release-android.yml index 35a73b4..68242c8 100644 --- a/.github/workflows/release-android.yml +++ b/.github/workflows/release-android.yml @@ -1,29 +1,32 @@ -name: Build and Release Android APK +name: Build and Release Android on: push: + # branches: + # - 'feat/android-release-workflow' tags: - - 'v*.*.*' # Triggers on version tags like v1.0.0, v2.1.3, etc. - -env: - EXPO_PUBLIC_FIREBASE_API_KEY: ${{ vars.EXPO_PUBLIC_FIREBASE_API_KEY }} - EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ vars.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN }} - EXPO_PUBLIC_FIREBASE_DATABASE_URL: ${{ vars.EXPO_PUBLIC_FIREBASE_DATABASE_URL }} - EXPO_PUBLIC_FIREBASE_PROJECT_ID: ${{ vars.EXPO_PUBLIC_FIREBASE_PROJECT_ID }} - EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ vars.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET }} - EXPO_PUBLIC_FIREBASE_APP_ID: ${{ vars.EXPO_PUBLIC_FIREBASE_APP_ID }} - EXPO_PUBLIC_GRAPHQL_ENDPOINT: ${{ vars.EXPO_PUBLIC_GRAPHQL_ENDPOINT }} - EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT: ${{ vars.EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT }} + - 'v*.*.*' jobs: - build-and-release: - name: Build Android APK and Create Release - environment: 'alpha-2' + # ─── JOB 1: STAGING BUILD ─────────────────────────────────────────── + build-staging: + name: Build and Deploy Staging APK + # FIXME: Update this to staging later on + environment: alpha-2 runs-on: ubuntu-latest - permissions: contents: write + env: + EXPO_PUBLIC_FIREBASE_API_KEY: ${{ vars.EXPO_PUBLIC_FIREBASE_API_KEY }} + EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ vars.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN }} + EXPO_PUBLIC_FIREBASE_DATABASE_URL: ${{ vars.EXPO_PUBLIC_FIREBASE_DATABASE_URL }} + EXPO_PUBLIC_FIREBASE_PROJECT_ID: ${{ vars.EXPO_PUBLIC_FIREBASE_PROJECT_ID }} + EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ vars.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET }} + EXPO_PUBLIC_FIREBASE_APP_ID: ${{ vars.EXPO_PUBLIC_FIREBASE_APP_ID }} + EXPO_PUBLIC_GRAPHQL_ENDPOINT: ${{ vars.EXPO_PUBLIC_GRAPHQL_ENDPOINT }} + EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT: ${{ vars.EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT }} + steps: - name: Checkout code uses: actions/checkout@v4 @@ -39,14 +42,135 @@ jobs: node-version: '22' cache: 'pnpm' - - name: Get pnpm store directory - shell: bash + - name: Install dependencies + run: pnpm install + + - name: Create .env.local for codegen + run: | + echo "EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT=${{ vars.EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT }}" > .env.local + + - name: Generate GraphQL types + run: pnpm generate:type + + - name: Setup JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Decode staging keystore + run: | + echo "${{ secrets.ANDROID_STAGING_KEYSTORE_BASE64 }}" | base64 --decode > ${{ runner.temp }}/mapswipe-staging.keystore + + - name: Expo prebuild (staging) + run: APP_ENV=staging npx expo prebuild --platform android --clean + + - name: Build staging APK + env: + KEYSTORE_PATH: ${{ runner.temp }}/mapswipe-staging.keystore + STORE_PASSWORD: ${{ secrets.ANDROID_STAGING_KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_STAGING_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.ANDROID_STAGING_KEY_PASSWORD }} + run: | + cd android && ./gradlew assembleRelease \ + -Pandroid.injected.signing.store.file=$KEYSTORE_PATH \ + -Pandroid.injected.signing.store.password=$STORE_PASSWORD \ + -Pandroid.injected.signing.key.alias=$KEY_ALIAS \ + -Pandroid.injected.signing.key.password=$KEY_PASSWORD + + - name: Get tag name + id: tag + run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Rename APK + run: | + mkdir -p generated + cp android/app/build/outputs/apk/release/app-release.apk \ + generated/mapswipe-staging-${{ steps.tag.outputs.tag }}.apk + + - name: Create staging pre-release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + gh release create ${{ steps.tag.outputs.tag }}-beta \ + --title "MapSwipe Mobile ${{ steps.tag.outputs.tag }} (Staging)" \ + --prerelease \ + --notes "## Staging build for ${{ steps.tag.outputs.tag }} + + This is a **staging pre-release** for internal testing only. + + ### 📱 Install + 1. Download the APK below + 2. Enable 'Install from unknown sources' in Android settings + 3. Install and test against the **staging database** + + ### 🔧 Build info + - Commit: \`${{ github.sha }}\` + - Build: #${{ github.run_number }} + - Date: $(date +'%Y-%m-%d %H:%M:%S UTC') + - Package: \`org.missingmaps.mapswipe.staging\` + + --- + Once testing is complete, approve the production deployment in GitHub Actions." \ + generated/mapswipe-staging-${{ steps.tag.outputs.tag }}.apk + + # ─── JOB 2: PRODUCTION BUILD (requires manual approval) ───────────── + build-production: + name: Build and Deploy Production APK + environment: production + runs-on: ubuntu-latest + needs: build-staging + permissions: + contents: write + + env: + EXPO_PUBLIC_FIREBASE_API_KEY: ${{ vars.EXPO_PUBLIC_FIREBASE_API_KEY }} + EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ vars.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN }} + EXPO_PUBLIC_FIREBASE_DATABASE_URL: ${{ vars.EXPO_PUBLIC_FIREBASE_DATABASE_URL }} + EXPO_PUBLIC_FIREBASE_PROJECT_ID: ${{ vars.EXPO_PUBLIC_FIREBASE_PROJECT_ID }} + EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ vars.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET }} + EXPO_PUBLIC_FIREBASE_APP_ID: ${{ vars.EXPO_PUBLIC_FIREBASE_APP_ID }} + EXPO_PUBLIC_GRAPHQL_ENDPOINT: ${{ vars.EXPO_PUBLIC_GRAPHQL_ENDPOINT }} + EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT: ${{ vars.EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' - name: Install dependencies run: pnpm install + - name: Create .env.local for codegen + run: | + echo "EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT=${{ vars.EXPO_PUBLIC_GRAPHQL_CODEGEN_ENDPOINT }}" > .env.local + + - name: Generate GraphQL types + run: pnpm generate:type + - name: Setup JDK 17 uses: actions/setup-java@v4 with: @@ -66,49 +190,52 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Build Release APK - run: pnpm build:release + - name: Decode production keystore + run: | + echo "${{ secrets.ANDROID_PROD_KEYSTORE_BASE64 }}" | base64 --decode > ${{ runner.temp }}/mapswipe-prod.keystore + + - name: Expo prebuild (production) + run: npx expo prebuild --platform android --clean - - name: Get release info - id: release-info + - name: Build production APK + env: + KEYSTORE_PATH: ${{ runner.temp }}/mapswipe-prod.keystore + STORE_PASSWORD: ${{ secrets.ANDROID_PROD_KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_PROD_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.ANDROID_PROD_KEY_PASSWORD }} run: | - APK_FILE=$(ls generated/app-release-*.apk | head -n 1) - APK_NAME=$(basename "$APK_FILE") - TAG_NAME=${GITHUB_REF#refs/tags/} - FILE_SIZE=$(du -h "$APK_FILE" | cut -f1) + cd android && ./gradlew assembleRelease \ + -Pandroid.injected.signing.store.file=$KEYSTORE_PATH \ + -Pandroid.injected.signing.store.password=$STORE_PASSWORD \ + -Pandroid.injected.signing.key.alias=$KEY_ALIAS \ + -Pandroid.injected.signing.key.password=$KEY_PASSWORD - echo "apk_file=$APK_FILE" >> $GITHUB_OUTPUT - echo "apk_name=$APK_NAME" >> $GITHUB_OUTPUT - echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT - echo "file_size=$FILE_SIZE" >> $GITHUB_OUTPUT + - name: Get tag name + id: tag + run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - echo "📦 APK: $APK_NAME ($FILE_SIZE)" + - name: Rename APK + run: | + mkdir -p generated + cp android/app/build/outputs/apk/release/app-release.apk \ + generated/mapswipe-${{ steps.tag.outputs.tag }}.apk - - name: Create Release with gh CLI + - name: Publish production release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh release create ${{ steps.release-info.outputs.tag_name }} \ - --title "MapSwipe Mobile ${{ steps.release-info.outputs.tag_name }}" \ - --notes "## MapSwipe Mobile ${{ steps.release-info.outputs.tag_name }} - - ### 📱 Download APK - - **File**: \`${{ steps.release-info.outputs.apk_name }}\` - - **Size**: ${{ steps.release-info.outputs.file_size }} - - ### 📦 Installation - 1. Download the APK file below - 2. Enable \"Install from unknown sources\" in your Android settings - 3. Open the downloaded APK to install - - ### 🔧 Build Information - - **Commit**: \`${{ github.sha }}\` - - **Build**: #${{ github.run_number }} - - **Date**: $(date +'%Y-%m-%d %H:%M:%S UTC') - - ### ⚙️ Technical Details - - Built with Expo SDK 54 - - React Native 0.81.5 - - Node.js 22" \ - ${{ steps.release-info.outputs.apk_file }} + gh release create ${{ steps.tag.outputs.tag }} \ + --title "MapSwipe Mobile ${{ steps.tag.outputs.tag }}" \ + --notes "## MapSwipe Mobile ${{ steps.tag.outputs.tag }} + + ### 📱 Install + 1. Download the APK below + 2. Enable 'Install from unknown sources' in Android settings + 3. Install + + ### 🔧 Build info + - Commit: \`${{ github.sha }}\` + - Build: #${{ github.run_number }} + - Date: $(date +'%Y-%m-%d %H:%M:%S UTC') + - Package: \`org.missingmaps.mapswipe\`" \ + generated/mapswipe-${{ steps.tag.outputs.tag }}.apk diff --git a/app.config.js b/app.config.js new file mode 100644 index 0000000..dd64aa7 --- /dev/null +++ b/app.config.js @@ -0,0 +1,12 @@ +/* eslint-env node */ + +const isStaging = process.env.APP_ENV === 'staging'; + +const stagingConfig = require('./app.staging.json').expo; +const prodConfig = require('./app.prod.json').expo; + +const config = isStaging ? stagingConfig : prodConfig; + +module.exports = { + expo: config, +}; diff --git a/app.json b/app.prod.json similarity index 100% rename from app.json rename to app.prod.json diff --git a/app.staging.json b/app.staging.json new file mode 100644 index 0000000..d3684c3 --- /dev/null +++ b/app.staging.json @@ -0,0 +1,46 @@ +{ + "expo": { + "name": "MapSwipe Staging", + "slug": "mapswipe-mobile-staging", + "version": "0.0.1", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "mapswipemobilestaging", + "newArchEnabled": true, + "backgroundColor": "#ffffff", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/images/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "bundleIdentifier": "org.missingmaps.mapswipe.staging", + "backgroundColor": "#ffffff", + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/images/adaptive-icon.png", + "backgroundColor": "#ffaa00" + }, + "edgeToEdgeEnabled": true, + "predictiveBackGestureEnabled": false, + "package": "org.missingmaps.mapswipe.staging", + "userInterfaceStyle": "light" + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + "expo-system-ui", + "@maplibre/maplibre-react-native" + ], + "experiments": { + "typedRoutes": true + } + } +} diff --git a/app/(auth)/exploreGroup/[id]/index.tsx b/app/(auth)/exploreGroup/[id]/index.tsx index ff3e2f2..7e3e302 100644 --- a/app/(auth)/exploreGroup/[id]/index.tsx +++ b/app/(auth)/exploreGroup/[id]/index.tsx @@ -32,7 +32,7 @@ import Page from '@/components/Page'; import Text from '@/components/Text'; import { showAlert } from '@/components/Toast'; import { - managerDashboardUrl, + communityDashboardUrl, supportedLanguages, } from '@/constants/common'; import { SPACING_XS } from '@/constants/dimensions'; @@ -186,7 +186,7 @@ function ExploreGroup() { const handleMoreStatsClick = useCallback(() => { if (userGroupId) { - Linking.openURL(`${managerDashboardUrl}/user-group/${userGroupId}/`); + Linking.openURL(`${communityDashboardUrl}/user-group/${userGroupId}/`); } }, [userGroupId]); diff --git a/assets/images/adaptive-icon.png b/assets/images/adaptive-icon.png index 3f24e49..4a5d8ba 100644 Binary files a/assets/images/adaptive-icon.png and b/assets/images/adaptive-icon.png differ diff --git a/components/ProfileStats.tsx b/components/ProfileStats.tsx index 01e5f37..2b1a28b 100644 --- a/components/ProfileStats.tsx +++ b/components/ProfileStats.tsx @@ -11,7 +11,7 @@ import { import { useRouter } from 'expo-router'; import { - managerDashboardUrl, + communityDashboardUrl, supportedLanguages, } from '@/constants/common'; import { UserStatsQuery } from '@/generated/types/graphql'; @@ -125,7 +125,7 @@ function ProfileStats({ userStats }: {userStats: UserStatsQuery | undefined}) { const handleMoreStatsClick = useCallback(() => { if (user?.uid) { - Linking.openURL(`${managerDashboardUrl}/user/${user?.uid}/`); + Linking.openURL(`${communityDashboardUrl}/user/${user?.uid}/`); } }, [user]); diff --git a/constants/common.ts b/constants/common.ts index e3600a4..5f0a9c5 100644 --- a/constants/common.ts +++ b/constants/common.ts @@ -14,7 +14,7 @@ export const SUPPORTED_PROJECT_TYPES = [ PROJECT_TYPE_VALIDATE_IMAGE, ]; -export const managerDashboardUrl = process.env.EXPO_PUBLIC_MANAGER_DASHBOARD_URL; +export const communityDashboardUrl = process.env.EXPO_PUBLIC_COMMUNITY_DASHBOARD_URL; export const missingMapUrl = 'https://www.missingmaps.org'; export const mapSwipeWebUrl = 'https://mapswipe.org/'; export const gqlEndpoint = `${process.env.EXPO_PUBLIC_GRAPHQL_ENDPOINT}/graphql/`; diff --git a/eslint.config.js b/eslint.config.js index 86c6134..3c7c1c0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -139,7 +139,21 @@ const otherConfig = { ...js.configs.recommended, }; +const nodeConfig = { + files: ['app.config.js', 'metro.config.js', 'babel.config.js'], + ...js.configs.recommended, + languageOptions: { + globals: { + require: 'readonly', + module: 'writable', + process: 'readonly', + __dirname: 'readonly', + }, + }, +}; + export default [ ...appConfigs, otherConfig, + nodeConfig, ]; diff --git a/knip.jsonc b/knip.jsonc index 9fb7ba0..4031cb5 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -15,6 +15,8 @@ "firebase-admin", // FIXME: Remove this after type fixes are made "expo-splash-screen", + // FIXME: Remove this after type fixes are made + "expo-updates", "@typescript-eslint/eslint-plugin" ], "ignoreExportsUsedInFile": true,