Skip to content

Commit

Permalink
Show the real embedded content! (#9796)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaynewstrom-stripe authored Dec 17, 2024
1 parent 256db17 commit 36dbd3b
Show file tree
Hide file tree
Showing 7 changed files with 454 additions and 41 deletions.
7 changes: 2 additions & 5 deletions paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@
<ID>LargeClass:SavedPaymentMethodMutatorTest.kt$SavedPaymentMethodMutatorTest</ID>
<ID>LargeClass:USBankAccountFormViewModelTest.kt$USBankAccountFormViewModelTest</ID>
<ID>LongMethod:AutocompleteScreen.kt$@Composable internal fun AutocompleteScreenUI(viewModel: AutocompleteViewModel)</ID>
<ID>LongMethod:ConfirmationOptionKtx.kt$internal fun PaymentSelection.toConfirmationOption( initializationMode: PaymentElementLoader.InitializationMode, configuration: CommonConfiguration, appearance: PaymentSheet.Appearance, linkConfiguration: LinkConfiguration?, ): ConfirmationHandler.Option?</ID>
<ID>LongMethod:CustomerSheetScreen.kt$@Composable internal fun SelectPaymentMethod( viewState: CustomerSheetViewState.SelectPaymentMethod, viewActionHandler: (CustomerSheetViewAction) -> Unit, paymentMethodNameProvider: (PaymentMethodCode?) -> ResolvableString, modifier: Modifier = Modifier, )</ID>
<ID>LongMethod:DefaultConfirmationHandlerTest.kt$DefaultConfirmationHandlerTest$@Test fun `On lifecycle destroyed, should unregister all launchers`()</ID>
<ID>LongMethod:DefaultConfirmationHandlerTest.kt$DefaultConfirmationHandlerTest$private fun test( someDefinitionAction: ConfirmationDefinition.Action&lt;SomeConfirmationDefinition.LauncherArgs> = ConfirmationDefinition.Action.Fail( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, errorType = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), someDefinitionResult: ConfirmationDefinition.Result = ConfirmationDefinition.Result.Failed( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, type = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), someOtherDefinitionAction: ConfirmationDefinition.Action&lt;SomeOtherConfirmationDefinition.LauncherArgs> = ConfirmationDefinition.Action.Fail( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, errorType = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), someOtherDefinitionResult: ConfirmationDefinition.Result = ConfirmationDefinition.Result.Failed( cause = IllegalStateException("Failed!"), message = R.string.stripe_something_went_wrong.resolvableString, type = ConfirmationHandler.Result.Failed.ErrorType.Internal, ), shouldRegister: Boolean = true, savedStateHandle: SavedStateHandle = SavedStateHandle(), dispatcher: CoroutineDispatcher = UnconfinedTestDispatcher(), scenarioTest: suspend Scenario.() -> Unit )</ID>
<ID>LongMethod:EditPaymentMethod.kt$@Composable internal fun EditPaymentMethodUi( viewState: EditPaymentMethodViewState, viewActionHandler: (action: EditPaymentMethodViewAction) -> Unit, modifier: Modifier = Modifier )</ID>
<ID>LongMethod:EmbeddedContentHelper.kt$DefaultEmbeddedContentHelper$private fun createInteractor( coroutineScope: CoroutineScope, paymentMethodMetadata: PaymentMethodMetadata, ): PaymentMethodVerticalLayoutInteractor</ID>
<ID>LongMethod:FormViewModelTest.kt$FormViewModelTest$@Test fun `Verify params are set when element address fields are complete`()</ID>
<ID>LongMethod:FormViewModelTest.kt$FormViewModelTest$@Test fun `Verify params are set when required address fields are complete`()</ID>
<ID>LongMethod:PaymentMethodMetadataTest.kt$PaymentMethodMetadataTest$@OptIn(ExperimentalCardBrandFilteringApi::class) @Test fun `should create metadata properly with elements session response, payment sheet config, and data specs`()</ID>
Expand All @@ -49,7 +48,6 @@
<ID>MagicNumber:PrimaryButton.kt$PrimaryButton$0.5f</ID>
<ID>MagicNumber:USBankAccountForm.kt$0.5f</ID>
<ID>MaxLineLength:CardDefinition.kt$internal</ID>
<ID>MaxLineLength:CommonConfiguration.kt$CommonConfiguration$"secret. See CustomerSession API: https://docs.stripe.com/api/customer_sessions/create"</ID>
<ID>MaxLineLength:CustomerRepositoryTest.kt$CustomerRepositoryTest$fun</ID>
<ID>MaxLineLength:CustomerSheetViewModelTest.kt$CustomerSheetViewModelTest$fun</ID>
<ID>MaxLineLength:CustomerSheetViewModelTest.kt$CustomerSheetViewModelTest$publishableKey = "pk_test_51HvTI7Lu5o3livep6t5AgBSkMvWoTtA0nyA7pVYDqpfLkRtWun7qZTYCOHCReprfLM464yaBeF72UFfB7cY9WG4a00ZnDtiC2C"</ID>
Expand All @@ -72,15 +70,14 @@
<ID>MaxLineLength:SupportedPaymentMethod.kt$SupportedPaymentMethod$/** This describes the image in the LPM selector. These can be found internally [here](https://www.figma.com/file/2b9r3CJbyeVAmKi1VHV2h9/Mobile-Payment-Element?node-id=1128%3A0) */</ID>
<ID>MaxLineLength:USBankAccountFormViewModelTest.kt$USBankAccountFormViewModelTest$fun</ID>
<ID>MaximumLineLength:CardDefinition.kt$internal</ID>
<ID>MaximumLineLength:CommonConfiguration.kt$CommonConfiguration$ </ID>
<ID>ThrowsCount:CommonConfiguration.kt$CommonConfiguration$fun validate()</ID>
<ID>TooManyFunctions:CustomerSheetEventReporter.kt$CustomerSheetEventReporter</ID>
<ID>TooManyFunctions:DefaultCustomerSheetEventReporter.kt$DefaultCustomerSheetEventReporter : CustomerSheetEventReporter</ID>
<ID>TooManyFunctions:DefaultEventReporter.kt$DefaultEventReporter : EventReporter</ID>
<ID>TooManyFunctions:DefaultFlowController.kt$DefaultFlowController : FlowController</ID>
<ID>TooManyFunctions:DelegateDrawable.kt$DelegateDrawable : Drawable</ID>
<ID>TooManyFunctions:EventReporter.kt$EventReporter</ID>
<ID>TooManyFunctions:PaymentMethodMetadata.kt$PaymentMethodMetadata : Parcelable</ID>
<ID>TooManyFunctions:SharedPaymentElementViewModel.kt$SharedPaymentElementViewModelModule</ID>
<ID>UnusedPrivateClass:PaymentOptionsViewModelTest.kt$PaymentOptionsViewModelTest$MyHostActivity : AppCompatActivity</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.paymentelement.embedded

import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
Expand All @@ -16,13 +17,14 @@ import com.stripe.android.uicore.strings.resolve
@Immutable
internal data class EmbeddedContent(
private val interactor: PaymentMethodVerticalLayoutInteractor,
private val mandate: ResolvableString? = null,
val mandate: ResolvableString? = null,
) {
@Composable
fun Content() {
Column(
modifier = Modifier
.padding(top = 8.dp)
.animateContentSize()
) {
EmbeddedVerticalList()
EmbeddedMandate()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package com.stripe.android.paymentelement.embedded

import androidx.lifecycle.SavedStateHandle
import com.stripe.android.cards.CardAccountRangeRepository
import com.stripe.android.core.injection.IOContext
import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.strings.orEmpty
import com.stripe.android.link.LinkConfigurationCoordinator
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.paymentsheet.CustomerStateHolder
import com.stripe.android.paymentsheet.FormHelper
import com.stripe.android.paymentsheet.LinkInlineHandler
import com.stripe.android.paymentsheet.NewOrExternalPaymentSelection
import com.stripe.android.paymentsheet.SavedPaymentMethodMutator
import com.stripe.android.paymentsheet.analytics.EventReporter
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.repositories.CustomerRepository
import com.stripe.android.paymentsheet.verticalmode.DefaultPaymentMethodVerticalLayoutInteractor
import com.stripe.android.paymentsheet.verticalmode.DefaultPaymentMethodVerticalLayoutInteractor.FormType
import com.stripe.android.paymentsheet.verticalmode.PaymentMethodIncentiveInteractor
import com.stripe.android.paymentsheet.verticalmode.PaymentMethodVerticalLayoutInteractor
import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility
import com.stripe.android.uicore.utils.stateFlowOf
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext

internal interface EmbeddedContentHelper {
val embeddedContent: StateFlow<EmbeddedContent?>

fun dataLoaded(paymentMethodMetadata: PaymentMethodMetadata)
}

internal fun interface EmbeddedContentHelperFactory {
fun create(coroutineScope: CoroutineScope): EmbeddedContentHelper
}

@AssistedFactory
internal interface DefaultEmbeddedContentHelperFactory : EmbeddedContentHelperFactory {
override fun create(coroutineScope: CoroutineScope): DefaultEmbeddedContentHelper
}

internal class DefaultEmbeddedContentHelper @AssistedInject constructor(
@Assisted private val coroutineScope: CoroutineScope,
private val cardAccountRangeRepositoryFactory: CardAccountRangeRepository.Factory,
private val savedStateHandle: SavedStateHandle,
private val eventReporter: EventReporter,
private val linkConfigurationCoordinator: LinkConfigurationCoordinator,
@IOContext private val workContext: CoroutineContext,
private val customerRepository: CustomerRepository,
private val selectionHolder: EmbeddedSelectionHolder,
) : EmbeddedContentHelper {

private val paymentMethodMetadata: StateFlow<PaymentMethodMetadata?> = savedStateHandle.getStateFlow(
key = PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT,
initialValue = null,
)
private val mandate: StateFlow<ResolvableString?> = savedStateHandle.getStateFlow(
key = MANDATE_KEY_EMBEDDED_CONTENT,
initialValue = null,
)
private val _embeddedContent = MutableStateFlow<EmbeddedContent?>(null)
override val embeddedContent: StateFlow<EmbeddedContent?> = _embeddedContent.asStateFlow()

init {
coroutineScope.launch {
paymentMethodMetadata.collect { paymentMethodMetadata ->
_embeddedContent.value = if (paymentMethodMetadata == null) {
null
} else {
EmbeddedContent(
interactor = createInteractor(
coroutineScope = coroutineScope,
paymentMethodMetadata = paymentMethodMetadata,
)
)
}
}
}
coroutineScope.launch {
mandate.collect { mandate ->
_embeddedContent.update { originalEmbeddedContent ->
originalEmbeddedContent?.copy(mandate = mandate)
}
}
}
}

override fun dataLoaded(paymentMethodMetadata: PaymentMethodMetadata) {
savedStateHandle[PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT] = paymentMethodMetadata
}

private fun createInteractor(
coroutineScope: CoroutineScope,
paymentMethodMetadata: PaymentMethodMetadata,
): PaymentMethodVerticalLayoutInteractor {
val paymentMethodIncentiveInteractor = PaymentMethodIncentiveInteractor(
incentive = paymentMethodMetadata.paymentMethodIncentive,
)
val customerStateHolder: CustomerStateHolder = CustomerStateHolder(
savedStateHandle = savedStateHandle,
selection = selectionHolder.selection,
)
val formHelper = createFormHelper(
coroutineScope = coroutineScope,
paymentMethodMetadata = paymentMethodMetadata,
)
val savedPaymentMethodMutator = createSavedPaymentMethodMutator(
coroutineScope = coroutineScope,
paymentMethodMetadata = paymentMethodMetadata,
customerStateHolder = customerStateHolder,
)

return DefaultPaymentMethodVerticalLayoutInteractor(
paymentMethodMetadata = paymentMethodMetadata,
processing = stateFlowOf(false),
selection = selectionHolder.selection,
paymentMethodIncentiveInteractor = paymentMethodIncentiveInteractor,
formTypeForCode = { code ->
if (formHelper.requiresFormScreen(code)) {
FormType.UserInteractionRequired
} else {
val mandate = formHelper.formElementsForCode(code).firstNotNullOfOrNull { it.mandateText }
if (mandate == null) {
FormType.Empty
} else {
FormType.MandateOnly(mandate)
}
}
},
onFormFieldValuesChanged = formHelper::onFormFieldValuesChanged,
transitionToManageScreen = {
},
transitionToManageOneSavedPaymentMethodScreen = {
},
transitionToFormScreen = {
},
paymentMethods = customerStateHolder.paymentMethods,
mostRecentlySelectedSavedPaymentMethod = customerStateHolder.mostRecentlySelectedSavedPaymentMethod,
providePaymentMethodName = savedPaymentMethodMutator.providePaymentMethodName,
canRemove = customerStateHolder.canRemove,
onEditPaymentMethod = {
},
onSelectSavedPaymentMethod = {
setSelection(PaymentSelection.Saved(it))
},
walletsState = stateFlowOf(null),
canShowWalletsInline = true,
onMandateTextUpdated = { updatedMandate ->
savedStateHandle[MANDATE_KEY_EMBEDDED_CONTENT] = updatedMandate
},
updateSelection = { updatedSelection ->
setSelection(updatedSelection)
},
isCurrentScreen = stateFlowOf(false),
reportPaymentMethodTypeSelected = eventReporter::onSelectPaymentMethod,
reportFormShown = eventReporter::onPaymentMethodFormShown,
onUpdatePaymentMethod = savedPaymentMethodMutator::updatePaymentMethod,
isLiveMode = paymentMethodMetadata.stripeIntent.isLiveMode,
)
}

private fun createSavedPaymentMethodMutator(
coroutineScope: CoroutineScope,
paymentMethodMetadata: PaymentMethodMetadata,
customerStateHolder: CustomerStateHolder,
): SavedPaymentMethodMutator {
return SavedPaymentMethodMutator(
eventReporter = eventReporter,
coroutineScope = coroutineScope,
workContext = workContext,
customerRepository = customerRepository,
selection = selectionHolder.selection,
providePaymentMethodName = { code ->
code?.let {
paymentMethodMetadata.supportedPaymentMethodForCode(code)
}?.displayName.orEmpty()
},
clearSelection = {
setSelection(null)
},
customerStateHolder = customerStateHolder,
onPaymentMethodRemoved = {
},
onModifyPaymentMethod = { _, _, _, _, _ ->
},
onUpdatePaymentMethod = { _, _, _, _ ->
},
navigationPop = {
},
isCbcEligible = {
paymentMethodMetadata.cbcEligibility is CardBrandChoiceEligibility.Eligible
},
isGooglePayReady = stateFlowOf(false),
isLinkEnabled = stateFlowOf(false),
isNotPaymentFlow = false,
)
}

private fun createFormHelper(
coroutineScope: CoroutineScope,
paymentMethodMetadata: PaymentMethodMetadata,
): FormHelper {
val linkInlineHandler = createLinkInlineHandler(coroutineScope)
return FormHelper(
cardAccountRangeRepositoryFactory = cardAccountRangeRepositoryFactory,
paymentMethodMetadata = paymentMethodMetadata,
newPaymentSelectionProvider = {
when (val currentSelection = selectionHolder.selection.value) {
is PaymentSelection.ExternalPaymentMethod -> {
NewOrExternalPaymentSelection.External(currentSelection)
}
is PaymentSelection.New -> {
NewOrExternalPaymentSelection.New(currentSelection)
}
else -> null
}
},
selectionUpdater = {
setSelection(it)
},
linkConfigurationCoordinator = linkConfigurationCoordinator,
onLinkInlineSignupStateChanged = linkInlineHandler::onStateUpdated,
)
}

private fun createLinkInlineHandler(
coroutineScope: CoroutineScope,
): LinkInlineHandler {
return LinkInlineHandler(
coroutineScope = coroutineScope,
payWithLink = { _, _, _ ->
},
selection = selectionHolder.selection,
updateLinkPrimaryButtonUiState = {
},
primaryButtonLabel = stateFlowOf(null),
shouldCompleteLinkFlowInline = false,
)
}

private fun setSelection(paymentSelection: PaymentSelection?) {
savedStateHandle[MANDATE_KEY_EMBEDDED_CONTENT] = null
selectionHolder.set(paymentSelection)
}

companion object {
const val PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT = "PAYMENT_METHOD_METADATA_KEY_EMBEDDED_CONTENT"
const val MANDATE_KEY_EMBEDDED_CONTENT = "MANDATE_KEY_EMBEDDED_CONTENT"
}
}
Loading

0 comments on commit 36dbd3b

Please sign in to comment.