Skip to content

Commit

Permalink
feat(logout): Keep user signed in (AndroidDev-social#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
krizzu committed Jan 4, 2023
1 parent 0bf705a commit 1f33a41
Show file tree
Hide file tree
Showing 29 changed files with 310 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import androidx.compose.ui.Modifier
import com.arkivanov.decompose.defaultComponentContext
import kotlinx.coroutines.Dispatchers
import social.androiddev.common.theme.DodoTheme
import social.androiddev.root.composables.RootContent
import social.androiddev.root.navigation.DefaultRootComponent
import social.androiddev.root.root.DefaultRootComponent
import social.androiddev.root.root.RootContent

class MainActivity : AppCompatActivity() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import kotlinx.coroutines.Dispatchers
import org.koin.core.context.startKoin
import social.androiddev.common.di.appModule
import social.androiddev.common.theme.DodoTheme
import social.androiddev.root.composables.RootContent
import social.androiddev.root.navigation.DefaultRootComponent
import social.androiddev.root.root.DefaultRootComponent
import social.androiddev.root.root.RootContent

fun main() {
startKoin {
Expand Down
2 changes: 2 additions & 0 deletions data/persistence/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ kotlin {
val commonMain by getting {
dependencies {
implementation(libs.multiplatform.settings)
implementation(libs.multiplatform.settings.coroutines)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.io.insert.koin.core)
implementation(libs.store)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
package social.androiddev.common.persistence.localstorage

import kotlinx.coroutines.flow.Flow

/**
* Contract for key => value storage for any authentication related data
*/
Expand All @@ -24,6 +26,11 @@ interface DodoAuthStorage {
*/
var currentDomain: String?

/**
* List of servers that user has access to
*/
val authorizedServersFlow: Flow<List<String>>

/**
* Save the @param token keyed by @param server
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,44 @@
*/
package social.androiddev.common.persistence.localstorage

import com.russhwolf.settings.Settings
import com.russhwolf.settings.ExperimentalSettingsApi
import com.russhwolf.settings.ObservableSettings
import com.russhwolf.settings.coroutines.getStringOrNullFlow
import com.russhwolf.settings.get
import com.russhwolf.settings.set
import kotlinx.atomicfu.locks.ReentrantLock
import kotlinx.atomicfu.locks.reentrantLock
import kotlinx.atomicfu.locks.withLock
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json

@OptIn(ExperimentalSettingsApi::class)
internal class DodoAuthStorageImpl(
private val settings: Settings,
private val settings: ObservableSettings,
private val json: Json,
private val lock: ReentrantLock = reentrantLock()
private val lock: ReentrantLock = reentrantLock(),
) : DodoAuthStorage {
override var currentDomain: String?
get() = settings[KEY_DOMAIN_CACHE]
set(value) {
settings[KEY_DOMAIN_CACHE] = value
}

override val authorizedServersFlow: Flow<List<String>> = flow {
settings.getStringOrNullFlow(KEY_ACCESS_TOKENS_CACHE).collect { authTokens ->
if (authTokens == null) {
emit(listOf())
} else {
val serverList = json.decodeFromString(ListSerializer(AccessToken.serializer()), authTokens)
.associateBy { it.server }.keys.toList()
emit(serverList)
}
}
}

/**
* The user can set up multiple accounts on their device. So we
* key the AccessToken by the unique server/domain
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CREATE TABLE Application (
CREATE TABLE IF NOT EXISTS Application (
instance Text NOT NULL PRIMARY KEY,
client_id Text NOT NULL,
client_secret TEXT NOT NULL,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CREATE TABLE failedWrite (
CREATE TABLE IF NOT EXISTS failedWrite (
key TEXT NOT NULL PRIMARY KEY,
datetime INTEGER AS Long
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CREATE TABLE StatusDB (
CREATE TABLE IF NOT EXISTS StatusDB (
type TEXT NOT NULL,
remoteId Text NOT NULL PRIMARY KEY,
uri Text NOT NULL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import social.androiddev.common.persistence.AuthenticationDatabase
import social.androiddev.common.persistence.localstorage.DodoAuthStorage
import social.androiddev.common.persistence.localstorage.DodoAuthStorageImpl
import social.androiddev.common.timeline.TimelineDatabase
import java.io.File

/**
* Koin DI module for all desktop specific persistence dependencies
Expand All @@ -39,7 +40,13 @@ actual val persistenceModule: Module = module {
}

single {
val driver = JdbcSqliteDriver(url = JdbcSqliteDriver.IN_MEMORY).also { driver ->
// refers to the user directory, such us /Users/my_username or /home/my_username or C:\Users\my_username
val dbDir = File(System.getProperty("user.home"), "dodo")
if (!dbDir.exists()) {
dbDir.mkdirs()
}
val dbFile = File(dbDir, AUTH_DB_NAME)
val driver = JdbcSqliteDriver(url = "jdbc:sqlite:${dbFile.absolutePath}").also { driver ->
AuthenticationDatabase.Schema.create(driver = driver)
}
AuthenticationDatabase(driver)
Expand Down
4 changes: 3 additions & 1 deletion data/repository/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ kotlin {
implementation(projects.domain.timeline)
implementation(libs.io.insert.koin.core)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.com.squareup.sqldelight.coroutines.extensions)
//TODO remove as api dependency once we can stop dependening on StoreResponse in UI
api(libs.store)
implementation(libs.com.squareup.sqldelight.coroutines.extensions)
Expand All @@ -32,7 +33,7 @@ kotlin {
}
val androidMain by getting {
dependencies {
api (libs.org.jetbrains.kotlinx.atomicfu)
api(libs.org.jetbrains.kotlinx.atomicfu)
}
}
val desktopTest by getting {
Expand All @@ -47,6 +48,7 @@ kotlin {
implementation(libs.org.jetbrains.kotlin.test.common)
implementation(libs.org.jetbrains.kotlin.test.annotations.common)
implementation(libs.org.jetbrains.kotlinx.coroutines.test)
implementation(libs.app.cash.turbine)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
package social.androiddev.common.repository

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.withContext
import social.androiddev.common.network.MastodonApi
import social.androiddev.common.persistence.AuthenticationDatabase
Expand All @@ -34,7 +36,7 @@ internal class AuthenticationRepositoryImpl(
clientName: String,
redirectUris: String,
scopes: String,
website: String?
website: String?,
): NewAppOAuthToken? {
val application = mastodonApi.createApplication(
domain = domain,
Expand Down Expand Up @@ -112,4 +114,9 @@ internal class AuthenticationRepositoryImpl(
}

override val selectedServer: String? = settings.currentDomain

override suspend fun getIsAccessTokenPresent(): Flow<Boolean> =
settings.authorizedServersFlow.transform { servers ->
emit(servers.isNotEmpty())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import social.androiddev.domain.authentication.repository.AuthenticationReposito
*/
val repositoryModule: Module = module {

factory<AuthenticationRepository> {
single<AuthenticationRepository> {
AuthenticationRepositoryImpl(
mastodonApi = get(),
database = get(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* This file is part of Dodo.
*
* Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Dodo.
* If not, see <https://www.gnu.org/licenses/>.
*/
package social.androiddev.common.repository.timeline

import app.cash.turbine.test
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import social.androiddev.common.repository.AuthenticationRepositoryImpl
import social.androiddev.common.repository.timeline.fixtures.FakeAuthDatabase
import social.androiddev.common.repository.timeline.fixtures.FakeAuthStorage
import social.androiddev.common.repository.timeline.fixtures.fakeApi
import social.androiddev.domain.authentication.repository.AuthenticationRepository
import kotlin.coroutines.CoroutineContext
import kotlin.test.Test
import kotlin.test.assertEquals

private fun createRepo(
ctx: CoroutineContext,
serversFlow: Flow<List<String>>,
): AuthenticationRepository = AuthenticationRepositoryImpl(
mastodonApi = fakeApi,
database = FakeAuthDatabase(),
settings = FakeAuthStorage(serversFlow),
ioCoroutineContext = ctx
)

class AuthenticationRepositoryTest {

@Test
fun notifiesAboutAuthServerChange() = runTest {
val serverFlow = MutableStateFlow<List<String>>(listOf())
val repo = createRepo(this.coroutineContext, serverFlow)

repo.getIsAccessTokenPresent().test {
skipItems(1) // skip initial
serverFlow.emit(listOf("server1", "server2"))
assertEquals(true, awaitItem())
serverFlow.emit(listOf("server3"))
assertEquals(true, awaitItem())
serverFlow.emit(listOf())
assertEquals(false, awaitItem())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import kotlinx.coroutines.test.runTest
import org.mobilenativefoundation.store.store5.FetcherResult
import social.androiddev.common.network.model.Privacy
import social.androiddev.common.network.model.Status
import social.androiddev.common.repository.timeline.fixtures.FakeAuthStorage
import social.androiddev.common.repository.timeline.fixtures.fakeApi
import social.androiddev.common.repository.timeline.fixtures.fakeStorage
import social.androiddev.common.timeline.StatusDB
import social.androiddev.domain.timeline.FeedType
import kotlin.test.Test
Expand All @@ -29,7 +29,7 @@ import kotlin.test.assertTrue
class TimelineFetcherKtTest {
@Test
fun timelineFetcher() = runTest {
val fetcher = fakeApi.timelineFetcher(fakeStorage)
val fetcher = fakeApi.timelineFetcher(FakeAuthStorage())
val result = fetcher.invoke(FeedType.Home)
val value: FetcherResult<List<StatusDB>> = result.first()
assertTrue { value is FetcherResult.Data<*> }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
*/
package social.androiddev.common.repository.timeline.fixtures

import com.squareup.sqldelight.TransactionWithReturn
import com.squareup.sqldelight.TransactionWithoutReturn
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.mobilenativefoundation.store.store5.ResponseOrigin
import org.mobilenativefoundation.store.store5.StoreResponse
import social.androiddev.common.network.MastodonApi
Expand All @@ -22,21 +26,37 @@ import social.androiddev.common.network.model.NewOauthApplication
import social.androiddev.common.network.model.Privacy
import social.androiddev.common.network.model.Status
import social.androiddev.common.network.model.Token
import social.androiddev.common.persistence.AuthenticationDatabase
import social.androiddev.common.persistence.authentication.ApplicationQueries
import social.androiddev.common.persistence.localstorage.DodoAuthStorage
import social.androiddev.domain.timeline.FeedType
import social.androiddev.domain.timeline.model.StatusLocal
import social.androiddev.domain.timeline.model.Visibility

val fakeStorage = object : DodoAuthStorage {
class FakeAuthStorage(serversFlow: Flow<List<String>> = flowOf(listOf())) : DodoAuthStorage {
override var currentDomain: String? = "androiddev.social"

override val authorizedServersFlow = serversFlow
override suspend fun saveAccessToken(server: String, token: String) {
TODO("Not yet implemented")
}

override fun getAccessToken(server: String): String = "FakeToken"
}

class FakeAuthDatabase : AuthenticationDatabase {
override fun transaction(noEnclosing: Boolean, body: TransactionWithoutReturn.() -> Unit) {
TODO("Not yet implemented")
}

override fun <R> transactionWithResult(noEnclosing: Boolean, bodyWithReturn: TransactionWithReturn<R>.() -> R): R {
TODO("Not yet implemented")
}

override val applicationQueries: ApplicationQueries
get() = TODO("Not yet implemented")
}

val failureResponse = StoreResponse.Error.Message("We failed", ResponseOrigin.Cache)

val fakeLocalStatus = StatusLocal(
Expand Down Expand Up @@ -77,7 +97,7 @@ val fakeApi = object : MastodonApi {
clientName: String,
redirectUris: String,
scopes: String,
website: String?
website: String?,
): Result<NewOauthApplication> {
TODO("Not yet implemented")
}
Expand All @@ -89,7 +109,7 @@ val fakeApi = object : MastodonApi {
redirectUri: String,
grantType: String,
code: String,
scope: String
scope: String,
): Result<Token> {
TODO("Not yet implemented")
}
Expand All @@ -104,7 +124,7 @@ val fakeApi = object : MastodonApi {

override suspend fun getHomeFeed(
domain: String,
accessToken: String
accessToken: String,
): Result<List<Status>> {
return Result.success(
listOf<Status>(
Expand Down
2 changes: 1 addition & 1 deletion domain/authentication/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ kotlin {
val commonMain by getting {
dependencies {
implementation(libs.io.insert.koin.core)

implementation(libs.kotlinx.coroutines.core)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.koin.core.module.Module
import org.koin.dsl.module
import social.androiddev.domain.authentication.usecase.AuthenticateClient
import social.androiddev.domain.authentication.usecase.CreateAccessToken
import social.androiddev.domain.authentication.usecase.GetAuthStatus
import social.androiddev.domain.authentication.usecase.GetSelectedApplicationOAuthToken

/**
Expand All @@ -41,4 +42,10 @@ val domainAuthModule: Module = module {
authenticationRepository = get(),
)
}

factory {
GetAuthStatus(
authenticationRepository = get()
)
}
}
Loading

0 comments on commit 1f33a41

Please sign in to comment.