Skip to content

release 1.5.2#22

Merged
SeongHoonC merged 3 commits into
prodfrom
main
Jun 2, 2026
Merged

release 1.5.2#22
SeongHoonC merged 3 commits into
prodfrom
main

Conversation

@SeongHoonC
Copy link
Copy Markdown
Collaborator

@SeongHoonC SeongHoonC commented Jun 2, 2026

변경 사항

  • 앱 부트스트랩 및 트래킹 초기화 흐름을 개선했습니다.
  • 커스텀 스플래시가 인트로 애니메이션 완료와 최소 표시 시간을 만족한 뒤 전환되도록 수정했습니다.
  • iOS prebuild/archive 안정화를 위한 Expo config plugin과 지원 파일을 추가했습니다.

검증

  • npm run lint
  • npx tsc --noEmit

Summary by CodeRabbit

  • Chores
    • 안드로이드 릴리스 빌드 난독화 및 리소스 축소 기능 활성화
    • iOS 빌드 프로세스 개선 및 자동화 스크립트 추가
    • 앱 초기화 흐름 최적화로 시작 성능 개선
    • 스플래시 스크린 애니메이션 단계별 처리 정밀화
    • 강제 업데이트 검사 로직 캐시 기반으로 개선

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

워크스루

이 PR은 앱의 부트스트랩 프로세스를 재설계하여 초기화 결과를 확장하고, Mixpanel과 구독 클럽 context의 초기화를 최적화하며, iOS 빌드를 위한 Expo prebuild 플러그인과 지원 스크립트를 추가합니다. 강제 업데이트 판정을 캐시/리프레시 모드로 분리하고, 스플래시 애니메이션을 단계 기반 제어로 개선하며, import 경로를 정규화합니다.

변경 사항

부트스트랩 및 앱 초기화 생명주기

Layer / File(s) 요약
부트스트랩 결과 타입 및 계약
services/app-bootstrap.service.ts
BootstrapResult 타입을 도입하여 액세스 토큰, subject, Mixpanel 세션 ID, 구독 클럽 ID 목록, 타이밍 정보를 포함하도록 부트스트랩 서비스 계약을 확장합니다.
부트스트랩 서비스 및 헬퍼 함수
services/app-bootstrap.service.ts
runAppBootstrap을 재구현하여 FCM 등록, 구독 클럽 동기화, Mixpanel 식별을 Promise.all로 병렬 실행하고 각 단계의 타이밍을 수집한 후 확장된 결과를 반환합니다.
토큰 및 인증 저장소 최적화
services/auth-token-storage.ts, services/auth-token.service.ts
모듈 스코프에 액세스 토큰과 auth subject 캐시를 추가하여 AsyncStorage 반복 호출을 제거하고, 토큰 추출 로직을 any 캐스팅 기반 타입 체크로 개선합니다.
강제 업데이트 판정 모드 분리
services/force-update.service.ts
강제 업데이트 판정 로직을 getCachedForceUpdateRequired(캐시 기반)와 refreshForceUpdateRequired(백그라운드 갱신)로 분리하고, Remote Config 설정을 재사용 가능하게 합니다.
Context 초기화 최적화
contexts/mixpanel-context.tsx, contexts/subscribed-clubs-context.tsx, utils/mixpanel.ts
MixpanelProviderSubscribedClubsProvider가 부트스트랩으로부터 initialSessionId, initialReady, initialClubIds를 받아 초기화를 건너뛰고, Mixpanel 세션 ID 생성/저장 함수를 추가합니다.
RootLayout 부트스트랩 흐름 구현
app/_layout.tsx
강제 업데이트를 캐시로 먼저 판정하고 필요 시 부트스트랩을 대기한 후 백그라운드 갱신하며, 부트스트랩 결과를 bootstrapResult 상태로 관리하고 context 초기값으로 전달합니다.
스플래시 애니메이션 단계 제어 및 타이밍 로깅
components/custom-splash-screen.tsx, app/(tabs)/_layout.tsx, ui/home/home-webview-screen.tsx
CustomSplashScreen을 4단계 애니메이션으로 재구성하여 단계 완료 추적으로 페이드아웃을 제어하고, 탭 마운트 및 WebView 로딩 단계에서 [StartupTiming] 로그를 추가합니다.

iOS 빌드 설정 및 Expo prebuild 플러그인

