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
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ interface LoginRepository {

suspend fun setIsShopDeleted(isShopDeleted: Boolean)

suspend fun clearUserPreferences()
/**
* Forgets the signed-in shopkeeper (session, account, permissions, one-shot flags) and keeps the
* device settings (dark theme, tips already shown).
*/
suspend fun clearSession()

fun isLoggedIn(): Flow<Boolean>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ class LoginRepositoryImpl @Inject constructor(
val response = api.logout()

response.suspendOnSuccess {
clearUserPreferences()
clearSession()
emit(true)
}.suspendOnFailure {
clearUserPreferences()
clearSession()
throwApiError(response, message())
}
}.flowOn(ioDispatcher)
Expand Down Expand Up @@ -111,8 +111,8 @@ class LoginRepositoryImpl @Inject constructor(
natPreferencesDataSource.setIsShopDeleted(isShopDeleted)
}

override suspend fun clearUserPreferences() {
natPreferencesDataSource.clearUserPreferences()
override suspend fun clearSession() {
natPreferencesDataSource.clearSession()
}

override fun isLoggedIn(): Flow<Boolean> = natPreferencesDataSource.isLoggedIn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,19 @@ class NativeAppTemplatePreferencesDataSource @Inject constructor(
}
}

suspend fun clearUserPreferences() {
updatePreferences("Failed to clear user preferences") {
it.toBuilder().clear().build()
/**
* Resets everything to its default except the device settings, which belong to whoever uses this
* device rather than to the signed-in shopkeeper: the dark theme and the "tap shop below" tip
* (an iOS TipKit tip, also per device). Everything else is the shopkeeper's (session, account,
* permissions and limits, locale) or a one-shot flag about their session, and must not reach
* whoever signs in next.
*/
suspend fun clearSession() {
updatePreferences("Failed to clear the session") {
UserPreferences.newBuilder()
.setDarkThemeConfig(it.darkThemeConfig)
.setDidShowTapShopBelowTip(it.didShowTapShopBelowTip)
.build()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class SignInEmailAndPasswordViewModel @Inject constructor(
.catchShowingError(_uiState)
.collect { loggedInShopkeeper ->
_uiState.update { it.copy(loggedInShopkeeper = loggedInShopkeeper) }
loginRepository.clearUserPreferences()
loginRepository.clearSession()
loginRepository.setShopkeeper(loggedInShopkeeper)
// The account may have been created, or last used, in another language.
viewModelScope.launch { appLanguageSyncer.sync() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class ShopkeeperEditViewModel @Inject constructor(
isDeleted = true,
)
}
loginRepository.clearUserPreferences()
loginRepository.clearSession()
loginRepository.setIsMyAccountDeleted(true)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ class RepositoryRequestsTest {
loginRepository.logout().first()

assertEquals("DELETE /shopkeeper_auth/sign_out", received.single().let { "${it.method} ${it.pathAndQuery}" })
// clearUserPreferences resets UserPreferences to its defaults.
// clearSession forgets the session (and keeps only the device settings).
val userData = dataSource.userData.first()
assertFalse(userData.isLoggedIn)
assertEquals("", userData.token)
Expand Down Expand Up @@ -378,7 +378,7 @@ class RepositoryRequestsTest {

@Test
fun aSignedOutRequest_carriesNoTokenAndNoAccountSegment() = runBlocking {
dataSource.clearUserPreferences()
dataSource.clearSession()
respond(200, """{"data":[]}""")

shopRepository.getShops().first()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.nativeapptemplate.nativeapptemplatefree.common.errors.ApiException
import com.nativeapptemplate.nativeapptemplatefree.datastore.NativeAppTemplatePreferencesDataSource
import com.nativeapptemplate.nativeapptemplatefree.datastoreTest.InMemoryDataStore
import com.nativeapptemplate.nativeapptemplatefree.model.Attributes
import com.nativeapptemplate.nativeapptemplatefree.model.DarkThemeConfig
import com.nativeapptemplate.nativeapptemplatefree.model.Data
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
import com.nativeapptemplate.nativeapptemplatefree.model.Login
Expand All @@ -23,6 +24,10 @@ import kotlin.test.assertFailsWith

class LoginRepositoryImplTest {

private val preferences = NativeAppTemplatePreferencesDataSource(
InMemoryDataStore(UserPreferences.getDefaultInstance()),
)

private fun repository(
loginResponse: LoggedInShopkeeper = sessionWith(),
logoutResponse: ApiResponse<Status> = ApiResponse.Success(Status()),
Expand All @@ -34,9 +39,7 @@ class LoginRepositoryImplTest {
override suspend fun updateConfirmedPrivacyVersion(accountId: String): ApiResponse<Status> = error("not used")
override suspend fun updateConfirmedTermsVersion(accountId: String): ApiResponse<Status> = error("not used")
},
natPreferencesDataSource = NativeAppTemplatePreferencesDataSource(
InMemoryDataStore(UserPreferences.getDefaultInstance()),
),
natPreferencesDataSource = preferences,
ioDispatcher = UnconfinedTestDispatcher(),
)

Expand All @@ -63,6 +66,18 @@ class LoginRepositoryImplTest {
}
}

@Test
fun logout_keepsTheDarkTheme() = runTest {
// A device setting: the next shopkeeper on this device keeps the theme chosen here.
preferences.setDarkThemeConfig(DarkThemeConfig.DARK)
preferences.setShopkeeper(sessionWith())

repository().logout().first()

assertEquals(false, preferences.userData.first().isLoggedIn)
assertEquals(DarkThemeConfig.DARK, preferences.userData.first().darkThemeConfig)
}

@Test
fun logout_whenTheServerRejectsIt_reportsTheServerMessage() = runTest {
val response = ApiResponse.Failure.Error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.nativeapptemplate.nativeapptemplatefree.di.modules.networkJson
import com.nativeapptemplate.nativeapptemplatefree.model.Attributes
import com.nativeapptemplate.nativeapptemplatefree.model.DarkThemeConfig
import com.nativeapptemplate.nativeapptemplatefree.model.Data
import com.nativeapptemplate.nativeapptemplatefree.model.Locales
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
import com.nativeapptemplate.nativeapptemplatefree.model.Meta
import com.nativeapptemplate.nativeapptemplatefree.model.Permissions
Expand Down Expand Up @@ -202,29 +203,57 @@ class NativeAppTemplatePreferencesDataSourceTest {
}
}

// Signing out (LoginRepositoryImpl.logout) clears the whole UserPreferences: the session, and also
// the device-level settings stored next to it (dark mode, the shop tip). Current behavior.
@Test
fun clearUserPreferences_resetsEverythingToTheDefaults() = testScope.runTest {
subject.setShopkeeper(testInputLoggedInShopkeeper)
fun clearSession_forgetsEverythingAboutTheShopkeeper() = testScope.runTest {
// Whoever signs in next on this device must see nothing of this shopkeeper.
subject.setShopkeeper(LoggedInShopkeeper(datum = testInputLoggedInShopkeeperData.copy(attributes = testInputLoggedInShopkeeperData.attributes!!.copy(locale = "ja"))))
subject.setPermissions(Permissions(meta = Meta(androidAppVersion = BuildConfig.VERSION_CODE + 1, shouldUpdatePrivacy = true, shouldUpdateTerms = true, shopLimitCount = 99)))
subject.setDarkThemeConfig(DarkThemeConfig.DARK)
subject.setDidShowTapShopBelowTip(true)
subject.setIsEmailUpdated(true)
subject.setIsMyAccountDeleted(true)
subject.setIsShopDeleted(true)

subject.clearUserPreferences()
subject.clearSession()

// Expected: the proto3 defaults ("" / 0 / false), as on a fresh install.
val userData = subject.userData.first()
assertFalse(userData.isLoggedIn)
assertEquals("", userData.token)
assertEquals("", userData.id)
assertEquals("", userData.accountId)
assertEquals("", userData.personalAccountId)
assertEquals("", userData.accountOwnerId)
assertEquals("", userData.accountName)
assertEquals("", userData.email)
assertEquals("", userData.name)
assertEquals("", userData.timeZone)
// An empty stored locale reads as Locales.DEFAULT.
assertEquals(Locales.DEFAULT, userData.locale)
assertEquals("", userData.token)
assertEquals("", userData.client)
assertEquals("", userData.uid)
assertEquals("", userData.expiry)
assertEquals(0, userData.androidAppVersion)
assertEquals(0, userData.shopLimitCount)
assertFalse(userData.shouldUpdatePrivacy)
assertFalse(userData.shouldUpdateTerms)
assertFalse(subject.shouldUpdateApp().first())
assertFalse(subject.shouldUpdatePrivacy().first())
assertEquals(DarkThemeConfig.FOLLOW_SYSTEM, userData.darkThemeConfig)
assertFalse(subject.didShowTapShopBelowTip().first())
assertFalse(userData.isEmailUpdated)
assertFalse(userData.isMyAccountDeleted)
assertFalse(subject.isShopDeleted().first())
}

@Test
fun clearSession_keepsTheDeviceSettings() = testScope.runTest {
// Chosen on this device, not for the account: signing in or out must not reset them.
subject.setDarkThemeConfig(DarkThemeConfig.DARK)
subject.setDidShowTapShopBelowTip(true)
subject.setShopkeeper(testInputLoggedInShopkeeper)

subject.clearSession()

assertEquals(DarkThemeConfig.DARK, subject.userData.first().darkThemeConfig)
assertTrue(subject.didShowTapShopBelowTip().first())
}

@Test
fun flags_readBackWhatWasSet() = testScope.runTest {
subject.setIsEmailUpdated(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,6 @@ class NativeAppTemplatePreferencesDataSourceWriteFailureTest {

@Test
fun clearing_whenTheWriteFails_isLoggedAndRethrown() = runTest {
assertLoggedAndRethrown("Failed to clear user preferences") { subject.clearUserPreferences() }
assertLoggedAndRethrown("Failed to clear the session") { subject.clearSession() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class DemoLoginRepository @Inject constructor(
override suspend fun setIsShopDeleted(isShopDeleted: Boolean) {
}

override suspend fun clearUserPreferences() {
override suspend fun clearSession() {
}

override fun isLoggedIn(): Flow<Boolean> = MutableStateFlow(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ class TestLoginRepository : LoginRepository {
override suspend fun setIsShopDeleted(isShopDeleted: Boolean) {
}

/** How many times [clearUserPreferences] was called. */
var clearUserPreferencesCallCount = 0
/** How many times [clearSession] was called. */
var clearSessionCallCount = 0
private set

override suspend fun clearUserPreferences() {
clearUserPreferencesCallCount++
override suspend fun clearSession() {
clearSessionCallCount++
}

override fun isLoggedIn(): Flow<Boolean> = isLoggedInReturnFlow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class NativeAppTemplateAppStateTest {
dataSource.setPermissions(Permissions(meta = Meta(androidAppVersion = BuildConfig.VERSION_CODE + 1)))
assertEquals(AuthDestination.NEED_APP_UPDATES, appState.authDestination.value)

dataSource.clearUserPreferences()
dataSource.clearSession()
assertEquals(AuthDestination.ONBOARDING, appState.authDestination.value)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class SignInEmailAndPasswordViewModelTest {
// ApiException.UnprocessableError = "[NATIVEAPPTEMPLATE-2002] Processing error: $rawMessage".
assertEquals("[NATIVEAPPTEMPLATE-2002] Processing error: Invalid login credentials. Please try again.", uiStateValue.message)
// The previous session (if any) is left alone, and a rejected sign-in is not a sign-out.
assertEquals(0, loginRepository.clearUserPreferencesCallCount)
assertEquals(0, loginRepository.clearSessionCallCount)
assertEquals(0, loginRepository.logoutCallCount)
}

Expand All @@ -148,7 +148,7 @@ class SignInEmailAndPasswordViewModelTest {

viewModel.login()

assertEquals(1, loginRepository.clearUserPreferencesCallCount)
assertEquals(1, loginRepository.clearSessionCallCount)
val userData = loginRepository.userData.first()
assertTrue(userData.isLoggedIn)
assertEquals(LOGGED_IN_SHOPKEEPER_ACCOUNT_ID, userData.accountId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class ShopkeeperEditViewModelTest {
viewModel.deleteShopkeeper()

// The account still exists on the server, so the session must survive.
assertEquals(0, loginRepository.clearUserPreferencesCallCount)
assertEquals(0, loginRepository.clearSessionCallCount)
assertFalse(viewModel.uiState.value.isLoading)
assertFalse(viewModel.uiState.value.isDeleted)
// ApiException.UnprocessableError = "[NATIVEAPPTEMPLATE-2002] Processing error: $rawMessage".
Expand Down
Loading