diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/lpm/TestCardInCustomerSheet.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/lpm/TestCardInCustomerSheet.kt new file mode 100644 index 00000000000..d96c897a08e --- /dev/null +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/lpm/TestCardInCustomerSheet.kt @@ -0,0 +1,44 @@ +package com.stripe.android.lpm + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.stripe.android.BasePlaygroundTest +import com.stripe.android.paymentsheet.example.playground.settings.Country +import com.stripe.android.paymentsheet.example.playground.settings.CountrySettingsDefinition +import com.stripe.android.paymentsheet.example.playground.settings.CustomerSettingsDefinition +import com.stripe.android.paymentsheet.example.playground.settings.CustomerSheetPaymentMethodModeDefinition +import com.stripe.android.paymentsheet.example.playground.settings.CustomerType +import com.stripe.android.paymentsheet.example.playground.settings.PaymentMethodMode +import com.stripe.android.test.core.TestParameters +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +internal class TestCardInCustomerSheet : BasePlaygroundTest() { + @Test + fun testCard() { + testDriver.savePaymentMethodInCustomerSheet( + TestParameters.create(paymentMethodCode = "card").copyPlaygroundSettings { settings -> + settings[CustomerSettingsDefinition] = CustomerType.NEW + settings[CountrySettingsDefinition] = Country.US + settings[CustomerSheetPaymentMethodModeDefinition] = PaymentMethodMode.CreateAndAttach + }, + populateCustomLpmFields = { + populateCardDetails() + }, + ) + } + + @Test + fun testCardWithNonUsMerchant() { + testDriver.savePaymentMethodInCustomerSheet( + TestParameters.create(paymentMethodCode = "card").copyPlaygroundSettings { settings -> + settings[CustomerSettingsDefinition] = CustomerType.NEW + settings[CountrySettingsDefinition] = Country.FR + settings[CustomerSheetPaymentMethodModeDefinition] = PaymentMethodMode.CreateAndAttach + }, + populateCustomLpmFields = { + populateCardDetails() + }, + ) + } +} diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/FieldPopulator.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/FieldPopulator.kt index 7cf389adb45..746351600e2 100644 --- a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/FieldPopulator.kt +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/FieldPopulator.kt @@ -43,6 +43,8 @@ internal class FieldPopulator( } fun populateFields() { + selectors.formElement.waitFor() + populateCustomLpmFields() selectors.composeTestRule.waitForIdle() diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/PlaygroundTestDriver.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/PlaygroundTestDriver.kt index 75834d42ad6..8a72c1156a8 100644 --- a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/PlaygroundTestDriver.kt +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/PlaygroundTestDriver.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onAllNodesWithText @@ -195,7 +196,7 @@ internal class PlaygroundTestDriver( // and click the payment method. addPaymentMethodNode().performClick() } - selectors.paymentSelection.click() + clickPaymentSelection() val fieldPopulator = FieldPopulator( selectors, @@ -251,7 +252,7 @@ internal class PlaygroundTestDriver( // and click the payment method. addPaymentMethodNode().performClick() } - selectors.paymentSelection.click() + clickPaymentSelection() val fieldPopulator = FieldPopulator( selectors, @@ -313,6 +314,50 @@ internal class PlaygroundTestDriver( teardown() } + fun savePaymentMethodInCustomerSheet( + testParameters: TestParameters, + values: FieldPopulator.Values = FieldPopulator.Values(), + populateCustomLpmFields: FieldPopulator.() -> Unit = {}, + ): PlaygroundState? { + setup( + testParameters.copyPlaygroundSettings { settings -> + settings.updateConfigurationData { configurationData -> + configurationData.copy( + integrationType = PlaygroundConfigurationData.IntegrationType.CustomerSheet + ) + } + } + ) + + launchCustomerSheet() + + clickPaymentSelection() + + val fieldPopulator = FieldPopulator( + selectors, + testParameters, + populateCustomLpmFields, + {}, + values, + ) + fieldPopulator.populateFields() + + val result = playgroundState + + pressCustomerSheetSave() + + waitForManageSavedPaymentMethods() + + pressCustomerSheetConfirm() + + Espresso.onIdle() + composeTestRule.waitForIdle() + + teardown() + + return result + } + private fun pressMultiStepSelect() { selectors.multiStepSelect.click() waitForNotPlaygroundActivity() @@ -325,6 +370,20 @@ internal class PlaygroundTestDriver( } } + private fun pressCustomerSheetSave() { + Espresso.onIdle() + composeTestRule.waitForIdle() + + selectors.customerSheetSaveButton.click() + } + + private fun pressCustomerSheetConfirm() { + Espresso.onIdle() + composeTestRule.waitForIdle() + + selectors.customerSheetConfirmButton.click() + } + /** * This will open the payment sheet complete flow from the playground with a new or * guest user and complete the confirmation including any browser interactions. @@ -340,7 +399,7 @@ internal class PlaygroundTestDriver( setup(testParameters) launchComplete() - selectors.paymentSelection.click() + clickPaymentSelection() FieldPopulator( selectors, @@ -393,7 +452,7 @@ internal class PlaygroundTestDriver( waitForAddPaymentMethodNode() addPaymentMethodNode().performClick() - selectors.paymentSelection.click() + clickPaymentSelection() FieldPopulator( selectors, @@ -515,7 +574,7 @@ internal class PlaygroundTestDriver( private fun confirmExternalPaymentMethod( button: ComposeButton, ) { - selectors.paymentSelection.click() + clickPaymentSelection() pressBuy() @@ -576,7 +635,7 @@ internal class PlaygroundTestDriver( setup(testParameters) launchComplete() - selectors.paymentSelection.click() + clickPaymentSelection() FieldPopulator( selectors = selectors, @@ -632,7 +691,7 @@ internal class PlaygroundTestDriver( // and click the payment method. addPaymentMethodNode().performClick() } - selectors.paymentSelection.click() + clickPaymentSelection() FieldPopulator( selectors = selectors, @@ -717,7 +776,7 @@ internal class PlaygroundTestDriver( internal fun pressSelection() { composeTestRule.waitForIdle() - selectors.paymentSelection.click() + clickPaymentSelection() } internal fun scrollToBottom() { @@ -739,6 +798,14 @@ internal class PlaygroundTestDriver( .performClick() } + private fun clickPaymentSelection() { + selectors.formElement.waitFor() + selectors.paymentSelection.click() + + Espresso.onIdle() + composeTestRule.waitForIdle() + } + /** * Here we wait for an activity different from the playground to be in view. We * don't specifically look for PaymentSheetActivity or PaymentOptionsActivity because @@ -840,6 +907,17 @@ internal class PlaygroundTestDriver( } } + private fun launchCustomerSheet() { + selectors.reload.click() + Espresso.onIdle() + selectors.composeTestRule.waitForIdle() + + selectors.multiStepSelect.waitForEnabled() + selectors.multiStepSelect.click() + + waitForNotPlaygroundActivity() + } + private fun doAuthorization() { selectors.apply { val checkoutMode = @@ -1039,6 +1117,7 @@ internal class PlaygroundTestDriver( internal fun teardown() { application?.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks) + playgroundState = null currentActivity = null } @@ -1058,6 +1137,14 @@ internal class PlaygroundTestDriver( composeTestRule.waitUntilAtLeastOneExists(hasTestTag(ADD_PAYMENT_METHOD_NODE_TAG), 5000L) } + @OptIn(ExperimentalTestApi::class) + private fun waitForManageSavedPaymentMethods() { + composeTestRule.waitUntilAtLeastOneExists( + hasText("Manage your payment methods"), + DEFAULT_UI_TIMEOUT.inWholeMilliseconds + ) + } + private companion object { const val ADD_PAYMENT_METHOD_NODE_TAG = "${SAVED_PAYMENT_METHOD_CARD_TEST_TAG}_+ Add" const val FINANCIAL_CONNECTIONS_ACTIVITY = diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/FormElement.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/FormElement.kt new file mode 100644 index 00000000000..cce63dfd35a --- /dev/null +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/FormElement.kt @@ -0,0 +1,19 @@ +package com.stripe.android.test.core.ui + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.ComposeTestRule +import com.stripe.android.paymentsheet.ui.FORM_ELEMENT_TEST_TAG +import com.stripe.android.test.core.DEFAULT_UI_TIMEOUT + +class FormElement( + private val composeTestRule: ComposeTestRule +) { + @OptIn(ExperimentalTestApi::class) + fun waitFor() { + composeTestRule.waitUntilExactlyOneExists( + hasTestTag(FORM_ELEMENT_TEST_TAG), + DEFAULT_UI_TIMEOUT.inWholeMilliseconds + ) + } +} diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/PaymentSelection.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/PaymentSelection.kt index c5ef59b7ab1..f801571f82d 100644 --- a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/PaymentSelection.kt +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/PaymentSelection.kt @@ -1,44 +1,29 @@ package com.stripe.android.test.core.ui -import androidx.compose.ui.test.ComposeTimeoutException import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode -import androidx.test.platform.app.InstrumentationRegistry import com.stripe.android.paymentsheet.ui.TEST_TAG_LIST -import com.stripe.android.test.core.DEFAULT_UI_TIMEOUT class PaymentSelection(val composeTestRule: ComposeTestRule, val paymentMethodCode: String) { fun click() { - val resource = InstrumentationRegistry.getInstrumentation().targetContext.resources + if (composeTestRule.onAllNodes(hasTestTag(TEST_TAG_LIST)).fetchSemanticsNodes().isNotEmpty()) { + val paymentMethodMatcher = hasTestTag(TEST_TAG_LIST + paymentMethodCode) - try { - // If we don't find the node, it means that there's only one payment method available - // and we don't show the payment method carousel as a result. - composeTestRule.waitUntil(timeoutMillis = DEFAULT_UI_TIMEOUT.inWholeMilliseconds) { - composeTestRule - .onAllNodesWithTag(TEST_TAG_LIST) - .fetchSemanticsNodes().size == 1 - } - } catch (_: ComposeTimeoutException) { - return - } - - val paymentMethodMatcher = hasTestTag(TEST_TAG_LIST + paymentMethodCode) - composeTestRule.onNodeWithTag(TEST_TAG_LIST, true) - .performScrollToNode(paymentMethodMatcher) - composeTestRule.waitForIdle() - composeTestRule - .onNode(paymentMethodMatcher) - .assertIsDisplayed() - .assertIsEnabled() - .performClick() + composeTestRule.onNodeWithTag(TEST_TAG_LIST, true) + .performScrollToNode(paymentMethodMatcher) + composeTestRule.waitForIdle() + composeTestRule + .onNode(paymentMethodMatcher) + .assertIsDisplayed() + .assertIsEnabled() + .performClick() - composeTestRule.waitForIdle() + composeTestRule.waitForIdle() + } } } diff --git a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/Selectors.kt b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/Selectors.kt index 2b82a6e6a1f..718620ecd44 100644 --- a/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/Selectors.kt +++ b/paymentsheet-example/src/androidTest/java/com/stripe/android/test/core/ui/Selectors.kt @@ -16,6 +16,8 @@ import androidx.test.uiautomator.UiObject import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until import com.google.common.truth.Truth.assertThat +import com.stripe.android.customersheet.ui.CUSTOMER_SHEET_CONFIRM_BUTTON_TEST_TAG +import com.stripe.android.customersheet.ui.CUSTOMER_SHEET_SAVE_BUTTON_TEST_TAG import com.stripe.android.model.PaymentMethod.Type.Blik import com.stripe.android.model.PaymentMethod.Type.CashAppPay import com.stripe.android.paymentsheet.example.playground.RELOAD_TEST_TAG @@ -59,6 +61,8 @@ internal class Selectors( testParameters.paymentMethodCode ) + val formElement = FormElement(composeTestRule) + val mandateText = composeTestRule.onNodeWithTag(MANDATE_TEST_TAG) val buyButton = BuyButton( @@ -74,6 +78,16 @@ internal class Selectors( } ) + val customerSheetSaveButton = ComposeButton( + composeTestRule, + hasTestTag(CUSTOMER_SHEET_SAVE_BUTTON_TEST_TAG) + ) + + val customerSheetConfirmButton = ComposeButton( + composeTestRule, + hasTestTag(CUSTOMER_SHEET_CONFIRM_BUTTON_TEST_TAG) + ) + val externalPaymentMethodSucceedButton = ComposeButton( composeTestRule, hasTestTag(FawryActivity.COMPLETED_BUTTON_TEST_TAG) diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundActivity.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundActivity.kt index cbe796b3559..9897dcb637f 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundActivity.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundActivity.kt @@ -106,13 +106,16 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay val localPlaygroundSettings = playgroundSettings ?: return@setContent val playgroundState by viewModel.state.collectAsState() + val customerAdapter by viewModel.customerAdapter.collectAsState() val customerSheet = playgroundState?.asCustomerState()?.let { customerPlaygroundState -> - rememberCustomerSheet( - configuration = customerPlaygroundState.customerSheetConfiguration(), - customerAdapter = customerPlaygroundState.adapter, - callback = viewModel::onCustomerSheetCallback - ) + customerAdapter?.let { adapter -> + rememberCustomerSheet( + configuration = customerPlaygroundState.customerSheetConfiguration(), + customerAdapter = adapter, + callback = viewModel::onCustomerSheetCallback + ) + } } PlaygroundTheme( diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt index f4922d6ae43..a0492d77eeb 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt @@ -48,6 +48,7 @@ import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import java.io.IOException +@OptIn(ExperimentalCustomerSheetApi::class) internal class PaymentSheetPlaygroundViewModel( application: Application, launchUri: Uri?, @@ -61,6 +62,7 @@ internal class PaymentSheetPlaygroundViewModel( val state = MutableStateFlow(null) val flowControllerState = MutableStateFlow(null) val customerSheetState = MutableStateFlow(null) + val customerAdapter = MutableStateFlow(null) init { viewModelScope.launch(Dispatchers.IO) { @@ -76,6 +78,7 @@ internal class PaymentSheetPlaygroundViewModel( state.value = null flowControllerState.value = null customerSheetState.value = null + customerAdapter.value = null if (playgroundSettings.configurationData.value.integrationType.isPaymentFlow()) { prepareCheckout(playgroundSettings) @@ -149,23 +152,23 @@ internal class PaymentSheetPlaygroundViewModel( snapshot.saveToSharedPreferences(getApplication()) - state.value = PlaygroundState.Customer( - snapshot = snapshot, - adapter = CustomerAdapter.create( - context = getApplication(), - customerEphemeralKeyProvider = { - fetchEphemeralKey(snapshot) - }, - setupIntentClientSecretProvider = if ( - snapshot[CustomerSheetPaymentMethodModeDefinition] == PaymentMethodMode.SetupIntent - ) { - { customerId -> createSetupIntentClientSecret(snapshot, customerId) } - } else { - null - } - ) + val adapter = CustomerAdapter.create( + context = getApplication(), + customerEphemeralKeyProvider = { + fetchEphemeralKey(snapshot) + }, + setupIntentClientSecretProvider = if ( + snapshot[CustomerSheetPaymentMethodModeDefinition] == PaymentMethodMode.SetupIntent + ) { + { customerId -> createSetupIntentClientSecret(snapshot, customerId) } + } else { + null + } ) + customerSheetState.value = CustomerSheetState() + customerAdapter.value = adapter + state.value = PlaygroundState.Customer(snapshot = snapshot) } } diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PlaygroundState.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PlaygroundState.kt index 9a06ad7b868..9b0b1e10b48 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PlaygroundState.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PlaygroundState.kt @@ -1,7 +1,6 @@ package com.stripe.android.paymentsheet.example.playground import androidx.compose.runtime.Stable -import com.stripe.android.customersheet.CustomerAdapter import com.stripe.android.customersheet.CustomerSheet import com.stripe.android.customersheet.ExperimentalCustomerSheetApi import com.stripe.android.paymentsheet.PaymentSheet @@ -50,7 +49,6 @@ internal sealed interface PlaygroundState { @OptIn(ExperimentalCustomerSheetApi::class) data class Customer( private val snapshot: PlaygroundSettings.Snapshot, - val adapter: CustomerAdapter, ) : PlaygroundState { override val integrationType = snapshot.configurationData.integrationType override val countryCode = snapshot[CountrySettingsDefinition] diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/ui/CustomerSheetScreen.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/ui/CustomerSheetScreen.kt index 71ce6bcd692..0898fac8077 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/ui/CustomerSheetScreen.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/ui/CustomerSheetScreen.kt @@ -1,5 +1,6 @@ package com.stripe.android.customersheet.ui +import androidx.annotation.RestrictTo import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -293,8 +294,11 @@ private fun EditPaymentMethod( } } -internal const val CUSTOMER_SHEET_CONFIRM_BUTTON_TEST_TAG = "CustomerSheetConfirmButton" -internal const val CUSTOMER_SHEET_SAVE_BUTTON_TEST_TAG = "CustomerSheetSaveButton" +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +const val CUSTOMER_SHEET_CONFIRM_BUTTON_TEST_TAG = "CustomerSheetConfirmButton" + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +const val CUSTOMER_SHEET_SAVE_BUTTON_TEST_TAG = "CustomerSheetSaveButton" private class DefaultCardNumberCompletedEventReporter( private val viewActionHandler: (CustomerSheetViewAction) -> Unit diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentElement.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentElement.kt index c64c00a3769..e527436fb07 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentElement.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentElement.kt @@ -1,5 +1,6 @@ package com.stripe.android.paymentsheet.ui +import androidx.annotation.RestrictTo import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,6 +14,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -116,6 +118,7 @@ internal fun FormElement( Box( modifier = Modifier + .testTag(FORM_ELEMENT_TEST_TAG) .pointerInput("AddPaymentMethod") { awaitEachGesture { val gesture = awaitPointerEvent() @@ -184,3 +187,6 @@ internal fun LinkElement( } } } + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +const val FORM_ELEMENT_TEST_TAG = "FORM_ELEMENT_UI"