Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paywalls: PaywallData errors shouldn't make Offerings fail to decode #1402

Merged
merged 2 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,13 @@ internal abstract class OfferingParser {

val paywallDataJson = offeringJson.optJSONObject("paywall")

val paywallData = paywallDataJson?.let {
json.decodeFromString<PaywallData>(it.toString())
val paywallData: PaywallData? = paywallDataJson?.let {
try {
json.decodeFromString<PaywallData>(it.toString())
} catch (e: IllegalArgumentException) {
errorLog("Error deserializing paywall data", e)
null
}
}

return if (availablePackages.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,49 +234,62 @@ data class PaywallData(
/**
* The subtitle of the paywall screen.
*/
@Serializable(with = EmptyStringToNullSerializer::class)
val subtitle: String? = null,

/**
* The content of the main action button for purchasing a subscription.
*/
@SerialName("call_to_action") val callToAction: String,
@SerialName("call_to_action")
val callToAction: String,

/**
* The content of the main action button for purchasing a subscription when an intro offer is available.
* If `null`, no information regarding trial eligibility will be displayed.
*/
@SerialName("call_to_action_with_intro_offer") val callToActionWithIntroOffer: String? = null,
@SerialName("call_to_action_with_intro_offer")
@Serializable(with = EmptyStringToNullSerializer::class)
val callToActionWithIntroOffer: String? = null,

/**
* The content of the main action button for purchasing a subscription when multiple intro offer are available.
* This may happen in Google Play, if you have an offer with both a free trial and a discounted price.
* If `null`, no information regarding trial eligibility will be displayed.
*/
@SerialName("call_to_action_with_multiple_intro_offers")
@Serializable(with = EmptyStringToNullSerializer::class)
val callToActionWithMultipleIntroOffers: String? = null,

/**
* Description for the offer to be purchased.
*/
@SerialName("offer_details") val offerDetails: String? = null,
@SerialName("offer_details")
@Serializable(with = EmptyStringToNullSerializer::class)
val offerDetails: String? = null,

/**
* Description for the offer to be purchased when an intro offer is available.
* If `null`, no information regarding trial eligibility will be displayed.
*/
@SerialName("offer_details_with_intro_offer") val offerDetailsWithIntroOffer: String? = null,
@SerialName("offer_details_with_intro_offer")
@Serializable(with = EmptyStringToNullSerializer::class)
val offerDetailsWithIntroOffer: String? = null,

/**
* Description for the offer to be purchased when multiple intro offers are available.
* This may happen in Google Play, if you have an offer with both a free trial and a discounted price.
* If `null`, no information regarding trial eligibility will be displayed.
*/
@SerialName("offer_details_with_multiple_intro_offers") val offerDetailsWithMultipleIntroOffers: String? = null,
@SerialName("offer_details_with_multiple_intro_offers")
@Serializable(with = EmptyStringToNullSerializer::class)
val offerDetailsWithMultipleIntroOffers: String? = null,

/**
* The name representing each of the packages, most commonly a variable.
*/
@SerialName("offer_name") val offerName: String? = null,
@SerialName("offer_name")
@Serializable(with = EmptyStringToNullSerializer::class)
val offerName: String? = null,

/**
* An optional list of features that describe this paywall.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,63 @@ class OfferingsFactoryTest {
"'description': 'This is the base offering', " +
"'packages': []}]," +
"'current_offering_id': '$STUB_OFFERING_IDENTIFIER'}")
private val oneOfferingWithInvalidPaywallResponse = JSONObject(
"" +
"{" +
"'offerings': [" +
"{" +
"'identifier': '$STUB_OFFERING_IDENTIFIER', " +
"'description': 'This is the base offering', " +
"'packages': [" +
"{'identifier': '\$rc_monthly','platform_product_identifier': '$STUB_PRODUCT_IDENTIFIER'}" +
"]," +
"'paywall': 'not a paywall'" +
"}" +
"]," +
"'current_offering_id': '$STUB_OFFERING_IDENTIFIER'" +
"}"
)
private val oneOfferingWithPaywall = JSONObject(
"" +
"{" +
"'offerings': [" +
"{" +
"'identifier': '$STUB_OFFERING_IDENTIFIER', " +
"'description': 'This is the base offering', " +
"'packages': [" +
"{'identifier': '\$rc_monthly','platform_product_identifier': '$STUB_PRODUCT_IDENTIFIER'}" +
"]," +
"'paywall': {\n" +
" \"template_name\": \"1\",\n" +
" \"localized_strings\": {\n" +
" \"en_US\": {\n" +
" \"title\": \"Paywall\",\n" +
" \"call_to_action\": \"Purchase\",\n" +
" \"subtitle\": \"Description\"\n" +
" }\n" +
" },\n" +
" \"config\": {\n" +
" \"packages\": [\"\$rc_monthly\"],\n" +
" \"default_package\": \"\$rc_monthly\",\n" +
" \"images\": {},\n" +
" \"colors\": {\n" +
" \"light\": {\n" +
" \"background\": \"#FF00AA\",\n" +
" \"text_1\": \"#FF00AA22\",\n" +
" \"call_to_action_background\": \"#FF00AACC\",\n" +
" \"call_to_action_foreground\": \"#FF00AA\"\n" +
" }\n" +
" }\n" +
" },\n" +
" \"asset_base_url\": \"https://rc-paywalls.s3.amazonaws.com\",\n" +
" \"revision\": 7\n" +
"}" +
"}" +
"]," +
"'current_offering_id': '$STUB_OFFERING_IDENTIFIER'" +
"}"
)

private val oneOfferingResponse = JSONObject(ONE_OFFERINGS_RESPONSE)
private val oneOfferingInAppProductResponse = JSONObject(ONE_OFFERINGS_INAPP_PRODUCT_RESPONSE)

Expand Down Expand Up @@ -142,6 +199,42 @@ class OfferingsFactoryTest {
assertThat(offerings!![STUB_OFFERING_IDENTIFIER]!!.monthly!!.product).isNotNull
}

@Test
fun `createOfferings with paywall`() {
val productIds = listOf(productId)
mockStoreProduct(productIds, emptyList(), ProductType.SUBS)
mockStoreProduct(productIds, productIds, ProductType.INAPP)

var offerings: Offerings? = null
offeringsFactory.createOfferings(
oneOfferingWithPaywall,
{ fail("Error: $it") },
{ offerings = it }
)

assertThat(offerings).isNotNull
assertThat(offerings!!.current).isNotNull
assertThat(offerings!!.current?.paywall).isNotNull
}

@Test
fun `createOfferings does not fail if paywall is invalid`() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

val productIds = listOf(productId)
mockStoreProduct(productIds, emptyList(), ProductType.SUBS)
mockStoreProduct(productIds, productIds, ProductType.INAPP)

var offerings: Offerings? = null
offeringsFactory.createOfferings(
oneOfferingWithInvalidPaywallResponse,
{ fail("Error: $it") },
{ offerings = it }
)

assertThat(offerings).isNotNull
assertThat(offerings!!.current).isNotNull
assertThat(offerings!!.current?.paywall).isNull()
}

// region helpers

private fun mockStoreProduct(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,42 @@ class PaywallDataTest {
val unknownLocale = "gl_ES".toLocale()
assertThat(paywall.configForLocale(unknownLocale)).isNull()

val validLocale = "en_US".toLocale()
val localizedConfiguration = paywall.configForLocale(validLocale)
assertThat(localizedConfiguration).isNotNull
assertThat(localizedConfiguration?.callToActionWithMultipleIntroOffers).isEqualTo(
"Purchase now with multiple offers"
)
assertThat(localizedConfiguration?.offerDetailsWithMultipleIntroOffers).isEqualTo(
"Try {{ sub_offer_duration }} for free, then {{ sub_offer_price_2 }} for your first " +
"{{ sub_offer_duration_2 }}, and just {{ sub_price_per_month }} thereafter."
)
val english = paywall.configForLocale("en_US".toLocale())
assertThat(english).isNotNull
english?.apply {
assertThat(title).isEqualTo("Paywall")
assertThat(subtitle).isEqualTo("Description")

assertThat(callToAction).isEqualTo("Purchase now")
assertThat(callToActionWithIntroOffer).isEqualTo("Purchase now")
assertThat(callToActionWithMultipleIntroOffers).isEqualTo("Purchase now with multiple offers")

assertThat(offerDetails).isEqualTo("{{ sub_price_per_month }} per month")
assertThat(offerDetailsWithIntroOffer).isEqualTo(
"Start your {{ sub_offer_duration }} trial, " +
"then {{ sub_price_per_month }} per month"
)
assertThat(offerDetailsWithMultipleIntroOffers).isEqualTo(
"Try {{ sub_offer_duration }} for free, " +
"then {{ sub_offer_price_2 }} for your first {{ sub_offer_duration_2 }}, " +
"and just {{ sub_price_per_month }} thereafter."
)
}

val spanish = paywall.configForLocale("es".toLocale())
assertThat(spanish).isNotNull
spanish?.apply {
assertThat(title).isEqualTo("Tienda")
assertThat(subtitle).isNull()

assertThat(callToAction).isEqualTo("Comprar")
assertThat(callToActionWithIntroOffer).isNull()
assertThat(callToActionWithMultipleIntroOffers).isNull()

assertThat(offerDetails).isNull()
assertThat(offerDetailsWithIntroOffer).isNull()
assertThat(offerDetailsWithMultipleIntroOffers).isNull()
}
}

@Test
Expand Down
2 changes: 2 additions & 0 deletions purchases/src/test/resources/paywalldata-sample1.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"es_ES": {
"title": "Tienda",
"call_to_action": "Comprar",
"call_to_action_with_multiple_intro_offers": " ",
"offer_details_with_intro_offer": " ",
"offer_details_with_multiple_intro_offers": "",
"offer_name": "{{ period }}",
"features": [
{
Expand Down