Skip to content

Commit 51f62d3

Browse files
Merge pull request #13308 from woocommerce/issue/3974-remove-xmlrpc-account-mismatch
[Login] Remove dependency on XMLRPC for the Account Mismatch flow
2 parents bbebf6c + 1e6f712 commit 51f62d3

File tree

4 files changed

+161
-132
lines changed

4 files changed

+161
-132
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- [**] Enhanced WebView to dynamically scale receipt content, ensuring optimal fit across all device screen sizes. [https://github.com/woocommerce/woocommerce-android/pull/13266]
77
- [*] Fixes missing text in Blaze Campaigns card on larger display and font sizes [https://github.com/woocommerce/woocommerce-android/pull/13300]
88
- [*] Puerto Rico is now available in the list of countries that are supported by in-person payments [https://github.com/woocommerce/woocommerce-android/pull/13200]
9+
- [Internal] [*] XMLRPC is not needed now for Jetpack Connection during an Account Mismatch flow [https://github.com/woocommerce/woocommerce-android/pull/13308]
910
- [*] Removed the outdated feedback survey for shipping labels [https://github.com/woocommerce/woocommerce-android/pull/13319]
1011

1112
21.4

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/login/accountmismatch/AccountMismatchErrorViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ class AccountMismatchErrorViewModel @Inject constructor(
263263
val site = site.await() ?: error("The site is not cached")
264264
accountMismatchRepository.fetchJetpackConnectedEmail(site).fold(
265265
onSuccess = { email ->
266+
// Remove the WPAPI site from DB before continuing
267+
// This ensure we don't have a duplicate site with a wrong origin
268+
accountMismatchRepository.removeSiteFromDB(site)
269+
266270
val isUserAuthenticated = accountRepository.isUserLoggedIn() &&
267271
accountRepository.getUserAccount()?.email == email
268272
triggerEvent(OnJetpackConnectedEvent(email, isAuthenticated = isUserAuthenticated))
Lines changed: 48 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,33 @@
11
package com.woocommerce.android.ui.login.accountmismatch
22

33
import com.woocommerce.android.OnChangedException
4+
import com.woocommerce.android.ui.login.WPApiSiteRepository
45
import com.woocommerce.android.util.WooLog
5-
import com.woocommerce.android.util.awaitAny
6-
import com.woocommerce.android.util.awaitEvent
7-
import com.woocommerce.android.util.dispatchAndAwait
86
import kotlinx.coroutines.Dispatchers
9-
import kotlinx.coroutines.async
10-
import kotlinx.coroutines.coroutineScope
117
import kotlinx.coroutines.withContext
128
import org.wordpress.android.fluxc.Dispatcher
13-
import org.wordpress.android.fluxc.generated.AuthenticationActionBuilder
149
import org.wordpress.android.fluxc.generated.SiteActionBuilder
1510
import org.wordpress.android.fluxc.model.SiteModel
16-
import org.wordpress.android.fluxc.store.AccountStore.OnAuthenticationChanged
17-
import org.wordpress.android.fluxc.store.AccountStore.OnDiscoveryResponse
11+
import org.wordpress.android.fluxc.model.jetpack.JetpackUser
1812
import org.wordpress.android.fluxc.store.JetpackStore
1913
import org.wordpress.android.fluxc.store.SiteStore
20-
import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged
21-
import org.wordpress.android.fluxc.store.SiteStore.RefreshSitesXMLRPCPayload
2214
import org.wordpress.android.login.util.SiteUtils
2315
import javax.inject.Inject
2416

2517
class AccountMismatchRepository @Inject constructor(
2618
private val jetpackStore: JetpackStore,
2719
private val siteStore: SiteStore,
20+
private val wpApiSiteRepository: WPApiSiteRepository,
2821
private val dispatcher: Dispatcher
2922
) {
3023
suspend fun getSiteByUrl(url: String): SiteModel? = withContext(Dispatchers.IO) {
3124
SiteUtils.getSiteByMatchingUrl(siteStore, url)
3225
}
3326

27+
fun removeSiteFromDB(site: SiteModel) {
28+
dispatcher.dispatch(SiteActionBuilder.newRemoveSiteAction(site))
29+
}
30+
3431
suspend fun fetchJetpackConnectionUrl(site: SiteModel): Result<String> {
3532
WooLog.d(WooLog.T.LOGIN, "Fetching Jetpack Connection URL")
3633
val result = jetpackStore.fetchJetpackConnectionUrl(site, autoRegisterSiteIfNeeded = true)
@@ -39,6 +36,7 @@ class AccountMismatchRepository @Inject constructor(
3936
WooLog.w(WooLog.T.LOGIN, "Fetching Jetpack Connection URL failed: ${result.error.message}")
4037
Result.failure(OnChangedException(result.error, result.error.message))
4138
}
39+
4240
else -> {
4341
WooLog.d(WooLog.T.LOGIN, "Jetpack connection URL fetched successfully")
4442
Result.success(result.url)
@@ -48,21 +46,21 @@ class AccountMismatchRepository @Inject constructor(
4846

4947
suspend fun fetchJetpackConnectedEmail(site: SiteModel): Result<String> {
5048
WooLog.d(WooLog.T.LOGIN, "Fetching email of Jetpack User")
51-
val result = jetpackStore.fetchJetpackUser(site)
52-
return when {
53-
result.isError -> {
54-
WooLog.w(WooLog.T.LOGIN, "Fetching Jetpack User failed error: $result.error.message")
55-
Result.failure(OnChangedException(result.error, result.error.message))
56-
}
57-
result.user?.wpcomEmail.isNullOrEmpty() -> {
58-
WooLog.w(WooLog.T.LOGIN, "Cannot find Jetpack Email in response")
59-
Result.failure(Exception("Email missing from response"))
60-
}
61-
else -> {
62-
WooLog.d(WooLog.T.LOGIN, "Jetpack User fetched successfully")
63-
Result.success(result.user!!.wpcomEmail)
49+
50+
return fetchJetpackUser(site)
51+
.onFailure {
52+
WooLog.w(WooLog.T.LOGIN, "Fetching Jetpack User failed error: ${it.message}")
53+
}.mapCatching {
54+
val wpcomEmail = it?.wpcomEmail
55+
if (wpcomEmail.isNullOrEmpty()) {
56+
WooLog.w(WooLog.T.LOGIN, "Cannot find Jetpack Email in response")
57+
@Suppress("TooGenericExceptionThrown")
58+
throw Exception("Email missing from response")
59+
} else {
60+
WooLog.d(WooLog.T.LOGIN, "Jetpack User fetched successfully")
61+
wpcomEmail
62+
}
6463
}
65-
}
6664
}
6765

6866
suspend fun checkJetpackConnection(
@@ -72,121 +70,39 @@ class AccountMismatchRepository @Inject constructor(
7270
): Result<JetpackConnectionStatus> {
7371
WooLog.d(WooLog.T.LOGIN, "Checking Jetpack Connection status for site $siteUrl")
7472

75-
return discoverXMLRPCAddress(siteUrl)
76-
.mapCatching { xmlrpcEndpoint ->
77-
val xmlrpcSite = fetchXMLRPCSite(siteUrl, xmlrpcEndpoint, username, password).getOrThrow()
78-
when {
79-
xmlrpcSite.jetpackUserEmail.isNullOrEmpty() -> {
80-
WooLog.d(WooLog.T.LOGIN, "Jetpack site is not connected to a WPCom account")
81-
JetpackConnectionStatus.NotConnected
82-
}
83-
else -> {
84-
WooLog.d(
85-
tag = WooLog.T.LOGIN,
86-
message = "Jetpack site is connected to different WPCom account:" +
87-
" ${xmlrpcSite.jetpackUserEmail}"
88-
)
89-
JetpackConnectionStatus.ConnectedToDifferentAccount(xmlrpcSite.jetpackUserEmail)
90-
}
91-
}
92-
}
93-
}
94-
95-
/**
96-
* Represents Jetpack Connection status for the current wp-admin account
97-
*/
98-
sealed interface JetpackConnectionStatus {
99-
object NotConnected : JetpackConnectionStatus
100-
data class ConnectedToDifferentAccount(val wpcomEmail: String) : JetpackConnectionStatus
101-
}
102-
103-
private suspend fun discoverXMLRPCAddress(siteUrl: String): Result<String> {
104-
WooLog.d(WooLog.T.LOGIN, "Running discovery to fetch XMLRPC endpoint for site $siteUrl")
105-
106-
val action = AuthenticationActionBuilder.newDiscoverEndpointAction(siteUrl)
107-
val event: OnDiscoveryResponse = dispatcher.dispatchAndAwait(action)
108-
109-
return if (event.isError) {
110-
WooLog.w(WooLog.T.LOGIN, "XMLRPC Discovery failed, error: ${event.error}")
111-
Result.failure(OnChangedException(event.error, event.error.name))
112-
} else {
113-
WooLog.d(
114-
tag = WooLog.T.LOGIN,
115-
message = "XMLRPC Discovery succeeded, xmrlpc endpoint: ${event.xmlRpcEndpoint}"
116-
)
117-
Result.success(event.xmlRpcEndpoint)
73+
val site = wpApiSiteRepository.fetchSite(siteUrl, username, password).getOrElse {
74+
WooLog.w(WooLog.T.LOGIN, "Site fetch failed, error: ${it.message}")
75+
return Result.failure(it)
11876
}
119-
}
12077

121-
@Suppress("ReturnCount")
122-
private suspend fun fetchXMLRPCSite(
123-
siteUrl: String,
124-
xmlrpcEndpoint: String,
125-
username: String,
126-
password: String
127-
): Result<SiteModel> = coroutineScope {
128-
val authenticationTask = async {
129-
dispatcher.awaitEvent<OnAuthenticationChanged>().also { event ->
130-
if (event.isError) {
131-
WooLog.w(
132-
tag = WooLog.T.LOGIN,
133-
message = "Authenticating to XMLRPC site $xmlrpcEndpoint failed, " +
134-
"error: ${event.error.message}"
135-
)
136-
}
78+
return fetchJetpackUser(site).onFailure {
79+
WooLog.w(WooLog.T.LOGIN, "Jetpack User fetch failed, error: ${it.message}")
80+
}.map {
81+
if (it?.isConnected != true) {
82+
WooLog.w(WooLog.T.LOGIN, "Account is not connected to a WPCom account")
83+
JetpackConnectionStatus.NotConnected
84+
} else {
85+
WooLog.d(WooLog.T.LOGIN, "Account is connected to different WPCom account: ${it.wpcomEmail}")
86+
JetpackConnectionStatus.ConnectedToDifferentAccount(it.wpcomEmail)
13787
}
13888
}
139-
val siteTask = async {
140-
val selfHostedPayload = RefreshSitesXMLRPCPayload(
141-
username = username,
142-
password = password,
143-
url = xmlrpcEndpoint
144-
)
145-
dispatcher.dispatchAndAwait<RefreshSitesXMLRPCPayload, OnSiteChanged>(
146-
action = SiteActionBuilder.newFetchSitesXmlRpcAction(
147-
selfHostedPayload
148-
)
149-
).also { event ->
150-
if (event.isError) {
151-
WooLog.w(
152-
tag = WooLog.T.LOGIN,
153-
message = "XMLRPC site $xmlrpcEndpoint fetch failed, error: ${event.error.message}"
154-
)
155-
} else {
156-
WooLog.d(WooLog.T.LOGIN, "XMLRPC site $xmlrpcEndpoint fetch succeeded")
157-
}
158-
}
159-
}
160-
161-
val tasks = listOf(authenticationTask, siteTask)
162-
163-
val event = tasks.awaitAny()
164-
165-
if (event.isError) return@coroutineScope Result.failure(OnChangedException(event.error))
166-
167-
return@coroutineScope fetchSiteOptions(siteUrl)
16889
}
16990

170-
private suspend fun fetchSiteOptions(siteUrl: String): Result<SiteModel> {
171-
WooLog.d(WooLog.T.LOGIN, "Fetch XMLRPC site options")
172-
val site = getSiteByUrl(siteUrl) ?: run {
173-
WooLog.e(WooLog.T.LOGIN, "XMLRPC site null after fetching!!")
174-
return Result.failure(IllegalStateException("XMLRPC Site not found"))
175-
}
176-
177-
val fetchSiteResult = siteStore.fetchSite(site)
178-
return when {
179-
fetchSiteResult.isError -> {
180-
WooLog.w(
181-
tag = WooLog.T.LOGIN,
182-
message = "XMLRPC site $siteUrl fetch failed, error: ${fetchSiteResult.error.message}"
183-
)
184-
Result.failure(OnChangedException(fetchSiteResult.error, fetchSiteResult.error.message))
185-
}
186-
else -> {
187-
WooLog.d(WooLog.T.LOGIN, "XMLRPC site $siteUrl fetch succeeded")
188-
Result.success(getSiteByUrl(siteUrl)!!)
91+
private suspend fun fetchJetpackUser(site: SiteModel): Result<JetpackUser?> {
92+
return jetpackStore.fetchJetpackUser(site).let {
93+
if (it.isError) {
94+
Result.failure(OnChangedException(it.error, it.error.message))
95+
} else {
96+
Result.success(it.user)
18997
}
19098
}
19199
}
100+
101+
/**
102+
* Represents Jetpack Connection status for the current wp-admin account
103+
*/
104+
sealed interface JetpackConnectionStatus {
105+
data object NotConnected : JetpackConnectionStatus
106+
data class ConnectedToDifferentAccount(val wpcomEmail: String) : JetpackConnectionStatus
107+
}
192108
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.woocommerce.android.ui.login.accountmismatch
2+
3+
import com.woocommerce.android.FakeDispatcher
4+
import com.woocommerce.android.ui.login.WPApiSiteRepository
5+
import com.woocommerce.android.viewmodel.BaseUnitTest
6+
import kotlinx.coroutines.ExperimentalCoroutinesApi
7+
import org.assertj.core.api.Assertions.assertThat
8+
import org.junit.Test
9+
import org.mockito.kotlin.any
10+
import org.mockito.kotlin.eq
11+
import org.mockito.kotlin.mock
12+
import org.mockito.kotlin.whenever
13+
import org.wordpress.android.fluxc.model.SiteModel
14+
import org.wordpress.android.fluxc.model.jetpack.JetpackUser
15+
import org.wordpress.android.fluxc.store.JetpackStore
16+
import org.wordpress.android.fluxc.store.SiteStore
17+
18+
@OptIn(ExperimentalCoroutinesApi::class)
19+
class AccountMismatchRepositoryTest : BaseUnitTest() {
20+
private val jetpackStore = mock<JetpackStore>()
21+
private val siteStore = mock<SiteStore>()
22+
private val wpApiSiteRepository = mock<WPApiSiteRepository> {
23+
onBlocking { fetchSite(any(), any(), any()) }.thenReturn(Result.success(SiteModel()))
24+
}
25+
private val dispatcher = FakeDispatcher()
26+
27+
private val sut = AccountMismatchRepository(
28+
jetpackStore = jetpackStore,
29+
siteStore = siteStore,
30+
wpApiSiteRepository = wpApiSiteRepository,
31+
dispatcher = dispatcher
32+
)
33+
34+
@Test
35+
fun `given a non-connected Jetpack Account, when fetching status, then return correct status`() = testBlocking {
36+
whenever(jetpackStore.fetchJetpackUser(any(), eq(false)))
37+
.thenReturn(JetpackStore.JetpackUserResult(createJetpackUser(isConnected = false)))
38+
39+
val result = sut.checkJetpackConnection(
40+
siteUrl = "https://example.com",
41+
username = "username",
42+
password = "password"
43+
)
44+
45+
assertThat(result.getOrNull()).isEqualTo(AccountMismatchRepository.JetpackConnectionStatus.NotConnected)
46+
}
47+
48+
@Test
49+
fun `given a null jetpack user, when fetching connection status, then assume non-connected`() = testBlocking {
50+
whenever(jetpackStore.fetchJetpackUser(any(), eq(false)))
51+
.thenReturn(JetpackStore.JetpackUserResult(null))
52+
53+
val result = sut.checkJetpackConnection(
54+
siteUrl = "https://example.com",
55+
username = "username",
56+
password = "password"
57+
)
58+
59+
assertThat(result.getOrNull()).isEqualTo(AccountMismatchRepository.JetpackConnectionStatus.NotConnected)
60+
}
61+
62+
@Test
63+
fun `given a connected Jetpack Account, when fetching status, then return correct status`() = testBlocking {
64+
whenever(jetpackStore.fetchJetpackUser(any(), eq(false)))
65+
.thenReturn(JetpackStore.JetpackUserResult(createJetpackUser(isConnected = true, wpcomEmail = "email")))
66+
67+
val result = sut.checkJetpackConnection(
68+
siteUrl = "https://example.com",
69+
username = "username",
70+
password = "password"
71+
)
72+
73+
assertThat(result.getOrNull())
74+
.isEqualTo(AccountMismatchRepository.JetpackConnectionStatus.ConnectedToDifferentAccount("email"))
75+
}
76+
77+
@Test
78+
fun `given a correctly connected Jetpack account, when fetching email, then return it`() = testBlocking {
79+
whenever(jetpackStore.fetchJetpackUser(any(), eq(false)))
80+
.thenReturn(JetpackStore.JetpackUserResult(createJetpackUser(isConnected = true, wpcomEmail = "email")))
81+
82+
val result = sut.fetchJetpackConnectedEmail(SiteModel())
83+
84+
assertThat(result.getOrNull()).isEqualTo("email")
85+
}
86+
87+
@Test
88+
fun `given an issue with Jetpack account, when fetching email, then return error`() = testBlocking {
89+
whenever(jetpackStore.fetchJetpackUser(any(), eq(false)))
90+
.thenReturn(JetpackStore.JetpackUserResult(createJetpackUser(isConnected = true, wpcomEmail = "")))
91+
92+
val result = sut.fetchJetpackConnectedEmail(SiteModel())
93+
94+
assertThat(result.isFailure).isTrue()
95+
}
96+
97+
private fun createJetpackUser(
98+
isConnected: Boolean = false,
99+
wpcomEmail: String = ""
100+
) = JetpackUser(
101+
isConnected = isConnected,
102+
wpcomEmail = wpcomEmail,
103+
isMaster = false,
104+
username = "username",
105+
wpcomId = 1L,
106+
wpcomUsername = "wpcomUsername"
107+
)
108+
}

0 commit comments

Comments
 (0)