Skip to content

[feat] Splash 화면 Compose 마이그레이션#620

Merged
jerry8282 merged 5 commits intodevelopfrom
feature/#619
Mar 5, 2026
Merged

[feat] Splash 화면 Compose 마이그레이션#620
jerry8282 merged 5 commits intodevelopfrom
feature/#619

Conversation

@yrsel
Copy link
Contributor

@yrsel yrsel commented Mar 3, 2026

Issues

✔️ Check-list

  • : Label을 지정해 주세요.
  • : Merge할 브랜치를 확인해 주세요.

🗒️ Work Description

#618 PR 에 이어서 작업해서 커밋 내역이 섞여 있어요
당장 확인할 때는 ❗️링크를 통해 변경사항 확인 부탁드려요!

Navigation 적용하며 단일 Activity 구조에 Splash Screen API 로 구현하는 게 좋을 것 같아서 Compose 로 화면만 보여주도록 수정했습니다!

📷 Screenshot

정상 작동

record.webm

📚 Reference

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 앱 시작 시 스플래시 화면 표시
    • 게스트 모드로 로그인 없이 앱 이용 가능
    • 예상 오류 상태에 대한 오류 메시지 추가
  • 개선 사항

    • 세션 및 토큰 관리 시스템 개선
    • 사용자 인증 흐름 최적화

@yrsel yrsel self-assigned this Mar 3, 2026
@yrsel yrsel added 🌰 채넛 우아한 테크코스 7기 안드로이드 채넛 🤖 Android 안드로이드얌 📝 feat labels Mar 3, 2026
@github-actions github-actions bot requested a review from jerry8282 March 3, 2026 16:46
@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

Walkthrough

인증 상태 관리를 AuthState/UserType에서 SessionStore/SessionState/TokenManager로 개편하고, Splash 화면을 XML에서 Compose로 마이그레이션하며, 로그인 및 초기 세션 결정 플로우를 재구성하고, 관련 DI 모듈과 네트워크 계층을 업데이트합니다.

Changes

