Skip to content

Commit

Permalink
Merge pull request #13655 from woocommerce/issue/13628-jetpack-connec…
Browse files Browse the repository at this point in the history
…tion-integration

[Jetpack Setup] Integrate the Connection API for Application Passwords
  • Loading branch information
hichamboushaba authored Mar 10, 2025
2 parents 5548557 + 3d5855b commit 4c5b04d
Show file tree
Hide file tree
Showing 24 changed files with 526 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
)
)
)
)
Expand All @@ -64,29 +69,69 @@ 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)
)
)
)
}
}
}
}

private suspend fun checkIfJetpackIsInstalled(): Result<Boolean> {
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
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
}
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,6 +38,58 @@ class JetpackActivationRepository @Inject constructor(
SiteUtils.getSiteByMatchingUrl(siteStore, url)
}

suspend fun connectJetpackAccount(
site: SiteModel,
jetpackConnectionStatus: JetpackConnectionStatus.AccountNotConnected,
useApplicationPasswords: Boolean
): Result<Unit> {
suspend fun registerSite(): Result<Long> {
return jetpackStore.registerSite(site, useApplicationPasswords).let {
when {
it.isError -> Result.failure<Long>(OnChangedException(it.error))
it.data == null -> Result.failure<Long>(IllegalStateException("Blog ID missing"))
else -> Result.success(it.data!!)
}
}
}

suspend fun connectJetpackAccount(blogId: Long): Result<Unit> {
return jetpackStore.connectJetpackAccount(site, blogId, useApplicationPasswords).let {
if (it.isError) {
Result.failure<Unit>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class JetpackActivationDispatcherFragment : BaseFragment() {
JetpackActivationDispatcherFragmentDirections
.actionJetpackActivationDispatcherFragmentToJetpackActivationStartFragment(
siteUrl = event.siteUrl,
isJetpackInstalled = event.isJetpackInstalled
jetpackStatus = event.jetpackStatus
)
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
)
)
Expand All @@ -39,8 +40,8 @@ class JetpackActivationDispatcherViewModel @Inject constructor(
// Handle connecting a new site
triggerEvent(
StartJetpackActivationForNewSite(
args.siteUrl,
jetpackStatus.isJetpackInstalled
siteUrl = args.siteUrl,
jetpackStatus = jetpackStatus
)
)
}
Expand All @@ -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(
Expand Down
Loading

0 comments on commit 4c5b04d

Please sign in to comment.