Skip to content

Commit

Permalink
Use ConfirmationHandler in embedded. (#9750)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaynewstrom-stripe authored Dec 5, 2024
1 parent 51f98a2 commit 54b60fe
Show file tree
Hide file tree
Showing 13 changed files with 403 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ internal class PaymentSheetPlaygroundViewModel(
fun onEmbeddedResult(success: Boolean) {
if (success) {
setPlaygroundState(null)
status.value = StatusMessage(SUCCESS_RESULT)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.stripe.android.paymentsheet.flowcontroller
package com.stripe.android.common.ui

import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultCaller
Expand All @@ -7,7 +7,8 @@ import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.ActivityResultRegistryOwner
import androidx.activity.result.contract.ActivityResultContract

internal class FlowControllerActivityResultCaller(
internal class PaymentElementActivityResultCaller(
private val key: String,
private val registryOwner: ActivityResultRegistryOwner
) : ActivityResultCaller {
override fun <I : Any?, O : Any?> registerForActivityResult(
Expand All @@ -26,10 +27,6 @@ internal class FlowControllerActivityResultCaller(
}

private fun <I : Any?, O : Any?> createKey(contract: ActivityResultContract<I, O>): String {
return "${FLOW_CONTROLLER_KEY}_${contract::class.java.name}"
}

private companion object {
const val FLOW_CONTROLLER_KEY = "FlowController"
return "${key}_${contract::class.java.name}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package com.stripe.android.paymentelement

import android.graphics.drawable.Drawable
import android.os.Parcelable
import androidx.activity.result.ActivityResultCaller
import androidx.annotation.RestrictTo
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.AnnotatedString
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import com.stripe.android.ExperimentalAllowsRemovalOfLastSavedPaymentMethodApi
Expand All @@ -16,6 +18,7 @@ import com.stripe.android.common.ui.DelegateDrawable
import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.SetupIntent
import com.stripe.android.paymentelement.embedded.EmbeddedConfirmationHelper
import com.stripe.android.paymentelement.embedded.SharedPaymentElementViewModel
import com.stripe.android.paymentsheet.ExternalPaymentMethodConfirmHandler
import com.stripe.android.paymentsheet.PaymentSheet
Expand All @@ -28,8 +31,8 @@ import kotlinx.parcelize.Parcelize
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@ExperimentalEmbeddedPaymentElementApi
class EmbeddedPaymentElement private constructor(
private val embeddedConfirmationHelper: EmbeddedConfirmationHelper,
private val sharedViewModel: SharedPaymentElementViewModel,
private val resultCallback: ResultCallback,
) {
/**
* Contains information about the customer's selected payment option.
Expand Down Expand Up @@ -67,7 +70,7 @@ class EmbeddedPaymentElement private constructor(
* Results will be delivered to the [ResultCallback] supplied during initialization of [EmbeddedPaymentElement].
*/
fun confirm() {
resultCallback.onResult(Result.Failed(NotImplementedError("Not yet implemented.")))
embeddedConfirmationHelper.confirm()
}

/** Configuration for [EmbeddedPaymentElement] **/
Expand Down Expand Up @@ -439,16 +442,25 @@ class EmbeddedPaymentElement private constructor(
internal companion object {
@ExperimentalEmbeddedPaymentElementApi
fun create(
statusBarColor: Int?,
activityResultCaller: ActivityResultCaller,
viewModelStoreOwner: ViewModelStoreOwner,
lifecycleOwner: LifecycleOwner,
resultCallback: ResultCallback,
): EmbeddedPaymentElement {
val sharedViewModel = ViewModelProvider(
owner = viewModelStoreOwner,
factory = SharedPaymentElementViewModel.Factory()
factory = SharedPaymentElementViewModel.Factory(statusBarColor)
)[SharedPaymentElementViewModel::class.java]
return EmbeddedPaymentElement(
embeddedConfirmationHelper = EmbeddedConfirmationHelper(
confirmationHandler = sharedViewModel.confirmationHandler,
resultCallback = resultCallback,
activityResultCaller = activityResultCaller,
lifecycleOwner = lifecycleOwner,
confirmationStateSupplier = { sharedViewModel.confirmationState }
),
sharedViewModel = sharedViewModel,
resultCallback = resultCallback,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.stripe.android.paymentelement

import androidx.activity.compose.LocalActivityResultRegistryOwner
import androidx.annotation.RestrictTo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import com.stripe.android.common.ui.PaymentElementActivityResultCaller
import com.stripe.android.common.ui.UpdateExternalPaymentMethodConfirmHandler
import com.stripe.android.common.ui.UpdateIntentConfirmationInterceptor
import com.stripe.android.paymentsheet.CreateIntentCallback
import com.stripe.android.paymentsheet.ExternalPaymentMethodConfirmHandler
import com.stripe.android.utils.rememberActivity

/**
* Creates an [EmbeddedPaymentElement] that is remembered across compositions.
Expand All @@ -35,10 +39,25 @@ fun rememberEmbeddedPaymentElement(
UpdateExternalPaymentMethodConfirmHandler(externalPaymentMethodConfirmHandler)
UpdateIntentConfirmationInterceptor(createIntentCallback)

val lifecycleOwner = LocalLifecycleOwner.current
val activityResultRegistryOwner = requireNotNull(LocalActivityResultRegistryOwner.current) {
"EmbeddedPaymentElement must have an ActivityResultRegistryOwner."
}

val onResult by rememberUpdatedState(newValue = resultCallback::onResult)

val activity = rememberActivity {
"EmbeddedPaymentElement must be created in the context of an Activity."
}

return remember {
EmbeddedPaymentElement.create(
statusBarColor = activity.window?.statusBarColor,
activityResultCaller = PaymentElementActivityResultCaller(
key = "Embedded",
registryOwner = activityResultRegistryOwner,
),
lifecycleOwner = lifecycleOwner,
viewModelStoreOwner = viewModelStoreOwner,
resultCallback = onResult,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ internal interface ConfirmationHandler {
* A factory for creating a [ConfirmationHandler] instance using a provided [CoroutineScope]. This scope is
* used to launch confirmation tasks.
*/
interface Factory {
fun interface Factory {
fun create(scope: CoroutineScope): ConfirmationHandler
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.stripe.android.paymentelement.embedded

import androidx.activity.result.ActivityResultCaller
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.stripe.android.common.model.asCommonConfiguration
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata
import com.stripe.android.paymentelement.EmbeddedPaymentElement
import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentelement.confirmation.toConfirmationOption
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.state.PaymentElementLoader
import kotlinx.coroutines.launch

@ExperimentalEmbeddedPaymentElementApi
internal class EmbeddedConfirmationHelper(
private val confirmationHandler: ConfirmationHandler,
private val resultCallback: EmbeddedPaymentElement.ResultCallback,
private val activityResultCaller: ActivityResultCaller,
private val lifecycleOwner: LifecycleOwner,
private val confirmationStateSupplier: () -> State?,
) {
init {
confirmationHandler.register(
activityResultCaller = activityResultCaller,
lifecycleOwner = lifecycleOwner
)

lifecycleOwner.lifecycleScope.launch {
confirmationHandler.state.collect { state ->
when (state) {
is ConfirmationHandler.State.Complete -> {
resultCallback.onResult(state.result.asEmbeddedResult())
}
is ConfirmationHandler.State.Confirming, ConfirmationHandler.State.Idle -> Unit
}
}
}
}

fun confirm() {
confirmationArgs()?.let { confirmationArgs ->
confirmationHandler.start(confirmationArgs)
} ?: run {
resultCallback.onResult(
EmbeddedPaymentElement.Result.Failed(IllegalStateException("Not in a state that's confirmable."))
)
}
}

private fun confirmationArgs(): ConfirmationHandler.Args? {
val loadedState = confirmationStateSupplier() ?: return null
val confirmationOption = loadedState.selection?.toConfirmationOption(
initializationMode = loadedState.initializationMode,
configuration = loadedState.configuration.asCommonConfiguration(),
appearance = loadedState.configuration.appearance,
) ?: return null

return ConfirmationHandler.Args(
intent = loadedState.paymentMethodMetadata.stripeIntent,
confirmationOption = confirmationOption,
)
}

data class State(
val paymentMethodMetadata: PaymentMethodMetadata,
val selection: PaymentSelection?,
val initializationMode: PaymentElementLoader.InitializationMode,
val configuration: EmbeddedPaymentElement.Configuration,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.stripe.android.paymentelement.embedded

import com.stripe.android.paymentelement.EmbeddedPaymentElement
import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler

@ExperimentalEmbeddedPaymentElementApi
internal fun ConfirmationHandler.Result.asEmbeddedResult(): EmbeddedPaymentElement.Result {
return when (this) {
is ConfirmationHandler.Result.Canceled -> {
EmbeddedPaymentElement.Result.Canceled()
}
is ConfirmationHandler.Result.Failed -> {
EmbeddedPaymentElement.Result.Failed(cause)
}
is ConfirmationHandler.Result.Succeeded -> {
EmbeddedPaymentElement.Result.Completed()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.stripe.android.PaymentConfiguration
import com.stripe.android.core.injection.CoreCommonModule
Expand All @@ -30,9 +31,13 @@ import com.stripe.android.paymentelement.EmbeddedPaymentElement
import com.stripe.android.paymentelement.EmbeddedPaymentElement.ConfigureResult
import com.stripe.android.paymentelement.EmbeddedPaymentElement.PaymentOptionDisplayData
import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi
import com.stripe.android.paymentelement.confirmation.ALLOWS_MANUAL_CONFIRMATION
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.paymentelement.confirmation.injection.PaymentElementConfirmationModule
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.payments.core.analytics.RealErrorReporter
import com.stripe.android.payments.core.injection.PRODUCT_USAGE
import com.stripe.android.payments.core.injection.STATUS_BAR_COLOR
import com.stripe.android.payments.core.injection.StripeRepositoryModule
import com.stripe.android.paymentsheet.BuildConfig
import com.stripe.android.paymentsheet.DefaultPrefsRepository
Expand Down Expand Up @@ -60,6 +65,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.plus
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
Expand All @@ -69,12 +75,19 @@ import kotlin.reflect.KClass

@OptIn(ExperimentalEmbeddedPaymentElementApi::class)
internal class SharedPaymentElementViewModel @Inject constructor(
confirmationHandlerFactory: ConfirmationHandler.Factory,
@IOContext ioContext: CoroutineContext,
private val configurationHandler: EmbeddedConfigurationHandler,
private val paymentOptionDisplayDataFactory: PaymentOptionDisplayDataFactory,
) : ViewModel() {
private val _paymentOption: MutableStateFlow<PaymentOptionDisplayData?> = MutableStateFlow(null)
val paymentOption: StateFlow<PaymentOptionDisplayData?> = _paymentOption.asStateFlow()

val confirmationHandler = confirmationHandlerFactory.create(viewModelScope + ioContext)

@Volatile
var confirmationState: EmbeddedConfirmationHelper.State? = null

suspend fun configure(
intentConfiguration: PaymentSheet.IntentConfiguration,
configuration: EmbeddedPaymentElement.Configuration,
Expand All @@ -84,6 +97,12 @@ internal class SharedPaymentElementViewModel @Inject constructor(
configuration = configuration,
).fold(
onSuccess = { state ->
confirmationState = EmbeddedConfirmationHelper.State(
paymentMethodMetadata = state.paymentMethodMetadata,
selection = state.paymentSelection,
initializationMode = PaymentElementLoader.InitializationMode.DeferredIntent(intentConfiguration),
configuration = configuration,
)
_paymentOption.value = paymentOptionDisplayDataFactory.create(state.paymentSelection)
ConfigureResult.Succeeded()
},
Expand All @@ -93,11 +112,12 @@ internal class SharedPaymentElementViewModel @Inject constructor(
)
}

class Factory : ViewModelProvider.Factory {
class Factory(private val statusBarColor: Int?) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: KClass<T>, extras: CreationExtras): T {
val component = DaggerSharedPaymentElementViewModelComponent.builder()
.savedStateHandle(extras.createSavedStateHandle())
.context(extras.requireApplication())
.statusBarColor(statusBarColor)
.build()
@Suppress("UNCHECKED_CAST")
return component.viewModel as T
Expand All @@ -113,6 +133,7 @@ internal class SharedPaymentElementViewModel @Inject constructor(
GooglePayLauncherModule::class,
CoreCommonModule::class,
StripeRepositoryModule::class,
PaymentElementConfirmationModule::class,
]
)
internal interface SharedPaymentElementViewModelComponent {
Expand All @@ -126,6 +147,9 @@ internal interface SharedPaymentElementViewModelComponent {
@BindsInstance
fun context(context: Context): Builder

@BindsInstance
fun statusBarColor(@Named(STATUS_BAR_COLOR) statusBarColor: Int?): Builder

fun build(): SharedPaymentElementViewModelComponent
}
}
Expand Down Expand Up @@ -171,6 +195,11 @@ internal interface SharedPaymentElementViewModelModule {

@Suppress("TooManyFunctions")
companion object {
@Provides
@Singleton
@Named(ALLOWS_MANUAL_CONFIRMATION)
fun provideAllowsManualConfirmation() = true

@Provides
fun provideEventReporterMode(): EventReporter.Mode {
return EventReporter.Mode.Embedded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.activity.result.ActivityResultRegistryOwner
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStoreOwner
import com.stripe.android.common.ui.PaymentElementActivityResultCaller
import com.stripe.android.paymentsheet.PaymentOptionCallback
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.PaymentSheetResultCallback
Expand Down Expand Up @@ -48,7 +49,10 @@ internal class FlowControllerFactory(
DefaultFlowController.getInstance(
viewModelStoreOwner = viewModelStoreOwner,
lifecycleOwner = lifecycleOwner,
activityResultCaller = FlowControllerActivityResultCaller(activityResultRegistryOwner),
activityResultCaller = PaymentElementActivityResultCaller(
key = "FlowController",
registryOwner = activityResultRegistryOwner,
),
statusBarColor = statusBarColor,
paymentOptionCallback = paymentOptionCallback,
paymentResultCallback = paymentResultCallback,
Expand Down
Loading

0 comments on commit 54b60fe

Please sign in to comment.