Skip to content

Commit 763ed78

Browse files
authored
Jetpack REST connection: connect site and user (#22126)
* Skip wp.com login if access token exists * First pass at installing Jetpack * Second pass at installing Jetpack * First pass at handling error response * Return a pair for installJetpack and restored launch wpcom login from UI thread * Added InstallJetpackResult to make it clear the second item in the pair is an HTTP status code * Added InstallJetpackResult to make it clear the second item in the pair is an HTTP status code * refresh the site before starting jetpack install * Fixed detekt warnings, fetch site directly * Added/updated comments * Handle inactive Jetpack install, refresh site in init * Tweaked comments * Restored check for access token * Check if we need to install or simply activate the jp plugin * Simplified JetpackInstaller to always install the plugin * Increased timeout * Fixed Detekt warnings * Simplified JetpackInstaller.kt * Simplified JetpackInstaller.kt, p2 * Added log message * Get plugin status before activating * Use correct names for slugs * Removed iOS commented code * Reduced timeout from 60 to 45 seconds * Simplified installer * Code cleanup * Removed refreshSite * Pass WpApiClient as a parameter * Added missing space * Use a Result<PluginStatus> for installJetpack * Add a delay to wp.com login if already logged in * Removed unnecessary launch * Suppress TooGenericExceptionCaught * Moved JetpackConnectionHelper to its own helper class * Connect Jetpack to site * Handle failure * Added fun to connect user * Added error handling to connect user * Updated error handling to connect user * Remove extra message from all errors except ErrorType.Unknown * Throw exception on invalid authentication * Simplified JetpackConnectionHelper.kt * Simplified JetpackConnector * Code tweaks * Further simplified JetpackConnector.kt * Modified text & icon for user connection step * Clarified that REST credentials are required * Corrected ConnectUser step completion * Corrected ConnectUser step failure * Removed unnecessary job?.cancel * Minor code cleanup
1 parent 3d0e12d commit 763ed78

File tree

6 files changed

+206
-48
lines changed

6 files changed

+206
-48
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.wordpress.android.ui.jetpackrestconnection
2+
3+
import org.wordpress.android.fluxc.model.SiteModel
4+
import org.wordpress.android.fluxc.utils.AppLogWrapper
5+
import org.wordpress.android.util.AppLog
6+
import rs.wordpress.api.kotlin.WpApiClient
7+
import rs.wordpress.api.kotlin.WpRequestExecutor
8+
import uniffi.wp_api.JetpackConnectionClient
9+
import uniffi.wp_api.ParsedUrl
10+
import uniffi.wp_api.WpApiClientDelegate
11+
import uniffi.wp_api.WpApiMiddlewarePipeline
12+
import uniffi.wp_api.WpAppNotifier
13+
import uniffi.wp_api.WpAuthenticationProvider
14+
import java.net.URL
15+
import javax.inject.Inject
16+
17+
class JetpackConnectionHelper @Inject constructor(
18+
private val appLogWrapper: AppLogWrapper
19+
) {
20+
fun initWpApiClient(site: SiteModel): WpApiClient {
21+
requireRestCredentials(site)
22+
return WpApiClient(
23+
wpOrgSiteApiRootUrl = URL(resolveRestApiUrl(site)),
24+
authProvider = createRestAuthProvider(site)
25+
)
26+
}
27+
28+
fun initJetpackConnectionClient(site: SiteModel): JetpackConnectionClient {
29+
requireRestCredentials(site)
30+
31+
val delegate = WpApiClientDelegate(
32+
authProvider = createRestAuthProvider(site),
33+
requestExecutor = WpRequestExecutor(),
34+
middlewarePipeline = WpApiMiddlewarePipeline(emptyList()),
35+
appNotifier = InvalidAuthNotifier()
36+
)
37+
38+
return JetpackConnectionClient(
39+
apiRootUrl = ParsedUrl.parse(resolveRestApiUrl(site)),
40+
delegate = delegate
41+
)
42+
}
43+
44+
private fun createRestAuthProvider(site: SiteModel) =
45+
WpAuthenticationProvider.staticWithUsernameAndPassword(
46+
site.apiRestUsernamePlain!!,
47+
site.apiRestPasswordPlain!!
48+
)
49+
50+
private fun requireRestCredentials(site: SiteModel) {
51+
require(!site.apiRestUsernamePlain.isNullOrBlank()) {
52+
"API username is required"
53+
}
54+
require(!site.apiRestPasswordPlain.isNullOrBlank()) {
55+
"API password is required"
56+
}
57+
}
58+
59+
private fun resolveRestApiUrl(site: SiteModel) =
60+
site.wpApiRestUrl ?: "${site.url}/wp-json"
61+
62+
private inner class InvalidAuthNotifier : WpAppNotifier {
63+
override suspend fun requestedWithInvalidAuthentication() {
64+
appLogWrapper.d(AppLog.T.API, "$TAG: requestedWithInvalidAuthentication")
65+
throw IllegalArgumentException("Invalid credentials")
66+
}
67+
}
68+
69+
companion object {
70+
private const val TAG = "JetpackConnectionHelper"
71+
}
72+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.wordpress.android.ui.jetpackrestconnection
2+
3+
import org.wordpress.android.fluxc.model.SiteModel
4+
import uniffi.wp_api.WpAuthentication
5+
import uniffi.wp_api.WpComSiteId
6+
import javax.inject.Inject
7+
8+
class JetpackConnector @Inject constructor(
9+
private val jetpackConnectionHelper: JetpackConnectionHelper
10+
) {
11+
/**
12+
* Connects the Jetpack site to WordPress.com and returns the site ID
13+
*/
14+
suspend fun connectSite(site: SiteModel): Result<WpComSiteId> = runCatching {
15+
val client = jetpackConnectionHelper.initJetpackConnectionClient(site)
16+
val wpComSiteId = client.connectSite(CONNECT_FROM)
17+
requireValidSiteId(wpComSiteId)
18+
}
19+
20+
/**
21+
* Connects the Jetpack user to WordPress.com and returns the site ID
22+
*/
23+
suspend fun connectUser(site: SiteModel, accessToken: String): Result<WpComSiteId> = runCatching {
24+
val client = jetpackConnectionHelper.initJetpackConnectionClient(site)
25+
val wpComAuthentication = WpAuthentication.Bearer(token = accessToken)
26+
val wpComSiteId = client.connectUser(
27+
wpComAuthentication = wpComAuthentication,
28+
from = CONNECT_FROM
29+
)
30+
requireValidSiteId(wpComSiteId)
31+
}
32+
33+
private fun requireValidSiteId(wpComSiteId: WpComSiteId): WpComSiteId {
34+
require(wpComSiteId > 0UL) { "Jetpack connection failed, no site ID returned" }
35+
return wpComSiteId
36+
}
37+
38+
companion object {
39+
const val CONNECT_FROM = "jetpack-android-app"
40+
}
41+
}

WordPress/src/main/java/org/wordpress/android/ui/jetpackrestconnection/JetpackInstaller.kt

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,19 @@ import uniffi.wp_api.PluginSlug
1111
import uniffi.wp_api.PluginStatus
1212
import uniffi.wp_api.PluginUpdateParams
1313
import uniffi.wp_api.PluginWpOrgDirectorySlug
14-
import uniffi.wp_api.WpAuthenticationProvider
15-
import java.net.URL
1614
import javax.inject.Inject
1715

1816
/**
1917
* Installs the Jetpack plugin on the given site using wordpress-rs
2018
*/
2119
class JetpackInstaller @Inject constructor(
20+
private val jetpackConnectionHelper: JetpackConnectionHelper,
2221
private val appLogWrapper: AppLogWrapper,
2322
) {
2423
@Suppress("TooGenericExceptionCaught")
2524
suspend fun installJetpack(site: SiteModel): Result<PluginStatus> {
26-
if (!validateCredentials(site)) {
27-
return Result.failure(IllegalArgumentException("Missing credentials for Jetpack installation"))
28-
}
29-
30-
val apiClient = initApiClient(site)
31-
3225
return try {
26+
val apiClient = jetpackConnectionHelper.initWpApiClient(site)
3327
val info = getPluginInfo(apiClient)
3428
when (info?.status) {
3529
PluginStatus.ACTIVE, PluginStatus.NETWORK_ACTIVE -> {
@@ -56,20 +50,6 @@ class JetpackInstaller @Inject constructor(
5650
}
5751
}
5852

59-
private fun validateCredentials(site: SiteModel): Boolean {
60-
return !site.apiRestUsernamePlain.isNullOrBlank() && !site.apiRestPasswordPlain.isNullOrBlank()
61-
}
62-
63-
private fun initApiClient(site: SiteModel): WpApiClient {
64-
return WpApiClient(
65-
wpOrgSiteApiRootUrl = URL(site.wpApiRestUrl),
66-
authProvider = WpAuthenticationProvider.staticWithUsernameAndPassword(
67-
site.apiRestUsernamePlain!!,
68-
site.apiRestPasswordPlain!!
69-
)
70-
)
71-
}
72-
7353
private suspend fun getPluginInfo(apiClient: WpApiClient): PluginInfo? {
7454
val response = apiClient.request { requestBuilder ->
7555
requestBuilder.plugins().listWithEditContext(

WordPress/src/main/java/org/wordpress/android/ui/jetpackrestconnection/JetpackRestConnectionScreen.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import androidx.compose.material.icons.filled.Build
2626
import androidx.compose.material.icons.filled.Check
2727
import androidx.compose.material.icons.filled.CheckCircle
2828
import androidx.compose.material.icons.filled.Home
29-
import androidx.compose.material.icons.filled.Settings
29+
import androidx.compose.material.icons.filled.Person
3030
import androidx.compose.material.icons.filled.Warning
3131
import androidx.compose.material3.CircularProgressIndicator
3232
import androidx.compose.material3.Icon
@@ -148,9 +148,9 @@ private val stepConfigs = listOf(
148148
icon = Icons.Default.Home
149149
),
150150
StepConfig(
151-
step = ConnectionStep.ConnectWpCom,
152-
titleRes = R.string.jetpack_rest_connection_step_connect_wpcom,
153-
icon = Icons.Default.Settings
151+
step = ConnectionStep.ConnectUser,
152+
titleRes = R.string.jetpack_rest_connection_step_connect_user,
153+
icon = Icons.Default.Person
154154
),
155155
StepConfig(
156156
step = ConnectionStep.Finalize,
@@ -270,9 +270,12 @@ private fun getErrorText(context: Context, errorType: ErrorType): String {
270270
ErrorType.LoginWpComFailed -> R.string.jetpack_rest_connection_error_login_wpcom
271271
ErrorType.ConnectWpComFailed -> R.string.jetpack_rest_connection_error_connect_wpcom
272272
ErrorType.InstallJetpackInactive -> R.string.jetpack_rest_connection_error_install_jetpack_inactive
273-
is ErrorType.InstallJetpackFailed -> R.string.jetpack_rest_connection_error_install_jetpack
274-
is ErrorType.Timeout -> R.string.jetpack_rest_connection_error_timeout
275-
is ErrorType.Offline -> R.string.jetpack_rest_connection_error_offline
273+
ErrorType.ConnectUserFailed -> R.string.jetpack_rest_connection_error_connect_user
274+
ErrorType.MissingAccessToken -> R.string.jetpack_rest_connection_error_access_token
275+
ErrorType.ConnectSiteFailed -> R.string.jetpack_rest_connection_error_connect_site
276+
ErrorType.InstallJetpackFailed -> R.string.jetpack_rest_connection_error_install_jetpack
277+
ErrorType.Timeout -> R.string.jetpack_rest_connection_error_timeout
278+
ErrorType.Offline -> R.string.jetpack_rest_connection_error_offline
276279
is ErrorType.Unknown -> R.string.jetpack_rest_connection_error_unknown
277280
}
278281
val baseMessage = context.getString(messageRes)
@@ -421,7 +424,7 @@ private fun JetpackRestConnectionScreenPreview() {
421424
ConnectionStep.LoginWpCom to StepState(ConnectionStatus.Completed),
422425
ConnectionStep.InstallJetpack to StepState(ConnectionStatus.Completed),
423426
ConnectionStep.ConnectSite to StepState(ConnectionStatus.InProgress),
424-
ConnectionStep.ConnectWpCom to StepState(
427+
ConnectionStep.ConnectUser to StepState(
425428
ConnectionStatus.Failed,
426429
ErrorType.ConnectWpComFailed
427430
),

WordPress/src/main/java/org/wordpress/android/ui/jetpackrestconnection/JetpackRestConnectionViewModel.kt

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class JetpackRestConnectionViewModel @Inject constructor(
2828
private val selectedSiteRepository: SelectedSiteRepository,
2929
private val accountStore: AccountStore,
3030
private val jetpackInstaller: JetpackInstaller,
31+
private val jetpackConnector: JetpackConnector,
3132
private val appLogWrapper: AppLogWrapper,
3233
) : ScopedViewModel(mainDispatcher) {
3334
private val _currentStep = MutableStateFlow<ConnectionStep?>(null)
@@ -39,11 +40,6 @@ class JetpackRestConnectionViewModel @Inject constructor(
3940
private val _buttonType = MutableStateFlow<ButtonType?>(ButtonType.Start)
4041
val buttonType = _buttonType
4142

42-
data class StepState(
43-
val status: ConnectionStatus = ConnectionStatus.NotStarted,
44-
val errorType: ErrorType? = null,
45-
)
46-
4743
private val _stepStates = MutableStateFlow(initialStepStates)
4844
val stepStates = _stepStates
4945

@@ -68,7 +64,6 @@ class JetpackRestConnectionViewModel @Inject constructor(
6864
*/
6965
private fun onJobCompleted() {
7066
appLogWrapper.d(AppLog.T.API, "$TAG: Jetpack connection job completed")
71-
job?.cancel()
7267
_buttonType.value = ButtonType.Done
7368
_currentStep.value = null
7469
}
@@ -77,8 +72,8 @@ class JetpackRestConnectionViewModel @Inject constructor(
7772
null -> ConnectionStep.LoginWpCom
7873
ConnectionStep.LoginWpCom -> ConnectionStep.InstallJetpack
7974
ConnectionStep.InstallJetpack -> ConnectionStep.ConnectSite
80-
ConnectionStep.ConnectSite -> ConnectionStep.ConnectWpCom
81-
ConnectionStep.ConnectWpCom -> ConnectionStep.Finalize
75+
ConnectionStep.ConnectSite -> ConnectionStep.ConnectUser
76+
ConnectionStep.ConnectUser -> ConnectionStep.Finalize
8277
ConnectionStep.Finalize -> null
8378
}
8479

@@ -238,7 +233,7 @@ class JetpackRestConnectionViewModel @Inject constructor(
238233
} catch (e: Exception) {
239234
appLogWrapper.e(AppLog.T.API, "$TAG: Error in step $step: ${e.message}")
240235
val errorType = when (e) {
241-
is TimeoutCancellationException -> ErrorType.Timeout(e.message)
236+
is TimeoutCancellationException -> ErrorType.Timeout
242237
else -> ErrorType.Unknown(e.message)
243238
}
244239
updateStepStatus(
@@ -262,12 +257,12 @@ class JetpackRestConnectionViewModel @Inject constructor(
262257

263258
ConnectionStep.ConnectSite -> {
264259
appLogWrapper.d(AppLog.T.API, "$TAG: Connecting site")
265-
// TODO
260+
connectSite()
266261
}
267262

268-
ConnectionStep.ConnectWpCom -> {
263+
ConnectionStep.ConnectUser -> {
269264
appLogWrapper.d(AppLog.T.API, "$TAG: Connecting WordPress.com user")
270-
// TODO
265+
connectUser()
271266
}
272267

273268
ConnectionStep.Finalize -> {
@@ -334,6 +329,7 @@ class JetpackRestConnectionViewModel @Inject constructor(
334329
status = ConnectionStatus.Completed
335330
)
336331
}
332+
337333
PluginStatus.INACTIVE -> {
338334
updateStepStatus(
339335
step = ConnectionStep.InstallJetpack,
@@ -353,6 +349,61 @@ class JetpackRestConnectionViewModel @Inject constructor(
353349
)
354350
}
355351

352+
/**
353+
* Connects the current site to Jetpack
354+
*/
355+
private suspend fun connectSite() {
356+
val result = jetpackConnector.connectSite(getSite())
357+
result.fold(
358+
onSuccess = {
359+
updateStepStatus(
360+
step = ConnectionStep.ConnectSite,
361+
status = ConnectionStatus.Completed
362+
)
363+
},
364+
onFailure = {
365+
updateStepStatus(
366+
step = ConnectionStep.ConnectSite,
367+
status = ConnectionStatus.Failed,
368+
error = ErrorType.ConnectSiteFailed
369+
)
370+
}
371+
)
372+
}
373+
374+
/**
375+
* Connects the user to the current site to Jetpack
376+
*/
377+
private suspend fun connectUser() {
378+
if (!accountStore.hasAccessToken()) {
379+
updateStepStatus(
380+
step = ConnectionStep.ConnectUser,
381+
status = ConnectionStatus.Failed,
382+
error = ErrorType.MissingAccessToken
383+
)
384+
return
385+
}
386+
val result = jetpackConnector.connectUser(
387+
site = getSite(),
388+
accessToken = accountStore.accessToken!!
389+
)
390+
result.fold(
391+
onSuccess = {
392+
updateStepStatus(
393+
step = ConnectionStep.ConnectUser,
394+
status = ConnectionStatus.Completed
395+
)
396+
},
397+
onFailure = {
398+
updateStepStatus(
399+
step = ConnectionStep.ConnectUser,
400+
status = ConnectionStatus.Failed,
401+
error = ErrorType.ConnectUserFailed
402+
)
403+
}
404+
)
405+
}
406+
356407
/**
357408
* Gets the current site from the store
358409
*/
@@ -363,7 +414,7 @@ class JetpackRestConnectionViewModel @Inject constructor(
363414
data object LoginWpCom : ConnectionStep()
364415
data object InstallJetpack : ConnectionStep()
365416
data object ConnectSite : ConnectionStep()
366-
data object ConnectWpCom : ConnectionStep()
417+
data object ConnectUser : ConnectionStep()
367418
data object Finalize : ConnectionStep()
368419
}
369420

@@ -385,8 +436,11 @@ class JetpackRestConnectionViewModel @Inject constructor(
385436
data object InstallJetpackFailed : ErrorType()
386437
data object InstallJetpackInactive : ErrorType()
387438
data object ConnectWpComFailed : ErrorType()
388-
data class Timeout(override val message: String? = null) : ErrorType(message)
389-
data class Offline(override val message: String? = null) : ErrorType(message)
439+
data object ConnectSiteFailed : ErrorType()
440+
data object ConnectUserFailed : ErrorType()
441+
data object MissingAccessToken : ErrorType()
442+
data object Timeout : ErrorType()
443+
data object Offline : ErrorType()
390444
data class Unknown(override val message: String? = null) : ErrorType(message)
391445
}
392446

@@ -396,6 +450,11 @@ class JetpackRestConnectionViewModel @Inject constructor(
396450
data object Retry : ButtonType()
397451
}
398452

453+
data class StepState(
454+
val status: ConnectionStatus = ConnectionStatus.NotStarted,
455+
val errorType: ErrorType? = null,
456+
)
457+
399458
companion object {
400459
private const val TAG = "JetpackRestConnectionViewModel"
401460
private const val LIMIT_VERSION = "14.2"
@@ -419,7 +478,7 @@ class JetpackRestConnectionViewModel @Inject constructor(
419478
ConnectionStep.LoginWpCom to StepState(),
420479
ConnectionStep.InstallJetpack to StepState(),
421480
ConnectionStep.ConnectSite to StepState(),
422-
ConnectionStep.ConnectWpCom to StepState(),
481+
ConnectionStep.ConnectUser to StepState(),
423482
ConnectionStep.Finalize to StepState()
424483
)
425484
}

0 commit comments

Comments
 (0)