Layer / File(s) 요약
iOS prebuild 수정 플러그인
plugins/withIosPrebuildFixes.js
Expo Config Plugin을 구현하여 Podfile에 RNFB 스크립트 빌드 페이즈를 삽입하고, Xcode 프로젝트의 쉘 스크립트 페이즈를 수정하며, 지원 파일들을 iOS 프로젝트 루트에 복사합니다.
iOS 빌드 지원 스크립트
plugins/ios-prebuild-support/ci_post_clone.sh, plugins/ios-prebuild-support/xcode.env
Homebrew를 통해 Node.js와 CocoaPods를 자동 설치하고, GoogleService-Info.plist를 base64 디코딩 또는 원문으로 준비하며, 환경변수와 .env 파일을 생성하고 pod install을 실행합니다.
Xcode 프로젝트 구조 및 빌드 설정 업데이트
ios/app.xcodeproj/project.pbxproj
Pods 프레임워크, ExpoModulesProvider.swift, GoogleService-Info.plist, PrivacyInfo.xcprivacy 참조를 갱신하고, 빌드 페이즈 스크립트 단계를 재정렬하며, xcconfig 기본 설정 참조를 업데이트합니다.
앱 설정 및 iOS 지원 plist 추가
app.json, plugins/ios-prebuild-support/IDEWorkspaceChecks.plist
app.jsonexpo-build-properties의 Android 릴리즈 빌드 옵션과 ./plugins/withIosPrebuildFixes 플러그인을 등록하고, Xcode 32비트 경고 플래그 plist를 추가합니다.

Import 경로 정규화 및 타입 정의 개선

Layer / File(s) 요약
바렐 import를 개별 모듈 import로 정규화
app/(tabs)/more.tsx, app/webview/[slug].tsx, components/permission-dialog.tsx, ui/club-detail/club-detail-screen.tsx, ui/home/components/banner.tsx, ui/home/home-screen.tsx, ui/home/home-webview-screen.tsx, ui/subscribe/components/empty-state.tsx, ui/subscribe/subscribe-screen.tsx
@/hooks@/contexts의 바렐 import를 구체적인 모듈 경로(예: @/hooks/use-mixpanel-track, @/contexts/mixpanel-context)로 변경합니다.
UI 컴포넌트 Props 타입 정의 개선
components/ui/column.tsx, components/ui/row.tsx
StyledColumnStyledRow의 props 타입을 inline 정의에서 StyledColumnProps, StyledRowProps로 분리하여 NonNullable을 적용하고 타입 안전성을 강화합니다.

예상 코드 리뷰 노력

🎯 4 (Complex) | ⏱️ ~75분

관련 가능성이 있는 PR

  • Moadong/moadong-react-native#8: app/_layout.tsx의 초기화/스플래시 전환 로직과 services/force-update.service.ts의 강제 업데이트 판정 흐름이 Remote Config 기반 강제 업데이트 체크 및 화면 연동 변경과 겹칩니다.
  • Moadong/moadong-react-native#6: app/(tabs)/_layout.tsx에서 같은 TabLayout 컴포넌트의 탭 구성/추적 로직을 수정하므로 같은 파일의 변경으로 인한 충돌이 가능합니다.

제안 리뷰어

  • seongwon030
  • oesnuj
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive 제목이 PR의 주요 변경사항을 반영하지 못하고 있습니다. 'release 1.5.2'는 버전 번호일 뿐 실제 기술적 개선 사항을 구체적으로 설명하지 않습니다. 제목을 더 구체적으로 수정하세요. 예: '앱 부트스트랩 및 스플래시 애니메이션 개선, iOS prebuild 안정화' 또는 '부트스트랩 흐름 개선 및 iOS prebuild 지원 추가'
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch main

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@SeongHoonC SeongHoonC changed the title 앱 부트스트랩 및 iOS prebuild 개선 release 1.5.2 Jun 2, 2026
@SeongHoonC SeongHoonC marked this pull request as ready for review June 2, 2026 13:15
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
components/ui/column.tsx (1)

52-55: 💤 Low value

명시적 타입 어노테이션은 선택 사항입니다.

styled-components v6는 제네릭 파라미터로부터 props 타입을 자동 추론하므로, 보간 함수의 (props: StyledColumnProps) 어노테이션은 중복입니다. 제거해도 동일하게 동작하며 코드가 간결해집니다.

♻️ 제안하는 간소화
-  align-items: ${(props: StyledColumnProps) => props.align};
-  justify-content: ${(props: StyledColumnProps) => props.justify};
-  flex-wrap: ${(props: StyledColumnProps) => props.wrap};
-  gap: ${(props: StyledColumnProps) => props.gap}px;
+  align-items: ${props => props.align};
+  justify-content: ${props => props.justify};
+  flex-wrap: ${props => props.wrap};
+  gap: ${props => props.gap}px;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/ui/column.tsx` around lines 52 - 55, The styled interpolation
functions in components/ui/column.tsx currently annotate props as (props:
StyledColumnProps) which is redundant with styled-components v6; remove the
explicit type annotations so the interpolations use implicit typing (e.g. props
=> props.align, props => props.justify, props => props.wrap, props => props.gap)
to simplify the code while keeping the same behavior for the
StyledColumnProps-backed styled component.
components/ui/row.tsx (1)

52-55: 💤 Low value

명시적 타입 어노테이션은 선택 사항입니다.

styled-components v6는 제네릭 파라미터로부터 props 타입을 자동 추론하므로, 보간 함수의 (props: StyledRowProps) 어노테이션은 중복입니다. 제거해도 동일하게 동작하며 코드가 간결해집니다.

♻️ 제안하는 간소화
-  align-items: ${(props: StyledRowProps) => props.align};
-  justify-content: ${(props: StyledRowProps) => props.justify};
-  flex-wrap: ${(props: StyledRowProps) => props.wrap};
-  gap: ${(props: StyledRowProps) => props.gap}px;
+  align-items: ${props => props.align};
+  justify-content: ${props => props.justify};
+  flex-wrap: ${props => props.wrap};
+  gap: ${props => props.gap}px;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/ui/row.tsx` around lines 52 - 55, The interpolation callbacks in
the styled component are using redundant explicit annotations; remove the
`(props: StyledRowProps)` type annotations and use implicit typing (e.g., `props
=> props.align`) for the four style lines (align-items, justify-content,
flex-wrap, gap) so styled-components v6 can infer props from the component's
generic; update the usages referencing align/justify/wrap/gap accordingly to
keep the same behavior with simpler code.
app/(tabs)/index.tsx (1)

