From 50fb89cd3d15f0bb2a9a4855e80839a151366a8e Mon Sep 17 00:00:00 2001 From: Michael Shafrir <45020849+mshafrir-stripe@users.noreply.github.com> Date: Fri, 17 Apr 2020 15:23:37 -0400 Subject: [PATCH] Expose Klarna property on Source object (#2396) Added unit test --- .../example/activity/KlarnaSourceActivity.kt | 4 + .../java/com/stripe/android/model/Source.kt | 37 ++++++++- .../android/model/parsers/SourceJsonParser.kt | 80 ++++++++++++++++--- .../stripe/android/model/SourceFixtures.kt | 35 ++++++++ .../model/parsers/SourceJsonParserTest.kt | 36 +++++++++ 5 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 stripe/src/test/java/com/stripe/android/model/parsers/SourceJsonParserTest.kt diff --git a/example/src/main/java/com/stripe/example/activity/KlarnaSourceActivity.kt b/example/src/main/java/com/stripe/example/activity/KlarnaSourceActivity.kt index face09ec9a4..dba0b6d26ad 100644 --- a/example/src/main/java/com/stripe/example/activity/KlarnaSourceActivity.kt +++ b/example/src/main/java/com/stripe/example/activity/KlarnaSourceActivity.kt @@ -121,6 +121,10 @@ class KlarnaSourceActivity : AppCompatActivity() { klarnaParams = KlarnaSourceParams( purchaseCountry = "UK", lineItems = LINE_ITEMS, + customPaymentMethods = setOf( + KlarnaSourceParams.CustomPaymentMethods.Installments, + KlarnaSourceParams.CustomPaymentMethods.PayIn4 + ), billingFirstName = "Arthur", billingLastName = "Dent", billingAddress = Address.Builder() diff --git a/stripe/src/main/java/com/stripe/android/model/Source.kt b/stripe/src/main/java/com/stripe/android/model/Source.kt index 28ef8959ea5..8a96c405a79 100644 --- a/stripe/src/main/java/com/stripe/android/model/Source.kt +++ b/stripe/src/main/java/com/stripe/android/model/Source.kt @@ -126,7 +126,9 @@ data class Source internal constructor( @param:Usage @field:Usage @get:Usage val usage: String? = null, - private val weChatParam: WeChat? = null, + private val _weChat: WeChat? = null, + + private val _klarna: Klarna? = null, /** * Information about the items and shipping associated with the source. Required for @@ -147,7 +149,16 @@ data class Source internal constructor( "Source type must be '${SourceType.WECHAT}'" } - return requireNotNull(weChatParam) + return requireNotNull(_weChat) + } + + val klarna: Klarna + get() { + check(SourceType.KLARNA == type) { + "Source type must be '${SourceType.KLARNA}'" + } + + return requireNotNull(_klarna) } @Retention(AnnotationRetention.SOURCE) @@ -208,6 +219,28 @@ data class Source internal constructor( } } + @Parcelize + data class Klarna( + val firstName: String?, + val lastName: String?, + val purchaseCountry: String?, + val clientToken: String?, + val payNowAssetUrlsDescriptive: String?, + val payNowAssetUrlsStandard: String?, + val payNowName: String?, + val payNowRedirectUrl: String?, + val payLaterAssetUrlsDescriptive: String?, + val payLaterAssetUrlsStandard: String?, + val payLaterName: String?, + val payLaterRedirectUrl: String?, + val payOverTimeAssetUrlsDescriptive: String?, + val payOverTimeAssetUrlsStandard: String?, + val payOverTimeName: String?, + val payOverTimeRedirectUrl: String?, + val paymentMethodCategories: Set, + val customPaymentMethods: Set + ) : StripeModel + companion object { internal const val OBJECT_TYPE = "source" diff --git a/stripe/src/main/java/com/stripe/android/model/parsers/SourceJsonParser.kt b/stripe/src/main/java/com/stripe/android/model/parsers/SourceJsonParser.kt index 072088a4e6d..e1325f85e03 100644 --- a/stripe/src/main/java/com/stripe/android/model/parsers/SourceJsonParser.kt +++ b/stripe/src/main/java/com/stripe/android/model/parsers/SourceJsonParser.kt @@ -4,6 +4,7 @@ import androidx.annotation.Size import com.stripe.android.model.Source import com.stripe.android.model.SourceTypeModel import com.stripe.android.model.StripeJsonUtils +import com.stripe.android.model.StripeJsonUtils.optString import com.stripe.android.model.StripeModel import org.json.JSONObject @@ -16,6 +17,56 @@ internal class SourceJsonParser : ModelJsonParser { } } + internal class KlarnaJsonParser : ModelJsonParser { + override fun parse(json: JSONObject): Source.Klarna { + return Source.Klarna( + firstName = optString(json, FIELD_FIRST_NAME), + lastName = optString(json, FIELD_LAST_NAME), + purchaseCountry = optString(json, FIELD_PURCHASE_COUNTRY), + clientToken = optString(json, FIELD_CLIENT_TOKEN), + payLaterAssetUrlsDescriptive = optString(json, FIELD_PAY_LATER_ASSET_URLS_DESCRIPTIVE), + payLaterAssetUrlsStandard = optString(json, FIELD_PAY_LATER_ASSET_URLS_STANDARD), + payLaterName = optString(json, FIELD_PAY_LATER_NAME), + payLaterRedirectUrl = optString(json, FIELD_PAY_LATER_REDIRECT_URL), + payNowAssetUrlsDescriptive = optString(json, FIELD_PAY_NOW_ASSET_URLS_DESCRIPTIVE), + payNowAssetUrlsStandard = optString(json, FIELD_PAY_NOW_ASSET_URLS_STANDARD), + payNowName = optString(json, FIELD_PAY_NOW_NAME), + payNowRedirectUrl = optString(json, FIELD_PAY_NOW_REDIRECT_URL), + payOverTimeAssetUrlsDescriptive = optString(json, FIELD_PAY_OVER_TIME_ASSET_URLS_DESCRIPTIVE), + payOverTimeAssetUrlsStandard = optString(json, FIELD_PAY_OVER_TIME_ASSET_URLS_STANDARD), + payOverTimeName = optString(json, FIELD_PAY_OVER_TIME_NAME), + payOverTimeRedirectUrl = optString(json, FIELD_PAY_OVER_TIME_REDIRECT_URL), + paymentMethodCategories = parseSet(json, FIELD_PAYMENT_METHOD_CATEGORIES), + customPaymentMethods = parseSet(json, FIELD_CUSTOM_PAYMENT_METHODS) + ) + } + + private fun parseSet(json: JSONObject, key: String): Set { + return optString(json, key)?.split(",")?.toSet().orEmpty() + } + + private companion object { + private const val FIELD_FIRST_NAME = "first_name" + private const val FIELD_LAST_NAME = "last_name" + private const val FIELD_PURCHASE_COUNTRY = "purchase_country" + private const val FIELD_CLIENT_TOKEN = "client_token" + private const val FIELD_PAY_LATER_ASSET_URLS_DESCRIPTIVE = "pay_later_asset_urls_descriptive" + private const val FIELD_PAY_LATER_ASSET_URLS_STANDARD = "pay_later_asset_urls_standard" + private const val FIELD_PAY_LATER_NAME = "pay_later_name" + private const val FIELD_PAY_LATER_REDIRECT_URL = "pay_later_redirect_url" + private const val FIELD_PAY_NOW_ASSET_URLS_DESCRIPTIVE = "pay_now_asset_urls_descriptive" + private const val FIELD_PAY_NOW_ASSET_URLS_STANDARD = "pay_now_asset_urls_standard" + private const val FIELD_PAY_NOW_NAME = "pay_now_name" + private const val FIELD_PAY_NOW_REDIRECT_URL = "pay_now_redirect_url" + private const val FIELD_PAY_OVER_TIME_ASSET_URLS_DESCRIPTIVE = "pay_over_time_asset_urls_descriptive" + private const val FIELD_PAY_OVER_TIME_ASSET_URLS_STANDARD = "pay_over_time_asset_urls_standard" + private const val FIELD_PAY_OVER_TIME_NAME = "pay_over_time_name" + private const val FIELD_PAY_OVER_TIME_REDIRECT_URL = "pay_over_time_redirect_url" + private const val FIELD_PAYMENT_METHOD_CATEGORIES = "payment_method_categories" + private const val FIELD_CUSTOM_PAYMENT_METHODS = "custom_payment_methods" + } + } + private companion object { private const val VALUE_SOURCE = "source" private const val VALUE_CARD = "card" @@ -43,10 +94,11 @@ internal class SourceJsonParser : ModelJsonParser { private const val FIELD_TYPE: String = "type" private const val FIELD_USAGE: String = "usage" private const val FIELD_WECHAT: String = "wechat" + private const val FIELD_KLARNA: String = "klarna" private fun fromCardJson(jsonObject: JSONObject): Source { return Source( - StripeJsonUtils.optString(jsonObject, FIELD_ID), + optString(jsonObject, FIELD_ID), sourceTypeModel = SourceCardDataJsonParser().parse(jsonObject), type = Source.SourceType.CARD, typeRaw = Source.SourceType.CARD @@ -54,7 +106,7 @@ internal class SourceJsonParser : ModelJsonParser { } private fun fromSourceJson(jsonObject: JSONObject): Source { - @Source.SourceType val typeRaw = StripeJsonUtils.optString(jsonObject, FIELD_TYPE) + @Source.SourceType val typeRaw = optString(jsonObject, FIELD_TYPE) ?: Source.SourceType.UNKNOWN @Source.SourceType val type = asSourceType(typeRaw) @@ -71,16 +123,16 @@ internal class SourceJsonParser : ModelJsonParser { } return Source( - id = StripeJsonUtils.optString(jsonObject, FIELD_ID), + id = optString(jsonObject, FIELD_ID), amount = StripeJsonUtils.optLong(jsonObject, FIELD_AMOUNT), - clientSecret = StripeJsonUtils.optString(jsonObject, FIELD_CLIENT_SECRET), + clientSecret = optString(jsonObject, FIELD_CLIENT_SECRET), codeVerification = optStripeJsonModel( jsonObject, FIELD_CODE_VERIFICATION ), created = StripeJsonUtils.optLong(jsonObject, FIELD_CREATED), - currency = StripeJsonUtils.optString(jsonObject, FIELD_CURRENCY), - flow = asSourceFlow(StripeJsonUtils.optString(jsonObject, FIELD_FLOW)), + currency = optString(jsonObject, FIELD_CURRENCY), + flow = asSourceFlow(optString(jsonObject, FIELD_FLOW)), isLiveMode = jsonObject.optBoolean(FIELD_LIVEMODE), metaData = StripeJsonUtils.jsonObjectToStringMap( jsonObject.optJSONObject(FIELD_METADATA) @@ -91,19 +143,26 @@ internal class SourceJsonParser : ModelJsonParser { sourceOrder = jsonObject.optJSONObject(FIELD_SOURCE_ORDER)?.let { SourceOrderJsonParser().parse(it) }, - statementDescriptor = StripeJsonUtils.optString(jsonObject, FIELD_STATEMENT_DESCRIPTOR), - status = asSourceStatus(StripeJsonUtils.optString(jsonObject, FIELD_STATUS)), + statementDescriptor = optString(jsonObject, FIELD_STATEMENT_DESCRIPTOR), + status = asSourceStatus(optString(jsonObject, FIELD_STATUS)), sourceTypeData = sourceTypeData, sourceTypeModel = sourceTypeModel, type = type, typeRaw = typeRaw, - usage = asUsage(StripeJsonUtils.optString(jsonObject, FIELD_USAGE)), - weChatParam = if (Source.SourceType.WECHAT == type) { + usage = asUsage(optString(jsonObject, FIELD_USAGE)), + _weChat = if (Source.SourceType.WECHAT == type) { WeChatJsonParser().parse( jsonObject.optJSONObject(FIELD_WECHAT) ?: JSONObject() ) } else { null + }, + _klarna = if (Source.SourceType.KLARNA == type) { + KlarnaJsonParser().parse( + jsonObject.optJSONObject(FIELD_KLARNA) ?: JSONObject() + ) + } else { + null } ) } @@ -184,6 +243,7 @@ internal class SourceJsonParser : ModelJsonParser { Source.SourceType.MULTIBANCO -> Source.SourceType.MULTIBANCO Source.SourceType.WECHAT -> Source.SourceType.WECHAT Source.SourceType.UNKNOWN -> Source.SourceType.UNKNOWN + Source.SourceType.KLARNA -> Source.SourceType.KLARNA else -> Source.SourceType.UNKNOWN } } diff --git a/stripe/src/test/java/com/stripe/android/model/SourceFixtures.kt b/stripe/src/test/java/com/stripe/android/model/SourceFixtures.kt index 31051f5d941..050eea791f1 100644 --- a/stripe/src/test/java/com/stripe/android/model/SourceFixtures.kt +++ b/stripe/src/test/java/com/stripe/android/model/SourceFixtures.kt @@ -471,4 +471,39 @@ internal object SourceFixtures { } """.trimIndent() ) + + internal val KLARNA = requireNotNull(PARSER.parse(JSONObject( + """ + { + "id": "src_1FfB6GKmrohBAXC", + "object": "source", + "amount": 1000, + "created": 1573848540, + "currency": "eur", + "flow": "redirect", + "livemode": false, + "metadata": {}, + "source_order": $SOURCE_ORDER_JSON, + "statement_descriptor": "WIDGET FACTORY", + "status": "pending", + "type": "klarna", + "usage": "single_use", + "klarna": { + "first_name": "Arthur", + "last_name": "Dent", + "purchase_country": "UK", + "client_token": "CLIENT_TOKEN", + "pay_later_asset_urls_descriptive": "https:\/\/x.klarnacdn.net\/payment-method\/assets\/badges\/generic\/klarna.svg", + "pay_later_asset_urls_standard": "https:\/\/x.klarnacdn.net\/payment-method\/assets\/badges\/generic\/klarna.svg", + "pay_later_name": "Pay later in 14 days", + "pay_later_redirect_url": "https:\/\/payment-eu.playground.klarna.com\/8b45xe2", + "pay_over_time_asset_urls_descriptive": "https:\/\/x.klarnacdn.net\/payment-method\/assets\/badges\/generic\/klarna.svg", + "pay_over_time_asset_urls_standard": "https:\/\/x.klarnacdn.net\/payment-method\/assets\/badges\/generic\/klarna.svg", + "pay_over_time_name": "3 interest-free instalments", + "pay_over_time_redirect_url": "https:\/\/payment-eu.playground.klarna.com\/8DA6imn", + "payment_method_categories": "pay_later,pay_over_time" + } + } + """.trimIndent() + ))) } diff --git a/stripe/src/test/java/com/stripe/android/model/parsers/SourceJsonParserTest.kt b/stripe/src/test/java/com/stripe/android/model/parsers/SourceJsonParserTest.kt new file mode 100644 index 00000000000..9db97131f70 --- /dev/null +++ b/stripe/src/test/java/com/stripe/android/model/parsers/SourceJsonParserTest.kt @@ -0,0 +1,36 @@ +package com.stripe.android.model.parsers + +import com.google.common.truth.Truth.assertThat +import com.stripe.android.model.Source +import com.stripe.android.model.SourceFixtures +import org.junit.Test + +class SourceJsonParserTest { + + @Test + fun parse_shouldReturnExpectedObject() { + assertThat(SourceFixtures.KLARNA.klarna) + .isEqualTo( + Source.Klarna( + firstName = "Arthur", + lastName = "Dent", + purchaseCountry = "UK", + clientToken = "CLIENT_TOKEN", + payLaterAssetUrlsDescriptive = "https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg", + payLaterAssetUrlsStandard = "https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg", + payLaterName = "Pay later in 14 days", + payLaterRedirectUrl = "https://payment-eu.playground.klarna.com/8b45xe2", + payNowAssetUrlsDescriptive = null, + payNowAssetUrlsStandard = null, + payNowName = null, + payNowRedirectUrl = null, + payOverTimeAssetUrlsDescriptive = "https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg", + payOverTimeAssetUrlsStandard = "https://x.klarnacdn.net/payment-method/assets/badges/generic/klarna.svg", + payOverTimeName = "3 interest-free instalments", + payOverTimeRedirectUrl = "https://payment-eu.playground.klarna.com/8DA6imn", + paymentMethodCategories = setOf("pay_later", "pay_over_time"), + customPaymentMethods = emptySet() + ) + ) + } +}