diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e99a780 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +EXPO_PUBLIC_BASE_URL=https://api.example.com +EXPO_PUBLIC_WEBVIEW_URL=https://www.example.com +EXPO_PUBLIC_MIXPANEL_TOKEN= diff --git a/.github/workflows/android-check.yml b/.github/workflows/android-check.yml new file mode 100644 index 0000000..8335aeb --- /dev/null +++ b/.github/workflows/android-check.yml @@ -0,0 +1,82 @@ +name: Android Check + +on: + pull_request: + push: + branches: + - main + +jobs: + android-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Create public env file + run: | + { + echo "EXPO_PUBLIC_BASE_URL=${EXPO_PUBLIC_BASE_URL:-https://api.example.com}" + echo "EXPO_PUBLIC_WEBVIEW_URL=${EXPO_PUBLIC_WEBVIEW_URL:-https://www.example.com}" + echo "EXPO_PUBLIC_MIXPANEL_TOKEN=${EXPO_PUBLIC_MIXPANEL_TOKEN:-}" + } > .env + env: + EXPO_PUBLIC_BASE_URL: ${{ secrets.EXPO_PUBLIC_BASE_URL || vars.EXPO_PUBLIC_BASE_URL }} + EXPO_PUBLIC_WEBVIEW_URL: ${{ secrets.EXPO_PUBLIC_WEBVIEW_URL || vars.EXPO_PUBLIC_WEBVIEW_URL }} + EXPO_PUBLIC_MIXPANEL_TOKEN: ${{ secrets.EXPO_PUBLIC_MIXPANEL_TOKEN || vars.EXPO_PUBLIC_MIXPANEL_TOKEN }} + + - name: Run lint + run: npm run lint + + - name: Create placeholder google-services.json + run: | + cat > google-services.json <<'JSON' + { + "project_info": { + "project_number": "123456789012", + "project_id": "moadong-ci", + "storage_bucket": "moadong-ci.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:0000000000000000000000", + "android_client_info": { + "package_name": "com.moadong.moadong" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "DUMMY_API_KEY" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" + } + JSON + + - name: Prebuild Android + run: npx expo prebuild --platform android --clean + env: + CI: "1" + + - name: Assemble debug + working-directory: android + run: ./gradlew :app:assembleDebug diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml new file mode 100644 index 0000000..827a65a --- /dev/null +++ b/.github/workflows/android-release.yml @@ -0,0 +1,89 @@ +name: Android Release + +on: + push: + branches: + - prod + workflow_dispatch: + inputs: + lane: + description: Fastlane Android lane to run + required: true + type: choice + default: internal + options: + - internal + - production_draft + +jobs: + android-release: + runs-on: ubuntu-latest + + env: + ANDROID_PACKAGE_NAME: com.moadong.moadong + EXPO_PUBLIC_BASE_URL: ${{ secrets.EXPO_PUBLIC_BASE_URL || vars.EXPO_PUBLIC_BASE_URL }} + EXPO_PUBLIC_WEBVIEW_URL: ${{ secrets.EXPO_PUBLIC_WEBVIEW_URL || vars.EXPO_PUBLIC_WEBVIEW_URL }} + EXPO_PUBLIC_MIXPANEL_TOKEN: ${{ secrets.EXPO_PUBLIC_MIXPANEL_TOKEN || vars.EXPO_PUBLIC_MIXPANEL_TOKEN }} + SUPPLY_JSON_KEY: ${{ github.workspace }}/google-play-service-account.json + MYAPP_UPLOAD_STORE_FILE: ${{ secrets.MYAPP_UPLOAD_STORE_FILE || 'moadong-upload.keystore' }} + MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }} + MYAPP_UPLOAD_KEY_ALIAS: ${{ secrets.MYAPP_UPLOAD_KEY_ALIAS }} + MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} + FASTLANE_ANDROID_LANE: ${{ github.event_name == 'push' && 'production_draft' || github.event.inputs.lane || 'internal' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + + - name: Install Node dependencies + run: npm ci + + - name: Validate Android release secrets + run: | + test -n "$ANDROID_KEYSTORE_BASE64" + test -n "$MYAPP_UPLOAD_STORE_FILE" + test -n "$MYAPP_UPLOAD_STORE_PASSWORD" + test -n "$MYAPP_UPLOAD_KEY_ALIAS" + test -n "$MYAPP_UPLOAD_KEY_PASSWORD" + test -n "$GOOGLE_SERVICES_JSON_BASE64" + test -n "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" + test -n "$EXPO_PUBLIC_BASE_URL" + test -n "$EXPO_PUBLIC_WEBVIEW_URL" + test -n "$EXPO_PUBLIC_MIXPANEL_TOKEN" + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} + GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} + + - name: Create public env file + run: | + { + echo "EXPO_PUBLIC_BASE_URL=${EXPO_PUBLIC_BASE_URL}" + echo "EXPO_PUBLIC_WEBVIEW_URL=${EXPO_PUBLIC_WEBVIEW_URL}" + echo "EXPO_PUBLIC_MIXPANEL_TOKEN=${EXPO_PUBLIC_MIXPANEL_TOKEN}" + } > .env + + - name: Restore Android secret files + run: | + printf '%s' "$GOOGLE_SERVICES_JSON_BASE64" | base64 --decode > google-services.json + printf '%s' "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" > "$SUPPLY_JSON_KEY" + printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$MYAPP_UPLOAD_STORE_FILE" + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} + GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} + + - name: Upload Android release + run: bundle exec fastlane android "$FASTLANE_ANDROID_LANE" diff --git a/.gitignore b/.gitignore index 32194e6..743b28c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,9 @@ yarn-error.* app-example # generated native folders -/ios /android /.env google-services.json -GoogleService-Info.plist \ No newline at end of file +GoogleService-Info.plist +ios/**/GoogleService-Info.plist diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9e675e9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,90 @@ +# AGENTS.md + +이 파일은 Codex (Codex.ai/code)가 이 저장소에서 작업할 때 참고하는 가이드입니다. + +## 프로젝트 개요 + +**모아동 (Moadong)** — 대학교 동아리 탐색 및 알림 구독을 위한 React Native + Expo 앱. 사용자는 동아리를 탐색하고 푸시 알림을 구독하며, WebView를 통해 동아리 상세 페이지를 볼 수 있습니다. + +- **Bundle ID**: `com.moadong.moadong` +- **딥링크 스킴**: `moadongapp://`, associated domain: `www.moadong.com` +- **React Native New Architecture** 활성화 (`newArchEnabled: true`) +- **React Compiler** (실험적 기능) 활성화 + +## 명령어 + +```bash +npm start # Expo 개발 서버 시작 +npm run ios # iOS 시뮬레이터 실행 +npm run android # Android 에뮬레이터 실행 +npm run lint # ESLint 실행 (expo lint) +npx expo start --dev-client # 개발 클라이언트 빌드로 시작 +``` + +환경 변수: API 기본 URL은 `.env`에 `EXPO_PUBLIC_BASE_URL`로 설정합니다. + +## 아키텍처 + +### 라우팅 (Expo Router 파일 기반) +```text +app/ + _layout.tsx # 루트 레이아웃: 부트스트랩, 스플래시, 강제 업데이트, Context 프로바이더 + (tabs)/ # 하단 탭 네비게이터 + index.tsx # 홈 탭 + more.tsx # 더보기 탭 + club/[id].tsx # 동아리 상세 (WebView) + clubDetail/[id].tsx # 동아리 상세 (네이티브) + webview/[slug].tsx # 범용 WebView 화면 + modal.tsx # 모달 화면 +``` + +### 부트스트랩 순서 (app/_layout.tsx) +앱 시작 시 루트 레이아웃이 다음 순서로 실행됩니다: +1. Firebase Remote Config를 통한 강제 업데이트 체크 +2. iOS ATT (앱 추적 투명성) 권한 요청 +3. 액세스 토큰 조회/생성 (`auth-token-storage`) +4. FCM 토큰 등록 +5. 서버에서 구독 동아리 목록 동기화 +6. Mixpanel 애널리틱스 초기화 + +부트스트랩이 완료될 때까지 커스텀 스플래시 화면이 UI를 차단합니다. + +### API 레이어 (services/api.ts) +두 가지 Axios 클라이언트 인스턴스: +- `publicApi` — 인증 없는 요청 +- `authApi` — `Bearer` 토큰 자동 첨부; 401 응답 시 `/auth/student`로 토큰 자동 갱신 + +신규 코드는 항상 `authApi` / `publicApi` 헬퍼를 사용하세요. `api` (default export)는 deprecated입니다. + +### 상태 관리 +Redux/Zustand 미사용. React Context 사용: +- `SubscribedClubsProvider` (`contexts/subscribed-clubs-context.tsx`) — 구독 동아리 ID 목록, 구독 토글, 서버 동기화 +- `MixpanelProvider` (`contexts/mixpanel-context.tsx`) — 애널리틱스 + +### UI 레이어 패턴 (`ui/`) +`ui/` 하위 기능별 폴더 구조: +- `hook/` — 데이터 페칭 훅 (예: `useClubs`, `useSubscribedClubs`) +- `model/` — 파생 상태 / 데이터 변환 +- `components/` — 기능별 컴포넌트 +- `index.ts` — barrel export + +### 디자인 시스템 (constants/theme.ts) +`@/constants/theme`에서 임포트: +- `MainColors` — 오렌지 계열 팔레트 (`main` = `#FF5414`) +- `TagColors` — 카테고리별 색상 (봉사/학술/종교/취미교양/운동/공연) +- `Spacing` — 4px 기준 스케일: `xs`(4) `sm`(8) `md`(16) `lg`(24) `xl`(32) `xxl`(40) `xxxl`(48) +- `BorderRadius` — `xs`(4) `sm`(8) `md`(12) `lg`(16) `xl`(20) `full`(9999) + +폰트: **Pretendard** (Regular/Medium/SemiBold/Bold). React Native의 `Text` 대신 `@/components/moa-text`의 `` 사용. + +타이포그래피 변형: `heading1-3`, `title1-3`, `body1SemiBold`, `body1Medium`, `body1Regular`, `body2Regular`, `caption1SemiBold`, `caption1Medium`. + +### 네이밍 컨벤션 +- 파일명: `kebab-case.tsx` +- 컴포넌트: `PascalCase` +- 훅: `use` 접두사 + `camelCase` +- named export 선호; default export는 `app/` 하위 화면 컴포넌트에만 사용 +- 경로 별칭: `@/`는 프로젝트 루트를 가리킴 + +### 플랫폼별 파일 +플랫폼 오버라이드는 `.ios.tsx` / `.web.ts` 접미사 사용 (예: `icon-symbol.ios.tsx`, `use-color-scheme.web.ts`). diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..5ea9434 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane", ">= 2.228", "< 3.0" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..57b95cd --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,231 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.9) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.2) + aws-partitions (1.1109.0) + aws-sdk-core (3.224.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.101.0) + aws-sdk-core (~> 3, >= 3.216.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.188.0) + aws-sdk-core (~> 3, >= 3.224.1) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.11.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.5) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.109.0) + faraday (1.10.5) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.4) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.1) + fastlane (2.230.0) + CFPropertyList (>= 2.3, < 4.0.0) + abbrev (~> 0.1.2) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3.0) + naturally (~> 2.2) + nkf (~> 0.2.0) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.29.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.45.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.29.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + jmespath (1.6.2) + json (2.7.6) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + nkf (0.2.0) + optparse (0.8.1) + os (1.1.4) + plist (3.7.2) + public_suffix (5.1.1) + rake (13.4.2) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.4.1) + rexml (3.4.4) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + security (0.1.5) + signet (0.18.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.2.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + fastlane (>= 2.228, < 3.0) + +BUNDLED WITH + 1.17.2 diff --git a/app.config.js b/app.config.js new file mode 100644 index 0000000..5d9b286 --- /dev/null +++ b/app.config.js @@ -0,0 +1,19 @@ +const appJson = require('./app.json'); + +const getApsEnvironment = () => + process.env.IOS_APS_ENVIRONMENT === 'production' ? 'production' : 'development'; + +module.exports = () => { + const expo = appJson.expo; + + return { + ...expo, + ios: { + ...expo.ios, + entitlements: { + ...(expo.ios?.entitlements ?? {}), + 'aps-environment': getApsEnvironment(), + }, + }, + }; +}; diff --git a/app.json b/app.json index e6c1c59..16486f8 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "모아동", "slug": "moadong-app", - "version": "1.5.0", + "version": "1.5.1", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "moadongapp", @@ -10,6 +10,7 @@ "newArchEnabled": true, "ios": { "supportsTablet": false, + "buildNumber": "13", "googleServicesFile": "./GoogleService-Info.plist", "bundleIdentifier": "com.moadong.moadong", "associatedDomains": [ @@ -25,7 +26,7 @@ }, "android": { "jsEngine": "hermes", - "versionCode": 11, + "versionCode": 12, "adaptiveIcon": { "backgroundColor": "#E6F4FE", "foregroundImage": "./assets/images/android-icon-foreground.png", @@ -95,7 +96,8 @@ } ], "expo-tracking-transparency", - "./plugins/withFcmNotificationColorFix" + "./plugins/withFcmNotificationColorFix", + "./plugins/withAndroidReleaseSigning" ], "experiments": { "typedRoutes": true, diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh new file mode 100755 index 0000000..4636edf --- /dev/null +++ b/ci_scripts/ci_post_clone.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +set -eu + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) + +cd "$REPO_ROOT" + +npm ci + +decode_google_service_info_plist() { + if printf '%s' "$GOOGLE_SERVICE_INFO_PLIST_BASE64" | base64 --decode > GoogleService-Info.plist 2>/dev/null; then + return + fi + + if printf '%s' "$GOOGLE_SERVICE_INFO_PLIST_BASE64" | base64 -D > GoogleService-Info.plist 2>/dev/null; then + return + fi + + if command -v openssl >/dev/null 2>&1 && + printf '%s' "$GOOGLE_SERVICE_INFO_PLIST_BASE64" | openssl base64 -d -A > GoogleService-Info.plist 2>/dev/null; then + return + fi + + echo "Failed to decode GOOGLE_SERVICE_INFO_PLIST_BASE64" >&2 + exit 1 +} + +if [ -n "${GOOGLE_SERVICE_INFO_PLIST_BASE64:-}" ]; then + decode_google_service_info_plist +elif [ -n "${GOOGLE_SERVICE_INFO_PLIST:-}" ]; then + printf '%s' "$GOOGLE_SERVICE_INFO_PLIST" > GoogleService-Info.plist +else + echo "Missing GOOGLE_SERVICE_INFO_PLIST_BASE64 or GOOGLE_SERVICE_INFO_PLIST for iOS archive" >&2 + exit 1 +fi + +mkdir -p ios/app +cp GoogleService-Info.plist ios/app/GoogleService-Info.plist + +: "${EXPO_PUBLIC_BASE_URL:?Missing EXPO_PUBLIC_BASE_URL for iOS archive}" +: "${EXPO_PUBLIC_WEBVIEW_URL:?Missing EXPO_PUBLIC_WEBVIEW_URL for iOS archive}" +: "${EXPO_PUBLIC_MIXPANEL_TOKEN:?Missing EXPO_PUBLIC_MIXPANEL_TOKEN for iOS archive}" + +{ + echo "EXPO_PUBLIC_BASE_URL=${EXPO_PUBLIC_BASE_URL}" + echo "EXPO_PUBLIC_WEBVIEW_URL=${EXPO_PUBLIC_WEBVIEW_URL}" + echo "EXPO_PUBLIC_MIXPANEL_TOKEN=${EXPO_PUBLIC_MIXPANEL_TOKEN}" +} > .env + +APS_ENVIRONMENT="${IOS_APS_ENVIRONMENT:-development}" +ENTITLEMENTS_FILE="${IOS_ENTITLEMENTS_FILE:-ios/app/app.entitlements}" + +if [ -f "$ENTITLEMENTS_FILE" ]; then + /usr/libexec/PlistBuddy -c "Set :aps-environment $APS_ENVIRONMENT" "$ENTITLEMENTS_FILE" 2>/dev/null || + /usr/libexec/PlistBuddy -c "Add :aps-environment string $APS_ENVIRONMENT" "$ENTITLEMENTS_FILE" +fi + +cd ios +pod install diff --git a/docs/cicd.md b/docs/cicd.md new file mode 100644 index 0000000..782de53 --- /dev/null +++ b/docs/cicd.md @@ -0,0 +1,304 @@ +# CI/CD 운영 계획 + +이 문서는 모아동 Expo React Native 앱의 Android/iOS 배포 전략과 저장소에 추가할 CI/CD 구성 계획을 정리한다. + +## 기본 원칙 + +- EAS Build는 사용하지 않는다. +- Android는 GitHub Actions, fastlane, Expo prebuild를 사용해 Google Play에 배포한다. +- `prod` 브랜치에 머지되면 Android release AAB를 Google Play production 트랙의 draft release로 업로드한다. +- Android internal testing 업로드는 수동 GitHub Actions 실행으로 유지한다. +- iOS는 비용 문제로 Xcode Cloud의 Archive + TestFlight 배포를 우선 사용한다. +- 실제 secret 값, keystore, Firebase/Google/Apple 계정 파일은 저장소에 커밋하지 않는다. +- `EXPO_PUBLIC_*` 값은 클라이언트 번들에 포함되므로 비밀값으로 취급하지 않는다. + +## 환경 변수 + +앱 실행과 CI에서 필요한 공개 환경 변수는 `.env.example`에 문서화한다. + +| 이름 | 필수 | 설명 | +| --- | --- | --- | +| `EXPO_PUBLIC_BASE_URL` | O | API 서버 base URL | +| `EXPO_PUBLIC_WEBVIEW_URL` | O | WebView 화면에서 사용할 웹 base URL | +| `EXPO_PUBLIC_MIXPANEL_TOKEN` | O | Mixpanel 프로젝트 토큰. 번들에 포함되는 공개값 | + +GitHub Actions에서는 가능하면 GitHub Variables에 `EXPO_PUBLIC_*` 값을 둔다. Secrets에 넣어도 동작은 하며, 현재 workflow는 Secrets를 먼저 읽고 없으면 Variables를 사용한다. 단, `EXPO_PUBLIC_*` 값은 번들에 포함되는 공개값이라는 점을 운영자가 오해하지 않아야 한다. + +## 커밋 금지 파일 + +다음 파일은 로컬 또는 CI에서만 생성하고 저장소에 커밋하지 않는다. + +- `.env` +- `google-services.json` +- `GoogleService-Info.plist` +- `*.jks` +- `*.keystore` +- `*.p8` +- `*.p12` +- `*.mobileprovision` + +현재 `.gitignore`에는 `/android`, `.env`, `google-services.json`, `GoogleService-Info.plist`가 포함되어 있다. Android는 prebuild 산출물을 커밋하지 않는 흐름을 유지한다. +또한 iOS 네이티브 프로젝트를 버전 관리하기 위해 `/ios` ignore 규칙은 제거한다. + +## GitHub Actions 트리거 + +현재 설정된 workflow 트리거는 다음과 같다. + +| Workflow | 파일 | 트리거 | 동작 | +| --- | --- | --- | --- | +| Android Check | `.github/workflows/android-check.yml` | 모든 `pull_request`, `main` 브랜치 `push` | lint, Android prebuild, debug assemble 검증 | +| Android Release | `.github/workflows/android-release.yml` | `prod` 브랜치 `push` | production 트랙에 draft release AAB 업로드 | +| Android Release | `.github/workflows/android-release.yml` | `workflow_dispatch` 수동 실행 | 선택한 fastlane lane 실행. 기본값은 `internal` | + +GitHub에서 PR을 `prod` 브랜치로 merge하면 `prod` 브랜치에 push 이벤트가 발생하므로 Android Release workflow가 자동 실행된다. 직접 push도 같은 이벤트로 취급된다. + +## Android 배포 흐름 + +Android production draft release는 `prod` 브랜치에 merge될 때 자동으로 시작한다. + +1. `prod` 브랜치 push로 `.github/workflows/android-release.yml`이 실행된다. +2. workflow가 `npm ci`로 의존성을 설치한다. +3. GitHub Secrets 또는 Variables의 `EXPO_PUBLIC_*` 값을 사용해 `.env`를 생성한다. +4. GitHub Secrets의 Firebase/Play/keystore 값을 임시 파일로 복원한다. +5. `bundle exec fastlane android production_draft`를 실행한다. +6. fastlane lane이 `CI=1 npx expo prebuild --platform android --clean`를 실행한다. +7. config plugin이 release signing config를 `android/app/build.gradle`에 반영한다. +8. Gradle이 release AAB를 생성한다. +9. fastlane `upload_to_play_store(track: "production", release_status: "draft")`로 Google Play production 트랙에 draft release를 생성한다. + +수동으로 internal testing 배포가 필요하면 GitHub Actions에서 Android Release workflow를 `workflow_dispatch`로 실행하고 기본 lane인 `internal`을 선택한다. + +### Android GitHub Secrets + +| 이름 | 설명 | +| --- | --- | +| `ANDROID_KEYSTORE_BASE64` | 업로드 keystore 파일을 base64로 인코딩한 값 | +| `MYAPP_UPLOAD_STORE_FILE` | CI에서 복원할 upload keystore 파일명. 권장값은 `moadong-upload.keystore` | +| `MYAPP_UPLOAD_STORE_PASSWORD` | upload keystore password | +| `MYAPP_UPLOAD_KEY_ALIAS` | upload key alias | +| `MYAPP_UPLOAD_KEY_PASSWORD` | upload key password | +| `GOOGLE_SERVICES_JSON_BASE64` | Android Firebase 설정 JSON 전체 내용을 base64로 인코딩한 값 | +| `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` | Google Play Android Publisher API service account JSON 전체 내용 | +| `EXPO_PUBLIC_BASE_URL` | API 서버 base URL. Secrets 또는 Variables에 설정 가능 | +| `EXPO_PUBLIC_WEBVIEW_URL` | WebView 화면에서 사용할 웹 base URL. Secrets 또는 Variables에 설정 가능 | +| `EXPO_PUBLIC_MIXPANEL_TOKEN` | Mixpanel 프로젝트 토큰. Secrets 또는 Variables에 설정 가능 | + +`MYAPP_UPLOAD_STORE_FILE`을 설정하지 않으면 workflow 기본값은 `moadong-upload.keystore`다. + +### Android release signing + +Expo prebuild 이후 Android native 파일은 재생성될 수 있으므로 release signing 설정은 수동 편집 대신 Expo config plugin으로 주입한다. + +추가할 plugin: + +```text +plugins/withAndroidReleaseSigning.js +``` + +`app.json`의 `plugins` 배열에는 다음 항목을 추가한다. + +```json +"./plugins/withAndroidReleaseSigning" +``` + +plugin은 `android/app/build.gradle`에 다음 Gradle properties 기반 signing config를 idempotent하게 삽입한다. + +```properties +MYAPP_UPLOAD_STORE_FILE=moadong-upload.keystore +MYAPP_UPLOAD_STORE_PASSWORD=... +MYAPP_UPLOAD_KEY_ALIAS=... +MYAPP_UPLOAD_KEY_PASSWORD=... +``` + +값은 로컬 또는 CI의 `android/gradle.properties`에서만 공급한다. 저장소에는 실제 비밀번호나 keystore를 넣지 않는다. + +## Android check workflow + +`.github/workflows/android-check.yml`은 PR/push 검증용으로 둔다. + +검증 단계: + +1. `npm ci` +2. `npm run lint` +3. CI용 placeholder `google-services.json` 생성 +4. `CI=1 npx expo prebuild --platform android --clean` +5. `cd android && ./gradlew :app:assembleDebug` + +이 workflow는 배포를 하지 않으며, secret 없이 실행 가능해야 한다. Firebase 실제 프로젝트 파일 대신 placeholder를 사용해 prebuild와 debug assemble만 확인한다. + +## fastlane 구성 + +추가할 파일: + +```text +Gemfile +fastlane/Appfile +fastlane/Fastfile +``` + +`Gemfile`은 fastlane 실행 환경을 고정한다. + +`fastlane/Appfile`은 Android package name과 service account JSON 경로를 환경 변수 기반으로 둔다. + +권장 환경 변수: + +| 이름 | 설명 | +| --- | --- | +| `SUPPLY_JSON_KEY` | Google Play service account JSON 파일 경로 | +| `ANDROID_PACKAGE_NAME` | 기본값 `com.moadong.moadong` | + +`fastlane/Fastfile`에는 `android internal`, `android production_draft` lane을 둔다. + +공통 빌드 책임: + +- 필요한 CI 파일이 있는지 확인한다. +- Android prebuild를 실행한다. +- `android/gradle.properties`에 signing property를 작성한다. +- `./gradlew bundleRelease`로 AAB를 만든다. + +lane별 업로드 책임: + +- `upload_to_play_store(track: "internal")`로 internal testing에 업로드한다. +- `upload_to_play_store(track: "production", release_status: "draft")`로 production draft release를 만든다. +- 초기 도입 단계에서는 metadata, images, screenshots 업로드를 skip한다. + +## iOS Xcode Cloud 전략 + +iOS는 Xcode Cloud의 Archive + TestFlight 배포를 기본 경로로 사용한다. fastlane 기반 iOS 배포는 fallback 문서로만 남기고, 기본 구현 대상에는 포함하지 않는다. + +### ios 폴더 커밋 전략 + +Xcode Cloud가 scheme을 안정적으로 인식하려면 `ios/`와 shared scheme이 저장소에 있어야 한다. + +Xcode Cloud 연결을 위해 `/ios` ignore 규칙은 제거하고, `ios/` 전체를 커밋 대상으로 둔다. Xcode project, workspace, shared scheme, entitlements, Podfile 변경 이력을 일반 Git 변경으로 관리하기 위함이다. + +단, 다음 파일과 산출물은 계속 커밋하지 않는다. + +- `ios/**/GoogleService-Info.plist` +- `ios/Pods/` +- `ios/build/` +- `ios/**/*.xcuserstate` +- `ios/**/xcuserdata/` + +native dependency, Expo plugin, app config, bundle identifier, entitlements, Firebase iOS 설정 방식이 바뀌면 로컬에서 다음 명령으로 iOS 산출물을 갱신한 뒤 커밋한다. + +```bash +npx expo prebuild --platform ios --clean +``` + +### Xcode Cloud post clone script + +추가할 파일: + +```text +ci_scripts/ci_post_clone.sh +``` + +script 책임: + +- npm 의존성 설치 +- Xcode Cloud 환경 변수에서 `GoogleService-Info.plist`를 루트와 `ios/app/`에 복원 +- Xcode Cloud 환경 변수에서 `.env` 생성 +- `IOS_APS_ENVIRONMENT` 값에 따라 entitlement의 `aps-environment` 보정 +- CocoaPods 설치 + +Xcode Cloud에서는 기본값을 `IOS_APS_ENVIRONMENT=production`으로 둔다. 로컬 개발이나 debug archive는 값을 지정하지 않거나 `development`로 둔다. + +### iOS Xcode Cloud 환경 변수 + +Xcode Cloud workflow에는 다음 환경 변수를 설정한다. + +| 이름 | 필수 | 설명 | +| --- | --- | --- | +| `GOOGLE_SERVICE_INFO_PLIST_BASE64` | O | iOS Firebase 설정 plist 전체 내용을 base64로 인코딩한 값 | +| `GOOGLE_SERVICE_INFO_PLIST` | 대체 | base64 대신 plist 원문을 넣을 때 사용 | +| `IOS_APS_ENVIRONMENT` | O | TestFlight/Archive 기본값 `production` | +| `EXPO_PUBLIC_BASE_URL` | O | API 서버 base URL | +| `EXPO_PUBLIC_WEBVIEW_URL` | O | WebView 화면에서 사용할 웹 base URL | +| `EXPO_PUBLIC_MIXPANEL_TOKEN` | O | Mixpanel 프로젝트 토큰 | + +`GOOGLE_SERVICE_INFO_PLIST_BASE64`와 `GOOGLE_SERVICE_INFO_PLIST` 중 하나만 있으면 된다. Xcode Cloud에서는 secret 값 줄바꿈 이슈를 줄이기 위해 base64 방식을 권장한다. + +### iOS build number + +TestFlight 업로드마다 iOS build number는 이전 업로드보다 커야 한다. `app.json`의 `ios.buildNumber`를 배포 전 증가시키고, `npx expo prebuild --platform ios --clean`으로 native project에 반영한 뒤 커밋한다. + +현재 기준값: + +```json +"buildNumber": "13" +``` + +App Store Connect에 이미 더 큰 build number가 올라가 있다면 그보다 큰 값으로 조정한다. + +### Apple/Firebase 콘솔 체크리스트 + +iOS Archive 전에 외부 콘솔에서 다음 설정을 확인한다. + +- Apple Developer의 App ID `com.moadong.moadong`에 Push Notifications와 Associated Domains capability를 활성화한다. +- Xcode Cloud에서 repository, scheme `app`, Archive action을 연결한다. +- Signing은 Apple Developer Team `2QMK9GBWN6`에서 자동 signing이 가능해야 한다. +- Firebase iOS app의 bundle identifier가 `com.moadong.moadong`인지 확인한다. +- Firebase Cloud Messaging에 APNs auth key 또는 certificate를 등록한다. +- Associated Domains에 필요한 `apple-app-site-association` 파일이 `https://www.moadong.com/.well-known/apple-app-site-association`에서 제공되는지 확인한다. + +## Expo config 전환 + +iOS entitlement의 `aps-environment`를 환경별로 바꾸기 위해 `app.config.js`를 추가한다. + +전략: + +- 기존 `app.json`은 정적 설정의 source of truth로 유지한다. +- `app.config.js`는 `app.json`을 읽고 필요한 값만 환경 변수에 따라 보정한다. +- `IOS_APS_ENVIRONMENT=production`이면 `ios.entitlements["aps-environment"]`를 `production`으로 설정한다. +- 그 외에는 `development`로 설정한다. + +확인 명령: + +```bash +npx expo config --type public +IOS_APS_ENVIRONMENT=production npx expo config --type public +``` + +## 구현 대상 파일 + +최종 구현 시 추가하거나 수정할 파일은 다음과 같다. + +```text +docs/cicd.md +.env.example +.gitignore +app.config.js +app.json +ios/ +plugins/withAndroidReleaseSigning.js +Gemfile +fastlane/Appfile +fastlane/Fastfile +.github/workflows/android-check.yml +.github/workflows/android-release.yml +ci_scripts/ci_post_clone.sh +``` + +secret 값, keystore, `google-services.json`, `GoogleService-Info.plist`는 구현 대상 파일에 포함하지 않는다. + +## 검증 계획 + +구현 완료 후 다음 순서로 확인한다. + +```bash +npm run lint +npx expo config --type public +IOS_APS_ENVIRONMENT=production npx expo config --type public +CI=1 npx expo prebuild --platform android --clean +cd android && ./gradlew :app:assembleDebug +``` + +검증 포인트: + +- Expo config가 기존 앱 설정을 유지한다. +- `IOS_APS_ENVIRONMENT=production`일 때 `aps-environment`가 `production`으로 바뀐다. +- Android prebuild 후 `android/app/build.gradle`에 release signing config가 중복 없이 들어간다. +- debug assemble은 secret 없이 placeholder Firebase 파일로 통과한다. +- release workflow는 `prod` 브랜치 push에서 Google Play production draft release 업로드를 수행한다. +- release workflow를 수동 실행하면 선택한 lane을 실행하며, 기본값은 internal testing 업로드다. diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..3827309 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +json_key_file(ENV["SUPPLY_JSON_KEY"]) if ENV["SUPPLY_JSON_KEY"] +package_name(ENV.fetch("ANDROID_PACKAGE_NAME", "com.moadong.moadong")) diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..44745b6 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,97 @@ +require "fileutils" + +default_platform(:android) + +def require_file!(path, label) + UI.user_error!("#{label} is missing: #{path}") unless path && File.exist?(path) +end + +def require_env!(name) + value = ENV[name] + UI.user_error!("Missing required environment variable: #{name}") if value.to_s.empty? + value +end + +def upsert_gradle_properties(path, values) + lines = File.exist?(path) ? File.readlines(path, chomp: true) : [] + keys = values.keys + filtered_lines = lines.reject do |line| + key = line.split("=", 2).first + keys.include?(key) + end + + File.write(path, (filtered_lines + values.map { |key, value| "#{key}=#{value}" }).join("\n") + "\n") +end + +def build_android_release_aab + json_key = require_env!("SUPPLY_JSON_KEY") + keystore_name = ENV.fetch("MYAPP_UPLOAD_STORE_FILE", "moadong-upload.keystore") + + require_file!(json_key, "Google Play service account JSON") + require_file!("google-services.json", "Android Firebase config") + require_file!(keystore_name, "Android upload keystore") + + store_password = require_env!("MYAPP_UPLOAD_STORE_PASSWORD") + key_alias = require_env!("MYAPP_UPLOAD_KEY_ALIAS") + key_password = require_env!("MYAPP_UPLOAD_KEY_PASSWORD") + + sh("CI=1 npx expo prebuild --platform android --clean") + + android_app_keystore = File.join("android", "app", File.basename(keystore_name)) + FileUtils.cp(keystore_name, android_app_keystore) + + upsert_gradle_properties( + File.join("android", "gradle.properties"), + { + "MYAPP_UPLOAD_STORE_FILE" => File.basename(keystore_name), + "MYAPP_UPLOAD_STORE_PASSWORD" => store_password, + "MYAPP_UPLOAD_KEY_ALIAS" => key_alias, + "MYAPP_UPLOAD_KEY_PASSWORD" => key_password, + } + ) + + Dir.chdir("android") do + sh("./gradlew bundleRelease") + end + + "android/app/build/outputs/bundle/release/app-release.aab" +end + +platform :android do + desc "Build and upload the Android release AAB to Google Play internal testing" + lane :internal do + package_name = ENV.fetch("ANDROID_PACKAGE_NAME", "com.moadong.moadong") + json_key = require_env!("SUPPLY_JSON_KEY") + aab_path = build_android_release_aab + + upload_to_play_store( + package_name: package_name, + json_key: json_key, + track: "internal", + aab: aab_path, + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true, + skip_upload_changelogs: true + ) + end + + desc "Build and upload the Android release AAB as a draft production release" + lane :production_draft do + package_name = ENV.fetch("ANDROID_PACKAGE_NAME", "com.moadong.moadong") + json_key = require_env!("SUPPLY_JSON_KEY") + aab_path = build_android_release_aab + + upload_to_play_store( + package_name: package_name, + json_key: json_key, + track: "production", + release_status: "draft", + aab: aab_path, + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true, + skip_upload_changelogs: true + ) + end +end diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..8beb344 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,30 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +.xcode.env.local + +# Bundle artifacts +*.jsbundle + +# CocoaPods +/Pods/ diff --git a/ios/.xcode.env b/ios/.xcode.env new file mode 100644 index 0000000..3d5782c --- /dev/null +++ b/ios/.xcode.env @@ -0,0 +1,11 @@ +# This `.xcode.env` file is versioned and is used to source the environment +# used when running script phases inside Xcode. +# To customize your local environment, you can create an `.xcode.env.local` +# file that is not versioned. + +# NODE_BINARY variable contains the PATH to the node executable. +# +# Customize the NODE_BINARY variable here. +# For example, to use nvm with brew, add the following line +# . "$(brew --prefix nvm)/nvm.sh" --no-use +export NODE_BINARY=$(command -v node) diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..c7828f0 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,63 @@ +require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") +require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") + +require 'json' +podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} + +def ccache_enabled?(podfile_properties) + # Environment variable takes precedence + return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE'] + + # Fall back to Podfile properties + podfile_properties['apple.ccacheEnabled'] == 'true' +end + +ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false' +ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] +ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false' +ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false' +platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' + +prepare_react_native_project! + +target 'app' do + use_expo_modules! + + if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' + config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; + else + config_command = [ + 'node', + '--no-warnings', + '--eval', + 'require(\'expo/bin/autolinking\')', + 'expo-modules-autolinking', + 'react-native-config', + '--json', + '--platform', + 'ios' + ] + end + + config = use_native_modules!(config_command) + + use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] + use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] + + use_react_native!( + :path => config[:reactNativePath], + :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/..", + :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', + ) + + post_install do |installer| + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + :ccache_enabled => ccache_enabled?(podfile_properties), + ) + end +end diff --git a/ios/Podfile.properties.json b/ios/Podfile.properties.json new file mode 100644 index 0000000..cc6ac36 --- /dev/null +++ b/ios/Podfile.properties.json @@ -0,0 +1,10 @@ +{ + "expo.jsEngine": "hermes", + "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", + "newArchEnabled": "true", + "ios.useFrameworks": "static", + "ios.forceStaticLinking": "[]", + "apple.extraPods": "[{\"name\":\"FirebaseAuth\",\"modular_headers\":true}]", + "apple.privacyManifestAggregationEnabled": "true", + "ios.buildReactNativeFromSource": "true" +} diff --git a/ios/app.xcodeproj/project.pbxproj b/ios/app.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c56e047 --- /dev/null +++ b/ios/app.xcodeproj/project.pbxproj @@ -0,0 +1,442 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; + F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; }; + 0FF7848F61AA47FAA6C76782 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 77AE80BD50D547E1A8A56CC6 /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 13B07F961A680F5B00A75B9A /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = app/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = app/Info.plist; sourceTree = ""; }; + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = app/SplashScreen.storyboard; sourceTree = ""; }; + BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = app/AppDelegate.swift; sourceTree = ""; }; + F11748442D0722820044C1D9 /* app-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "app-Bridging-Header.h"; path = "app/app-Bridging-Header.h"; sourceTree = ""; }; + 77AE80BD50D547E1A8A56CC6 /* GoogleService-Info.plist */ = {isa = PBXFileReference; name = "GoogleService-Info.plist"; path = "app/GoogleService-Info.plist"; sourceTree = ""; fileEncoding = 4; lastKnownFileType = text.plist.xml; explicitFileType = undefined; includeInIndex = 0; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* app */ = { + isa = PBXGroup; + children = ( + F11748412D0307B40044C1D9 /* AppDelegate.swift */, + F11748442D0722820044C1D9 /* app-Bridging-Header.h */, + BB2F792B24A3F905000567C9 /* Supporting */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, + 77AE80BD50D547E1A8A56CC6 /* GoogleService-Info.plist */, + ); + name = app; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* app */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* app.app */, + ); + name = Products; + sourceTree = ""; + }; + BB2F792B24A3F905000567C9 /* Supporting */ = { + isa = PBXGroup; + children = ( + BB2F792C24A3F905000567C9 /* Expo.plist */, + ); + name = Supporting; + path = app/Supporting; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* app */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "app" */; + buildPhases = ( + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = app; + productName = app; + productReference = 13B07F961A680F5B00A75B9A /* app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1130; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1250; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "app" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* app */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, + 0FF7848F61AA47FAA6C76782 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env", + "$(SRCROOT)/.xcode.env.local", + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; + }; + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-app-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FB_SONARKIT_ENABLED=1", + ); + INFOPLIST_FILE = app/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.moadong.moadong"; + PRODUCT_NAME = "app"; + SWIFT_OBJC_BRIDGING_HEADER = "app/app-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + DEVELOPMENT_TEAM = 2QMK9GBWN6; + TARGETED_DEVICE_FAMILY = "1"; + CODE_SIGN_ENTITLEMENTS = app/app.entitlements; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = app/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.moadong.moadong"; + PRODUCT_NAME = "app"; + SWIFT_OBJC_BRIDGING_HEADER = "app/app-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + DEVELOPMENT_TEAM = 2QMK9GBWN6; + TARGETED_DEVICE_FAMILY = "1"; + CODE_SIGN_ENTITLEMENTS = app/app.entitlements; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = "\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = "\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "app" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "app" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme b/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme new file mode 100644 index 0000000..7deaa48 --- /dev/null +++ b/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/app/AppDelegate.swift b/ios/app/AppDelegate.swift new file mode 100644 index 0000000..054eb52 --- /dev/null +++ b/ios/app/AppDelegate.swift @@ -0,0 +1,80 @@ +import Expo +import FirebaseCore +import React +import ReactAppDependencyProvider + +@UIApplicationMain +public class AppDelegate: ExpoAppDelegate { + var window: UIWindow? + + var reactNativeDelegate: ExpoReactNativeFactoryDelegate? + var reactNativeFactory: RCTReactNativeFactory? + + public override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + let delegate = ReactNativeDelegate() + let factory = ExpoReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeDelegate = delegate + reactNativeFactory = factory + bindReactNativeFactory(factory) + +#if os(iOS) || os(tvOS) + window = UIWindow(frame: UIScreen.main.bounds) +// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-10e8520570672fd76b2403b7e1e27f5198a6349a +FirebaseApp.configure() +// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions + factory.startReactNative( + withModuleName: "main", + in: window, + launchOptions: launchOptions) +#endif + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + // Linking API + public override func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { +// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY) + if url.host?.lowercased() == "firebaseauth" { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return false + } +// @generated end @react-native-firebase/auth-openURL + return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) + } + + // Universal Links + public override func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result + } +} + +class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { + // Extension point for config-plugins + + override func sourceURL(for bridge: RCTBridge) -> URL? { + // needed to return the correct URL for expo-dev-client. + bridge.bundleURL ?? bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") +#else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} diff --git a/ios/app/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/ios/app/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png new file mode 100644 index 0000000..78150a6 Binary files /dev/null and b/ios/app/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..90d8d4c --- /dev/null +++ b/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": [ + { + "filename": "App-Icon-1024x1024@1x.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/ios/app/Images.xcassets/Contents.json b/ios/app/Images.xcassets/Contents.json new file mode 100644 index 0000000..ed285c2 --- /dev/null +++ b/ios/app/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "expo" + } +} diff --git a/ios/app/Images.xcassets/SplashScreenBackground.colorset/Contents.json b/ios/app/Images.xcassets/SplashScreenBackground.colorset/Contents.json new file mode 100644 index 0000000..8c50dd5 --- /dev/null +++ b/ios/app/Images.xcassets/SplashScreenBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors": [ + { + "color": { + "components": { + "alpha": "1.000", + "blue": "0.00000000000000", + "green": "0.270588235294118", + "red": "1.00000000000000" + }, + "color-space": "srgb" + }, + "idiom": "universal" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/ios/app/Images.xcassets/SplashScreenLogo.imageset/Contents.json b/ios/app/Images.xcassets/SplashScreenLogo.imageset/Contents.json new file mode 100644 index 0000000..f65c008 --- /dev/null +++ b/ios/app/Images.xcassets/SplashScreenLogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "image.png", + "scale": "1x" + }, + { + "idiom": "universal", + "filename": "image@2x.png", + "scale": "2x" + }, + { + "idiom": "universal", + "filename": "image@3x.png", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/ios/app/Images.xcassets/SplashScreenLogo.imageset/image.png b/ios/app/Images.xcassets/SplashScreenLogo.imageset/image.png new file mode 100644 index 0000000..7e27c1d Binary files /dev/null and b/ios/app/Images.xcassets/SplashScreenLogo.imageset/image.png differ diff --git a/ios/app/Images.xcassets/SplashScreenLogo.imageset/image@2x.png b/ios/app/Images.xcassets/SplashScreenLogo.imageset/image@2x.png new file mode 100644 index 0000000..40f6fcc Binary files /dev/null and b/ios/app/Images.xcassets/SplashScreenLogo.imageset/image@2x.png differ diff --git a/ios/app/Images.xcassets/SplashScreenLogo.imageset/image@3x.png b/ios/app/Images.xcassets/SplashScreenLogo.imageset/image@3x.png new file mode 100644 index 0000000..47804a8 Binary files /dev/null and b/ios/app/Images.xcassets/SplashScreenLogo.imageset/image@3x.png differ diff --git a/ios/app/Info.plist b/ios/app/Info.plist new file mode 100644 index 0000000..1d3700c --- /dev/null +++ b/ios/app/Info.plist @@ -0,0 +1,82 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + 모아동 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.5.1 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + moadongapp + com.moadong.moadong + + + + CFBundleURLSchemes + + app-1-514481945377-ios-60c148e20418b9fb2fdc60 + + + + CFBundleVersion + 13 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + NSUserActivityTypes + + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + + NSUserTrackingUsageDescription + 동아리 지원 시 입력한 이름과 전화번호를 동아리 관리자에게 전달하는 용도로만 사용됩니다. + RCTNewArchEnabled + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UIUserInterfaceStyle + Light + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/ios/app/SplashScreen.storyboard b/ios/app/SplashScreen.storyboard new file mode 100644 index 0000000..60e1988 --- /dev/null +++ b/ios/app/SplashScreen.storyboard @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ios/app/Supporting/Expo.plist b/ios/app/Supporting/Expo.plist new file mode 100644 index 0000000..750be02 --- /dev/null +++ b/ios/app/Supporting/Expo.plist @@ -0,0 +1,12 @@ + + + + + EXUpdatesCheckOnLaunch + ALWAYS + EXUpdatesEnabled + + EXUpdatesLaunchWaitMs + 0 + + \ No newline at end of file diff --git a/ios/app/app-Bridging-Header.h b/ios/app/app-Bridging-Header.h new file mode 100644 index 0000000..8361941 --- /dev/null +++ b/ios/app/app-Bridging-Header.h @@ -0,0 +1,3 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// diff --git a/ios/app/app.entitlements b/ios/app/app.entitlements new file mode 100644 index 0000000..fee19fc --- /dev/null +++ b/ios/app/app.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.developer.associated-domains + + applinks:www.moadong.com + + + \ No newline at end of file diff --git a/plugins/withAndroidReleaseSigning.js b/plugins/withAndroidReleaseSigning.js new file mode 100644 index 0000000..2addf58 --- /dev/null +++ b/plugins/withAndroidReleaseSigning.js @@ -0,0 +1,142 @@ +const { withAppBuildGradle } = require('@expo/config-plugins'); + +const RELEASE_SIGNING_CONFIG = ` release { + if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) { + storeFile file(MYAPP_UPLOAD_STORE_FILE) + storePassword MYAPP_UPLOAD_STORE_PASSWORD + keyAlias MYAPP_UPLOAD_KEY_ALIAS + keyPassword MYAPP_UPLOAD_KEY_PASSWORD + } + }`; + +const SIGNING_CONFIGS_BLOCK = ` signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + +${RELEASE_SIGNING_CONFIG} + }`; + +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +function findBlock(source, name, fromIndex = 0) { + const pattern = new RegExp(`\\b${escapeRegExp(name)}\\s*\\{`, 'g'); + pattern.lastIndex = fromIndex; + + const match = pattern.exec(source); + if (!match) { + return null; + } + + const openBraceIndex = source.indexOf('{', match.index); + let depth = 0; + + for (let index = openBraceIndex; index < source.length; index += 1) { + const character = source[index]; + + if (character === '{') { + depth += 1; + } else if (character === '}') { + depth -= 1; + } + + if (depth === 0) { + return { + start: match.index, + openBraceIndex, + closeBraceIndex: index, + end: index + 1, + }; + } + } + + return null; +} + +function insertBeforeBlockClose(source, block, content) { + const closingLineStart = source.lastIndexOf('\n', block.closeBraceIndex) + 1; + return `${source.slice(0, closingLineStart)}${content}\n${source.slice(closingLineStart)}`; +} + +function ensureReleaseSigningConfig(source, androidBlock) { + const signingConfigs = findBlock(source, 'signingConfigs', androidBlock.openBraceIndex); + + if (!signingConfigs || signingConfigs.start > androidBlock.closeBraceIndex) { + const defaultConfig = findBlock(source, 'defaultConfig', androidBlock.openBraceIndex); + if (!defaultConfig || defaultConfig.start > androidBlock.closeBraceIndex) { + return insertBeforeBlockClose(source, androidBlock, SIGNING_CONFIGS_BLOCK); + } + + return `${source.slice(0, defaultConfig.end)}\n${SIGNING_CONFIGS_BLOCK}\n${source.slice(defaultConfig.end)}`; + } + + const releaseSigningConfig = findBlock(source, 'release', signingConfigs.openBraceIndex); + + if ( + releaseSigningConfig && + releaseSigningConfig.start < signingConfigs.closeBraceIndex && + source.slice(releaseSigningConfig.start, releaseSigningConfig.end).includes('MYAPP_UPLOAD_STORE_FILE') + ) { + return source; + } + + if (releaseSigningConfig && releaseSigningConfig.start < signingConfigs.closeBraceIndex) { + return `${source.slice(0, releaseSigningConfig.start)}${RELEASE_SIGNING_CONFIG}${source.slice( + releaseSigningConfig.end + )}`; + } + + return insertBeforeBlockClose(source, signingConfigs, RELEASE_SIGNING_CONFIG); +} + +function ensureReleaseBuildTypeUsesSigningConfig(source, androidBlock) { + const buildTypes = findBlock(source, 'buildTypes', androidBlock.openBraceIndex); + if (!buildTypes || buildTypes.start > androidBlock.closeBraceIndex) { + return source; + } + + const releaseBuildType = findBlock(source, 'release', buildTypes.openBraceIndex); + if (!releaseBuildType || releaseBuildType.start > buildTypes.closeBraceIndex) { + return source; + } + + const releaseBlock = source.slice(releaseBuildType.start, releaseBuildType.end); + + if (releaseBlock.includes('signingConfig signingConfigs.release')) { + return source; + } + + if (releaseBlock.includes('signingConfig signingConfigs.debug')) { + return `${source.slice(0, releaseBuildType.start)}${releaseBlock.replace( + 'signingConfig signingConfigs.debug', + 'signingConfig signingConfigs.release' + )}${source.slice(releaseBuildType.end)}`; + } + + const insertionPoint = releaseBuildType.openBraceIndex + 1; + return `${source.slice(0, insertionPoint)}\n signingConfig signingConfigs.release${source.slice( + insertionPoint + )}`; +} + +function addAndroidReleaseSigning(source) { + const androidBlock = findBlock(source, 'android'); + if (!androidBlock) { + return source; + } + + const withSigningConfig = ensureReleaseSigningConfig(source, androidBlock); + const updatedAndroidBlock = findBlock(withSigningConfig, 'android'); + + return ensureReleaseBuildTypeUsesSigningConfig(withSigningConfig, updatedAndroidBlock); +} + +module.exports = function withAndroidReleaseSigning(config) { + return withAppBuildGradle(config, (appBuildGradleConfig) => { + appBuildGradleConfig.modResults.contents = addAndroidReleaseSigning(appBuildGradleConfig.modResults.contents); + return appBuildGradleConfig; + }); +}; diff --git a/ui/home/components/banner.tsx b/ui/home/components/banner.tsx index 4f26206..69f26a5 100644 --- a/ui/home/components/banner.tsx +++ b/ui/home/components/banner.tsx @@ -37,7 +37,6 @@ interface BannerResponse { }; } -const APP_STORE_LINK = process.env.EXPO_PUBLIC_APP_STORE_LINK; const CLUB_FESTIVAL_LINK = "CLUB_FESTIVAL"; function resolveLinkTo(linkTo?: string | null): string | null { @@ -45,10 +44,6 @@ function resolveLinkTo(linkTo?: string | null): string | null { return null; } - if (linkTo === "APP_STORE_LINK") { - return APP_STORE_LINK ?? null; - } - return linkTo; }