release 1.5.2#22
Conversation
워크스루이 PR은 앱의 부트스트랩 프로세스를 재설계하여 초기화 결과를 확장하고, Mixpanel과 구독 클럽 context의 초기화를 최적화하며, iOS 빌드를 위한 Expo prebuild 플러그인과 지원 스크립트를 추가합니다. 강제 업데이트 판정을 캐시/리프레시 모드로 분리하고, 스플래시 애니메이션을 단계 기반 제어로 개선하며, import 경로를 정규화합니다. 변경 사항부트스트랩 및 앱 초기화 생명주기
iOS 빌드 설정 및 Expo prebuild 플러그인
Import 경로 정규화 및 타입 정의 개선
예상 코드 리뷰 노력🎯 4 (Complex) | ⏱️ ~75분 관련 가능성이 있는 PR
제안 리뷰어
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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-screen가export 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
📒 Files selected for processing (29)
app.jsonapp/(tabs)/_layout.tsxapp/(tabs)/index.tsxapp/(tabs)/more.tsxapp/_layout.tsxapp/webview/[slug].tsxcomponents/custom-splash-screen.tsxcomponents/permission-dialog.tsxcomponents/ui/column.tsxcomponents/ui/row.tsxcontexts/mixpanel-context.tsxcontexts/subscribed-clubs-context.tsxios/app.xcodeproj/project.pbxprojplugins/ios-prebuild-support/IDEWorkspaceChecks.plistplugins/ios-prebuild-support/ci_post_clone.shplugins/ios-prebuild-support/xcode.envplugins/withIosPrebuildFixes.jsservices/app-bootstrap.service.tsservices/auth-token-storage.tsservices/auth-token.service.tsservices/force-update.service.tsui/club-detail/club-detail-screen.tsxui/home/components/banner.tsxui/home/components/club-list.tsxui/home/home-screen.tsxui/home/home-webview-screen.tsxui/subscribe/components/empty-state.tsxui/subscribe/subscribe-screen.tsxutils/mixpanel.ts
| // 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(); |
There was a problem hiding this comment.
캐시 판정만으로 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.
| 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]); |
There was a problem hiding this comment.
ATT 요청이 부트스트랩 완료 뒤로 밀려 순서 계약을 깨고 있습니다.
Line 159의 가드 때문에 ATT는 bootstrapSucceeded와 showSplash === 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.
| 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" |
There was a problem hiding this comment.
배포용 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 호출부는 기존대로 유지하면 됩니다.
| 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}`; |
There was a problem hiding this comment.
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.
| 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(); | ||
| }), | ||
| ]); |
There was a problem hiding this comment.
🧩 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 20Repository: 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.tsx는 bootstrapStatus를 'failed'로 두면서 shouldBlockSplash가 true가 됩니다. 그 결과 CustomSplashScreen의 blockFinish가 활성화되어 스플래시 페이드아웃/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.
| 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.
| 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; | ||
| } |
There was a problem hiding this comment.
읽기 오류 시 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.
| 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.
변경 사항
검증
Summary by CodeRabbit