From fff3b37be70e7b5889b07ac6cedb1e0f0124583b Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Wed, 15 Jan 2025 15:15:54 -0500 Subject: [PATCH 01/13] Add EmbeddedActivityLauncher --- paymentsheet/src/main/AndroidManifest.xml | 3 + .../paymentelement/EmbeddedPaymentElement.kt | 7 ++ .../DefaultEmbeddedActivityLauncher.kt | 41 +++++++ .../embedded/EmbeddedContentHelper.kt | 12 ++ .../paymentelement/embedded/FormActivity.kt | 115 ++++++++++++++++++ .../embedded/SharedPaymentElementViewModel.kt | 4 + 6 files changed, 182 insertions(+) create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt diff --git a/paymentsheet/src/main/AndroidManifest.xml b/paymentsheet/src/main/AndroidManifest.xml index c23e33c7ee1..7587190cac3 100644 --- a/paymentsheet/src/main/AndroidManifest.xml +++ b/paymentsheet/src/main/AndroidManifest.xml @@ -37,6 +37,9 @@ + Unit) +} + +internal class DefaultEmbeddedActivityLauncher( + private val activityResultCaller: ActivityResultCaller, + private val lifecycleOwner: LifecycleOwner, +) : EmbeddedActivityLauncher { + + private var formActivityLauncher: ActivityResultLauncher? = null + + init { + lifecycleOwner.lifecycle.addObserver( + object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + formActivityLauncher?.unregister() + super.onDestroy(owner) + } + } + ) + } + + override fun launchForm(args: FormContract.Args, onSelected: (paymentSelection: PaymentSelection) -> Unit) { + if (formActivityLauncher == null ) { + formActivityLauncher = activityResultCaller.registerForActivityResult( + FormContract() + ) { result -> + if (result is FormResult.Complete) onSelected(result.selection) + } + } + formActivityLauncher?.launch(args) + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt index 799361f59c5..cc91fd02844 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt @@ -46,6 +46,8 @@ internal interface EmbeddedContentHelper { rowStyle: Embedded.RowStyle, embeddedViewDisplaysMandateText: Boolean, ) + + fun setEmbeddedActivityLauncher(embeddedActivityLauncher: EmbeddedActivityLauncher) } internal fun interface EmbeddedContentHelperFactory { @@ -80,6 +82,8 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( private val _embeddedContent = MutableStateFlow(null) override val embeddedContent: StateFlow = _embeddedContent.asStateFlow() + private var embeddedActivityLauncher: EmbeddedActivityLauncher? = null + init { coroutineScope.launch { state.collect { state -> @@ -119,6 +123,10 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( ) } + override fun setEmbeddedActivityLauncher(embeddedActivityLauncher: EmbeddedActivityLauncher) { + this.embeddedActivityLauncher = embeddedActivityLauncher + } + private fun createInteractor( coroutineScope: CoroutineScope, paymentMethodMetadata: PaymentMethodMetadata, @@ -161,6 +169,10 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( transitionToManageScreen = { }, transitionToFormScreen = { + embeddedActivityLauncher?.launchForm( + FormContract.Args(it, state.value?.paymentMethodMetadata), + ::setSelection + ) }, paymentMethods = customerStateHolder.paymentMethods, mostRecentlySelectedSavedPaymentMethod = customerStateHolder.mostRecentlySelectedSavedPaymentMethod, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt new file mode 100644 index 00000000000..dfe8a17633c --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt @@ -0,0 +1,115 @@ +package com.stripe.android.paymentelement.embedded + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContract +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Text +import androidx.core.os.BundleCompat +import com.stripe.android.common.ui.ElementsBottomSheetLayout +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import com.stripe.android.paymentsheet.model.PaymentSelection +import com.stripe.android.uicore.StripeTheme +import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState +import com.stripe.android.uicore.utils.fadeOut +import com.stripe.android.view.ActivityStarter +import kotlinx.parcelize.Parcelize + +class FormActivity : AppCompatActivity() { + private val args: FormContract.Args by lazy { + FormContract.Args.fromIntent(intent) ?: throw IllegalStateException( + "Args required" + ) + } + + @OptIn(ExperimentalMaterialApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + StripeTheme { + val bottomSheetState = rememberStripeBottomSheetState() + ElementsBottomSheetLayout( + state = bottomSheetState, + onDismissed = { + setResult( + Activity.RESULT_OK, + FormResult.toIntent(intent, FormResult.Cancelled) + ) + finish() + } + ) { + Text(args.selectedPaymentMethodCode) + } + } + } + } + + override fun finish() { + super.finish() + fadeOut() + } + +} + +internal sealed interface FormResult : Parcelable { + + @Parcelize + data class Complete(val selection: PaymentSelection) : FormResult + + @Parcelize + object Cancelled : FormResult + + companion object { + internal const val EXTRA_RESULT = ActivityStarter.Result.EXTRA + + fun toIntent(intent: Intent, result: FormResult): Intent { + return intent.putExtra(EXTRA_RESULT, result) + } + + fun fromIntent(intent: Intent?): FormResult { + val result = intent?.extras?.let { bundle -> + BundleCompat.getParcelable(bundle, EXTRA_RESULT, FormResult::class.java) + } + + return result ?: Cancelled + } + } +} + + + +internal class FormContract : ActivityResultContract() { + override fun createIntent(context: Context, input: Args): Intent { + return Intent(context, FormActivity::class.java) + .putExtra(EXTRA_ARGS, input) + } + + override fun parseResult(resultCode: Int, intent: Intent?): FormResult { + return FormResult.fromIntent(intent) + } + + @Parcelize + internal data class Args( + val selectedPaymentMethodCode: String, + val paymentMethodMetadata: PaymentMethodMetadata?, + ) : Parcelable { + companion object { + fun fromIntent(intent: Intent): Args? { + return intent.extras?.let { bundle -> + BundleCompat.getParcelable(bundle, EXTRA_ARGS, Args::class.java) + } + } + } + } + + internal companion object { + internal const val EXTRA_ARGS: String = "extra_activity_args" + } +} + diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt index 6c324519681..91f28687ef8 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt @@ -110,6 +110,10 @@ internal class SharedPaymentElementViewModel @Inject constructor( } } + fun setEmbeddedActivityLauncher(embeddedActivityLauncher: EmbeddedActivityLauncher) { + embeddedContentHelper.setEmbeddedActivityLauncher(embeddedActivityLauncher) + } + suspend fun configure( intentConfiguration: PaymentSheet.IntentConfiguration, configuration: EmbeddedPaymentElement.Configuration, From 69d06b31f1ffec67ec4426d560f340c79b2e5240 Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Wed, 15 Jan 2025 17:47:28 -0500 Subject: [PATCH 02/13] Add EmbeddedActivityLauncherFactory --- .../paymentelement/EmbeddedPaymentElement.kt | 10 ++-- .../DefaultEmbeddedActivityLauncher.kt | 48 ++++++++++++------- .../embedded/EmbeddedContentHelper.kt | 15 +++--- .../paymentelement/embedded/FormActivity.kt | 2 +- .../embedded/SharedPaymentElementViewModel.kt | 20 ++++++-- 5 files changed, 64 insertions(+), 31 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt index 3e1a01527cb..b705172178f 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt @@ -16,11 +16,13 @@ import com.stripe.android.ExperimentalAllowsRemovalOfLastSavedPaymentMethodApi import com.stripe.android.ExperimentalCardBrandFilteringApi import com.stripe.android.common.configuration.ConfigurationDefaults import com.stripe.android.common.ui.DelegateDrawable +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata import com.stripe.android.model.CardBrand import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent import com.stripe.android.paymentelement.confirmation.intent.IntentConfirmationInterceptor import com.stripe.android.paymentelement.embedded.DefaultEmbeddedActivityLauncher +import com.stripe.android.paymentelement.embedded.EmbeddedActivityLauncher import com.stripe.android.paymentelement.embedded.EmbeddedConfirmationHelper import com.stripe.android.paymentelement.embedded.SharedPaymentElementViewModel import com.stripe.android.paymentsheet.CreateIntentCallback @@ -39,11 +41,12 @@ import kotlinx.parcelize.Parcelize class EmbeddedPaymentElement private constructor( private val embeddedConfirmationHelper: EmbeddedConfirmationHelper, private val sharedViewModel: SharedPaymentElementViewModel, - private val embeddedActivityLauncher: DefaultEmbeddedActivityLauncher + private val activityResultCaller: ActivityResultCaller, + private val lifecycleOwner: LifecycleOwner ) { init { - sharedViewModel.setEmbeddedActivityLauncher(embeddedActivityLauncher) + sharedViewModel.initEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner) } /** * Contains information about the customer's selected payment option. @@ -509,7 +512,8 @@ class EmbeddedPaymentElement private constructor( confirmationStateSupplier = { sharedViewModel.confirmationStateHolder.state }, ), sharedViewModel = sharedViewModel, - embeddedActivityLauncher = DefaultEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner) + activityResultCaller = activityResultCaller, + lifecycleOwner = lifecycleOwner ) } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt index f89bfa71873..577b69c2ccd 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt @@ -4,38 +4,54 @@ import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import com.stripe.android.paymentsheet.model.PaymentSelection +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject internal interface EmbeddedActivityLauncher { - fun launchForm(args: FormContract.Args, onSelected: (paymentSelection: PaymentSelection) -> Unit) + fun launchForm(code: String, paymentMethodMetadata: PaymentMethodMetadata?) } -internal class DefaultEmbeddedActivityLauncher( - private val activityResultCaller: ActivityResultCaller, - private val lifecycleOwner: LifecycleOwner, +internal class DefaultEmbeddedActivityLauncher @AssistedInject constructor( + @Assisted private val activityResultCaller: ActivityResultCaller, + @Assisted private val lifecycleOwner: LifecycleOwner, + private val selectionHolder: EmbeddedSelectionHolder ) : EmbeddedActivityLauncher { - private var formActivityLauncher: ActivityResultLauncher? = null + private var formActivityLauncher: ActivityResultLauncher = + activityResultCaller.registerForActivityResult( + FormContract() + ) { result -> + if (result is FormResult.Complete){ + selectionHolder.set(result.selection) + } + } init { lifecycleOwner.lifecycle.addObserver( object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { - formActivityLauncher?.unregister() + formActivityLauncher.unregister() super.onDestroy(owner) } } ) } - override fun launchForm(args: FormContract.Args, onSelected: (paymentSelection: PaymentSelection) -> Unit) { - if (formActivityLauncher == null ) { - formActivityLauncher = activityResultCaller.registerForActivityResult( - FormContract() - ) { result -> - if (result is FormResult.Complete) onSelected(result.selection) - } - } - formActivityLauncher?.launch(args) + override fun launchForm(code: String, paymentMethodMetadata: PaymentMethodMetadata?) { + formActivityLauncher.launch(FormContract.Args(code, paymentMethodMetadata)) } } + +internal fun interface EmbeddedActivityLauncherFactory { + fun create(activityResultCaller: ActivityResultCaller, lifecycleOwner: LifecycleOwner): EmbeddedActivityLauncher +} + +@AssistedFactory +internal interface DefaultEmbeddedActivityLauncherFactory : EmbeddedActivityLauncherFactory { + override fun create( + activityResultCaller: ActivityResultCaller, + lifecycleOwner: LifecycleOwner + ): DefaultEmbeddedActivityLauncher +} \ No newline at end of file diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt index cc91fd02844..491e77d4498 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt @@ -47,7 +47,7 @@ internal interface EmbeddedContentHelper { embeddedViewDisplaysMandateText: Boolean, ) - fun setEmbeddedActivityLauncher(embeddedActivityLauncher: EmbeddedActivityLauncher) + fun setLaunchForm(launchForm: (code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit) } internal fun interface EmbeddedContentHelperFactory { @@ -82,7 +82,7 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( private val _embeddedContent = MutableStateFlow(null) override val embeddedContent: StateFlow = _embeddedContent.asStateFlow() - private var embeddedActivityLauncher: EmbeddedActivityLauncher? = null + private var launchForm: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? = null init { coroutineScope.launch { @@ -123,8 +123,10 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( ) } - override fun setEmbeddedActivityLauncher(embeddedActivityLauncher: EmbeddedActivityLauncher) { - this.embeddedActivityLauncher = embeddedActivityLauncher + override fun setLaunchForm( + launchForm: (code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit + ) { + this.launchForm = launchForm } private fun createInteractor( @@ -169,10 +171,7 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( transitionToManageScreen = { }, transitionToFormScreen = { - embeddedActivityLauncher?.launchForm( - FormContract.Args(it, state.value?.paymentMethodMetadata), - ::setSelection - ) + launchForm?.invoke(it, state.value?.paymentMethodMetadata) }, paymentMethods = customerStateHolder.paymentMethods, mostRecentlySelectedSavedPaymentMethod = customerStateHolder.mostRecentlySelectedSavedPaymentMethod, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt index dfe8a17633c..813a6205200 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt @@ -20,7 +20,7 @@ import com.stripe.android.uicore.utils.fadeOut import com.stripe.android.view.ActivityStarter import kotlinx.parcelize.Parcelize -class FormActivity : AppCompatActivity() { +internal class FormActivity : AppCompatActivity() { private val args: FormContract.Args by lazy { FormContract.Args.fromIntent(intent) ?: throw IllegalStateException( "Args required" diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt index 91f28687ef8..fdc7a96b37a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt @@ -2,6 +2,8 @@ package com.stripe.android.paymentelement.embedded import android.content.Context import android.content.res.Resources +import androidx.activity.result.ActivityResultCaller +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -29,6 +31,7 @@ import com.stripe.android.link.LinkConfigurationCoordinator import com.stripe.android.link.RealLinkConfigurationCoordinator import com.stripe.android.link.injection.LinkAnalyticsComponent import com.stripe.android.link.injection.LinkComponent +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata import com.stripe.android.paymentelement.EmbeddedPaymentElement import com.stripe.android.paymentelement.EmbeddedPaymentElement.ConfigureResult import com.stripe.android.paymentelement.EmbeddedPaymentElement.PaymentOptionDisplayData @@ -82,8 +85,9 @@ internal class SharedPaymentElementViewModel @Inject constructor( @IOContext ioContext: CoroutineContext, private val configurationHandler: EmbeddedConfigurationHandler, private val paymentOptionDisplayDataFactory: PaymentOptionDisplayDataFactory, - private val selectionHolder: EmbeddedSelectionHolder, + val selectionHolder: EmbeddedSelectionHolder, embeddedContentHelperFactory: EmbeddedContentHelperFactory, + private val embeddedActivityLauncherFactory: EmbeddedActivityLauncherFactory ) : ViewModel() { private val _paymentOption: MutableStateFlow = MutableStateFlow(null) val paymentOption: StateFlow = _paymentOption.asStateFlow() @@ -110,8 +114,13 @@ internal class SharedPaymentElementViewModel @Inject constructor( } } - fun setEmbeddedActivityLauncher(embeddedActivityLauncher: EmbeddedActivityLauncher) { - embeddedContentHelper.setEmbeddedActivityLauncher(embeddedActivityLauncher) + fun initEmbeddedActivityLauncher(activityResultCaller: ActivityResultCaller, lifecycleOwner: LifecycleOwner) { + val launcher = embeddedActivityLauncherFactory.create(activityResultCaller, lifecycleOwner) + setLaunchForm(launcher::launchForm) + } + + private fun setLaunchForm(launch: (code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit) { + embeddedContentHelper.setLaunchForm(launch) } suspend fun configure( @@ -212,6 +221,11 @@ internal interface SharedPaymentElementViewModelModule { handler: DefaultEmbeddedConfigurationHandler ): EmbeddedConfigurationHandler + @Binds + fun bindsEmbeddedActivityLauncherFactory( + factory: DefaultEmbeddedActivityLauncherFactory + ): EmbeddedActivityLauncherFactory + @Singleton @Binds fun bindsEventReporter(eventReporter: DefaultEventReporter): EventReporter From 513fcf6f54f04941b66741e4af5eaa2faf157c24 Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Wed, 15 Jan 2025 17:55:33 -0500 Subject: [PATCH 03/13] Remove factory --- .../DefaultEmbeddedActivityLauncher.kt | 21 +++---------------- .../embedded/SharedPaymentElementViewModel.kt | 10 ++------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt index 577b69c2ccd..5f6e4e551ed 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt @@ -5,17 +5,14 @@ import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject internal interface EmbeddedActivityLauncher { fun launchForm(code: String, paymentMethodMetadata: PaymentMethodMetadata?) } -internal class DefaultEmbeddedActivityLauncher @AssistedInject constructor( - @Assisted private val activityResultCaller: ActivityResultCaller, - @Assisted private val lifecycleOwner: LifecycleOwner, +internal class DefaultEmbeddedActivityLauncher( + private val activityResultCaller: ActivityResultCaller, + private val lifecycleOwner: LifecycleOwner, private val selectionHolder: EmbeddedSelectionHolder ) : EmbeddedActivityLauncher { @@ -43,15 +40,3 @@ internal class DefaultEmbeddedActivityLauncher @AssistedInject constructor( formActivityLauncher.launch(FormContract.Args(code, paymentMethodMetadata)) } } - -internal fun interface EmbeddedActivityLauncherFactory { - fun create(activityResultCaller: ActivityResultCaller, lifecycleOwner: LifecycleOwner): EmbeddedActivityLauncher -} - -@AssistedFactory -internal interface DefaultEmbeddedActivityLauncherFactory : EmbeddedActivityLauncherFactory { - override fun create( - activityResultCaller: ActivityResultCaller, - lifecycleOwner: LifecycleOwner - ): DefaultEmbeddedActivityLauncher -} \ No newline at end of file diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt index fdc7a96b37a..1cadff93a04 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt @@ -85,9 +85,8 @@ internal class SharedPaymentElementViewModel @Inject constructor( @IOContext ioContext: CoroutineContext, private val configurationHandler: EmbeddedConfigurationHandler, private val paymentOptionDisplayDataFactory: PaymentOptionDisplayDataFactory, - val selectionHolder: EmbeddedSelectionHolder, + private val selectionHolder: EmbeddedSelectionHolder, embeddedContentHelperFactory: EmbeddedContentHelperFactory, - private val embeddedActivityLauncherFactory: EmbeddedActivityLauncherFactory ) : ViewModel() { private val _paymentOption: MutableStateFlow = MutableStateFlow(null) val paymentOption: StateFlow = _paymentOption.asStateFlow() @@ -115,7 +114,7 @@ internal class SharedPaymentElementViewModel @Inject constructor( } fun initEmbeddedActivityLauncher(activityResultCaller: ActivityResultCaller, lifecycleOwner: LifecycleOwner) { - val launcher = embeddedActivityLauncherFactory.create(activityResultCaller, lifecycleOwner) + val launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner, selectionHolder) setLaunchForm(launcher::launchForm) } @@ -221,11 +220,6 @@ internal interface SharedPaymentElementViewModelModule { handler: DefaultEmbeddedConfigurationHandler ): EmbeddedConfigurationHandler - @Binds - fun bindsEmbeddedActivityLauncherFactory( - factory: DefaultEmbeddedActivityLauncherFactory - ): EmbeddedActivityLauncherFactory - @Singleton @Binds fun bindsEventReporter(eventReporter: DefaultEventReporter): EventReporter From 09981de5e65f81c4ef108e0b8c73a3e5c735914c Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Wed, 15 Jan 2025 18:25:29 -0500 Subject: [PATCH 04/13] Refactor --- .../paymentelement/EmbeddedPaymentElement.kt | 4 +--- .../embedded/DefaultEmbeddedActivityLauncher.kt | 14 ++++++++------ .../embedded/EmbeddedContentHelper.kt | 12 ++++++------ .../paymentelement/embedded/FormActivity.kt | 14 +++++--------- .../embedded/SharedPaymentElementViewModel.kt | 6 +++--- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt index b705172178f..9a895adffe2 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt @@ -16,13 +16,10 @@ import com.stripe.android.ExperimentalAllowsRemovalOfLastSavedPaymentMethodApi import com.stripe.android.ExperimentalCardBrandFilteringApi import com.stripe.android.common.configuration.ConfigurationDefaults import com.stripe.android.common.ui.DelegateDrawable -import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata import com.stripe.android.model.CardBrand import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent import com.stripe.android.paymentelement.confirmation.intent.IntentConfirmationInterceptor -import com.stripe.android.paymentelement.embedded.DefaultEmbeddedActivityLauncher -import com.stripe.android.paymentelement.embedded.EmbeddedActivityLauncher import com.stripe.android.paymentelement.embedded.EmbeddedConfirmationHelper import com.stripe.android.paymentelement.embedded.SharedPaymentElementViewModel import com.stripe.android.paymentsheet.CreateIntentCallback @@ -48,6 +45,7 @@ class EmbeddedPaymentElement private constructor( init { sharedViewModel.initEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner) } + /** * Contains information about the customer's selected payment option. * Use this to display the payment option in your own UI. diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt index 5f6e4e551ed..054c3d97919 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.LifecycleOwner import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata internal interface EmbeddedActivityLauncher { - fun launchForm(code: String, paymentMethodMetadata: PaymentMethodMetadata?) + var formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit)? } internal class DefaultEmbeddedActivityLauncher( @@ -20,23 +20,25 @@ internal class DefaultEmbeddedActivityLauncher( activityResultCaller.registerForActivityResult( FormContract() ) { result -> - if (result is FormResult.Complete){ + if (result is FormResult.Complete) { selectionHolder.set(result.selection) } } + override var formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit)? = + { code, metadata -> + formActivityLauncher.launch(FormContract.Args(code, metadata)) + } + init { lifecycleOwner.lifecycle.addObserver( object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { formActivityLauncher.unregister() + formLauncher = null super.onDestroy(owner) } } ) } - - override fun launchForm(code: String, paymentMethodMetadata: PaymentMethodMetadata?) { - formActivityLauncher.launch(FormContract.Args(code, paymentMethodMetadata)) - } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt index 491e77d4498..b155eee9a5a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt @@ -47,7 +47,7 @@ internal interface EmbeddedContentHelper { embeddedViewDisplaysMandateText: Boolean, ) - fun setLaunchForm(launchForm: (code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit) + fun setFormLauncher(formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)?) } internal fun interface EmbeddedContentHelperFactory { @@ -82,7 +82,7 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( private val _embeddedContent = MutableStateFlow(null) override val embeddedContent: StateFlow = _embeddedContent.asStateFlow() - private var launchForm: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? = null + private var formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? = null init { coroutineScope.launch { @@ -123,10 +123,10 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( ) } - override fun setLaunchForm( - launchForm: (code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit + override fun setFormLauncher( + formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? ) { - this.launchForm = launchForm + this.formLauncher = formLauncher } private fun createInteractor( @@ -171,7 +171,7 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( transitionToManageScreen = { }, transitionToFormScreen = { - launchForm?.invoke(it, state.value?.paymentMethodMetadata) + formLauncher?.invoke(it, state.value?.paymentMethodMetadata) }, paymentMethods = customerStateHolder.paymentMethods, mostRecentlySelectedSavedPaymentMethod = customerStateHolder.mostRecentlySelectedSavedPaymentMethod, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt index 813a6205200..8229f253342 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt @@ -21,16 +21,16 @@ import com.stripe.android.view.ActivityStarter import kotlinx.parcelize.Parcelize internal class FormActivity : AppCompatActivity() { - private val args: FormContract.Args by lazy { - FormContract.Args.fromIntent(intent) ?: throw IllegalStateException( - "Args required" - ) + private val args: FormContract.Args? by lazy { + FormContract.Args.fromIntent(intent) } @OptIn(ExperimentalMaterialApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (args == null) finish() + setContent { StripeTheme { val bottomSheetState = rememberStripeBottomSheetState() @@ -44,7 +44,7 @@ internal class FormActivity : AppCompatActivity() { finish() } ) { - Text(args.selectedPaymentMethodCode) + Text(args?.selectedPaymentMethodCode ?: "Whoops") } } } @@ -54,7 +54,6 @@ internal class FormActivity : AppCompatActivity() { super.finish() fadeOut() } - } internal sealed interface FormResult : Parcelable { @@ -82,8 +81,6 @@ internal sealed interface FormResult : Parcelable { } } - - internal class FormContract : ActivityResultContract() { override fun createIntent(context: Context, input: Args): Intent { return Intent(context, FormActivity::class.java) @@ -112,4 +109,3 @@ internal class FormContract : ActivityResultContract Unit) { - embeddedContentHelper.setLaunchForm(launch) + private fun setFormLauncher(launch: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit)?) { + embeddedContentHelper.setFormLauncher(launch) } suspend fun configure( From c8c28df1efd7574ca6b8277db4dfc3e10c34886c Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Thu, 16 Jan 2025 19:29:02 -0500 Subject: [PATCH 05/13] Add tests --- .../DefaultEmbeddedActivityLauncher.kt | 4 +- .../paymentelement/embedded/FormActivity.kt | 71 ++------------- .../paymentelement/embedded/FormContract.kt | 65 ++++++++++++++ .../embedded/EmbeddedActivityLauncherTest.kt | 86 +++++++++++++++++++ .../embedded/FakeEmbeddedContentHelper.kt | 6 ++ .../embedded/FormActivityTest.kt | 44 ++++++++++ 6 files changed, 211 insertions(+), 65 deletions(-) create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt create mode 100644 paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt create mode 100644 paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt index 054c3d97919..264c566eb8c 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt @@ -17,9 +17,7 @@ internal class DefaultEmbeddedActivityLauncher( ) : EmbeddedActivityLauncher { private var formActivityLauncher: ActivityResultLauncher = - activityResultCaller.registerForActivityResult( - FormContract() - ) { result -> + activityResultCaller.registerForActivityResult(FormContract()) { result -> if (result is FormResult.Complete) { selectionHolder.set(result.selection) } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt index 8229f253342..3a72a7c27bd 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt @@ -1,24 +1,15 @@ package com.stripe.android.paymentelement.embedded import android.app.Activity -import android.content.Context -import android.content.Intent import android.os.Bundle -import android.os.Parcelable import androidx.activity.compose.setContent -import androidx.activity.result.contract.ActivityResultContract import androidx.appcompat.app.AppCompatActivity import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Text -import androidx.core.os.BundleCompat import com.stripe.android.common.ui.ElementsBottomSheetLayout -import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata -import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.uicore.StripeTheme import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState import com.stripe.android.uicore.utils.fadeOut -import com.stripe.android.view.ActivityStarter -import kotlinx.parcelize.Parcelize internal class FormActivity : AppCompatActivity() { private val args: FormContract.Args? by lazy { @@ -29,7 +20,10 @@ internal class FormActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (args == null) finish() + if (args == null) { + setFormResult(FormResult.Cancelled) + finish() + } setContent { StripeTheme { @@ -54,58 +48,11 @@ internal class FormActivity : AppCompatActivity() { super.finish() fadeOut() } -} - -internal sealed interface FormResult : Parcelable { - - @Parcelize - data class Complete(val selection: PaymentSelection) : FormResult - - @Parcelize - object Cancelled : FormResult - - companion object { - internal const val EXTRA_RESULT = ActivityStarter.Result.EXTRA - - fun toIntent(intent: Intent, result: FormResult): Intent { - return intent.putExtra(EXTRA_RESULT, result) - } - - fun fromIntent(intent: Intent?): FormResult { - val result = intent?.extras?.let { bundle -> - BundleCompat.getParcelable(bundle, EXTRA_RESULT, FormResult::class.java) - } - - return result ?: Cancelled - } - } -} - -internal class FormContract : ActivityResultContract() { - override fun createIntent(context: Context, input: Args): Intent { - return Intent(context, FormActivity::class.java) - .putExtra(EXTRA_ARGS, input) - } - - override fun parseResult(resultCode: Int, intent: Intent?): FormResult { - return FormResult.fromIntent(intent) - } - - @Parcelize - internal data class Args( - val selectedPaymentMethodCode: String, - val paymentMethodMetadata: PaymentMethodMetadata?, - ) : Parcelable { - companion object { - fun fromIntent(intent: Intent): Args? { - return intent.extras?.let { bundle -> - BundleCompat.getParcelable(bundle, EXTRA_ARGS, Args::class.java) - } - } - } - } - internal companion object { - internal const val EXTRA_ARGS: String = "extra_activity_args" + private fun setFormResult(result: FormResult) { + setResult( + Activity.RESULT_OK, + FormResult.toIntent(intent, result) + ) } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt new file mode 100644 index 00000000000..4c66fc8a5fd --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt @@ -0,0 +1,65 @@ +package com.stripe.android.paymentelement.embedded + +import android.content.Context +import android.content.Intent +import android.os.Parcelable +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.os.BundleCompat +import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata +import com.stripe.android.paymentsheet.model.PaymentSelection +import com.stripe.android.view.ActivityStarter +import kotlinx.parcelize.Parcelize + +internal sealed interface FormResult : Parcelable { + + @Parcelize + data class Complete(val selection: PaymentSelection) : FormResult + + @Parcelize + object Cancelled : FormResult + + companion object { + internal const val EXTRA_RESULT = ActivityStarter.Result.EXTRA + + fun toIntent(intent: Intent, result: FormResult): Intent { + return intent.putExtra(EXTRA_RESULT, result) + } + + fun fromIntent(intent: Intent?): FormResult { + val result = intent?.extras?.let { bundle -> + BundleCompat.getParcelable(bundle, EXTRA_RESULT, FormResult::class.java) + } + + return result ?: Cancelled + } + } +} + +internal class FormContract : ActivityResultContract() { + override fun createIntent(context: Context, input: Args): Intent { + return Intent(context, FormActivity::class.java) + .putExtra(EXTRA_ARGS, input) + } + + override fun parseResult(resultCode: Int, intent: Intent?): FormResult { + return FormResult.fromIntent(intent) + } + + @Parcelize + internal data class Args( + val selectedPaymentMethodCode: String, + val paymentMethodMetadata: PaymentMethodMetadata?, + ) : Parcelable { + companion object { + fun fromIntent(intent: Intent): Args? { + return intent.extras?.let { bundle -> + BundleCompat.getParcelable(bundle, EXTRA_ARGS, Args::class.java) + } + } + } + } + + internal companion object { + internal const val EXTRA_ARGS: String = "extra_activity_args" + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt new file mode 100644 index 00000000000..4fcdad77c67 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt @@ -0,0 +1,86 @@ +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.testing.TestLifecycleOwner +import com.stripe.android.model.PaymentMethodFixtures +import com.stripe.android.paymentelement.embedded.DefaultEmbeddedActivityLauncher +import com.stripe.android.paymentelement.embedded.EmbeddedSelectionHolder +import com.stripe.android.paymentelement.embedded.FormContract +import com.stripe.android.paymentelement.embedded.FormResult +import junit.framework.TestCase.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.capture +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class EmbeddedActivityLauncherTest { + + private lateinit var activityResultCaller: ActivityResultCaller + private lateinit var lifecycleOwner: TestLifecycleOwner + private lateinit var selectionHolder: EmbeddedSelectionHolder + private lateinit var launcher: DefaultEmbeddedActivityLauncher + private lateinit var formActivityLauncher: ActivityResultLauncher + + @Captor + private lateinit var contractCallbackCaptor: ArgumentCaptor> + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + activityResultCaller = mock() + lifecycleOwner = TestLifecycleOwner() + selectionHolder = mock() + + formActivityLauncher = mock() + + whenever( + activityResultCaller.registerForActivityResult( + any(), + capture(contractCallbackCaptor) + ) + ).thenReturn(formActivityLauncher) + + launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner, selectionHolder) + } + + @Test + fun `formLauncher launches activity with correct parameters`() { + val code = "test_code" + val expectedArgs = FormContract.Args(code, null) + launcher.formLauncher?.invoke(code, null) + verify(formActivityLauncher).launch(expectedArgs) + } + + @Test + fun `formActivityLauncher callback updates selection holder on complete result`() { + val selection = PaymentMethodFixtures.CARD_PAYMENT_SELECTION + val result = FormResult.Complete(PaymentMethodFixtures.CARD_PAYMENT_SELECTION) + contractCallbackCaptor.value.onActivityResult(result) + verify(selectionHolder).set(selection) + } + + @Test + fun `formActivityLauncher callback does not update selection holder on non-complete result`() { + val result = FormResult.Cancelled + contractCallbackCaptor.value.onActivityResult(result) + verify(selectionHolder, never()).set(any()) + } + + @Test + fun `cleanup happens on lifecycle destroy`() { + lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + verify(formActivityLauncher).unregister() + assertNull(launcher.formLauncher) + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt index 96bd337665e..9b7a45e61fe 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt @@ -28,6 +28,12 @@ internal class FakeEmbeddedContentHelper( ) } + override fun setFormLauncher( + formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? + ) { + // NO-OP + } + fun validate() { dataLoadedTurbine.ensureAllEventsConsumed() } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt new file mode 100644 index 00000000000..8bad1e4833b --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt @@ -0,0 +1,44 @@ +package com.stripe.android.paymentelement.embedded + +import android.content.Context +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class FormActivityTest { + + private lateinit var scenario: ActivityScenario + private lateinit var context: Context + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + } + + @After + fun tearDown() { + if (::scenario.isInitialized) { + scenario.close() + } + } + + @Test + fun `when launched without args should finish with cancelled result`() { + ActivityScenario.launchActivityForResult( + FormActivity::class.java, + Bundle.EMPTY + ).use { activityScenario -> + assertThat(activityScenario.state).isEqualTo(Lifecycle.State.DESTROYED) + val result = FormContract().parseResult(0, activityScenario.result.resultData) + assertThat(result).isInstanceOf(FormResult.Cancelled::class.java) + } + } +} From 0f53c5dcc6d9091e4b2b2649188e4effb3baf69e Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Fri, 17 Jan 2025 13:01:14 -0500 Subject: [PATCH 06/13] Lint --- paymentsheet/api/paymentsheet.api | 24 +++++++++++++++++++ .../embedded/EmbeddedActivityLauncherTest.kt | 2 +- .../embedded/FormActivityTest.kt | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/paymentsheet/api/paymentsheet.api b/paymentsheet/api/paymentsheet.api index cb023ed5956..7e065da03e5 100644 --- a/paymentsheet/api/paymentsheet.api +++ b/paymentsheet/api/paymentsheet.api @@ -679,6 +679,30 @@ public final class com/stripe/android/paymentelement/embedded/EmbeddedConfirmati public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/stripe/android/paymentelement/embedded/FormContract$Args$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/embedded/FormContract$Args; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/paymentelement/embedded/FormContract$Args; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/paymentelement/embedded/FormResult$Cancelled$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/embedded/FormResult$Cancelled; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/paymentelement/embedded/FormResult$Cancelled; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/stripe/android/paymentelement/embedded/FormResult$Complete$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/paymentelement/embedded/FormResult$Complete; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/paymentelement/embedded/FormResult$Complete; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/stripe/android/paymentsheet/BuildConfig { public static final field BUILD_TYPE Ljava/lang/String; public static final field DEBUG Z diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt index 4fcdad77c67..24711f264d5 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt @@ -24,7 +24,7 @@ import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class EmbeddedActivityLauncherTest { +internal class EmbeddedActivityLauncherTest { private lateinit var activityResultCaller: ActivityResultCaller private lateinit var lifecycleOwner: TestLifecycleOwner diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt index 8bad1e4833b..88794c98288 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt @@ -13,7 +13,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class FormActivityTest { +internal class FormActivityTest { private lateinit var scenario: ActivityScenario private lateinit var context: Context From 72236440dd6fd308b89b3ee238e49ce9e49c1af5 Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Tue, 21 Jan 2025 14:21:40 -0500 Subject: [PATCH 07/13] Refactor --- .../paymentelement/EmbeddedPaymentElement.kt | 5 ++--- .../DefaultEmbeddedActivityLauncher.kt | 20 +++---------------- .../embedded/EmbeddedContentHelper.kt | 6 ++++++ .../paymentelement/embedded/FormActivity.kt | 3 ++- .../embedded/SharedPaymentElementViewModel.kt | 20 +++++++++---------- .../embedded/EmbeddedActivityLauncherTest.kt | 14 +------------ .../embedded/FakeEmbeddedContentHelper.kt | 4 ++++ 7 files changed, 27 insertions(+), 45 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt index 9a895adffe2..9f21a27fcc7 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt @@ -39,11 +39,10 @@ class EmbeddedPaymentElement private constructor( private val embeddedConfirmationHelper: EmbeddedConfirmationHelper, private val sharedViewModel: SharedPaymentElementViewModel, private val activityResultCaller: ActivityResultCaller, - private val lifecycleOwner: LifecycleOwner ) { init { - sharedViewModel.initEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner) + sharedViewModel.initEmbeddedActivityLauncher(activityResultCaller) } /** @@ -498,6 +497,7 @@ class EmbeddedPaymentElement private constructor( override fun onDestroy(owner: LifecycleOwner) { IntentConfirmationInterceptor.createIntentCallback = null ExternalPaymentMethodInterceptor.externalPaymentMethodConfirmHandler = null + sharedViewModel.clearFormLauncher() } } ) @@ -511,7 +511,6 @@ class EmbeddedPaymentElement private constructor( ), sharedViewModel = sharedViewModel, activityResultCaller = activityResultCaller, - lifecycleOwner = lifecycleOwner ) } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt index 264c566eb8c..cf0820cda2b 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt @@ -2,17 +2,14 @@ package com.stripe.android.paymentelement.embedded import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata internal interface EmbeddedActivityLauncher { - var formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit)? + val formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit)? } internal class DefaultEmbeddedActivityLauncher( - private val activityResultCaller: ActivityResultCaller, - private val lifecycleOwner: LifecycleOwner, + activityResultCaller: ActivityResultCaller, private val selectionHolder: EmbeddedSelectionHolder ) : EmbeddedActivityLauncher { @@ -27,16 +24,5 @@ internal class DefaultEmbeddedActivityLauncher( { code, metadata -> formActivityLauncher.launch(FormContract.Args(code, metadata)) } - - init { - lifecycleOwner.lifecycle.addObserver( - object : DefaultLifecycleObserver { - override fun onDestroy(owner: LifecycleOwner) { - formActivityLauncher.unregister() - formLauncher = null - super.onDestroy(owner) - } - } - ) - } + private set } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt index b155eee9a5a..521c2cddfe2 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt @@ -48,6 +48,8 @@ internal interface EmbeddedContentHelper { ) fun setFormLauncher(formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)?) + + fun clearFormLauncher() } internal fun interface EmbeddedContentHelperFactory { @@ -129,6 +131,10 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( this.formLauncher = formLauncher } + override fun clearFormLauncher() { + formLauncher = null + } + private fun createInteractor( coroutineScope: CoroutineScope, paymentMethodMetadata: PaymentMethodMetadata, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt index 3a72a7c27bd..b973423e614 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt @@ -23,6 +23,7 @@ internal class FormActivity : AppCompatActivity() { if (args == null) { setFormResult(FormResult.Cancelled) finish() + return } setContent { @@ -38,7 +39,7 @@ internal class FormActivity : AppCompatActivity() { finish() } ) { - Text(args?.selectedPaymentMethodCode ?: "Whoops") + args?.selectedPaymentMethodCode?.let { Text(it) } } } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt index 7a2611a4ace..6714766d90a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt @@ -3,7 +3,6 @@ package com.stripe.android.paymentelement.embedded import android.content.Context import android.content.res.Resources import androidx.activity.result.ActivityResultCaller -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -31,7 +30,6 @@ import com.stripe.android.link.LinkConfigurationCoordinator import com.stripe.android.link.RealLinkConfigurationCoordinator import com.stripe.android.link.injection.LinkAnalyticsComponent import com.stripe.android.link.injection.LinkComponent -import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata import com.stripe.android.paymentelement.EmbeddedPaymentElement import com.stripe.android.paymentelement.EmbeddedPaymentElement.ConfigureResult import com.stripe.android.paymentelement.EmbeddedPaymentElement.PaymentOptionDisplayData @@ -113,15 +111,6 @@ internal class SharedPaymentElementViewModel @Inject constructor( } } - fun initEmbeddedActivityLauncher(activityResultCaller: ActivityResultCaller, lifecycleOwner: LifecycleOwner) { - val launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner, selectionHolder) - setFormLauncher(launcher.formLauncher) - } - - private fun setFormLauncher(launch: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit)?) { - embeddedContentHelper.setFormLauncher(launch) - } - suspend fun configure( intentConfiguration: PaymentSheet.IntentConfiguration, configuration: EmbeddedPaymentElement.Configuration, @@ -155,6 +144,15 @@ internal class SharedPaymentElementViewModel @Inject constructor( selectionHolder.set(null) } + fun initEmbeddedActivityLauncher(activityResultCaller: ActivityResultCaller) { + val launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, selectionHolder) + embeddedContentHelper.setFormLauncher(launcher.formLauncher) + } + + fun clearFormLauncher() { + embeddedContentHelper.clearFormLauncher() + } + class Factory(private val statusBarColor: Int?) : ViewModelProvider.Factory { override fun create(modelClass: KClass, extras: CreationExtras): T { val component = DaggerSharedPaymentElementViewModelComponent.builder() diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt index 24711f264d5..4d9d978aef5 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt @@ -1,14 +1,11 @@ import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.testing.TestLifecycleOwner import com.stripe.android.model.PaymentMethodFixtures import com.stripe.android.paymentelement.embedded.DefaultEmbeddedActivityLauncher import com.stripe.android.paymentelement.embedded.EmbeddedSelectionHolder import com.stripe.android.paymentelement.embedded.FormContract import com.stripe.android.paymentelement.embedded.FormResult -import junit.framework.TestCase.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -27,7 +24,6 @@ import org.robolectric.RobolectricTestRunner internal class EmbeddedActivityLauncherTest { private lateinit var activityResultCaller: ActivityResultCaller - private lateinit var lifecycleOwner: TestLifecycleOwner private lateinit var selectionHolder: EmbeddedSelectionHolder private lateinit var launcher: DefaultEmbeddedActivityLauncher private lateinit var formActivityLauncher: ActivityResultLauncher @@ -39,7 +35,6 @@ internal class EmbeddedActivityLauncherTest { fun setUp() { MockitoAnnotations.openMocks(this) activityResultCaller = mock() - lifecycleOwner = TestLifecycleOwner() selectionHolder = mock() formActivityLauncher = mock() @@ -51,7 +46,7 @@ internal class EmbeddedActivityLauncherTest { ) ).thenReturn(formActivityLauncher) - launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner, selectionHolder) + launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, selectionHolder) } @Test @@ -76,11 +71,4 @@ internal class EmbeddedActivityLauncherTest { contractCallbackCaptor.value.onActivityResult(result) verify(selectionHolder, never()).set(any()) } - - @Test - fun `cleanup happens on lifecycle destroy`() { - lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) - verify(formActivityLauncher).unregister() - assertNull(launcher.formLauncher) - } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt index 9b7a45e61fe..ee29b084c52 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt @@ -34,6 +34,10 @@ internal class FakeEmbeddedContentHelper( // NO-OP } + override fun clearFormLauncher() { + // NO-OP + } + fun validate() { dataLoadedTurbine.ensureAllEventsConsumed() } From e9df1de2f5e315f5347041e2aca22d52c45312a5 Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Tue, 21 Jan 2025 15:38:53 -0500 Subject: [PATCH 08/13] Add EmbeddedActivityLauncher test to SharedPaymentElementViewModelTest --- .../paymentelement/EmbeddedPaymentElement.kt | 11 +++---- .../DefaultEmbeddedActivityLauncher.kt | 5 ++- .../embedded/SharedPaymentElementViewModel.kt | 2 +- .../embedded/EmbeddedActivityLauncherTest.kt | 2 +- .../embedded/FakeEmbeddedContentHelper.kt | 5 +-- .../SharedPaymentElementViewModelTest.kt | 32 ++++++++++++++++++- 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt index 9f21a27fcc7..9e9765e1f27 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt @@ -38,13 +38,8 @@ import kotlinx.parcelize.Parcelize class EmbeddedPaymentElement private constructor( private val embeddedConfirmationHelper: EmbeddedConfirmationHelper, private val sharedViewModel: SharedPaymentElementViewModel, - private val activityResultCaller: ActivityResultCaller, ) { - init { - sharedViewModel.initEmbeddedActivityLauncher(activityResultCaller) - } - /** * Contains information about the customer's selected payment option. * Use this to display the payment option in your own UI. @@ -497,10 +492,13 @@ class EmbeddedPaymentElement private constructor( override fun onDestroy(owner: LifecycleOwner) { IntentConfirmationInterceptor.createIntentCallback = null ExternalPaymentMethodInterceptor.externalPaymentMethodConfirmHandler = null - sharedViewModel.clearFormLauncher() + sharedViewModel.clearEmbeddedActivityLauncher() } } ) + + sharedViewModel.initEmbeddedActivityLauncher(activityResultCaller) + return EmbeddedPaymentElement( embeddedConfirmationHelper = EmbeddedConfirmationHelper( confirmationHandler = sharedViewModel.confirmationHandler, @@ -510,7 +508,6 @@ class EmbeddedPaymentElement private constructor( confirmationStateSupplier = { sharedViewModel.confirmationStateHolder.state }, ), sharedViewModel = sharedViewModel, - activityResultCaller = activityResultCaller, ) } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt index cf0820cda2b..ee0c5f02bb0 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt @@ -5,7 +5,7 @@ import androidx.activity.result.ActivityResultLauncher import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata internal interface EmbeddedActivityLauncher { - val formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit)? + val formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit) } internal class DefaultEmbeddedActivityLauncher( @@ -20,9 +20,8 @@ internal class DefaultEmbeddedActivityLauncher( } } - override var formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit)? = + override val formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit) = { code, metadata -> formActivityLauncher.launch(FormContract.Args(code, metadata)) } - private set } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt index 6714766d90a..c0489569a55 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt @@ -149,7 +149,7 @@ internal class SharedPaymentElementViewModel @Inject constructor( embeddedContentHelper.setFormLauncher(launcher.formLauncher) } - fun clearFormLauncher() { + fun clearEmbeddedActivityLauncher() { embeddedContentHelper.clearFormLauncher() } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt index 4d9d978aef5..271205e3ad8 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt @@ -53,7 +53,7 @@ internal class EmbeddedActivityLauncherTest { fun `formLauncher launches activity with correct parameters`() { val code = "test_code" val expectedArgs = FormContract.Args(code, null) - launcher.formLauncher?.invoke(code, null) + launcher.formLauncher.invoke(code, null) verify(formActivityLauncher).launch(expectedArgs) } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt index ee29b084c52..ef2ae1f0a86 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeEmbeddedContentHelper.kt @@ -13,6 +13,7 @@ internal class FakeEmbeddedContentHelper( ) : EmbeddedContentHelper { private val _dataLoadedTurbine = Turbine() val dataLoadedTurbine: ReceiveTurbine = _dataLoadedTurbine + var testFormLauncher: ((String, PaymentMethodMetadata) -> Unit)? = null override fun dataLoaded( paymentMethodMetadata: PaymentMethodMetadata, @@ -31,11 +32,11 @@ internal class FakeEmbeddedContentHelper( override fun setFormLauncher( formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? ) { - // NO-OP + this.testFormLauncher = formLauncher } override fun clearFormLauncher() { - // NO-OP + testFormLauncher = null } fun validate() { diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt index bb276801639..2829328e206 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt @@ -1,5 +1,8 @@ package com.stripe.android.paymentelement.embedded +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.SavedStateHandle import androidx.test.core.app.ApplicationProvider import app.cash.turbine.Turbine @@ -22,7 +25,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith -import org.mockito.Mockito.mock +import org.mockito.ArgumentCaptor +import org.mockito.kotlin.any +import org.mockito.kotlin.capture +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import kotlin.test.Test @@ -303,6 +310,29 @@ internal class SharedPaymentElementViewModelTest { assertThat(embeddedContentHelper.dataLoadedTurbine.awaitItem()).isNotNull() } + @Test + fun `initEmbeddedActivityLauncher and clearEmbeddedActivityLauncher successfully init and clear formLauncher`() = + testScenario { + val launcher: ActivityResultLauncher = mock() + val activityResultCaller: ActivityResultCaller = mock() + + @Suppress("UNCHECKED_CAST") + val contractCallbackCaptor: ArgumentCaptor> = ArgumentCaptor + .forClass(ActivityResultCallback::class.java) as ArgumentCaptor> + + whenever( + activityResultCaller.registerForActivityResult( + any(), + capture(contractCallbackCaptor) + ) + ).thenReturn(launcher) + assertThat(embeddedContentHelper.testFormLauncher).isNull() + viewModel.initEmbeddedActivityLauncher(activityResultCaller) + assertThat(embeddedContentHelper.testFormLauncher).isNotNull() + viewModel.clearEmbeddedActivityLauncher() + assertThat(embeddedContentHelper.testFormLauncher).isNull() + } + private fun testScenario( block: suspend Scenario.() -> Unit, ) = runTest { From a8e1e939b9017a0a641aeb3ee3acdfb02e59cea5 Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Tue, 21 Jan 2025 16:47:25 -0500 Subject: [PATCH 09/13] Unregister formActivityLauncher in onDestroy --- .../paymentelement/EmbeddedPaymentElement.kt | 2 +- .../embedded/DefaultEmbeddedActivityLauncher.kt | 14 ++++++++++++++ .../embedded/SharedPaymentElementViewModel.kt | 5 +++-- .../embedded/EmbeddedActivityLauncherTest.kt | 12 +++++++++++- .../embedded/SharedPaymentElementViewModelTest.kt | 3 ++- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt index 9e9765e1f27..a65b8aee78a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt @@ -497,7 +497,7 @@ class EmbeddedPaymentElement private constructor( } ) - sharedViewModel.initEmbeddedActivityLauncher(activityResultCaller) + sharedViewModel.initEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner) return EmbeddedPaymentElement( embeddedConfirmationHelper = EmbeddedConfirmationHelper( diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt index ee0c5f02bb0..e431f0eb259 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt @@ -2,6 +2,8 @@ package com.stripe.android.paymentelement.embedded import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata internal interface EmbeddedActivityLauncher { @@ -10,9 +12,21 @@ internal interface EmbeddedActivityLauncher { internal class DefaultEmbeddedActivityLauncher( activityResultCaller: ActivityResultCaller, + lifecycleOwner: LifecycleOwner, private val selectionHolder: EmbeddedSelectionHolder ) : EmbeddedActivityLauncher { + init { + lifecycleOwner.lifecycle.addObserver( + object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + formActivityLauncher.unregister() + super.onDestroy(owner) + } + } + ) + } + private var formActivityLauncher: ActivityResultLauncher = activityResultCaller.registerForActivityResult(FormContract()) { result -> if (result is FormResult.Complete) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt index c0489569a55..498a91cdbc9 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModel.kt @@ -3,6 +3,7 @@ package com.stripe.android.paymentelement.embedded import android.content.Context import android.content.res.Resources import androidx.activity.result.ActivityResultCaller +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -144,8 +145,8 @@ internal class SharedPaymentElementViewModel @Inject constructor( selectionHolder.set(null) } - fun initEmbeddedActivityLauncher(activityResultCaller: ActivityResultCaller) { - val launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, selectionHolder) + fun initEmbeddedActivityLauncher(activityResultCaller: ActivityResultCaller, lifecycleOwner: LifecycleOwner) { + val launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner, selectionHolder) embeddedContentHelper.setFormLauncher(launcher.formLauncher) } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt index 271205e3ad8..31af4528e1b 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt @@ -1,6 +1,8 @@ import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.testing.TestLifecycleOwner import com.stripe.android.model.PaymentMethodFixtures import com.stripe.android.paymentelement.embedded.DefaultEmbeddedActivityLauncher import com.stripe.android.paymentelement.embedded.EmbeddedSelectionHolder @@ -25,6 +27,7 @@ internal class EmbeddedActivityLauncherTest { private lateinit var activityResultCaller: ActivityResultCaller private lateinit var selectionHolder: EmbeddedSelectionHolder + private lateinit var lifecycleOwner: TestLifecycleOwner private lateinit var launcher: DefaultEmbeddedActivityLauncher private lateinit var formActivityLauncher: ActivityResultLauncher @@ -35,6 +38,7 @@ internal class EmbeddedActivityLauncherTest { fun setUp() { MockitoAnnotations.openMocks(this) activityResultCaller = mock() + lifecycleOwner = TestLifecycleOwner() selectionHolder = mock() formActivityLauncher = mock() @@ -46,7 +50,7 @@ internal class EmbeddedActivityLauncherTest { ) ).thenReturn(formActivityLauncher) - launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, selectionHolder) + launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner, selectionHolder) } @Test @@ -71,4 +75,10 @@ internal class EmbeddedActivityLauncherTest { contractCallbackCaptor.value.onActivityResult(result) verify(selectionHolder, never()).set(any()) } + + @Test + fun `cleanup happens on lifecycle destroy`() { + lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + verify(formActivityLauncher).unregister() + } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt index 2829328e206..4477c49b5ff 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt @@ -4,6 +4,7 @@ import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.core.app.ApplicationProvider import app.cash.turbine.Turbine import app.cash.turbine.test @@ -327,7 +328,7 @@ internal class SharedPaymentElementViewModelTest { ) ).thenReturn(launcher) assertThat(embeddedContentHelper.testFormLauncher).isNull() - viewModel.initEmbeddedActivityLauncher(activityResultCaller) + viewModel.initEmbeddedActivityLauncher(activityResultCaller, TestLifecycleOwner()) assertThat(embeddedContentHelper.testFormLauncher).isNotNull() viewModel.clearEmbeddedActivityLauncher() assertThat(embeddedContentHelper.testFormLauncher).isNull() From d3ef1c1ba45150414048c5a9aeaf3e020b037394 Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Tue, 21 Jan 2025 19:44:53 -0500 Subject: [PATCH 10/13] Update tests --- .../embedded/EmbeddedActivityLauncherTest.kt | 98 +++++++++++-------- .../embedded/FakeFormActivityLauncher.kt | 48 +++++++++ .../embedded/FormActivityTest.kt | 19 ---- .../SharedPaymentElementViewModelTest.kt | 22 +---- 4 files changed, 106 insertions(+), 81 deletions(-) create mode 100644 paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeFormActivityLauncher.kt diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt index 31af4528e1b..7f9d51d0e42 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt @@ -1,84 +1,98 @@ import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCaller -import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.Lifecycle +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.testing.TestLifecycleOwner import com.stripe.android.model.PaymentMethodFixtures import com.stripe.android.paymentelement.embedded.DefaultEmbeddedActivityLauncher import com.stripe.android.paymentelement.embedded.EmbeddedSelectionHolder +import com.stripe.android.paymentelement.embedded.FakeFormActivityLauncher import com.stripe.android.paymentelement.embedded.FormContract import com.stripe.android.paymentelement.embedded.FormResult -import org.junit.Before +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.Captor +import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.capture -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) internal class EmbeddedActivityLauncherTest { - private lateinit var activityResultCaller: ActivityResultCaller - private lateinit var selectionHolder: EmbeddedSelectionHolder - private lateinit var lifecycleOwner: TestLifecycleOwner - private lateinit var launcher: DefaultEmbeddedActivityLauncher - private lateinit var formActivityLauncher: ActivityResultLauncher - - @Captor - private lateinit var contractCallbackCaptor: ArgumentCaptor> - - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - activityResultCaller = mock() - lifecycleOwner = TestLifecycleOwner() - selectionHolder = mock() - - formActivityLauncher = mock() - - whenever( - activityResultCaller.registerForActivityResult( - any(), - capture(contractCallbackCaptor) - ) - ).thenReturn(formActivityLauncher) - - launcher = DefaultEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner, selectionHolder) - } - @Test - fun `formLauncher launches activity with correct parameters`() { + fun `formLauncher launches activity with correct parameters`() = testScenario { val code = "test_code" val expectedArgs = FormContract.Args(code, null) launcher.formLauncher.invoke(code, null) - verify(formActivityLauncher).launch(expectedArgs) + assert(formActivityLauncher.didLaunch) + assert(formActivityLauncher.launchArgs == expectedArgs) } @Test - fun `formActivityLauncher callback updates selection holder on complete result`() { + fun `formActivityLauncher callback updates selection holder on complete result`() = testScenario { val selection = PaymentMethodFixtures.CARD_PAYMENT_SELECTION val result = FormResult.Complete(PaymentMethodFixtures.CARD_PAYMENT_SELECTION) contractCallbackCaptor.value.onActivityResult(result) - verify(selectionHolder).set(selection) + assert(selectionHolder.selection.value == selection) } @Test - fun `formActivityLauncher callback does not update selection holder on non-complete result`() { + fun `formActivityLauncher callback does not update selection holder on non-complete result`() = testScenario { val result = FormResult.Cancelled contractCallbackCaptor.value.onActivityResult(result) - verify(selectionHolder, never()).set(any()) + assert(selectionHolder.selection.value == null) } @Test - fun `cleanup happens on lifecycle destroy`() { + fun `cleanup happens on lifecycle destroy`() = testScenario { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) - verify(formActivityLauncher).unregister() + assert(formActivityLauncher.didUnregister) } + + private fun testScenario( + block: suspend Scenario.() -> Unit, + ) = runTest { + MockitoAnnotations.openMocks(this) + val activityResultCaller = mock() + val lifecycleOwner = TestLifecycleOwner() + val formActivityLauncher = FakeFormActivityLauncher() + val selectionHolder = EmbeddedSelectionHolder(SavedStateHandle()) + + @Suppress("UNCHECKED_CAST") + val contractCallbackCaptor: ArgumentCaptor> = ArgumentCaptor + .forClass(ActivityResultCallback::class.java) as ArgumentCaptor> + + whenever( + activityResultCaller.registerForActivityResult( + any(), + capture(contractCallbackCaptor) + ) + ).thenReturn(formActivityLauncher) + + val embeddedActivityLauncher = DefaultEmbeddedActivityLauncher( + activityResultCaller = activityResultCaller, + lifecycleOwner = lifecycleOwner, + selectionHolder = selectionHolder + ) + + Scenario( + contractCallbackCaptor = contractCallbackCaptor, + selectionHolder = selectionHolder, + lifecycleOwner = lifecycleOwner, + formActivityLauncher = formActivityLauncher, + launcher = embeddedActivityLauncher + ).block() + } + + private class Scenario( + val contractCallbackCaptor: ArgumentCaptor>, + val selectionHolder: EmbeddedSelectionHolder, + val lifecycleOwner: TestLifecycleOwner, + val formActivityLauncher: FakeFormActivityLauncher, + val launcher: DefaultEmbeddedActivityLauncher + ) } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeFormActivityLauncher.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeFormActivityLauncher.kt new file mode 100644 index 00000000000..eab49d8cbe5 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FakeFormActivityLauncher.kt @@ -0,0 +1,48 @@ +package com.stripe.android.paymentelement.embedded + +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.app.ActivityOptionsCompat + +internal class FakeFormActivityLauncher : ActivityResultLauncher() { + var didLaunch = false + private set + var launchArgs: FormContract.Args? = null + private set + var didUnregister = false + private set + + override fun launch(input: FormContract.Args?, options: ActivityOptionsCompat?) { + didLaunch = true + launchArgs = input + } + + override fun unregister() { + didUnregister = true + } + + override fun getContract(): ActivityResultContract { + return FormContract() + } +} + +@Suppress("UNCHECKED_CAST") +internal class FakeActivityResultCaller(private val fakeLauncher: FakeFormActivityLauncher) : ActivityResultCaller { + override fun registerForActivityResult( + contract: ActivityResultContract, + callback: ActivityResultCallback + ): ActivityResultLauncher { + return fakeLauncher as ActivityResultLauncher + } + + override fun registerForActivityResult( + contract: ActivityResultContract, + registry: ActivityResultRegistry, + callback: ActivityResultCallback + ): ActivityResultLauncher { + return fakeLauncher as ActivityResultLauncher + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt index 88794c98288..399ccd78753 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/FormActivityTest.kt @@ -1,13 +1,9 @@ package com.stripe.android.paymentelement.embedded -import android.content.Context import android.os.Bundle import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario -import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat -import org.junit.After -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -15,21 +11,6 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) internal class FormActivityTest { - private lateinit var scenario: ActivityScenario - private lateinit var context: Context - - @Before - fun setUp() { - context = ApplicationProvider.getApplicationContext() - } - - @After - fun tearDown() { - if (::scenario.isInitialized) { - scenario.close() - } - } - @Test fun `when launched without args should finish with cancelled result`() { ActivityScenario.launchActivityForResult( diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt index 4477c49b5ff..4317cef99dc 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt @@ -1,8 +1,5 @@ package com.stripe.android.paymentelement.embedded -import androidx.activity.result.ActivityResultCallback -import androidx.activity.result.ActivityResultCaller -import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.core.app.ApplicationProvider @@ -26,11 +23,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.kotlin.any -import org.mockito.kotlin.capture import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import kotlin.test.Test @@ -314,19 +307,8 @@ internal class SharedPaymentElementViewModelTest { @Test fun `initEmbeddedActivityLauncher and clearEmbeddedActivityLauncher successfully init and clear formLauncher`() = testScenario { - val launcher: ActivityResultLauncher = mock() - val activityResultCaller: ActivityResultCaller = mock() - - @Suppress("UNCHECKED_CAST") - val contractCallbackCaptor: ArgumentCaptor> = ArgumentCaptor - .forClass(ActivityResultCallback::class.java) as ArgumentCaptor> - - whenever( - activityResultCaller.registerForActivityResult( - any(), - capture(contractCallbackCaptor) - ) - ).thenReturn(launcher) + val launcher = FakeFormActivityLauncher() + val activityResultCaller = FakeActivityResultCaller(launcher) assertThat(embeddedContentHelper.testFormLauncher).isNull() viewModel.initEmbeddedActivityLauncher(activityResultCaller, TestLifecycleOwner()) assertThat(embeddedContentHelper.testFormLauncher).isNotNull() From 801c24716f347aeef87189dccd1ec27f4ecb2552 Mon Sep 17 00:00:00 2001 From: Tyler Clawson Date: Tue, 21 Jan 2025 19:53:15 -0500 Subject: [PATCH 11/13] Clean up --- .../com/stripe/android/paymentelement/embedded/FormActivity.kt | 2 +- .../paymentelement/embedded/EmbeddedActivityLauncherTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt index b973423e614..6e0aecf1c2a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt @@ -39,7 +39,7 @@ internal class FormActivity : AppCompatActivity() { finish() } ) { - args?.selectedPaymentMethodCode?.let { Text(it) } + Text((args as FormContract.Args).selectedPaymentMethodCode) } } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt index 7f9d51d0e42..e1a71ecc27f 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/EmbeddedActivityLauncherTest.kt @@ -48,7 +48,7 @@ internal class EmbeddedActivityLauncherTest { } @Test - fun `cleanup happens on lifecycle destroy`() = testScenario { + fun `formActivityLauncher unregisters onDestroy`() = testScenario { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) assert(formActivityLauncher.didUnregister) } From 018d12cd2349613a064258a505f028ee7604fa0a Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Wed, 22 Jan 2025 06:46:56 -0700 Subject: [PATCH 12/13] Updates. --- .../paymentelement/EmbeddedPaymentElement.kt | 4 +-- ...her.kt => DefaultEmbeddedSheetLauncher.kt} | 15 +++++----- .../embedded/EmbeddedContentHelper.kt | 18 +++++------ .../paymentelement/embedded/FormActivity.kt | 5 ++-- .../paymentelement/embedded/FormContract.kt | 2 +- .../embedded/SharedPaymentElementViewModel.kt | 10 +++---- ...kt => DefaultEmbeddedSheetLauncherTest.kt} | 30 +++++++++---------- .../embedded/FakeEmbeddedContentHelper.kt | 12 ++++---- .../SharedPaymentElementViewModelTest.kt | 10 +++---- 9 files changed, 51 insertions(+), 55 deletions(-) rename paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/{DefaultEmbeddedActivityLauncher.kt => DefaultEmbeddedSheetLauncher.kt} (71%) rename paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/{EmbeddedActivityLauncherTest.kt => DefaultEmbeddedSheetLauncherTest.kt} (77%) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt index a65b8aee78a..8be50bfb7df 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/EmbeddedPaymentElement.kt @@ -492,12 +492,12 @@ class EmbeddedPaymentElement private constructor( override fun onDestroy(owner: LifecycleOwner) { IntentConfirmationInterceptor.createIntentCallback = null ExternalPaymentMethodInterceptor.externalPaymentMethodConfirmHandler = null - sharedViewModel.clearEmbeddedActivityLauncher() + sharedViewModel.clearEmbeddedSheetLauncher() } } ) - sharedViewModel.initEmbeddedActivityLauncher(activityResultCaller, lifecycleOwner) + sharedViewModel.initEmbeddedSheetLauncher(activityResultCaller, lifecycleOwner) return EmbeddedPaymentElement( embeddedConfirmationHelper = EmbeddedConfirmationHelper( diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedSheetLauncher.kt similarity index 71% rename from paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt rename to paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedSheetLauncher.kt index e431f0eb259..1d686c7a4a5 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedActivityLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedSheetLauncher.kt @@ -6,15 +6,15 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadata -internal interface EmbeddedActivityLauncher { - val formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit) +internal interface EmbeddedSheetLauncher { + fun launchForm(code: String, paymentMethodMetadata: PaymentMethodMetadata) } -internal class DefaultEmbeddedActivityLauncher( +internal class DefaultEmbeddedSheetLauncher( activityResultCaller: ActivityResultCaller, lifecycleOwner: LifecycleOwner, private val selectionHolder: EmbeddedSelectionHolder -) : EmbeddedActivityLauncher { +) : EmbeddedSheetLauncher { init { lifecycleOwner.lifecycle.addObserver( @@ -34,8 +34,7 @@ internal class DefaultEmbeddedActivityLauncher( } } - override val formLauncher: ((code: String, paymentMethodMetadata: PaymentMethodMetadata?) -> Unit) = - { code, metadata -> - formActivityLauncher.launch(FormContract.Args(code, metadata)) - } + override fun launchForm(code: String, paymentMethodMetadata: PaymentMethodMetadata) { + formActivityLauncher.launch(FormContract.Args(code, paymentMethodMetadata)) + } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt index 521c2cddfe2..fc5c422a5c7 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/EmbeddedContentHelper.kt @@ -47,9 +47,9 @@ internal interface EmbeddedContentHelper { embeddedViewDisplaysMandateText: Boolean, ) - fun setFormLauncher(formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)?) + fun setSheetLauncher(sheetLauncher: EmbeddedSheetLauncher) - fun clearFormLauncher() + fun clearSheetLauncher() } internal fun interface EmbeddedContentHelperFactory { @@ -84,7 +84,7 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( private val _embeddedContent = MutableStateFlow(null) override val embeddedContent: StateFlow = _embeddedContent.asStateFlow() - private var formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? = null + private var sheetLauncher: EmbeddedSheetLauncher? = null init { coroutineScope.launch { @@ -125,14 +125,12 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( ) } - override fun setFormLauncher( - formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? - ) { - this.formLauncher = formLauncher + override fun setSheetLauncher(sheetLauncher: EmbeddedSheetLauncher) { + this.sheetLauncher = sheetLauncher } - override fun clearFormLauncher() { - formLauncher = null + override fun clearSheetLauncher() { + sheetLauncher = null } private fun createInteractor( @@ -177,7 +175,7 @@ internal class DefaultEmbeddedContentHelper @AssistedInject constructor( transitionToManageScreen = { }, transitionToFormScreen = { - formLauncher?.invoke(it, state.value?.paymentMethodMetadata) + sheetLauncher?.launchForm(it, paymentMethodMetadata) }, paymentMethods = customerStateHolder.paymentMethods, mostRecentlySelectedSavedPaymentMethod = customerStateHolder.mostRecentlySelectedSavedPaymentMethod, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt index 6e0aecf1c2a..e38e1c0b90b 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormActivity.kt @@ -20,7 +20,8 @@ internal class FormActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (args == null) { + val localArgs = args + if (localArgs == null) { setFormResult(FormResult.Cancelled) finish() return @@ -39,7 +40,7 @@ internal class FormActivity : AppCompatActivity() { finish() } ) { - Text((args as FormContract.Args).selectedPaymentMethodCode) + Text(localArgs.selectedPaymentMethodCode) } } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt index 4c66fc8a5fd..d57e611afed 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/FormContract.kt @@ -48,7 +48,7 @@ internal class FormContract : ActivityResultContract() val dataLoadedTurbine: ReceiveTurbine = _dataLoadedTurbine - var testFormLauncher: ((String, PaymentMethodMetadata) -> Unit)? = null + var testSheetLauncher: EmbeddedSheetLauncher? = null override fun dataLoaded( paymentMethodMetadata: PaymentMethodMetadata, @@ -29,14 +29,12 @@ internal class FakeEmbeddedContentHelper( ) } - override fun setFormLauncher( - formLauncher: ((code: String, paymentMethodMetaData: PaymentMethodMetadata?) -> Unit)? - ) { - this.testFormLauncher = formLauncher + override fun setSheetLauncher(sheetLauncher: EmbeddedSheetLauncher) { + this.testSheetLauncher = sheetLauncher } - override fun clearFormLauncher() { - testFormLauncher = null + override fun clearSheetLauncher() { + testSheetLauncher = null } fun validate() { diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt index 4317cef99dc..1405c98a9c4 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentelement/embedded/SharedPaymentElementViewModelTest.kt @@ -309,11 +309,11 @@ internal class SharedPaymentElementViewModelTest { testScenario { val launcher = FakeFormActivityLauncher() val activityResultCaller = FakeActivityResultCaller(launcher) - assertThat(embeddedContentHelper.testFormLauncher).isNull() - viewModel.initEmbeddedActivityLauncher(activityResultCaller, TestLifecycleOwner()) - assertThat(embeddedContentHelper.testFormLauncher).isNotNull() - viewModel.clearEmbeddedActivityLauncher() - assertThat(embeddedContentHelper.testFormLauncher).isNull() + assertThat(embeddedContentHelper.testSheetLauncher).isNull() + viewModel.initEmbeddedSheetLauncher(activityResultCaller, TestLifecycleOwner()) + assertThat(embeddedContentHelper.testSheetLauncher).isNotNull() + viewModel.clearEmbeddedSheetLauncher() + assertThat(embeddedContentHelper.testSheetLauncher).isNull() } private fun testScenario( From 524321e4066036c814c9282667e0102282b73446 Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Wed, 22 Jan 2025 06:47:45 -0700 Subject: [PATCH 13/13] Updates. --- .../paymentelement/embedded/DefaultEmbeddedSheetLauncher.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedSheetLauncher.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedSheetLauncher.kt index 1d686c7a4a5..6e5cea0a590 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedSheetLauncher.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/DefaultEmbeddedSheetLauncher.kt @@ -27,7 +27,7 @@ internal class DefaultEmbeddedSheetLauncher( ) } - private var formActivityLauncher: ActivityResultLauncher = + private val formActivityLauncher: ActivityResultLauncher = activityResultCaller.registerForActivityResult(FormContract()) { result -> if (result is FormResult.Complete) { selectionHolder.set(result.selection)