1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

Refactored select-account-use-case + refactored tests. (#4)

* Refactored select-account-use-case + refactored tests.

* Added launch-account-use-case, also added new implementation for defining current user account

* Navigation fixes.

* Refactored event flow.

* Fixed tests
This commit is contained in:
ubu 2019-10-31 15:26:39 +03:00 committed by GitHub
parent f542e2b7e4
commit 996d9135bb
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 419 additions and 170 deletions

View file

@ -200,11 +200,11 @@ class SetupSelectedAccountModule {
@Provides
@PerScreen
fun provideSetupSelectedAccountViewModelFactory(
selectAccount: SelectAccount,
startAccount: StartAccount,
pathProvider: PathProvider
): SetupSelectedAccountViewModelFactory {
return SetupSelectedAccountViewModelFactory(
selectAccount = selectAccount,
startAccount = startAccount,
pathProvider = pathProvider
)
}
@ -213,8 +213,8 @@ class SetupSelectedAccountModule {
@PerScreen
fun provideSelectAccountUseCase(
repository: AuthRepository
): SelectAccount {
return SelectAccount(
): StartAccount {
return StartAccount(
repository = repository
)
}

View file

@ -2,7 +2,7 @@ package com.agileburo.anytype.di.feature
import com.agileburo.anytype.core_utils.di.scope.PerScreen
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
import com.agileburo.anytype.domain.desktop.interactor.GetCurrentAccount
import com.agileburo.anytype.domain.image.ImageLoader
import com.agileburo.anytype.domain.image.LoadImage
import com.agileburo.anytype.presentation.desktop.DesktopViewModelFactory
@ -33,10 +33,10 @@ class DesktopModule {
@Provides
@PerScreen
fun provideDesktopViewModelFactory(
getAccount: GetAccount,
getCurrentAccount: GetCurrentAccount,
loadImage: LoadImage
): DesktopViewModelFactory = DesktopViewModelFactory(
getAccount = getAccount,
getCurrentAccount = getCurrentAccount,
loadImage = loadImage
)
@ -44,7 +44,7 @@ class DesktopModule {
@PerScreen
fun provideGetAccountUseCase(
repository: AuthRepository
): GetAccount = GetAccount(
): GetCurrentAccount = GetCurrentAccount(
repository = repository
)

View file

@ -3,7 +3,7 @@ package com.agileburo.anytype.di.feature
import com.agileburo.anytype.core_utils.di.scope.PerScreen
import com.agileburo.anytype.domain.auth.interactor.Logout
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
import com.agileburo.anytype.domain.desktop.interactor.GetCurrentAccount
import com.agileburo.anytype.domain.image.ImageLoader
import com.agileburo.anytype.domain.image.LoadImage
import com.agileburo.anytype.presentation.profile.ProfileViewModelFactory
@ -36,11 +36,11 @@ class ProfileModule {
fun provideProfileViewModelFactory(
logout: Logout,
loadImage: LoadImage,
getAccount: GetAccount
getCurrentAccount: GetCurrentAccount
): ProfileViewModelFactory = ProfileViewModelFactory(
logout = logout,
loadImage = loadImage,
getAccount = getAccount
getCurrentAccount = getCurrentAccount
)
@Provides
@ -61,7 +61,7 @@ class ProfileModule {
@PerScreen
fun provideGetAccountUseCase(
authRepository: AuthRepository
): GetAccount = GetAccount(
): GetCurrentAccount = GetCurrentAccount(
repository = authRepository
)
}

View file

@ -3,6 +3,8 @@ package com.agileburo.anytype.di.feature
import com.agileburo.anytype.core_utils.di.scope.PerScreen
import com.agileburo.anytype.domain.auth.interactor.CheckAuthorizationStatus
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.auth.repo.PathProvider
import com.agileburo.anytype.domain.launch.LaunchAccount
import com.agileburo.anytype.presentation.splash.SplashViewModelFactory
import com.agileburo.anytype.ui.splash.SplashFragment
import dagger.Module
@ -33,11 +35,30 @@ class SplashModule {
@PerScreen
@Provides
fun provideSplashViewModelFactory(checkAuthorizationStatus: CheckAuthorizationStatus) =
SplashViewModelFactory(checkAuthorizationStatus)
fun provideSplashViewModelFactory(
checkAuthorizationStatus: CheckAuthorizationStatus,
launchAccount: LaunchAccount
): SplashViewModelFactory = SplashViewModelFactory(
checkAuthorizationStatus = checkAuthorizationStatus,
launchAccount = launchAccount
)
@PerScreen
@Provides
fun provideCheckAuthorizationStatus(authRepository: AuthRepository) =
CheckAuthorizationStatus(authRepository)
fun provideCheckAuthorizationStatusUseCase(
authRepository: AuthRepository
): CheckAuthorizationStatus = CheckAuthorizationStatus(
repository = authRepository
)
@PerScreen
@Provides
fun provideLaunchAccountUseCase(
authRepository: AuthRepository,
pathProvider: PathProvider
): LaunchAccount = LaunchAccount(
repository = authRepository,
pathProvider = pathProvider
)
}

View file

@ -27,7 +27,7 @@ class Navigator : AppNavigation {
}
override fun createProfile() {
navController?.navigate(R.id.action_open_sign_up)
navController?.navigate(R.id.action_create_profile)
}
override fun setupNewAccount() {
@ -42,7 +42,7 @@ class Navigator : AppNavigation {
navController?.navigate(R.id.action_open_congratulation_screen)
}
override fun chooseProfile() {
override fun chooseAccount() {
navController?.navigate(R.id.action_select_account)
}

View file

@ -25,11 +25,11 @@ abstract class NavigationFragment(
is Command.StartDesktopFromLogin -> navigation.startDesktopFromLogin()
is Command.StartDesktopFromSplash -> navigation.startDesktopFromSplash()
is Command.OpenStartLoginScreen -> navigation.startLogin()
is Command.OpenCreateProfile -> navigation.createProfile()
is Command.OpenCreateAccount -> navigation.createProfile()
is Command.ChoosePinCodeScreen -> navigation.choosePinCode()
is Command.CongratulationScreen -> navigation.congratulation()
is Command.EnterKeyChainScreen -> navigation.enterKeychain()
is Command.ChooseProfileScreen -> navigation.chooseProfile()
is Command.ChooseAccountScreen -> navigation.chooseAccount()
is Command.WorkspaceScreen -> navigation.workspace()
is Command.SetupNewAccountScreen -> navigation.setupNewAccount()
is Command.SetupSelectedAccountScreen -> navigation.setupSelectedAccount(command.id)

View file

@ -63,6 +63,7 @@ class ProfileFragment : ViewStateFragment<ViewState<ProfileView>>(R.layout.fragm
pinCodeText.setOnClickListener { vm.onPinCodeClicked() }
keychainPhrase.setOnClickListener { vm.onKeyChainPhraseClicked() }
backButton.setOnClickListener { vm.onBackButtonClicked() }
switchProfileButton.setOnClickListener { vm.onAddProfileClicked() }
}
is ViewState.Success -> {
name.text = state.data.name

View file

@ -11,11 +11,11 @@
android:label="DesktopFragment"
tools:layout="@layout/fragment_desktop">
<action
android:id="@+id/action_open_profile"
app:destination="@id/profileScreen"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
android:id="@+id/action_open_profile"
app:destination="@id/profileScreen"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
@ -29,6 +29,41 @@
app:destination="@+id/main_navigation"
app:popUpTo="@+id/main_navigation"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_create_profile"
app:destination="@id/createAccountScreen"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/createAccountScreen"
android:name="com.agileburo.anytype.ui.auth.account.CreateAccountFragment"
android:label="StartLoginFragment"
tools:layout="@layout/fragment_create_account">
<action
android:id="@+id/action_setup_new_account"
app:destination="@id/setupNewAccountScreen"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:popUpTo="@+id/startLoginScreen"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/setupNewAccountScreen"
android:name="com.agileburo.anytype.ui.auth.account.SetupNewAccountFragment"
android:label="SetupAccount"
tools:layout="@layout/fragment_setup_new_account">
<action
android:id="@+id/action_open_congratulation_screen"
app:destination="@id/congratulationScreen"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popUpTo="@+id/startLoginScreen"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/splashScreen"
@ -54,6 +89,11 @@
app:popUpTo="@+id/splashScreen"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/congratulationScreen"
android:name="com.agileburo.anytype.ui.auth.CongratulationFragment"
android:label="StartLoginFragment"
tools:layout="@layout/fragment_congratulation" />
<navigation
android:id="@+id/login_nav"
app:startDestination="@id/startLoginScreen">
@ -157,7 +197,7 @@
app:popExitAnim="@anim/slide_out_right"
app:popUpToInclusive="false" />
<action
android:id="@+id/action_open_sign_up"
android:id="@+id/action_create_profile"
app:destination="@id/createAccountScreen"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"

View file

@ -3,10 +3,17 @@ package com.agileburo.anytype.data.auth.repo
import com.agileburo.anytype.data.auth.model.AccountEntity
interface AuthCache {
suspend fun saveAccount(account: AccountEntity)
suspend fun updateAccount(account: AccountEntity)
suspend fun saveMnemonic(mnemonic: String)
suspend fun getMnemonic(): String
suspend fun getAccount(): AccountEntity
suspend fun getCurrentAccount(): AccountEntity
suspend fun getCurrentAccountId(): String
suspend fun logout()
suspend fun getAccounts(): List<AccountEntity>
suspend fun setCurrentAccount(id: String)
}

View file

@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow
class AuthCacheDataStore(private val cache: AuthCache) : AuthDataStore {
override suspend fun selectAccount(id: String, path: String): AccountEntity {
override suspend fun startAccount(id: String, path: String): AccountEntity {
throw UnsupportedOperationException()
}
@ -22,6 +22,10 @@ class AuthCacheDataStore(private val cache: AuthCache) : AuthDataStore {
cache.saveAccount(account)
}
override suspend fun updateAccount(account: AccountEntity) {
cache.updateAccount(account)
}
override fun observeAccounts(): Flow<AccountEntity> {
throw UnsupportedOperationException()
}
@ -44,8 +48,13 @@ class AuthCacheDataStore(private val cache: AuthCache) : AuthDataStore {
cache.logout()
}
override suspend fun getStoredAccounts(): List<AccountEntity> =
cache.getAccounts()
override suspend fun getAccounts() = cache.getAccounts()
override suspend fun getAccount() = cache.getAccount()
override suspend fun getCurrentAccount() = cache.getCurrentAccount()
override suspend fun getCurrentAccountId() = cache.getCurrentAccountId()
override suspend fun setCurrentAccount(id: String) {
cache.setCurrentAccount(id)
}
}

View file

@ -11,16 +11,16 @@ class AuthDataRepository(
private val factory: AuthDataStoreFactory
) : AuthRepository {
override suspend fun selectAccount(
override suspend fun startAccount(
id: String, path: String
): Account = factory.remote.selectAccount(id, path).toDomain()
): Account = factory.remote.startAccount(id, path).toDomain()
override suspend fun createAccount(
name: String,
avatarPath: String?
): Account = factory.remote.createAccount(name, avatarPath).toDomain()
override suspend fun recoverAccount() {
override suspend fun startLoadingAccounts() {
factory.remote.recoverAccount()
}
@ -28,6 +28,10 @@ class AuthDataRepository(
factory.cache.saveAccount(account.toEntity())
}
override suspend fun updateAccount(account: Account) {
factory.cache.updateAccount(account.toEntity())
}
override fun observeAccounts() = factory.remote.observeAccounts().map { it.toDomain() }
override suspend fun createWallet(
@ -38,7 +42,9 @@ class AuthDataRepository(
factory.remote.recoverWallet(path, mnemonic)
}
override suspend fun getAccount() = factory.cache.getAccount().toDomain()
override suspend fun getCurrentAccount() = factory.cache.getCurrentAccount().toDomain()
override suspend fun getCurrentAccountId() = factory.cache.getCurrentAccountId()
override suspend fun saveMnemonic(
mnemonic: String
@ -50,6 +56,9 @@ class AuthDataRepository(
factory.cache.logout()
}
override suspend fun getAvailableAccounts(): List<Account> =
factory.cache.getStoredAccounts().map { it.toDomain() }
override suspend fun getAccounts() = factory.cache.getAccounts().map { it.toDomain() }
override suspend fun setCurrentAccount(id: String) {
factory.cache.setCurrentAccount(id)
}
}

View file

@ -5,13 +5,21 @@ import com.agileburo.anytype.data.auth.model.WalletEntity
import kotlinx.coroutines.flow.Flow
interface AuthDataStore {
suspend fun selectAccount(id: String, path: String): AccountEntity
suspend fun startAccount(id: String, path: String): AccountEntity
suspend fun createAccount(name: String, avatarPath: String?): AccountEntity
suspend fun recoverAccount()
suspend fun saveAccount(account: AccountEntity)
suspend fun updateAccount(account: AccountEntity)
fun observeAccounts(): Flow<AccountEntity>
suspend fun getAccount(): AccountEntity
suspend fun getCurrentAccount(): AccountEntity
suspend fun getCurrentAccountId(): String
suspend fun createWallet(path: String): WalletEntity
suspend fun recoverWallet(path: String, mnemonic: String)
@ -19,5 +27,6 @@ interface AuthDataStore {
suspend fun getMnemonic(): String
suspend fun logout()
suspend fun getStoredAccounts(): List<AccountEntity>
suspend fun getAccounts(): List<AccountEntity>
suspend fun setCurrentAccount(id: String)
}

View file

@ -5,7 +5,7 @@ import com.agileburo.anytype.data.auth.model.WalletEntity
import kotlinx.coroutines.flow.Flow
interface AuthRemote {
suspend fun selectAccount(id: String, path: String): AccountEntity
suspend fun startAccount(id: String, path: String): AccountEntity
suspend fun createAccount(name: String, avatarPath: String?): AccountEntity
suspend fun recoverAccount()
fun observeAccounts(): Flow<AccountEntity>

View file

@ -7,9 +7,9 @@ class AuthRemoteDataStore(
private val authRemote: AuthRemote
) : AuthDataStore {
override suspend fun selectAccount(
override suspend fun startAccount(
id: String, path: String
) = authRemote.selectAccount(id, path)
) = authRemote.startAccount(id, path)
override suspend fun createAccount(
name: String,
@ -46,11 +46,23 @@ class AuthRemoteDataStore(
throw UnsupportedOperationException()
}
override suspend fun getStoredAccounts(): List<AccountEntity> {
override suspend fun getAccounts(): List<AccountEntity> {
throw UnsupportedOperationException()
}
override suspend fun getAccount(): AccountEntity {
override suspend fun getCurrentAccount(): AccountEntity {
throw UnsupportedOperationException()
}
override suspend fun setCurrentAccount(id: String) {
throw UnsupportedOperationException()
}
override suspend fun getCurrentAccountId(): String {
throw UnsupportedOperationException()
}
override suspend fun updateAccount(account: AccountEntity) {
throw UnsupportedOperationException()
}
}

View file

@ -50,17 +50,17 @@ class AuthDataRepositoryTest {
)
authRemote.stub {
onBlocking { selectAccount(id = id, path = path) } doReturn account
onBlocking { startAccount(id = id, path = path) } doReturn account
}
repo.selectAccount(
repo.startAccount(
id = id,
path = path
)
verifyZeroInteractions(authCache)
verify(authRemote, times(1)).selectAccount(
verify(authRemote, times(1)).startAccount(
id = id,
path = path
)
@ -107,7 +107,7 @@ class AuthDataRepositoryTest {
onBlocking { recoverAccount() } doReturn Unit
}
repo.recoverAccount()
repo.startLoadingAccounts()
verifyZeroInteractions(authCache)
verify(authRemote, times(1)).recoverAccount()
@ -164,13 +164,13 @@ class AuthDataRepositoryTest {
)
authCache.stub {
onBlocking { getAccount() } doReturn account
onBlocking { getCurrentAccount() } doReturn account
}
repo.getAccount()
repo.getCurrentAccount()
verifyZeroInteractions(authRemote)
verify(authCache, times(1)).getAccount()
verify(authCache, times(1)).getCurrentAccount()
verifyNoMoreInteractions(authCache)
}
@ -235,7 +235,7 @@ class AuthDataRepositoryTest {
onBlocking { getAccounts() } doReturn accounts
}
repo.getAvailableAccounts()
repo.getAccounts()
verifyZeroInteractions(authRemote)
verify(authCache, times(1)).getAccounts()

View file

@ -15,7 +15,7 @@ class CheckAuthorizationStatus(
) : BaseUseCase<AuthStatus, Unit>() {
override suspend fun run(params: Unit) = try {
repository.getAvailableAccounts().let { accounts ->
repository.getAccounts().let { accounts ->
if (accounts.isNotEmpty())
Either.Right(AuthStatus.AUTHORIZED)
else

View file

@ -5,7 +5,7 @@ import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.Either
/**
* Creates an account, then stores it.
* Creates an account, then stores it and sets as current user account.
*/
open class CreateAccount(
private val repository: AuthRepository
@ -16,7 +16,10 @@ open class CreateAccount(
name = params.name,
avatarPath = params.avatarPath
).let { account ->
repository.saveAccount(account)
with(repository) {
saveAccount(account)
setCurrentAccount(account.id)
}
}.let {
Either.Right(it)
}

View file

@ -7,16 +7,19 @@ import com.agileburo.anytype.domain.base.Either
/**
* Use case for selecting user account.
*/
class SelectAccount(
class StartAccount(
private val repository: AuthRepository
) : BaseUseCase<Unit, SelectAccount.Params>() {
) : BaseUseCase<Unit, StartAccount.Params>() {
override suspend fun run(params: Params) = try {
repository.selectAccount(
repository.startAccount(
id = params.id,
path = params.path
).let { account ->
repository.saveAccount(account)
with(repository) {
saveAccount(account)
setCurrentAccount(account.id)
}
}.let {
Either.Right(it)
}

View file

@ -12,7 +12,7 @@ class StartLoadingAccounts(
) : BaseUseCase<Unit, StartLoadingAccounts.Params>() {
override suspend fun run(params: Params) = try {
repository.recoverAccount().let {
repository.startLoadingAccounts().let {
Either.Right(it)
}
} catch (e: Throwable) {

View file

@ -5,20 +5,43 @@ import com.agileburo.anytype.domain.auth.model.Wallet
import kotlinx.coroutines.flow.Flow
interface AuthRepository {
suspend fun selectAccount(id: String, path: String): Account
/**
* Launches an account.
* @param id user account id
* @param path wallet repository path
*/
suspend fun startAccount(id: String, path: String): Account
suspend fun createAccount(name: String, avatarPath: String?): Account
suspend fun recoverAccount()
suspend fun startLoadingAccounts()
suspend fun saveAccount(account: Account)
suspend fun updateAccount(account: Account)
fun observeAccounts(): Flow<Account>
suspend fun getAccount(): Account
suspend fun getCurrentAccount(): Account
suspend fun getCurrentAccountId(): String
suspend fun createWallet(path: String): Wallet
suspend fun recoverWallet(path: String, mnemonic: String)
suspend fun saveMnemonic(mnemonic: String)
suspend fun getMnemonic(): String
suspend fun logout()
suspend fun getAvailableAccounts(): List<Account>
suspend fun getAccounts(): List<Account>
/**
* Sets currently selected user account
* @param id account's id
*/
suspend fun setCurrentAccount(id: String)
}

View file

@ -8,12 +8,12 @@ import com.agileburo.anytype.domain.base.Either
/** Use case for getting currently selected user account.
* @property repository repository containing user account
*/
class GetAccount(
class GetCurrentAccount(
private val repository: AuthRepository
) : BaseUseCase<Account, BaseUseCase.None>() {
override suspend fun run(params: None) = try {
repository.getAccount().let {
repository.getCurrentAccount().let {
Either.Right(it)
}
} catch (t: Throwable) {

View file

@ -0,0 +1,25 @@
package com.agileburo.anytype.domain.launch
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.auth.repo.PathProvider
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.Either
class LaunchAccount(
private val repository: AuthRepository,
private val pathProvider: PathProvider
) : BaseUseCase<Unit, BaseUseCase.None>() {
override suspend fun run(params: None) = try {
repository.startAccount(
id = repository.getCurrentAccountId(),
path = pathProvider.providePath()
).let { account ->
repository.updateAccount(account)
}.let {
Either.Right(it)
}
} catch (e: Throwable) {
Either.Left(e)
}
}

View file

@ -38,14 +38,14 @@ class CheckAuthorizationStatusTest {
fun `should return unauthorized status if account list is empty`() = runBlocking {
repo.stub {
onBlocking { getAvailableAccounts() } doReturn emptyList()
onBlocking { getAccounts() } doReturn emptyList()
}
val result = checkAuthorizationStatus.run(params = Unit)
assertTrue { result == Either.Right(AuthStatus.UNAUTHORIZED) }
verify(repo, times(1)).getAvailableAccounts()
verify(repo, times(1)).getAccounts()
verifyNoMoreInteractions(repo)
}
@ -59,14 +59,14 @@ class CheckAuthorizationStatusTest {
)
repo.stub {
onBlocking { getAvailableAccounts() } doReturn listOf(account)
onBlocking { getAccounts() } doReturn listOf(account)
}
val result = checkAuthorizationStatus.run(params = Unit)
assertTrue { result == Either.Right(AuthStatus.AUTHORIZED) }
verify(repo, times(1)).getAvailableAccounts()
verify(repo, times(1)).getAccounts()
verifyNoMoreInteractions(repo)
}

View file

@ -32,7 +32,7 @@ class CreateAccountTest {
}
@Test
fun `should create account and save it by calling repository method`() = runBlocking {
fun `should create account and save it and set as current user account`() = runBlocking {
val name = DataFactory.randomString()
@ -56,7 +56,8 @@ class CreateAccountTest {
createAccount.run(param)
verify(repo, times(1)).createAccount(name, path)
verify(repo, times(1)).saveAccount(any())
verify(repo, times(1)).saveAccount(account)
verify(repo, times(1)).setCurrentAccount(account.id)
verifyNoMoreInteractions(repo)
}
}

View file

@ -1,6 +1,6 @@
package com.agileburo.anytype.domain.auth
import com.agileburo.anytype.domain.auth.interactor.SelectAccount
import com.agileburo.anytype.domain.auth.interactor.StartAccount
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.auth.repo.AuthRepository
import com.agileburo.anytype.domain.base.Either
@ -16,7 +16,7 @@ import org.mockito.Mock
import org.mockito.MockitoAnnotations
import kotlin.test.assertTrue
class SelectAccountTest {
class StartAccountTest {
@ExperimentalCoroutinesApi
@get:Rule
@ -25,21 +25,21 @@ class SelectAccountTest {
@Mock
lateinit var repo: AuthRepository
lateinit var selectAccount: SelectAccount
lateinit var startAccount: StartAccount
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
selectAccount = SelectAccount(repo)
startAccount = StartAccount(repo)
}
@Test
fun `should select account and save it`() = runBlocking {
fun `should select account, set it as current user account and save it`() = runBlocking {
val id = DataFactory.randomString()
val path = DataFactory.randomString()
val params = SelectAccount.Params(
val params = StartAccount.Params(
id = id,
path = path
)
@ -52,22 +52,24 @@ class SelectAccountTest {
repo.stub {
onBlocking {
selectAccount(
startAccount(
id = id,
path = path
)
} doReturn account
}
selectAccount.run(params)
startAccount.run(params)
verify(repo, times(1)).selectAccount(
verify(repo, times(1)).startAccount(
id = id,
path = path
)
verify(repo, times(1)).saveAccount(account)
verify(repo, times(1)).setCurrentAccount(account.id)
verifyNoMoreInteractions(repo)
}
@ -77,7 +79,7 @@ class SelectAccountTest {
val id = DataFactory.randomString()
val path = DataFactory.randomString()
val params = SelectAccount.Params(
val params = StartAccount.Params(
id = id,
path = path
)
@ -90,14 +92,14 @@ class SelectAccountTest {
repo.stub {
onBlocking {
selectAccount(
startAccount(
id = id,
path = path
)
} doReturn account
}
val result = selectAccount.run(params)
val result = startAccount.run(params)
assertTrue { result == Either.Right(Unit) }
}

View file

@ -1,7 +1,8 @@
package com.agileburo.anytype.middleware
import anytype.Events
import kotlinx.coroutines.flow.Flow
interface EventProxy {
fun flow(): Flow<Event>
fun flow(): Flow<Events.Event>
}

View file

@ -1,9 +1,9 @@
package com.agileburo.anytype.middleware.auth
import anytype.Events
import com.agileburo.anytype.data.auth.model.AccountEntity
import com.agileburo.anytype.data.auth.model.WalletEntity
import com.agileburo.anytype.data.auth.repo.AuthRemote
import com.agileburo.anytype.middleware.Event
import com.agileburo.anytype.middleware.EventProxy
import com.agileburo.anytype.middleware.interactor.Middleware
import com.agileburo.anytype.middleware.toEntity
@ -17,13 +17,13 @@ class AuthMiddleware(
private val events: EventProxy
) : AuthRemote {
override suspend fun selectAccount(
override suspend fun startAccount(
id: String, path: String
) = middleware.selectAccount(id, path).let { response ->
AccountEntity(
id = response.id,
name = response.name,
avatar = null
avatar = response.avatar?.toEntity()
)
}
@ -47,13 +47,13 @@ class AuthMiddleware(
override fun observeAccounts() = events
.flow()
.filter { event -> event is Event.AccountAdd }
.map { event -> event as Event.AccountAdd }
.filter { event -> event.messageCase == Events.Event.MessageCase.ACCOUNTADD }
.map { event ->
AccountEntity(
id = event.id,
name = event.name,
avatar = null
id = event.accountAdd.account.id,
name = event.accountAdd.account.name,
avatar = event.accountAdd.account.avatar.toEntity()
)
}

View file

@ -1,14 +1,11 @@
package com.agileburo.anytype.middleware.interactor
import anytype.Events
import com.agileburo.anytype.middleware.Event
import com.agileburo.anytype.middleware.EventProxy
import com.google.protobuf.InvalidProtocolBufferException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import lib.Lib
import timber.log.Timber
@ -31,16 +28,7 @@ class Handler : EventProxy {
}
}
private fun events(): Flow<Event> = channel
.consumeAsFlow()
.onEach { Timber.d(it.toString()) }
.map { event ->
Event.AccountAdd(
id = event.accountAdd.account.id,
name = event.accountAdd.account.name,
index = event.accountAdd.index.toInt()
)
}
private fun events(): Flow<Events.Event> = channel.consumeAsFlow()
override fun flow(): Flow<Event> = events()
override fun flow(): Flow<Events.Event> = events()
}

View file

@ -136,7 +136,8 @@ public class Middleware {
} else {
return new SelectAccountResponse(
response.getAccount().getId(),
response.getAccount().getName()
response.getAccount().getName(),
response.getAccount().getAvatar()
);
}
}

View file

@ -1,6 +1,9 @@
package com.agileburo.anytype.middleware.model
import anytype.Models
class SelectAccountResponse(
val id: String,
val name: String
val name: String,
val avatar: Models.Image? = null
)

View file

@ -12,4 +12,6 @@ object Config {
const val QUERY_LAST_ACCOUNT =
"SELECT * FROM $ACCOUNT_TABLE_NAME ORDER BY timestamp DESC LIMIT 1"
const val QUERY_ACCOUNT_BY_ID = "SELECT * FROM $ACCOUNT_TABLE_NAME WHERE id = :id"
}

View file

@ -14,6 +14,9 @@ abstract class AccountDao : BaseDao<AccountTable> {
@Query(Config.QUERY_LAST_ACCOUNT)
abstract suspend fun lastAccount(): List<AccountTable>
@Query(Config.QUERY_ACCOUNT_BY_ID)
abstract suspend fun getAccount(id: String): AccountTable?
@Query(Config.GET_ACCOUNTS)
abstract suspend fun getAccounts(): List<AccountTable>
}

View file

@ -17,6 +17,20 @@ fun AccountTable.toEntity(): AccountEntity {
)
}
fun AccountEntity.toTable(): AccountTable {
return AccountTable(
id = id,
name = name,
timestamp = System.currentTimeMillis(),
avatar = avatar?.let { avatar ->
AccountTable.Avatar(
avatarId = avatar.id,
sizes = avatar.sizes.map { it.toTable() }
)
}
)
}
fun ImageEntity.Size.toTable(): AccountTable.Size = when (this) {
ImageEntity.Size.SMALL -> AccountTable.Size.SMALL
ImageEntity.Size.THUMB -> AccountTable.Size.THUMB

View file

@ -6,7 +6,6 @@ import com.agileburo.anytype.data.auth.repo.AuthCache
import com.agileburo.anytype.db.AnytypeDatabase
import com.agileburo.anytype.mapper.toEntity
import com.agileburo.anytype.mapper.toTable
import com.agileburo.anytype.model.AccountTable
import timber.log.Timber
class DefaultAuthCache(
@ -15,26 +14,21 @@ class DefaultAuthCache(
) : AuthCache {
override suspend fun saveAccount(account: AccountEntity) {
db.accountDao().insert(
AccountTable(
id = account.id,
name = account.name,
timestamp = System.currentTimeMillis(),
avatar = account.avatar?.let { avatar ->
AccountTable.Avatar(
avatarId = avatar.id,
sizes = avatar.sizes.map { it.toTable() }
)
}
)
)
db.accountDao().insert(account.toTable())
}
override suspend fun getAccount() = db.accountDao().lastAccount().let { list ->
if (list.isEmpty())
throw IllegalStateException("Could not found user account")
else
list.first().toEntity()
override suspend fun updateAccount(account: AccountEntity) {
db.accountDao().update(account.toTable())
}
override suspend fun getCurrentAccount() = getCurrentAccountId().let { id ->
db.accountDao().getAccount(id)?.toEntity()
?: throw IllegalStateException("Account with the following id not found: $id")
}
override suspend fun getCurrentAccountId(): String {
val id: String? = prefs.getString(CURRENT_ACCOUNT_ID_KEY, null)
return id ?: throw IllegalStateException("Current account not set")
}
override suspend fun saveMnemonic(mnemonic: String) {
@ -52,12 +46,15 @@ class DefaultAuthCache(
prefs.edit().putString(MNEMONIC_KEY, null).apply()
}
override suspend fun getAccounts(): List<AccountEntity> =
db.accountDao()
.getAccounts()
.map { it.toEntity() }
override suspend fun getAccounts() = db.accountDao().getAccounts().map { it.toEntity() }
override suspend fun setCurrentAccount(id: String) {
prefs.edit().putString(CURRENT_ACCOUNT_ID_KEY, id).apply()
}
companion object {
const val MNEMONIC_KEY = "mnemonic"
const val CURRENT_ACCOUNT_ID_KEY = "current_account"
}
}

View file

@ -13,6 +13,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@RunWith(RobolectricTestRunner::class)
@ -86,4 +87,24 @@ class AccountDaoTest {
assertTrue { result.size == 1 }
assertTrue { result.first() == account }
}
@Test
fun `should return expected account when queried using account id`() = runBlocking {
val account = AccountTable(
id = MockDataFactory.randomString(),
name = MockDataFactory.randomString(),
timestamp = System.currentTimeMillis(),
avatar = AccountTable.Avatar(
avatarId = MockDataFactory.randomString(),
sizes = listOf(AccountTable.Size.LARGE, AccountTable.Size.SMALL)
)
)
database.accountDao().insert(account)
val result = database.accountDao().getAccount(account.id)
assertEquals(account, result)
}
}

View file

@ -4,23 +4,23 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.agileburo.anytype.core_utils.common.Event
import com.agileburo.anytype.domain.auth.interactor.SelectAccount
import com.agileburo.anytype.domain.auth.interactor.StartAccount
import com.agileburo.anytype.domain.auth.repo.PathProvider
import com.agileburo.anytype.presentation.navigation.AppNavigation
import com.agileburo.anytype.presentation.navigation.SupportNavigation
import timber.log.Timber
class SetupSelectedAccountViewModel(
private val selectAccount: SelectAccount,
private val startAccount: StartAccount,
private val pathProvider: PathProvider
) : ViewModel(), SupportNavigation<Event<AppNavigation.Command>> {
override val navigation: MutableLiveData<Event<AppNavigation.Command>> = MutableLiveData()
fun selectAccount(id: String) {
selectAccount.invoke(
startAccount.invoke(
scope = viewModelScope,
params = SelectAccount.Params(
params = StartAccount.Params(
id = id,
path = pathProvider.providePath()
)

View file

@ -2,18 +2,18 @@ package com.agileburo.anytype.presentation.auth.account
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.domain.auth.interactor.SelectAccount
import com.agileburo.anytype.domain.auth.interactor.StartAccount
import com.agileburo.anytype.domain.auth.repo.PathProvider
class SetupSelectedAccountViewModelFactory(
private val selectAccount: SelectAccount,
private val startAccount: StartAccount,
private val pathProvider: PathProvider
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return SetupSelectedAccountViewModel(
selectAccount = selectAccount,
startAccount = startAccount,
pathProvider = pathProvider
) as T
}

View file

@ -60,7 +60,7 @@ class KeychainLoginViewModel(
) { result ->
result.either(
fnR = {
navigation.postValue(Event(AppNavigation.Command.ChooseProfileScreen))
navigation.postValue(Event(AppNavigation.Command.ChooseAccountScreen))
},
fnL = { Timber.e(it, "Error while saving mnemonic: $mnemonic") }
)

View file

@ -33,7 +33,7 @@ class StartLoginViewModel(
Timber.e(it, "Error while setting up wallet")
},
fnR = {
navigation.postValue(Event(AppNavigation.Command.OpenCreateProfile))
navigation.postValue(Event(AppNavigation.Command.OpenCreateAccount))
}
)
}

View file

@ -8,7 +8,7 @@ import com.agileburo.anytype.core_utils.ui.ViewState
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
import com.agileburo.anytype.domain.desktop.interactor.GetCurrentAccount
import com.agileburo.anytype.domain.image.LoadImage
import com.agileburo.anytype.presentation.navigation.AppNavigation
import com.agileburo.anytype.presentation.navigation.SupportNavigation
@ -17,7 +17,7 @@ import timber.log.Timber
class DesktopViewModel(
private val loadImage: LoadImage,
private val getAccount: GetAccount
private val getCurrentAccount: GetCurrentAccount
) : ViewStateViewModel<ViewState<List<DesktopView>>>(),
SupportNavigation<Event<AppNavigation.Command>> {
@ -33,7 +33,7 @@ class DesktopViewModel(
override val navigation = MutableLiveData<Event<AppNavigation.Command>>()
private fun proceedWithGettingAccount() {
getAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
getCurrentAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
result.either(
fnL = { e -> Timber.e(e, "Error while getting account") },
fnR = { account ->

View file

@ -2,18 +2,18 @@ package com.agileburo.anytype.presentation.desktop
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
import com.agileburo.anytype.domain.desktop.interactor.GetCurrentAccount
import com.agileburo.anytype.domain.image.LoadImage
class DesktopViewModelFactory(
private val getAccount: GetAccount,
private val getCurrentAccount: GetCurrentAccount,
private val loadImage: LoadImage
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return DesktopViewModel(
getAccount = getAccount,
getCurrentAccount = getCurrentAccount,
loadImage = loadImage
) as T
}

View file

@ -1,6 +1,7 @@
package com.agileburo.anytype.presentation.navigation
interface AppNavigation {
fun startLogin()
fun createProfile()
fun enterKeychain()
@ -9,7 +10,7 @@ interface AppNavigation {
fun setupNewAccount()
fun setupSelectedAccount(id: String)
fun congratulation()
fun chooseProfile()
fun chooseAccount()
fun workspace()
fun openProfile()
fun openDocument(id: String)
@ -20,13 +21,13 @@ interface AppNavigation {
sealed class Command {
object OpenStartLoginScreen : Command()
object OpenCreateProfile : Command()
object OpenCreateAccount : Command()
object ChoosePinCodeScreen : Command()
object SetupNewAccountScreen : Command()
data class SetupSelectedAccountScreen(val id: String) : Command()
data class ConfirmPinCodeScreen(val code: String) : Command()
object CongratulationScreen : Command()
object ChooseProfileScreen : Command()
object ChooseAccountScreen : Command()
object EnterKeyChainScreen : Command()
object WorkspaceScreen : Command()
data class OpenDocument(val id: String) : Command()

View file

@ -9,14 +9,14 @@ import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
import com.agileburo.anytype.domain.auth.interactor.Logout
import com.agileburo.anytype.domain.auth.model.Account
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
import com.agileburo.anytype.domain.desktop.interactor.GetCurrentAccount
import com.agileburo.anytype.domain.image.LoadImage
import com.agileburo.anytype.presentation.navigation.AppNavigation
import com.agileburo.anytype.presentation.navigation.SupportNavigation
import timber.log.Timber
class ProfileViewModel(
private val getAccount: GetAccount,
private val getCurrentAccount: GetCurrentAccount,
private val loadImage: LoadImage,
private val logout: Logout
) : ViewStateViewModel<ViewState<ProfileView>>(), SupportNavigation<Event<AppNavigation.Command>> {
@ -36,8 +36,12 @@ class ProfileViewModel(
// TODO dispatch navigation command
}
fun onAddProfileClicked() {
navigation.postValue(Event(AppNavigation.Command.OpenCreateAccount))
}
private fun proceedWithGettingAccount() {
getAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
getCurrentAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
result.either(
fnL = { e -> Timber.e(e, "Error while getting account") },
fnR = { account ->

View file

@ -3,12 +3,12 @@ package com.agileburo.anytype.presentation.profile
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.domain.auth.interactor.Logout
import com.agileburo.anytype.domain.desktop.interactor.GetAccount
import com.agileburo.anytype.domain.desktop.interactor.GetCurrentAccount
import com.agileburo.anytype.domain.image.LoadImage
class ProfileViewModelFactory(
private val logout: Logout,
private val getAccount: GetAccount,
private val getCurrentAccount: GetCurrentAccount,
private val loadImage: LoadImage
) : ViewModelProvider.Factory {
@ -16,7 +16,7 @@ class ProfileViewModelFactory(
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ProfileViewModel(
logout = logout,
getAccount = getAccount,
getCurrentAccount = getCurrentAccount,
loadImage = loadImage
) as T
}

View file

@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
import com.agileburo.anytype.core_utils.common.Event
import com.agileburo.anytype.domain.auth.interactor.CheckAuthorizationStatus
import com.agileburo.anytype.domain.auth.model.AuthStatus
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.launch.LaunchAccount
import com.agileburo.anytype.presentation.navigation.AppNavigation
import timber.log.Timber
@ -14,23 +16,33 @@ import timber.log.Timber
* email : ki@agileburo.com
* on 2019-10-21.
*/
class SplashViewModel(private val checkAuthorizationStatus: CheckAuthorizationStatus) :
ViewModel() {
class SplashViewModel(
private val checkAuthorizationStatus: CheckAuthorizationStatus,
private val launchAccount: LaunchAccount
) : ViewModel() {
val navigation: MutableLiveData<Event<AppNavigation.Command>> = MutableLiveData()
fun onViewCreated() {
checkAuthorizationStatus.invoke(viewModelScope, Unit) {
it.either(
checkAuthorizationStatus.invoke(viewModelScope, Unit) { result ->
result.either(
fnL = { e -> Timber.e(e, "Error while checking auth status") },
fnR = ::proceedWithAuthStatus
fnR = { status ->
if (status == AuthStatus.UNAUTHORIZED)
navigation.postValue(Event(AppNavigation.Command.OpenStartLoginScreen))
else
proceedWithLaunchingAccount()
}
)
}
}
private fun proceedWithAuthStatus(status: AuthStatus) =
if (status == AuthStatus.UNAUTHORIZED)
navigation.postValue(Event(AppNavigation.Command.OpenStartLoginScreen))
else
navigation.postValue(Event(AppNavigation.Command.StartDesktopFromSplash))
private fun proceedWithLaunchingAccount() {
launchAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
result.either(
fnR = { navigation.postValue(Event(AppNavigation.Command.StartDesktopFromSplash)) },
fnL = { e -> Timber.e(e, "Error while launching account") }
)
}
}
}

View file

@ -3,16 +3,22 @@ package com.agileburo.anytype.presentation.splash
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.domain.auth.interactor.CheckAuthorizationStatus
import com.agileburo.anytype.domain.launch.LaunchAccount
/**
* Created by Konstantin Ivanov
* email : ki@agileburo.com
* on 2019-10-21.
*/
class SplashViewModelFactory(private val checkAuthorizationStatus: CheckAuthorizationStatus) :
ViewModelProvider.Factory {
class SplashViewModelFactory(
private val checkAuthorizationStatus: CheckAuthorizationStatus,
private val launchAccount: LaunchAccount
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
SplashViewModel(checkAuthorizationStatus) as T
SplashViewModel(
checkAuthorizationStatus = checkAuthorizationStatus,
launchAccount = launchAccount
) as T
}

View file

@ -47,7 +47,7 @@ class StartLoginViewModelTest {
vm.onSignUpClicked()
testObserver.assertValue(NavigationCommand.OpenCreateProfile)
testObserver.assertValue(NavigationCommand.OpenCreateAccount)
}
@Test

View file

@ -4,6 +4,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.agileburo.anytype.domain.auth.interactor.CheckAuthorizationStatus
import com.agileburo.anytype.domain.auth.model.AuthStatus
import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.launch.LaunchAccount
import com.agileburo.anytype.presentation.navigation.AppNavigation
import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.*
@ -22,13 +23,19 @@ class SplashViewModelTest {
@Mock
lateinit var checkAuthorizationStatus: CheckAuthorizationStatus
@Mock
lateinit var launchAccount: LaunchAccount
lateinit var vm: SplashViewModel
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
vm = SplashViewModel(checkAuthorizationStatus)
vm = SplashViewModel(
checkAuthorizationStatus = checkAuthorizationStatus,
launchAccount = launchAccount
)
}
@Test
@ -44,7 +51,7 @@ class SplashViewModelTest {
}
@Test
fun `should emit appropriate navigation command if user is authorized`() {
fun `should start launching account if user is authorized`() {
val status = AuthStatus.AUTHORIZED
@ -58,6 +65,30 @@ class SplashViewModelTest {
vm.onViewCreated()
verify(launchAccount, times(1)).invoke(any(), any(), any())
}
@Test
fun `should emit appropriate navigation command if account is launched`() {
val status = AuthStatus.AUTHORIZED
val response = Either.Right(status)
checkAuthorizationStatus.stub {
on { invoke(any(), any(), any()) } doAnswer { answer ->
answer.getArgument<(Either<Throwable, AuthStatus>) -> Unit>(2)(response)
}
}
launchAccount.stub {
on { invoke(any(), any(), any()) } doAnswer { answer ->
answer.getArgument<(Either<Throwable, Unit>) -> Unit>(2)(Either.Right(Unit))
}
}
vm.onViewCreated()
vm.navigation.test().assertValue { value ->
value.peekContent() == AppNavigation.Command.StartDesktopFromSplash
}