diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkFlow.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkFlow.kt new file mode 100644 index 00000000000..d01ac140358 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkFlow.kt @@ -0,0 +1,15 @@ +package com.woocommerce.android.ui.login + +import org.wordpress.android.fluxc.store.AccountStore.AuthEmailFlow + +enum class MagicLinkFlow(private val value: String) : AuthEmailFlow { + JetpackConnection("jetpack-connection"); + + override fun getName(): String = value + + companion object { + fun fromString(value: String): MagicLinkFlow? { + return MagicLinkFlow.values().firstOrNull { it.value == value } + } + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptActivity.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptActivity.kt index 7e8a3bcc65d..4bfa2c6485e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptActivity.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptActivity.kt @@ -19,6 +19,7 @@ import com.google.android.material.snackbar.Snackbar import com.woocommerce.android.R import com.woocommerce.android.analytics.AnalyticsEvent import com.woocommerce.android.analytics.AnalyticsTracker +import com.woocommerce.android.ui.login.MagicLinkInterceptViewModel.CancelJetpackActivation import com.woocommerce.android.ui.login.MagicLinkInterceptViewModel.ContinueJetpackActivation import com.woocommerce.android.ui.login.MagicLinkInterceptViewModel.OpenLogin import com.woocommerce.android.ui.login.MagicLinkInterceptViewModel.OpenSitePicker @@ -35,7 +36,6 @@ import javax.inject.Inject class MagicLinkInterceptActivity : AppCompatActivity() { companion object { private const val TOKEN_PARAMETER = "token" - private const val SOURCE_PARAMETER = "source" private const val FLOW_PARAMETER = "flow" } @@ -74,14 +74,11 @@ class MagicLinkInterceptActivity : AppCompatActivity() { val uri = requireNotNull(intent.data) val authToken = uri.getQueryParameter(TOKEN_PARAMETER) - val source = uri.getQueryParameter(SOURCE_PARAMETER)?.let { - MagicLinkSource.fromString(it) - } val flow = uri.getQueryParameter(FLOW_PARAMETER)?.let { MagicLinkFlow.fromString(it) } - authToken?.let { viewModel.handleMagicLink(it, flow, source) } + authToken?.let { viewModel.handleMagicLink(authToken = it, flow = flow) } } private fun setupObservers() { @@ -91,7 +88,7 @@ class MagicLinkInterceptActivity : AppCompatActivity() { viewModel.event.observe(this) { event -> when (event) { - OpenSitePicker -> showSitePickerScreen() + OpenSitePicker, CancelJetpackActivation -> openMainActivity() OpenLogin -> showLoginScreen() is ContinueJetpackActivation -> continueJetpackActivation(event) is ShowSnackbar -> showSnackBar(event.message) @@ -138,7 +135,7 @@ class MagicLinkInterceptActivity : AppCompatActivity() { retryContainer?.isVisible = show } - private fun showSitePickerScreen() { + private fun openMainActivity() { val intent = Intent(this, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK startActivity(intent) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptViewModel.kt index 21bdd502204..6cd68e4861e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptViewModel.kt @@ -4,13 +4,13 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import com.woocommerce.android.R -import com.woocommerce.android.R.string import com.woocommerce.android.model.JetpackConnectionStatus import com.woocommerce.android.model.JetpackSiteRegistrationStatus import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.model.RequestResult import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.tools.SiteConnectionType +import com.woocommerce.android.ui.jetpack.FetchJetpackStatus import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ShowSnackbar import com.woocommerce.android.viewmodel.ScopedViewModel @@ -24,7 +24,7 @@ class MagicLinkInterceptViewModel @Inject constructor( savedState: SavedStateHandle, private val magicLinkInterceptRepository: MagicLinkInterceptRepository, private val selectedSite: SelectedSite, - private val accountRepository: AccountRepository + private val fetchJetpackStatus: FetchJetpackStatus ) : ScopedViewModel(savedState) { private val _isLoading = MutableLiveData() val isLoading: LiveData = _isLoading @@ -33,11 +33,9 @@ class MagicLinkInterceptViewModel @Inject constructor( val showRetryOption: LiveData = _showRetryOption private var flow: MagicLinkFlow? = null - private var source: MagicLinkSource? = null - fun handleMagicLink(authToken: String, flow: MagicLinkFlow?, source: MagicLinkSource?) { + fun handleMagicLink(authToken: String, flow: MagicLinkFlow?) { this.flow = flow - this.source = source launch { _isLoading.value = true handleRequestResultResponse( @@ -56,19 +54,12 @@ class MagicLinkInterceptViewModel @Inject constructor( private fun handleRequestResultResponse(requestResult: RequestResult) { _isLoading.value = false - val source = this.source when (requestResult) { RequestResult.SUCCESS -> { - if (flow == MagicLinkFlow.SiteCredentialsToWPCom && - source != null && + if (flow == MagicLinkFlow.JetpackConnection && selectedSite.connectionType == SiteConnectionType.ApplicationPasswords ) { - triggerEvent( - ContinueJetpackActivation( - jetpackStatus = source.inferJetpackStatus(), - siteUrl = selectedSite.get().url - ) - ) + handleJetpackConnectionFlow() } else { triggerEvent(OpenSitePicker) } @@ -78,7 +69,7 @@ class MagicLinkInterceptViewModel @Inject constructor( // or if the user is not logged in // Either way, display error message and redirect user to login screen RequestResult.ERROR -> { - triggerEvent(ShowSnackbar(string.magic_link_update_error)) + triggerEvent(ShowSnackbar(R.string.magic_link_update_error)) triggerEvent(OpenLogin) } @@ -94,31 +85,42 @@ class MagicLinkInterceptViewModel @Inject constructor( } } - override fun onCleared() { - super.onCleared() - magicLinkInterceptRepository.onCleanup() - } + private fun handleJetpackConnectionFlow() { + _isLoading.value = true + launch { + fetchJetpackStatus(selectedSite.get(), useApplicationPasswords = true).fold( + onSuccess = { result -> + _isLoading.value = false + val jetpackStatus = when (result) { + is FetchJetpackStatus.JetpackStatusFetchResponse.Success -> result.status - private fun MagicLinkSource.inferJetpackStatus(): JetpackStatus { - val isJetpackInstalled = this != MagicLinkSource.JetpackInstallation - val isJetpackConnected = this == MagicLinkSource.WPComAuthentication - val wpComEmail = if (isJetpackConnected) { - accountRepository.getUserAccount()?.email - } else { - null + FetchJetpackStatus.JetpackStatusFetchResponse.ConnectionForbidden -> { + // Shouldn't happen unless the user changed their account's role after starting the setup. + // If this occurs, we'll just use a default value, and then the next screens + // will show an error message. + JetpackStatus( + isJetpackInstalled = false, + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.NOT_REGISTERED, + blogId = null + ) + ) + } + } + triggerEvent(ContinueJetpackActivation(jetpackStatus, selectedSite.get().url)) + }, + onFailure = { + triggerEvent(ShowSnackbar(R.string.magic_link_fetch_account_error)) + _isLoading.value = false + _showRetryOption.value = true + } + ) } + } - return JetpackStatus( - isJetpackInstalled = isJetpackInstalled, - jetpackConnectionStatus = if (isJetpackConnected) { - JetpackConnectionStatus.AccountConnected(wpComEmail.orEmpty()) - } else { - JetpackConnectionStatus.AccountNotConnected( - siteRegistrationStatus = JetpackSiteRegistrationStatus.UNKNOWN, - blogId = null - ) - } - ) + override fun onCleared() { + super.onCleared() + magicLinkInterceptRepository.onCleanup() } object OpenSitePicker : MultiLiveEvent.Event() @@ -127,4 +129,5 @@ class MagicLinkInterceptViewModel @Inject constructor( val jetpackStatus: JetpackStatus, val siteUrl: String ) : MultiLiveEvent.Event() + data object CancelJetpackActivation : MultiLiveEvent.Event() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkProperties.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkProperties.kt deleted file mode 100644 index 677aa506e83..00000000000 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/MagicLinkProperties.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.woocommerce.android.ui.login - -import org.wordpress.android.fluxc.store.AccountStore.AuthEmailFlow -import org.wordpress.android.fluxc.store.AccountStore.AuthEmailSource - -enum class MagicLinkSource(private val value: String) : AuthEmailSource { - JetpackInstallation("jetpack-installation"), - JetpackConnection("jetpack-connection"), - WPComAuthentication("wpcom-authentication"); - - override fun getName(): String = value - - companion object { - fun fromString(value: String): MagicLinkSource? { - return MagicLinkSource.values().firstOrNull { it.value == value } - } - } -} - -enum class MagicLinkFlow(private val value: String) : AuthEmailFlow { - SiteCredentialsToWPCom("sitecredentials-to-wpcom"); - - override fun getName(): String = value - - companion object { - fun fromString(value: String): MagicLinkFlow? { - return MagicLinkFlow.values().firstOrNull { it.value == value } - } - } -} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/WPComLoginRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/WPComLoginRepository.kt index e57be540267..97b90fd88a9 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/WPComLoginRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/WPComLoginRepository.kt @@ -79,7 +79,6 @@ class WPComLoginRepository @Inject constructor( suspend fun requestMagicLink( emailOrUsername: String, flow: MagicLinkFlow, - source: MagicLinkSource, isSignup: Boolean ): Result { WooLog.i(WooLog.T.LOGIN, "Submitting a Magic Link request") @@ -89,7 +88,7 @@ class WPComLoginRepository @Inject constructor( emailOrUsername, isSignup, flow, - source, + null, AuthEmailPayloadScheme.WOOCOMMERCE ) ) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkRequestViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkRequestViewModel.kt index 2be79caa6a9..84dc8e21c0c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkRequestViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkRequestViewModel.kt @@ -15,7 +15,6 @@ import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.analytics.AnalyticsTrackerWrapper import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.ui.login.MagicLinkFlow -import com.woocommerce.android.ui.login.MagicLinkSource import com.woocommerce.android.ui.login.WPComLoginRepository import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit @@ -108,15 +107,9 @@ class JetpackActivationMagicLinkRequestViewModel @Inject constructor( magicLinkFallbackButton = navArgs.fallbackButton, isLoadingDialogShown = true ) - val source = when { - !navArgs.jetpackStatus.isJetpackInstalled -> MagicLinkSource.JetpackInstallation - !navArgs.jetpackStatus.isCurrentUserConnected -> MagicLinkSource.JetpackConnection - else -> MagicLinkSource.WPComAuthentication - } wpComLoginRepository.requestMagicLink( emailOrUsername = navArgs.emailOrUsername, - flow = MagicLinkFlow.SiteCredentialsToWPCom, - source = source, + flow = MagicLinkFlow.JetpackConnection, isSignup = navArgs.isNewWpComAccount ).fold( onSuccess = { diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptViewModelTest.kt new file mode 100644 index 00000000000..787a6376853 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/MagicLinkInterceptViewModelTest.kt @@ -0,0 +1,150 @@ +package com.woocommerce.android.ui.login + +import androidx.lifecycle.SavedStateHandle +import com.woocommerce.android.R +import com.woocommerce.android.model.JetpackConnectionStatus +import com.woocommerce.android.model.JetpackSiteRegistrationStatus +import com.woocommerce.android.model.JetpackStatus +import com.woocommerce.android.model.RequestResult +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.tools.SiteConnectionType +import com.woocommerce.android.ui.jetpack.FetchJetpackStatus +import com.woocommerce.android.util.runAndCaptureValues +import com.woocommerce.android.viewmodel.BaseUnitTest +import com.woocommerce.android.viewmodel.MultiLiveEvent +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.given +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.wordpress.android.fluxc.model.SiteModel + +@OptIn(ExperimentalCoroutinesApi::class) +class MagicLinkInterceptViewModelTest : BaseUnitTest() { + private val site = SiteModel().apply { + url = "https://woocommerce.com/" + } + private val magicLinkInterceptRepository: MagicLinkInterceptRepository = mock() + private val selectedSite: SelectedSite = mock { + on { get() } doReturn site + } + private val fetchJetpackStatus: FetchJetpackStatus = mock() + + private lateinit var viewModel: MagicLinkInterceptViewModel + + suspend fun setup(prepareMocks: suspend () -> Unit = {}) { + prepareMocks() + + viewModel = MagicLinkInterceptViewModel( + savedState = SavedStateHandle(), + magicLinkInterceptRepository = magicLinkInterceptRepository, + selectedSite = selectedSite, + fetchJetpackStatus = fetchJetpackStatus + ) + } + + @Test + fun `given jetpack connection, when account info is fetched, then fetch jetpack status`() = testBlocking { + setup { + givenAuthTokenUpdateResult(RequestResult.SUCCESS) + given(selectedSite.connectionType).willReturn(SiteConnectionType.ApplicationPasswords) + givenJetpackStatusFetchResult( + Result.success( + FetchJetpackStatus.JetpackStatusFetchResponse.Success( + JetpackStatus( + isJetpackInstalled = true, + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.UNKNOWN, + blogId = null + ) + ) + ) + ) + ) + } + + viewModel.handleMagicLink("authToken", MagicLinkFlow.JetpackConnection) + + verify(fetchJetpackStatus).invoke(site, useApplicationPasswords = true) + } + + @Test + fun `given jetpack connection, when fetching jetpack status succeeds, then continue jetpack setup`() = + testBlocking { + val jetpackStatus = JetpackStatus( + isJetpackInstalled = true, + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.REGISTERED, + blogId = null + ) + ) + setup { + givenAuthTokenUpdateResult(RequestResult.SUCCESS) + given(selectedSite.connectionType).willReturn(SiteConnectionType.ApplicationPasswords) + givenJetpackStatusFetchResult( + Result.success(FetchJetpackStatus.JetpackStatusFetchResponse.Success(jetpackStatus)) + ) + } + + val event = viewModel.event.runAndCaptureValues { + viewModel.handleMagicLink("authToken", MagicLinkFlow.JetpackConnection) + }.last() + + assertThat(event).isEqualTo(MagicLinkInterceptViewModel.ContinueJetpackActivation(jetpackStatus, site.url)) + } + + @Test + fun `given jetpack connection, when fetching jetpack status fails, then show error message`() = + testBlocking { + setup { + givenAuthTokenUpdateResult(RequestResult.SUCCESS) + given(selectedSite.connectionType).willReturn(SiteConnectionType.ApplicationPasswords) + givenJetpackStatusFetchResult(Result.failure(Exception())) + } + + val event = viewModel.event.runAndCaptureValues { + viewModel.handleMagicLink("authToken", MagicLinkFlow.JetpackConnection) + }.last() + + assertThat(event).isEqualTo(MultiLiveEvent.Event.ShowSnackbar(R.string.magic_link_fetch_account_error)) + } + + @Test + fun `given jetpack connection, when fetching jetpack status is forbidden, then use default value for jetpack setup`() = + testBlocking { + setup { + givenAuthTokenUpdateResult(RequestResult.SUCCESS) + given(selectedSite.connectionType).willReturn(SiteConnectionType.ApplicationPasswords) + givenJetpackStatusFetchResult( + Result.success(FetchJetpackStatus.JetpackStatusFetchResponse.ConnectionForbidden) + ) + } + + val event = viewModel.event.runAndCaptureValues { + viewModel.handleMagicLink("authToken", MagicLinkFlow.JetpackConnection) + }.last() + + assertThat(event).isEqualTo( + MagicLinkInterceptViewModel.ContinueJetpackActivation( + JetpackStatus( + isJetpackInstalled = false, + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.NOT_REGISTERED, + blogId = null + ) + ), + site.url + ) + ) + } + + private suspend fun givenAuthTokenUpdateResult(result: RequestResult) { + given(magicLinkInterceptRepository.updateMagicLinkAuthToken("authToken")).willReturn(result) + } + + private suspend fun givenJetpackStatusFetchResult(result: Result) { + given(fetchJetpackStatus.invoke(site, useApplicationPasswords = true)).willReturn(result) + } +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkRequestViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkRequestViewModelTest.kt index 27e470d748d..43558e60a9f 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkRequestViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkRequestViewModelTest.kt @@ -5,7 +5,6 @@ import com.woocommerce.android.model.JetpackConnectionStatus import com.woocommerce.android.model.JetpackSiteRegistrationStatus import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.ui.login.MagicLinkFlow -import com.woocommerce.android.ui.login.MagicLinkSource import com.woocommerce.android.ui.login.WPComLoginRepository import com.woocommerce.android.util.runAndCaptureValues import com.woocommerce.android.viewmodel.BaseUnitTest @@ -62,8 +61,7 @@ class JetpackActivationMagicLinkRequestViewModelTest : BaseUnitTest() { verify(wpComLoginRepository).requestMagicLink( emailOrUsername = EMAIL, - flow = MagicLinkFlow.SiteCredentialsToWPCom, - source = MagicLinkSource.JetpackConnection, + flow = MagicLinkFlow.JetpackConnection, isSignup = false ) } @@ -75,8 +73,7 @@ class JetpackActivationMagicLinkRequestViewModelTest : BaseUnitTest() { verify(wpComLoginRepository, never()).requestMagicLink( emailOrUsername = EMAIL, - flow = MagicLinkFlow.SiteCredentialsToWPCom, - source = MagicLinkSource.JetpackConnection, + flow = MagicLinkFlow.JetpackConnection, isSignup = false ) } @@ -90,8 +87,7 @@ class JetpackActivationMagicLinkRequestViewModelTest : BaseUnitTest() { verify(wpComLoginRepository).requestMagicLink( emailOrUsername = EMAIL, - flow = MagicLinkFlow.SiteCredentialsToWPCom, - source = MagicLinkSource.JetpackConnection, + flow = MagicLinkFlow.JetpackConnection, isSignup = false ) }