Skip to content
This repository was archived by the owner on Feb 4, 2025. It is now read-only.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.wordpress.android.fluxc.generated.EditorThemeActionBuilder
import org.wordpress.android.fluxc.model.EditorTheme
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.module.ResponseMockingInterceptor
import org.wordpress.android.fluxc.module.ResponseMockingInterceptor.InterceptorMode.STICKY
import org.wordpress.android.fluxc.store.EditorThemeStore
import org.wordpress.android.fluxc.store.EditorThemeStore.FetchEditorThemePayload
import org.wordpress.android.fluxc.store.EditorThemeStore.OnEditorThemeChanged
Expand Down Expand Up @@ -183,6 +184,112 @@ class MockedStack_EditorThemeStoreTest : MockedStack_Base() {
Assert.assertNotNull(features)
}

@Test
fun testEditorSettingsUrl() {
val wordPressPayload = payloadWithGSS.apply {
site.softwareVersion = "5.8"
site.setIsWPCom(true)
}
interceptor.respondWith("global-styles-full-success.json")
dispatcher.dispatch(EditorThemeActionBuilder.newFetchEditorThemeAction(wordPressPayload))

// See onEditorThemeChanged for the latch's countdown to fire.
Assert.assertTrue(countDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS))

val id = payloadWithGSS.site.siteId
val expectedUrl = "https://public-api.wordpress.com/wp-block-editor/v1/sites/$id/settings"
interceptor.assertExpectedUrl(expectedUrl)
}

@Test
fun testEditorSettingsOldUrl() {
val wordPressPayload = payloadWithGSS.apply {
site.softwareVersion = "5.7"
site.setIsWPCom(true)
}
interceptor.respondWith("editor-theme-custom-elements-success-response.json")
dispatcher.dispatch(EditorThemeActionBuilder.newFetchEditorThemeAction(wordPressPayload))

// See onEditorThemeChanged for the latch's countdown to fire.
Assert.assertTrue(countDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS))

val id = payloadWithGSS.site.siteId
val expectedUrl = "https://public-api.wordpress.com/wp/v2/sites/$id/themes"
interceptor.assertExpectedUrl(expectedUrl)
}

@Test
fun testEditorSettingsRetryUrl() {
val wordPressPayload = payloadWithGSS.apply {
site.softwareVersion = "5.8"
site.setIsWPCom(true)
}
interceptor.respondWithError(JsonObject(), 404)
dispatcher.dispatch(EditorThemeActionBuilder.newFetchEditorThemeAction(wordPressPayload))

// See onEditorThemeChanged for the latch's countdown to fire.
Assert.assertTrue(countDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS))

// In case of failure we call the theme endpoint
val id = payloadWithGSS.site.siteId
val expectedUrl = "https://public-api.wordpress.com/wp/v2/sites/$id/themes"
interceptor.assertExpectedUrl(expectedUrl)
}

@Test
fun testEditorSettingsOrgUrl() {
val wordPressPayload = payloadWithGSS.apply {
site.softwareVersion = "5.8"
site.url = "https://test.com"
site.setIsWPCom(false)
}
interceptor.respondWith("global-styles-full-success.json")
dispatcher.dispatch(EditorThemeActionBuilder.newFetchEditorThemeAction(wordPressPayload))

// See onEditorThemeChanged for the latch's countdown to fire.
Assert.assertTrue(countDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS))
val expectedUrl = "https://test.com/wp-json/wp-block-editor/v1/settings"
interceptor.assertExpectedUrl(expectedUrl)
}

@Test
fun testEditorSettingsOldOrgUrl() {
val wordPressPayload = payloadWithGSS.apply {
site.softwareVersion = "5.7"
site.url = "https://test.com"
site.setIsWPCom(false)
}
interceptor.respondWith("editor-theme-custom-elements-success-response.json")
dispatcher.dispatch(EditorThemeActionBuilder.newFetchEditorThemeAction(wordPressPayload))

// See onEditorThemeChanged for the latch's countdown to fire.
Assert.assertTrue(countDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS))

val expectedUrl = "https://test.com/wp-json/wp/v2/themes"
interceptor.assertExpectedUrl(expectedUrl)
}

