Skip to content

Commit

Permalink
Adding new PaymentSheet FormElement for Blik payment method. (#7062)
Browse files Browse the repository at this point in the history
* Added pln currency support

* BLIK Localization

* Added BLIK to PaymentSheet

* Discovery

* UI Form

* Add parameter isOptions to data class IdentifierSpec

* Parse isOptions IdentifierSpec

* Ignore isOptions in Params parser

* passing PaymentMethodOptionsParams through layers

* Add blik

* formatting

* set options to Parsed form

* cleanup discovery commit

* cleanup discovery commit test

* undo accidental deletion

* undo accidental comment formatting

* use IdentifierSpec.Code to lookup fieldValuePairs entry

* use enum RequestDestination to determine api parameter

* rename Code to BlikCode

* Move Enum to Top-Level

* Remove redundant if branch

* removed unused function overrides

* Revert "BLIK Localization"

This reverts commit 62cf223.

* run `./gradlew ktlint`

* Formatting

* Ran ./gradlew :stripe-ui-core:apiDump

* Ran ./gradlew :stripe-ui-core:apiDump

* Ran ./gradlew apiDump

* Default null

* use blik text

* remove sectionFieldElement label

* cleanup and pass parameters

* remove unused import

* remove unused parameter

* restructure parameters

* ran ./gradlew apiDump

* Add data driven tests for BLIK

* Fix data driven tests for BLIK

* polling ctaText

* using BLIK specific polling

* Formatting

* update test for multiple authenticators

* fine tune validator

* don't look for browser if polling

* logic for PollingSucceedsAfterDelay

* BLIK payment sheet test

* Support PLN

* Add Polling AuthorizeAction

* Formatting and Grammar

* Formatting

* revert temp changes

* Use polling specific logic when polling

* Import success text

* Make blik polling timeout 30 seconds

* Specify blik instead of using automatic

* remove waitPollingComplete

* update logic to wait for polling to finish

* update comment

* format

* revert temp debugging change

* update changelog

* remove redundant length check

* make IdentifierSpec.BlikCode the default argument

* remove comment

* remove unused parameter

* error instead of defaulting to UPI

* remove comment

* deduplicate declaration in if

* validate argument of unregisterAuthenticator

* mark Blik and BlikCode as RestrictTo.Scope.LIBRARY_GROUP

* mark RequestDestination with RestrictTo.Scope.LIBRARY_GROUP

* rename confirmPaymentMethodOptions to optionsParams

* replace VALID_INPUT_RANGES with isDigit()

* ktlint

* fix MaximumLineLength exceeded

* remove BLIK and RequestDestination

* ktlint

* rename RequestDestination to ApiParameterDestination

* detekt

* add @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) for transformToPaymentMethodOptionsParams and fix apiCheck

* Fix BLIK PaymentSheet Changelog
  • Loading branch information
fionnbarrett-stripe authored Aug 31, 2023
1 parent c889dff commit 7501a0e
Show file tree
Hide file tree
Showing 37 changed files with 573 additions and 64 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## XX.XX.XX - 2023-XX-XX

