From 4e57079dc4ae74e0ff0f8c7ba04cbdb799030301 Mon Sep 17 00:00:00 2001 From: Isabel Martin Date: Tue, 5 Mar 2024 19:33:32 +0100 Subject: [PATCH] [no-jira]: unify login screens, two viewModels. [PROPOSAL] (#1962) --- app/src/main/AndroidManifest.xml | 8 +- .../ui/activities/LoginToutActivity.kt | 78 ++++++++++++- .../ui/activities/OAuthActivity.kt | 103 ------------------ .../kickstarter/ui/extensions/ActivityExt.kt | 7 -- .../kickstarter/viewmodels/OAuthViewModel.kt | 21 ++-- ...ViewModelTest.kt => OAuthViewModelTest.kt} | 35 +++++- 6 files changed, 120 insertions(+), 132 deletions(-) delete mode 100644 app/src/main/java/com/kickstarter/ui/activities/OAuthActivity.kt rename app/src/test/java/com/kickstarter/viewmodels/{OAuthActivityViewModelTest.kt => OAuthViewModelTest.kt} (85%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3865a8549f..4e2915aefd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -146,7 +146,9 @@ android:windowSoftInputMode="adjustResize" android:theme="@style/Login" /> @@ -158,10 +160,6 @@ - environment = env viewModelFactory = LoginToutViewModel.Factory(env) + oAuthViewModelFactory = OAuthViewModelFactory(environment = env) this.ksString = requireNotNull(env.ksString()) darkModeEnabled = env.featureFlagClient()?.getBoolean(FlagKey.ANDROID_DARK_MODE_ENABLED) ?: false @@ -99,12 +117,26 @@ class LoginToutActivity : ComponentActivity() { }, featureFlagState = oauthFlagEnabled, onSignUpOrLogInClicked = { - this@LoginToutActivity.startOauthActivity() + oAuthViewModel.produceState(intent = intent) } ) } } + logInAndSignUpAndLoginWithFacebookVM() + + if (oauthFlagEnabled) { + setUpOAuthViewModel() + } + } + + /*** + * Handles the the viewModel RXJava subscriptions for the user cases: + * - LogIn non OAuth + * - SignUp non OAuth + * - LogIn with Facebook + */ + private fun logInAndSignUpAndLoginWithFacebookVM() { val loginReason = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getSerializableExtra(IntentKey.LOGIN_REASON, LoginReason::class.java) } else { @@ -210,6 +242,46 @@ class LoginToutActivity : ComponentActivity() { .addToDisposable(disposables) } + override fun onDestroy() { + Timber.d("$oAuthLogcat onDestroy") + super.onDestroy() + } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + if (oauthFlagEnabled) { + Timber.d("$oAuthLogcat onNewIntent Intent: $intent, data: ${intent?.data}") + // - Intent generated when the deepLink redirection takes place + intent?.let { oAuthViewModel.produceState(intent = it) } + } + } + + private fun setUpOAuthViewModel() { + lifecycleScope.launch { + oAuthViewModel.uiState.collect { state -> + // - Intent generated with onCreate + if (state.isAuthorizationStep && state.authorizationUrl.isNotEmpty()) { + openChromeTabWithUrl(state.authorizationUrl) + } + + if (state.user.isNotNull()) { + setResult(RESULT_OK) + this@LoginToutActivity.finish() + } + } + } + } + + private fun openChromeTabWithUrl(url: String) { + val authorizationUri = Uri.parse(url) + + val tabIntent = CustomTabsIntent.Builder().build() + tabIntent.intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY + + val packageName = ChromeTabsHelper.getPackageNameToUse(this) + tabIntent.intent.setPackage(packageName) + tabIntent.launchUrl(this, authorizationUri) + } + private fun facebookLoginClick() = viewModel.inputs.facebookLoginClick( this, diff --git a/app/src/main/java/com/kickstarter/ui/activities/OAuthActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/OAuthActivity.kt deleted file mode 100644 index 14ce8d9a8b..0000000000 --- a/app/src/main/java/com/kickstarter/ui/activities/OAuthActivity.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.kickstarter.ui.activities - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.browser.customtabs.CustomTabsIntent -import androidx.lifecycle.lifecycleScope -import com.kickstarter.libs.utils.TransitionUtils -import com.kickstarter.libs.utils.extensions.getEnvironment -import com.kickstarter.libs.utils.extensions.isNotNull -import com.kickstarter.models.chrome.ChromeTabsHelperActivity -import com.kickstarter.ui.IntentKey -import com.kickstarter.ui.extensions.setUpConnectivityStatusCheck -import com.kickstarter.viewmodels.OAuthViewModel -import com.kickstarter.viewmodels.OAuthViewModelFactory -import kotlinx.coroutines.launch -import timber.log.Timber - -class OAuthActivity : AppCompatActivity() { - - private lateinit var helper: ChromeTabsHelperActivity.CustomTabSessionAndClientHelper - private lateinit var viewModelFactory: OAuthViewModelFactory - private val viewModel: OAuthViewModel by viewModels { - viewModelFactory - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setUpConnectivityStatusCheck(lifecycle) - - Timber.d("OAuthActivity: onCreate Intent: $intent, onCreate data: ${intent.data}") - - this.getEnvironment()?.let { env -> - viewModelFactory = OAuthViewModelFactory(environment = env) - } - - viewModel.produceState(intent = intent) - - lifecycleScope.launch { - - viewModel.uiState.collect { state -> - // - Intent generated with onCreate - if (state.isAuthorizationStep && state.authorizationUrl.isNotEmpty()) { - openChromeTabWithUrl(state.authorizationUrl) - } - - if (state.user.isNotNull()) { - setResult(RESULT_OK) - this@OAuthActivity.finish() - } - } - } - } - - override fun onDestroy() { - Timber.d("OAuthActivity: onDestroy") - super.onDestroy() - } - override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) - Timber.d("OAuthActivity: onNewIntent Intent: $intent, data: ${intent?.data}") - // - Intent generated when the deepLink redirection takes place - intent?.let { viewModel.produceState(intent = it) } - } - - private fun openChromeTabWithUrl(url: String) { - val authorizationUri = Uri.parse(url) - - // BindCustomTabsService, obtain CustomTabsClient and Client, listens to navigation events - helper = ChromeTabsHelperActivity.CustomTabSessionAndClientHelper(this, authorizationUri) { - // finish() - } - - // - Fallback in case Chrome is not installed, open WebViewActivity - val fallback = object : ChromeTabsHelperActivity.CustomTabFallback { - override fun openUri(activity: Activity, uri: Uri) { - val intent: Intent = Intent(activity, WebViewActivity::class.java) - .putExtra(IntentKey.URL, uri.toString()) - - activity.startActivity(intent) - TransitionUtils.slideInFromRight() - } - } - - lifecycleScope.launch { - // - Once the session is ready and client warmed-up load the url - helper.isSessionReady().collect { ready -> - val tabIntent = CustomTabsIntent.Builder(helper.getSession()).build() - tabIntent.intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY - // tabIntent.intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP) - ChromeTabsHelperActivity.openCustomTab( - this@OAuthActivity, - tabIntent, - authorizationUri, - fallback - ) - } - } - } -} diff --git a/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt b/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt index 2fb280e9c8..adf9c08ef0 100644 --- a/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt +++ b/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt @@ -36,7 +36,6 @@ import com.kickstarter.ui.IntentKey import com.kickstarter.ui.activities.DisclaimerItems import com.kickstarter.ui.activities.HelpActivity import com.kickstarter.ui.activities.LoginToutActivity -import com.kickstarter.ui.activities.OAuthActivity import com.kickstarter.ui.activities.SignupActivity import com.kickstarter.ui.data.PledgeData import com.kickstarter.ui.data.PledgeReason @@ -234,12 +233,6 @@ fun Activity.startPreLaunchProjectActivity(project: Project, previousScreen: Str TransitionUtils.transition(this, TransitionUtils.slideInFromRight()) } -fun Activity.startOauthActivity() { - val intent = Intent().setClass(this, OAuthActivity::class.java) - startActivityForResult(intent, ActivityRequestCodes.LOGIN_FLOW) - TransitionUtils.transition(this, TransitionUtils.slideInFromRight()) -} - fun Activity.startLogin() { val intent = Intent().getLoginActivityIntent(this) startActivityForResult(intent, ActivityRequestCodes.LOGIN_FLOW) diff --git a/app/src/main/java/com/kickstarter/viewmodels/OAuthViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/OAuthViewModel.kt index 31c09a8cea..41eadf21ce 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/OAuthViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/OAuthViewModel.kt @@ -44,6 +44,7 @@ class OAuthViewModel( private val verifier: PKCE ) : ViewModel() { + private val logcat = "Oauth :" private val hostEndpoint = environment.webEndpoint() private val loginUseCase = LoginUseCase(environment) private val apiClient = requireNotNull(environment.apiClientV2()) @@ -52,10 +53,9 @@ class OAuthViewModel( WebEndpoint.STAGING -> Secrets.Api.Client.STAGING else -> "" } - private val codeVerifier = verifier.generateRandomCodeVerifier(entropy = CodeVerifier.MIN_CODE_VERIFIER_ENTROPY) private var mutableUIState = MutableStateFlow(OAuthUiState()) - + lateinit var codeVerifier: String val uiState: StateFlow get() = mutableUIState.asStateFlow() .stateIn( @@ -73,11 +73,11 @@ class OAuthViewModel( val code = uri.getQueryParameter("code") if (scheme == REDIRECT_URI_SCHEMA && host == REDIRECT_URI_HOST && !code.isNullOrBlank()) { - Timber.d("retrieve token after redirectionDeeplink: $code") + Timber.d("$logcat retrieve token after redirectionDeeplink: $code") apiClient.loginWithCodes(codeVerifier, code, clientID) .asFlow() .flatMapLatest { token -> - Timber.d("About to persist token to currentUser: $token") + Timber.d("$logcat About to persist token to currentUser: $token") loginUseCase.setToken(token.accessToken()) apiClient.fetchCurrentUser() .asFlow() @@ -86,7 +86,7 @@ class OAuthViewModel( } } .catch { - Timber.e("error while getting the token or user: $it") + Timber.e("$logcat error while getting the token or user: ${processThrowable(it)}") mutableUIState.emit( OAuthUiState( error = processThrowable(it), @@ -96,7 +96,7 @@ class OAuthViewModel( loginUseCase.logout() } .collect { user -> - Timber.d("About to persist user to currentUser: $user") + Timber.d("$logcat About to persist user to currentUser: $user") loginUseCase.setUser(user) mutableUIState.emit( OAuthUiState( @@ -107,7 +107,7 @@ class OAuthViewModel( } if (scheme == REDIRECT_URI_SCHEMA && host == REDIRECT_URI_HOST && code.isNullOrBlank()) { - val error = "No code after redirection" + val error = "$logcat No code after redirection" Timber.e(error) mutableUIState.emit( OAuthUiState( @@ -118,8 +118,9 @@ class OAuthViewModel( } if (intent.data == null) { + codeVerifier = verifier.generateRandomCodeVerifier(entropy = CodeVerifier.MIN_CODE_VERIFIER_ENTROPY) val url = generateAuthorizationUrlWithParams() - Timber.d("isAuthorizationStep $url") + Timber.d("$logcat isAuthorizationStep $url and codeVerifier: $codeVerifier") mutableUIState.emit( OAuthUiState( authorizationUrl = url, @@ -134,7 +135,9 @@ class OAuthViewModel( if (!throwable.message.isNullOrBlank()) return throwable.message ?: "" if (throwable is ApiException) { - return throwable.errorEnvelope().errorMessages().toString() + val apiError = throwable.errorEnvelope()?.errorMessages()?.toString() ?: "" + val genericError = throwable.response().message() + return "$genericError / $apiError" } return "error while getting the token or user" diff --git a/app/src/test/java/com/kickstarter/viewmodels/OAuthActivityViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/OAuthViewModelTest.kt similarity index 85% rename from app/src/test/java/com/kickstarter/viewmodels/OAuthActivityViewModelTest.kt rename to app/src/test/java/com/kickstarter/viewmodels/OAuthViewModelTest.kt index d8a914e512..577b8282cc 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/OAuthActivityViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/OAuthViewModelTest.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test -class OAuthActivityViewModelTest : KSRobolectricTestCase() { +class OAuthViewModelTest : KSRobolectricTestCase() { private lateinit var vm: OAuthViewModel private fun setUpEnvironment(environment: Environment, mockCodeVerifier: PKCE) { @@ -149,21 +149,34 @@ class OAuthActivityViewModelTest : KSRobolectricTestCase() { .currentUserV2(currentUserV2) .build() - setUpEnvironment(environment, CodeVerifier()) + val mockCodeVerifier = object : PKCE { + override fun generateCodeChallenge(codeVerifier: String): String { + return "codeChallenge" + } + + override fun generateRandomCodeVerifier(entropy: Int): String { + return "codeVerifier" + } + } + + setUpEnvironment(environment, mockCodeVerifier) val testCode = "1235462834129834" val state = mutableListOf() val redirectionUrl = "ksrauth2://authenticate?code=$testCode&redirect_uri=ksrauth2&response_type=1&scope=1" backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + vm.produceState(Intent()) vm.produceState(Intent().setData(Uri.parse(redirectionUrl))) vm.uiState.toList(state) } + val authorizationUrlTest = "https://www.kickstarter.com/oauth/authorizations/new?redirect_uri=ksrauth2&scope=1&client_id=6B5W0CGU6NQPQ67588QEU1DOQL19BPF521VGPNY3XQXXUEGTND&response_type=1&code_challenge=codeChallenge&code_challenge_method=S256" // - First empty emission due the initialization assertEquals( listOf( OAuthUiState(authorizationUrl = "", isAuthorizationStep = false, user = null, error = ""), + OAuthUiState(authorizationUrl = authorizationUrlTest, isAuthorizationStep = true, user = null, error = ""), OAuthUiState(authorizationUrl = "", isAuthorizationStep = false, user = user, error = ""), ), state @@ -175,7 +188,6 @@ class OAuthActivityViewModelTest : KSRobolectricTestCase() { @Test fun testProduceState_getTokeAndUser_ErrorWhileFetchUser() = runTest { - val user = UserFactory.user() val apiClient = object : MockApiClientV2() { override fun loginWithCodes( codeVerifier: String, @@ -197,23 +209,36 @@ class OAuthActivityViewModelTest : KSRobolectricTestCase() { .currentUserV2(currentUserV2) .build() - setUpEnvironment(environment, CodeVerifier()) + val mockCodeVerifier = object : PKCE { + override fun generateCodeChallenge(codeVerifier: String): String { + return "testChallenge" + } + + override fun generateRandomCodeVerifier(entropy: Int): String { + return "testVerifier" + } + } + + setUpEnvironment(environment, mockCodeVerifier) val testCode = "1235462834129834" val state = mutableListOf() val redirectionUrl = "ksrauth2://authenticate?code=$testCode&redirect_uri=ksrauth2&response_type=1&scope=1" backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + vm.produceState(Intent()) vm.produceState(Intent().setData(Uri.parse(redirectionUrl))) vm.uiState.toList(state) } - val testErrorMessage = ApiExceptionFactory.badRequestException().errorEnvelope().errorMessages().toString() + val authorizationUrl = "https://www.kickstarter.com/oauth/authorizations/new?redirect_uri=ksrauth2&scope=1&client_id=6B5W0CGU6NQPQ67588QEU1DOQL19BPF521VGPNY3XQXXUEGTND&response_type=1&code_challenge=testChallenge&code_challenge_method=S256" + val testErrorMessage = "Response.error() / ${ApiExceptionFactory.badRequestException().errorEnvelope().errorMessages()}" // - First empty emission due the initialization assertEquals( listOf( OAuthUiState(authorizationUrl = "", isAuthorizationStep = false, user = null, error = ""), + OAuthUiState(authorizationUrl = authorizationUrl, isAuthorizationStep = true, user = null, error = ""), OAuthUiState(authorizationUrl = "", isAuthorizationStep = false, user = null, error = testErrorMessage), ), state