Cohort / File(s) Summary
세션 및 토큰 관리 인터페이스
domain/session/SessionState.kt, domain/session/SessionStore.kt, domain/session/TokenManager.kt, domain/session/AuthTokenCacheController.kt
세션 상태 타입, 토큰 관리자, 캐시 컨트롤러 인터페이스 신규 추가; AuthState/UserType 제거.
세션 및 토큰 관리 구현
data/session/DefaultSessionStore.kt, data/session/DefaultTokenManager.kt, data/session/DefaultAuthTokenCacheController.kt
StateFlow 기반 세션 상태 관리, 로컬 스토리지 동기화 토큰 관리자, HTTP 클라이언트 기반 캐시 컨트롤러 구현 추가.
Use Case 계층 리팩토링
domain/login/usecase/LoginUseCase.kt, domain/login/usecase/CheckUserSignedInUseCase.kt (삭제), domain/login/usecase/LoginUserUseCase.kt (삭제), domain/session/usecase/DetermineInitialSessionUseCase.kt, domain/session/usecase/SwitchToGuestUseCase.kt
로그인 플로우 및 초기 세션 결정 로직 재구성; 기존 체크 및 사용자 로그인 Use Case 제거.
로그인 데이터 소스 업데이트
data/login/datasource/CredentialProvider.kt, data/login/datasource/GoogleCredentialManager.kt
ID 토큰 반환 타입을 GoogleIdTokenCredential에서 String으로 변경.
DI 모듈 설정
di/SessionModule.kt, di/NetworkModule.kt (수정), di/LocalStorageModule.kt (수정)`
새로운 SessionModule 추가; NetworkModule은 UserStorageRepository에서 TokenManager로 전환; DataStore API 업데이트.
로그인 화면 UI 개선
ui/compose/login/LoginScreen.kt, ui/compose/login/component/GoogleLoginButton.kt, ui/compose/login/component/GuestModeSection.kt, ui/compose/login/LoginViewmodel.kt, ui/login/LoginActivity.kt
콜백 매개변수 명칭 정규화; HelpText를 GuestModeSection으로 대체; LoginViewmodel에서 LoginUseCase/SwitchToGuestUseCase 통합.
Splash 화면 Compose 마이그레이션
ui/compose/splash/SplashScreen.kt, ui/compose/splash/SplashViewModel.kt, ui/compose/splash/SplashUiEffect.kt, ui/splash/SplashActivity.kt, res/layout/activity_splash.xml (삭제)
XML 레이아웃 기반 Splash에서 Compose 기반으로 완전 마이그레이션; ViewModel 재작성 및 UI 이펙트 기반 네비게이션 도입.
다른 화면 세션 상태 통합
ui/compose/mypage/MyPageScreen.kt, ui/compose/mypage/MyPageViewModel.kt, ui/compose/mypage/component/MyPageSettingsSection.kt, ui/compose/trip/turipselection/PlaceTuripSelectionViewModel.kt, ui/compose/turipdetail/TuripDetailViewModel.kt
AuthState/UserType 체크를 SessionStore 기반 SessionState 체크로 대체.
토큰 인증 제거 및 오류 처리
data/login/TokenAuthenticator.kt (삭제), ui/common/error/ErrorUiState.kt, ui/compose/designsystem/component/ErrorScreen.kt
TokenAuthenticator 제거; ErrorUiState.Unexpected 추가 및 UI 렌더링 업데이트.
리소스 및 의존성 업데이트
res/values/colors.xml, res/values/strings.xml, res/values-v31/themes.xml, gradle/libs.versions.toml
splash_background 색상 리소스 추가; unexpected_error 문자열 추가; 테마에서 색상 리소스 참조; ktorfit 버전 업데이트.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SplashScreen as SplashScreen<br/>(Compose)
    participant SplashViewModel as SplashViewModel
    participant DetermineUseCase as DetermineInitialSessionUseCase
    participant TokenManager as TokenManager
    participant AuthRepo as AuthRepository
    participant SessionStore as SessionStore

    User->>SplashScreen: 앱 시작
    SplashScreen->>SplashViewModel: determineStartDestination() 호출
    SplashViewModel->>DetermineUseCase: invoke() 호출
    DetermineUseCase->>TokenManager: initialize()
    DetermineUseCase->>TokenManager: currentTokens() 조회
    
    alt 토큰 없음
        DetermineUseCase->>SessionStore: setGuest()
        DetermineUseCase-->>SplashViewModel: SessionState.Guest
    else 토큰 있음
        DetermineUseCase->>AuthRepo: verifyToken()
        alt 토큰 유효
            DetermineUseCase->>SessionStore: setMember()
            DetermineUseCase-->>SplashViewModel: SessionState.Member
        else 인증 오류
            DetermineUseCase->>SwitchToGuestUseCase: invoke()
            DetermineUseCase-->>SplashViewModel: SessionState.Guest
        else 기타 오류
            DetermineUseCase->>SessionStore: setMember()
            DetermineUseCase-->>SplashViewModel: SessionState.Member
        end
    end
    
    alt Member
        SplashViewModel->>SplashScreen: NavigateToMain 이펙트 발송
        SplashScreen->>User: MainActivity로 이동
    else Guest/Uninitialized
        SplashViewModel->>SplashScreen: NavigateToLogin 이펙트 발송
        SplashScreen->>User: LoginActivity로 이동
    end
Loading
sequenceDiagram
    actor User
    participant LoginScreen as LoginScreen<br/>(Compose)
    participant LoginViewModel as LoginViewModel
    participant GoogleCredMgr as GoogleCredentialManager
    participant LoginUseCase as LoginUseCase
    participant AuthRepo as AuthRepository
    participant TokenManager as TokenManager
    participant SessionStore as SessionStore

    User->>LoginScreen: Google 로그인 버튼 클릭
    LoginScreen->>LoginViewModel: loginWithGoogle()
    LoginViewModel->>GoogleCredMgr: getIdToken()
    GoogleCredMgr-->>LoginViewModel: ID Token (String)
    LoginViewModel->>LoginUseCase: invoke(idToken)
    LoginUseCase->>AuthRepo: login(idToken)
    AuthRepo-->>LoginUseCase: AuthResult (tokens)
    LoginUseCase->>TokenManager: setTokens()
    TokenManager-->>LoginUseCase: Result.Success
    LoginUseCase->>SessionStore: setMember()
    LoginUseCase-->>LoginViewModel: TuripResult.Success(isNewMember)
    
    alt 신규 회원
        LoginViewModel->>LoginScreen: 마이그레이션 다이얼로그 표시
        User->>LoginScreen: 확인 클릭
        LoginScreen->>LoginViewModel: confirmMigration()
        LoginViewModel->>LoginScreen: MainActivity로 이동
    else 기존 회원
        LoginViewModel->>LoginScreen: MainActivity로 이동
    end
Loading
sequenceDiagram
    actor User
    participant MyPageScreen as MyPageScreen<br/>(Compose)
    participant SessionStore as SessionStore
    participant TokenManager as TokenManager
    participant SwitchToGuestUseCase as SwitchToGuestUseCase

    User->>MyPageScreen: 로그아웃 클릭
    MyPageScreen->>SwitchToGuestUseCase: invoke()
    SwitchToGuestUseCase->>TokenManager: clearTokens()
    TokenManager-->>SwitchToGuestUseCase: Result.Success
    SwitchToGuestUseCase->>SessionStore: setGuest()
    SessionStore-->>SwitchToGuestUseCase: 상태 업데이트
    SwitchToGuestUseCase-->>MyPageScreen: 완료
    MyPageScreen->>User: LoginActivity로 이동
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning PR에 포함된 변경사항 중 일부(AuthState/UserType 제거, TokenManager 도입, LoginUseCase 변경 등)는 이슈 #619의 Splash 마이그레이션 범위를 벗어난 것으로 보입니다. PR 설명에서 언급한 '#618 PR 작업과 커밋 내역이 섞여 있다'는 부분을 해결하고, Splash 마이그레이션과 관련 없는 변경사항들을 분리하거나 명확히 문서화해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive 체크리스트 항목(라벨 지정, 병합 브랜치 확인)은 완료되었으나 Work Description이 충분하지 못하고 Reference 섹션이 비어 있습니다. Work Description을 더 자세히 작성하고, 관련 기술적 참고자료나 링크를 Reference 섹션에 추가해주세요.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 Splash 화면을 Compose로 마이그레이션한다는 PR의 주요 변경사항을 명확하게 나타내고 있습니다.
Linked Issues check ✅ Passed PR의 코드 변경사항이 링크된 이슈 #619의 'Splash 화면을 Compose로 마이그레이션'이라는 목표를 잘 충족하고 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#619

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

Copy link

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
android/app/src/main/java/com/on/turip/ui/compose/login/LoginViewmodel.kt (1)

73-75: ⚠️ Potential issue | 🟡 Minor

마이그레이션 실패 시 사용자 피드백이 누락됩니다.

Line [73]~Line [75]에서 실패를 로그로만 처리하면 다이얼로그가 유지된 채 원인을 알기 어렵습니다. errorType을 받아 handleError로 전달해 주세요.

💡 제안 수정안
-                }.onFailure {
-                    Timber.e("마이그레이션 실패")
+                }.onFailure { errorType: ErrorType ->
+                    handleError(errorType)
+                    Timber.e("마이그레이션 실패: $errorType")
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/login/LoginViewmodel.kt`
around lines 73 - 75, The onFailure block currently only logs the migration
failure; update the onFailure handler in LoginViewmodel.kt (the onFailure { ...
} following the migration call) to construct or extract an errorType and pass it
to handleError(errorType) so the UI receives feedback, and also update any
dialog/progress state (e.g., hide the migration dialog or set
showProgress=false) so the dialog does not remain displayed; keep the Timber.e
logging but add handleError(errorType) and the dialog dismissal/state change in
that same onFailure branch.
🧹 Nitpick comments (6)
android/app/src/main/java/com/on/turip/data/login/datasource/CredentialProvider.kt (1)