### PaymentSheet
* [ADDED][7062](https://github.com/stripe/stripe-android/pull/7062) PaymentSheet now supports BLIK for PaymentIntents.

## 20.29.0 - 2023-08-28

### PaymentSheet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ sealed class ConfirmStripeIntentParamsFactory<out T : ConfirmStripeIntentParams>

abstract fun create(
createParams: PaymentMethodCreateParams,
optionsParams: PaymentMethodOptionsParams? = null,
setupFutureUsage: ConfirmPaymentIntentParams.SetupFutureUsage? = null,
): T

Expand Down Expand Up @@ -76,6 +77,7 @@ internal class ConfirmPaymentIntentParamsFactory(

override fun create(
createParams: PaymentMethodCreateParams,
optionsParams: PaymentMethodOptionsParams?,
setupFutureUsage: ConfirmPaymentIntentParams.SetupFutureUsage?,
): ConfirmPaymentIntentParams {
return ConfirmPaymentIntentParams.createWithPaymentMethodCreateParams(
Expand All @@ -97,6 +99,9 @@ internal class ConfirmPaymentIntentParamsFactory(
PaymentMethod.Type.USBankAccount.code -> {
PaymentMethodOptionsParams.USBankAccount(setupFutureUsage = setupFutureUsage)
}
PaymentMethod.Type.Blik.code -> {
optionsParams
}
PaymentMethod.Type.Link.code -> {
null
}
Expand All @@ -112,7 +117,6 @@ internal class ConfirmPaymentIntentParamsFactory(
internal class ConfirmSetupIntentParamsFactory(
private val clientSecret: String,
) : ConfirmStripeIntentParamsFactory<ConfirmSetupIntentParams>() {

override fun create(paymentMethod: PaymentMethod): ConfirmSetupIntentParams {
return ConfirmSetupIntentParams.create(
paymentMethodId = paymentMethod.id.orEmpty(),
Expand All @@ -125,6 +129,7 @@ internal class ConfirmSetupIntentParamsFactory(

override fun create(
createParams: PaymentMethodCreateParams,
optionsParams: PaymentMethodOptionsParams?,
setupFutureUsage: ConfirmPaymentIntentParams.SetupFutureUsage?,
): ConfirmSetupIntentParams {
return ConfirmSetupIntentParams.create(
Expand Down
16 changes: 16 additions & 0 deletions payments-ui-core/api/payments-ui-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,22 @@ public final class com/stripe/android/ui/core/elements/AuBankAccountNumberSpec$C
public final class com/stripe/android/ui/core/elements/AuBecsDebitMandateElementUIKt {
}

public final class com/stripe/android/ui/core/elements/BlikSpec$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field $stable I
public static final field INSTANCE Lcom/stripe/android/ui/core/elements/BlikSpec$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/stripe/android/ui/core/elements/BlikSpec;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/stripe/android/ui/core/elements/BlikSpec;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class com/stripe/android/ui/core/elements/BlikSpec$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/stripe/android/ui/core/elements/BsbElementUIKt {
}

Expand Down
26 changes: 26 additions & 0 deletions payments-ui-core/res/drawable/stripe_ic_paymentsheet_pm_blik.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<vector android:height="16dp" android:viewportHeight="32"
android:viewportWidth="32" android:width="16dp"
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:pathData="M0,0h32v32H0z">
<aapt:attr name="android:fillColor">
<gradient android:endX="15.999" android:endY="0.123"
android:startX="15.999" android:startY="31.772" android:type="linear">
<item android:color="#FF5A5A5A" android:offset="0"/>
<item android:color="#FF494949" android:offset="0.146"/>
<item android:color="#FF212121" android:offset="0.52"/>
<item android:color="#FF090909" android:offset="0.817"/>
<item android:color="#FF000000" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:fillColor="#FFF" android:pathData="M16.276,13.305a6.269,6.269 0,0 0,-2.968 0.745L13.308,6.95L10,6.95v12.629a6.277,6.277 0,0 0,12.553 0,6.275 6.275,0 0,0 -6.277,-6.274ZM16.276,22.6a3.021,3.021 0,1 1,0 -6.042,3.021 3.021,0 0,1 0,6.042Z"/>
<path android:pathData="M19.592,8.961m-2.961,0a2.961,2.961 0,1 1,5.922 0a2.961,2.961 0,1 1,-5.922 0">
<aapt:attr name="android:fillColor">
<gradient android:endX="21.686" android:endY="11.097"
android:startX="17.497" android:startY="6.835" android:type="linear">
<item android:color="#FFE52F08" android:offset="0"/>
<item android:color="#FFE94F96" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
</vector>
1 change: 1 addition & 0 deletions payments-ui-core/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<string name="stripe_paymentsheet_payment_method_amazon_pay">Amazon Pay</string>
<string name="stripe_paymentsheet_payment_method_mobile_pay">MobilePay</string>
<string name="stripe_paymentsheet_payment_method_zip">Zip</string>
<string name="stripe_paymentsheet_payment_method_blik">BLIK</string>
<!-- <string name="paymentsheet_payment_method_item_card_number">····%1$s</string>-->

<!-- &lt;!&ndash; Label indicating the payment intent is in testmode &ndash;&gt;-->
Expand Down
9 changes: 9 additions & 0 deletions payments-ui-core/src/main/assets/lpms.json
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,15 @@
}
]
},
{
"type": "blik",
"async": false,
"fields": [
{
"type": "blik"
}
]
},
{
"type": "us_bank_account",
"async": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ internal val UpiRequirement = PaymentMethodRequirements(
confirmPMFromCustomer = null
)

internal val BlikRequirement = PaymentMethodRequirements(
piRequirements = emptySet(),
siRequirements = null,
confirmPMFromCustomer = null
)

internal val CashAppPayRequirement = PaymentMethodRequirements(
piRequirements = emptySet(),
siRequirements = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.stripe.android.ui.core

import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCode
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.uicore.elements.IdentifierSpec
import com.stripe.android.uicore.forms.FormFieldEntry

Expand Down Expand Up @@ -35,6 +37,25 @@ class FieldValuesToParamsMapConverter {
)
}

/**
* This function will convert fieldValuePairs to PaymentMethodOptionsParams.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun transformToPaymentMethodOptionsParams(
fieldValuePairs: Map<IdentifierSpec, FormFieldEntry>,
code: PaymentMethodCode,
): PaymentMethodOptionsParams? {
if (code == PaymentMethod.Type.Blik.code) {
val blikCode = fieldValuePairs[IdentifierSpec.BlikCode]?.value
if (blikCode != null) {
return PaymentMethodOptionsParams.Blik(
blikCode
)
}
}
return null
}

/**
* This function will put the field values as defined in the fieldValuePairs into a map
* according to their keys.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.stripe.android.ui.core.elements

import androidx.annotation.RestrictTo
import androidx.annotation.StringRes
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import com.stripe.android.ui.core.R
import com.stripe.android.uicore.elements.TextFieldConfig
import com.stripe.android.uicore.elements.TextFieldIcon
import com.stripe.android.uicore.elements.TextFieldState
import com.stripe.android.uicore.elements.TextFieldStateConstants
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

private const val BLIK_MAX_LENGTH = 6

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class BlikConfig : TextFieldConfig {

private val blikPattern: Regex by lazy {
"^[0-9]{6}\$".toRegex()
}

@StringRes
override val label: Int = R.string.stripe_blik_code

override val capitalization: KeyboardCapitalization = KeyboardCapitalization.None
override val debugLabel: String = "blik_code"
override val keyboard: KeyboardType = KeyboardType.Number
override val visualTransformation: VisualTransformation? = null
override val trailingIcon: StateFlow<TextFieldIcon?> = MutableStateFlow(value = null)
override val loading: StateFlow<Boolean> = MutableStateFlow(value = false)

override fun determineState(input: String): TextFieldState {
val isValid = blikPattern.matches(input)
return if (input.isEmpty()) {
TextFieldStateConstants.Error.Blank
} else if (isValid) {
TextFieldStateConstants.Valid.Limitless
} else if (!input.all { it.isDigit() }) {
TextFieldStateConstants.Error.Invalid(
errorMessageResId = R.string.stripe_invalid_blik_code
)
} else if (input.length < BLIK_MAX_LENGTH) {
TextFieldStateConstants.Error.Incomplete(
errorMessageResId = R.string.stripe_incomplete_blik_code
)
} else {
TextFieldStateConstants.Error.Invalid(
errorMessageResId = R.string.stripe_invalid_blik_code
)
}
}

override fun filter(userTyped: String) =
userTyped.filter { it.isDigit() }.take(BLIK_MAX_LENGTH)

override fun convertToRaw(displayName: String): String = displayName

override fun convertFromRaw(rawValue: String): String = rawValue
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.stripe.android.ui.core.elements

import androidx.annotation.RestrictTo
import com.stripe.android.uicore.elements.IdentifierSpec
import com.stripe.android.uicore.elements.InputController
import com.stripe.android.uicore.elements.SectionSingleFieldElement
import com.stripe.android.uicore.elements.SimpleTextFieldController
import com.stripe.android.uicore.forms.FormFieldEntry
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class BlikElement(
override val identifier: IdentifierSpec = IdentifierSpec.BlikCode,
override val controller: InputController = SimpleTextFieldController(
textFieldConfig = BlikConfig()
)
) : SectionSingleFieldElement(identifier = identifier) {

override fun getFormFieldValueFlow(): Flow<List<Pair<IdentifierSpec, FormFieldEntry>>> {
return controller.formFieldValue.map { entry ->
listOf(identifier to entry)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.stripe.android.ui.core.elements

import androidx.annotation.RestrictTo
import com.stripe.android.uicore.elements.IdentifierSpec
import com.stripe.android.uicore.elements.SectionElement
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Serializable
data class BlikSpec(
@SerialName("api_path")
override val apiPath: IdentifierSpec = IdentifierSpec.Blik
) : FormItemSpec() {
fun transform(): SectionElement {
return createSectionElement(
sectionFieldElement = BlikElement(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ object FormItemSpecSerializer :
"card_details" -> CardDetailsSectionSpec.serializer()
"card_billing" -> CardBillingSpec.serializer()
"upi" -> UpiSpec.serializer()
"blik" -> BlikSpec.serializer()
"placeholder" -> PlaceholderSpec.serializer()
else -> EmptyFormSpec.serializer()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.stripe.android.ui.core.elements.AffirmTextSpec
import com.stripe.android.ui.core.elements.AfterpayClearpayTextSpec
import com.stripe.android.ui.core.elements.AuBankAccountNumberSpec
import com.stripe.android.ui.core.elements.AuBecsDebitMandateTextSpec
import com.stripe.android.ui.core.elements.BlikSpec
import com.stripe.android.ui.core.elements.BsbSpec
import com.stripe.android.ui.core.elements.CardBillingSpec
import com.stripe.android.ui.core.elements.CardDetailsSectionSpec
Expand Down Expand Up @@ -99,6 +100,7 @@ class TransformSpecToElements(
)
is SepaMandateTextSpec -> it.transform(merchantName)
is UpiSpec -> it.transform()
is BlikSpec -> it.transform()
is ContactInformationSpec -> it.transform(initialValues)
is PlaceholderSpec ->
error("Placeholders should be processed before calling transform.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.stripe.android.paymentsheet.forms.AfterpayClearpayRequirement
import com.stripe.android.paymentsheet.forms.AmazonPayRequirement
import com.stripe.android.paymentsheet.forms.AuBecsDebitRequirement
import com.stripe.android.paymentsheet.forms.BancontactRequirement
import com.stripe.android.paymentsheet.forms.BlikRequirement
import com.stripe.android.paymentsheet.forms.CardRequirement
import com.stripe.android.paymentsheet.forms.CashAppPayRequirement
import com.stripe.android.paymentsheet.forms.EpsRequirement
Expand Down Expand Up @@ -94,6 +95,7 @@ class LpmRepository constructor(
PaymentMethod.Type.Zip.code,
PaymentMethod.Type.AuBecsDebit.code,
PaymentMethod.Type.Upi.code,
PaymentMethod.Type.Blik.code,
PaymentMethod.Type.CashAppPay.code,
PaymentMethod.Type.GrabPay.code,
PaymentMethod.Type.Fpx.code,
Expand Down Expand Up @@ -507,6 +509,20 @@ class LpmRepository constructor(
requirement = UpiRequirement,
formSpec = LayoutSpec(sharedDataSpec.fields)
)

PaymentMethod.Type.Blik.code -> SupportedPaymentMethod(
code = "blik",
requiresMandate = false,
mandateRequirement = MandateRequirement.Never,
displayNameResource = R.string.stripe_paymentsheet_payment_method_blik,
iconResource = R.drawable.stripe_ic_paymentsheet_pm_blik,
lightThemeIconUrl = sharedDataSpec.selectorIcon?.lightThemePng,
darkThemeIconUrl = sharedDataSpec.selectorIcon?.darkThemePng,
tintIconOnSelection = false,
requirement = BlikRequirement,
formSpec = LayoutSpec(sharedDataSpec.fields)
)

PaymentMethod.Type.CashAppPay.code -> SupportedPaymentMethod(
code = "cashapp",
requiresMandate = false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.stripe.android.ui.core.elements

import com.google.common.truth.Truth.assertThat
import com.stripe.android.uicore.elements.TextFieldStateConstants.Error.Blank
import com.stripe.android.uicore.elements.TextFieldStateConstants.Error.Incomplete
import com.stripe.android.uicore.elements.TextFieldStateConstants.Error.Invalid
import com.stripe.android.uicore.elements.TextFieldStateConstants.Valid.Limitless
import org.junit.Test

class BlikConfigTest {
private val config = BlikConfig()

@Test
fun `Treats empty input as blank`() {
val state = config.determineState("")
assertThat(state).isEqualTo(Blank)
}

@Test
fun `Rejects blank input`() {
assertThat(config.filter(" ")).isEqualTo("")
}

@Test
fun `Rejects non-numeric input`() {
assertThat(config.filter(" ")).isEqualTo("")
}

@Test
fun `Treats input with less than six digits as incomplete`() {
val state = config.determineState("12345")
assertThat(state).isInstanceOf(Incomplete::class.java)
}

@Test
fun `Treats input more than six digits as invalid`() {
assertThat(config.determineState("1234567")).isInstanceOf(Invalid::class.java)
}

@Test
fun `Treats non-numeric input as blank`() {
assertThat(config.determineState("12a456")).isInstanceOf(Invalid::class.java)
assertThat(config.determineState("abcdef")).isInstanceOf(Invalid::class.java)
assertThat(config.determineState("stripe.com")).isInstanceOf(Invalid::class.java)
}

@Test
fun `Treats valid input as valid and limitless`() {
val state = config.determineState("123456")
assertThat(state).isInstanceOf(Limitless::class.java)
}
}
Loading

0 comments on commit 7501a0e

Please sign in to comment.