From cec97c7106b900336dbc210a10b5831765abf54b Mon Sep 17 00:00:00 2001 From: Gibran Chevalley Date: Mon, 24 Nov 2025 15:39:28 +0100 Subject: [PATCH 1/9] feat: Add ServerError InternalTranslatedErrorCode for authentication logic --- .../infomaniak/core/network/api/InternalTranslatedErrorCode.kt | 1 + Network/src/main/res/values-de/strings.xml | 1 + Network/src/main/res/values-es/strings.xml | 1 + Network/src/main/res/values-fr/strings.xml | 1 + Network/src/main/res/values-it/strings.xml | 1 + Network/src/main/res/values/strings.xml | 3 ++- 6 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Network/src/main/kotlin/com/infomaniak/core/network/api/InternalTranslatedErrorCode.kt b/Network/src/main/kotlin/com/infomaniak/core/network/api/InternalTranslatedErrorCode.kt index 2687bb92d..8f89020ee 100644 --- a/Network/src/main/kotlin/com/infomaniak/core/network/api/InternalTranslatedErrorCode.kt +++ b/Network/src/main/kotlin/com/infomaniak/core/network/api/InternalTranslatedErrorCode.kt @@ -27,6 +27,7 @@ enum class InternalTranslatedErrorCode( ) : ErrorCodeTranslated { NoConnection("no_connection", R.string.noConnection), ConnectionError("connection_error", R.string.connectionError), + ServerError("server_error", R.string.serverError), UnknownError("an_error_has_occurred", R.string.anErrorHasOccurred), UserLoggedOut("user_logged_out", R.string.anErrorHasOccurred), // We don't display this error UserAlreadyPresent("user_already_present", R.string.errorUserAlreadyPresent), diff --git a/Network/src/main/res/values-de/strings.xml b/Network/src/main/res/values-de/strings.xml index 3f1a8abe5..489d3b0ec 100644 --- a/Network/src/main/res/values-de/strings.xml +++ b/Network/src/main/res/values-de/strings.xml @@ -20,4 +20,5 @@ Problem mit der Internetverbindung Fehler Benutzer bereits vorhanden Keine Verbindung + Server-Fehler diff --git a/Network/src/main/res/values-es/strings.xml b/Network/src/main/res/values-es/strings.xml index e8fce8a98..fbe2e6c2a 100644 --- a/Network/src/main/res/values-es/strings.xml +++ b/Network/src/main/res/values-es/strings.xml @@ -20,4 +20,5 @@ Problema de conexión a Internet Error, usuario ya registrado Sin conexión + Error del servidor diff --git a/Network/src/main/res/values-fr/strings.xml b/Network/src/main/res/values-fr/strings.xml index cd8e99548..d974bffb6 100644 --- a/Network/src/main/res/values-fr/strings.xml +++ b/Network/src/main/res/values-fr/strings.xml @@ -20,4 +20,5 @@ Problème de connexion internet Erreur utilisateur déjà présent Pas de connexion + Erreur serveur diff --git a/Network/src/main/res/values-it/strings.xml b/Network/src/main/res/values-it/strings.xml index b163c2561..0f485d89d 100644 --- a/Network/src/main/res/values-it/strings.xml +++ b/Network/src/main/res/values-it/strings.xml @@ -20,4 +20,5 @@ Problema di connessione a Internet Errore utente già esistente Nessuna connessione + Errore del server diff --git a/Network/src/main/res/values/strings.xml b/Network/src/main/res/values/strings.xml index 46bfc3695..90890d855 100644 --- a/Network/src/main/res/values/strings.xml +++ b/Network/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ An error has occurred Internet connection problem - Error, User already present + Error, user already present No connection + Server Error From 80bb5d0c0f16459376bebd346ee756fc265d995f Mon Sep 17 00:00:00 2001 From: Gibran Chevalley Date: Fri, 21 Nov 2025 17:01:10 +0100 Subject: [PATCH 2/9] feat: Start extracting shared logic to Core module --- .../core/auth/api/ApiRepositoryCore.kt | 25 ++- CrossAppLogin/Login/build.gradle.kts | 34 ++++ .../crossapplogin/login/AuthCodeResult.kt | 24 +++ .../core/crossapplogin/login/LoginUtils.kt | 158 ++++++++++++++++++ .../core/crossapplogin/login/UserResult.kt | 36 ++++ 5 files changed, 269 insertions(+), 8 deletions(-) create mode 100644 CrossAppLogin/Login/build.gradle.kts create mode 100644 CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/AuthCodeResult.kt create mode 100644 CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt create mode 100644 CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/UserResult.kt diff --git a/Auth/src/main/kotlin/com/infomaniak/core/auth/api/ApiRepositoryCore.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/api/ApiRepositoryCore.kt index eec678830..65b83da03 100644 --- a/Auth/src/main/kotlin/com/infomaniak/core/auth/api/ApiRepositoryCore.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/api/ApiRepositoryCore.kt @@ -29,14 +29,23 @@ abstract class ApiRepositoryCore { withEmails: Boolean = false, withPhones: Boolean = false, withSecurity: Boolean = false, - ): ApiResponse { - var with = "" - if (withEmails) with += "emails" - if (withPhones) with += "phones" - if (withSecurity) with += "security" - if (with.isNotEmpty()) with = "?with=$with" + ): ApiResponse = ApiRepositoryCore.getUserProfile(okHttpClient, withEmails, withPhones, withSecurity) - val url = "${ApiRoutesCore.getUserProfile()}$with" - return ApiController.callApi(url, ApiController.ApiMethod.GET, okHttpClient = okHttpClient) + companion object { + suspend fun getUserProfile( + okHttpClient: OkHttpClient, + withEmails: Boolean = false, + withPhones: Boolean = false, + withSecurity: Boolean = false, + ): ApiResponse { + var with = "" + if (withEmails) with += "emails" + if (withPhones) with += "phones" + if (withSecurity) with += "security" + if (with.isNotEmpty()) with = "?with=$with" + + val url = "${ApiRoutesCore.getUserProfile()}$with" + return ApiController.callApi(url, ApiController.ApiMethod.GET, okHttpClient = okHttpClient) + } } } diff --git a/CrossAppLogin/Login/build.gradle.kts b/CrossAppLogin/Login/build.gradle.kts new file mode 100644 index 000000000..39324379e --- /dev/null +++ b/CrossAppLogin/Login/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("com.android.library") + alias(core.plugins.kotlin.android) +} + +val coreCompileSdk: Int by rootProject.extra +val coreMinSdk: Int by rootProject.extra +val javaVersion: JavaVersion by rootProject.extra + +android { + namespace = "com.infomaniak.core.crossapplogin.login" + compileSdk = coreCompileSdk + + defaultConfig { + minSdk = coreMinSdk + } + + compileOptions { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + } + + kotlinOptions { + jvmTarget = javaVersion.toString() + } +} + +dependencies { + implementation(project(":Core:Auth")) + implementation(project(":Core:Network")) + + implementation(core.appcompat) + api(core.okhttp) +} diff --git a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/AuthCodeResult.kt b/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/AuthCodeResult.kt new file mode 100644 index 000000000..572e245a4 --- /dev/null +++ b/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/AuthCodeResult.kt @@ -0,0 +1,24 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.core.crossapplogin.login + +internal sealed interface AuthCodeResult { + data class Success(val code: String) : AuthCodeResult + data class Error(val message: String) : AuthCodeResult + data object Canceled : AuthCodeResult +} diff --git a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt b/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt new file mode 100644 index 000000000..e828eb03d --- /dev/null +++ b/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt @@ -0,0 +1,158 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.core.crossapplogin.login + +import android.content.Context +import androidx.activity.result.ActivityResult +import androidx.appcompat.app.AppCompatActivity +import com.infomaniak.core.auth.CredentialManager +import com.infomaniak.core.auth.TokenAuthenticator.Companion.changeAccessToken +import com.infomaniak.core.auth.api.ApiRepositoryCore +import com.infomaniak.core.auth.models.user.User +import com.infomaniak.core.network.api.ApiController.toApiError +import com.infomaniak.core.network.api.InternalTranslatedErrorCode +import com.infomaniak.core.network.models.ApiResponse +import com.infomaniak.core.network.models.ApiResponseStatus +import com.infomaniak.core.network.networking.HttpClient +import com.infomaniak.core.network.utils.ApiErrorCode.Companion.translateError +import com.infomaniak.lib.login.ApiToken +import com.infomaniak.lib.login.InfomaniakLogin +import com.infomaniak.lib.login.InfomaniakLogin.ErrorStatus +import com.infomaniak.lib.login.InfomaniakLogin.TokenResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.invoke + + +object LoginUtils { + /** + * Logs the user based on the ActivityResult of our login WebView. + * + * @return a [UserLoginResult] or null if the user canceled the login webview without going through to the end + */ + suspend fun getLoginResultAfterWebView( + result: ActivityResult, + context: Context, + infomaniakLogin: InfomaniakLogin, + credentialManager: CredentialManager, + ): UserLoginResult? { + val authCodeResult = result.toAuthCodeResult(context) + when (authCodeResult) { + is AuthCodeResult.Error -> return UserLoginResult.Failure(authCodeResult.message) + is AuthCodeResult.Canceled -> return null + is AuthCodeResult.Success -> Unit + } + + val tokenResult = infomaniakLogin.getToken(okHttpClient = HttpClient.okHttpClient, code = authCodeResult.code) + when (tokenResult) { + is TokenResult.Error -> { + return UserLoginResult.Failure(context.getUserAuthenticationErrorMessage(tokenResult.errorStatus)) + } + is TokenResult.Success -> Unit + } + + val userResult = authenticateUsers(listOf(tokenResult.apiToken), credentialManager).single() + return when (userResult) { + is UserResult.Failure -> { + UserLoginResult.Failure(context.getString(userResult.apiResponse.translateError())) + } + is UserResult.Success -> UserLoginResult.Success(userResult.user) + } + } + + /** + * Logs the user based on the [ApiToken]s returned by the cross app login logic. + * + * @return a [UserLoginResult] or null if the user canceled the login webview without going through to the end + */ + suspend fun getLoginResultsAfterCrossApp( + apiTokens: List, + context: Context, + credentialManager: CredentialManager, + ): List = buildList { + authenticateUsers(apiTokens, credentialManager).forEach { result -> + when (result) { + is UserResult.Success -> add(UserLoginResult.Success(result.user)) + is UserResult.Failure -> add(UserLoginResult.Failure(context.getString(result.apiResponse.translateError()))) + } + } + } + + private suspend fun authenticateUsers( + apiTokens: List, + credentialManager: CredentialManager, + ): List = apiTokens.map { apiToken -> + runCatching { + authenticateUser(apiToken, credentialManager) + }.getOrDefault(UserResult.Failure.Unknown) + } + + private fun ActivityResult.toAuthCodeResult(context: Context): AuthCodeResult { + if (resultCode != AppCompatActivity.RESULT_OK) return AuthCodeResult.Canceled + + val authCode = data?.getStringExtra(InfomaniakLogin.CODE_TAG) + val translatedError = data?.getStringExtra(InfomaniakLogin.ERROR_TRANSLATED_TAG) + + return when { + translatedError?.isNotBlank() == true -> AuthCodeResult.Error(translatedError) + authCode?.isNotBlank() == true -> AuthCodeResult.Success(authCode) + else -> AuthCodeResult.Error(context.getString(InternalTranslatedErrorCode.UnknownError.translateRes)) + } + } + + private fun getErrorResponse(error: InternalTranslatedErrorCode): ApiResponse { + return ApiResponse(result = ApiResponseStatus.ERROR, error = error.toApiError()) + } + + private suspend fun authenticateUser(apiToken: ApiToken, credentialManager: CredentialManager): UserResult { + if (credentialManager.getUserById(apiToken.userId) != null) return UserResult.Failure( + getErrorResponse(InternalTranslatedErrorCode.UserAlreadyPresent) + ) + + val okhttpClient = HttpClient.okHttpClient.newBuilder().addInterceptor { chain -> + val newRequest = changeAccessToken(chain.request(), apiToken) + chain.proceed(newRequest) + }.build() + + val userProfileResponse = Dispatchers.IO { ApiRepositoryCore.getUserProfile(okhttpClient) } + + if (userProfileResponse.result == ApiResponseStatus.ERROR) return UserResult.Failure(userProfileResponse) + if (userProfileResponse.data == null) return UserResult.Failure.Unknown + + val user = userProfileResponse.data!!.apply { + this.apiToken = apiToken + this.organizations = arrayListOf() + } + + return UserResult.Success(user) + } +} + +private fun Context.getUserAuthenticationErrorMessage(errorStatus: ErrorStatus): String { + return getString( + when (errorStatus) { + ErrorStatus.SERVER -> InternalTranslatedErrorCode.ServerError + ErrorStatus.CONNECTION -> InternalTranslatedErrorCode.ConnectionError + else -> InternalTranslatedErrorCode.UnknownError + }.translateRes + ) +} + +sealed interface UserLoginResult { + data class Success(val user: User) : UserLoginResult + data class Failure(val errorMessage: String) : UserLoginResult +} diff --git a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/UserResult.kt b/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/UserResult.kt new file mode 100644 index 000000000..964cb9112 --- /dev/null +++ b/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/UserResult.kt @@ -0,0 +1,36 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.core.crossapplogin.login + +import com.infomaniak.core.auth.models.user.User +import com.infomaniak.core.network.api.ApiController.toApiError +import com.infomaniak.core.network.api.InternalTranslatedErrorCode +import com.infomaniak.core.network.models.ApiResponse +import com.infomaniak.core.network.models.ApiResponseStatus + +internal sealed interface UserResult { + data class Success(val user: User) : UserResult + open class Failure(val apiResponse: ApiResponse<*>) : UserResult { + data object Unknown : Failure( + ApiResponse( + result = ApiResponseStatus.ERROR, + error = InternalTranslatedErrorCode.UnknownError.toApiError() + ) + ) + } +} From 856c6c2bf03f9c9951237e16fabd26eee96a623f Mon Sep 17 00:00:00 2001 From: Gibran Chevalley Date: Tue, 25 Nov 2025 13:03:58 +0100 Subject: [PATCH 3/9] refactor: Split cross app independent login logic to its own independent module --- CrossAppLogin/Login/build.gradle.kts | 4 +- .../core/crossapplogin/login/LoginUtils.kt | 148 ++---------------- Login/build.gradle.kts | 34 ++++ .../com/infomaniak/core/login/LoginUtils.kt | 142 +++++++++++++++++ .../core/login/models}/AuthCodeResult.kt | 2 +- .../core/login/models/UserLoginResult.kt | 25 +++ .../core/login/models}/UserResult.kt | 4 +- 7 files changed, 222 insertions(+), 137 deletions(-) create mode 100644 Login/build.gradle.kts create mode 100644 Login/src/main/kotlin/com/infomaniak/core/login/LoginUtils.kt rename {CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login => Login/src/main/kotlin/com/infomaniak/core/login/models}/AuthCodeResult.kt (95%) create mode 100644 Login/src/main/kotlin/com/infomaniak/core/login/models/UserLoginResult.kt rename {CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login => Login/src/main/kotlin/com/infomaniak/core/login/models}/UserResult.kt (94%) diff --git a/CrossAppLogin/Login/build.gradle.kts b/CrossAppLogin/Login/build.gradle.kts index 39324379e..21cb7a466 100644 --- a/CrossAppLogin/Login/build.gradle.kts +++ b/CrossAppLogin/Login/build.gradle.kts @@ -26,9 +26,7 @@ android { } dependencies { + api(project(":Core:Login")) implementation(project(":Core:Auth")) implementation(project(":Core:Network")) - - implementation(core.appcompat) - api(core.okhttp) } diff --git a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt b/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt index e828eb03d..7f36e4fb9 100644 --- a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt +++ b/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt @@ -18,141 +18,27 @@ package com.infomaniak.core.crossapplogin.login import android.content.Context -import androidx.activity.result.ActivityResult -import androidx.appcompat.app.AppCompatActivity import com.infomaniak.core.auth.CredentialManager -import com.infomaniak.core.auth.TokenAuthenticator.Companion.changeAccessToken -import com.infomaniak.core.auth.api.ApiRepositoryCore -import com.infomaniak.core.auth.models.user.User -import com.infomaniak.core.network.api.ApiController.toApiError -import com.infomaniak.core.network.api.InternalTranslatedErrorCode -import com.infomaniak.core.network.models.ApiResponse -import com.infomaniak.core.network.models.ApiResponseStatus -import com.infomaniak.core.network.networking.HttpClient +import com.infomaniak.core.login.LoginUtils +import com.infomaniak.core.login.models.UserLoginResult +import com.infomaniak.core.login.models.UserResult import com.infomaniak.core.network.utils.ApiErrorCode.Companion.translateError import com.infomaniak.lib.login.ApiToken -import com.infomaniak.lib.login.InfomaniakLogin -import com.infomaniak.lib.login.InfomaniakLogin.ErrorStatus -import com.infomaniak.lib.login.InfomaniakLogin.TokenResult -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.invoke - -object LoginUtils { - /** - * Logs the user based on the ActivityResult of our login WebView. - * - * @return a [UserLoginResult] or null if the user canceled the login webview without going through to the end - */ - suspend fun getLoginResultAfterWebView( - result: ActivityResult, - context: Context, - infomaniakLogin: InfomaniakLogin, - credentialManager: CredentialManager, - ): UserLoginResult? { - val authCodeResult = result.toAuthCodeResult(context) - when (authCodeResult) { - is AuthCodeResult.Error -> return UserLoginResult.Failure(authCodeResult.message) - is AuthCodeResult.Canceled -> return null - is AuthCodeResult.Success -> Unit - } - - val tokenResult = infomaniakLogin.getToken(okHttpClient = HttpClient.okHttpClient, code = authCodeResult.code) - when (tokenResult) { - is TokenResult.Error -> { - return UserLoginResult.Failure(context.getUserAuthenticationErrorMessage(tokenResult.errorStatus)) - } - is TokenResult.Success -> Unit - } - - val userResult = authenticateUsers(listOf(tokenResult.apiToken), credentialManager).single() - return when (userResult) { - is UserResult.Failure -> { - UserLoginResult.Failure(context.getString(userResult.apiResponse.translateError())) - } - is UserResult.Success -> UserLoginResult.Success(userResult.user) - } - } - - /** - * Logs the user based on the [ApiToken]s returned by the cross app login logic. - * - * @return a [UserLoginResult] or null if the user canceled the login webview without going through to the end - */ - suspend fun getLoginResultsAfterCrossApp( - apiTokens: List, - context: Context, - credentialManager: CredentialManager, - ): List = buildList { - authenticateUsers(apiTokens, credentialManager).forEach { result -> - when (result) { - is UserResult.Success -> add(UserLoginResult.Success(result.user)) - is UserResult.Failure -> add(UserLoginResult.Failure(context.getString(result.apiResponse.translateError()))) - } - } - } - - private suspend fun authenticateUsers( - apiTokens: List, - credentialManager: CredentialManager, - ): List = apiTokens.map { apiToken -> - runCatching { - authenticateUser(apiToken, credentialManager) - }.getOrDefault(UserResult.Failure.Unknown) - } - - private fun ActivityResult.toAuthCodeResult(context: Context): AuthCodeResult { - if (resultCode != AppCompatActivity.RESULT_OK) return AuthCodeResult.Canceled - - val authCode = data?.getStringExtra(InfomaniakLogin.CODE_TAG) - val translatedError = data?.getStringExtra(InfomaniakLogin.ERROR_TRANSLATED_TAG) - - return when { - translatedError?.isNotBlank() == true -> AuthCodeResult.Error(translatedError) - authCode?.isNotBlank() == true -> AuthCodeResult.Success(authCode) - else -> AuthCodeResult.Error(context.getString(InternalTranslatedErrorCode.UnknownError.translateRes)) - } - } - - private fun getErrorResponse(error: InternalTranslatedErrorCode): ApiResponse { - return ApiResponse(result = ApiResponseStatus.ERROR, error = error.toApiError()) - } - - private suspend fun authenticateUser(apiToken: ApiToken, credentialManager: CredentialManager): UserResult { - if (credentialManager.getUserById(apiToken.userId) != null) return UserResult.Failure( - getErrorResponse(InternalTranslatedErrorCode.UserAlreadyPresent) - ) - - val okhttpClient = HttpClient.okHttpClient.newBuilder().addInterceptor { chain -> - val newRequest = changeAccessToken(chain.request(), apiToken) - chain.proceed(newRequest) - }.build() - - val userProfileResponse = Dispatchers.IO { ApiRepositoryCore.getUserProfile(okhttpClient) } - - if (userProfileResponse.result == ApiResponseStatus.ERROR) return UserResult.Failure(userProfileResponse) - if (userProfileResponse.data == null) return UserResult.Failure.Unknown - - val user = userProfileResponse.data!!.apply { - this.apiToken = apiToken - this.organizations = arrayListOf() +/** + * Logs the user based on the [ApiToken]s returned by the cross app login logic. + * + * @return a [UserLoginResult] or null if the user canceled the login webview without going through to the end + */ +suspend fun LoginUtils.getLoginResultsAfterCrossApp( + apiTokens: List, + context: Context, + credentialManager: CredentialManager, +): List = buildList { + authenticateUsers(apiTokens, credentialManager).forEach { result -> + when (result) { + is UserResult.Success -> add(UserLoginResult.Success(result.user)) + is UserResult.Failure -> add(UserLoginResult.Failure(context.getString(result.apiResponse.translateError()))) } - - return UserResult.Success(user) } } - -private fun Context.getUserAuthenticationErrorMessage(errorStatus: ErrorStatus): String { - return getString( - when (errorStatus) { - ErrorStatus.SERVER -> InternalTranslatedErrorCode.ServerError - ErrorStatus.CONNECTION -> InternalTranslatedErrorCode.ConnectionError - else -> InternalTranslatedErrorCode.UnknownError - }.translateRes - ) -} - -sealed interface UserLoginResult { - data class Success(val user: User) : UserLoginResult - data class Failure(val errorMessage: String) : UserLoginResult -} diff --git a/Login/build.gradle.kts b/Login/build.gradle.kts new file mode 100644 index 000000000..486eac3dd --- /dev/null +++ b/Login/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("com.android.library") + alias(core.plugins.kotlin.android) +} + +val coreCompileSdk: Int by rootProject.extra +val coreMinSdk: Int by rootProject.extra +val javaVersion: JavaVersion by rootProject.extra + +android { + namespace = "com.infomaniak.core.login" + compileSdk = coreCompileSdk + + defaultConfig { + minSdk = coreMinSdk + } + + compileOptions { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + } + + kotlinOptions { + jvmTarget = javaVersion.toString() + } +} + +dependencies { + api(project(":Core:Auth")) // The class CredentialManager is required by the exposed public method + implementation(project(":Core:Network")) + + implementation(core.appcompat) + api(core.okhttp) +} diff --git a/Login/src/main/kotlin/com/infomaniak/core/login/LoginUtils.kt b/Login/src/main/kotlin/com/infomaniak/core/login/LoginUtils.kt new file mode 100644 index 000000000..24989cdd1 --- /dev/null +++ b/Login/src/main/kotlin/com/infomaniak/core/login/LoginUtils.kt @@ -0,0 +1,142 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.core.login + +import android.content.Context +import androidx.activity.result.ActivityResult +import androidx.appcompat.app.AppCompatActivity +import com.infomaniak.core.auth.CredentialManager +import com.infomaniak.core.auth.TokenAuthenticator.Companion.changeAccessToken +import com.infomaniak.core.auth.api.ApiRepositoryCore +import com.infomaniak.core.login.LoginUtils.getLoginResultAfterWebView +import com.infomaniak.core.login.models.AuthCodeResult +import com.infomaniak.core.login.models.UserLoginResult +import com.infomaniak.core.login.models.UserResult +import com.infomaniak.core.network.api.ApiController.toApiError +import com.infomaniak.core.network.api.InternalTranslatedErrorCode +import com.infomaniak.core.network.models.ApiResponse +import com.infomaniak.core.network.models.ApiResponseStatus +import com.infomaniak.core.network.networking.HttpClient +import com.infomaniak.core.network.utils.ApiErrorCode.Companion.translateError +import com.infomaniak.lib.login.ApiToken +import com.infomaniak.lib.login.InfomaniakLogin +import com.infomaniak.lib.login.InfomaniakLogin.ErrorStatus +import com.infomaniak.lib.login.InfomaniakLogin.TokenResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.invoke + +object LoginUtils { + /** + * Logs the user based on the ActivityResult of our login WebView. + * + * @return a [com.infomaniak.core.login.models.UserLoginResult] or null if the user canceled the login webview without going through to the end + */ + suspend fun getLoginResultAfterWebView( + result: ActivityResult, + context: Context, + infomaniakLogin: InfomaniakLogin, + credentialManager: CredentialManager, + ): UserLoginResult? { + val authCodeResult = result.toAuthCodeResult(context) + when (authCodeResult) { + is AuthCodeResult.Error -> return UserLoginResult.Failure(authCodeResult.message) + is AuthCodeResult.Canceled -> return null + is AuthCodeResult.Success -> Unit + } + + val tokenResult = infomaniakLogin.getToken(okHttpClient = HttpClient.okHttpClient, code = authCodeResult.code) + when (tokenResult) { + is TokenResult.Error -> { + return UserLoginResult.Failure(context.getUserAuthenticationErrorMessage(tokenResult.errorStatus)) + } + is TokenResult.Success -> Unit + } + + val userResult = authenticateUsers(listOf(tokenResult.apiToken), credentialManager).single() + return when (userResult) { + is UserResult.Failure -> { + UserLoginResult.Failure(context.getString(userResult.apiResponse.translateError())) + } + is UserResult.Success -> UserLoginResult.Success(userResult.user) + } + } + + /** + * This method is the basis on which [getLoginResultAfterWebView] and its alternative getLoginResultAfterWebView are based on. + * It needs to be public to be used inside of the CrossAppLgin:Login module but there is no reason to call this directly, the + * other two methods make it easier to reuse correctly. + */ + suspend fun authenticateUsers( + apiTokens: List, + credentialManager: CredentialManager, + ): List = apiTokens.map { apiToken -> + runCatching { + authenticateUser(apiToken, credentialManager) + }.getOrDefault(UserResult.Failure.Unknown) + } +} + +private fun ActivityResult.toAuthCodeResult(context: Context): AuthCodeResult { + if (resultCode != AppCompatActivity.RESULT_OK) return AuthCodeResult.Canceled + + val authCode = data?.getStringExtra(InfomaniakLogin.CODE_TAG) + val translatedError = data?.getStringExtra(InfomaniakLogin.ERROR_TRANSLATED_TAG) + + return when { + translatedError?.isNotBlank() == true -> AuthCodeResult.Error(translatedError) + authCode?.isNotBlank() == true -> AuthCodeResult.Success(authCode) + else -> AuthCodeResult.Error(context.getString(InternalTranslatedErrorCode.UnknownError.translateRes)) + } +} + +private fun Context.getUserAuthenticationErrorMessage(errorStatus: ErrorStatus): String { + return getString( + when (errorStatus) { + ErrorStatus.SERVER -> InternalTranslatedErrorCode.ServerError + ErrorStatus.CONNECTION -> InternalTranslatedErrorCode.ConnectionError + else -> InternalTranslatedErrorCode.UnknownError + }.translateRes + ) +} + +private suspend fun authenticateUser(apiToken: ApiToken, credentialManager: CredentialManager): UserResult { + if (credentialManager.getUserById(apiToken.userId) != null) return UserResult.Failure( + getErrorResponse(InternalTranslatedErrorCode.UserAlreadyPresent) + ) + + val okhttpClient = HttpClient.okHttpClient.newBuilder().addInterceptor { chain -> + val newRequest = changeAccessToken(chain.request(), apiToken) + chain.proceed(newRequest) + }.build() + + val userProfileResponse = Dispatchers.IO { ApiRepositoryCore.getUserProfile(okhttpClient) } + + if (userProfileResponse.result == ApiResponseStatus.ERROR) return UserResult.Failure(userProfileResponse) + if (userProfileResponse.data == null) return UserResult.Failure.Unknown + + val user = userProfileResponse.data!!.apply { + this.apiToken = apiToken + this.organizations = arrayListOf() + } + + return UserResult.Success(user) +} + +private fun getErrorResponse(error: InternalTranslatedErrorCode): ApiResponse { + return ApiResponse(result = ApiResponseStatus.ERROR, error = error.toApiError()) +} diff --git a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/AuthCodeResult.kt b/Login/src/main/kotlin/com/infomaniak/core/login/models/AuthCodeResult.kt similarity index 95% rename from CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/AuthCodeResult.kt rename to Login/src/main/kotlin/com/infomaniak/core/login/models/AuthCodeResult.kt index 572e245a4..6314d5c59 100644 --- a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/AuthCodeResult.kt +++ b/Login/src/main/kotlin/com/infomaniak/core/login/models/AuthCodeResult.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.crossapplogin.login +package com.infomaniak.core.login.models internal sealed interface AuthCodeResult { data class Success(val code: String) : AuthCodeResult diff --git a/Login/src/main/kotlin/com/infomaniak/core/login/models/UserLoginResult.kt b/Login/src/main/kotlin/com/infomaniak/core/login/models/UserLoginResult.kt new file mode 100644 index 000000000..90d4c7c28 --- /dev/null +++ b/Login/src/main/kotlin/com/infomaniak/core/login/models/UserLoginResult.kt @@ -0,0 +1,25 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package com.infomaniak.core.login.models + +import com.infomaniak.core.auth.models.user.User + +sealed interface UserLoginResult { + data class Success(val user: User) : UserLoginResult + data class Failure(val errorMessage: String) : UserLoginResult +} diff --git a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/UserResult.kt b/Login/src/main/kotlin/com/infomaniak/core/login/models/UserResult.kt similarity index 94% rename from CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/UserResult.kt rename to Login/src/main/kotlin/com/infomaniak/core/login/models/UserResult.kt index 964cb9112..2a4694a87 100644 --- a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/UserResult.kt +++ b/Login/src/main/kotlin/com/infomaniak/core/login/models/UserResult.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.crossapplogin.login +package com.infomaniak.core.login.models import com.infomaniak.core.auth.models.user.User import com.infomaniak.core.network.api.ApiController.toApiError @@ -23,7 +23,7 @@ import com.infomaniak.core.network.api.InternalTranslatedErrorCode import com.infomaniak.core.network.models.ApiResponse import com.infomaniak.core.network.models.ApiResponseStatus -internal sealed interface UserResult { +sealed interface UserResult { data class Success(val user: User) : UserResult open class Failure(val apiResponse: ApiResponse<*>) : UserResult { data object Unknown : Failure( From e6f485fa834042828456eccf7a0940470be07827 Mon Sep 17 00:00:00 2001 From: Gibran Chevalley Date: Wed, 26 Nov 2025 14:10:39 +0100 Subject: [PATCH 4/9] refactor: Move login logic to Auth module and remove other modules --- .../infomaniak/core/auth/utils}/LoginUtils.kt | 30 ++++++++++--- .../core/auth/utils}/models/AuthCodeResult.kt | 2 +- .../auth/utils}/models/UserLoginResult.kt | 2 +- .../core/auth/utils}/models/UserResult.kt | 2 +- CrossAppLogin/Login/build.gradle.kts | 32 -------------- .../core/crossapplogin/login/LoginUtils.kt | 44 ------------------- Login/build.gradle.kts | 34 -------------- 7 files changed, 27 insertions(+), 119 deletions(-) rename {Login/src/main/kotlin/com/infomaniak/core/login => Auth/src/main/kotlin/com/infomaniak/core/auth/utils}/LoginUtils.kt (83%) rename {Login/src/main/kotlin/com/infomaniak/core/login => Auth/src/main/kotlin/com/infomaniak/core/auth/utils}/models/AuthCodeResult.kt (95%) rename {Login/src/main/kotlin/com/infomaniak/core/login => Auth/src/main/kotlin/com/infomaniak/core/auth/utils}/models/UserLoginResult.kt (95%) rename {Login/src/main/kotlin/com/infomaniak/core/login => Auth/src/main/kotlin/com/infomaniak/core/auth/utils}/models/UserResult.kt (96%) delete mode 100644 CrossAppLogin/Login/build.gradle.kts delete mode 100644 CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt delete mode 100644 Login/build.gradle.kts diff --git a/Login/src/main/kotlin/com/infomaniak/core/login/LoginUtils.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt similarity index 83% rename from Login/src/main/kotlin/com/infomaniak/core/login/LoginUtils.kt rename to Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt index 24989cdd1..70ed4fc30 100644 --- a/Login/src/main/kotlin/com/infomaniak/core/login/LoginUtils.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.login +package com.infomaniak.core.auth.utils import android.content.Context import androidx.activity.result.ActivityResult @@ -23,10 +23,10 @@ import androidx.appcompat.app.AppCompatActivity import com.infomaniak.core.auth.CredentialManager import com.infomaniak.core.auth.TokenAuthenticator.Companion.changeAccessToken import com.infomaniak.core.auth.api.ApiRepositoryCore -import com.infomaniak.core.login.LoginUtils.getLoginResultAfterWebView -import com.infomaniak.core.login.models.AuthCodeResult -import com.infomaniak.core.login.models.UserLoginResult -import com.infomaniak.core.login.models.UserResult +import com.infomaniak.core.auth.utils.LoginUtils.getLoginResultAfterWebView +import com.infomaniak.core.auth.utils.models.AuthCodeResult +import com.infomaniak.core.auth.utils.models.UserLoginResult +import com.infomaniak.core.auth.utils.models.UserResult import com.infomaniak.core.network.api.ApiController.toApiError import com.infomaniak.core.network.api.InternalTranslatedErrorCode import com.infomaniak.core.network.models.ApiResponse @@ -44,7 +44,7 @@ object LoginUtils { /** * Logs the user based on the ActivityResult of our login WebView. * - * @return a [com.infomaniak.core.login.models.UserLoginResult] or null if the user canceled the login webview without going through to the end + * @return a [UserLoginResult] or null if the user canceled the login webview without going through to the end */ suspend fun getLoginResultAfterWebView( result: ActivityResult, @@ -76,6 +76,24 @@ object LoginUtils { } } + /** + * Logs the user based on the [ApiToken]s returned by the cross app login logic. + * + * @return a [UserLoginResult] or null if the user canceled the login webview without going through to the end + */ + suspend fun getLoginResultsAfterCrossApp( + apiTokens: List, + context: Context, + credentialManager: CredentialManager, + ): List = buildList { + authenticateUsers(apiTokens, credentialManager).forEach { result -> + when (result) { + is UserResult.Success -> add(UserLoginResult.Success(result.user)) + is UserResult.Failure -> add(UserLoginResult.Failure(context.getString(result.apiResponse.translateError()))) + } + } + } + /** * This method is the basis on which [getLoginResultAfterWebView] and its alternative getLoginResultAfterWebView are based on. * It needs to be public to be used inside of the CrossAppLgin:Login module but there is no reason to call this directly, the diff --git a/Login/src/main/kotlin/com/infomaniak/core/login/models/AuthCodeResult.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/AuthCodeResult.kt similarity index 95% rename from Login/src/main/kotlin/com/infomaniak/core/login/models/AuthCodeResult.kt rename to Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/AuthCodeResult.kt index 6314d5c59..c924758f6 100644 --- a/Login/src/main/kotlin/com/infomaniak/core/login/models/AuthCodeResult.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/AuthCodeResult.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.login.models +package com.infomaniak.core.auth.utils.models internal sealed interface AuthCodeResult { data class Success(val code: String) : AuthCodeResult diff --git a/Login/src/main/kotlin/com/infomaniak/core/login/models/UserLoginResult.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserLoginResult.kt similarity index 95% rename from Login/src/main/kotlin/com/infomaniak/core/login/models/UserLoginResult.kt rename to Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserLoginResult.kt index 90d4c7c28..a1d02ca73 100644 --- a/Login/src/main/kotlin/com/infomaniak/core/login/models/UserLoginResult.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserLoginResult.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.login.models +package com.infomaniak.core.auth.utils.models import com.infomaniak.core.auth.models.user.User diff --git a/Login/src/main/kotlin/com/infomaniak/core/login/models/UserResult.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserResult.kt similarity index 96% rename from Login/src/main/kotlin/com/infomaniak/core/login/models/UserResult.kt rename to Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserResult.kt index 2a4694a87..7115f8782 100644 --- a/Login/src/main/kotlin/com/infomaniak/core/login/models/UserResult.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserResult.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.login.models +package com.infomaniak.core.auth.utils.models import com.infomaniak.core.auth.models.user.User import com.infomaniak.core.network.api.ApiController.toApiError diff --git a/CrossAppLogin/Login/build.gradle.kts b/CrossAppLogin/Login/build.gradle.kts deleted file mode 100644 index 21cb7a466..000000000 --- a/CrossAppLogin/Login/build.gradle.kts +++ /dev/null @@ -1,32 +0,0 @@ -plugins { - id("com.android.library") - alias(core.plugins.kotlin.android) -} - -val coreCompileSdk: Int by rootProject.extra -val coreMinSdk: Int by rootProject.extra -val javaVersion: JavaVersion by rootProject.extra - -android { - namespace = "com.infomaniak.core.crossapplogin.login" - compileSdk = coreCompileSdk - - defaultConfig { - minSdk = coreMinSdk - } - - compileOptions { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - } - - kotlinOptions { - jvmTarget = javaVersion.toString() - } -} - -dependencies { - api(project(":Core:Login")) - implementation(project(":Core:Auth")) - implementation(project(":Core:Network")) -} diff --git a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt b/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt deleted file mode 100644 index 7f36e4fb9..000000000 --- a/CrossAppLogin/Login/src/main/kotlin/com/infomaniak/core/crossapplogin/login/LoginUtils.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2025 Infomaniak Network SA - * - * This program 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. - * - * This program 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 this program. If not, see . - */ -package com.infomaniak.core.crossapplogin.login - -import android.content.Context -import com.infomaniak.core.auth.CredentialManager -import com.infomaniak.core.login.LoginUtils -import com.infomaniak.core.login.models.UserLoginResult -import com.infomaniak.core.login.models.UserResult -import com.infomaniak.core.network.utils.ApiErrorCode.Companion.translateError -import com.infomaniak.lib.login.ApiToken - -/** - * Logs the user based on the [ApiToken]s returned by the cross app login logic. - * - * @return a [UserLoginResult] or null if the user canceled the login webview without going through to the end - */ -suspend fun LoginUtils.getLoginResultsAfterCrossApp( - apiTokens: List, - context: Context, - credentialManager: CredentialManager, -): List = buildList { - authenticateUsers(apiTokens, credentialManager).forEach { result -> - when (result) { - is UserResult.Success -> add(UserLoginResult.Success(result.user)) - is UserResult.Failure -> add(UserLoginResult.Failure(context.getString(result.apiResponse.translateError()))) - } - } -} diff --git a/Login/build.gradle.kts b/Login/build.gradle.kts deleted file mode 100644 index 486eac3dd..000000000 --- a/Login/build.gradle.kts +++ /dev/null @@ -1,34 +0,0 @@ -plugins { - id("com.android.library") - alias(core.plugins.kotlin.android) -} - -val coreCompileSdk: Int by rootProject.extra -val coreMinSdk: Int by rootProject.extra -val javaVersion: JavaVersion by rootProject.extra - -android { - namespace = "com.infomaniak.core.login" - compileSdk = coreCompileSdk - - defaultConfig { - minSdk = coreMinSdk - } - - compileOptions { - sourceCompatibility = javaVersion - targetCompatibility = javaVersion - } - - kotlinOptions { - jvmTarget = javaVersion.toString() - } -} - -dependencies { - api(project(":Core:Auth")) // The class CredentialManager is required by the exposed public method - implementation(project(":Core:Network")) - - implementation(core.appcompat) - api(core.okhttp) -} From e73fb726c4ec1680dbed9a6dfb2fccdfa4ca2a7d Mon Sep 17 00:00:00 2001 From: Gibran Chevalley Date: Wed, 26 Nov 2025 15:07:13 +0100 Subject: [PATCH 5/9] refactor: Clean LoginUtils --- .../infomaniak/core/auth/utils/LoginUtils.kt | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt index 70ed4fc30..e9509d94b 100644 --- a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt @@ -24,6 +24,7 @@ import com.infomaniak.core.auth.CredentialManager import com.infomaniak.core.auth.TokenAuthenticator.Companion.changeAccessToken import com.infomaniak.core.auth.api.ApiRepositoryCore import com.infomaniak.core.auth.utils.LoginUtils.getLoginResultAfterWebView +import com.infomaniak.core.auth.utils.LoginUtils.getLoginResultsAfterCrossApp import com.infomaniak.core.auth.utils.models.AuthCodeResult import com.infomaniak.core.auth.utils.models.UserLoginResult import com.infomaniak.core.auth.utils.models.UserResult @@ -37,8 +38,6 @@ import com.infomaniak.lib.login.ApiToken import com.infomaniak.lib.login.InfomaniakLogin import com.infomaniak.lib.login.InfomaniakLogin.ErrorStatus import com.infomaniak.lib.login.InfomaniakLogin.TokenResult -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.invoke object LoginUtils { /** @@ -61,13 +60,11 @@ object LoginUtils { val tokenResult = infomaniakLogin.getToken(okHttpClient = HttpClient.okHttpClient, code = authCodeResult.code) when (tokenResult) { - is TokenResult.Error -> { - return UserLoginResult.Failure(context.getUserAuthenticationErrorMessage(tokenResult.errorStatus)) - } + is TokenResult.Error -> return UserLoginResult.Failure(context.formatAuthErrorMessage(tokenResult.errorStatus)) is TokenResult.Success -> Unit } - val userResult = authenticateUsers(listOf(tokenResult.apiToken), credentialManager).single() + val userResult = getUserByToken(listOf(tokenResult.apiToken), credentialManager).single() return when (userResult) { is UserResult.Failure -> { UserLoginResult.Failure(context.getString(userResult.apiResponse.translateError())) @@ -85,21 +82,19 @@ object LoginUtils { apiTokens: List, context: Context, credentialManager: CredentialManager, - ): List = buildList { - authenticateUsers(apiTokens, credentialManager).forEach { result -> - when (result) { - is UserResult.Success -> add(UserLoginResult.Success(result.user)) - is UserResult.Failure -> add(UserLoginResult.Failure(context.getString(result.apiResponse.translateError()))) - } + ): List = getUserByToken(apiTokens, credentialManager).map { result -> + when (result) { + is UserResult.Success -> UserLoginResult.Success(result.user) + is UserResult.Failure -> UserLoginResult.Failure(context.getString(result.apiResponse.translateError())) } } /** - * This method is the basis on which [getLoginResultAfterWebView] and its alternative getLoginResultAfterWebView are based on. - * It needs to be public to be used inside of the CrossAppLgin:Login module but there is no reason to call this directly, the - * other two methods make it easier to reuse correctly. + * This method is the basis on which [getLoginResultAfterWebView] and its alternative [getLoginResultsAfterCrossApp] are built + * upon. It needs to be public to be used inside of the CrossAppLgin:Login module but there is no reason to call this + * directly, the other two methods make it easier to reuse correctly. */ - suspend fun authenticateUsers( + private suspend fun getUserByToken( apiTokens: List, credentialManager: CredentialManager, ): List = apiTokens.map { apiToken -> @@ -122,7 +117,7 @@ private fun ActivityResult.toAuthCodeResult(context: Context): AuthCodeResult { } } -private fun Context.getUserAuthenticationErrorMessage(errorStatus: ErrorStatus): String { +private fun Context.formatAuthErrorMessage(errorStatus: ErrorStatus): String { return getString( when (errorStatus) { ErrorStatus.SERVER -> InternalTranslatedErrorCode.ServerError @@ -142,12 +137,12 @@ private suspend fun authenticateUser(apiToken: ApiToken, credentialManager: Cred chain.proceed(newRequest) }.build() - val userProfileResponse = Dispatchers.IO { ApiRepositoryCore.getUserProfile(okhttpClient) } + val userProfileResponse = ApiRepositoryCore.getUserProfile(okhttpClient) if (userProfileResponse.result == ApiResponseStatus.ERROR) return UserResult.Failure(userProfileResponse) - if (userProfileResponse.data == null) return UserResult.Failure.Unknown + val userData = userProfileResponse.data ?: return UserResult.Failure.Unknown - val user = userProfileResponse.data!!.apply { + val user = userData.apply { this.apiToken = apiToken this.organizations = arrayListOf() } From ebf529fe26dc40e0b724c88f21172d0f4bedd124 Mon Sep 17 00:00:00 2001 From: Gibran Chevalley Date: Thu, 27 Nov 2025 15:51:05 +0100 Subject: [PATCH 6/9] refactor: Adapt code now that getUsersByToken is private --- .../infomaniak/core/auth/utils/LoginUtils.kt | 28 +++++++++---------- .../core/auth/utils/models/UserResult.kt | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt index e9509d94b..60e97580f 100644 --- a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt @@ -64,7 +64,7 @@ object LoginUtils { is TokenResult.Success -> Unit } - val userResult = getUserByToken(listOf(tokenResult.apiToken), credentialManager).single() + val userResult = getUsersByToken(listOf(tokenResult.apiToken), credentialManager).single() return when (userResult) { is UserResult.Failure -> { UserLoginResult.Failure(context.getString(userResult.apiResponse.translateError())) @@ -82,26 +82,24 @@ object LoginUtils { apiTokens: List, context: Context, credentialManager: CredentialManager, - ): List = getUserByToken(apiTokens, credentialManager).map { result -> + ): List = getUsersByToken(apiTokens, credentialManager).map { result -> when (result) { is UserResult.Success -> UserLoginResult.Success(result.user) is UserResult.Failure -> UserLoginResult.Failure(context.getString(result.apiResponse.translateError())) } } +} - /** - * This method is the basis on which [getLoginResultAfterWebView] and its alternative [getLoginResultsAfterCrossApp] are built - * upon. It needs to be public to be used inside of the CrossAppLgin:Login module but there is no reason to call this - * directly, the other two methods make it easier to reuse correctly. - */ - private suspend fun getUserByToken( - apiTokens: List, - credentialManager: CredentialManager, - ): List = apiTokens.map { apiToken -> - runCatching { - authenticateUser(apiToken, credentialManager) - }.getOrDefault(UserResult.Failure.Unknown) - } +/** + * This method is the basis on which [getLoginResultAfterWebView] and its alternative [getLoginResultsAfterCrossApp] are built. + */ +private suspend fun getUsersByToken( + apiTokens: List, + credentialManager: CredentialManager, +): List = apiTokens.map { apiToken -> + runCatching { + authenticateUser(apiToken, credentialManager) + }.getOrDefault(UserResult.Failure.Unknown) } private fun ActivityResult.toAuthCodeResult(context: Context): AuthCodeResult { diff --git a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserResult.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserResult.kt index 7115f8782..8c3f9a5e4 100644 --- a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserResult.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserResult.kt @@ -23,7 +23,7 @@ import com.infomaniak.core.network.api.InternalTranslatedErrorCode import com.infomaniak.core.network.models.ApiResponse import com.infomaniak.core.network.models.ApiResponseStatus -sealed interface UserResult { +internal sealed interface UserResult { data class Success(val user: User) : UserResult open class Failure(val apiResponse: ApiResponse<*>) : UserResult { data object Unknown : Failure( From a17dbd75b2f3991aaec4160ead7567096c4642c8 Mon Sep 17 00:00:00 2001 From: Gibran Chevalley Date: Thu, 27 Nov 2025 15:55:08 +0100 Subject: [PATCH 7/9] feat: Rethrow cancelation exceptions --- Auth/build.gradle.kts | 1 + .../main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Auth/build.gradle.kts b/Auth/build.gradle.kts index 4ed24df4d..1e4e96cfb 100644 --- a/Auth/build.gradle.kts +++ b/Auth/build.gradle.kts @@ -45,6 +45,7 @@ android { } dependencies { + implementation(project(":Core")) implementation(project(":Core:Network")) implementation(project(":Core:Sentry")) diff --git a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt index 60e97580f..780d3f961 100644 --- a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt @@ -28,6 +28,7 @@ import com.infomaniak.core.auth.utils.LoginUtils.getLoginResultsAfterCrossApp import com.infomaniak.core.auth.utils.models.AuthCodeResult import com.infomaniak.core.auth.utils.models.UserLoginResult import com.infomaniak.core.auth.utils.models.UserResult +import com.infomaniak.core.cancellable import com.infomaniak.core.network.api.ApiController.toApiError import com.infomaniak.core.network.api.InternalTranslatedErrorCode import com.infomaniak.core.network.models.ApiResponse @@ -99,7 +100,7 @@ private suspend fun getUsersByToken( ): List = apiTokens.map { apiToken -> runCatching { authenticateUser(apiToken, credentialManager) - }.getOrDefault(UserResult.Failure.Unknown) + }.cancellable().getOrDefault(UserResult.Failure.Unknown) } private fun ActivityResult.toAuthCodeResult(context: Context): AuthCodeResult { From 6706f3f428f626ececfc66df965ed8d33f60c002 Mon Sep 17 00:00:00 2001 From: Gibran Chevalley Date: Thu, 27 Nov 2025 16:06:55 +0100 Subject: [PATCH 8/9] refactor: Access the new static ApiRepositoryCore.getUserProfile() method --- .../core/crossapplogin/back/BaseCrossAppLoginViewModel.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CrossAppLogin/Back/src/main/kotlin/com/infomaniak/core/crossapplogin/back/BaseCrossAppLoginViewModel.kt b/CrossAppLogin/Back/src/main/kotlin/com/infomaniak/core/crossapplogin/back/BaseCrossAppLoginViewModel.kt index 140d02a17..e890e882b 100644 --- a/CrossAppLogin/Back/src/main/kotlin/com/infomaniak/core/crossapplogin/back/BaseCrossAppLoginViewModel.kt +++ b/CrossAppLogin/Back/src/main/kotlin/com/infomaniak/core/crossapplogin/back/BaseCrossAppLoginViewModel.kt @@ -98,8 +98,6 @@ abstract class BaseCrossAppLoginViewModel(applicationId: String, clientId: Strin createElement = { account -> async { getFirstValidTokenOrError(account) } }, ) - private val apiRepository = object : ApiRepositoryCore() {} - private val baseOkHttpClient by lazy { OkHttpClient.Builder().apply { addCache() @@ -191,7 +189,7 @@ abstract class BaseCrossAppLoginViewModel(applicationId: String, clientId: Strin val customTokenHttpClient = baseOkHttpClient.addInterceptor(CustomTokenInterceptor(token)).build() - when (apiRepository.getUserProfile(customTokenHttpClient).data) { + when (ApiRepositoryCore.getUserProfile(customTokenHttpClient).data) { is User -> { // Put the just checked token first. Will speed up later derivation attempts. val tokens = setOf(token) + account.tokens From 5ddec9ad6d4b5d48427607918e83d693833c3cb5 Mon Sep 17 00:00:00 2001 From: Gibran Chevalley Date: Fri, 28 Nov 2025 15:51:44 +0100 Subject: [PATCH 9/9] refactor: Move models to their correct package --- .../core/auth/{utils => }/models/AuthCodeResult.kt | 2 +- .../core/auth/{utils => }/models/UserLoginResult.kt | 2 +- .../infomaniak/core/auth/{utils => }/models/UserResult.kt | 2 +- .../kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) rename Auth/src/main/kotlin/com/infomaniak/core/auth/{utils => }/models/AuthCodeResult.kt (95%) rename Auth/src/main/kotlin/com/infomaniak/core/auth/{utils => }/models/UserLoginResult.kt (95%) rename Auth/src/main/kotlin/com/infomaniak/core/auth/{utils => }/models/UserResult.kt (96%) diff --git a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/AuthCodeResult.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/models/AuthCodeResult.kt similarity index 95% rename from Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/AuthCodeResult.kt rename to Auth/src/main/kotlin/com/infomaniak/core/auth/models/AuthCodeResult.kt index c924758f6..8b8b324c1 100644 --- a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/AuthCodeResult.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/models/AuthCodeResult.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.auth.utils.models +package com.infomaniak.core.auth.models internal sealed interface AuthCodeResult { data class Success(val code: String) : AuthCodeResult diff --git a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserLoginResult.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/models/UserLoginResult.kt similarity index 95% rename from Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserLoginResult.kt rename to Auth/src/main/kotlin/com/infomaniak/core/auth/models/UserLoginResult.kt index a1d02ca73..59d75cbd7 100644 --- a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserLoginResult.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/models/UserLoginResult.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.auth.utils.models +package com.infomaniak.core.auth.models import com.infomaniak.core.auth.models.user.User diff --git a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserResult.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/models/UserResult.kt similarity index 96% rename from Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserResult.kt rename to Auth/src/main/kotlin/com/infomaniak/core/auth/models/UserResult.kt index 8c3f9a5e4..a9f29390d 100644 --- a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/models/UserResult.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/models/UserResult.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.auth.utils.models +package com.infomaniak.core.auth.models import com.infomaniak.core.auth.models.user.User import com.infomaniak.core.network.api.ApiController.toApiError diff --git a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt index 780d3f961..0be8ea8ae 100644 --- a/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt +++ b/Auth/src/main/kotlin/com/infomaniak/core/auth/utils/LoginUtils.kt @@ -23,11 +23,11 @@ import androidx.appcompat.app.AppCompatActivity import com.infomaniak.core.auth.CredentialManager import com.infomaniak.core.auth.TokenAuthenticator.Companion.changeAccessToken import com.infomaniak.core.auth.api.ApiRepositoryCore +import com.infomaniak.core.auth.models.AuthCodeResult +import com.infomaniak.core.auth.models.UserLoginResult +import com.infomaniak.core.auth.models.UserResult import com.infomaniak.core.auth.utils.LoginUtils.getLoginResultAfterWebView import com.infomaniak.core.auth.utils.LoginUtils.getLoginResultsAfterCrossApp -import com.infomaniak.core.auth.utils.models.AuthCodeResult -import com.infomaniak.core.auth.utils.models.UserLoginResult -import com.infomaniak.core.auth.utils.models.UserResult import com.infomaniak.core.cancellable import com.infomaniak.core.network.api.ApiController.toApiError import com.infomaniak.core.network.api.InternalTranslatedErrorCode