Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/src/main/java/com/example/slowclock/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.example.slowclock.data.model.ThemeMode
import com.example.slowclock.data.remote.repository.AuthRepository
import com.example.slowclock.data.remote.repository.SettingsRepository
import com.example.slowclock.data.remote.repository.UserRepository
import com.example.slowclock.domain.profile.SignOutUseCase
import com.example.slowclock.navigation.AppNavigation
import com.example.slowclock.ui.theme.SlowClockTheme
import com.firebase.ui.auth.AuthUI
Expand All @@ -43,6 +44,9 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var settingsRepository: SettingsRepository

@Inject
lateinit var signOutUseCase: SignOutUseCase

private fun handleNewInstallation() {
val prefs = getSharedPreferences("app_state", MODE_PRIVATE)
val isAppEverLaunched = prefs.getBoolean("app_launched", false)
Expand All @@ -51,7 +55,9 @@ class MainActivity : ComponentActivity() {
// 이 앱이 처음 실행됨 = 새 설치
Log.d("INSTALL", "새 설치 감지 - Firebase 로그아웃")

authRepository.signOut()
// 앱을 지웠다 다시 깐 기기다. 세션뿐 아니라 앞 설치가 남긴 알람 장부와 공유 코드도
// 함께 비운다. 남기면 목록에 없는 알람이 계속 울린다(#165).
signOutUseCase()
AuthUI.getInstance().signOut(this)

// 플래그 저장
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ class SettingsRepository
prefs.edit().putString(KEY_SHARE_CODE, shareCode.trim()).apply()
}

/**
* 등록해 둔 가족의 공유 코드를 지운다. 세션이 끝날 때 부른다.
*
* 남겨 두면 같은 기기에 다른 사람이 로그인했을 때 앞 사람이 등록한 가족의 일정이
* 그대로 보인다. 어르신 폰을 자녀가 잠깐 쓰는 일이 이 앱에서는 드물지 않다(#165).
*/
fun clearShareCode() {
prefs.edit().remove(KEY_SHARE_CODE).apply()
}

fun getThemeMode(): ThemeMode = ThemeMode.fromName(prefs.getString(KEY_THEME_MODE, null))

fun setThemeMode(mode: ThemeMode) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.slowclock.domain.profile

import com.example.slowclock.core.alarm.AlarmScheduler
import com.example.slowclock.data.remote.repository.AuthRepository
import com.example.slowclock.data.remote.repository.SettingsRepository
import javax.inject.Inject

/**
* 로그아웃. 세션을 끊는 것으로 끝나지 않고 이 기기에 남는 그 사람의 흔적을 함께 지운다.
*
* 알람은 기기 안 장부로 살아 있고 로그인 여부를 보지 않는다. 반복 일정은 회차가 무한히
* 이어지고(#130) 재부팅에도 살아남으므로(#127), 지우지 않으면 로그아웃한 계정의 알람이 그
* 기기에서 영구히 매일 울린다. 목록에는 없으니 사용자가 끌 방법도 없다(#165).
*
* 등록해 둔 가족의 공유 코드도 지운다. 남겨 두면 같은 기기에 다른 사람이 로그인했을 때 앞
* 사람이 등록한 가족의 일정이 그대로 보인다.
*
* 여러 Repository 를 순서대로 조합하고 두 화면이 함께 쓰므로 UseCase 로 둔다. 계정 삭제가
* 이미 같은 짝을 [DeleteAccountUseCase] 에서 맞추고 있어 자리도 같다.
*/
class SignOutUseCase
@Inject
constructor(
private val authRepository: AuthRepository,
private val alarmScheduler: AlarmScheduler,
private val settingsRepository: SettingsRepository,
) {
operator fun invoke() {
// 세션을 끊기 전에 기기 잔재부터 지운다. 순서가 반대면 중간에 실패했을 때
// 로그인은 풀렸는데 알람만 남는 상태가 된다.
alarmScheduler.cancelAll()
settingsRepository.clearShareCode()
authRepository.signOut()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.example.slowclock.data.remote.repository.UserRepository
import com.example.slowclock.domain.profile.DeleteAccountResult
import com.example.slowclock.domain.profile.DeleteAccountStep
import com.example.slowclock.domain.profile.DeleteAccountUseCase
import com.example.slowclock.domain.profile.SignOutUseCase
import com.example.slowclock.ui.mvi.MviViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
Expand All @@ -18,6 +19,7 @@ class ProfileViewModel
private val userRepository: UserRepository,
private val authRepository: AuthRepository,
private val deleteAccount: DeleteAccountUseCase,
private val signOutUseCase: SignOutUseCase,
) : MviViewModel<ProfileIntent, ProfileUiState, ProfileReducerEvent>(ProfileUiState()) {
init {
loadProfile()
Expand Down Expand Up @@ -145,7 +147,8 @@ class ProfileViewModel
}

private fun signOut() {
authRepository.signOut()
// 세션만 끊으면 이 기기에 걸린 알람과 등록해 둔 공유 코드가 남는다(#165).
signOutUseCase()
dispatch(ProfileReducerEvent.Left(ProfileLeaveReason.SIGNED_OUT))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.slowclock.domain.profile

import com.example.slowclock.core.alarm.AlarmScheduler
import com.example.slowclock.data.remote.repository.AuthRepository
import com.example.slowclock.data.remote.repository.SettingsRepository
import io.mockk.mockk
import io.mockk.verifyOrder
import org.junit.Test

/**
* 로그아웃이 기기에 남는 흔적을 함께 지우는지 본다.
*
* 지우지 않으면 로그아웃한 계정의 반복 알람이 그 기기에서 영구히 매일 울린다. 목록에는 없으니
* 사용자가 끌 방법도 없다(#165).
*/
class SignOutUseCaseTest {
private val authRepository = mockk<AuthRepository>(relaxed = true)
private val alarmScheduler = mockk<AlarmScheduler>(relaxed = true)
private val settingsRepository = mockk<SettingsRepository>(relaxed = true)

private val signOut = SignOutUseCase(authRepository, alarmScheduler, settingsRepository)

@Test
fun `세션을 끊기 전에 기기 잔재를 먼저 지운다`() {
// 순서가 반대면 중간에 실패했을 때 로그인은 풀렸는데 알람만 남는다.
signOut()

verifyOrder {
alarmScheduler.cancelAll()
settingsRepository.clearShareCode()
authRepository.signOut()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.example.slowclock.data.remote.repository.UserRepository
import com.example.slowclock.domain.profile.DeleteAccountResult
import com.example.slowclock.domain.profile.DeleteAccountStep
import com.example.slowclock.domain.profile.DeleteAccountUseCase
import com.example.slowclock.domain.profile.SignOutUseCase
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
Expand Down Expand Up @@ -35,6 +36,7 @@ class ProfileViewModelTest {
private val userRepository = mockk<UserRepository>()
private val authRepository = mockk<AuthRepository>()
private val deleteAccount = mockk<DeleteAccountUseCase>()
private val signOutUseCase = mockk<SignOutUseCase>(relaxed = true)

@Before
fun setUp() {
Expand All @@ -51,7 +53,7 @@ class ProfileViewModelTest {
Dispatchers.resetMain()
}

private fun createViewModel() = ProfileViewModel(userRepository, authRepository, deleteAccount)
private fun createViewModel() = ProfileViewModel(userRepository, authRepository, deleteAccount, signOutUseCase)

@Test
fun `Firestore 사용자 문서로 이름·이메일·공유 코드를 채운다`() =
Expand Down Expand Up @@ -94,7 +96,7 @@ class ProfileViewModelTest {

viewModel.onIntent(ProfileIntent.SignOut)

verify(exactly = 1) { authRepository.signOut() }
verify(exactly = 1) { signOutUseCase() }
assertEquals(ProfileLeaveReason.SIGNED_OUT, viewModel.uiState.value.leave)

viewModel.onIntent(ProfileIntent.ConsumeLeave)
Expand Down
Loading