4-12: ⚡ Quick win

lazy() 폴백은 현재 설정대로 동작합니다

@/ui/home/home-screenexport default HomeScreen;를 함께 제공하므로 lazy(() => import(...))default를 렌더해 WebView 실패 시에도 런타임 에러로 깨질 근거가 없습니다.
다만 “named exports 기본” 가이드를 더 엄격히 맞추려면(선택) lazy에서 HomeScreen을 named에서 default로 매핑하는 방식으로 정리하는 것을 권장합니다.

선택적 수정 예시
-const LazyHomeScreen = lazy(() => import('`@/ui/home/home-screen`'));
+const LazyHomeScreen = lazy(() =>
+  import('`@/ui/home/home-screen`').then((m) => ({ default: m.HomeScreen })),
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(tabs)/index.tsx around lines 4 - 12, The lazy import currently works
because '`@/ui/home/home-screen`' exports a default, but to strictly follow the
"named exports as default" guideline update the LazyHomeScreen declaration used
in HomeTab to explicitly map the named export to default during dynamic import;
replace the existing lazy(() => import('`@/ui/home/home-screen`')) with a lazy
import that calls import(...).then(mod => ({ default: mod.HomeScreen }))
(referencing LazyHomeScreen and HomeTab) so the component consumes the named
export consistently even if the module switches to named-only exports.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/_layout.tsx`:
- Around line 105-121: The code currently uses getCachedForceUpdateRequired()
and immediately starts runBootstrapSequence() if that cached value is false,
which can let a newly-enforced server min_supported_version be bypassed; update
the logic so bootstrap is gated on a fresh Remote Config check: call and await
refreshForceUpdateRequired() (or an explicit freshFetchForceUpdate function)
after checking the cache, use its returned boolean to setForceUpdateRequired and
setForceUpdateChecked, and only proceed to runBootstrapSequence() when that
fresh check returns false; keep refreshForceUpdateInBackground() for periodic
updates but do not start runBootstrapSequence() until the fresh check
completes—refer to getCachedForceUpdateRequired, refreshForceUpdateInBackground,
refreshForceUpdateRequired, setForceUpdateRequired, setForceUpdateChecked, and
runBootstrapSequence to locate where to change the flow.
- Around line 158-176: The ATT request is gated behind the useEffect that waits
for bootstrapSucceeded and showSplash false, causing ATT to run after
access-token/FCM/sync/Mixpanel; move ATT into the root bootstrap flow so it runs
as step (2) before access-token/FCM/sync/Mixpanel and while splash remains
blocking. Concretely, remove or relocate the call to
requestTrackingPermissionAfterLaunch from the InteractionManager-based effect
and invoke a new/renamed function (e.g.,
requestTrackingPermissionDuringBootstrap) from the main bootstrap sequence in
app/_layout.tsx immediately after the remote-config/forced-update check and
before the access token retrieval/creation code paths (the code currently gated
by bootstrapSucceeded), ensure the bootstrap promise awaits its completion, and
keep showSplash true (or the existing splash blocker) until that bootstrap
promise (including ATT) resolves so startup order follows the required (1)
remote-config forced update, (2) ATT, (3) access token, (4) FCM registration,
(5) subscribed clubs sync, (6) Mixpanel init.

In `@plugins/ios-prebuild-support/ci_post_clone.sh`:
- Around line 118-123: 현재 스크립트가 IOS_APS_ENVIRONMENT가 없을 때 APS_ENVIRONMENT를 기본값
"development"로 설정해 archive/배포 시 실수로 잘못된 entitlements가 생성될 위험이 있으니,
APS_ENVIRONMENT 변수(현재 APS_ENVIRONMENT="${IOS_APS_ENVIRONMENT:-development}")를
기본값 없이 필수로 요구하도록 변경하고, IOS_APS_ENVIRONMENT가 비어있으면 명확한 에러 메시지를 출력하고 비정상 종료(exit
1)하도록 추가 검사(변수 이름: IOS_APS_ENVIRONMENT / APS_ENVIRONMENT, 파일 변수:
ENTITLEMENTS_FILE, PlistBuddy 사용 블록)를 넣어 보호하세요; 이후 PlistBuddy 호출부는 기존대로 유지하면
됩니다.

In `@plugins/withIosPrebuildFixes.js`:
- Around line 120-133: The replacements using String.prototype.replace for
DEPLOYMENT_TARGET_LINES (the contents =
contents.replace(GENERATED_PLATFORM_LINE, DEPLOYMENT_TARGET_LINES) around the
GENERATED_PLATFORM_LINE/DEPLOYMENT_TARGET_LINES symbols) and for
POST_INSTALL_ANCHOR (contents = contents.replace(POST_INSTALL_ANCHOR,
POST_INSTALL_FIXES)) must validate that a substitution actually occurred; change
these to perform a guarded replace (like the existing insertAfter behavior) by
checking whether GENERATED_PLATFORM_LINE and POST_INSTALL_ANCHOR are present
before replacing, or compute a new string and compare to the original and throw
or log+fail if unchanged, so that missing anchors do not silently leave
configure_fmt_for_xcode_26 or deployment target normalization out of the
Podfile; ensure you reference and preserve the existing
insertAfter/POST_INSTALL_FIXES/POST_INTEGRATE_BLOCK logic when implementing the
validation.

In `@services/app-bootstrap.service.ts`:
- Around line 54-64: The Promise.all grouping in runAppBootstrap() causes any
rejection from syncSubscribedClubIds, initializeMixpanelIdentity, or
registerFcmToken to mark bootstrapStatus as 'failed' and block the splash;
change the non-essential parallel tasks to run via Promise.allSettled (or
individually awaited with try/catch) so their failures are logged but do not set
bootstrapStatus to failed or keep shouldBlockSplash true. Specifically, in
runAppBootstrap() replace the Promise.all that includes syncSubscribedClubIds,
initializeMixpanelIdentity, and registerFcmToken with a pattern that: 1) awaits
only truly essential startup work as before, 2) triggers
syncSubscribedClubIds(), initializeMixpanelIdentity(subject), and
registerFcmToken() via Promise.allSettled (or wrap each call with try/catch) and
still set timings.subscriptionsReady / timings.mixpanelReady / timings.fcmReady
in finally, and 3) on rejection only log the error (do not flip
bootstrapStatus/shouldBlockSplash or enable CustomSplashScreen.blockFinish) so
transient non-critical errors don’t block splash or set bootstrap failure.

In `@services/auth-token-storage.ts`:
- Around line 23-30: The try/catch in the access token getter incorrectly caches
null on AsyncStorage read errors (cachedAccessToken = null), which prevents
future reads and causes unnecessary token refreshes; change the catch handling
inside the function that calls AsyncStorage.getItem(ACCESS_TOKEN_KEY) so that on
error you do not set cachedAccessToken to null but leave it undefined (or
explicitly set to undefined) so subsequent calls to ensureAccessToken will retry
reading AsyncStorage; update the catch block around AsyncStorage.getItem and any
code paths that assign cachedAccessToken in that function to use undefined on
error instead of null, while still logging the error with the existing
console.error call.

---

Nitpick comments:
In `@app/`(tabs)/index.tsx:
- Around line 4-12: The lazy import currently works because
'`@/ui/home/home-screen`' exports a default, but to strictly follow the "named
exports as default" guideline update the LazyHomeScreen declaration used in
HomeTab to explicitly map the named export to default during dynamic import;
replace the existing lazy(() => import('`@/ui/home/home-screen`')) with a lazy
import that calls import(...).then(mod => ({ default: mod.HomeScreen }))
(referencing LazyHomeScreen and HomeTab) so the component consumes the named
export consistently even if the module switches to named-only exports.

In `@components/ui/column.tsx`:
- Around line 52-55: The styled interpolation functions in
components/ui/column.tsx currently annotate props as (props: StyledColumnProps)
which is redundant with styled-components v6; remove the explicit type
annotations so the interpolations use implicit typing (e.g. props =>
props.align, props => props.justify, props => props.wrap, props => props.gap) to
simplify the code while keeping the same behavior for the
StyledColumnProps-backed styled component.

In `@components/ui/row.tsx`:
- Around line 52-55: The interpolation callbacks in the styled component are
using redundant explicit annotations; remove the `(props: StyledRowProps)` type
annotations and use implicit typing (e.g., `props => props.align`) for the four
style lines (align-items, justify-content, flex-wrap, gap) so styled-components
v6 can infer props from the component's generic; update the usages referencing
align/justify/wrap/gap accordingly to keep the same behavior with simpler code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b905e8b0-9057-40bb-ac7d-357eb9313b96

📥 Commits

Reviewing files that changed from the base of the PR and between 8ccaf8a and 0eb9d75.

📒 Files selected for processing (29)
  • app.json
  • app/(tabs)/_layout.tsx
  • app/(tabs)/index.tsx
  • app/(tabs)/more.tsx
  • app/_layout.tsx
  • app/webview/[slug].tsx
  • components/custom-splash-screen.tsx
  • components/permission-dialog.tsx
  • components/ui/column.tsx
  • components/ui/row.tsx
  • contexts/mixpanel-context.tsx
  • contexts/subscribed-clubs-context.tsx
  • ios/app.xcodeproj/project.pbxproj
  • plugins/ios-prebuild-support/IDEWorkspaceChecks.plist
  • plugins/ios-prebuild-support/ci_post_clone.sh
  • plugins/ios-prebuild-support/xcode.env
  • plugins/withIosPrebuildFixes.js
  • services/app-bootstrap.service.ts
  • services/auth-token-storage.ts
  • services/auth-token.service.ts
  • services/force-update.service.ts
  • ui/club-detail/club-detail-screen.tsx
  • ui/home/components/banner.tsx
  • ui/home/components/club-list.tsx
  • ui/home/home-screen.tsx
  • ui/home/home-webview-screen.tsx
  • ui/subscribe/components/empty-state.tsx
  • ui/subscribe/subscribe-screen.tsx
  • utils/mixpanel.ts

Comment thread app/_layout.tsx
Comment on lines +105 to 121
// 1) 강제 업데이트 캐시 판정 (Remote Config fetch는 백그라운드에서 갱신)
const required = await getCachedForceUpdateRequired();
if (cancelled) {
return;
}

setForceUpdateRequired(required);
setForceUpdateChecked(true);
refreshForceUpdateInBackground();

if (required) {
console.log('⛔️ 강제 업데이트 필요: 부트스트랩 중단');
console.log('⛔️ 캐시 기준 강제 업데이트 필요: 부트스트랩 대기');
return;
}

// 2) 강제 업데이트가 아닐 때만 ATT 요청
await requestTrackingPermissionOnLaunch();

// 3) Access Token -> FCM -> 구독 목록 -> Mixpanel 순서 부트스트랩
// 2) Access Token -> FCM/구독 목록/Mixpanel 병렬 부트스트랩
await runBootstrapSequence();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

캐시 판정만으로 runAppBootstrap()을 열어 두면 신규 강제 업데이트를 우회합니다.

Line 106은 활성/기본값 캐시만 보고, false이면 Line 121에서 바로 부트스트랩을 시작합니다. 이 상태에서 서버의 min_supported_version이 방금 올라간 경우에는 refreshForceUpdateRequired()true를 돌려주기 전에 토큰 생성, FCM 등록, 구독 동기화, Mixpanel 초기화가 먼저 실행됩니다. 강제 업데이트를 진입 게이트로 쓰는 흐름이면 fresh fetch 결과가 확정될 때까지 이 단계들은 보류되어야 합니다.

As per coding guidelines, app/_layout.tsx must execute bootstrap in order: Firebase Remote Config forced update check → iOS ATT permission request → access token retrieval/creation → FCM token registration → subscription sync → Mixpanel initialization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_layout.tsx` around lines 105 - 121, The code currently uses
getCachedForceUpdateRequired() and immediately starts runBootstrapSequence() if
that cached value is false, which can let a newly-enforced server
min_supported_version be bypassed; update the logic so bootstrap is gated on a
fresh Remote Config check: call and await refreshForceUpdateRequired() (or an
explicit freshFetchForceUpdate function) after checking the cache, use its
returned boolean to setForceUpdateRequired and setForceUpdateChecked, and only
proceed to runBootstrapSequence() when that fresh check returns false; keep
refreshForceUpdateInBackground() for periodic updates but do not start
runBootstrapSequence() until the fresh check completes—refer to
getCachedForceUpdateRequired, refreshForceUpdateInBackground,
refreshForceUpdateRequired, setForceUpdateRequired, setForceUpdateChecked, and
runBootstrapSequence to locate where to change the flow.

Comment thread app/_layout.tsx
Comment on lines +158 to +176
useEffect(() => {
if (!bootstrapSucceeded || forceUpdateRequired || showSplash || Platform.OS !== 'ios') {
return;
}

let timeoutId: ReturnType<typeof setTimeout> | undefined;
const interactionTask = InteractionManager.runAfterInteractions(() => {
timeoutId = setTimeout(() => {
requestTrackingPermissionAfterLaunch();
}, 250);
});

return () => {
interactionTask.cancel();
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [bootstrapSucceeded, forceUpdateRequired, showSplash]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

ATT 요청이 부트스트랩 완료 뒤로 밀려 순서 계약을 깨고 있습니다.

Line 159의 가드 때문에 ATT는 bootstrapSucceededshowSplash === false 이후에만 요청됩니다. 지금 흐름에서는 access token 처리, FCM 등록, 구독 동기화, Mixpanel 초기화가 모두 ATT보다 먼저 실행되므로 iOS 첫 실행의 추적 동의 상태와 초기화 결과가 어긋납니다. ATT를 루트 bootstrap 단계로 되돌리고, 완료 전까지 현재 스플래시 블로킹을 유지하는 쪽이 안전합니다.

As per coding guidelines, app/_layout.tsx must implement bootstrap in this order: (1) Firebase Remote Config forced update check, (2) iOS ATT permission request, (3) access token retrieval/creation, (4) FCM token registration, (5) subscribed clubs list sync, (6) Mixpanel analytics initialization. Block UI with custom splash screen until bootstrap completes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_layout.tsx` around lines 158 - 176, The ATT request is gated behind the
useEffect that waits for bootstrapSucceeded and showSplash false, causing ATT to
run after access-token/FCM/sync/Mixpanel; move ATT into the root bootstrap flow
so it runs as step (2) before access-token/FCM/sync/Mixpanel and while splash
remains blocking. Concretely, remove or relocate the call to
requestTrackingPermissionAfterLaunch from the InteractionManager-based effect
and invoke a new/renamed function (e.g.,
requestTrackingPermissionDuringBootstrap) from the main bootstrap sequence in
app/_layout.tsx immediately after the remote-config/forced-update check and
before the access token retrieval/creation code paths (the code currently gated
by bootstrapSucceeded), ensure the bootstrap promise awaits its completion, and
keep showSplash true (or the existing splash blocker) until that bootstrap
promise (including ATT) resolves so startup order follows the required (1)
remote-config forced update, (2) ATT, (3) access token, (4) FCM registration,
(5) subscribed clubs sync, (6) Mixpanel init.

Comment on lines +118 to +123
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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

배포용 archive에서도 aps-environment가 기본값 development로 내려갑니다.

Line 118이 환경 변수가 없을 때 development를 사용해서, CI 설정 실수만으로 프로덕션 entitlements가 잘못 생성될 수 있습니다. archive 스크립트라면 이 값은 필수 입력으로 강제하는 쪽이 안전합니다.

수정 예시
-APS_ENVIRONMENT="${IOS_APS_ENVIRONMENT:-development}"
+: "${IOS_APS_ENVIRONMENT:?Missing IOS_APS_ENVIRONMENT for iOS archive}"
+APS_ENVIRONMENT="$IOS_APS_ENVIRONMENT"
 ENTITLEMENTS_FILE="${IOS_ENTITLEMENTS_FILE:-ios/app/app.entitlements}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/ios-prebuild-support/ci_post_clone.sh` around lines 118 - 123, 현재
스크립트가 IOS_APS_ENVIRONMENT가 없을 때 APS_ENVIRONMENT를 기본값 "development"로 설정해
archive/배포 시 실수로 잘못된 entitlements가 생성될 위험이 있으니, APS_ENVIRONMENT 변수(현재
APS_ENVIRONMENT="${IOS_APS_ENVIRONMENT:-development}")를 기본값 없이 필수로 요구하도록 변경하고,
IOS_APS_ENVIRONMENT가 비어있으면 명확한 에러 메시지를 출력하고 비정상 종료(exit 1)하도록 추가 검사(변수 이름:
IOS_APS_ENVIRONMENT / APS_ENVIRONMENT, 파일 변수: ENTITLEMENTS_FILE, PlistBuddy 사용
블록)를 넣어 보호하세요; 이후 PlistBuddy 호출부는 기존대로 유지하면 됩니다.

Comment on lines +120 to +133
if (!contents.includes(DEPLOYMENT_TARGET_LINES)) {
contents = contents.replace(GENERATED_PLATFORM_LINE, DEPLOYMENT_TARGET_LINES);
}

if (!contents.includes('def configure_fmt_for_xcode_26(installer)')) {
contents = insertAfter(contents, `${DEPLOYMENT_TARGET_LINES}\n\n`, `${PODFILE_BUILD_FIXES_BLOCK}\n`);
}

if (!contents.includes(' configure_fmt_for_xcode_26(installer)')) {
contents = contents.replace(POST_INSTALL_ANCHOR, POST_INSTALL_FIXES);
}

if (!contents.includes('post_integrate do |installer|')) {
contents = `${contents.trimEnd()}\n\n${POST_INTEGRATE_BLOCK}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Podfile 치환 실패가 조용히 무시됩니다.

Line 121과 Line 129의 replace(...)는 앵커를 못 찾아도 그대로 통과해서, 생성된 Podfile 포맷이 조금만 달라져도 configure_fmt_for_xcode_26나 deployment target 정규화가 빠진 채 prebuild가 성공합니다. 이 플러그인의 핵심 수정 경로라서 insertAfter처럼 치환 성공 여부를 강제하는 편이 안전합니다.

수정 예시
+function replaceOrThrow(source, searchValue, replacement, label) {
+  if (!source.includes(searchValue)) {
+    throw new Error(`Unable to find expected Podfile anchor: ${label}`);
+  }
+  return source.replace(searchValue, replacement);
+}
+
 function applyPodfileFixes(source) {
   let contents = source;
 ...
   if (!contents.includes(DEPLOYMENT_TARGET_LINES)) {
-    contents = contents.replace(GENERATED_PLATFORM_LINE, DEPLOYMENT_TARGET_LINES);
+    contents = replaceOrThrow(
+      contents,
+      GENERATED_PLATFORM_LINE,
+      DEPLOYMENT_TARGET_LINES,
+      GENERATED_PLATFORM_LINE
+    );
   }
 ...
   if (!contents.includes('    configure_fmt_for_xcode_26(installer)')) {
-    contents = contents.replace(POST_INSTALL_ANCHOR, POST_INSTALL_FIXES);
+    contents = replaceOrThrow(
+      contents,
+      POST_INSTALL_ANCHOR,
+      POST_INSTALL_FIXES,
+      'react_native_post_install'
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/withIosPrebuildFixes.js` around lines 120 - 133, The replacements
using String.prototype.replace for DEPLOYMENT_TARGET_LINES (the contents =
contents.replace(GENERATED_PLATFORM_LINE, DEPLOYMENT_TARGET_LINES) around the
GENERATED_PLATFORM_LINE/DEPLOYMENT_TARGET_LINES symbols) and for
POST_INSTALL_ANCHOR (contents = contents.replace(POST_INSTALL_ANCHOR,
POST_INSTALL_FIXES)) must validate that a substitution actually occurred; change
these to perform a guarded replace (like the existing insertAfter behavior) by
checking whether GENERATED_PLATFORM_LINE and POST_INSTALL_ANCHOR are present
before replacing, or compute a new string and compare to the original and throw
or log+fail if unchanged, so that missing anchors do not silently leave
configure_fmt_for_xcode_26 or deployment target normalization out of the
Podfile; ensure you reference and preserve the existing
insertAfter/POST_INSTALL_FIXES/POST_INTEGRATE_BLOCK logic when implementing the
validation.

Comment on lines +54 to +64
const [subscribedClubIds, sessionId] = await Promise.all([
syncSubscribedClubIds().finally(() => {
timings.subscriptionsReady = Date.now();
}),
initializeMixpanelIdentity(subject).finally(() => {
timings.mixpanelReady = Date.now();
}),
registerFcmToken().finally(() => {
timings.fcmReady = Date.now();
}),
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd '_layout.tsx' app | xargs -I{} sh -c 'echo "== {} =="; cat "{}"' | rg -n -C5 'runAppBootstrap|catch|Bootstrap|splash'

Repository: Moadong/moadong-react-native

Length of output: 7141


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="app/_layout.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE"; exit 1; }
echo "== Provider/splash/render sections (approx 150-420) =="
nl -ba "$FILE" | sed -n '150,420p'
echo
echo "== runAppBootstrap / prepare / splash gating (approx 210-320) =="
nl -ba "$FILE" | sed -n '210,330p'

Repository: Moadong/moadong-react-native

Length of output: 188


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="app/_layout.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE"; exit 1; }
echo "== Provider/splash/render sections (approx 150-420) =="
cat -n "$FILE" | sed -n '150,420p'
echo
echo "== runAppBootstrap / prepare / splash gating (approx 200-330) =="
cat -n "$FILE" | sed -n '200,330p'

Repository: Moadong/moadong-react-native

Length of output: 8697


🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd 'custom-splash-screen' . | head -n 20

Repository: Moadong/moadong-react-native

Length of output: 110


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="components/custom-splash-screen.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE"; exit 1; }
echo "== custom-splash-screen.tsx =="
cat -n "$FILE" | sed -n '1,240p'

Repository: Moadong/moadong-react-native

Length of output: 7940


Promise.all로 비핵심 부트스트랩 실패가 스플래시 진입까지 차단됨

services/app-bootstrap.service.ts에서 Promise.all의 3개 작업 중 하나라도 reject되면 runAppBootstrap()이 실패하고, app/_layout.tsxbootstrapStatus'failed'로 두면서 shouldBlockSplashtrue가 됩니다. 그 결과 CustomSplashScreenblockFinish가 활성화되어 스플래시 페이드아웃/onFinish가 막혀 메인 화면 전환이 지연되고(재시도 다이얼로그 노출), 비핵심(구독 동기화/ Mixpanel/ FCM) 일시 오류도 앱 시작 흐름을 사실상 막습니다. 필수 경로만 예외로 유지하고 나머지는 Promise.allSettled 등으로 격리하세요.

♻️ 비핵심 작업 격리 예시
-  const [subscribedClubIds, sessionId] = await Promise.all([
-    syncSubscribedClubIds().finally(() => {
-      timings.subscriptionsReady = Date.now();
-    }),
-    initializeMixpanelIdentity(subject).finally(() => {
-      timings.mixpanelReady = Date.now();
-    }),
-    registerFcmToken().finally(() => {
-      timings.fcmReady = Date.now();
-    }),
-  ]);
+  const [subscriptionsResult, mixpanelResult] = await Promise.allSettled([
+    syncSubscribedClubIds().finally(() => {
+      timings.subscriptionsReady = Date.now();
+    }),
+    initializeMixpanelIdentity(subject).finally(() => {
+      timings.mixpanelReady = Date.now();
+    }),
+    registerFcmToken().finally(() => {
+      timings.fcmReady = Date.now();
+    }),
+  ]);
+
+  const subscribedClubIds =
+    subscriptionsResult.status === 'fulfilled' ? subscriptionsResult.value : [];
+  const sessionId =
+    mixpanelResult.status === 'fulfilled' ? mixpanelResult.value : '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [subscribedClubIds, sessionId] = await Promise.all([
syncSubscribedClubIds().finally(() => {
timings.subscriptionsReady = Date.now();
}),
initializeMixpanelIdentity(subject).finally(() => {
timings.mixpanelReady = Date.now();
}),
registerFcmToken().finally(() => {
timings.fcmReady = Date.now();
}),
]);
const [subscriptionsResult, mixpanelResult] = await Promise.allSettled([
syncSubscribedClubIds().finally(() => {
timings.subscriptionsReady = Date.now();
}),
initializeMixpanelIdentity(subject).finally(() => {
timings.mixpanelReady = Date.now();
}),
registerFcmToken().finally(() => {
timings.fcmReady = Date.now();
}),
]);
const subscribedClubIds =
subscriptionsResult.status === 'fulfilled' ? subscriptionsResult.value : [];
const sessionId =
mixpanelResult.status === 'fulfilled' ? mixpanelResult.value : '';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/app-bootstrap.service.ts` around lines 54 - 64, The Promise.all
grouping in runAppBootstrap() causes any rejection from syncSubscribedClubIds,
initializeMixpanelIdentity, or registerFcmToken to mark bootstrapStatus as
'failed' and block the splash; change the non-essential parallel tasks to run
via Promise.allSettled (or individually awaited with try/catch) so their
failures are logged but do not set bootstrapStatus to failed or keep
shouldBlockSplash true. Specifically, in runAppBootstrap() replace the
Promise.all that includes syncSubscribedClubIds, initializeMixpanelIdentity, and
registerFcmToken with a pattern that: 1) awaits only truly essential startup
work as before, 2) triggers syncSubscribedClubIds(),
initializeMixpanelIdentity(subject), and registerFcmToken() via
Promise.allSettled (or wrap each call with try/catch) and still set
timings.subscriptionsReady / timings.mixpanelReady / timings.fcmReady in
finally, and 3) on rejection only log the error (do not flip
bootstrapStatus/shouldBlockSplash or enable CustomSplashScreen.blockFinish) so
transient non-critical errors don’t block splash or set bootstrap failure.

Comment on lines 23 to 30
try {
return await AsyncStorage.getItem(ACCESS_TOKEN_KEY);
cachedAccessToken = await AsyncStorage.getItem(ACCESS_TOKEN_KEY);
return cachedAccessToken;
} catch (error) {
console.error('❌ Access Token 조회 실패:', error);
cachedAccessToken = null;
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

읽기 오류 시 null 캐싱은 재시도를 막습니다.

일시적인 AsyncStorage 읽기 실패에서 cachedAccessToken = null로 캐싱하면(Line 28) 이후 호출이 Line 19-21에서 캐시된 null을 그대로 반환하여 저장소에 유효 토큰이 있어도 재조회하지 않습니다. 결과적으로 ensureAccessToken이 불필요하게 새 토큰을 재발급하게 됩니다. 오류 시에는 캐시를 undefined로 두어 다음 호출에서 재시도되도록 하는 것이 안전합니다.

🛡️ 제안 수정
   } catch (error) {
     console.error('❌ Access Token 조회 실패:', error);
-    cachedAccessToken = null;
     return null;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
return await AsyncStorage.getItem(ACCESS_TOKEN_KEY);
cachedAccessToken = await AsyncStorage.getItem(ACCESS_TOKEN_KEY);
return cachedAccessToken;
} catch (error) {
console.error('❌ Access Token 조회 실패:', error);
cachedAccessToken = null;
return null;
}
try {
cachedAccessToken = await AsyncStorage.getItem(ACCESS_TOKEN_KEY);
return cachedAccessToken;
} catch (error) {
console.error('❌ Access Token 조회 실패:', error);
return null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/auth-token-storage.ts` around lines 23 - 30, The try/catch in the
access token getter incorrectly caches null on AsyncStorage read errors
(cachedAccessToken = null), which prevents future reads and causes unnecessary
token refreshes; change the catch handling inside the function that calls
AsyncStorage.getItem(ACCESS_TOKEN_KEY) so that on error you do not set
cachedAccessToken to null but leave it undefined (or explicitly set to
undefined) so subsequent calls to ensureAccessToken will retry reading
AsyncStorage; update the catch block around AsyncStorage.getItem and any code
paths that assign cachedAccessToken in that function to use undefined on error
instead of null, while still logging the error with the existing console.error
call.

@SeongHoonC SeongHoonC merged commit a8636d1 into prod Jun 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant