Repository navigation
fix: 서버 패스키 등록과 목록 상태 복구 (#1944) - #2236
Conversation
|
📸 CI 표준 환경에서 screenshot baseline 2개를 생성·검증해 |
Sadturtleman
left a comment
There was a problem hiding this comment.
서버 등록 성공 뒤의 로컬 캐시 저장이 화면 이탈 시 취소될 수 있어, 서버에는 패스키가 있는데 애프터노트 지문 관문(isPasskeyRegistered)이 꺼진 채 남는 경로가 있습니다. 인라인 코멘트 확인 부탁드립니다.
| // 서버 성공을 먼저 확정한다. 캐시의 실패나 취소는 재등록을 유도하지 않는다. | ||
| dispatch(PassKeyReducerEvent.Finished(registrationId, PasskeyRegistrationResult.Success)) | ||
| currentCoroutineContext().ensureActive() | ||
| runCatchingCancellable { userProfileCacheRepository.savePasskeyRegistered(true) } |
There was a problem hiding this comment.
서버 등록이 성공한 뒤의 캐시 저장이 registrationJob 안에서 취소 가능한 상태로 실행됩니다. 그런데 이 캐시(isPasskeyRegisteredFlow)는 단순 표시용이 아니라 AfternoteHostViewModel → FingerprintLoginRoute 의 지문 관문 on/off 판단에 쓰입니다(false 면 관문 없이 바로 통과).
실패 시나리오(PIN 경로): PassKeyPasswordScreen 에서 Finished(Success) 가 나가면 다음 프레임의 ObserveSignal 이 바로 onPinComplete → popBack() 을 호출하고, 그 다음 프레임에 DisposableEffect 의 CancelRegistration 이 canceledJob?.cancel() 을 부릅니다. 이 시점에 DataStore 쓰기(IO 전환 + 파일 기록)가 아직 끝나지 않았으면 119행 ensureActive() 또는 edit 안에서 취소되어 저장이 빠집니다. 회전 등 구성 변경으로 화면이 폐기돼도 같습니다. 결과적으로 서버에는 패스키가 등록됐는데 로컬 플래그는 false 로 남아 애프터노트 생체 관문이 계속 꺼져 있고, 목록 화면은 서버 목록만 보므로 이를 다시 맞춰 주는 경로도 없습니다.
제안: 서버 성공 이후의 캐시 저장은 화면 수명과 분리해 주세요. 예) withContext(NonCancellable) { userProfileCacheRepository.savePasskeyRegistered(true) } 로 감싸고 119행 ensureActive() 제거, 또는 Finished(Success) 이후에는 CancelRegistration 이 job 을 취소하지 않도록 처리. (아울러 PassKeyListViewModel 이 서버 목록 조회 성공 시 캐시를 동기화하면 기존 불일치도 복구됩니다.) 현재 registration_serverSuccessIsVisibleBeforeCacheCompletesAndCacheCancellationKeepsIt 테스트는 이 취소를 허용 동작으로 고정하고 있으니 함께 조정이 필요합니다.
There was a problem hiding this comment.
e398dca 에서 반영했습니다.
- 서버 등록 성공 뒤의
savePasskeyRegistered(true)를withContext(NonCancellable)로 감싸고 앞의ensureActive()를 뺐습니다. 화면 이탈이나 구성 변경으로CancelRegistration이 job 을 취소해도 저장은 끝납니다. 이 경로는registration_leavingScreenAfterServerSuccessStillSavesCache로 고정했습니다. PassKeyListViewModel은 서버 목록 조회에 성공했고 목록이 비어 있지 않으면 캐시를 true 로 맞춥니다. 이미 어긋난 기기도 목록 화면에 한 번 들어오면 관문이 다시 켜집니다. 빈 목록으로 캐시를 false 로 내리는 방향은 넣지 않았습니다. 목록 응답 하나로 지문 관문이 꺼지는 경로를 새로 만들지 않으려는 것입니다.- 기존
registration_serverSuccessIsVisibleBeforeCacheCompletesAndCacheCancellationKeepsIt는 캐시 쓰기 자체가 취소 예외를 낸 경우(job 취소가 아님)라 그대로 둡니다. 서버 성공 상태가 유지되는 것은 여전히 맞습니다.
23f5532 to
a9c892a
Compare
a9c892a to
eb46e61
Compare
merge queue 방출 — merge group CI 실패사유 실패한 job: 고친 뒤 |
먼저
PassKeyViewModel.kt,PassKeyListViewModel.kt,SettingNavHost.kt,PasskeyRepositoryImpl.kt를 읽어 주세요.변경
생체 인증이나 PIN 입력만으로 패스키 등록 완료를 표시하던 경로를 서버 등록에 연결했습니다. 서버 옵션을 Credential Manager에 전달하고 생성된 credential을 서버가 검증·등록한 뒤에만 완료 상태와 로컬 캐시를 갱신합니다. 옵션·플랫폼·서버 실패와 사용자 취소를 구분하며, 캐시 실패는 이미 성공한 서버 등록을 실패로 바꾸지 않습니다. 취소된 요청과 이전 결과 소비는 새로운 시도의 상태나 서버 호출을 덮어쓰지 않습니다.
패스키 관리는 로컬 boolean 대신 서버 목록을 표시합니다. 로딩·조회 실패·재시도·실제 빈 목록을 구분하고, 등록 화면에서 같은 Navigation3 entry로 돌아와도 목록을 갱신합니다. PassKey와 AppLockSetup은 MVI 계약으로 전환했으며, 생체 인증은 선행 PR #2226의 API29 대응 공통 구현을 그대로 사용합니다.
Closes #1944
검증
CI Test Plan
{ "androidTest": { "mode": "full", "reason": "앱에 setting data 테스트 fixture를 연결하여 Hilt와 실제 설정 Navigation3 진입, Credential Manager 실패 및 목록 재진입을 API30 전체 계측 테스트로 확인하고 API34 접근성 smoke도 실행합니다.", "tests": [] } }선행 관계와 검증 범위
실제 base는
fix/2200-legacy-biometric-auth입니다. #708 → #2202 → #2226의 최종 구조를 소비하며 이 PR의 diff에는 패스키 복구만 포함합니다. 이전 #1927의 관련 구현을 현재 MVI·생체 인증 경계에 맞게 재구성했습니다. BE main6adc0c402b7253f5f23bf6878eb5d36054c5bb6a의 옵션·등록·목록 계약과 대조했습니다. 실제 서버 WebAuthn 등록 성공과 물리 기기 생체 QA는 #555 범위로 남습니다.