diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/JetpackStatus.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/JetpackStatus.kt index ea06961bdb8..6b1e48d7570 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/JetpackStatus.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/JetpackStatus.kt @@ -6,6 +6,29 @@ import kotlinx.parcelize.Parcelize @Parcelize data class JetpackStatus( val isJetpackInstalled: Boolean, - val isJetpackConnected: Boolean, - val wpComEmail: String? -) : Parcelable + val jetpackConnectionStatus: JetpackConnectionStatus +) : Parcelable { + val isCurrentUserConnected: Boolean + get() = jetpackConnectionStatus is JetpackConnectionStatus.AccountConnected +} + +sealed interface JetpackConnectionStatus : Parcelable { + @Parcelize + data class AccountConnected(val wpComEmail: String) : JetpackConnectionStatus + + @Parcelize + data class AccountNotConnected( + val siteRegistrationStatus: JetpackSiteRegistrationStatus, + val blogId: Long? + ) : JetpackConnectionStatus { + // The `isRegistered` field was added at the same time as the connection API support, + // so we can use this as a proxy for whether the site supports the connection API. + // See: pe5sF9-401-p2 + val supportsConnectionApi: Boolean + get() = siteRegistrationStatus != JetpackSiteRegistrationStatus.UNKNOWN + } +} + +enum class JetpackSiteRegistrationStatus { + UNKNOWN, REGISTERED, NOT_REGISTERED +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/benefits/FetchJetpackStatus.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/benefits/FetchJetpackStatus.kt index 48570e8fd66..3220229215e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/benefits/FetchJetpackStatus.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/benefits/FetchJetpackStatus.kt @@ -1,9 +1,12 @@ package com.woocommerce.android.ui.jetpack.benefits import com.woocommerce.android.OnChangedException -import com.woocommerce.android.extensions.orNullIfEmpty +import com.woocommerce.android.extensions.isNotNullOrEmpty +import com.woocommerce.android.model.JetpackConnectionStatus +import com.woocommerce.android.model.JetpackSiteRegistrationStatus import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.tools.SelectedSite +import org.wordpress.android.fluxc.model.jetpack.JetpackConnectionData import org.wordpress.android.fluxc.store.JetpackStore import org.wordpress.android.fluxc.store.WooCommerceStore import javax.inject.Inject @@ -52,8 +55,10 @@ class FetchJetpackStatus @Inject constructor( JetpackStatusFetchResponse.Success( JetpackStatus( isJetpackInstalled = false, - isJetpackConnected = false, - wpComEmail = null + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.NOT_REGISTERED, + blogId = null + ) ) ) ) @@ -64,24 +69,17 @@ class FetchJetpackStatus @Inject constructor( } else -> { - val isJetpackInstalled = wooCommerceStore.fetchSitePlugins(selectedSite.get()).let { pluginResult -> - when { - pluginResult.isError -> { - return Result.failure(OnChangedException(pluginResult.error)) - } - - else -> { - pluginResult.model!!.any { it.slug == JETPACK_SLUG && it.isActive } - } - } + val isJetpackInstalled = checkIfJetpackIsInstalled().getOrElse { + return Result.failure(it) } + val jetpackConnectionData = userResult.data!! + Result.success( JetpackStatusFetchResponse.Success( JetpackStatus( isJetpackInstalled = isJetpackInstalled, - isJetpackConnected = userResult.data!!.currentUser.isConnected, - wpComEmail = userResult.data!!.currentUser.wpcomEmail.orNullIfEmpty() + jetpackConnectionStatus = jetpackConnectionData.toConnectionStatus(isJetpackInstalled) ) ) ) @@ -89,4 +87,51 @@ class FetchJetpackStatus @Inject constructor( } } } + + private suspend fun checkIfJetpackIsInstalled(): Result { + return wooCommerceStore.fetchSitePlugins(selectedSite.get()) + .let { pluginResult -> + when { + pluginResult.isError -> { + Result.failure(OnChangedException(pluginResult.error)) + } + + else -> { + Result.success(pluginResult.model!!.any { it.slug == JETPACK_SLUG && it.isActive }) + } + } + } + } + + private fun JetpackConnectionData.toConnectionStatus(isJetpackInstalled: Boolean): JetpackConnectionStatus { + return if (currentUser.isConnected) { + JetpackConnectionStatus.AccountConnected(currentUser.wpcomEmail) + } else { + JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = when (isSiteRegistered) { + true -> JetpackSiteRegistrationStatus.REGISTERED + false -> JetpackSiteRegistrationStatus.NOT_REGISTERED + else -> { + if (isJetpackInstalled) { + // Which means the installed version of Jetpack doesn't support the connection API + JetpackSiteRegistrationStatus.UNKNOWN + } else { + // Infer the site registration status based on whether the site has an owner or not + // Discussion: + // - If the site has an owner, it means the site is already registered + // - If the site doesn't have an owner, while the site may already be registered, it's OK + // to treat it as not registered since we'll register it later when connecting the account + // and there are no accounts that could be affected by the second registration. + if (connectionOwner.isNotNullOrEmpty()) { + JetpackSiteRegistrationStatus.REGISTERED + } else { + JetpackSiteRegistrationStatus.NOT_REGISTERED + } + } + } + }, + blogId = blogId + ) + } + } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/benefits/JetpackBenefitsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/benefits/JetpackBenefitsViewModel.kt index b753a45c020..06c891ef299 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/benefits/JetpackBenefitsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/jetpack/benefits/JetpackBenefitsViewModel.kt @@ -92,9 +92,9 @@ class JetpackBenefitsViewModel @Inject constructor( analyticsTrackerWrapper.track( stat = JETPACK_SETUP_CONNECTION_CHECK_COMPLETED, properties = mapOf( - AnalyticsTracker.KEY_JETPACK_SETUP_IS_ALREADY_CONNECTED to jetpackStatus.isJetpackConnected, + AnalyticsTracker.KEY_JETPACK_SETUP_IS_ALREADY_CONNECTED to jetpackStatus.isCurrentUserConnected, AnalyticsTracker.KEY_JETPACK_SETUP_REQUIRES_CONNECTION_ONLY to - (jetpackStatus.isJetpackInstalled && !jetpackStatus.isJetpackConnected) + (jetpackStatus.isJetpackInstalled && !jetpackStatus.isCurrentUserConnected) ) ) } 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 c5f3c5ac611..21bdd502204 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 @@ -5,6 +5,8 @@ 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 @@ -108,8 +110,14 @@ class MagicLinkInterceptViewModel @Inject constructor( return JetpackStatus( isJetpackInstalled = isJetpackInstalled, - isJetpackConnected = isJetpackConnected, - wpComEmail = wpComEmail + jetpackConnectionStatus = if (isJetpackConnected) { + JetpackConnectionStatus.AccountConnected(wpComEmail.orEmpty()) + } else { + JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.UNKNOWN, + blogId = null + ) + } ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/JetpackActivationRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/JetpackActivationRepository.kt index 700506af763..ec10125596c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/JetpackActivationRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/JetpackActivationRepository.kt @@ -4,6 +4,8 @@ import com.woocommerce.android.OnChangedException import com.woocommerce.android.WooException import com.woocommerce.android.analytics.AnalyticsEvent import com.woocommerce.android.analytics.AnalyticsTrackerWrapper +import com.woocommerce.android.model.JetpackConnectionStatus +import com.woocommerce.android.model.JetpackSiteRegistrationStatus import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.util.WooLog import kotlinx.coroutines.Dispatchers @@ -36,6 +38,58 @@ class JetpackActivationRepository @Inject constructor( SiteUtils.getSiteByMatchingUrl(siteStore, url) } + suspend fun connectJetpackAccount( + site: SiteModel, + jetpackConnectionStatus: JetpackConnectionStatus.AccountNotConnected, + useApplicationPasswords: Boolean + ): Result { + suspend fun registerSite(): Result { + return jetpackStore.registerSite(site, useApplicationPasswords).let { + when { + it.isError -> Result.failure(OnChangedException(it.error)) + it.data == null -> Result.failure(IllegalStateException("Blog ID missing")) + else -> Result.success(it.data!!) + } + } + } + + suspend fun connectJetpackAccount(blogId: Long): Result { + return jetpackStore.connectJetpackAccount(site, blogId, useApplicationPasswords).let { + if (it.isError) { + Result.failure(OnChangedException(it.error)) + } else { + Result.success(Unit) + } + } + } + + WooLog.d(WooLog.T.LOGIN, "Connecting Jetpack using the API") + + val blogId: Long = when (jetpackConnectionStatus.siteRegistrationStatus) { + JetpackSiteRegistrationStatus.NOT_REGISTERED -> { + registerSite().getOrElse { + WooLog.w(WooLog.T.LOGIN, "Jetpack registration failed: ${it.message}") + return Result.failure(it) + } + } + + JetpackSiteRegistrationStatus.REGISTERED -> { + requireNotNull(jetpackConnectionStatus.blogId) { + "Invalid Jetpack Connection Status: site registered but blogId is missing $jetpackConnectionStatus" + } + } + + else -> error("The site doesn't support Jetpack Connection API") + } + + return connectJetpackAccount(blogId) + .onSuccess { + WooLog.d(WooLog.T.LOGIN, "Jetpack connected successfully") + }.onFailure { + WooLog.w(WooLog.T.LOGIN, "Jetpack connection failed: ${it.message}") + } + } + suspend fun fetchJetpackConnectionUrl( site: SiteModel, useApplicationPasswords: Boolean diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/dispatcher/JetpackActivationDispatcherFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/dispatcher/JetpackActivationDispatcherFragment.kt index 447dd7be642..c7291452e61 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/dispatcher/JetpackActivationDispatcherFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/dispatcher/JetpackActivationDispatcherFragment.kt @@ -38,7 +38,7 @@ class JetpackActivationDispatcherFragment : BaseFragment() { JetpackActivationDispatcherFragmentDirections .actionJetpackActivationDispatcherFragmentToJetpackActivationStartFragment( siteUrl = event.siteUrl, - isJetpackInstalled = event.isJetpackInstalled + jetpackStatus = event.jetpackStatus ) ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/dispatcher/JetpackActivationDispatcherViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/dispatcher/JetpackActivationDispatcherViewModel.kt index ad6dbb46bf7..b48dd17f10c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/dispatcher/JetpackActivationDispatcherViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/dispatcher/JetpackActivationDispatcherViewModel.kt @@ -1,6 +1,7 @@ package com.woocommerce.android.ui.login.jetpack.dispatcher import androidx.lifecycle.SavedStateHandle +import com.woocommerce.android.model.JetpackConnectionStatus import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.tools.SiteConnectionType @@ -21,11 +22,11 @@ class JetpackActivationDispatcherViewModel @Inject constructor( val jetpackStatus = args.jetpackStatus when (selectedSite.connectionType) { SiteConnectionType.ApplicationPasswords -> { - if (jetpackStatus.isJetpackConnected && jetpackStatus.wpComEmail != null) { + if (jetpackStatus.jetpackConnectionStatus is JetpackConnectionStatus.AccountConnected) { // Jetpack is already connected and we know the address email, handle the authentication triggerEvent( StartWPComAuthenticationForEmail( - wpComEmail = jetpackStatus.wpComEmail, + wpComEmail = jetpackStatus.jetpackConnectionStatus.wpComEmail, jetpackStatus = jetpackStatus ) ) @@ -39,8 +40,8 @@ class JetpackActivationDispatcherViewModel @Inject constructor( // Handle connecting a new site triggerEvent( StartJetpackActivationForNewSite( - args.siteUrl, - jetpackStatus.isJetpackInstalled + siteUrl = args.siteUrl, + jetpackStatus = jetpackStatus ) ) } @@ -49,7 +50,7 @@ class JetpackActivationDispatcherViewModel @Inject constructor( data class StartJetpackActivationForNewSite( val siteUrl: String, - val isJetpackInstalled: Boolean + val jetpackStatus: JetpackStatus ) : MultiLiveEvent.Event() data class StartWPComLoginForJetpackActivation( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/main/JetpackActivationMainViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/main/JetpackActivationMainViewModel.kt index 91069e9f909..ea7f288656e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/main/JetpackActivationMainViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/main/JetpackActivationMainViewModel.kt @@ -14,6 +14,7 @@ import com.woocommerce.android.analytics.AnalyticsEvent.LOGIN_JETPACK_SETUP_INST import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.analytics.AnalyticsTrackerWrapper import com.woocommerce.android.extensions.isNotNullOrEmpty +import com.woocommerce.android.model.JetpackConnectionStatus import com.woocommerce.android.support.help.HelpOrigin.JETPACK_INSTALLATION import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.tools.SiteConnectionType @@ -66,7 +67,7 @@ class JetpackActivationMainViewModel @Inject constructor( private val accountRepository: AccountRepository, private val appPrefsWrapper: AppPrefsWrapper, private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, - private val selectedSite: SelectedSite + selectedSite: SelectedSite ) : ScopedViewModel(savedStateHandle) { companion object { private const val JETPACK_SLUG = "jetpack" @@ -83,8 +84,14 @@ class JetpackActivationMainViewModel @Inject constructor( } private val navArgs: JetpackActivationMainFragmentArgs by savedStateHandle.navArgs() + // Whether to use the application passwords for fetching the connection URL or the Cookie-nonce authentication private val useApplicationPasswords = selectedSite.connectionType == SiteConnectionType.ApplicationPasswords + + // The cast is safe because we use this flow only when Jetpack is not connected + private val supportNativeConnectionAPI = (navArgs.jetpackStatus.jetpackConnectionStatus + as JetpackConnectionStatus.AccountNotConnected).supportsConnectionApi + private val site: Deferred get() = async { val site = jetpackActivationRepository.getSiteByUrl(navArgs.siteUrl)?.takeIf { @@ -104,7 +111,7 @@ class JetpackActivationMainViewModel @Inject constructor( private val currentStep = savedStateHandle.getStateFlow( scope = viewModelScope, - initialValue = Step(if (navArgs.isJetpackInstalled) StepType.Connection else StepType.Installation), + initialValue = Step(if (navArgs.jetpackStatus.isJetpackInstalled) StepType.Connection else StepType.Installation), ) private val connectionStep = savedStateHandle.getStateFlow( scope = viewModelScope, @@ -114,13 +121,13 @@ class JetpackActivationMainViewModel @Inject constructor( val viewState = combine( currentStep, connectionStep, - flowOf(if (navArgs.isJetpackInstalled) stepsForConnection() else stepsForInstallation()), + flowOf(if (navArgs.jetpackStatus.isJetpackInstalled) stepsForConnection() else stepsForInstallation()), isShowingErrorState ) { currentStep, connectionStep, stepTypes, isShowingErrorState -> when (isShowingErrorState) { false -> ViewState.ProgressViewState( siteUrl = UrlUtils.removeScheme(navArgs.siteUrl), - isJetpackInstalled = navArgs.isJetpackInstalled, + isJetpackInstalled = navArgs.jetpackStatus.isJetpackInstalled, steps = stepTypes.map { stepType -> Step( type = stepType, @@ -422,7 +429,54 @@ class JetpackActivationMainViewModel @Inject constructor( @Suppress("LongMethod") private suspend fun startJetpackConnection() { - WooLog.d(WooLog.T.LOGIN, "Jetpack Activation: start Jetpack Connection") + val onFailure: (Throwable) -> Unit = { + val error = (it as? OnChangedException)?.error as? JetpackStore.JetpackError + + if (isFromBanner) { + analyticsTrackerWrapper.track( + stat = JETPACK_SETUP_FLOW, + properties = mapOf( + AnalyticsTracker.KEY_STEP to currentStep.value.type.analyticsName, + AnalyticsTracker.KEY_FAILURE to "Jetpack connection failed: ${it.message}", + ) + ) + } else { + analyticsTrackerWrapper.track( + stat = AnalyticsEvent.LOGIN_JETPACK_SETUP_FETCH_JETPACK_CONNECTION_URL_FAILED, + properties = mapOf(AnalyticsTracker.KEY_ERROR_CODE to error?.errorCode.toString()), + errorContext = this@JetpackActivationMainViewModel::class.simpleName, + errorType = it::class.simpleName, + errorDescription = it.message.orEmpty() + ) + } + currentStep.update { state -> state.copy(state = StepState.Error(error?.errorCode)) } + } + + if (supportNativeConnectionAPI) { + startJetpackNativeConnection(onFailure) + } else { + startJetpackWebViewConnection(onFailure) + } + } + + private suspend fun startJetpackNativeConnection(onFailure: (Throwable) -> Unit) { + WooLog.d(WooLog.T.LOGIN, "Jetpack Activation: start Jetpack Connection using Native API") + val currentSite = site.await() + jetpackActivationRepository.connectJetpackAccount( + site = currentSite, + jetpackConnectionStatus = navArgs.jetpackStatus.jetpackConnectionStatus + as JetpackConnectionStatus.AccountNotConnected, + useApplicationPasswords = useApplicationPasswords + ).fold( + onSuccess = { + connectionStep.value = ConnectionStep.Validation + }, + onFailure = onFailure + ) + } + + private suspend fun startJetpackWebViewConnection(onFailure: (Throwable) -> Unit) { + WooLog.d(WooLog.T.LOGIN, "Jetpack Activation: start Jetpack Connection using WebView") val currentSite = site.await() jetpackActivationRepository.fetchJetpackConnectionUrl(currentSite, useApplicationPasswords).fold( onSuccess = { connectionUrl -> @@ -470,34 +524,21 @@ class JetpackActivationMainViewModel @Inject constructor( ) } }, - onFailure = { - val error = (it as? OnChangedException)?.error as? JetpackStore.JetpackError - - if (isFromBanner) { - analyticsTrackerWrapper.track( - stat = JETPACK_SETUP_FLOW, - properties = mapOf( - AnalyticsTracker.KEY_STEP to currentStep.value.type.analyticsName, - AnalyticsTracker.KEY_FAILURE to "Jetpack installation failed: ${it.message}", - ) - ) - } else { - analyticsTrackerWrapper.track( - stat = AnalyticsEvent.LOGIN_JETPACK_SETUP_FETCH_JETPACK_CONNECTION_URL_FAILED, - properties = mapOf(AnalyticsTracker.KEY_ERROR_CODE to error?.errorCode.toString()), - errorContext = this@JetpackActivationMainViewModel::class.simpleName, - errorType = it::class.simpleName, - errorDescription = it.message.orEmpty() - ) - } - currentStep.update { state -> state.copy(state = StepState.Error(error?.errorCode)) } - } + onFailure = onFailure ) } private suspend fun startJetpackValidation() { WooLog.d(WooLog.T.LOGIN, "Jetpack Activation: start Jetpack Connection validation") - jetpackActivationRepository.fetchJetpackConnectedEmail(site.await(), useApplicationPasswords).fold( + + val connectedEmail = if (supportNativeConnectionAPI) { + // If we're using the native connection API, we can assume the same email as the logged in user + Result.success(accountRepository.getUserAccount()!!.email) + } else { + jetpackActivationRepository.fetchJetpackConnectedEmail(site.await(), useApplicationPasswords) + } + + connectedEmail.fold( onSuccess = { email -> jetpackConnectedEmail = email if (accountRepository.getUserAccount()?.email != email) { @@ -573,9 +614,9 @@ class JetpackActivationMainViewModel @Inject constructor( ) } - private fun stepsForInstallation() = StepType.values() + private fun stepsForInstallation() = StepType.entries - private fun stepsForConnection() = arrayOf(StepType.Connection, StepType.Done) + private fun stepsForConnection() = listOf(StepType.Connection, StepType.Done) sealed interface ViewState { data class ProgressViewState( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/sitecredentials/JetpackActivationSiteCredentialsFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/sitecredentials/JetpackActivationSiteCredentialsFragment.kt index 9d4b9773dab..119a59fefab 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/sitecredentials/JetpackActivationSiteCredentialsFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/sitecredentials/JetpackActivationSiteCredentialsFragment.kt @@ -65,8 +65,8 @@ class JetpackActivationSiteCredentialsFragment : BaseFragment() { findNavController().navigateSafely( JetpackActivationSiteCredentialsFragmentDirections .actionJetpackActivationSiteCredentialsFragmentToJetpackActivationMainFragment( - siteUrl = event.siteUrl, - isJetpackInstalled = event.isJetpackInstalled + jetpackStatus = event.jetpackStatus, + siteUrl = event.siteUrl ) ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/sitecredentials/JetpackActivationSiteCredentialsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/sitecredentials/JetpackActivationSiteCredentialsViewModel.kt index a4003082e5c..77718db5a16 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/sitecredentials/JetpackActivationSiteCredentialsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/sitecredentials/JetpackActivationSiteCredentialsViewModel.kt @@ -8,6 +8,7 @@ import com.woocommerce.android.OnChangedException import com.woocommerce.android.R import com.woocommerce.android.analytics.AnalyticsEvent import com.woocommerce.android.analytics.AnalyticsTrackerWrapper +import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.model.UiString import com.woocommerce.android.model.UiString.UiStringRes import com.woocommerce.android.ui.login.WPApiSiteRepository @@ -38,7 +39,7 @@ class JetpackActivationSiteCredentialsViewModel @Inject constructor( private val _viewState = savedStateHandle.getStateFlow( scope = viewModelScope, initialValue = JetpackActivationSiteCredentialsViewState( - isJetpackInstalled = navArgs.isJetpackInstalled, + isJetpackInstalled = navArgs.jetpackStatus.isJetpackInstalled, siteUrl = UrlUtils.removeScheme(navArgs.siteUrl) ) ) @@ -90,8 +91,8 @@ class JetpackActivationSiteCredentialsViewModel @Inject constructor( analyticsTrackerWrapper.track(AnalyticsEvent.LOGIN_JETPACK_SITE_CREDENTIAL_DID_FINISH_LOGIN) triggerEvent( NavigateToJetpackActivationSteps( - navArgs.siteUrl, - navArgs.isJetpackInstalled + siteUrl = navArgs.siteUrl, + jetpackStatus = navArgs.jetpackStatus ) ) }, @@ -134,7 +135,7 @@ class JetpackActivationSiteCredentialsViewModel @Inject constructor( data class NavigateToJetpackActivationSteps( val siteUrl: String, - val isJetpackInstalled: Boolean + val jetpackStatus: JetpackStatus ) : MultiLiveEvent.Event() data class ResetPassword(val siteUrl: String) : MultiLiveEvent.Event() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/start/JetpackActivationStartFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/start/JetpackActivationStartFragment.kt index b6edad693a5..a9f4bfd2d65 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/start/JetpackActivationStartFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/start/JetpackActivationStartFragment.kt @@ -68,7 +68,7 @@ class JetpackActivationStartFragment : BaseFragment() { JetpackActivationStartFragmentDirections .actionJetpackActivationStartFragmentToJetpackActivationSiteCredentialsFragment( siteUrl = event.siteUrl, - isJetpackInstalled = event.isJetpackInstalled + jetpackStatus = event.jetpackStatus ) ) } @@ -78,7 +78,7 @@ class JetpackActivationStartFragment : BaseFragment() { JetpackActivationStartFragmentDirections .actionJetpackActivationStartFragmentToJetpackActivationMainFragment( siteUrl = event.siteUrl, - isJetpackInstalled = true + jetpackStatus = event.jetpackStatus ) ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/start/JetpackActivationStartViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/start/JetpackActivationStartViewModel.kt index f1060cbd619..355e1978c0c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/start/JetpackActivationStartViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/start/JetpackActivationStartViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.woocommerce.android.analytics.AnalyticsEvent import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.analytics.AnalyticsTrackerWrapper +import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.support.help.HelpOrigin.JETPACK_INSTALLATION import com.woocommerce.android.ui.login.jetpack.main.JetpackActivationMainViewModel import com.woocommerce.android.viewmodel.MultiLiveEvent @@ -41,14 +42,14 @@ class JetpackActivationStartViewModel @Inject constructor( JetpackActivationState( url = UrlUtils.removeScheme(navArgs.siteUrl), faviconUrl = "${navArgs.siteUrl.trimEnd('/')}/favicon.ico", - isJetpackInstalled = navArgs.isJetpackInstalled, + isJetpackInstalled = navArgs.jetpackStatus.isJetpackInstalled, isConnectionDismissed = isConnectionDismissed ) }.asLiveData() init { analyticsTrackerWrapper.track( - stat = if (navArgs.isJetpackInstalled) { + stat = if (navArgs.jetpackStatus.isJetpackInstalled) { AnalyticsEvent.LOGIN_JETPACK_CONNECTION_ERROR_SHOWN } else { AnalyticsEvent.LOGIN_JETPACK_REQUIRED_SCREEN_VIEWED @@ -88,12 +89,13 @@ class JetpackActivationStartViewModel @Inject constructor( isConnectionDismissed.value = false triggerEvent( ContinueJetpackConnection( - siteUrl = navArgs.siteUrl + siteUrl = navArgs.siteUrl, + jetpackStatus = navArgs.jetpackStatus ) ) } else { analyticsTrackerWrapper.track( - stat = if (navArgs.isJetpackInstalled) { + stat = if (navArgs.jetpackStatus.isJetpackInstalled) { AnalyticsEvent.LOGIN_JETPACK_CONNECT_BUTTON_TAPPED } else { AnalyticsEvent.LOGIN_JETPACK_SETUP_BUTTON_TAPPED @@ -102,7 +104,7 @@ class JetpackActivationStartViewModel @Inject constructor( triggerEvent( NavigateToSiteCredentialsScreen( siteUrl = navArgs.siteUrl, - isJetpackInstalled = navArgs.isJetpackInstalled + jetpackStatus = navArgs.jetpackStatus ) ) } @@ -117,8 +119,11 @@ class JetpackActivationStartViewModel @Inject constructor( data class NavigateToSiteCredentialsScreen( val siteUrl: String, - val isJetpackInstalled: Boolean + val jetpackStatus: JetpackStatus ) : MultiLiveEvent.Event() - data class ContinueJetpackConnection(val siteUrl: String) : MultiLiveEvent.Event() + data class ContinueJetpackConnection( + val siteUrl: String, + val jetpackStatus: JetpackStatus + ) : MultiLiveEvent.Event() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkHandlerFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkHandlerFragment.kt index 7916f62495d..68c9c1e5e99 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkHandlerFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationMagicLinkHandlerFragment.kt @@ -64,7 +64,7 @@ class JetpackActivationMagicLinkHandlerFragment : BaseFragment() { findNavController().navigateSafely( JetpackActivationMagicLinkHandlerFragmentDirections .actionJetpackActivationMagicLinkHandlerFragmentToJetpackActivationMainFragment( - isJetpackInstalled = event.isJetpackInstalled, + jetpackStatus = event.jetpackStatus, siteUrl = event.siteUrl ) ) 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 d9148bcaa9a..2be79caa6a9 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 @@ -110,7 +110,7 @@ class JetpackActivationMagicLinkRequestViewModel @Inject constructor( ) val source = when { !navArgs.jetpackStatus.isJetpackInstalled -> MagicLinkSource.JetpackInstallation - !navArgs.jetpackStatus.isJetpackConnected -> MagicLinkSource.JetpackConnection + !navArgs.jetpackStatus.isCurrentUserConnected -> MagicLinkSource.JetpackConnection else -> MagicLinkSource.WPComAuthentication } wpComLoginRepository.requestMagicLink( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPCom2FAFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPCom2FAFragment.kt index e747a88e717..12b6e8bd661 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPCom2FAFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPCom2FAFragment.kt @@ -64,7 +64,7 @@ class JetpackActivationWPCom2FAFragment : BaseFragment() { findNavController().navigateSafely( JetpackActivationWPCom2FAFragmentDirections .actionJetpackActivationWPCom2FAFragmentToJetpackActivationMainFragment( - isJetpackInstalled = event.isJetpackInstalled, + jetpackStatus = event.jetpackStatus, siteUrl = event.siteUrl ) ) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComPasswordFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComPasswordFragment.kt index 81020e14cba..0308ab62bd1 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComPasswordFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComPasswordFragment.kt @@ -87,7 +87,7 @@ class JetpackActivationWPComPasswordFragment : BaseFragment() { findNavController().navigateSafely( JetpackActivationWPComPasswordFragmentDirections .actionJetpackActivationWPComPasswordFragmentToJetpackActivationMainFragment( - isJetpackInstalled = event.isJetpackInstalled, + jetpackStatus = event.jetpackStatus, siteUrl = event.siteUrl ) ) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComPostLoginViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComPostLoginViewModel.kt index cfa967a9f68..7a537ca7d0d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComPostLoginViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComPostLoginViewModel.kt @@ -23,7 +23,7 @@ open class JetpackActivationWPComPostLoginViewModel( analyticsTrackerWrapper.track(JETPACK_SETUP_LOGIN_COMPLETED) val siteUrl = selectedSite.get().url - if (jetpackStatus.isJetpackConnected) { + if (jetpackStatus.isCurrentUserConnected) { // Attempt returning the site from the DB if it exists, otherwise fetch it from API val jetpackSite = jetpackActivationRepository.getJetpackSiteByUrl(siteUrl) .takeIf { it?.hasWooCommerce == true } @@ -43,7 +43,7 @@ open class JetpackActivationWPComPostLoginViewModel( } else { triggerEvent( ShowJetpackActivationScreen( - isJetpackInstalled = jetpackStatus.isJetpackInstalled, + jetpackStatus = jetpackStatus, siteUrl = siteUrl ) ) @@ -52,7 +52,7 @@ open class JetpackActivationWPComPostLoginViewModel( } data class ShowJetpackActivationScreen( - val isJetpackInstalled: Boolean, + val jetpackStatus: JetpackStatus, val siteUrl: String ) : MultiLiveEvent.Event() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/sitepicker/sitediscovery/SitePickerSiteDiscoveryFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/sitepicker/sitediscovery/SitePickerSiteDiscoveryFragment.kt index 800e5043940..6bf95af2418 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/sitepicker/sitediscovery/SitePickerSiteDiscoveryFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/sitepicker/sitediscovery/SitePickerSiteDiscoveryFragment.kt @@ -15,6 +15,8 @@ import com.woocommerce.android.R import com.woocommerce.android.extensions.handleNotice import com.woocommerce.android.extensions.navigateBackWithResult import com.woocommerce.android.extensions.navigateToHelpScreen +import com.woocommerce.android.model.JetpackConnectionStatus +import com.woocommerce.android.model.JetpackSiteRegistrationStatus import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.support.help.HelpOrigin import com.woocommerce.android.support.requests.SupportRequestFormActivity @@ -112,8 +114,10 @@ class SitePickerSiteDiscoveryFragment : BaseFragment() { siteUrl = event.siteAddress, jetpackStatus = JetpackStatus( isJetpackInstalled = event.isJetpackInstalled, - isJetpackConnected = false, - wpComEmail = null + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.UNKNOWN, + blogId = null + ) ) ) ) diff --git a/WooCommerce/src/main/res/navigation/nav_graph_jetpack_activation.xml b/WooCommerce/src/main/res/navigation/nav_graph_jetpack_activation.xml index e808a6a842c..cc7cf9fab9f 100644 --- a/WooCommerce/src/main/res/navigation/nav_graph_jetpack_activation.xml +++ b/WooCommerce/src/main/res/navigation/nav_graph_jetpack_activation.xml @@ -39,12 +39,12 @@ android:id="@+id/jetpackActivationStartFragment" android:name="com.woocommerce.android.ui.login.jetpack.start.JetpackActivationStartFragment" android:label="JetpackActivationStartFragment"> + - @@ -56,12 +56,12 @@ android:id="@+id/jetpackActivationSiteCredentialsFragment" android:name="com.woocommerce.android.ui.login.jetpack.sitecredentials.JetpackActivationSiteCredentialsFragment" android:label="JetpackActivationSiteCredentialsFragment"> + - + android:name="jetpackStatus" + app:argType="com.woocommerce.android.model.JetpackStatus" /> diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/jetpack/benefits/JetpackBenefitsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/jetpack/benefits/JetpackBenefitsViewModelTest.kt index 813a5097183..2b0982938d1 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/jetpack/benefits/JetpackBenefitsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/jetpack/benefits/JetpackBenefitsViewModelTest.kt @@ -2,6 +2,8 @@ package com.woocommerce.android.ui.jetpack.benefits import androidx.lifecycle.SavedStateHandle import com.woocommerce.android.analytics.AnalyticsTrackerWrapper +import com.woocommerce.android.model.JetpackConnectionStatus +import com.woocommerce.android.model.JetpackSiteRegistrationStatus import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.model.User import com.woocommerce.android.model.UserRole @@ -74,8 +76,7 @@ class JetpackBenefitsViewModelTest : BaseUnitTest() { // Given val jetpackStatus = JetpackStatus( isJetpackInstalled = true, - isJetpackConnected = true, - wpComEmail = null + jetpackConnectionStatus = JetpackConnectionStatus.AccountConnected("email") ) givenConnectionType(SiteConnectionType.ApplicationPasswords) givenJetpackFetchResult( @@ -118,8 +119,10 @@ class JetpackBenefitsViewModelTest : BaseUnitTest() { // Given val jetpackStatus = JetpackStatus( isJetpackInstalled = false, - isJetpackConnected = false, - wpComEmail = null + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.NOT_REGISTERED, + blogId = null + ) ) givenConnectionType(SiteConnectionType.ApplicationPasswords) givenJetpackFetchResult(FetchJetpackStatus.JetpackStatusFetchResponse.Success(jetpackStatus)) @@ -142,8 +145,10 @@ class JetpackBenefitsViewModelTest : BaseUnitTest() { // Given val jetpackStatus = JetpackStatus( isJetpackInstalled = false, - isJetpackConnected = false, - wpComEmail = null + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.NOT_REGISTERED, + blogId = null + ) ) givenConnectionType(SiteConnectionType.ApplicationPasswords) givenJetpackFetchResult(FetchJetpackStatus.JetpackStatusFetchResponse.Success(jetpackStatus)) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/JetpackActivationRepositoryTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/JetpackActivationRepositoryTest.kt new file mode 100644 index 00000000000..d4e05634da3 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/JetpackActivationRepositoryTest.kt @@ -0,0 +1,126 @@ +package com.woocommerce.android.ui.login.jetpack + +import com.woocommerce.android.analytics.AnalyticsTrackerWrapper +import com.woocommerce.android.model.JetpackConnectionStatus +import com.woocommerce.android.model.JetpackSiteRegistrationStatus +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.viewmodel.BaseUnitTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.JetpackStore +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.fluxc.store.WooCommerceStore + +@OptIn(ExperimentalCoroutinesApi::class) +class JetpackActivationRepositoryTest : BaseUnitTest() { + private val dispatcher: Dispatcher = mock() + private val siteStore: SiteStore = mock() + private val jetpackStore: JetpackStore = mock { + onBlocking { registerSite(any(), any()) }.thenReturn(JetpackStore.JetpackResult(123L)) + onBlocking { connectJetpackAccount(any(), any(), any()) }.thenReturn(JetpackStore.JetpackResult(Unit)) + } + private val wooCommerceStore: WooCommerceStore = mock() + private val selectedSite: SelectedSite = mock() + private val analyticsTracker: AnalyticsTrackerWrapper = mock() + + private val repository = JetpackActivationRepository( + dispatcher = dispatcher, + siteStore = siteStore, + jetpackStore = jetpackStore, + wooCommerceStore = wooCommerceStore, + selectedSite = selectedSite, + analyticsTrackerWrapper = analyticsTracker + ) + + @Test + fun `given site is already registered, when connecting Jetpack account, then skip registration`() = testBlocking { + val jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.REGISTERED, + blogId = 123L + ) + + repository.connectJetpackAccount( + site = SiteModel(), + jetpackConnectionStatus = jetpackConnectionStatus, + useApplicationPasswords = false + ) + + verify(jetpackStore, never()).registerSite(any(), any()) + } + + @Test + fun `given site is not registered, when connecting Jetpack account, then register site`() = testBlocking { + val jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.NOT_REGISTERED, + blogId = null + ) + + repository.connectJetpackAccount( + site = SiteModel(), + jetpackConnectionStatus = jetpackConnectionStatus, + useApplicationPasswords = false + ) + + verify(jetpackStore).registerSite(any(), any()) + } + + @Test + fun `when connecting Jetpack account succeeds, then return success`() = testBlocking { + val jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.NOT_REGISTERED, + blogId = null + ) + + val result = repository.connectJetpackAccount( + site = SiteModel(), + jetpackConnectionStatus = jetpackConnectionStatus, + useApplicationPasswords = false + ) + + assertThat(result.isSuccess).isTrue() + } + + @Test + fun `when connecting Jetpack account fails, then return failure`() = testBlocking { + whenever(jetpackStore.connectJetpackAccount(any(), any(), any())) + .thenReturn(JetpackStore.JetpackResult(JetpackStore.JetpackError("error"))) + val jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.NOT_REGISTERED, + blogId = null + ) + + val result = repository.connectJetpackAccount( + site = SiteModel(), + jetpackConnectionStatus = jetpackConnectionStatus, + useApplicationPasswords = false + ) + + assertThat(result.isFailure).isTrue() + } + + @Test + fun `when site registration fails, then return failure`() = testBlocking { + whenever(jetpackStore.registerSite(any(), any())) + .thenReturn(JetpackStore.JetpackResult(JetpackStore.JetpackError("error"))) + val jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.NOT_REGISTERED, + blogId = null + ) + + val result = repository.connectJetpackAccount( + site = SiteModel(), + jetpackConnectionStatus = jetpackConnectionStatus, + useApplicationPasswords = false + ) + + assertThat(result.isFailure).isTrue() + } +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/main/JetpackActivationMainViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/main/JetpackActivationMainViewModelTest.kt index d9282c10ad1..389ebf8af28 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/main/JetpackActivationMainViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/main/JetpackActivationMainViewModelTest.kt @@ -2,6 +2,9 @@ package com.woocommerce.android.ui.login.jetpack.main import com.woocommerce.android.AppPrefsWrapper import com.woocommerce.android.analytics.AnalyticsTrackerWrapper +import com.woocommerce.android.model.JetpackConnectionStatus +import com.woocommerce.android.model.JetpackSiteRegistrationStatus +import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.tools.SiteConnectionType import com.woocommerce.android.ui.common.PluginRepository @@ -24,12 +27,14 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.doSuspendableAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.AccountModel import org.wordpress.android.fluxc.model.SiteModel /** @@ -47,23 +52,29 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { private lateinit var viewModel: JetpackActivationMainViewModel private val jetpackActivationRepository: JetpackActivationRepository = mock { + onBlocking { fetchJetpackSite(siteUrl) } doReturn Result.success(site) onBlocking { getSiteByUrl(siteUrl) } doReturn site } private val analyticsTrackerWrapper: AnalyticsTrackerWrapper = mock() private val pluginRepository: PluginRepository = mock() - private val accountRepository: AccountRepository = mock() + private val accountRepository: AccountRepository = mock { + on { getUserAccount() } doReturn AccountModel().apply { + email = "email@example.com" + } + } private val appPrefsWrapper: AppPrefsWrapper = mock() private val selectedSite: SelectedSite = mock() suspend fun setup( - isJetpackInstalled: Boolean = false, + jetpackStatus: JetpackStatus, prepareMocks: suspend () -> Unit = { } ) { prepareMocks() viewModel = JetpackActivationMainViewModel( savedStateHandle = JetpackActivationMainFragmentArgs( - isJetpackInstalled = isJetpackInstalled, siteUrl = siteUrl + jetpackStatus = jetpackStatus, + siteUrl = siteUrl ).toSavedStateHandle(), jetpackActivationRepository = jetpackActivationRepository, pluginRepository = pluginRepository, @@ -75,9 +86,71 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { } @Test - fun `given site using application passwords and fully disconnected, when starting Jetpack connection, then use alternative URL`() = + fun `given using Jetpack Connection API, when starting Jetpack connection, then connect using Jetpack API`() = + testBlocking { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = true)) + + viewModel.viewState.getOrAwaitValue() + + verify(jetpackActivationRepository).connectJetpackAccount(any(), any(), any()) + } + + @Test + fun `given using Jetpack Connection API, when connection succeeds, then start validation`() = testBlocking { - setup(isJetpackInstalled = true) { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = true)) { + whenever(jetpackActivationRepository.connectJetpackAccount(any(), any(), any())) + .thenReturn(Result.success(Unit)) + + whenever(jetpackActivationRepository.fetchJetpackSite(siteUrl)).doSuspendableAnswer { + // To trigger suspension and allow reading the intermediate state + delay(100) + Result.success(site) + } + } + + val state = viewModel.viewState.getOrAwaitValue() + + assertThat((state as ProgressViewState).connectionStep).isEqualTo(ConnectionStep.Validation) + } + + @Test + fun `given using Jetpack Connection API, when connection fails, then show error`() = + testBlocking { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = true)) { + whenever(jetpackActivationRepository.connectJetpackAccount(any(), any(), any())) + .thenReturn(Result.failure(IllegalStateException())) + } + + val state = viewModel.viewState.runAndCaptureValues { + advanceUntilIdle() + }.last() + + assertThat(state).isInstanceOf(ViewState.ErrorViewState::class.java) + } + + @Test + fun `given using Jetpack Connection API, when validation succeeds, then then mark steps as done`() = + testBlocking { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = true)) { + whenever(jetpackActivationRepository.connectJetpackAccount(any(), any(), any())) + .thenReturn(Result.success(Unit)) + } + + val state = viewModel.viewState.runAndCaptureValues { + advanceUntilIdle() + runCurrent() + }.last() + + assertThat((state as ProgressViewState).steps).allSatisfy { step -> + assertThat(step.state).isEqualTo(StepState.Success) + } + } + + @Test + fun `given WebView connection, site using application passwords and fully disconnected and using WebView connection, when starting Jetpack connection, then use alternative URL`() = + testBlocking { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = false)) { whenever(selectedSite.connectionType).thenReturn(SiteConnectionType.ApplicationPasswords) whenever( jetpackActivationRepository.fetchJetpackConnectionUrl( @@ -98,10 +171,10 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { } @Test - fun `given site using application passwords and with site-level connection, when starting Jetpack connection, then use default URL`() = + fun `given WebView connection, site using application passwords and with site-level connection , when starting Jetpack connection, then use default URL`() = testBlocking { val connectionUrl = JetpackActivationMainViewModel.JETPACK_SITE_CONNECTED_AUTH_URL_PREFIX - setup(isJetpackInstalled = true) { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = false)) { whenever(selectedSite.connectionType).thenReturn(SiteConnectionType.ApplicationPasswords) whenever( jetpackActivationRepository.fetchJetpackConnectionUrl( @@ -121,10 +194,10 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { } @Test - fun `given site not using application passwords, when starting Jetpack connection, then use default URL`() = + fun `given WebView connection, site not using application passwords and using WebView connection, when starting Jetpack connection, then use default URL`() = testBlocking { val connectionUrl = JetpackActivationMainViewModel.JETPACK_SITE_CONNECTED_AUTH_URL_PREFIX - setup(isJetpackInstalled = true) { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = false)) { whenever(selectedSite.connectionType).thenReturn(null) whenever( jetpackActivationRepository.fetchJetpackConnectionUrl( @@ -144,8 +217,8 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { } @Test - fun `when connection step succeeds, then start connection validation`() = testBlocking { - setup(isJetpackInstalled = true) { + fun `given WebView connection, when connection step succeeds, then start connection validation`() = testBlocking { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = false)) { whenever( jetpackActivationRepository.fetchJetpackConnectionUrl(site = site, useApplicationPasswords = false) ).thenReturn(Result.success("https://example.com/connect")) @@ -167,8 +240,8 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { } @Test - fun `when validation step succeeds, then mark steps as done`() = testBlocking { - setup(isJetpackInstalled = true) { + fun `given WebView connection, when validation step succeeds, then mark steps as done`() = testBlocking { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = false)) { whenever( jetpackActivationRepository.fetchJetpackConnectionUrl(site = site, useApplicationPasswords = false) ).thenReturn(Result.success("https://example.com/connect")) @@ -190,9 +263,9 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { } @Test - fun `when validation step fails due to missing email, then retrying should restart the connection`() = + fun `given WebView connection, when validation step fails due to missing email, then retrying should restart the connection`() = testBlocking { - setup(isJetpackInstalled = true) { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = false)) { whenever( jetpackActivationRepository.fetchJetpackConnectionUrl(site = site, useApplicationPasswords = false) ).thenReturn(Result.success("https://example.com/connect")) @@ -215,8 +288,8 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { } @Test - fun `when connection is dismissed, then trigger correct event`() = testBlocking { - setup(isJetpackInstalled = true) { + fun `given WebView connection, when connection is dismissed, then trigger correct event`() = testBlocking { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = false)) { whenever( jetpackActivationRepository.fetchJetpackConnectionUrl(site = site, useApplicationPasswords = false) ).thenReturn(Result.success("https://example.com/connect")) @@ -230,8 +303,8 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { } @Test - fun `when connection fails due to an error, then show correct state`() = testBlocking { - setup(isJetpackInstalled = true) { + fun `given WebView connection, when connection fails due to an error, then show correct state`() = testBlocking { + setup(createJetpackStatus(isJetpackInstalled = true, supportsConnectionApi = false)) { whenever( jetpackActivationRepository.fetchJetpackConnectionUrl(site = site, useApplicationPasswords = false) ).thenReturn(Result.success("https://example.com/connect")) @@ -250,7 +323,7 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { @Test fun `given site using application passwords, when starting, then allow empty username and password`() = testBlocking { - setup(isJetpackInstalled = false) { + setup(createJetpackStatus(isJetpackInstalled = false)) { whenever(jetpackActivationRepository.getSiteByUrl(siteUrl)).thenReturn( site.apply { username = "" @@ -275,7 +348,7 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { var exception: Throwable? = null Thread.setDefaultUncaughtExceptionHandler { _, e -> exception = e } - setup(isJetpackInstalled = false) { + setup(createJetpackStatus(isJetpackInstalled = false)) { whenever(jetpackActivationRepository.getSiteByUrl(siteUrl)).thenReturn( site.apply { username = "" @@ -290,4 +363,15 @@ class JetpackActivationMainViewModelTest : BaseUnitTest() { // Restore the original handler Thread.setDefaultUncaughtExceptionHandler(originalHandler) } + + private fun createJetpackStatus( + isJetpackInstalled: Boolean = false, + supportsConnectionApi: Boolean = true, + ) = JetpackStatus( + isJetpackInstalled = isJetpackInstalled, + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = if (supportsConnectionApi) JetpackSiteRegistrationStatus.REGISTERED else JetpackSiteRegistrationStatus.UNKNOWN, + blogId = null + ), + ) } 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 c0b78102346..27e470d748d 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 @@ -1,6 +1,8 @@ package com.woocommerce.android.ui.login.jetpack.wpcom import com.woocommerce.android.analytics.AnalyticsTrackerWrapper +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 @@ -22,8 +24,10 @@ class JetpackActivationMagicLinkRequestViewModelTest : BaseUnitTest() { private const val EMAIL = "email@example.com" private val JetpackStatus = JetpackStatus( isJetpackInstalled = true, - isJetpackConnected = false, - wpComEmail = null + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.REGISTERED, + blogId = 1 + ) ) } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComEmailViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComEmailViewModelTest.kt index ab9ae9e3461..093b062cff5 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComEmailViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/login/jetpack/wpcom/JetpackActivationWPComEmailViewModelTest.kt @@ -3,6 +3,8 @@ package com.woocommerce.android.ui.login.jetpack.wpcom import com.woocommerce.android.OnChangedException import com.woocommerce.android.R import com.woocommerce.android.analytics.AnalyticsTrackerWrapper +import com.woocommerce.android.model.JetpackConnectionStatus +import com.woocommerce.android.model.JetpackSiteRegistrationStatus import com.woocommerce.android.model.JetpackStatus import com.woocommerce.android.ui.login.WPComLoginRepository import com.woocommerce.android.ui.login.jetpack.wpcom.JetpackActivationWPComEmailViewModel.ShowMagicLinkScreen @@ -31,8 +33,10 @@ class JetpackActivationWPComEmailViewModelTest : BaseUnitTest() { const val UNKNOWN_USERNAME = "newUser" val JETPACK_STATUS = JetpackStatus( isJetpackInstalled = true, - isJetpackConnected = false, - wpComEmail = "" + jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected( + siteRegistrationStatus = JetpackSiteRegistrationStatus.REGISTERED, + blogId = 1 + ) ) }