From b91597de40ab64aa4bf09fecdbfacb74f07c1b8e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Oct 2023 14:43:26 +0300 Subject: [PATCH 1/7] Adds all-domains endpoint --- fluxc-processor/src/main/resources/wp-com-endpoints.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fluxc-processor/src/main/resources/wp-com-endpoints.txt b/fluxc-processor/src/main/resources/wp-com-endpoints.txt index 408903117f..2c64e44a90 100644 --- a/fluxc-processor/src/main/resources/wp-com-endpoints.txt +++ b/fluxc-processor/src/main/resources/wp-com-endpoints.txt @@ -43,6 +43,8 @@ /read/site/$site#String/post_email_subscriptions/$action#String /read/site/$site#String/post_email_subscriptions/update +/all-domains + /domains/suggestions /domains/supported-countries/ /domains/supported-states/$countryCode#String From 3f5b31849f9b57e5b33bad31e895560a64e942a2 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Oct 2023 15:15:39 +0300 Subject: [PATCH 2/7] Extracts the boolean type adapter so that it can be reused with the all-domains response --- .../rest/wpcom/site/BooleanTypeAdapter.kt | 25 +++++++++++++++++++ .../rest/wpcom/site/DomainsResponse.kt | 19 -------------- 2 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/BooleanTypeAdapter.kt diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/BooleanTypeAdapter.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/BooleanTypeAdapter.kt new file mode 100644 index 0000000000..4a71d07772 --- /dev/null +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/BooleanTypeAdapter.kt @@ -0,0 +1,25 @@ +package org.wordpress.android.fluxc.network.rest.wpcom.site + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import java.lang.reflect.Type +import java.util.Locale + +internal class BooleanTypeAdapter : JsonDeserializer { + @Suppress("VariableNaming") private val TRUE_STRINGS: Set = HashSet(listOf("true", "1", "yes")) + + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean { + val jsonPrimitive = json.asJsonPrimitive + return when { + jsonPrimitive.isBoolean -> jsonPrimitive.asBoolean + jsonPrimitive.isNumber -> jsonPrimitive.asNumber.toInt() == 1 + jsonPrimitive.isString -> TRUE_STRINGS.contains(jsonPrimitive.asString.toLowerCase( + Locale.getDefault() + )) + else -> false + } + } +} diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/DomainsResponse.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/DomainsResponse.kt index 1c2df73d01..88bb727a5b 100644 --- a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/DomainsResponse.kt +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/DomainsResponse.kt @@ -1,13 +1,8 @@ package org.wordpress.android.fluxc.network.rest.wpcom.site -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer import com.google.gson.JsonElement -import com.google.gson.JsonParseException import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName -import java.lang.reflect.Type -import java.util.Locale data class DomainsResponse(val domains: List) @@ -155,17 +150,3 @@ data class TitanMailSubscription( val status: String? = null ) -internal class BooleanTypeAdapter : JsonDeserializer { - @Suppress("VariableNaming") private val TRUE_STRINGS: Set = HashSet(listOf("true", "1", "yes")) - - @Throws(JsonParseException::class) - override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean { - val jsonPrimitive = json.asJsonPrimitive - return when { - jsonPrimitive.isBoolean -> jsonPrimitive.asBoolean - jsonPrimitive.isNumber -> jsonPrimitive.asNumber.toInt() == 1 - jsonPrimitive.isString -> TRUE_STRINGS.contains(jsonPrimitive.asString.toLowerCase(Locale.getDefault())) - else -> false - } - } -} From 74e83c72fb98822f09ce77b89b55dc29d933f194 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Oct 2023 15:17:48 +0300 Subject: [PATCH 3/7] Adds the rest client implementation for the all-domains api call --- .../rest/wpcom/site/SiteRestClientTest.kt | 30 +++++++++++++++ .../resources/wp/all-domains/all-domains.json | 32 ++++++++++++++++ .../rest/wpcom/site/AllDomainsResponse.kt | 38 +++++++++++++++++++ .../network/rest/wpcom/site/SiteRestClient.kt | 5 +++ 4 files changed, 105 insertions(+) create mode 100644 example/src/test/resources/wp/all-domains/all-domains.json create mode 100644 fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/AllDomainsResponse.kt diff --git a/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClientTest.kt b/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClientTest.kt index 80822f5eb0..9d503e4492 100644 --- a/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClientTest.kt +++ b/example/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClientTest.kt @@ -2,6 +2,8 @@ package org.wordpress.android.fluxc.network.rest.wpcom.site import com.android.volley.RequestQueue import com.android.volley.VolleyError +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test @@ -16,6 +18,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.UnitTestUtils import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType @@ -526,6 +529,26 @@ class SiteRestClientTest { assertThat(options).containsEntry("site_creation_flow", siteCreationFlow) } + @Test + fun `when all domains are requested, then the correct response is built`() = test { + val allDomainsJson = "wp/all-domains/all-domains.json" + val json = UnitTestUtils.getStringFromResourceFile(javaClass, allDomainsJson) + val responseType = object : TypeToken() {}.type + val response = GsonBuilder().create().fromJson(json, responseType) as AllDomainsResponse + + initAllDomainsResponse(data = response) + + val responseModel = restClient.fetchAllDomains(noWpCom = true) + assert(responseModel is Success) + with((responseModel as Success).data) { + assertThat(domains).hasSize(2) + assertThat(domains[0].domain).isEqualTo("some.test.domain") + assertThat(domains[0].wpcomDomain).isFalse() + assertThat(domains[1].domain).isEqualTo("some.test.domain 2") + assertThat(domains[1].wpcomDomain).isTrue() + } + } + private suspend fun initSiteResponse( data: SiteWPComRestResponse? = null, error: WPComGsonNetworkError? = null @@ -561,6 +584,13 @@ class SiteRestClientTest { return initGetResponse(SitesFeaturesRestResponse::class.java, data ?: mock(), error) } + private suspend fun initAllDomainsResponse( + data: AllDomainsResponse? = null, + error: WPComGsonNetworkError? = null + ): Response { + return initGetResponse(AllDomainsResponse::class.java, data ?: mock(), error) + } + private suspend fun initGetResponse( clazz: Class, data: T, diff --git a/example/src/test/resources/wp/all-domains/all-domains.json b/example/src/test/resources/wp/all-domains/all-domains.json new file mode 100644 index 0000000000..4c92fcd2d9 --- /dev/null +++ b/example/src/test/resources/wp/all-domains/all-domains.json @@ -0,0 +1,32 @@ +{ + "domains": [ + { + "domain": "some.test.domain", + "blog_id": 11111, + "blog_name": "some test blog", + "type": "mapping", + "is_domain_only_site": false, + "is_wpcom_staging_domain": false, + "has_registration": false, + "registration_date": "2009-03-26T21:20:53+00:00", + "expiry": "2024-03-24T00:00:00+00:00", + "wpcom_domain": false, + "current_user_is_owner": true, + "site_slug": "test slug" + }, + { + "domain": "some.test.domain 2", + "blog_id": 22222, + "blog_name": "some test blog 2", + "type": "mapping", + "is_domain_only_site": false, + "is_wpcom_staging_domain": false, + "has_registration": false, + "registration_date": "2009-03-26T21:20:53+00:00", + "expiry": "2024-03-24T00:00:00+00:00", + "wpcom_domain": true, + "current_user_is_owner": false, + "site_slug": "test slug 2" + } + ] +} \ No newline at end of file diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/AllDomainsResponse.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/AllDomainsResponse.kt new file mode 100644 index 0000000000..745328abac --- /dev/null +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/AllDomainsResponse.kt @@ -0,0 +1,38 @@ +package org.wordpress.android.fluxc.network.rest.wpcom.site + +import com.google.gson.annotations.JsonAdapter +import com.google.gson.annotations.SerializedName + +data class AllDomainsResponse(val domains: List) + +data class AllDomainsDomain( + @SerializedName("domain") + val domain: String? = null, + @SerializedName("blog_id") + val blogId: Long = 0, + @SerializedName("blog_name") + val blogName: String? = null, + @SerializedName("type") + val type: String? = null, + @SerializedName("is_domain_only_site") + @JsonAdapter(BooleanTypeAdapter::class) + val isDomainOnlySite: Boolean = false, + @SerializedName("is_wpcom_staging_domain") + @JsonAdapter(BooleanTypeAdapter::class) + val isWpcomStagingDomain: Boolean = false, + @SerializedName("has_registration") + @JsonAdapter(BooleanTypeAdapter::class) + val hasRegistration: Boolean = false, + @SerializedName("registration_date") + val registrationDate: String? = null, + @SerializedName("expiry") + val expiry: String? = null, + @SerializedName("wpcom_domain") + @JsonAdapter(BooleanTypeAdapter::class) + val wpcomDomain: Boolean = false, + @SerializedName("current_user_is_owner") + @JsonAdapter(BooleanTypeAdapter::class) + val currentUserIsOwner: Boolean = false, + @SerializedName("site_slug") + val siteSlug: String? = null, +) diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClient.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClient.kt index 8d440f4208..06636eeed0 100644 --- a/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClient.kt +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/site/SiteRestClient.kt @@ -837,6 +837,11 @@ class SiteRestClient @Inject constructor( add(request) } + suspend fun fetchAllDomains(noWpCom: Boolean = true): Response { + val url = WPCOMREST.all_domains.urlV1_1 + val params = mapOf("no_wpcom" to noWpCom.toString()) + return wpComGsonRequestBuilder.syncGetRequest(this, url, params, AllDomainsResponse::class.java) + } suspend fun fetchSiteDomains(site: SiteModel): Response { val url = WPCOMREST.sites.site(site.siteId).domains.urlV1_1 return wpComGsonRequestBuilder.syncGetRequest(this, url, mapOf(), DomainsResponse::class.java) From 0d77b28b5ce23beb3508aa4889eee34e66f1a8ab Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Oct 2023 15:19:02 +0300 Subject: [PATCH 4/7] Adds the all-domains api call to the site store --- .../android/fluxc/store/SiteStoreTest.kt | 32 +++++++++++++++++ .../android/fluxc/store/SiteStore.kt | 36 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/example/src/test/java/org/wordpress/android/fluxc/store/SiteStoreTest.kt b/example/src/test/java/org/wordpress/android/fluxc/store/SiteStoreTest.kt index 057fc94ce5..7f071a22a2 100644 --- a/example/src/test/java/org/wordpress/android/fluxc/store/SiteStoreTest.kt +++ b/example/src/test/java/org/wordpress/android/fluxc/store/SiteStoreTest.kt @@ -26,6 +26,7 @@ import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType.PARSE_ER import org.wordpress.android.fluxc.network.rest.wpapi.site.SiteWPAPIRestClient import org.wordpress.android.fluxc.network.rest.wpcom.WPComGsonRequest.WPComGsonNetworkError import org.wordpress.android.fluxc.network.rest.wpcom.WPComGsonRequestBuilder.Response +import org.wordpress.android.fluxc.network.rest.wpcom.site.AllDomainsResponse import org.wordpress.android.fluxc.network.rest.wpcom.site.Domain import org.wordpress.android.fluxc.network.rest.wpcom.site.DomainsResponse import org.wordpress.android.fluxc.network.rest.wpcom.site.PlansResponse @@ -38,7 +39,10 @@ import org.wordpress.android.fluxc.persistence.PostSqlUtils import org.wordpress.android.fluxc.persistence.SiteSqlUtils import org.wordpress.android.fluxc.persistence.domains.DomainDao import org.wordpress.android.fluxc.persistence.jetpacksocial.JetpackSocialDao +import org.wordpress.android.fluxc.store.SiteStore.AllDomainsError +import org.wordpress.android.fluxc.store.SiteStore.AllDomainsErrorType import org.wordpress.android.fluxc.store.SiteStore.FetchSitesPayload +import org.wordpress.android.fluxc.store.SiteStore.FetchedAllDomainsPayload import org.wordpress.android.fluxc.store.SiteStore.FetchedDomainsPayload import org.wordpress.android.fluxc.store.SiteStore.FetchedPlansPayload import org.wordpress.android.fluxc.store.SiteStore.FetchedPostFormatsPayload @@ -72,8 +76,10 @@ class SiteStoreTest { @Mock lateinit var jetpackSocialDao: JetpackSocialDao @Mock lateinit var jetpackSocialMapper: JetpackSocialMapper @Mock lateinit var domainsSuccessResponse: Response.Success + @Mock lateinit var allDomainsSuccessResponse: Response.Success @Mock lateinit var plansSuccessResponse: Response.Success @Mock lateinit var domainsErrorResponse: Response.Error + @Mock lateinit var allDomainsErrorResponse: Response.Error @Mock lateinit var plansErrorResponse: Response.Error private lateinit var siteStore: SiteStore @@ -487,4 +493,30 @@ class SiteStoreTest { assertThat(onSitePlansFetched.error.type).isEqualTo(PlansError(PlansErrorType.GENERIC_ERROR, null).type) } + + @Test + fun `fetchAllDomains from WPCom endpoint`() = test { + whenever(siteRestClient.fetchAllDomains()).thenReturn(allDomainsSuccessResponse) + whenever(allDomainsSuccessResponse.data).thenReturn(AllDomainsResponse(listOf())) + + val onAllDomainsFetched = siteStore.fetchAllDomains() + + assertThat(onAllDomainsFetched.domains).isNotNull + assertThat(onAllDomainsFetched.error).isNull() + assertThat(onAllDomainsFetched).isEqualTo(FetchedAllDomainsPayload(onAllDomainsFetched.domains)) + } + + @Test + fun `fetchAllDomains error from WPCom endpoint returns error`() = test { + val site = SiteModel() + site.setIsWPCom(true) + + whenever(siteRestClient.fetchAllDomains()).thenReturn(allDomainsErrorResponse) + whenever(allDomainsErrorResponse.error).thenReturn(WPComGsonNetworkError(BaseNetworkError(NETWORK_ERROR))) + + val onAllDomainsFetched = siteStore.fetchAllDomains() + + val expectedErrorType = AllDomainsError(AllDomainsErrorType.GENERIC_ERROR, null).type + assertThat(onAllDomainsFetched.error.type).isEqualTo(expectedErrorType) + } } diff --git a/fluxc/src/main/java/org/wordpress/android/fluxc/store/SiteStore.kt b/fluxc/src/main/java/org/wordpress/android/fluxc/store/SiteStore.kt index d058b0043c..8e8c26764d 100644 --- a/fluxc/src/main/java/org/wordpress/android/fluxc/store/SiteStore.kt +++ b/fluxc/src/main/java/org/wordpress/android/fluxc/store/SiteStore.kt @@ -87,6 +87,7 @@ import org.wordpress.android.fluxc.network.rest.wpapi.site.SiteWPAPIRestClient import org.wordpress.android.fluxc.network.rest.wpcom.WPComGsonRequest.WPComGsonNetworkError import org.wordpress.android.fluxc.network.rest.wpcom.WPComGsonRequestBuilder.Response.Error import org.wordpress.android.fluxc.network.rest.wpcom.WPComGsonRequestBuilder.Response.Success +import org.wordpress.android.fluxc.network.rest.wpcom.site.AllDomainsDomain import org.wordpress.android.fluxc.network.rest.wpcom.site.Domain import org.wordpress.android.fluxc.network.rest.wpcom.site.DomainPriceResponse import org.wordpress.android.fluxc.network.rest.wpcom.site.DomainSuggestionResponse @@ -490,6 +491,11 @@ open class SiteStore @Inject constructor( } } + data class AllDomainsError @JvmOverloads constructor( + @JvmField val type: AllDomainsErrorType, + @JvmField val message: String? = null, + ) : OnChangedError + data class SiteError @JvmOverloads constructor( @JvmField val type: SiteErrorType, @JvmField val message: String? = null, @@ -757,6 +763,14 @@ open class SiteStore @Inject constructor( } } + data class FetchedAllDomainsPayload( + @JvmField val domains: List? = null + ) : Payload() { + constructor(error: AllDomainsError) : this() { + this.error = error + } + } + data class FetchedDomainsPayload( @JvmField val site: SiteModel, @JvmField val domains: List? = null @@ -886,6 +900,10 @@ open class SiteStore @Inject constructor( INVALID_SITE, UNKNOWN_SITE, DUPLICATE_SITE, INVALID_RESPONSE, UNAUTHORIZED, NOT_AUTHENTICATED, GENERIC_ERROR } + enum class AllDomainsErrorType { + UNAUTHORIZED, GENERIC_ERROR + } + enum class SuggestDomainErrorType { EMPTY_RESULTS, EMPTY_QUERY, INVALID_MINIMUM_QUANTITY, INVALID_MAXIMUM_QUANTITY, INVALID_QUERY, GENERIC_ERROR; @@ -2080,6 +2098,24 @@ open class SiteStore @Inject constructor( emitChange(event) } + suspend fun fetchAllDomains(noWpCom: Boolean = true): FetchedAllDomainsPayload = + coroutineEngine.withDefaultContext(T.API, this, "Fetch all domains") { + return@withDefaultContext when (val response = + siteRestClient.fetchAllDomains(noWpCom)) { + is Success -> { + val domains = response.data.domains + FetchedAllDomainsPayload(domains) + } + is Error -> { + val errorType = when (response.error.apiError) { + "authorization_required" -> AllDomainsErrorType.UNAUTHORIZED + else -> AllDomainsErrorType.GENERIC_ERROR + } + val domainsError = AllDomainsError(errorType, response.error.message) + FetchedAllDomainsPayload(domainsError) + } + } + } suspend fun fetchSiteDomains(siteModel: SiteModel): FetchedDomainsPayload = coroutineEngine.withDefaultContext(T.API, this, "Fetch site domains") { return@withDefaultContext when (val response = From b90b05ded3dc3ae8923616d1201ddcdfcef28ad8 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Oct 2023 15:19:58 +0300 Subject: [PATCH 5/7] Adds UI to the example client to test the new call --- .../android/fluxc/example/DomainsFragment.kt | 19 +++++++++++++++++++ .../src/main/res/layout/fragment_domains.xml | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/example/src/main/java/org/wordpress/android/fluxc/example/DomainsFragment.kt b/example/src/main/java/org/wordpress/android/fluxc/example/DomainsFragment.kt index 7ce73ae4fc..0459ca957a 100644 --- a/example/src/main/java/org/wordpress/android/fluxc/example/DomainsFragment.kt +++ b/example/src/main/java/org/wordpress/android/fluxc/example/DomainsFragment.kt @@ -67,5 +67,24 @@ class DomainsFragment : StoreSelectingFragment() { } } } + + fetch_all_domains.setOnClickListener { + lifecycleScope.launch { + val result = store.fetchAllDomains(noWpCom = false) // fetching wpcom too for debugging purposes + when { + result.isError -> { + prependToLog("Error fetching all domains: ${result.error.message}") + } + else -> { + prependToLog("All domains count: ${result.domains?.size}") + val domains = result.domains + ?.joinToString(separator = "\n") { + "${it.domain} (type: ${it.type}), expiry: ${it.expiry}" + } + prependToLog("Domains:\n$domains") + } + } + } + } } } diff --git a/example/src/main/res/layout/fragment_domains.xml b/example/src/main/res/layout/fragment_domains.xml index eb8cd4adb4..665af58238 100644 --- a/example/src/main/res/layout/fragment_domains.xml +++ b/example/src/main/res/layout/fragment_domains.xml @@ -24,5 +24,11 @@ android:layout_height="wrap_content" android:text="Fetch domain price" /> +