@Test
fun testEditorSettingsRetryOrgUrl() {
val wordPressPayload = payloadWithGSS.apply {
site.softwareVersion = "5.8"
site.url = "https://test.com"
site.setIsWPCom(false)
}
interceptor.respondWithError(JsonObject(), 404, STICKY)
dispatcher.dispatch(EditorThemeActionBuilder.newFetchEditorThemeAction(wordPressPayload))

// See onEditorThemeChanged for the latch's countdown to fire.
Assert.assertTrue(countDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS))

// In case of failure we call the theme endpoint
val expectedUrl = "https://test.com/wp-json/wp/v2/themes"
interceptor.assertExpectedUrl(expectedUrl)
}

private fun ResponseMockingInterceptor.assertExpectedUrl(expectedUrl: String) =
Assert.assertTrue(lastRequestUrl.startsWith(expectedUrl))

private fun assertNotEmpty(theme: EditorTheme?) {
Assert.assertFalse(theme?.themeSupport?.colors.isNullOrEmpty())
Assert.assertFalse(theme?.themeSupport?.gradients.isNullOrEmpty())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,10 @@ class ResponseMockingInterceptor @Inject constructor() : Interceptor {
}

@JvmOverloads
fun respondWithError(jsonResponse: JsonElement, errorCode: Int = 404) {
fun respondWithError(jsonResponse: JsonElement, errorCode: Int = 404, interceptorMode: InterceptorMode = ONE_TIME) {
nextResponseJson = jsonResponse.toString()
nextResponseCode = errorCode
mode = interceptorMode
}

@Throws(IOException::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.wordpress.android.fluxc.model.BlockEditorSettings
import org.wordpress.android.fluxc.model.EditorTheme
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError
import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType.NOT_FOUND
import org.wordpress.android.fluxc.persistence.EditorThemeSqlUtils
import org.wordpress.android.fluxc.store.ReactNativeFetchResponse.Error
import org.wordpress.android.fluxc.store.ReactNativeFetchResponse.Success
Expand All @@ -22,8 +23,8 @@ import javax.inject.Inject
import javax.inject.Singleton

private const val THEME_REQUEST_PATH = "/wp/v2/themes?status=active"
private const val GSS_REQUEST_PATH = "__experimental/wp-block-editor/v1/settings?context=mobile"
private const val GSS_LIMIT_VERSION = "5.8"
private const val EDITOR_SETTINGS_REQUEST_PATH = "wp-block-editor/v1/settings?context=mobile"
private const val EDITOR_SETTINGS_WP_VERSION = "5.8"

@Singleton
class EditorThemeStore
Expand All @@ -47,6 +48,7 @@ class EditorThemeStore
this.error = error
}
}

class EditorThemeError(var message: String? = null) : OnChangedError

fun getEditorThemeForSite(site: SiteModel): EditorTheme? {
Expand All @@ -64,10 +66,10 @@ class EditorThemeStore
EditorThemeStore::class.java.simpleName + ": On FETCH_EDITOR_THEME"
) {
val payload = action.payload as FetchEditorThemePayload
if (globalStyleSettingsAvailable(payload.site, payload.gssEnabled)) {
handleFetchGlobalStylesSettings(payload.site, actionType)
if (editorSettingsAvailable(payload.site, payload.gssEnabled)) {
fetchEditorSettings(payload.site, actionType)
} else {
handleFetchEditorTheme(payload.site, actionType)
fetchEditorTheme(payload.site, actionType)
}
}
}
Expand All @@ -78,7 +80,7 @@ class EditorThemeStore
AppLog.d(AppLog.T.API, EditorThemeStore::class.java.simpleName + " onRegister")
}

