diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb2f65ecf4..4948fbca9b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ This release patches on a crash on PaymentLauncher, updates the package name of changes the public API for CardImageVerificationSheet and releases Identity SDK. ### Payments (`com.stripe:stripe-android`) +* [ADDED] [4804](https://github.com/stripe/stripe-android/pull/4804) Added card-scanning feature to PaymentSheet * [FIXED] [4776](https://github.com/stripe/stripe-android/pull/4776) fix issue with PaymentLauncher configuration change * [CHANGED] [4358](https://github.com/stripe/stripe-android/pull/4358) Updated the card element on PaymentSheet to use Compose. diff --git a/payments-ui-core/api/payments-ui-core.api b/payments-ui-core/api/payments-ui-core.api index 7e9bd06f938..ae8a0b1a80d 100644 --- a/payments-ui-core/api/payments-ui-core.api +++ b/payments-ui-core/api/payments-ui-core.api @@ -19,6 +19,15 @@ public final class com/stripe/android/ui/core/address/AddressFieldElementReposit public static fun newInstance (Landroid/content/res/Resources;)Lcom/stripe/android/ui/core/address/AddressFieldElementRepository; } +public final class com/stripe/android/ui/core/databinding/ActivityCardScanBinding : androidx/viewbinding/ViewBinding { + public final field fragmentContainer Landroidx/fragment/app/FragmentContainerView; + public static fun bind (Landroid/view/View;)Lcom/stripe/android/ui/core/databinding/ActivityCardScanBinding; + public synthetic fun getRoot ()Landroid/view/View; + public fun getRoot ()Landroidx/constraintlayout/widget/ConstraintLayout; + public static fun inflate (Landroid/view/LayoutInflater;)Lcom/stripe/android/ui/core/databinding/ActivityCardScanBinding; + public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lcom/stripe/android/ui/core/databinding/ActivityCardScanBinding; +} + public final class com/stripe/android/ui/core/elements/AffirmElementUIKt { } @@ -42,6 +51,15 @@ public final class com/stripe/android/ui/core/elements/BankRepository_Factory : public final class com/stripe/android/ui/core/elements/BsbElementUIKt { } +public final class com/stripe/android/ui/core/elements/CardDetailsSectionController : com/stripe/android/ui/core/elements/SectionFieldErrorController { + public static final field $stable I + public fun (Landroid/content/Context;)V + public fun getError ()Lkotlinx/coroutines/flow/Flow; +} + +public final class com/stripe/android/ui/core/elements/CardDetailsSectionElementUIKt { +} + public final class com/stripe/android/ui/core/elements/ComposableSingletons$AfterpayClearpayElementUIKt { public static final field INSTANCE Lcom/stripe/android/ui/core/elements/ComposableSingletons$AfterpayClearpayElementUIKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; diff --git a/payments-ui-core/build.gradle b/payments-ui-core/build.gradle index c2239fb7f1b..e11f7835564 100644 --- a/payments-ui-core/build.gradle +++ b/payments-ui-core/build.gradle @@ -21,6 +21,7 @@ if (System.getenv("JITPACK")) { dependencies { implementation project(":stripe-core") implementation project(":payments-core") + compileOnly project(":stripecardscan") implementation 'androidx.constraintlayout:constraintlayout-compose:1.0.0' implementation "androidx.core:core-ktx:$androidxCoreVersion" @@ -63,6 +64,7 @@ dependencies { testImplementation "androidx.test.ext:junit-ktx:$androidTestJunitVersion" testImplementation "androidx.arch.core:core-testing:$androidxArchCoreVersion" testImplementation "androidx.fragment:fragment-testing:$androidxFragmentVersion" + testImplementation project(':stripecardscan') ktlint "com.pinterest:ktlint:$ktlintVersion" } @@ -102,6 +104,7 @@ android { buildFeatures { compose = true + viewBinding = true } testOptions { diff --git a/payments-ui-core/res/drawable/ic_photo_camera.xml b/payments-ui-core/res/drawable/ic_photo_camera.xml new file mode 100644 index 00000000000..39203cda916 --- /dev/null +++ b/payments-ui-core/res/drawable/ic_photo_camera.xml @@ -0,0 +1,13 @@ + + + + diff --git a/payments-ui-core/res/layout/activity_card_scan.xml b/payments-ui-core/res/layout/activity_card_scan.xml new file mode 100644 index 00000000000..98e236d88bd --- /dev/null +++ b/payments-ui-core/res/layout/activity_card_scan.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/payments-ui-core/res/values/totranslate.xml b/payments-ui-core/res/values/totranslate.xml index ba0d349623a..9911afb4bac 100644 --- a/payments-ui-core/res/values/totranslate.xml +++ b/payments-ui-core/res/values/totranslate.xml @@ -5,4 +5,7 @@ --> Card information + + + Scan card diff --git a/payments-ui-core/src/main/AndroidManifest.xml b/payments-ui-core/src/main/AndroidManifest.xml index 345ab2b6d48..d8dd5820485 100644 --- a/payments-ui-core/src/main/AndroidManifest.xml +++ b/payments-ui-core/src/main/AndroidManifest.xml @@ -2,4 +2,11 @@ + + + + diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/IsStripeCardScanAvailable.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/IsStripeCardScanAvailable.kt new file mode 100644 index 00000000000..d1733106996 --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/IsStripeCardScanAvailable.kt @@ -0,0 +1,16 @@ +package com.stripe.android.ui.core + +internal interface IsStripeCardScanAvailable { + operator fun invoke(): Boolean +} + +internal class DefaultIsStripeCardScanAvailable : IsStripeCardScanAvailable { + override fun invoke(): Boolean { + return try { + Class.forName("com.stripe.android.stripecardscan.cardscan.CardScanSheet") + true + } catch (_: Exception) { + false + } + } +} diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/StripeCardScanProxy.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/StripeCardScanProxy.kt new file mode 100644 index 00000000000..f570cd46d24 --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/StripeCardScanProxy.kt @@ -0,0 +1,108 @@ +package com.stripe.android.ui.core + +import androidx.annotation.IdRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LifecycleOwner +import com.stripe.android.BuildConfig +import com.stripe.android.stripecardscan.cardscan.CardScanSheet +import com.stripe.android.stripecardscan.cardscan.CardScanSheetResult + +/** + * Proxy to access stripecardscan code safely + * + */ +internal interface StripeCardScanProxy { + fun present() + + fun attachCardScanFragment( + lifecycleOwner: LifecycleOwner, + supportFragmentManager: FragmentManager, + @IdRes fragmentContainer: Int, + onFinished: (cardScanSheetResult: CardScanSheetResult) -> Unit + ) + + companion object { + fun create( + fragment: Fragment, + stripePublishableKey: String, + onFinished: (cardScanSheetResult: CardScanSheetResult) -> Unit, + provider: () -> StripeCardScanProxy = { + DefaultStripeCardScanProxy(CardScanSheet.create(fragment, stripePublishableKey, onFinished)) + }, + isStripeCardScanAvailable: IsStripeCardScanAvailable = DefaultIsStripeCardScanAvailable() + ): StripeCardScanProxy { + return if (isStripeCardScanAvailable()) { + provider() + } else { + UnsupportedStripeCardScanProxy() + } + } + + fun create( + activity: AppCompatActivity, + stripePublishableKey: String, + onFinished: (cardScanSheetResult: CardScanSheetResult) -> Unit, + provider: () -> StripeCardScanProxy = { + DefaultStripeCardScanProxy(CardScanSheet.create(activity, stripePublishableKey, onFinished)) + }, + isStripeCardScanAvailable: IsStripeCardScanAvailable = DefaultIsStripeCardScanAvailable() + ): StripeCardScanProxy { + return if (isStripeCardScanAvailable()) { + provider() + } else { + UnsupportedStripeCardScanProxy() + } + } + + fun removeCardScanFragment( + supportFragmentManager: FragmentManager, + isStripeCardScanAvailable: IsStripeCardScanAvailable = DefaultIsStripeCardScanAvailable() + ) { + if (isStripeCardScanAvailable()) { + CardScanSheet.removeCardScanFragment(supportFragmentManager) + } + } + } +} + +internal class DefaultStripeCardScanProxy( + private val cardScanSheet: CardScanSheet +) : StripeCardScanProxy { + override fun present() { + cardScanSheet.present() + } + + override fun attachCardScanFragment( + lifecycleOwner: LifecycleOwner, + supportFragmentManager: FragmentManager, + fragmentContainer: Int, + onFinished: (cardScanSheetResult: CardScanSheetResult) -> Unit + ) { + cardScanSheet.attachCardScanFragment(lifecycleOwner, supportFragmentManager, fragmentContainer, onFinished) + } +} + +internal class UnsupportedStripeCardScanProxy : StripeCardScanProxy { + override fun present() { + if (BuildConfig.DEBUG) { + throw IllegalStateException( + "Missing stripecardscan dependency, please add it to your apps build.gradle" + ) + } + } + + override fun attachCardScanFragment( + lifecycleOwner: LifecycleOwner, + supportFragmentManager: FragmentManager, + fragmentContainer: Int, + onFinished: (cardScanSheetResult: CardScanSheetResult) -> Unit + ) { + if (BuildConfig.DEBUG) { + throw IllegalStateException( + "Missing stripecardscan dependency, please add it to your apps build.gradle" + ) + } + } +} diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/cardscan/CardScanActivity.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/cardscan/CardScanActivity.kt new file mode 100644 index 00000000000..c9bc9fe6a30 --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/cardscan/CardScanActivity.kt @@ -0,0 +1,53 @@ +package com.stripe.android.ui.core.cardscan + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.stripe.android.PaymentConfiguration +import com.stripe.android.stripecardscan.cardscan.CardScanSheetResult +import com.stripe.android.ui.core.R +import com.stripe.android.ui.core.StripeCardScanProxy +import com.stripe.android.ui.core.databinding.ActivityCardScanBinding + +internal class CardScanActivity : AppCompatActivity() { + private val viewBinding by lazy { + ActivityCardScanBinding.inflate(layoutInflater) + } + + private lateinit var stripeCardScanProxy: StripeCardScanProxy + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(viewBinding.root) + + stripeCardScanProxy = StripeCardScanProxy.create( + this, PaymentConfiguration.getInstance(this).publishableKey, this::onScanFinished + ) + } + + override fun onStart() { + super.onStart() + stripeCardScanProxy.attachCardScanFragment( + this, supportFragmentManager, R.id.fragment_container, this::onScanFinished + ) + } + + private fun onScanFinished(result: CardScanSheetResult) { + val intent = Intent() + .putExtra( + CARD_SCAN_PARCELABLE_NAME, + result + ) + setResult(RESULT_OK, intent) + finish() + } + + override fun onStop() { + StripeCardScanProxy.removeCardScanFragment(supportFragmentManager) + super.onStop() + } + + companion object { + const val CARD_SCAN_PARCELABLE_NAME = "CardScanActivityResult" + } +} diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionController.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionController.kt new file mode 100644 index 00000000000..b25c93d9fe7 --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionController.kt @@ -0,0 +1,15 @@ +package com.stripe.android.ui.core.elements + +import android.content.Context +import com.stripe.android.ui.core.DefaultIsStripeCardScanAvailable + +class CardDetailsSectionController(context: Context) : SectionFieldErrorController { + + internal val cardDetailsElement = CardDetailsElement( + IdentifierSpec.Generic("card_detail"), context + ) + + internal val isStripeCardScanAvailable = DefaultIsStripeCardScanAvailable() + + override val error = cardDetailsElement.controller.error +} diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionElement.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionElement.kt new file mode 100644 index 00000000000..f6ddb9ad6bc --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionElement.kt @@ -0,0 +1,19 @@ +package com.stripe.android.ui.core.elements + +import android.content.Context +import androidx.annotation.RestrictTo +import com.stripe.android.ui.core.forms.FormFieldEntry +import kotlinx.coroutines.flow.Flow + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class CardDetailsSectionElement( + val context: Context, + override val identifier: IdentifierSpec, + override val controller: CardDetailsSectionController = CardDetailsSectionController(context), +) : FormElement() { + override fun getFormFieldValueFlow(): Flow>> = + controller.cardDetailsElement.getFormFieldValueFlow() + + override fun getTextFieldIdentifiers(): Flow> = + controller.cardDetailsElement.getTextFieldIdentifiers() +} diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionElementUI.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionElementUI.kt new file mode 100644 index 00000000000..88ae43805ae --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionElementUI.kt @@ -0,0 +1,60 @@ +package com.stripe.android.ui.core.elements + +import androidx.annotation.RestrictTo +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import com.stripe.android.stripecardscan.cardscan.CardScanSheetResult +import com.stripe.android.stripecardscan.cardscan.exception.UnknownScanException +import com.stripe.android.ui.core.R +import com.stripe.android.ui.core.cardscan.CardScanActivity + +@Composable +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +fun CardDetailsSectionElementUI( + enabled: Boolean, + controller: CardDetailsSectionController, + hiddenIdentifiers: List? +) { + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + H6Text( + text = stringResource(R.string.card_information), + modifier = Modifier + .semantics(mergeDescendants = true) { // Need to prevent form as focusable accessibility + heading() + } + ) + if (controller.isStripeCardScanAvailable()) { + ScanCardButtonUI { + controller.cardDetailsElement.controller.numberElement.controller.onCardScanResult( + it.getParcelableExtra(CardScanActivity.CARD_SCAN_PARCELABLE_NAME) + ?: CardScanSheetResult.Failed( + UnknownScanException("No data in the result intent") + ) + ) + } + } + } + SectionElementUI( + enabled, + SectionElement( + IdentifierSpec.Generic("credit_details"), + listOf(controller.cardDetailsElement), + SectionController(null, listOf(controller.cardDetailsElement.sectionFieldErrorController())) + ), + hiddenIdentifiers ?: emptyList(), + IdentifierSpec.Generic("card_details") + ) +} diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionSpec.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionSpec.kt new file mode 100644 index 00000000000..840847a9215 --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSectionSpec.kt @@ -0,0 +1,16 @@ +package com.stripe.android.ui.core.elements + +import android.content.Context +import kotlinx.parcelize.Parcelize + +/** + * Section containing card details form + */ +@Parcelize +internal data class CardDetailsSectionSpec( + val identifier: IdentifierSpec +) : FormItemSpec() { + fun transform(context: Context): FormElement = CardDetailsSectionElement( + context, identifier + ) +} diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSpec.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSpec.kt index 27b61115203..e69de29bb2d 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSpec.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardDetailsSpec.kt @@ -1,13 +0,0 @@ -package com.stripe.android.ui.core.elements - -import android.content.Context -import androidx.annotation.RestrictTo -import kotlinx.parcelize.Parcelize - -@Parcelize -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -object CardDetailsSpec : SectionFieldSpec(IdentifierSpec.Generic("card_details")) { - fun transform(context: Context): SectionFieldElement = CardDetailsElement( - IdentifierSpec.Generic("credit_detail"), context - ) -} diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardNumberController.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardNumberController.kt index f756e81758f..b5bc4a2ba26 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardNumberController.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/CardNumberController.kt @@ -12,6 +12,7 @@ import com.stripe.android.cards.DefaultStaticCardAccountRanges import com.stripe.android.cards.StaticCardAccountRanges import com.stripe.android.model.AccountRange import com.stripe.android.model.CardBrand +import com.stripe.android.stripecardscan.cardscan.CardScanSheetResult import com.stripe.android.ui.core.forms.FormFieldEntry import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -138,4 +139,11 @@ internal class CardNumberController constructor( override fun onFocusChange(newHasFocus: Boolean) { _hasFocus.value = newHasFocus } + + internal fun onCardScanResult(cardScanSheetResult: CardScanSheetResult) { + // Don't need to populate the card number if the result is Canceled or Failed + if (cardScanSheetResult is CardScanSheetResult.Completed) { + onRawValueChange(cardScanSheetResult.scannedCard.pan) + } + } } diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/ScanCardButtonUI.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/ScanCardButtonUI.kt new file mode 100644 index 00000000000..f9fb89b24a7 --- /dev/null +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/elements/ScanCardButtonUI.kt @@ -0,0 +1,72 @@ +package com.stripe.android.ui.core.elements + +import android.content.Intent +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.stripe.android.ui.core.PaymentsTheme +import com.stripe.android.ui.core.R +import com.stripe.android.ui.core.cardscan.CardScanActivity + +@Composable +internal fun ScanCardButtonUI( + onResult: (intent: Intent) -> Unit +) { + val context = LocalContext.current + + val cardScanLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + it.data?.let { + onResult(it) + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { + cardScanLauncher.launch( + Intent( + context, + CardScanActivity::class.java + ) + ) + } + ) + ) { + Image( + painter = painterResource(R.drawable.ic_photo_camera), + contentDescription = stringResource( + R.string.scan_card + ), + colorFilter = ColorFilter.tint(PaymentsTheme.colors.material.primary), + modifier = Modifier + .width(18.dp) + .height(18.dp) + ) + Text( + stringResource(R.string.scan_card), + Modifier + .padding(start = 4.dp), + color = PaymentsTheme.colors.material.primary, + style = PaymentsTheme.typography.h6, + ) + } +} diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/CardSpec.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/CardSpec.kt index 2679c067965..b22ecb943ba 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/CardSpec.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/CardSpec.kt @@ -3,7 +3,7 @@ package com.stripe.android.ui.core.forms import androidx.annotation.RestrictTo import com.stripe.android.ui.core.R import com.stripe.android.ui.core.elements.CardBillingSpec -import com.stripe.android.ui.core.elements.CardDetailsSpec +import com.stripe.android.ui.core.elements.CardDetailsSectionSpec import com.stripe.android.ui.core.elements.IdentifierSpec import com.stripe.android.ui.core.elements.LayoutSpec import com.stripe.android.ui.core.elements.SaveForFutureUseSpec @@ -25,12 +25,6 @@ val CardParamKey: MutableMap = mutableMapOf( "card" to cardParams ) -internal val creditDetailsSection = SectionSpec( - IdentifierSpec.Generic("credit_details_section"), - CardDetailsSpec, - R.string.card_information -) - internal val creditBillingSection = SectionSpec( IdentifierSpec.Generic("credit_billing_section"), CardBillingSpec( @@ -41,7 +35,7 @@ internal val creditBillingSection = SectionSpec( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val CardForm = LayoutSpec.create( - creditDetailsSection, + CardDetailsSectionSpec(IdentifierSpec.Generic("credit_details_section")), creditBillingSection, SaveForFutureUseSpec(emptyList()) ) diff --git a/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/TransformSpecToElements.kt b/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/TransformSpecToElements.kt index da0319b4f25..faad853ea7e 100644 --- a/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/TransformSpecToElements.kt +++ b/payments-ui-core/src/main/java/com/stripe/android/ui/core/forms/TransformSpecToElements.kt @@ -12,7 +12,7 @@ import com.stripe.android.ui.core.elements.BankDropdownSpec import com.stripe.android.ui.core.elements.BankRepository import com.stripe.android.ui.core.elements.BsbSpec import com.stripe.android.ui.core.elements.CardBillingSpec -import com.stripe.android.ui.core.elements.CardDetailsSpec +import com.stripe.android.ui.core.elements.CardDetailsSectionSpec import com.stripe.android.ui.core.elements.CountrySpec import com.stripe.android.ui.core.elements.EmailSpec import com.stripe.android.ui.core.elements.EmptyFormElement @@ -71,6 +71,7 @@ class TransformSpecToElements( it.transform() is EmptyFormSpec -> EmptyFormElement() is AuBecsDebitMandateTextSpec -> it.transform(merchantName) + is CardDetailsSectionSpec -> it.transform(context) is BsbSpec -> it.transform() } } @@ -129,7 +130,6 @@ class TransformSpecToElements( currencyCode, country ) - is CardDetailsSpec -> it.transform(context) is CardBillingSpec -> it.transform(addressRepository) is AuBankAccountNumberSpec -> it.transform() } diff --git a/payments-ui-core/src/test/java/com/stripe/android/ui/core/StripeCardScanProxyTest.kt b/payments-ui-core/src/test/java/com/stripe/android/ui/core/StripeCardScanProxyTest.kt new file mode 100644 index 00000000000..0f94a7afa3c --- /dev/null +++ b/payments-ui-core/src/test/java/com/stripe/android/ui/core/StripeCardScanProxyTest.kt @@ -0,0 +1,109 @@ +package com.stripe.android.ui.core + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LifecycleOwner +import com.stripe.android.stripecardscan.cardscan.CardScanSheet +import com.stripe.android.stripecardscan.cardscan.CardScanSheetResult +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class StripeCardScanProxyTest { + companion object { + private const val CARDSCANSHEET_CANONICAL_NAME = + "com.stripe.android.stripecardscan.cardscan.CardScanSheet" + } + + private val mockIsStripeCardScanAvailable: IsStripeCardScanAvailable = mock() + private val mockFragment: Fragment = mock() + private val mockActivity: AppCompatActivity = mock() + + private class FakeProxy : StripeCardScanProxy { + override fun present() { + // noop + } + + override fun attachCardScanFragment( + lifecycleOwner: LifecycleOwner, + supportFragmentManager: FragmentManager, + fragmentContainer: Int, + onFinished: (cardScanSheetResult: CardScanSheetResult) -> Unit + ) { + // noop + } + } + + @Test + fun `StripeCardScan SDK availability returns null when connections module is not loaded`() { + assertTrue( + StripeCardScanProxy.create( + fragment = mockFragment, + stripePublishableKey = "test", + onFinished = {}, + isStripeCardScanAvailable = mockIsStripeCardScanAvailable + ) is UnsupportedStripeCardScanProxy + ) + assertTrue( + StripeCardScanProxy.create( + activity = mockActivity, + stripePublishableKey = "test", + onFinished = {}, + isStripeCardScanAvailable = mockIsStripeCardScanAvailable + ) is UnsupportedStripeCardScanProxy + ) + } + + @Test + fun `StripeCardScan SDK availability returns sdk when stripecardscan module is loaded`() { + whenever(mockIsStripeCardScanAvailable()).thenAnswer { true } + + assertTrue( + StripeCardScanProxy.create( + fragment = mockFragment, + stripePublishableKey = "test", + onFinished = {}, + isStripeCardScanAvailable = mockIsStripeCardScanAvailable, + provider = { FakeProxy() } + ) is FakeProxy + ) + assertTrue( + StripeCardScanProxy.create( + activity = mockActivity, + stripePublishableKey = "test", + onFinished = {}, + isStripeCardScanAvailable = mockIsStripeCardScanAvailable, + provider = { FakeProxy() } + ) is FakeProxy + ) + } + + @Test + fun `calling present on UnsupportedStripeCardScanProxy throws an exception`() { + assertFailsWith { + StripeCardScanProxy.create( + fragment = mockFragment, + stripePublishableKey = "", + onFinished = {}, + isStripeCardScanAvailable = mockIsStripeCardScanAvailable + ).present() + } + assertFailsWith { + StripeCardScanProxy.create( + activity = mockActivity, + stripePublishableKey = "", + onFinished = {}, + isStripeCardScanAvailable = mockIsStripeCardScanAvailable + ).present() + } + } + + @Test + fun `ensure CardScanSheet exists`() { + assertEquals(CARDSCANSHEET_CANONICAL_NAME, CardScanSheet::class.qualifiedName) + } +} diff --git a/paymentsheet-example/build.gradle b/paymentsheet-example/build.gradle index 1d6af76e6bf..fcfaa4f8db7 100644 --- a/paymentsheet-example/build.gradle +++ b/paymentsheet-example/build.gradle @@ -12,6 +12,7 @@ def getBackendUrl() { dependencies { implementation project(':payments-core') implementation project(':paymentsheet') + implementation project(':stripecardscan') implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidxLifecycleVersion" diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/forms/FormUI.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/forms/FormUI.kt index 27ec1a066f4..fd36b4ea975 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/forms/FormUI.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/forms/FormUI.kt @@ -22,6 +22,8 @@ import com.stripe.android.ui.core.elements.AfterpayClearpayElementUI import com.stripe.android.ui.core.elements.AfterpayClearpayHeaderElement import com.stripe.android.ui.core.elements.AuBecsDebitMandateElementUI import com.stripe.android.ui.core.elements.AuBecsDebitMandateTextElement +import com.stripe.android.ui.core.elements.CardDetailsSectionElement +import com.stripe.android.ui.core.elements.CardDetailsSectionElementUI import com.stripe.android.ui.core.elements.BsbElement import com.stripe.android.ui.core.elements.BsbElementUI import com.stripe.android.ui.core.elements.EmptyFormElement @@ -85,6 +87,9 @@ internal fun FormInternal( is AuBecsDebitMandateTextElement -> AuBecsDebitMandateElementUI(element) is AffirmHeaderElement -> AffirmElementUI() is MandateTextElement -> MandateTextUI(element) + is CardDetailsSectionElement -> CardDetailsSectionElementUI( + enabled, element.controller, hiddenIdentifiers + ) is BsbElement -> BsbElementUI(enabled, element, lastTextFieldIdentifier) is EmptyFormElement -> {} }