Skip to content

Commit

Permalink
Merge pull request #32 from lneugebauer/fix/31-login-crash
Browse files Browse the repository at this point in the history
Fix crash after login in specific cases (Fixes #31)
  • Loading branch information
lneugebauer committed Jul 9, 2023
2 parents 901d3f5 + 2042546 commit ce6fb46
Show file tree
Hide file tree
Showing 19 changed files with 127 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
Expand All @@ -71,13 +73,14 @@ import de.lukasneugebauer.nextcloudcookbook.destinations.HomeScreenDestination
import de.lukasneugebauer.nextcloudcookbook.destinations.LoginScreenDestination
import kotlinx.coroutines.launch

@OptIn(ExperimentalComposeUiApi::class)
@Destination
@Composable
fun LoginScreen(
navigator: DestinationsNavigator,
viewModel: LoginViewModel = hiltViewModel(),
) {
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
Expand All @@ -94,22 +97,25 @@ fun LoginScreen(
var showManualLogin: Boolean by rememberSaveable { mutableStateOf(false) }
val uiState by viewModel.uiState.collectAsState()

// Navigate to home screen if user authorized
LaunchedEffect(key1 = uiState) {
// Navigate to home screen if user authorized
if (uiState.authorized) {
navigator.navigate(HomeScreenDestination()) {
popUpTo(LoginScreenDestination.route) {
inclusive = true
}
}
}
}

// Open modal bottom sheet as soon as an url is available
LaunchedEffect(key1 = uiState) {
// Open modal bottom sheet as soon as an url is available
if (uiState.webViewUrl != null) {
sheetState.show()
}

// Close modal bottom sheet if currently open and url is unavailable
if (sheetState.currentValue == ModalBottomSheetValue.Expanded && uiState.webViewUrl == null) {
sheetState.hide()
}
}

ModalBottomSheetLayout(
Expand All @@ -132,11 +138,12 @@ fun LoginScreen(
onClearError = viewModel::clearErrors,
onLoginClick = { url ->
viewModel.getLoginEndpoint(url)
focusManager.clearFocus()
keyboardController?.hide()
},
onShowManualLoginClick = { showManualLogin = !showManualLogin },
onManualLoginClick = { username, password, url ->
viewModel.tryManualLogin(username, password, url)
keyboardController?.hide()
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,26 @@ class LoginViewModel @Inject constructor(
combine(
accountRepository.getAccount(),
apiProvider.ncCookbookApiFlow,
) { accountResource, ncCookbookApi -> Pair(accountResource, ncCookbookApi) }
) { account, api -> Pair(account, api) }
.distinctUntilChanged()
.collect { (accountResource, ncCookbookApi) ->
Timber.d("accountResource: $accountResource, ${accountResource.data} ncCookbookApi: $ncCookbookApi")
if (accountResource is Resource.Success && ncCookbookApi != null) {
val capabilitiesResource = accountRepository.getCapabilities()
when {
capabilitiesResource is Resource.Success && capabilitiesResource.data?.userStatus?.enabled == true -> {
_uiState.update { it.copy(authorized = true) }
}
.collect { (account, api) ->
when {
api == null -> Unit

else -> {
account is Resource.Error -> _uiState.update { it.copy(authorized = false) }

account is Resource.Success -> {
val userMetadata = accountRepository.getUserMetadata()
if (userMetadata is Resource.Error) {
clearPreferencesUseCase()
_uiState.update { it.copy(urlError = capabilitiesResource.message) }
onHideWebView()
_uiState.update {
it.copy(authorized = false, urlError = userMetadata.message)
}
} else {
_uiState.update { it.copy(authorized = true) }
}
}
} else {
_uiState.update { it.copy(authorized = false) }
}
}
}
Expand Down Expand Up @@ -106,7 +108,7 @@ class LoginViewModel @Inject constructor(

fun onHideWebView() {
pollLoginServerIsActive = false
_uiState.value = _uiState.value.copy(webViewUrl = null)
_uiState.update { it.copy(webViewUrl = null) }
}

fun clearErrors() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,34 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import de.lukasneugebauer.nextcloudcookbook.auth.domain.state.SplashScreenState
import de.lukasneugebauer.nextcloudcookbook.core.domain.repository.AccountRepository
import de.lukasneugebauer.nextcloudcookbook.core.util.Resource
import de.lukasneugebauer.nextcloudcookbook.di.ApiProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class SplashViewModel @Inject constructor(
private val accountRepository: AccountRepository,
private val apiProvider: ApiProvider,
) : ViewModel() {

private val _uiState = MutableStateFlow<SplashScreenState>(SplashScreenState.Initial)
val uiState: StateFlow<SplashScreenState> = _uiState

fun initialize() {
viewModelScope.launch {
combine(
accountRepository.getAccount(),
apiProvider.ncCookbookApiFlow,
) { account, ncCookbookApi ->
Pair(account, ncCookbookApi)
}.collect { (account, ncCookbookApi) ->
when {
account is Resource.Success && ncCookbookApi != null -> {
val userEnabled = accountRepository.getCapabilities().data?.userStatus?.enabled
if (userEnabled == true) {
_uiState.update { SplashScreenState.Authorized }
}
}
account is Resource.Error -> {
_uiState.update { SplashScreenState.Unauthorized }
}
accountRepository.getAccount()
.distinctUntilChanged()
.onEach { account ->
val userMetadata = accountRepository.getUserMetadata()

if (account is Resource.Success && userMetadata is Resource.Success) {
_uiState.update { SplashScreenState.Authorized }
} else {
_uiState.update { SplashScreenState.Unauthorized }
}
}
}
.launchIn(viewModelScope)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.haroldadmin.cnradapter.NetworkResponse
import de.lukasneugebauer.nextcloudcookbook.category.data.dto.CategoryDto
import de.lukasneugebauer.nextcloudcookbook.core.data.remote.response.CapabilitiesResponse
import de.lukasneugebauer.nextcloudcookbook.core.data.remote.response.ErrorResponse
import de.lukasneugebauer.nextcloudcookbook.core.data.remote.response.UserMetadataResponse
import de.lukasneugebauer.nextcloudcookbook.core.util.Constants.FULL_PATH
import de.lukasneugebauer.nextcloudcookbook.recipe.data.dto.RecipeDto
import de.lukasneugebauer.nextcloudcookbook.recipe.data.dto.RecipePreviewDto
Expand All @@ -25,6 +26,14 @@ interface NcCookbookApi {
@GET("ocs/v2.php/cloud/capabilities?format=json")
suspend fun getCapabilities(): NetworkResponse<CapabilitiesResponse, ErrorResponse>

@Headers(
"Accept: application/json",
"OCS-APIRequest: true",
"Content-Type: application/json;charset=utf-8",
)
@GET("ocs/v2.php/cloud/users/{username}?format=json")
suspend fun getUserMetadata(@Path("username") username: String): NetworkResponse<UserMetadataResponse, ErrorResponse>

@GET("$FULL_PATH/categories")
suspend fun getCategories(): List<CategoryDto>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import de.lukasneugebauer.nextcloudcookbook.core.domain.model.Capabilities
data class CapabilitiesDto(
@SerializedName("theming")
val theming: ThemingDto,
@SerializedName("user_status")
val userStatus: UserStatusDto,
) {
fun toCapabilities(): Capabilities = Capabilities(
theming = theming.toTheming(),
userStatus = userStatus.toUserStatus(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package de.lukasneugebauer.nextcloudcookbook.core.data.dto

import com.google.gson.annotations.SerializedName

data class OcsDataDto(
data class CapabilitiesOcsDataDto(
@SerializedName("capabilities")
val capabilities: CapabilitiesDto,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package de.lukasneugebauer.nextcloudcookbook.core.data.dto

import com.google.gson.annotations.SerializedName

data class OcsDto(
data class OcsDto<T>(
@SerializedName("data")
val data: OcsDataDto,
val data: T,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package de.lukasneugebauer.nextcloudcookbook.core.data.dto

import com.google.gson.annotations.SerializedName
import de.lukasneugebauer.nextcloudcookbook.core.domain.model.UserMetadata

data class UserMetadataDto(
@SerializedName("id")
val id: String,
@SerializedName("displayname")
val displayname: String,
) {
fun toUserMetadata(): UserMetadata = UserMetadata(
id = id,
name = displayname,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package de.lukasneugebauer.nextcloudcookbook.core.data.remote
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.Response
import java.nio.charset.StandardCharsets.UTF_8

class BasicAuthInterceptor(username: String, password: String) : Interceptor {

private val credentials: String = Credentials.basic(username, password)
private val credentials: String = Credentials.basic(username, password, UTF_8)

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package de.lukasneugebauer.nextcloudcookbook.core.data.remote.response

import com.google.gson.annotations.SerializedName
import de.lukasneugebauer.nextcloudcookbook.core.data.dto.CapabilitiesOcsDataDto
import de.lukasneugebauer.nextcloudcookbook.core.data.dto.OcsDto

data class CapabilitiesResponse(
@SerializedName("ocs")
val ocs: OcsDto,
val ocs: OcsDto<CapabilitiesOcsDataDto>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.lukasneugebauer.nextcloudcookbook.core.data.remote.response

import com.google.gson.annotations.SerializedName
import de.lukasneugebauer.nextcloudcookbook.core.data.dto.OcsDto
import de.lukasneugebauer.nextcloudcookbook.core.data.dto.UserMetadataDto

data class UserMetadataResponse(
@SerializedName("ocs")
val ocs: OcsDto<UserMetadataDto>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import de.lukasneugebauer.nextcloudcookbook.R
import de.lukasneugebauer.nextcloudcookbook.core.data.PreferencesManager
import de.lukasneugebauer.nextcloudcookbook.core.domain.model.Capabilities
import de.lukasneugebauer.nextcloudcookbook.core.domain.model.NcAccount
import de.lukasneugebauer.nextcloudcookbook.core.domain.model.UserMetadata
import de.lukasneugebauer.nextcloudcookbook.core.domain.repository.AccountRepository
import de.lukasneugebauer.nextcloudcookbook.core.domain.repository.BaseRepository
import de.lukasneugebauer.nextcloudcookbook.core.util.IoDispatcher
Expand All @@ -14,6 +15,7 @@ import de.lukasneugebauer.nextcloudcookbook.di.ApiProvider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import javax.inject.Inject
Expand All @@ -34,7 +36,27 @@ class AccountRepositoryImpl @Inject constructor(
val result = response.body.ocs.data.capabilities.toCapabilities()
Resource.Success(data = result)
}
is NetworkResponse.Error -> handleResponseError(response.error?.cause)

is NetworkResponse.Error -> handleResponseError(response.error)
}
}
}

override suspend fun getUserMetadata(): Resource<UserMetadata> {
return withContext(ioDispatcher) {
val api = apiProvider.getNcCookbookApi()
?: return@withContext Resource.Error(message = UiText.StringResource(R.string.error_api_not_initialized))
val username = preferencesManager.preferencesFlow.map { it.ncAccount.username }.first()

when (val response = api.getUserMetadata(username)) {
is NetworkResponse.Success -> {
val result = response.body.ocs.data.toUserMetadata()
Resource.Success(data = result)
}

is NetworkResponse.Error -> {
handleResponseError(response.error)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ package de.lukasneugebauer.nextcloudcookbook.core.domain.model

data class Capabilities(
val theming: Theming,
val userStatus: UserStatus,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.lukasneugebauer.nextcloudcookbook.core.domain.model

data class UserMetadata(
val id: String,
val name: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package de.lukasneugebauer.nextcloudcookbook.core.domain.repository

import de.lukasneugebauer.nextcloudcookbook.core.domain.model.Capabilities
import de.lukasneugebauer.nextcloudcookbook.core.domain.model.NcAccount
import de.lukasneugebauer.nextcloudcookbook.core.domain.model.UserMetadata
import de.lukasneugebauer.nextcloudcookbook.core.util.Resource
import kotlinx.coroutines.flow.Flow

interface AccountRepository {

suspend fun getCapabilities(): Resource<Capabilities>

suspend fun getUserMetadata(): Resource<UserMetadata>

fun getAccount(): Flow<Resource<NcAccount>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class ApiProvider @Inject constructor(
}
}

fun resetApi() {
_ncCookbookApiFlow.value = null
}

private fun initRetrofitApi(ncAccount: NcAccount) {
val authInterceptor = BasicAuthInterceptor(ncAccount.username, ncAccount.token)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.lukasneugebauer.nextcloudcookbook.core.domain.usecase.ClearAllStoresUseCase
import de.lukasneugebauer.nextcloudcookbook.core.domain.usecase.ClearPreferencesUseCase
import kotlinx.coroutines.async
import de.lukasneugebauer.nextcloudcookbook.di.ApiProvider
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class SettingsViewModel @Inject constructor(
private val apiProvider: ApiProvider,
private val clearAllStoresUseCase: ClearAllStoresUseCase,
private val clearPreferencesUseCase: ClearPreferencesUseCase,
val sharedPreferences: SharedPreferences,
) : ViewModel() {

@Suppress("DeferredResultUnused")
fun logout(callback: () -> Unit) {
viewModelScope.launch {
async { clearAllStoresUseCase.invoke() }
async { clearPreferencesUseCase.invoke() }
apiProvider.resetApi()
clearAllStoresUseCase.invoke()
clearPreferencesUseCase.invoke()
callback.invoke()
}
}
Expand Down
Loading

0 comments on commit ce6fb46

Please sign in to comment.