private suspend fun handleFetchEditorTheme(site: SiteModel, action: EditorThemeAction) {
private suspend fun fetchEditorTheme(site: SiteModel, action: EditorThemeAction) {
val response = reactNativeStore.executeRequest(site, THEME_REQUEST_PATH, false)

when (response) {
Expand Down Expand Up @@ -110,57 +112,76 @@ class EditorThemeStore
}
}

private suspend fun handleFetchGlobalStylesSettings(site: SiteModel, action: EditorThemeAction) {
val response = reactNativeStore.executeRequest(site, GSS_REQUEST_PATH, false)
private suspend fun fetchEditorSettings(site: SiteModel, action: EditorThemeAction) {
val response = reactNativeStore.executeRequest(site, EDITOR_SETTINGS_REQUEST_PATH, false)

when (response) {
is Success -> {
val noGssError = OnEditorThemeChanged(EditorThemeError("Response does not contain GSS"), action)
if (response.result == null || !response.result.isJsonObject) {
emitChange(noGssError)
return
}

val responseTheme = response.result.asJsonObject
if (responseTheme == null) {
emitChange(noGssError)
return
}

val blockEditorSettings = Gson().fromJson(responseTheme, BlockEditorSettings::class.java)
val newTheme = EditorTheme(blockEditorSettings)
val existingTheme = editorThemeSqlUtils.getEditorThemeForSite(site)
if (newTheme != existingTheme) {
editorThemeSqlUtils.replaceEditorThemeForSite(site, newTheme)
val onChanged = OnEditorThemeChanged(newTheme, site.id, action)
emitChange(onChanged)
}
response.handleFetchEditorSettingsResponse(site, action)
}
is Error -> {
val onChanged = OnEditorThemeChanged(EditorThemeError(response.error.message), action)
emitChange(onChanged)
if (response.error.type == NOT_FOUND) {
/**
* We tried the editor settings call first but since that failed we fall back to the themes endpoint
* since the user may not have the gutenberg plugin installed.
*/
fetchEditorTheme(site, action)
} else {
response.handleFetchEditorSettingsResponse(action)
}
}
}
}

private fun globalStyleSettingsAvailable(site: SiteModel, gssEnabled: Boolean) =
gssEnabled && hasRequiredWordPressVersion(site.softwareVersion)
private fun ReactNativeFetchResponse.Success.handleFetchEditorSettingsResponse(
site: SiteModel,
action: EditorThemeAction
) {
val noGssError = OnEditorThemeChanged(EditorThemeError("Response does not contain GSS"), action)
if (result == null || !result.isJsonObject) {
emitChange(noGssError)
return
}

val responseTheme = result.asJsonObject
if (responseTheme == null) {
emitChange(noGssError)
return
}

val blockEditorSettings = Gson().fromJson(responseTheme, BlockEditorSettings::class.java)
val newTheme = EditorTheme(blockEditorSettings)
val existingTheme = editorThemeSqlUtils.getEditorThemeForSite(site)
if (newTheme != existingTheme) {
editorThemeSqlUtils.replaceEditorThemeForSite(site, newTheme)
val onChanged = OnEditorThemeChanged(newTheme, site.id, action)
emitChange(onChanged)
}
}

private fun ReactNativeFetchResponse.Error.handleFetchEditorSettingsResponse(action: EditorThemeAction) {
val onChanged = OnEditorThemeChanged(EditorThemeError(error.message), action)
emitChange(onChanged)
}

private fun editorSettingsAvailable(site: SiteModel, gssEnabled: Boolean) =
gssEnabled && site.hasRequiredWordPressVersion(EDITOR_SETTINGS_WP_VERSION)

/**
* Checks if the [wordPressSoftwareVersion] is higher or equal to [GSS_LIMIT_VERSION]
* Checks if the [SiteModel.getSoftwareVersion] is higher or equal to the [requiredVersion]
*
* Note: At this point semantic version information (alpha, beta etc) is stripped since it
* is not supported by our [Version] utility
*
* @param wordPressSoftwareVersion the WordPress version
* @param requiredVersion the required WordPress version
* @return true if the check is met
*/
private fun hasRequiredWordPressVersion(wordPressSoftwareVersion: String) = try {
val version = if (wordPressSoftwareVersion.contains("-")) {
private fun SiteModel.hasRequiredWordPressVersion(requiredVersion: String) = try {
val version = if (softwareVersion.contains("-")) {
// strip semantic versioning information (alpha, beta etc)
wordPressSoftwareVersion.substringBefore("-")
} else wordPressSoftwareVersion
Version(version) >= Version(GSS_LIMIT_VERSION)
softwareVersion.substringBefore("-")
} else softwareVersion
Version(version) >= Version(requiredVersion)
} catch (e: IllegalArgumentException) {
false // if version parsing fails return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class ReactNativeStore
val url = path?.let {
val newPath = it
.replace("wp/v2".toRegex(), "wp/v2/sites/$wpComSiteId")
.replace("wp-block-editor/v1".toRegex(), "wp-block-editor/v1/sites/$wpComSiteId")
.replace("oembed/1.0".toRegex(), "oembed/1.0/sites/$wpComSiteId")
slashJoin(WPCOM_ENDPOINT, newPath)
}
Expand Down