Skip to content

Commit

Permalink
Merge pull request #13682 from woocommerce/issue/13665-jetpack-connec…
Browse files Browse the repository at this point in the history
…tion-api-cookies

[Jetpack Setup] Jetpack Connection API using Cookies+Nonce authentication
  • Loading branch information
hichamboushaba authored Mar 11, 2025
2 parents 02d9b8e + 1f900f4 commit b673397
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 73 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.woocommerce.android.ui.jetpack.benefits
package com.woocommerce.android.ui.jetpack

import com.woocommerce.android.OnChangedException
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.SiteModel
import org.wordpress.android.fluxc.model.jetpack.JetpackConnectionData
import org.wordpress.android.fluxc.store.JetpackStore
import org.wordpress.android.fluxc.store.WooCommerceStore
Expand All @@ -25,7 +25,6 @@ import javax.inject.Inject
*/
class FetchJetpackStatus @Inject constructor(
private val jetpackStore: JetpackStore,
private val selectedSite: SelectedSite,
private val wooCommerceStore: WooCommerceStore
) {
companion object {
Expand All @@ -40,10 +39,14 @@ class FetchJetpackStatus @Inject constructor(
}

@Suppress("ReturnCount", "NestedBlockDepth")
suspend operator fun invoke(): Result<JetpackStatusFetchResponse> {
suspend operator fun invoke(
site: SiteModel,
useApplicationPasswords: Boolean,
isJetpackInstalled: Boolean? = null
): Result<JetpackStatusFetchResponse> {
return jetpackStore.fetchJetpackConnectionData(
site = selectedSite.get(),
useApplicationPasswords = true
site = site,
useApplicationPasswords = useApplicationPasswords
).let { userResult ->
when {
userResult.error?.errorCode == FORBIDDEN_CODE -> {
Expand All @@ -69,7 +72,7 @@ class FetchJetpackStatus @Inject constructor(
}

else -> {
val isJetpackInstalled = checkIfJetpackIsInstalled().getOrElse {
val isJetpackInstalled = isJetpackInstalled ?: checkIfJetpackIsInstalled(site).getOrElse {
return Result.failure(it)
}

Expand All @@ -88,8 +91,8 @@ class FetchJetpackStatus @Inject constructor(
}
}

private suspend fun checkIfJetpackIsInstalled(): Result<Boolean> {
return wooCommerceStore.fetchSitePlugins(selectedSite.get())
private suspend fun checkIfJetpackIsInstalled(site: SiteModel): Result<Boolean> {
return wooCommerceStore.fetchSitePlugins(site)
.let { pluginResult ->
when {
pluginResult.isError -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import com.woocommerce.android.model.UserRole
import com.woocommerce.android.support.help.HelpOrigin
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.common.UserEligibilityFetcher
import com.woocommerce.android.ui.jetpack.benefits.FetchJetpackStatus
import com.woocommerce.android.ui.jetpack.benefits.FetchJetpackStatus.JetpackStatusFetchResponse
import com.woocommerce.android.ui.jetpack.FetchJetpackStatus.JetpackStatusFetchResponse
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.NavigateToHelpScreen
Expand Down Expand Up @@ -56,7 +55,10 @@ class JetpackActivationEligibilityErrorViewModel @Inject constructor(

fun onRetryButtonClicked() = launch {
isRetrying.value = true
val jetpackStatusResult = fetchJetpackStatus()
val jetpackStatusResult = fetchJetpackStatus(
site = selectedSite.get(),
useApplicationPasswords = true
)
handleJetpackStatusResult(jetpackStatusResult)
isRetrying.value = false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import com.woocommerce.android.model.UserRole
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.tools.SiteConnectionType
import com.woocommerce.android.ui.common.UserEligibilityFetcher
import com.woocommerce.android.ui.jetpack.benefits.FetchJetpackStatus.JetpackStatusFetchResponse
import com.woocommerce.android.ui.jetpack.FetchJetpackStatus
import com.woocommerce.android.ui.jetpack.FetchJetpackStatus.JetpackStatusFetchResponse
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ShowSnackbar
Expand Down Expand Up @@ -62,12 +63,16 @@ class JetpackBenefitsViewModel @Inject constructor(

triggerEvent(StartJetpackActivationForJetpackCP)
}

SiteConnectionType.ApplicationPasswords -> {
AnalyticsTracker.track(stat = JETPACK_BENEFITS_LOGIN_BUTTON_TAPPED)

_viewState.update { it.copy(isLoadingDialogShown = true) }

val jetpackStatusResult = fetchJetpackStatus()
val jetpackStatusResult = fetchJetpackStatus(
site = selectedSite.get(),
useApplicationPasswords = true
)
handleJetpackStatusResult(jetpackStatusResult)

_viewState.update { it.copy(isLoadingDialogShown = false) }
Expand Down Expand Up @@ -109,6 +114,7 @@ class JetpackBenefitsViewModel @Inject constructor(
hasInstallCapability && statusCode == ERROR_CODE_NOT_FOUND && jetpackStatus != null -> {
startJetpackActivation(jetpackStatus)
}

else -> {
triggerEvent(OpenJetpackEligibilityError(user.username, user.roles.first().value))

Expand Down Expand Up @@ -138,6 +144,7 @@ class JetpackBenefitsViewModel @Inject constructor(
handleUserEligibility(ERROR_CODE_NOT_FOUND, fetchResponse.status)
}
}

JetpackStatusFetchResponse.ConnectionForbidden -> handleUserEligibility(ERROR_CODE_FORBIDDEN)
}
},
Expand Down Expand Up @@ -184,6 +191,7 @@ class JetpackBenefitsViewModel @Inject constructor(
val siteUrl: String,
val jetpackStatus: JetpackStatus
) : Event()

data class OpenWpAdminJetpackActivation(val activationUrl: String) : Event()
data class OpenJetpackEligibilityError(val username: String, val role: String) : Event()
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ 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.JetpackConnectionStatus
import com.woocommerce.android.model.JetpackSiteRegistrationStatus
import com.woocommerce.android.model.JetpackStatus
import com.woocommerce.android.model.UiString
import com.woocommerce.android.model.UiString.UiStringRes
import com.woocommerce.android.ui.jetpack.FetchJetpackStatus
import com.woocommerce.android.ui.jetpack.FetchJetpackStatus.JetpackStatusFetchResponse
import com.woocommerce.android.ui.login.WPApiSiteRepository
import com.woocommerce.android.ui.login.WPApiSiteRepository.CookieNonceAuthenticationException
import com.woocommerce.android.viewmodel.MultiLiveEvent
Expand All @@ -24,6 +28,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.store.SiteStore.SiteError
import org.wordpress.android.util.UrlUtils
import javax.inject.Inject
Expand All @@ -32,7 +37,8 @@ import javax.inject.Inject
class JetpackActivationSiteCredentialsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val wpApiSiteRepository: WPApiSiteRepository,
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,
private val fetchJetpackStatus: FetchJetpackStatus
) : ScopedViewModel(savedStateHandle) {
private val navArgs: JetpackActivationSiteCredentialsFragmentArgs by savedStateHandle.navArgs()

Expand Down Expand Up @@ -89,12 +95,7 @@ class JetpackActivationSiteCredentialsViewModel @Inject constructor(
).fold(
onSuccess = {
analyticsTrackerWrapper.track(AnalyticsEvent.LOGIN_JETPACK_SITE_CREDENTIAL_DID_FINISH_LOGIN)
triggerEvent(
NavigateToJetpackActivationSteps(
siteUrl = navArgs.siteUrl,
jetpackStatus = navArgs.jetpackStatus
)
)
fetchJetpackStatusAndContinue(it)
},
onFailure = { exception ->
val authenticationError = exception as? CookieNonceAuthenticationException
Expand All @@ -120,6 +121,36 @@ class JetpackActivationSiteCredentialsViewModel @Inject constructor(
_viewState.update { it.copy(isLoading = false) }
}

private suspend fun fetchJetpackStatusAndContinue(site: SiteModel) {
fetchJetpackStatus(
site = site,
useApplicationPasswords = false,
isJetpackInstalled = navArgs.jetpackStatus.isJetpackInstalled
).fold(
onSuccess = {
val jetpackStatus = when (it) {
is JetpackStatusFetchResponse.Success -> it.status
is JetpackStatusFetchResponse.ConnectionForbidden -> {
// When we can't fetch the connection data, we know that the site is not registered with Jetpack
// The user won't be to connect to Jetpack, and the next screen will show the error message
// So we can just proceed with default values
JetpackStatus(
isJetpackInstalled = navArgs.jetpackStatus.isJetpackInstalled,
jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected(
siteRegistrationStatus = JetpackSiteRegistrationStatus.NOT_REGISTERED,
blogId = null
)
)
}
}
triggerEvent(NavigateToJetpackActivationSteps(navArgs.siteUrl, jetpackStatus))
},
onFailure = {
triggerEvent(ShowUiStringSnackbar(UiStringRes(R.string.error_generic)))
}
)
}

@Parcelize
data class JetpackActivationSiteCredentialsViewState(
val isJetpackInstalled: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.woocommerce.android.NavGraphMainDirections
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
Expand All @@ -21,15 +18,11 @@ import com.woocommerce.android.model.JetpackStatus
import com.woocommerce.android.support.help.HelpOrigin
import com.woocommerce.android.support.requests.SupportRequestFormActivity
import com.woocommerce.android.ui.base.BaseFragment
import com.woocommerce.android.ui.common.webview.AuthenticatedWebViewFragment
import com.woocommerce.android.ui.common.webview.AuthenticatedWebViewViewModel
import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
import com.woocommerce.android.ui.login.LoginActivity
import com.woocommerce.android.ui.login.accountmismatch.AccountMismatchErrorFragment
import com.woocommerce.android.ui.main.AppBarStatus
import com.woocommerce.android.ui.sitepicker.sitediscovery.SitePickerSiteDiscoveryViewModel.CreateZendeskTicket
import com.woocommerce.android.ui.sitepicker.sitediscovery.SitePickerSiteDiscoveryViewModel.StartNativeJetpackActivation
import com.woocommerce.android.ui.sitepicker.sitediscovery.SitePickerSiteDiscoveryViewModel.StartWebBasedJetpackInstallation
import com.woocommerce.android.ui.sitepicker.sitediscovery.SitePickerSiteDiscoveryViewModel.StartJetpackActivation
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ExitWithResult
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Logout
Expand All @@ -41,8 +34,6 @@ import org.wordpress.android.login.LoginMode
class SitePickerSiteDiscoveryFragment : BaseFragment() {
companion object {
const val SITE_PICKER_SITE_ADDRESS_RESULT = "site-url"
private const val JETPACK_CONNECT_URL = "https://wordpress.com/jetpack/connect"
private const val JETPACK_CONNECTED_REDIRECT_URL = "woocommerce://jetpack-connected"
}

private val viewModel: SitePickerSiteDiscoveryViewModel by viewModels()
Expand All @@ -65,55 +56,30 @@ class SitePickerSiteDiscoveryFragment : BaseFragment() {
super.onViewCreated(view, savedInstanceState)

setupObservers()
setupResultHandlers()
}

private fun setupObservers() {
viewModel.event.observe(viewLifecycleOwner) { event ->
when (event) {
is CreateZendeskTicket -> startSupportRequestForm()
is NavigateToHelpScreen -> navigateToHelpScreen(event.origin)
is StartWebBasedJetpackInstallation -> startWebBasedJetpackInstallation(event.siteAddress)
is StartNativeJetpackActivation -> startNativeJetpackActivation(event)
is StartJetpackActivation -> startJetpackActivation(event)
is Logout -> onLogout()
is ExitWithResult<*> -> navigateBackWithResult(SITE_PICKER_SITE_ADDRESS_RESULT, event.data)
is Exit -> findNavController().navigateUp()
}
}
}

private fun setupResultHandlers() {
handleNotice(AuthenticatedWebViewFragment.WEBVIEW_RESULT) {
viewModel.onJetpackInstalled()
}
handleNotice(AccountMismatchErrorFragment.JETPACK_CONNECTED_NOTICE) {
viewModel.onJetpackConnected()
}
}

private fun startWebBasedJetpackInstallation(siteAddress: String) {
val url = "$JETPACK_CONNECT_URL?" +
"url=$siteAddress" +
"&mobile_redirect=$JETPACK_CONNECTED_REDIRECT_URL" +
"&from=mobile"

findNavController().navigate(
NavGraphMainDirections.actionGlobalAuthenticatedWebViewFragment(
urlToLoad = url,
urlsToTriggerExit = arrayOf(JETPACK_CONNECTED_REDIRECT_URL),
urlComparisonMode = AuthenticatedWebViewViewModel.UrlComparisonMode.EQUALITY,
title = getString(R.string.login_jetpack_install)
)
)
}

private fun startNativeJetpackActivation(event: StartNativeJetpackActivation) {
private fun startJetpackActivation(event: StartJetpackActivation) {
findNavController().navigate(
SitePickerSiteDiscoveryFragmentDirections
.actionSitePickerSiteDiscoveryFragmentToJetpackActivation(
siteUrl = event.siteAddress,
jetpackStatus = JetpackStatus(
isJetpackInstalled = event.isJetpackInstalled,
// Pass a default value, we'll update it later after the user signs in
// See JetpackActivationSiteCredentialsViewModel
jetpackConnectionStatus = JetpackConnectionStatus.AccountNotConnected(
siteRegistrationStatus = JetpackSiteRegistrationStatus.UNKNOWN,
blogId = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class SitePickerSiteDiscoveryViewModel @Inject constructor(
!it.isWordPress -> stepFlow.value = Step.NotWordpress
!it.isWPCom -> {
triggerEvent(
StartNativeJetpackActivation(
StartJetpackActivation(
siteAddress = siteAddress,
isJetpackInstalled = it.isJetpackActive
)
Expand All @@ -206,14 +206,6 @@ class SitePickerSiteDiscoveryViewModel @Inject constructor(
triggerEvent(NavigateToHelpScreen(LOGIN_SITE_ADDRESS))
}

fun onJetpackInstalled() {
navigateBackToSitePicker()
}

fun onJetpackConnected() {
navigateBackToSitePicker()
}

private fun navigateBackToSitePicker() {
fetchedSiteUrl.let { url ->
requireNotNull(url)
Expand Down Expand Up @@ -263,8 +255,7 @@ class SitePickerSiteDiscoveryViewModel @Inject constructor(
}

object CreateZendeskTicket : MultiLiveEvent.Event()
data class StartWebBasedJetpackInstallation(val siteAddress: String) : MultiLiveEvent.Event()
data class StartNativeJetpackActivation(
data class StartJetpackActivation(
val siteAddress: String,
val isJetpackInstalled: Boolean
) : MultiLiveEvent.Event()
Expand Down
2 changes: 1 addition & 1 deletion WooCommerce/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@
<string name="login_jetpack_installation_retry_activating">Try activating again</string>
<string name="login_jetpack_installation_retry_authorizing">Try authorising again</string>
<string name="login_jetpack_installation_cancel">Cancel installation</string>
<string name="login_jetpack_installation_error_forbidden_suggestion">Please contact your shop manager or administrator for help.</string>
<string name="login_jetpack_installation_error_forbidden_suggestion">Please contact your administrator for help.</string>
<string name="login_jetpack_installation_error_connection_permission_message">You don’t have permission to connect to Jetpack on this store</string>
<string name="login_jetpack_installation_connection_dismissed">Jetpack is installed, but not connected.</string>
<string name="login_jetpack_installation_connection_dismissed_explanation">Try connecting again to access your store.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.woocommerce.android.model.UserRole
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.tools.SiteConnectionType
import com.woocommerce.android.ui.common.UserEligibilityFetcher
import com.woocommerce.android.ui.jetpack.FetchJetpackStatus
import com.woocommerce.android.viewmodel.BaseUnitTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.assertj.core.api.Assertions.assertThat
Expand Down Expand Up @@ -183,7 +184,7 @@ class JetpackBenefitsViewModelTest : BaseUnitTest() {
jetpackStatusFetchResponse: FetchJetpackStatus.JetpackStatusFetchResponse
) = testBlocking {
val result = Result.success(jetpackStatusFetchResponse)
whenever(fetchJetpackStatus.invoke()).thenReturn(result)
whenever(fetchJetpackStatus.invoke(site = siteModelMock, useApplicationPasswords = true)).thenReturn(result)
}

private fun givenUserEligibility(user: User, role: UserRole) = testBlocking {
Expand Down
Loading

0 comments on commit b673397

Please sign in to comment.