6-6: 토큰 문자열의 유효성 계약을 인터페이스에 명시해 주세요.

TuripResult<String>로 단순화된 건 좋지만, 빈 문자열/공백 토큰 허용 여부를 KDoc으로 명시하면 호출부 방어 로직이 일관됩니다.

계약 명시 예시
 interface CredentialProvider {
+    /**
+     * 성공 시 비어있지 않은 Google ID Token 문자열을 반환한다.
+     */
     suspend fun getIdToken(): TuripResult<String>
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/data/login/datasource/CredentialProvider.kt`
at line 6, Add a KDoc comment to the CredentialProvider.getIdToken() declaration
that clearly specifies the token validity contract: state whether empty or
whitespace-only strings are allowed or must be treated as failure, what
TuripResult variants will be returned for success vs invalid/missing token, and
any normalization (e.g., trimmed) or lifetime expectations callers can rely on;
update the suspend fun getIdToken(): TuripResult<String> declaration's KDoc to
reflect these rules so callers implement consistent defensive checks.
android/app/src/main/java/com/on/turip/ui/compose/turipdetail/TuripDetailViewModel.kt (1)

302-317: shareTurip() 이펙트 전송 중복은 한 번의 launch로 줄일 수 있습니다.

동작은 맞지만 분기별 코루틴 생성이 중복되어 가독성이 떨어집니다. 이펙트를 먼저 결정하고 한 번만 전송하면 더 깔끔합니다.

중복 제거 예시
     fun shareTurip() {
-        when (sessionState.value) {
-            SessionState.Member -> {
-                val turipShareModel =
-                    TuripShareModel(
-                        name = uiState.value.selectedTurip.name,
-                        places = uiState.value.places.map { it.toUiModel() },
-                    )
-                viewModelScope.launch {
-                    _uiEffect.send(TuripPlaceUiEffect.ShareTurip(turipShareModel))
-                }
-            }
-
-            SessionState.Guest, SessionState.Uninitialized -> {
-                viewModelScope.launch {
-                    _uiEffect.send(TuripPlaceUiEffect.ShowTuripShareNotAllowed)
-                }
-            }
-        }
+        val effect =
+            when (sessionState.value) {
+                SessionState.Member -> {
+                    val turipShareModel =
+                        TuripShareModel(
+                            name = uiState.value.selectedTurip.name,
+                            places = uiState.value.places.map { it.toUiModel() },
+                        )
+                    TuripPlaceUiEffect.ShareTurip(turipShareModel)
+                }
+                SessionState.Guest, SessionState.Uninitialized -> TuripPlaceUiEffect.ShowTuripShareNotAllowed
+            }
+        viewModelScope.launch { _uiEffect.send(effect) }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/compose/turipdetail/TuripDetailViewModel.kt`
around lines 302 - 317, The branches in TuripDetailViewModel.kt duplicate
coroutine launches when sending effects; instead determine the
TuripPlaceUiEffect first (using sessionState.value to choose between
TuripPlaceUiEffect.ShareTurip(TuripShareModel(...)) and
TuripPlaceUiEffect.ShowTuripShareNotAllowed) and then call a single
viewModelScope.launch { _uiEffect.send(chosenEffect) } to send it; build the
TuripShareModel only when needed and reference sessionState, TuripShareModel,
_uiEffect, and viewModelScope to locate the code to refactor.
android/app/src/main/java/com/on/turip/ui/compose/login/component/GuestModeSection.kt (1)

37-37: 접근성 개선 권장: 클릭 피드백 추가

Modifier.clickable(onClick = ...)에 ripple 효과가 기본 적용되지만, 명시적으로 indicationinteractionSource를 설정하면 더 나은 터치 피드백을 제공할 수 있습니다. 현재 텍스트는 밑줄이 있어 클릭 가능함을 암시하지만, 아이콘은 시각적 힌트가 부족할 수 있습니다.

Also applies to: 46-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/compose/login/component/GuestModeSection.kt`
at line 37, The clickables in GuestModeSection (the Modifier.clickable usages
for the text and the icon) lack explicit touch feedback; update both
Modifier.clickable(onClick = onTextClick) and the other clickable at the second
occurrence to pass an explicit indication and interactionSource — e.g., create a
remember { MutableInteractionSource() } and use rememberRipple() as the
indication — so the icon and text provide consistent ripple/interaction feedback
for accessibility and touch affordance.
android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewmodel.kt (1)

21-29: 초기 목적지 결정 호출을 1회로 가드하는 편이 안전합니다.

현재 구조에서는 재호출 시 동일한 네비게이션 effect가 중복 발행될 수 있습니다.

중복 effect 발행 방지 예시
 class SplashViewmodel `@Inject` constructor(
     private val determineInitialSessionUseCase: DetermineInitialSessionUseCase,
 ) : ViewModel() {
+    private var hasDeterminedStartDestination: Boolean = false
+
     private val _uiEffect: Channel<SplashUiEffect> = Channel(Channel.BUFFERED)
     val uiEffect: Flow<SplashUiEffect> = _uiEffect.receiveAsFlow()
 
     fun determineStartDestination() {
+        if (hasDeterminedStartDestination) return
+        hasDeterminedStartDestination = true
+
         viewModelScope.launch {
             val sessionState: SessionState = determineInitialSessionUseCase()
             when (sessionState) {
                 is SessionState.Member -> _uiEffect.send(SplashUiEffect.NavigateToMain)
                 SessionState.Guest, SessionState.Uninitialized -> _uiEffect.send(SplashUiEffect.NavigateToLogin)
             }
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewmodel.kt`
around lines 21 - 29, determineStartDestination can emit duplicate navigation
effects on repeated calls; add a one-time guard (e.g., a private boolean or
AtomicBoolean like hasDeterminedStartDestination) checked at the start of
determineStartDestination to return early if already run, and set it to true
before launching the session check so that viewModelScope.launch and subsequent
_uiEffect.send(SplashUiEffect.NavigateToMain / SplashUiEffect.NavigateToLogin)
only execute once.
android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt (1)

74-79: 업데이트 재개 조건은 helper 재사용으로 통일하는 게 좋습니다.

동일한 조건 의미를 canResumeUpdate(...)로 모아두면 분기 일관성이 좋아지고 추후 변경 시 누락 가능성을 줄일 수 있습니다.

조건 재사용 정리 예시
                 when (appUpdateInfo.updateAvailability()) {
                     UpdateAvailability.UPDATE_AVAILABLE -> {
                         if (appUpdateInfo.isUpdateTypeAllowed(UPDATE_TYPE)) {
                             appUpdateInfo.startUpdate(UPDATE_TYPE)
                         } else {
                             proceed()
                         }
                     }

                     UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> {
-                        if (appUpdateInfo.isUpdateTypeAllowed(UPDATE_TYPE)) {
+                        if (appUpdateInfo.canResumeUpdate(UPDATE_TYPE)) {
                             appUpdateInfo.startUpdate(UPDATE_TYPE)
                         } else {
                             proceed()
                         }
                     }

Also applies to: 107-109

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt` around
lines 74 - 79, Extract the shared condition used before resuming an update into
a helper method canResumeUpdate(appUpdateInfo: AppUpdateInfo): Boolean and use
it in both branches that currently check
appUpdateInfo.isUpdateTypeAllowed(UPDATE_TYPE) (the
UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS branch and the other
similar branch around startUpdate calls), then replace the inline checks with if
(canResumeUpdate(appUpdateInfo)) { appUpdateInfo.startUpdate(UPDATE_TYPE) } else
{ proceed() } so both places call the same helper and maintain consistent
behavior.
android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt (1)

314-338: Preview에서 SessionState 변형에 대한 커버리지 개선을 고려해 보세요.

현재 Preview는 SessionState.Uninitialized만 사용하고 있습니다. MyPageSettingsSection이 세션 상태에 따라 다른 UI를 보여주므로, Member/Guest 상태에 대한 Preview도 추가하면 더 포괄적인 UI 검증이 가능합니다.

♻️ SessionState PreviewParameterProvider 추가 제안
private class SessionStatePreviewProvider : PreviewParameterProvider<SessionState> {
    override val values: Sequence<SessionState> = sequenceOf(
        SessionState.Member,
        SessionState.Guest,
        SessionState.Uninitialized,
    )
}

또는 기존 Preview에서 sessionState 파라미터를 변경하여 여러 상태를 테스트할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt`
around lines 314 - 338, Add previews that cover different SessionState values so
MyPageSettingsSection is validated for Member/Guest/Uninitialized: create a
PreviewParameterProvider (e.g., SessionStatePreviewProvider) that yields
SessionState.Member, SessionState.Guest, SessionState.Uninitialized, then change
MyPageScreenPreview to accept a sessionState parameter (annotated with
`@PreviewParameter`(SessionStatePreviewProvider::class)) and pass that
sessionState into MyPageScreenContent; keep existing MyPageScreenPreview name
and all other preview params the same so only session coverage is expanded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/src/main/java/com/on/turip/data/session/DefaultTokenManager.kt`:
- Around line 54-68: The clearTokens() implementation clears the in-memory token
and cache before attempting to clear the persistent storage, causing state
mismatch on storage failures; change clearTokens() to call
userStorageRepository.clearTokens() first and only onSuccess set tokens = null
and call authCacheController.clear(), returning Result.success(Unit) there,
while preserving the onFailure branch to log the error and return
Result.failure(it); reference functions/fields: clearTokens(), setTokens(),
tokens, authCacheController, and userStorageRepository.clearTokens() to mirror
setTokens()'s success-first update pattern.
- Around line 21-22: The tokens field is only marked `@Volatile` which ensures
visibility but not atomicity, so protect concurrent access to tokens by adding a
Mutex and wrapping all reads/writes in withLock blocks: replace or keep the
`@Volatile` var tokens: AuthTokens? but ensure setTokens(), clearTokens(), and
currentTokens() acquire the same Mutex before mutating or returning tokens to
eliminate races during simultaneous 401 refreshes; use a single private val
mutex = Mutex() in DefaultTokenManager and call mutex.withLock { ... } around
the logic in setTokens(), clearTokens(), and currentTokens() (and any other
places that touch tokens) so updates/clears are atomic.

In `@android/app/src/main/java/com/on/turip/di/NetworkModule.kt`:
- Around line 119-122: The onFailure lambda currently returns null for all
failures; update it to distinguish ErrorType.Auth vs transient errors: in the
onFailure handler (the onFailure = { ... } block) inspect the provided error,
and if it's ErrorType.Auth call your token cleanup and guest-transition routines
(e.g., invoke the stored-token removal function and the GUEST state hook) so the
token is cleared and user switched to GUEST; if the error is transient (e.g.,
ErrorType.Network) preserve the token and return the existing token/response as
before; ensure other error types keep existing semantics.

In `@android/app/src/main/java/com/on/turip/domain/login/usecase/LoginUseCase.kt`:
- Line 31: The log in LoginUseCase.kt currently uses Timber.e("로그인 성공") which is
the error level; update the logging call in the login success path (the place
where Timber.e is called inside the LoginUseCase class / relevant login method)
to use an appropriate non-error level such as Timber.i(...) or Timber.d(...),
preserving the message content and any surrounding context variables.

In
`@android/app/src/main/java/com/on/turip/domain/session/usecase/SwitchToGuestUseCase.kt`:
- Around line 11-14: tokenManager.clearTokens() returns a Result<Unit> but its
result is ignored; update the suspend operator fun invoke() in
SwitchToGuestUseCase to handle the Result from tokenManager.clearTokens() (check
isSuccess/isFailure or use fold/runCatching) and only call
sessionStore.setGuest() when clearing tokens succeeds; on failure propagate or
log the error (e.g., return/throw the failure or call sessionStore.setGuest()
conditionally after success) so you avoid leaving tokens in storage while the
session is set to Guest.

---

Outside diff comments:
In `@android/app/src/main/java/com/on/turip/ui/compose/login/LoginViewmodel.kt`:
- Around line 73-75: The onFailure block currently only logs the migration
failure; update the onFailure handler in LoginViewmodel.kt (the onFailure { ...
} following the migration call) to construct or extract an errorType and pass it
to handleError(errorType) so the UI receives feedback, and also update any
dialog/progress state (e.g., hide the migration dialog or set
showProgress=false) so the dialog does not remain displayed; keep the Timber.e
logging but add handleError(errorType) and the dialog dismissal/state change in
that same onFailure branch.

---

Nitpick comments:
In
`@android/app/src/main/java/com/on/turip/data/login/datasource/CredentialProvider.kt`:
- Line 6: Add a KDoc comment to the CredentialProvider.getIdToken() declaration
that clearly specifies the token validity contract: state whether empty or
whitespace-only strings are allowed or must be treated as failure, what
TuripResult variants will be returned for success vs invalid/missing token, and
any normalization (e.g., trimmed) or lifetime expectations callers can rely on;
update the suspend fun getIdToken(): TuripResult<String> declaration's KDoc to
reflect these rules so callers implement consistent defensive checks.

In
`@android/app/src/main/java/com/on/turip/ui/compose/login/component/GuestModeSection.kt`:
- Line 37: The clickables in GuestModeSection (the Modifier.clickable usages for
the text and the icon) lack explicit touch feedback; update both
Modifier.clickable(onClick = onTextClick) and the other clickable at the second
occurrence to pass an explicit indication and interactionSource — e.g., create a
remember { MutableInteractionSource() } and use rememberRipple() as the
indication — so the icon and text provide consistent ripple/interaction feedback
for accessibility and touch affordance.

In `@android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt`:
- Around line 314-338: Add previews that cover different SessionState values so
MyPageSettingsSection is validated for Member/Guest/Uninitialized: create a
PreviewParameterProvider (e.g., SessionStatePreviewProvider) that yields
SessionState.Member, SessionState.Guest, SessionState.Uninitialized, then change
MyPageScreenPreview to accept a sessionState parameter (annotated with
`@PreviewParameter`(SessionStatePreviewProvider::class)) and pass that
sessionState into MyPageScreenContent; keep existing MyPageScreenPreview name
and all other preview params the same so only session coverage is expanded.

In `@android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewmodel.kt`:
- Around line 21-29: determineStartDestination can emit duplicate navigation
effects on repeated calls; add a one-time guard (e.g., a private boolean or
AtomicBoolean like hasDeterminedStartDestination) checked at the start of
determineStartDestination to return early if already run, and set it to true
before launching the session check so that viewModelScope.launch and subsequent
_uiEffect.send(SplashUiEffect.NavigateToMain / SplashUiEffect.NavigateToLogin)
only execute once.

In
`@android/app/src/main/java/com/on/turip/ui/compose/turipdetail/TuripDetailViewModel.kt`:
- Around line 302-317: The branches in TuripDetailViewModel.kt duplicate
coroutine launches when sending effects; instead determine the
TuripPlaceUiEffect first (using sessionState.value to choose between
TuripPlaceUiEffect.ShareTurip(TuripShareModel(...)) and
TuripPlaceUiEffect.ShowTuripShareNotAllowed) and then call a single
viewModelScope.launch { _uiEffect.send(chosenEffect) } to send it; build the
TuripShareModel only when needed and reference sessionState, TuripShareModel,
_uiEffect, and viewModelScope to locate the code to refactor.

In `@android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt`:
- Around line 74-79: Extract the shared condition used before resuming an update
into a helper method canResumeUpdate(appUpdateInfo: AppUpdateInfo): Boolean and
use it in both branches that currently check
appUpdateInfo.isUpdateTypeAllowed(UPDATE_TYPE) (the
UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS branch and the other
similar branch around startUpdate calls), then replace the inline checks with if
(canResumeUpdate(appUpdateInfo)) { appUpdateInfo.startUpdate(UPDATE_TYPE) } else
{ proceed() } so both places call the same helper and maintain consistent
behavior.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0eff6d9 and 4d27ff1.

📒 Files selected for processing (42)
  • android/app/src/main/java/com/on/turip/common/AuthState.kt
  • android/app/src/main/java/com/on/turip/common/UserType.kt
  • android/app/src/main/java/com/on/turip/data/login/TokenAuthenticator.kt
  • android/app/src/main/java/com/on/turip/data/login/datasource/CredentialProvider.kt
  • android/app/src/main/java/com/on/turip/data/login/datasource/GoogleCredentialManager.kt
  • android/app/src/main/java/com/on/turip/data/session/DefaultAuthTokenCacheController.kt
  • android/app/src/main/java/com/on/turip/data/session/DefaultSessionStore.kt
  • android/app/src/main/java/com/on/turip/data/session/DefaultTokenManager.kt
  • android/app/src/main/java/com/on/turip/di/LocalStorageModule.kt
  • android/app/src/main/java/com/on/turip/di/NetworkModule.kt
  • android/app/src/main/java/com/on/turip/di/SessionModule.kt
  • android/app/src/main/java/com/on/turip/domain/login/usecase/CheckUserSignedInUseCase.kt
  • android/app/src/main/java/com/on/turip/domain/login/usecase/LoginUseCase.kt
  • android/app/src/main/java/com/on/turip/domain/login/usecase/LoginUserUseCase.kt
  • android/app/src/main/java/com/on/turip/domain/session/AuthTokenCacheController.kt
  • android/app/src/main/java/com/on/turip/domain/session/SessionState.kt
  • android/app/src/main/java/com/on/turip/domain/session/SessionStore.kt
  • android/app/src/main/java/com/on/turip/domain/session/TokenManager.kt
  • android/app/src/main/java/com/on/turip/domain/session/usecase/DetermineInitialSessionUseCase.kt
  • android/app/src/main/java/com/on/turip/domain/session/usecase/SwitchToGuestUseCase.kt
  • android/app/src/main/java/com/on/turip/ui/common/error/ErrorUiState.kt
  • android/app/src/main/java/com/on/turip/ui/compose/designsystem/component/ErrorScreen.kt
  • android/app/src/main/java/com/on/turip/ui/compose/login/LoginScreen.kt
  • android/app/src/main/java/com/on/turip/ui/compose/login/LoginViewmodel.kt
  • android/app/src/main/java/com/on/turip/ui/compose/login/component/GoogleLoginButton.kt
  • android/app/src/main/java/com/on/turip/ui/compose/login/component/GuestModeSection.kt
  • android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageScreen.kt
  • android/app/src/main/java/com/on/turip/ui/compose/mypage/MyPageViewModel.kt
  • android/app/src/main/java/com/on/turip/ui/compose/mypage/component/MyPageSettingsSection.kt
  • android/app/src/main/java/com/on/turip/ui/compose/splash/SplashScreen.kt
  • android/app/src/main/java/com/on/turip/ui/compose/splash/SplashUiEffect.kt
  • android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewmodel.kt
  • android/app/src/main/java/com/on/turip/ui/compose/trip/turipselection/PlaceTuripSelectionViewModel.kt
  • android/app/src/main/java/com/on/turip/ui/compose/turipdetail/TuripDetailViewModel.kt
  • android/app/src/main/java/com/on/turip/ui/login/LoginActivity.kt
  • android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt
  • android/app/src/main/java/com/on/turip/ui/splash/SplashViewmodel.kt
  • android/app/src/main/res/layout/activity_splash.xml
  • android/app/src/main/res/values-v31/themes.xml
  • android/app/src/main/res/values/colors.xml
  • android/app/src/main/res/values/strings.xml
  • android/gradle/libs.versions.toml
💤 Files with no reviewable changes (7)
  • android/app/src/main/java/com/on/turip/domain/login/usecase/CheckUserSignedInUseCase.kt
  • android/app/src/main/java/com/on/turip/common/AuthState.kt
  • android/app/src/main/java/com/on/turip/common/UserType.kt
  • android/app/src/main/java/com/on/turip/domain/login/usecase/LoginUserUseCase.kt
  • android/app/src/main/res/layout/activity_splash.xml
  • android/app/src/main/java/com/on/turip/ui/splash/SplashViewmodel.kt
  • android/app/src/main/java/com/on/turip/data/login/TokenAuthenticator.kt

Copy link
Contributor

@jerry8282 jerry8282 left a comment

Choose a reason for hiding this comment

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

빠른 구현 너무 좋네요 채넛 굿굿!!👍

import javax.inject.Inject

@HiltViewModel
class SplashViewmodel @Inject constructor(
Copy link
Contributor

Choose a reason for hiding this comment

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

c: 컨벤션 통일해야 될 거 같아유 SplashViewModel로 변경하는 게 좋아보이네용

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오, 매의 눈 제리! 👍
스플래시 화면 처음 구현했을 때부터 지금까지 전혀 몰랐네요, 😅

반영 커밋 : b107a94

Copy link

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewModel.kt`:
- Around line 22-27: Wrap the call to determineInitialSessionUseCase() inside a
try/catch within the existing viewModelScope.launch so any exceptions from
tokenManager.initialize(), sessionStore.setGuest/setMember(),
switchToGuestUseCase(), or the use case itself are caught; in the catch block
send a fallback effect via _uiEffect.send(SplashUiEffect.NavigateToLogin) (or
another agreed fallback) to ensure the splash never deadlocks, and keep the
original when handling for successful SessionState.Member/Guest/Uninitialized
paths unchanged.

In `@android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt`:
- Around line 111-117: The current AppUpdateInfo.startUpdate extension calls
appUpdateManager.startUpdateFlowForResult(...) without checking preconditions or
handling a false return; update this flow to first verify updateAvailability()
== UPDATE_AVAILABLE and isUpdateTypeAllowed(updateType) before calling
appUpdateManager.startUpdateFlowForResult, then check its boolean result and if
it returns false navigate to the main screen (rather than leaving the splash
stuck or calling finish()), and add logic in onResume() to detect
DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS and resume the update when appropriate;
reference the AppUpdateInfo.startUpdate function,
appUpdateManager.startUpdateFlowForResult,
updateAvailability()/UPDATE_AVAILABLE, isUpdateTypeAllowed(), updateLauncher,
and onResume()/DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS when applying the changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7b19e3f8-6830-49a3-8cee-d5844ba0730b

📥 Commits

Reviewing files that changed from the base of the PR and between 4d27ff1 and b107a94.

📒 Files selected for processing (2)
  • android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewModel.kt
  • android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt

Comment on lines +22 to +27
viewModelScope.launch {
val sessionState: SessionState = determineInitialSessionUseCase()
when (sessionState) {
is SessionState.Member -> _uiEffect.send(SplashUiEffect.NavigateToMain)
SessionState.Guest, SessionState.Uninitialized -> _uiEffect.send(SplashUiEffect.NavigateToLogin)
}
Copy link

@coderabbitai coderabbitai bot Mar 4, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "SplashViewModel.kt" -type f

Repository: woowacourse-teams/2025-Turip

Length of output: 149


🏁 Script executed:

fd -t f "SplashViewModel\.kt"

Repository: woowacourse-teams/2025-Turip

Length of output: 147


🏁 Script executed:

cat -n android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewModel.kt

Repository: woowacourse-teams/2025-Turip

Length of output: 1474


🏁 Script executed:

find . -name "*DetermineInitialSessionUseCase*" -type f

Repository: woowacourse-teams/2025-Turip

Length of output: 169


🏁 Script executed:

rg "class DetermineInitialSessionUseCase" -t kotlin -A 20

Repository: woowacourse-teams/2025-Turip

Length of output: 2826


🏁 Script executed:

cat -n android/app/src/main/java/com/on/turip/domain/session/usecase/DetermineInitialSessionUseCase.kt

Repository: woowacourse-teams/2025-Turip

Length of output: 2117


🏁 Script executed:

rg "viewModelScope" android/app/src/main/java/com/on/turip/ui/compose -t kotlin -B 5 -A 5 | head -80

Repository: woowacourse-teams/2025-Turip

Length of output: 8325


🏁 Script executed:

rg "CoroutineExceptionHandler" android/app/src/main/java/com/on/turip -t kotlin -B 2 -A 2

Repository: woowacourse-teams/2025-Turip

Length of output: 54


🏁 Script executed:

rg "viewModelScope.launch \{" -A 20 android/app/src/main/java/com/on/turip/ui/compose -t kotlin | grep -A 20 "try \{"

Repository: woowacourse-teams/2025-Turip

Length of output: 90


🏁 Script executed:

rg "runCatching" android/app/src/main/java/com/on/turip/ui/compose -t kotlin -B 2 -A 5

Repository: woowacourse-teams/2025-Turip

Length of output: 54


🏁 Script executed:

rg "try \{" android/app/src/main/java/com/on/turip/ui/compose -t kotlin -B 3 -A 8 | head -60

Repository: woowacourse-teams/2025-Turip

Length of output: 6260


🏁 Script executed:

rg "\.fold\(" android/app/src/main/java/com/on/turip/ui/compose -t kotlin -B 2 -A 5 | head -80

Repository: woowacourse-teams/2025-Turip

Length of output: 54


세션 판별 실패 시 스플래시에서 크래시가 날 수 있습니다.

determineInitialSessionUseCase() 내부의 tokenManager.initialize(), sessionStore.setGuest/setMember(), switchToGuestUseCase() 호출에서 예외가 발생하면 아무 처리 없이 coroutine이 실패합니다. 결과적으로 _uiEffect가 전송되지 않아 스플래시 화면이 멈추게 됩니다. 예외 발생 시 fallback effect를 명시적으로 전송하도록 수정하세요.

제안 수정안
     fun determineStartDestination() {
         viewModelScope.launch {
-            val sessionState: SessionState = determineInitialSessionUseCase()
-            when (sessionState) {
-                is SessionState.Member -> _uiEffect.send(SplashUiEffect.NavigateToMain)
-                SessionState.Guest, SessionState.Uninitialized -> _uiEffect.send(SplashUiEffect.NavigateToLogin)
-            }
+            val nextEffect: SplashUiEffect = runCatching {
+                val sessionState: SessionState = determineInitialSessionUseCase()
+                when (sessionState) {
+                    is SessionState.Member -> SplashUiEffect.NavigateToMain
+                    SessionState.Guest, SessionState.Uninitialized -> SplashUiEffect.NavigateToLogin
+                }
+            }.getOrElse {
+                SplashUiEffect.NavigateToLogin
+            }
+
+            _uiEffect.send(nextEffect)
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/splash/SplashViewModel.kt`
around lines 22 - 27, Wrap the call to determineInitialSessionUseCase() inside a
try/catch within the existing viewModelScope.launch so any exceptions from
tokenManager.initialize(), sessionStore.setGuest/setMember(),
switchToGuestUseCase(), or the use case itself are caught; in the catch block
send a fallback effect via _uiEffect.send(SplashUiEffect.NavigateToLogin) (or
another agreed fallback) to ensure the splash never deadlocks, and keep the
original when handling for successful SessionState.Member/Guest/Uninitialized
paths unchanged.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

예외 발생해도 다 타입으로 내려주도록 되어 있는데요..?

Copy link

Choose a reason for hiding this comment

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

@yrsel, 맞습니다. DetermineInitialSessionUseCase의 AI 요약을 다시 확인하니, 해당 유스케이스는 토큰 없음 → Guest, 토큰 검증 성공 → Member, 인증 오류 → Guest, 그 외 오류 → Member 등 모든 경로에서 예외를 내부적으로 처리하여 항상 SessionState 타입으로 반환하도록 설계되어 있네요. 제 리뷰 코멘트가 잘못되었습니다. 죄송합니다!


🧠 Learnings used
Learnt from: yrsel
Repo: woowacourse-teams/2025-Turip PR: 483
File: android/app/src/main/java/com/on/turip/ui/compose/login/LoginScreen.kt:30-30
Timestamp: 2025-11-30T12:23:26.672Z
Learning: In Android projects using Hilt with Jetpack Compose, the correct import for hiltViewModel() as of 2025 is androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel from the androidx.hilt:hilt-lifecycle-viewmodel-compose artifact. The old import androidx.hilt.navigation.compose.hiltViewModel from androidx.hilt:hilt-navigation-compose is deprecated.

Learnt from: yrsel
Repo: woowacourse-teams/2025-Turip PR: 483
File: android/app/src/main/java/com/on/turip/ui/main/favorite/FavoriteContentFragment.kt:92-99
Timestamp: 2025-12-02T06:06:33.426Z
Learning: In Android Kotlin Fragments, when collecting Flow or StateFlow in onViewCreated or setupObservers, always use viewLifecycleOwner.lifecycleScope.launch with viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) pattern instead of just lifecycleScope.launch. This prevents multiple overlapping collectors when the view is detached and reattached, and ensures collection stops when the view is destroyed. Example: viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { flow.collect { } } }

Comment on lines +111 to +117
private fun AppUpdateInfo.startUpdate(updateType: Int) {
appUpdateManager.startUpdateFlowForResult(
this,
updateLauncher,
AppUpdateOptions.newBuilder(updateType).build(),
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Google Play Core In-App Updates AppUpdateManager.startUpdateFlowForResult return value boolean documentation

💡 Result:

Per the official Play In-App Updates API reference, AppUpdateManager.startUpdateFlowForResult(...) returns a boolean indicating whether the requested update flow was successfully started (i.e., the update UI launch was initiated). If it returns false, the update flow did not start. [1]

Documentation: AppUpdateManagerstartUpdateFlowForResult(AppUpdateInfo, ActivityResultLauncher<IntentSenderRequest>, AppUpdateOptions)Returns:boolean whether the requested update has started.” [1]

Source(s)
[1] Android Developers – AppUpdateManager API reference (method startUpdateFlowForResult(...) return value) (developer.android.com)

Citations:


🌐 Web query:

Google Play In-App Updates startUpdateFlowForResult false return handling best practices what to do

💡 Result:

AppUpdateManager.startUpdateFlowForResult(...) returning false means the Play UI update flow did not start, so you should treat it as “no update UI was launched” and continue your normal app UX (optionally with a fallback). The method’s return value is explicitly “whether the requested update has started”. [1]

Best-practice handling when it returns false

  1. Always pre-check before starting the flow

    • Only call startUpdateFlowForResult when:
      • updateAvailability() is UPDATE_AVAILABLE, or you’re resuming an already-started update in DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS, and
      • the chosen update type/options are allowed via isUpdateTypeAllowed(...). [2][3]
        If the type isn’t allowed, the flow won’t start. [1]
  2. If you need to retry, fetch a fresh AppUpdateInfo

    • Each AppUpdateInfo instance is single-use for starting the update flow; reusing it for a retry can prevent the flow from starting (or throw) — so on false, call getAppUpdateInfo() again and re-evaluate availability/allowed-ness before retrying. [1][2]
  3. Resume stalled/in-progress updates on foreground

    • In onResume/when returning to foreground, if updateAvailability() is DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS, call startUpdateFlowForResult(...) again to resume. [3]
  4. Handle the result separately from the boolean

    • The boolean only says whether the UI launch started.
    • The user outcome comes from the activity result (accepted / canceled / failed), and your app “should determine the best way to proceed” when the Play UI closes. [2]
  5. Have a fallback when the flow can’t be started

    • Common fallback: open your Play Store listing (e.g., market://details?id=...), or show a non-blocking “Update available” UI and let the user try later—especially if startUpdateFlowForResult returns false repeatedly.
  6. Testing sanity check

    • In-app updates generally won’t behave as expected unless the app was installed from Google Play (internal/closed/open testing, internal app sharing, etc.). [4]

Practical default: if startUpdateFlowForResult(...) returns false, log it (with updateAvailability + isUpdateTypeAllowed), fetch a fresh AppUpdateInfo, and if it still can’t start, fall back to opening the Play Store listing or defer the prompt.

Sources

[1] https://developer.android.com/reference/com/google/android/play/core/appupdate/AppUpdateManager
[2] https://developer.android.com/guide/playcore/in-app-updates/kotlin-java
[3] https://developer.android.com/guide/playcore/in-app-updates/kotlin-java (resume DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS)
[4] https://stackoverflow.com/questions/57534545/appupdatemanager-startupdateflowforresult-not-showing-dialog


🌐 Web query:

splash screen in-app update flow handling when startUpdateFlowForResult returns false Google Play

💡 Result:

If AppUpdateManager.startUpdateFlowForResult(...) returns false, it means the requested update flow did not start—either because the update isn’t allowed for the provided AppUpdateInfo/AppUpdateOptions, or because an error/invalid request occurred. You should treat it as “no Play UI will appear” and continue your normal app navigation (e.g., leave the splash screen), unless you have a hard requirement to block usage. [1]

What to do (typical splash-screen handling)

  1. Only call startUpdateFlowForResult when:
    • updateAvailability() == UPDATE_AVAILABLE, and
    • isUpdateTypeAllowed(AppUpdateType.IMMEDIATE/FLEXIBLE) is true (or equivalent with AppUpdateOptions). (This prevents many “returns false” cases.)
  2. If it returns false:
    • Don’t keep the splash stuck waiting—navigate to your main screen.
    • Optionally retry later (next app entry point / next onResume) rather than looping on splash.
  3. Also implement the resume path:
    • On onResume(), if updateAvailability() == DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS, call startUpdateFlowForResult(...) again to resume an immediate update the user already started. [2]
  4. For a mandatory update experience:
    • If starting the in-app flow fails (returns false) you can show an explanatory screen and offer a button to open the Play Store listing as a fallback—but avoid trapping users on splash without a way forward.

Sources: Android Developers Play in-app updates reference (return value semantics) [1], Android Developers Kotlin/Java in-app updates guide (resume + cancel/decline handling guidance) [2].


반환값 확인 및 실패 시 처리 방식 개선 필요

startUpdateFlowForResult()는 boolean을 반환하여 업데이트 플로우가 성공적으로 시작되었는지를 나타냅니다. false 반환 시 플로우가 시작되지 않았으므로 스플래시 화면에 사용자가 갇히게 됩니다. 다음과 같이 개선하세요:

  1. 호출 전에 사전 조건 확인: updateAvailability() == UPDATE_AVAILABLE이고 isUpdateTypeAllowed()가 true인 경우에만 호출
  2. 반환값 확인 후 false 시: finish() 호출 대신 메인 화면으로 진행하여 사용자가 앱을 계속 사용할 수 있도록 함
  3. onResume()에서 DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS 상태를 확인하여 진행 중인 업데이트 재개

또는 유연한 업데이트 사용 시에는 false 반환 상황에 대해 나중에 재시도할 수 있도록 처리하는 것이 권장됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/splash/SplashActivity.kt` around
lines 111 - 117, The current AppUpdateInfo.startUpdate extension calls
appUpdateManager.startUpdateFlowForResult(...) without checking preconditions or
handling a false return; update this flow to first verify updateAvailability()
== UPDATE_AVAILABLE and isUpdateTypeAllowed(updateType) before calling
appUpdateManager.startUpdateFlowForResult, then check its boolean result and if
it returns false navigate to the main screen (rather than leaving the splash
stuck or calling finish()), and add logic in onResume() to detect
DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS and resume the update when appropriate;
reference the AppUpdateInfo.startUpdate function,
appUpdateManager.startUpdateFlowForResult,
updateAvailability()/UPDATE_AVAILABLE, isUpdateTypeAllowed(), updateLauncher,
and onResume()/DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS when applying the changes.

@jerry8282 jerry8282 merged commit 68f38da into develop Mar 5, 2026
4 checks passed
@jerry8282 jerry8282 deleted the feature/#619 branch March 5, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🌰 채넛 우아한 테크코스 7기 안드로이드 채넛 🤖 Android 안드로이드얌 📝 feat

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Splash 화면 Compose 마이그레이션

2 participants