From e16f3be1ab8723f9b6bf52ea9cf3416fc5fc4dc8 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Mon, 9 May 2022 18:34:04 -0700 Subject: [PATCH 01/19] Adds mavericks. --- financial-connections/build.gradle | 2 + .../activity_financialconnections_sheet.xml | 27 ++-- .../fragment_financial_connections_sheet.xml | 20 +++ .../FinancialConnectionsSheet.kt | 12 ++ .../FinancialConnectionsSheetActivity.kt | 93 +++---------- .../FinancialConnectionsSheetFragment.kt | 86 ++++++++++++ .../FinancialConnectionsSheetState.kt | 14 +- .../FinancialConnectionsSheetViewModel.kt | 131 +++++++----------- .../FragmentViewBindingDelegate.kt | 58 ++++++++ .../di/FinancialConnectionsSheetComponent.kt | 6 +- ...inancialConnectionsSheetForDataContract.kt | 8 +- .../FinancialConnectionsSheetTest.kt | 2 +- 12 files changed, 282 insertions(+), 177 deletions(-) create mode 100644 financial-connections/res/layout/fragment_financial_connections_sheet.xml create mode 100644 financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt create mode 100644 financial-connections/src/main/java/com/stripe/android/financialconnections/FragmentViewBindingDelegate.kt diff --git a/financial-connections/build.gradle b/financial-connections/build.gradle index 9dbad963cc3..343a56f3d22 100644 --- a/financial-connections/build.gradle +++ b/financial-connections/build.gradle @@ -24,6 +24,7 @@ dependencies { implementation "androidx.constraintlayout:constraintlayout:$androidxConstraintlayoutVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidxLifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$androidxLifecycleVersion" + implementation 'com.airbnb.android:mavericks:2.6.1' implementation "com.google.android.material:material:$materialVersion" implementation "com.google.dagger:dagger:$daggerVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion" @@ -70,3 +71,4 @@ ext { } apply from: "${rootDir}/deploy/deploy.gradle" +apply plugin: 'org.jetbrains.kotlin.android' diff --git a/financial-connections/res/layout/activity_financialconnections_sheet.xml b/financial-connections/res/layout/activity_financialconnections_sheet.xml index 60257da7ec6..e6365334b91 100644 --- a/financial-connections/res/layout/activity_financialconnections_sheet.xml +++ b/financial-connections/res/layout/activity_financialconnections_sheet.xml @@ -1,20 +1,15 @@ - + android:layout_height="match_parent" + android:orientation="vertical" + tools:context=".FinancialConnectionsSheetActivity"> - + - + \ No newline at end of file diff --git a/financial-connections/res/layout/fragment_financial_connections_sheet.xml b/financial-connections/res/layout/fragment_financial_connections_sheet.xml new file mode 100644 index 00000000000..60257da7ec6 --- /dev/null +++ b/financial-connections/res/layout/fragment_financial_connections_sheet.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt index f923781074a..cb86ed165a5 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt @@ -1,8 +1,10 @@ package com.stripe.android.financialconnections +import android.app.Application import android.os.Parcelable import androidx.activity.ComponentActivity import androidx.fragment.app.Fragment +import com.airbnb.mvrx.Mavericks import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetForDataLauncher import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetForTokenLauncher import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetLauncher @@ -15,8 +17,14 @@ import kotlinx.parcelize.Parcelize * typically as a field initializer of an Activity or Fragment. */ class FinancialConnectionsSheet internal constructor( + app: Application, private val financialConnectionsSheetLauncher: FinancialConnectionsSheetLauncher ) { + + init { + Mavericks.initialize(app) + } + /** * Configuration for a [FinancialConnectionsSheet] * @@ -52,6 +60,7 @@ class FinancialConnectionsSheet internal constructor( callback: FinancialConnectionsSheetResultCallback ): FinancialConnectionsSheet { return FinancialConnectionsSheet( + activity.application, FinancialConnectionsSheetForDataLauncher(activity, callback) ) } @@ -67,6 +76,7 @@ class FinancialConnectionsSheet internal constructor( callback: FinancialConnectionsSheetResultCallback ): FinancialConnectionsSheet { return FinancialConnectionsSheet( + fragment.requireActivity().application, FinancialConnectionsSheetForDataLauncher(fragment, callback) ) } @@ -83,6 +93,7 @@ class FinancialConnectionsSheet internal constructor( callback: (FinancialConnectionsSheetForTokenResult) -> Unit ): FinancialConnectionsSheet { return FinancialConnectionsSheet( + activity.application, FinancialConnectionsSheetForTokenLauncher(activity, callback) ) } @@ -99,6 +110,7 @@ class FinancialConnectionsSheet internal constructor( callback: (FinancialConnectionsSheetForTokenResult) -> Unit ): FinancialConnectionsSheet { return FinancialConnectionsSheet( + fragment.requireActivity().application, FinancialConnectionsSheetForTokenLauncher(fragment, callback) ) } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt index 0c9632568f4..b6752cf7d46 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt @@ -9,36 +9,29 @@ import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.InternalMavericksApi +import com.airbnb.mvrx.Mavericks +import com.airbnb.mvrx.MavericksView +import com.airbnb.mvrx.MavericksViewModelProvider +import com.airbnb.mvrx.asMavericksArgs +import com.airbnb.mvrx.viewModel import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl import com.stripe.android.financialconnections.databinding.ActivityFinancialconnectionsSheetBinding import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Canceled import com.stripe.android.financialconnections.presentation.CreateBrowserIntentForUrl import java.security.InvalidParameterException internal class FinancialConnectionsSheetActivity : AppCompatActivity() { - private val startForResult = registerForActivityResult(StartActivityForResult()) { - viewModel.onActivityResult() - } - @VisibleForTesting internal val viewBinding by lazy { ActivityFinancialconnectionsSheetBinding.inflate(layoutInflater) } - @VisibleForTesting - internal var viewModelFactory: ViewModelProvider.Factory = - FinancialConnectionsSheetViewModel.Factory( - { application }, - { requireNotNull(starterArgs) }, - this, - intent?.extras - ) - - private val viewModel: FinancialConnectionsSheetViewModel by viewModels { viewModelFactory } + val viewModel: FinancialConnectionsSheetViewModel by viewModel() private val starterArgs: FinancialConnectionsSheetActivityArgs? by lazy { FinancialConnectionsSheetActivityArgs.fromIntent(intent) @@ -47,63 +40,22 @@ internal class FinancialConnectionsSheetActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(viewBinding.root) - - val starterArgs = this.starterArgs - if (starterArgs == null) { - finishWithResult( - FinancialConnectionsSheetActivityResult.Failed( - IllegalArgumentException("ConnectionsSheet started without arguments.") - ) - ) - return - } else { - try { - starterArgs.validate() - } catch (e: InvalidParameterException) { - finishWithResult(FinancialConnectionsSheetActivityResult.Failed(e)) - return - } - } - - setupObservers() - if (savedInstanceState != null) viewModel.onActivityRecreated() + if (savedInstanceState == null) addFragment() } - private fun setupObservers() { - lifecycleScope.launchWhenStarted { - viewModel.state.collect { - // process state updates here. - } - } - lifecycleScope.launchWhenStarted { - viewModel.viewEffect.collect { viewEffect -> - when (viewEffect) { - is OpenAuthFlowWithUrl -> viewEffect.launch() - is FinishWithResult -> finishWithResult(viewEffect.result) - } - } - } - } - - private fun OpenAuthFlowWithUrl.launch() { - val uri = Uri.parse(this.url) - startForResult.launch( - CreateBrowserIntentForUrl( - context = this@FinancialConnectionsSheetActivity, - uri = uri, - ) - ) - } - - override fun onResume() { - super.onResume() - viewModel.onResume() + private fun addFragment() { + val fragment = FinancialConnectionsSheetFragment() + fragment.arguments = requireNotNull(starterArgs).asMavericksArgs() + supportFragmentManager.beginTransaction() + .setReorderingAllowed(true) + .add(R.id.nav_host_fragment, fragment, null) + .commit() } /** * Handles new intents in the form of the redirect from the custom tab hosted auth flow */ - public override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) viewModel.handleOnNewIntent(intent) } @@ -113,14 +65,7 @@ internal class FinancialConnectionsSheetActivity : AppCompatActivity() { * return canceled result */ override fun onBackPressed() { - finishWithResult(FinancialConnectionsSheetActivityResult.Canceled) - } - - private fun finishWithResult(result: FinancialConnectionsSheetActivityResult) { - setResult( - Activity.RESULT_OK, - Intent().putExtras(result.toBundle()) - ) + setResult(Activity.RESULT_OK, Intent().putExtras(Canceled.toBundle())) finish() } } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt new file mode 100644 index 00000000000..49ce750c0ec --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt @@ -0,0 +1,86 @@ +package com.stripe.android.financialconnections + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback +import androidx.fragment.app.Fragment +import androidx.activity.result.contract.ActivityResultContracts +import androidx.viewbinding.ViewBinding +import com.airbnb.mvrx.MavericksView +import com.airbnb.mvrx.UniqueOnly +import com.airbnb.mvrx.activityViewModel +import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult +import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl +import com.stripe.android.financialconnections.databinding.FragmentFinancialConnectionsSheetBinding +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult +import com.stripe.android.financialconnections.presentation.CreateBrowserIntentForUrl + +class FinancialConnectionsSheetFragment : Fragment(), MavericksView { + + private val startForResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + viewModel.onActivityResult() + } + + private val viewBinding: FragmentFinancialConnectionsSheetBinding by viewBinding() + private val viewModel: FinancialConnectionsSheetViewModel by activityViewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + //TODO validate args! + observeAsyncs() + if (savedInstanceState != null) viewModel.onActivityRecreated() + } + + private fun observeAsyncs() { + viewModel.onAsync( + asyncProp = FinancialConnectionsSheetState::sideEffect, + deliveryMode = UniqueOnly(subscriptionId = "financial-connections-view-effect"), + onSuccess = { viewEffect -> + when (viewEffect) { + is OpenAuthFlowWithUrl -> viewEffect.launch() + is FinishWithResult -> finishWithResult(viewEffect.result) + } + }, + ) + } + + override fun invalidate() = Unit + + private fun OpenAuthFlowWithUrl.launch() { + val uri = Uri.parse(this.url) + startForResult.launch( + CreateBrowserIntentForUrl( + context = requireContext(), + uri = uri, + ) + ) + } + + override fun onResume() { + super.onResume() + viewModel.onResume() + } + + private fun finishWithResult(result: FinancialConnectionsSheetActivityResult) { + requireActivity().setResult( + Activity.RESULT_OK, + Intent().putExtras(result.toBundle()) + ) + requireActivity().finish() + } +} + + +/** + * Create bindings for a view similar to bindView. + * + * To use, just call + * private val binding: FHomeWorkoutDetailsBinding by viewBinding() + * with your binding class and access it as you normally would. + */ +inline fun Fragment.viewBinding() = + FragmentViewBindingDelegate(T::class.java, this) \ No newline at end of file diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt index 7d68f4b304f..2f9c975bf98 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt @@ -1,6 +1,10 @@ package com.stripe.android.financialconnections import androidx.lifecycle.SavedStateHandle +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.Uninitialized +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetForDataContract import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest @@ -9,10 +13,16 @@ import com.stripe.android.financialconnections.model.FinancialConnectionsSession * Class containing all of the data needed to represent the screen. */ internal data class FinancialConnectionsSheetState( + val initialArgs: FinancialConnectionsSheetActivityArgs, val activityRecreated: Boolean = false, val manifest: FinancialConnectionsSessionManifest? = null, - val authFlowActive: Boolean = false -) { + val authFlowActive: Boolean = false, + val sideEffect: Async = Uninitialized +) : MavericksState { + + constructor(args: FinancialConnectionsSheetActivityArgs) : this( + initialArgs = args + ) /** * Restores existing persisted fields into the current [FinancialConnectionsSheetState] diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt index ead38e60f32..4156cf57462 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt @@ -1,13 +1,10 @@ package com.stripe.android.financialconnections -import android.app.Application import android.content.Intent -import android.os.Bundle -import androidx.lifecycle.AbstractSavedStateViewModelFactory -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.savedstate.SavedStateRegistryOwner +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl import com.stripe.android.financialconnections.analytics.FinancialConnectionsEventReporter @@ -18,13 +15,9 @@ import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsS import com.stripe.android.financialconnections.domain.GenerateFinancialConnectionsSessionManifest import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Canceled import com.stripe.android.financialconnections.model.FinancialConnectionsSession import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Named @@ -36,22 +29,17 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( private val generateFinancialConnectionsSessionManifest: GenerateFinancialConnectionsSessionManifest, private val fetchFinancialConnectionsSession: FetchFinancialConnectionsSession, private val fetchFinancialConnectionsSessionForToken: FetchFinancialConnectionsSessionForToken, - private val savedStateHandle: SavedStateHandle, - private val eventReporter: FinancialConnectionsEventReporter -) : ViewModel() { - - // on process recreation - restore saved fields from [SavedStateHandle]. - private val _state = MutableStateFlow(FinancialConnectionsSheetState().from(savedStateHandle)) - internal val state: StateFlow = _state - - private val _viewEffect = MutableSharedFlow() - internal val viewEffect: SharedFlow = _viewEffect + private val eventReporter: FinancialConnectionsEventReporter, + private val initialState: FinancialConnectionsSheetState +) : MavericksViewModel(initialState) { init { eventReporter.onPresented(starterArgs.configuration) - // avoid re-fetching manifest if already exists (this will happen on process recreations) - if (state.value.manifest == null) { - fetchManifest() + withState { + // avoid re-fetching manifest if already exists (this will happen on process recreations) + if (it.manifest == null) { + fetchManifest() + } } } @@ -82,13 +70,13 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( */ private suspend fun openAuthFlow(manifest: FinancialConnectionsSessionManifest) { // stores manifest in state for future references. - _state.updateAndPersist { - it.copy( + setState { + copy( manifest = manifest, - authFlowActive = true + authFlowActive = true, + sideEffect = Success(OpenAuthFlowWithUrl(manifest.hostedAuthUrl)) ) } - _viewEffect.emit(OpenAuthFlowWithUrl(manifest.hostedAuthUrl)) } /** @@ -108,7 +96,11 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * @see onActivityResult (we rely on this on config changes) */ internal fun onActivityRecreated() { - _state.updateAndPersist { it.copy(activityRecreated = true) } + setState { + copy( + activityRecreated = true + ) + } } /** @@ -117,10 +109,12 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * canceled. */ internal fun onResume() { - if (_state.value.authFlowActive && _state.value.activityRecreated.not()) { - viewModelScope.launch { - _viewEffect.emit(FinishWithResult(FinancialConnectionsSheetActivityResult.Canceled)) - } + setState { + if (authFlowActive && activityRecreated.not()) { + copy( + sideEffect = Success(FinishWithResult(Canceled)) + ) + } else this } } @@ -130,10 +124,12 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * the back button or closed the custom tabs UI, so return result as canceled. */ internal fun onActivityResult() { - if (_state.value.authFlowActive && _state.value.activityRecreated) { - viewModelScope.launch { - _viewEffect.emit(FinishWithResult(FinancialConnectionsSheetActivityResult.Canceled)) - } + setState { + if (authFlowActive && activityRecreated) { + copy( + sideEffect = Success(FinishWithResult(Canceled)) + ) + } else this } } @@ -151,7 +147,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( }.onSuccess { val result = FinancialConnectionsSheetActivityResult.Completed(it) eventReporter.onResult(starterArgs.configuration, result) - _viewEffect.emit(FinishWithResult(result)) + setState { copy(sideEffect = Success(FinishWithResult(result))) } }.onFailure { onFatal(it) } @@ -174,7 +170,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( }.onSuccess { (las, token) -> val result = FinancialConnectionsSheetActivityResult.Completed(las, token) eventReporter.onResult(starterArgs.configuration, result) - _viewEffect.emit(FinishWithResult(result)) + setState { copy(sideEffect = Success(FinishWithResult(result))) } }.onFailure { onFatal(it) } @@ -190,7 +186,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( private suspend fun onFatal(throwable: Throwable) { val result = FinancialConnectionsSheetActivityResult.Failed(throwable) eventReporter.onResult(starterArgs.configuration, result) - _viewEffect.emit(FinishWithResult(result)) + setState { copy(sideEffect = Success(FinishWithResult(result))) } } /** @@ -199,9 +195,9 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * URL callback, notify the [FinancialConnectionsSheetResultCallback] with [Canceled] */ private suspend fun onUserCancel() { - val result = FinancialConnectionsSheetActivityResult.Canceled + val result = Canceled eventReporter.onResult(starterArgs.configuration, result) - _viewEffect.emit(FinishWithResult(result)) + setState { copy(sideEffect = Success(FinishWithResult(result))) } } /** @@ -213,54 +209,33 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * @param intent the new intent with the redirect URL in the intent data */ internal fun handleOnNewIntent(intent: Intent?) { - _state.updateAndPersist { it.copy(authFlowActive = false) } - viewModelScope.launch { - val manifest = _state.value.manifest + setState { copy(authFlowActive = false) } + withState { when (intent?.data.toString()) { - manifest?.successUrl -> when (starterArgs) { + it.manifest?.successUrl -> when (starterArgs) { is FinancialConnectionsSheetActivityArgs.ForData -> fetchFinancialConnectionsSession() is FinancialConnectionsSheetActivityArgs.ForToken -> fetchFinancialConnectionsSessionForToken() } - manifest?.cancelUrl -> onUserCancel() - else -> onFatal(Exception("Error processing FinancialConnectionsSheet intent")) + it.manifest?.cancelUrl -> viewModelScope.launch { onUserCancel() } + else -> viewModelScope.launch { onFatal(Exception("Error processing FinancialConnectionsSheet intent")) } } } } - /** - * Updates state AND saves persistable fields into [SavedStateHandle] - */ - private inline fun MutableStateFlow.updateAndPersist( - function: (FinancialConnectionsSheetState) -> FinancialConnectionsSheetState - ) { - val previousValue = value - update(function) - value.to(savedStateHandle, previousValue) - } - - class Factory( - private val applicationSupplier: () -> Application, - private val starterArgsSupplier: () -> FinancialConnectionsSheetActivityArgs, - owner: SavedStateRegistryOwner, - defaultArgs: Bundle? = null - ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) { + companion object : MavericksViewModelFactory { - @Suppress("UNCHECKED_CAST") - override fun create( - key: String, - modelClass: Class, - savedStateHandle: SavedStateHandle - ): T { + override fun create( + viewModelContext: ViewModelContext, + state: FinancialConnectionsSheetState + ): FinancialConnectionsSheetViewModel { return DaggerFinancialConnectionsSheetComponent .builder() - .application(applicationSupplier()) - .savedStateHandle(savedStateHandle) - .internalArgs(starterArgsSupplier()) - .build().viewModel as T + .application(viewModelContext.app()) + .initialState(state) + .internalArgs(state.initialArgs) + .build().viewModel } - } - internal companion object { internal const val MAX_ACCOUNTS = 100 } } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FragmentViewBindingDelegate.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FragmentViewBindingDelegate.kt new file mode 100644 index 00000000000..a3f21dade98 --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FragmentViewBindingDelegate.kt @@ -0,0 +1,58 @@ +package com.stripe.android.financialconnections + +import android.os.Handler +import android.os.Looper +import android.view.View +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import androidx.lifecycle.lifecycleScope +import androidx.viewbinding.ViewBinding +import kotlinx.coroutines.launch +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +class FragmentViewBindingDelegate( + bindingClass: Class, + private val fragment: Fragment +) : ReadOnlyProperty { + private val clearBindingHandler by lazy(LazyThreadSafetyMode.NONE) { Handler(Looper.getMainLooper()) } + private var binding: T? = null + + private val bindMethod = bindingClass.getMethod("bind", View::class.java) + + init { + fragment.lifecycleScope.launch { + fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner -> + viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun onDestroy() { + // Lifecycle listeners are called before onDestroyView in a Fragment. + // However, we want views to be able to use bindings in onDestroyView + // to do cleanup so we clear the reference one frame later. + clearBindingHandler.post { binding = null } + } + }) + } + } + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): T { + // onCreateView may be called between onDestroyView and next Main thread cycle. + // In this case [binding] refers to the previous fragment view. Check that binding's root view matches current fragment view + if (binding != null && binding?.root !== thisRef.view) { + binding = null + } + binding?.let { return it } + + val lifecycle = fragment.viewLifecycleOwner.lifecycle + if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { + error("Cannot access view bindings. View lifecycle is ${lifecycle.currentState}!") + } + + @Suppress("UNCHECKED_CAST") + binding = bindMethod.invoke(null, thisRef.requireView()) as T + return binding!! + } +} \ No newline at end of file diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetComponent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetComponent.kt index ea7c52b8020..1e347efc5ad 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetComponent.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetComponent.kt @@ -4,6 +4,8 @@ import android.app.Application import androidx.lifecycle.SavedStateHandle import com.stripe.android.core.injection.CoroutineContextModule import com.stripe.android.core.injection.LoggingModule +import com.stripe.android.financialconnections.FinancialConnectionsSheet +import com.stripe.android.financialconnections.FinancialConnectionsSheetState import com.stripe.android.financialconnections.FinancialConnectionsSheetViewModel import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs import dagger.BindsInstance @@ -21,7 +23,7 @@ import javax.inject.Singleton internal interface FinancialConnectionsSheetComponent { val viewModel: FinancialConnectionsSheetViewModel - fun inject(factory: FinancialConnectionsSheetViewModel.Factory) + fun inject(factory: FinancialConnectionsSheetViewModel.Companion) @Component.Builder interface Builder { @@ -29,7 +31,7 @@ internal interface FinancialConnectionsSheetComponent { fun application(application: Application): Builder @BindsInstance - fun savedStateHandle(savedStateHandle: SavedStateHandle): Builder + fun initialState(initialState: FinancialConnectionsSheetState): Builder @BindsInstance fun internalArgs(financialConnectionsSheetActivityArgs: FinancialConnectionsSheetActivityArgs): Builder diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt index a19defdb507..1e439d835e8 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt @@ -3,6 +3,7 @@ package com.stripe.android.financialconnections.launcher import android.content.Context import android.content.Intent import androidx.activity.result.contract.ActivityResultContract +import com.airbnb.mvrx.Mavericks import com.stripe.android.financialconnections.FinancialConnectionsSheetActivity import com.stripe.android.financialconnections.FinancialConnectionsSheetResult import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.Companion.EXTRA_ARGS @@ -15,10 +16,9 @@ internal class FinancialConnectionsSheetForDataContract : context: Context, input: FinancialConnectionsSheetActivityArgs.ForData ): Intent { - return Intent(context, FinancialConnectionsSheetActivity::class.java).putExtra( - EXTRA_ARGS, - input - ) + return Intent(context, FinancialConnectionsSheetActivity::class.java) + .putExtra(EXTRA_ARGS, input) + .putExtra(Mavericks.KEY_ARG, input) } override fun parseResult( diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetTest.kt index 96f6e0090bb..ae392208eb5 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetTest.kt @@ -12,7 +12,7 @@ class FinancialConnectionsSheetTest { ApiKeyFixtures.DEFAULT_PUBLISHABLE_KEY ) private val financialConnectionsSheet = - FinancialConnectionsSheet(financialConnectionsSheetLauncher) + FinancialConnectionsSheet(activity.application, financialConnectionsSheetLauncher) @Test fun `present() should launch the connection sheet with the given configuration`() { From 1330e6d47da1ae6d366c903a94d718d0d34ff65e Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Mon, 9 May 2022 19:53:40 -0700 Subject: [PATCH 02/19] Removes viewBinding. --- financial-connections/build.gradle | 8 --- .../activity_financialconnections_sheet.xml | 13 +---- .../fragment_financial_connections_sheet.xml | 5 +- .../FinancialConnectionsSheetActivity.kt | 26 +-------- .../FinancialConnectionsSheetFragment.kt | 25 +++----- .../FinancialConnectionsSheetState.kt | 39 +++---------- .../FinancialConnectionsSheetViewModel.kt | 22 +++---- .../FragmentViewBindingDelegate.kt | 58 ------------------- .../FinancialConnectionsSheetActivityArgs.kt | 6 +- ...inancialConnectionsSheetForDataContract.kt | 2 - ...nancialConnectionsSheetForTokenContract.kt | 8 +-- 11 files changed, 37 insertions(+), 175 deletions(-) delete mode 100644 financial-connections/src/main/java/com/stripe/android/financialconnections/FragmentViewBindingDelegate.kt diff --git a/financial-connections/build.gradle b/financial-connections/build.gradle index 343a56f3d22..016ec3aa4b9 100644 --- a/financial-connections/build.gradle +++ b/financial-connections/build.gradle @@ -57,13 +57,6 @@ dependencies { ktlint "com.pinterest:ktlint:$ktlintVersion" } -android { - buildFeatures { - viewBinding true - } -} - - ext { artifactId = "financial-connections" artifactName = "financial-connections" @@ -71,4 +64,3 @@ ext { } apply from: "${rootDir}/deploy/deploy.gradle" -apply plugin: 'org.jetbrains.kotlin.android' diff --git a/financial-connections/res/layout/activity_financialconnections_sheet.xml b/financial-connections/res/layout/activity_financialconnections_sheet.xml index e6365334b91..f89928058db 100644 --- a/financial-connections/res/layout/activity_financialconnections_sheet.xml +++ b/financial-connections/res/layout/activity_financialconnections_sheet.xml @@ -1,15 +1,8 @@ - - - - - \ No newline at end of file + tools:context=".FinancialConnectionsSheetActivity" /> \ No newline at end of file diff --git a/financial-connections/res/layout/fragment_financial_connections_sheet.xml b/financial-connections/res/layout/fragment_financial_connections_sheet.xml index 60257da7ec6..a5a7ca05163 100644 --- a/financial-connections/res/layout/fragment_financial_connections_sheet.xml +++ b/financial-connections/res/layout/fragment_financial_connections_sheet.xml @@ -1,5 +1,5 @@ - @@ -8,6 +8,7 @@ android:id="@+id/spinner" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_gravity="center" android:indeterminate="true" app:indicatorColor="@color/stripe_toolbar_color_default" app:indicatorSize="@dimen/stripe_connectionssheet_loading_indicator_size" @@ -17,4 +18,4 @@ app:layout_constraintTop_toTopOf="parent" app:trackThickness="@dimen/stripe_connectionssheet_loading_indicator_stroke_width" /> - + diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt index b6752cf7d46..fc68fe53fdf 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt @@ -2,36 +2,17 @@ package com.stripe.android.financialconnections import android.app.Activity import android.content.Intent -import android.net.Uri import android.os.Bundle -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.activity.viewModels -import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModelProvider -import com.airbnb.mvrx.ActivityViewModelContext -import com.airbnb.mvrx.InternalMavericksApi -import com.airbnb.mvrx.Mavericks -import com.airbnb.mvrx.MavericksView -import com.airbnb.mvrx.MavericksViewModelProvider import com.airbnb.mvrx.asMavericksArgs import com.airbnb.mvrx.viewModel -import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl -import com.stripe.android.financialconnections.databinding.ActivityFinancialconnectionsSheetBinding import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs -import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Canceled -import com.stripe.android.financialconnections.presentation.CreateBrowserIntentForUrl -import java.security.InvalidParameterException -internal class FinancialConnectionsSheetActivity : AppCompatActivity() { +internal class FinancialConnectionsSheetActivity : + AppCompatActivity(R.layout.activity_financialconnections_sheet) { - @VisibleForTesting - internal val viewBinding by lazy { - ActivityFinancialconnectionsSheetBinding.inflate(layoutInflater) - } - - val viewModel: FinancialConnectionsSheetViewModel by viewModel() + val viewModel: FinancialConnectionsSheetViewModel by viewModel() private val starterArgs: FinancialConnectionsSheetActivityArgs? by lazy { FinancialConnectionsSheetActivityArgs.fromIntent(intent) @@ -39,7 +20,6 @@ internal class FinancialConnectionsSheetActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(viewBinding.root) if (savedInstanceState == null) addFragment() } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt index 49ce750c0ec..dde944d3f3b 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt @@ -4,28 +4,24 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle -import androidx.activity.OnBackPressedCallback -import androidx.activity.addCallback -import androidx.fragment.app.Fragment import androidx.activity.result.contract.ActivityResultContracts -import androidx.viewbinding.ViewBinding +import androidx.fragment.app.Fragment import com.airbnb.mvrx.MavericksView import com.airbnb.mvrx.UniqueOnly import com.airbnb.mvrx.activityViewModel import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl -import com.stripe.android.financialconnections.databinding.FragmentFinancialConnectionsSheetBinding import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult import com.stripe.android.financialconnections.presentation.CreateBrowserIntentForUrl -class FinancialConnectionsSheetFragment : Fragment(), MavericksView { +class FinancialConnectionsSheetFragment : + Fragment(R.layout.fragment_financial_connections_sheet), MavericksView { private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { viewModel.onActivityResult() } - private val viewBinding: FragmentFinancialConnectionsSheetBinding by viewBinding() private val viewModel: FinancialConnectionsSheetViewModel by activityViewModel() override fun onCreate(savedInstanceState: Bundle?) { @@ -37,7 +33,7 @@ class FinancialConnectionsSheetFragment : Fragment(), MavericksView { private fun observeAsyncs() { viewModel.onAsync( - asyncProp = FinancialConnectionsSheetState::sideEffect, + asyncProp = FinancialConnectionsSheetState::viewEffect, deliveryMode = UniqueOnly(subscriptionId = "financial-connections-view-effect"), onSuccess = { viewEffect -> when (viewEffect) { @@ -48,6 +44,9 @@ class FinancialConnectionsSheetFragment : Fragment(), MavericksView { ) } + /** + * handle state changes here. + */ override fun invalidate() = Unit private fun OpenAuthFlowWithUrl.launch() { @@ -74,13 +73,3 @@ class FinancialConnectionsSheetFragment : Fragment(), MavericksView { } } - -/** - * Create bindings for a view similar to bindView. - * - * To use, just call - * private val binding: FHomeWorkoutDetailsBinding by viewBinding() - * with your binding class and access it as you normally would. - */ -inline fun Fragment.viewBinding() = - FragmentViewBindingDelegate(T::class.java, this) \ No newline at end of file diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt index 2f9c975bf98..40ad0b691bc 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt @@ -1,8 +1,8 @@ package com.stripe.android.financialconnections -import androidx.lifecycle.SavedStateHandle import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.PersistState import com.airbnb.mvrx.Uninitialized import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult @@ -15,42 +15,17 @@ import com.stripe.android.financialconnections.model.FinancialConnectionsSession internal data class FinancialConnectionsSheetState( val initialArgs: FinancialConnectionsSheetActivityArgs, val activityRecreated: Boolean = false, - val manifest: FinancialConnectionsSessionManifest? = null, - val authFlowActive: Boolean = false, - val sideEffect: Async = Uninitialized + @PersistState val manifest: FinancialConnectionsSessionManifest? = null, + @PersistState val authFlowActive: Boolean = false, + val viewEffect: Async = Uninitialized ) : MavericksState { + /** + * Constructor used by Mavericks to build the initial state. + */ constructor(args: FinancialConnectionsSheetActivityArgs) : this( initialArgs = args ) - - /** - * Restores existing persisted fields into the current [FinancialConnectionsSheetState] - */ - internal fun from(savedStateHandle: SavedStateHandle): FinancialConnectionsSheetState { - return copy( - manifest = savedStateHandle.get(KEY_MANIFEST) ?: manifest, - authFlowActive = savedStateHandle.get(KEY_AUTHFLOW_ACTIVE) ?: authFlowActive, - ) - } - - /** - * Saves the persistable fields of this state that changed to the given [SavedStateHandle] - */ - internal fun to( - savedStateHandle: SavedStateHandle, - previousValue: FinancialConnectionsSheetState - ) { - if (previousValue.manifest != manifest) - savedStateHandle.set(KEY_MANIFEST, manifest) - if (previousValue.authFlowActive != authFlowActive) - savedStateHandle.set(KEY_AUTHFLOW_ACTIVE, authFlowActive) - } - - companion object { - private const val KEY_MANIFEST = "key_manifest" - private const val KEY_AUTHFLOW_ACTIVE = "key_authflow_active" - } } /** diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt index 4156cf57462..5f764e70fca 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt @@ -30,7 +30,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( private val fetchFinancialConnectionsSession: FetchFinancialConnectionsSession, private val fetchFinancialConnectionsSessionForToken: FetchFinancialConnectionsSessionForToken, private val eventReporter: FinancialConnectionsEventReporter, - private val initialState: FinancialConnectionsSheetState + initialState: FinancialConnectionsSheetState ) : MavericksViewModel(initialState) { init { @@ -68,13 +68,13 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * @param manifest the manifest containing the hosted auth flow URL to launch * */ - private suspend fun openAuthFlow(manifest: FinancialConnectionsSessionManifest) { + private fun openAuthFlow(manifest: FinancialConnectionsSessionManifest) { // stores manifest in state for future references. setState { copy( manifest = manifest, authFlowActive = true, - sideEffect = Success(OpenAuthFlowWithUrl(manifest.hostedAuthUrl)) + viewEffect = Success(OpenAuthFlowWithUrl(manifest.hostedAuthUrl)) ) } } @@ -111,9 +111,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( internal fun onResume() { setState { if (authFlowActive && activityRecreated.not()) { - copy( - sideEffect = Success(FinishWithResult(Canceled)) - ) + copy(viewEffect = Success(FinishWithResult(Canceled))) } else this } } @@ -126,9 +124,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( internal fun onActivityResult() { setState { if (authFlowActive && activityRecreated) { - copy( - sideEffect = Success(FinishWithResult(Canceled)) - ) + copy(viewEffect = Success(FinishWithResult(Canceled))) } else this } } @@ -147,7 +143,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( }.onSuccess { val result = FinancialConnectionsSheetActivityResult.Completed(it) eventReporter.onResult(starterArgs.configuration, result) - setState { copy(sideEffect = Success(FinishWithResult(result))) } + setState { copy(viewEffect = Success(FinishWithResult(result))) } }.onFailure { onFatal(it) } @@ -170,7 +166,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( }.onSuccess { (las, token) -> val result = FinancialConnectionsSheetActivityResult.Completed(las, token) eventReporter.onResult(starterArgs.configuration, result) - setState { copy(sideEffect = Success(FinishWithResult(result))) } + setState { copy(viewEffect = Success(FinishWithResult(result))) } }.onFailure { onFatal(it) } @@ -186,7 +182,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( private suspend fun onFatal(throwable: Throwable) { val result = FinancialConnectionsSheetActivityResult.Failed(throwable) eventReporter.onResult(starterArgs.configuration, result) - setState { copy(sideEffect = Success(FinishWithResult(result))) } + setState { copy(viewEffect = Success(FinishWithResult(result))) } } /** @@ -197,7 +193,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( private suspend fun onUserCancel() { val result = Canceled eventReporter.onResult(starterArgs.configuration, result) - setState { copy(sideEffect = Success(FinishWithResult(result))) } + setState { copy(viewEffect = Success(FinishWithResult(result))) } } /** diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FragmentViewBindingDelegate.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FragmentViewBindingDelegate.kt deleted file mode 100644 index a3f21dade98..00000000000 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FragmentViewBindingDelegate.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.stripe.android.financialconnections - -import android.os.Handler -import android.os.Looper -import android.view.View -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent -import androidx.lifecycle.lifecycleScope -import androidx.viewbinding.ViewBinding -import kotlinx.coroutines.launch -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - -class FragmentViewBindingDelegate( - bindingClass: Class, - private val fragment: Fragment -) : ReadOnlyProperty { - private val clearBindingHandler by lazy(LazyThreadSafetyMode.NONE) { Handler(Looper.getMainLooper()) } - private var binding: T? = null - - private val bindMethod = bindingClass.getMethod("bind", View::class.java) - - init { - fragment.lifecycleScope.launch { - fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner -> - viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver { - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - fun onDestroy() { - // Lifecycle listeners are called before onDestroyView in a Fragment. - // However, we want views to be able to use bindings in onDestroyView - // to do cleanup so we clear the reference one frame later. - clearBindingHandler.post { binding = null } - } - }) - } - } - } - - override fun getValue(thisRef: Fragment, property: KProperty<*>): T { - // onCreateView may be called between onDestroyView and next Main thread cycle. - // In this case [binding] refers to the previous fragment view. Check that binding's root view matches current fragment view - if (binding != null && binding?.root !== thisRef.view) { - binding = null - } - binding?.let { return it } - - val lifecycle = fragment.viewLifecycleOwner.lifecycle - if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { - error("Cannot access view bindings. View lifecycle is ${lifecycle.currentState}!") - } - - @Suppress("UNCHECKED_CAST") - binding = bindMethod.invoke(null, thisRef.requireView()) as T - return binding!! - } -} \ No newline at end of file diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt index 75418e77d4a..e51ae095d42 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs.kt @@ -2,6 +2,7 @@ package com.stripe.android.financialconnections.launcher import android.content.Intent import android.os.Parcelable +import com.airbnb.mvrx.Mavericks import com.stripe.android.financialconnections.FinancialConnectionsSheet import kotlinx.parcelize.Parcelize import java.security.InvalidParameterException @@ -39,11 +40,8 @@ internal sealed class FinancialConnectionsSheetActivityArgs constructor( } companion object { - const val EXTRA_ARGS = - "com.stripe.android.financialconnections.ConnectionsSheetContract.extra_args" - internal fun fromIntent(intent: Intent): FinancialConnectionsSheetActivityArgs? { - return intent.getParcelableExtra(EXTRA_ARGS) + return intent.getParcelableExtra(Mavericks.KEY_ARG) } } } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt index 1e439d835e8..ab239c1c710 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForDataContract.kt @@ -6,7 +6,6 @@ import androidx.activity.result.contract.ActivityResultContract import com.airbnb.mvrx.Mavericks import com.stripe.android.financialconnections.FinancialConnectionsSheetActivity import com.stripe.android.financialconnections.FinancialConnectionsSheetResult -import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.Companion.EXTRA_ARGS import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Companion.EXTRA_RESULT internal class FinancialConnectionsSheetForDataContract : @@ -17,7 +16,6 @@ internal class FinancialConnectionsSheetForDataContract : input: FinancialConnectionsSheetActivityArgs.ForData ): Intent { return Intent(context, FinancialConnectionsSheetActivity::class.java) - .putExtra(EXTRA_ARGS, input) .putExtra(Mavericks.KEY_ARG, input) } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForTokenContract.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForTokenContract.kt index 5f3d3ed3830..f9afdf5a39e 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForTokenContract.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/launcher/FinancialConnectionsSheetForTokenContract.kt @@ -3,9 +3,9 @@ package com.stripe.android.financialconnections.launcher import android.content.Context import android.content.Intent import androidx.activity.result.contract.ActivityResultContract +import com.airbnb.mvrx.Mavericks import com.stripe.android.financialconnections.FinancialConnectionsSheetActivity import com.stripe.android.financialconnections.FinancialConnectionsSheetForTokenResult -import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs.Companion.EXTRA_ARGS import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Companion.EXTRA_RESULT internal class FinancialConnectionsSheetForTokenContract : @@ -15,10 +15,8 @@ internal class FinancialConnectionsSheetForTokenContract : context: Context, input: FinancialConnectionsSheetActivityArgs.ForToken ): Intent { - return Intent(context, FinancialConnectionsSheetActivity::class.java).putExtra( - EXTRA_ARGS, - input - ) + return Intent(context, FinancialConnectionsSheetActivity::class.java) + .putExtra(Mavericks.KEY_ARG, input) } override fun parseResult( From b09df889a68b388621e186ae31edfb7f939c5ec0 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Mon, 9 May 2022 20:08:16 -0700 Subject: [PATCH 03/19] Move args validation to manifest fetching. --- .../FinancialConnectionsSheetViewModel.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt index 5f764e70fca..a22c4f10dbe 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt @@ -48,18 +48,22 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * as well as the success and cancel callback URLs to verify. */ private fun fetchManifest() { - viewModelScope.launch { - kotlin.runCatching { - generateFinancialConnectionsSessionManifest( - clientSecret = starterArgs.configuration.financialConnectionsSessionClientSecret, - applicationId = applicationId - ) - }.onFailure { - onFatal(it) - }.onSuccess { - openAuthFlow(it) + withState { state -> + viewModelScope.launch { + kotlin.runCatching { + state.initialArgs.validate() + generateFinancialConnectionsSessionManifest( + clientSecret = starterArgs.configuration.financialConnectionsSessionClientSecret, + applicationId = applicationId + ) + }.onFailure { + onFatal(it) + }.onSuccess { + openAuthFlow(it) + } } } + } /** From 8cfa170e67775c0dcb5c4d725b022ed017a6e16f Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Tue, 10 May 2022 09:46:31 -0700 Subject: [PATCH 04/19] Makes viewEffect a nullable prop and handles it in invalidate. --- financial-connections-example/build.gradle | 1 - .../FinancialConnectionsSheetFragment.kt | 44 +++++++------------ .../FinancialConnectionsSheetState.kt | 2 +- .../FinancialConnectionsSheetViewModel.kt | 40 +++++++++-------- 4 files changed, 39 insertions(+), 48 deletions(-) diff --git a/financial-connections-example/build.gradle b/financial-connections-example/build.gradle index deaa47d4acd..3d4f8ec6bca 100644 --- a/financial-connections-example/build.gradle +++ b/financial-connections-example/build.gradle @@ -47,7 +47,6 @@ dependencies { implementation "androidx.activity:activity-ktx:$androidxActivityVersion" implementation "androidx.appcompat:appcompat:$androidxAppcompatVersion" - implementation "androidx.constraintlayout:constraintlayout:$androidxConstraintlayoutVersion" implementation "androidx.core:core-ktx:$androidxCoreVersion" implementation "com.google.android.material:material:$materialVersion" implementation 'com.google.code.gson:gson:2.9.0' diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt index dde944d3f3b..2f958c7c17b 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt @@ -3,12 +3,11 @@ package com.stripe.android.financialconnections import android.app.Activity import android.content.Intent import android.net.Uri -import android.os.Bundle -import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.fragment.app.Fragment import com.airbnb.mvrx.MavericksView -import com.airbnb.mvrx.UniqueOnly import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult @@ -17,37 +16,26 @@ import com.stripe.android.financialconnections.presentation.CreateBrowserIntentF class FinancialConnectionsSheetFragment : Fragment(R.layout.fragment_financial_connections_sheet), MavericksView { - private val startForResult = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - viewModel.onActivityResult() - } - - private val viewModel: FinancialConnectionsSheetViewModel by activityViewModel() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - //TODO validate args! - observeAsyncs() - if (savedInstanceState != null) viewModel.onActivityRecreated() + private val startForResult = registerForActivityResult(StartActivityForResult()) { + viewModel.onActivityResult() } - private fun observeAsyncs() { - viewModel.onAsync( - asyncProp = FinancialConnectionsSheetState::viewEffect, - deliveryMode = UniqueOnly(subscriptionId = "financial-connections-view-effect"), - onSuccess = { viewEffect -> - when (viewEffect) { - is OpenAuthFlowWithUrl -> viewEffect.launch() - is FinishWithResult -> finishWithResult(viewEffect.result) - } - }, - ) - } + private val viewModel: FinancialConnectionsSheetViewModel by activityViewModel() /** * handle state changes here. */ - override fun invalidate() = Unit + override fun invalidate() { + withState(viewModel) { state -> + if (state.viewEffect != null) { + when (state.viewEffect) { + is OpenAuthFlowWithUrl -> state.viewEffect.launch() + is FinishWithResult -> finishWithResult(state.viewEffect.result) + } + viewModel.onViewEffectLaunched() + } + } + } private fun OpenAuthFlowWithUrl.launch() { val uri = Uri.parse(this.url) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt index 40ad0b691bc..e24fa944ea1 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt @@ -17,7 +17,7 @@ internal data class FinancialConnectionsSheetState( val activityRecreated: Boolean = false, @PersistState val manifest: FinancialConnectionsSessionManifest? = null, @PersistState val authFlowActive: Boolean = false, - val viewEffect: Async = Uninitialized + val viewEffect: FinancialConnectionsSheetViewEffect? = null ) : MavericksState { /** diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt index a22c4f10dbe..50c34fdf147 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt @@ -35,11 +35,10 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( init { eventReporter.onPresented(starterArgs.configuration) - withState { - // avoid re-fetching manifest if already exists (this will happen on process recreations) - if (it.manifest == null) { - fetchManifest() - } + // avoid re-fetching manifest if already exists (this will happen on process recreations) + if (initialState.manifest == null) { + onActivityRecreated() + fetchManifest() } } @@ -78,7 +77,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( copy( manifest = manifest, authFlowActive = true, - viewEffect = Success(OpenAuthFlowWithUrl(manifest.hostedAuthUrl)) + viewEffect = OpenAuthFlowWithUrl(manifest.hostedAuthUrl) ) } } @@ -99,7 +98,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * @see onResume (we rely on this on regular flows) * @see onActivityResult (we rely on this on config changes) */ - internal fun onActivityRecreated() { + private fun onActivityRecreated() { setState { copy( activityRecreated = true @@ -115,7 +114,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( internal fun onResume() { setState { if (authFlowActive && activityRecreated.not()) { - copy(viewEffect = Success(FinishWithResult(Canceled))) + copy(viewEffect = FinishWithResult(Canceled)) } else this } } @@ -128,7 +127,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( internal fun onActivityResult() { setState { if (authFlowActive && activityRecreated) { - copy(viewEffect = Success(FinishWithResult(Canceled))) + copy(viewEffect = FinishWithResult(Canceled)) } else this } } @@ -147,7 +146,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( }.onSuccess { val result = FinancialConnectionsSheetActivityResult.Completed(it) eventReporter.onResult(starterArgs.configuration, result) - setState { copy(viewEffect = Success(FinishWithResult(result))) } + setState { copy(viewEffect = FinishWithResult(result)) } }.onFailure { onFatal(it) } @@ -170,7 +169,7 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( }.onSuccess { (las, token) -> val result = FinancialConnectionsSheetActivityResult.Completed(las, token) eventReporter.onResult(starterArgs.configuration, result) - setState { copy(viewEffect = Success(FinishWithResult(result))) } + setState { copy(viewEffect = FinishWithResult(result)) } }.onFailure { onFatal(it) } @@ -183,10 +182,10 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * * @param throwable the error encountered during the [FinancialConnectionsSheet] auth flow */ - private suspend fun onFatal(throwable: Throwable) { + private fun onFatal(throwable: Throwable) { val result = FinancialConnectionsSheetActivityResult.Failed(throwable) eventReporter.onResult(starterArgs.configuration, result) - setState { copy(viewEffect = Success(FinishWithResult(result))) } + setState { copy(viewEffect = FinishWithResult(result)) } } /** @@ -194,10 +193,10 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * or clicking a cancel link within the hosted auth flow and the activity received the canceled * URL callback, notify the [FinancialConnectionsSheetResultCallback] with [Canceled] */ - private suspend fun onUserCancel() { + private fun onUserCancel() { val result = Canceled eventReporter.onResult(starterArgs.configuration, result) - setState { copy(viewEffect = Success(FinishWithResult(result))) } + setState { copy(viewEffect = FinishWithResult(result)) } } /** @@ -216,13 +215,18 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( is FinancialConnectionsSheetActivityArgs.ForData -> fetchFinancialConnectionsSession() is FinancialConnectionsSheetActivityArgs.ForToken -> fetchFinancialConnectionsSessionForToken() } - it.manifest?.cancelUrl -> viewModelScope.launch { onUserCancel() } - else -> viewModelScope.launch { onFatal(Exception("Error processing FinancialConnectionsSheet intent")) } + it.manifest?.cancelUrl -> onUserCancel() + else -> onFatal(Exception("Error processing FinancialConnectionsSheet intent")) } } } - companion object : MavericksViewModelFactory { + fun onViewEffectLaunched() { + setState { copy(viewEffect = null) } + } + + companion object : + MavericksViewModelFactory { override fun create( viewModelContext: ViewModelContext, From 6e079b309da762b7c0a63df02017dca782229d89 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Tue, 10 May 2022 17:43:21 -0700 Subject: [PATCH 05/19] Removes redundant activity tests. --- dependencies.gradle | 8 +- .../api/financial-connections.api | 17 +- financial-connections/build.gradle | 6 +- .../FinancialConnectionsSheet.kt | 8 +- .../FinancialConnectionsSheetActivity.kt | 19 +- .../FinancialConnectionsSheetFragment.kt | 2 +- .../FinancialConnectionsSheetState.kt | 3 + .../FinancialConnectionsSheetViewModel.kt | 52 ++-- .../FinancialConnectionsSheetActivityTest.kt | 161 ------------- .../FinancialConnectionsSheetTest.kt | 2 +- .../FinancialConnectionsSheetViewModelTest.kt | 226 +++++++++--------- 11 files changed, 172 insertions(+), 332 deletions(-) delete mode 100644 financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivityTest.kt diff --git a/dependencies.gradle b/dependencies.gradle index 14b8236f1f6..57aa54a10f8 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,5 +1,6 @@ ext.versions = [ detekt: "1.19.0", + mavericks: "2.6.1" ] ext.buildLibs = [ @@ -10,6 +11,11 @@ ext.configs = [ androidLibrary: "${project.rootDir}/build-configuration/android-library.gradle", ] +ext.libs = [ + mavericks: "com.airbnb.android:mavericks:$versions.mavericks" +] + ext.testLibs = [ - turbine: "app.cash.turbine:turbine:0.7.0" + turbine: "app.cash.turbine:turbine:0.7.0", + mavericks: "com.airbnb.android:mavericks-testing:$versions.mavericks" ] diff --git a/financial-connections/api/financial-connections.api b/financial-connections/api/financial-connections.api index 00a0ac16ab0..60f9e0b6021 100644 --- a/financial-connections/api/financial-connections.api +++ b/financial-connections/api/financial-connections.api @@ -118,11 +118,11 @@ public abstract interface class com/stripe/android/financialconnections/Financia } public final class com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel_Factory : dagger/internal/Factory { - public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V - public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel_Factory; + public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel_Factory; public fun get ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel; public synthetic fun get ()Ljava/lang/Object; - public static fun newInstance (Ljava/lang/String;Lcom/stripe/android/financialconnections/launcher/FinancialConnectionsSheetActivityArgs;Lcom/stripe/android/financialconnections/domain/GenerateFinancialConnectionsSessionManifest;Lcom/stripe/android/financialconnections/domain/FetchFinancialConnectionsSession;Lcom/stripe/android/financialconnections/domain/FetchFinancialConnectionsSessionForToken;Landroidx/lifecycle/SavedStateHandle;Lcom/stripe/android/financialconnections/analytics/FinancialConnectionsEventReporter;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel; + public static fun newInstance (Ljava/lang/String;Lcom/stripe/android/financialconnections/domain/GenerateFinancialConnectionsSessionManifest;Lcom/stripe/android/financialconnections/domain/FetchFinancialConnectionsSession;Lcom/stripe/android/financialconnections/domain/FetchFinancialConnectionsSessionForToken;Lcom/stripe/android/financialconnections/analytics/FinancialConnectionsEventReporter;Lcom/stripe/android/financialconnections/FinancialConnectionsSheetState;)Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel; } public final class com/stripe/android/financialconnections/analytics/DefaultFinancialConnectionsEventReporter_Factory : dagger/internal/Factory { @@ -133,19 +133,10 @@ public final class com/stripe/android/financialconnections/analytics/DefaultFina public static fun newInstance (Lcom/stripe/android/core/networking/AnalyticsRequestExecutor;Lcom/stripe/android/core/networking/AnalyticsRequestFactory;Lkotlin/coroutines/CoroutineContext;)Lcom/stripe/android/financialconnections/analytics/DefaultFinancialConnectionsEventReporter; } -public final class com/stripe/android/financialconnections/databinding/ActivityFinancialconnectionsSheetBinding : androidx/viewbinding/ViewBinding { - public final field spinner Lcom/google/android/material/progressindicator/CircularProgressIndicator; - public static fun bind (Landroid/view/View;)Lcom/stripe/android/financialconnections/databinding/ActivityFinancialconnectionsSheetBinding; - public synthetic fun getRoot ()Landroid/view/View; - public fun getRoot ()Landroidx/constraintlayout/widget/ConstraintLayout; - public static fun inflate (Landroid/view/LayoutInflater;)Lcom/stripe/android/financialconnections/databinding/ActivityFinancialconnectionsSheetBinding; - public static fun inflate (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Z)Lcom/stripe/android/financialconnections/databinding/ActivityFinancialconnectionsSheetBinding; -} - public final class com/stripe/android/financialconnections/di/DaggerFinancialConnectionsSheetComponent : com/stripe/android/financialconnections/di/FinancialConnectionsSheetComponent { public static fun builder ()Lcom/stripe/android/financialconnections/di/FinancialConnectionsSheetComponent$Builder; public fun getViewModel ()Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel; - public fun inject (Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel$Factory;)V + public fun inject (Lcom/stripe/android/financialconnections/FinancialConnectionsSheetViewModel$Companion;)V } public final class com/stripe/android/financialconnections/di/FinancialConnectionsSheetConfigurationModule_ProvidesApplicationIdFactory : dagger/internal/Factory { diff --git a/financial-connections/build.gradle b/financial-connections/build.gradle index 016ec3aa4b9..15fdb623f51 100644 --- a/financial-connections/build.gradle +++ b/financial-connections/build.gradle @@ -24,16 +24,17 @@ dependencies { implementation "androidx.constraintlayout:constraintlayout:$androidxConstraintlayoutVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidxLifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$androidxLifecycleVersion" - implementation 'com.airbnb.android:mavericks:2.6.1' implementation "com.google.android.material:material:$materialVersion" implementation "com.google.dagger:dagger:$daggerVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion" + implementation libs.mavericks + + debugImplementation "androidx.fragment:fragment-testing:1.4.1" kapt "com.google.dagger:dagger-compiler:$daggerVersion" - testImplementation 'app.cash.turbine:turbine:0.7.0' testImplementation "androidx.arch.core:core-testing:$androidxArchCoreVersion" testImplementation "androidx.fragment:fragment-testing:$androidxFragmentVersion" testImplementation "androidx.test.ext:junit-ktx:$androidTestJunitVersion" @@ -48,6 +49,7 @@ dependencies { testImplementation "org.mockito:mockito-core:$mockitoCoreVersion" testImplementation "org.mockito:mockito-inline:$mockitoCoreVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" + testImplementation testLibs.mavericks androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation "androidx.test:rules:$androidTestVersion" diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt index cb86ed165a5..e03dade835c 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt @@ -1,6 +1,5 @@ package com.stripe.android.financialconnections -import android.app.Application import android.os.Parcelable import androidx.activity.ComponentActivity import androidx.fragment.app.Fragment @@ -17,12 +16,11 @@ import kotlinx.parcelize.Parcelize * typically as a field initializer of an Activity or Fragment. */ class FinancialConnectionsSheet internal constructor( - app: Application, private val financialConnectionsSheetLauncher: FinancialConnectionsSheetLauncher ) { init { - Mavericks.initialize(app) + Mavericks.initialize(debugMode = false) } /** @@ -60,7 +58,6 @@ class FinancialConnectionsSheet internal constructor( callback: FinancialConnectionsSheetResultCallback ): FinancialConnectionsSheet { return FinancialConnectionsSheet( - activity.application, FinancialConnectionsSheetForDataLauncher(activity, callback) ) } @@ -76,7 +73,6 @@ class FinancialConnectionsSheet internal constructor( callback: FinancialConnectionsSheetResultCallback ): FinancialConnectionsSheet { return FinancialConnectionsSheet( - fragment.requireActivity().application, FinancialConnectionsSheetForDataLauncher(fragment, callback) ) } @@ -93,7 +89,6 @@ class FinancialConnectionsSheet internal constructor( callback: (FinancialConnectionsSheetForTokenResult) -> Unit ): FinancialConnectionsSheet { return FinancialConnectionsSheet( - activity.application, FinancialConnectionsSheetForTokenLauncher(activity, callback) ) } @@ -110,7 +105,6 @@ class FinancialConnectionsSheet internal constructor( callback: (FinancialConnectionsSheetForTokenResult) -> Unit ): FinancialConnectionsSheet { return FinancialConnectionsSheet( - fragment.requireActivity().application, FinancialConnectionsSheetForTokenLauncher(fragment, callback) ) } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt index fc68fe53fdf..b9861af3582 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt @@ -20,16 +20,15 @@ internal class FinancialConnectionsSheetActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (savedInstanceState == null) addFragment() - } - - private fun addFragment() { - val fragment = FinancialConnectionsSheetFragment() - fragment.arguments = requireNotNull(starterArgs).asMavericksArgs() - supportFragmentManager.beginTransaction() - .setReorderingAllowed(true) - .add(R.id.nav_host_fragment, fragment, null) - .commit() + if (savedInstanceState == null) { + val fragment = FinancialConnectionsSheetFragment().apply { + arguments = requireNotNull(starterArgs).asMavericksArgs() + } + supportFragmentManager.beginTransaction() + .setReorderingAllowed(true) + .add(R.id.nav_host_fragment, fragment, null) + .commit() + } } /** diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt index 2f958c7c17b..c37364c4ec2 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt @@ -13,7 +13,7 @@ import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffe import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult import com.stripe.android.financialconnections.presentation.CreateBrowserIntentForUrl -class FinancialConnectionsSheetFragment : +internal class FinancialConnectionsSheetFragment : Fragment(R.layout.fragment_financial_connections_sheet), MavericksView { private val startForResult = registerForActivityResult(StartActivityForResult()) { diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt index e24fa944ea1..d5232b8e15d 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt @@ -20,6 +20,9 @@ internal data class FinancialConnectionsSheetState( val viewEffect: FinancialConnectionsSheetViewEffect? = null ) : MavericksState { + val sessionSecret: String + get() = initialArgs.configuration.financialConnectionsSessionClientSecret + /** * Constructor used by Mavericks to build the initial state. */ diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt index 50c34fdf147..d7a96a7d458 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt @@ -3,7 +3,6 @@ package com.stripe.android.financialconnections import android.content.Intent import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl @@ -25,7 +24,6 @@ import javax.inject.Named @Suppress("LongParameterList", "TooManyFunctions") internal class FinancialConnectionsSheetViewModel @Inject constructor( @Named(APPLICATION_ID) private val applicationId: String, - private val starterArgs: FinancialConnectionsSheetActivityArgs, private val generateFinancialConnectionsSessionManifest: GenerateFinancialConnectionsSessionManifest, private val fetchFinancialConnectionsSession: FetchFinancialConnectionsSession, private val fetchFinancialConnectionsSessionForToken: FetchFinancialConnectionsSessionForToken, @@ -34,11 +32,12 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( ) : MavericksViewModel(initialState) { init { - eventReporter.onPresented(starterArgs.configuration) + eventReporter.onPresented(initialState.initialArgs.configuration) // avoid re-fetching manifest if already exists (this will happen on process recreations) if (initialState.manifest == null) { - onActivityRecreated() fetchManifest() + } else { + onActivityRecreated() } } @@ -52,11 +51,11 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( kotlin.runCatching { state.initialArgs.validate() generateFinancialConnectionsSessionManifest( - clientSecret = starterArgs.configuration.financialConnectionsSessionClientSecret, + clientSecret = state.sessionSecret, applicationId = applicationId ) }.onFailure { - onFatal(it) + onFatal(state, it) }.onSuccess { openAuthFlow(it) } @@ -139,16 +138,16 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * fetch the updated [FinancialConnectionsSession] model from the API * and return it back as a [Completed] result. */ - private fun fetchFinancialConnectionsSession() { + private fun fetchFinancialConnectionsSession(state: FinancialConnectionsSheetState) { viewModelScope.launch { kotlin.runCatching { - fetchFinancialConnectionsSession(starterArgs.configuration.financialConnectionsSessionClientSecret) + fetchFinancialConnectionsSession(state.sessionSecret) }.onSuccess { val result = FinancialConnectionsSheetActivityResult.Completed(it) - eventReporter.onResult(starterArgs.configuration, result) + eventReporter.onResult(state.initialArgs.configuration, result) setState { copy(viewEffect = FinishWithResult(result)) } }.onFailure { - onFatal(it) + onFatal(state, it) } } } @@ -160,18 +159,16 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * fetch the updated [FinancialConnectionsSession] and the generated [com.stripe.android.model.Token] * and return it back as a [Completed] result. */ - private fun fetchFinancialConnectionsSessionForToken() { + private fun fetchFinancialConnectionsSessionForToken(state: FinancialConnectionsSheetState) { viewModelScope.launch { kotlin.runCatching { - fetchFinancialConnectionsSessionForToken( - clientSecret = starterArgs.configuration.financialConnectionsSessionClientSecret - ) + fetchFinancialConnectionsSessionForToken(clientSecret = state.sessionSecret) }.onSuccess { (las, token) -> val result = FinancialConnectionsSheetActivityResult.Completed(las, token) - eventReporter.onResult(starterArgs.configuration, result) + eventReporter.onResult(state.initialArgs.configuration, result) setState { copy(viewEffect = FinishWithResult(result)) } }.onFailure { - onFatal(it) + onFatal(state, it) } } } @@ -182,9 +179,9 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * * @param throwable the error encountered during the [FinancialConnectionsSheet] auth flow */ - private fun onFatal(throwable: Throwable) { + private fun onFatal(state: FinancialConnectionsSheetState, throwable: Throwable) { val result = FinancialConnectionsSheetActivityResult.Failed(throwable) - eventReporter.onResult(starterArgs.configuration, result) + eventReporter.onResult(state.initialArgs.configuration, result) setState { copy(viewEffect = FinishWithResult(result)) } } @@ -193,9 +190,9 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( * or clicking a cancel link within the hosted auth flow and the activity received the canceled * URL callback, notify the [FinancialConnectionsSheetResultCallback] with [Canceled] */ - private fun onUserCancel() { + private fun onUserCancel(state: FinancialConnectionsSheetState) { val result = Canceled - eventReporter.onResult(starterArgs.configuration, result) + eventReporter.onResult(state.initialArgs.configuration, result) setState { copy(viewEffect = FinishWithResult(result)) } } @@ -209,14 +206,17 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( */ internal fun handleOnNewIntent(intent: Intent?) { setState { copy(authFlowActive = false) } - withState { + withState { state -> when (intent?.data.toString()) { - it.manifest?.successUrl -> when (starterArgs) { - is FinancialConnectionsSheetActivityArgs.ForData -> fetchFinancialConnectionsSession() - is FinancialConnectionsSheetActivityArgs.ForToken -> fetchFinancialConnectionsSessionForToken() + state.manifest?.successUrl -> when (state.initialArgs) { + is FinancialConnectionsSheetActivityArgs.ForData -> + fetchFinancialConnectionsSession(state) + is FinancialConnectionsSheetActivityArgs.ForToken -> + fetchFinancialConnectionsSessionForToken(state) } - it.manifest?.cancelUrl -> onUserCancel() - else -> onFatal(Exception("Error processing FinancialConnectionsSheet intent")) + state.manifest?.cancelUrl -> onUserCancel(state) + else -> + onFatal(state, Exception("Error processing FinancialConnectionsSheet intent")) } } } diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivityTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivityTest.kt deleted file mode 100644 index fae54efc683..00000000000 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivityTest.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.stripe.android.financialconnections - -import android.content.Context -import android.content.Intent -import android.net.Uri -import androidx.browser.customtabs.CustomTabsIntent -import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_OFF -import androidx.lifecycle.SavedStateHandle -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs -import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetForDataContract -import com.stripe.android.financialconnections.utils.InjectableActivityScenario -import com.stripe.android.financialconnections.utils.TestUtils.viewModelFactoryFor -import com.stripe.android.financialconnections.utils.injectableActivityScenario -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf - -@RunWith(RobolectricTestRunner::class) -@ExperimentalCoroutinesApi -class FinancialConnectionsSheetActivityTest { - - private val context = ApplicationProvider.getApplicationContext() - private val contract = FinancialConnectionsSheetForDataContract() - private val configuration = FinancialConnectionsSheet.Configuration( - ApiKeyFixtures.DEFAULT_FINANCIAL_CONNECTIONS_SESSION_SECRET, - ApiKeyFixtures.DEFAULT_PUBLISHABLE_KEY - ) - private val args = FinancialConnectionsSheetActivityArgs.ForData(configuration) - private val intent = contract.createIntent(context, args) - private val viewModel = createViewModel() - - private fun createViewModel(): FinancialConnectionsSheetViewModel = runBlocking { - FinancialConnectionsSheetViewModel( - applicationId = "com.example.test", - starterArgs = args, - savedStateHandle = SavedStateHandle(), - generateFinancialConnectionsSessionManifest = mock(), - fetchFinancialConnectionsSession = mock(), - fetchFinancialConnectionsSessionForToken = mock(), - eventReporter = mock() - ) - } - - private fun activityScenario( - viewModel: FinancialConnectionsSheetViewModel = this.viewModel - ): InjectableActivityScenario { - return injectableActivityScenario { - injectActivity { - viewModelFactory = viewModelFactoryFor(viewModel) - } - } - } - - @Test - fun `onCreate() with no args returns Failed result`() { - val scenario = activityScenario() - val intent = Intent(context, FinancialConnectionsSheetActivity::class.java) - scenario.launch(intent) - assertThat( - contract.parseResult( - scenario.getResult().resultCode, - scenario.getResult().resultData - ) - ).isInstanceOf( - FinancialConnectionsSheetResult.Failed::class.java - ) - } - - @Test - fun `onCreate() with invalid args returns Failed result`() { - val scenario = activityScenario() - val configuration = FinancialConnectionsSheet.Configuration("", "") - val args = FinancialConnectionsSheetActivityArgs.ForData(configuration) - val intent = contract.createIntent(context, args) - scenario.launch(intent) - assertThat( - contract.parseResult( - scenario.getResult().resultCode, - scenario.getResult().resultData - ) - ).isInstanceOf( - FinancialConnectionsSheetResult.Failed::class.java - ) - } - - @Test - fun `viewEffect - OpenAuthFlowWithUrl opens Chrome Custom Tab intent`() { - val chromeCustomTabUrl = "www.authflow.com" - val viewEffects = MutableSharedFlow() - val mockViewModel = mock { - on { viewEffect } doReturn viewEffects - } - activityScenario(mockViewModel).launch(intent).suspendOnActivity { - viewEffects.emit(FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl(chromeCustomTabUrl)) - val intent: Intent = shadowOf(it).nextStartedActivity - assertThat(intent.getIntExtra(CustomTabsIntent.EXTRA_SHARE_STATE, 0)).isEqualTo( - SHARE_STATE_OFF - ) - } - } - - @Test - fun `onNewIntent() calls view model handleOnNewIntent()`() { - val mockViewModel = mock() - val scenario = activityScenario(mockViewModel) - scenario.launch(intent).suspendOnActivity { - val newIntent = Intent(Intent.ACTION_VIEW) - newIntent.data = Uri.parse(ApiKeyFixtures.SUCCESS_URL) - it.onNewIntent(newIntent) - val argument: ArgumentCaptor = ArgumentCaptor.forClass(Intent::class.java) - verify(mockViewModel).handleOnNewIntent(argument.capture()) - assertThat(argument.value.data.toString()).isEqualTo(ApiKeyFixtures.SUCCESS_URL) - } - } - - @Test - fun `onBackPressed() cancels connection sheet`() { - val mockViewModel = mock { - on { state } doReturn MutableStateFlow(FinancialConnectionsSheetState(authFlowActive = true)) - } - val scenario = activityScenario(mockViewModel) - scenario.launch(intent).suspendOnActivity { - it.onBackPressed() - } - assertThat( - contract.parseResult( - scenario.getResult().resultCode, - scenario.getResult().resultData - ) - ).isInstanceOf( - FinancialConnectionsSheetResult.Canceled::class.java - ) - } - - /** - * When [InjectableActivityScenario.onActivity] triggers, - * runs the given block within the provided [TestScope]. - */ - private fun InjectableActivityScenario.suspendOnActivity( - block: suspend TestScope.(FinancialConnectionsSheetActivity) -> Unit - ) { - this.onActivity { - runTest { - block(this, it) - } - } - } -} diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetTest.kt index ae392208eb5..96f6e0090bb 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetTest.kt @@ -12,7 +12,7 @@ class FinancialConnectionsSheetTest { ApiKeyFixtures.DEFAULT_PUBLISHABLE_KEY ) private val financialConnectionsSheet = - FinancialConnectionsSheet(activity.application, financialConnectionsSheetLauncher) + FinancialConnectionsSheet(financialConnectionsSheetLauncher) @Test fun `present() should launch the connection sheet with the given configuration`() { diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt index dd534623415..4acc4520955 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt @@ -2,9 +2,9 @@ package com.stripe.android.financialconnections import android.content.Intent import android.net.Uri -import androidx.lifecycle.SavedStateHandle import androidx.test.ext.junit.runners.AndroidJUnit4 -import app.cash.turbine.test +import com.airbnb.mvrx.test.MvRxTestRule +import com.airbnb.mvrx.withState import com.google.common.truth.Truth.assertThat import com.stripe.android.core.exception.APIException import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult @@ -13,13 +13,17 @@ import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsS import com.stripe.android.financialconnections.domain.FetchFinancialConnectionsSessionForToken import com.stripe.android.financialconnections.domain.GenerateFinancialConnectionsSessionManifest import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs -import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Canceled +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Completed +import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Failed import com.stripe.android.financialconnections.model.FinancialConnectionsAccountFixtures import com.stripe.android.financialconnections.model.FinancialConnectionsAccountList import com.stripe.android.financialconnections.model.FinancialConnectionsSession import com.stripe.android.financialconnections.model.FinancialConnectionsSessionManifest import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any @@ -33,6 +37,9 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class FinancialConnectionsSheetViewModelTest { + @get:Rule + val mvrxRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher()) + private val eventReporter = mock() private val configuration = FinancialConnectionsSheet.Configuration( ApiKeyFixtures.DEFAULT_FINANCIAL_CONNECTIONS_SESSION_SECRET, @@ -44,23 +51,24 @@ class FinancialConnectionsSheetViewModelTest { ApiKeyFixtures.CANCEL_URL ) private val fetchFinancialConnectionsSession = mock() - private val fetchFinancialConnectionsSessionForToken = mock() - private val generateFinancialConnectionsSessionManifest = mock() + private val fetchFinancialConnectionsSessionForToken = + mock() + private val generateFinancialConnectionsSessionManifest = + mock() + private val defaultInitialState = FinancialConnectionsSheetState( + FinancialConnectionsSheetActivityArgs.ForData(configuration) + ) @Test fun `init - eventReporter fires onPresented`() { - createViewModel(configuration) - verify(eventReporter) - .onPresented(configuration) + createViewModel(defaultInitialState) + verify(eventReporter).onPresented(configuration) } @Test - fun `init - if manifest not restored from SavedStateHandle, fetchManifest triggered`() = + fun `init - if manifest not present in initial state, fetchManifest triggered`() = runTest { - createViewModel( - configuration = configuration, - savedStateHandle = SavedStateHandle() - ) + createViewModel(defaultInitialState) verify(generateFinancialConnectionsSessionManifest).invoke(any(), any()) } @@ -68,10 +76,7 @@ class FinancialConnectionsSheetViewModelTest { @Test fun `init - if manifest restored from SavedStateHandle, fetchManifest not triggered`() { runTest { - createViewModel( - configuration = configuration, - savedStateHandle = SavedStateHandle().also { it.set("key_manifest", manifest) } - ) + createViewModel(defaultInitialState.copy(manifest = manifest)) verifyNoInteractions(generateFinancialConnectionsSessionManifest) } @@ -81,14 +86,14 @@ class FinancialConnectionsSheetViewModelTest { fun `handleOnNewIntent - wrong intent should fire analytics event and set fail result`() { runTest { // Given - val viewModel = createViewModel(configuration) + val viewModel = createViewModel(defaultInitialState) // When viewModel.handleOnNewIntent(Intent("error_url")) // Then verify(eventReporter) - .onResult(eq(configuration), any()) + .onResult(eq(configuration), any()) } } @@ -97,7 +102,7 @@ class FinancialConnectionsSheetViewModelTest { runTest { // Given whenever(generateFinancialConnectionsSessionManifest(any(), any())).thenReturn(manifest) - val viewModel = createViewModel(configuration) + val viewModel = createViewModel(defaultInitialState) val cancelIntent = cancelIntent() // When @@ -106,7 +111,7 @@ class FinancialConnectionsSheetViewModelTest { // Then verify(eventReporter) - .onResult(configuration, FinancialConnectionsSheetActivityResult.Canceled) + .onResult(configuration, Canceled) } } @@ -115,39 +120,39 @@ class FinancialConnectionsSheetViewModelTest { runTest { // Given whenever(generateFinancialConnectionsSessionManifest(any(), any())).thenReturn(manifest) - val viewModel = createViewModel(configuration) - viewModel.viewEffect.test { - // When - // end auth flow - viewModel.handleOnNewIntent(cancelIntent()) - - // Then - assertThat(viewModel.state.value.authFlowActive).isFalse() - assertThat(FinishWithResult(FinancialConnectionsSheetActivityResult.Canceled)).isEqualTo( - awaitItem() - ) + val viewModel = createViewModel(defaultInitialState) + + // When + // end auth flow + viewModel.handleOnNewIntent(cancelIntent()) + + // Then + withState(viewModel) { + assertThat(it.authFlowActive).isFalse() + assertThat(it.viewEffect).isEqualTo(FinishWithResult(Canceled)) } } @Test fun `handleOnNewIntent - when intent with unknown received, then finish with Result#Failed`() = runTest { - val viewModel = createViewModel(configuration) - viewModel.viewEffect.test { - // Given - val errorIntent = Intent() - - // When - // end auth flow - viewModel.handleOnNewIntent(errorIntent) - - // Then - assertThat(viewModel.state.value.authFlowActive).isFalse() - val viewEffect = awaitItem() as FinishWithResult - assertThat(viewEffect.result).isInstanceOf(FinancialConnectionsSheetActivityResult.Failed::class.java) + // Given + val viewModel = createViewModel(defaultInitialState) + val errorIntent = Intent() + + // When + // end auth flow + viewModel.handleOnNewIntent(errorIntent) + + // Then + withState(viewModel) { + assertThat(it.authFlowActive).isFalse() + val viewEffect = it.viewEffect as FinishWithResult + assertThat(viewEffect.result).isInstanceOf(Failed::class.java) } } + @Test fun `handleOnNewIntent - when intent with success, then finish with Result#Success`() = runTest { @@ -156,22 +161,17 @@ class FinancialConnectionsSheetViewModelTest { whenever(generateFinancialConnectionsSessionManifest(any(), any())).thenReturn(manifest) whenever(fetchFinancialConnectionsSession(any())).thenReturn(expectedSession) - val viewModel = createViewModel(configuration) - viewModel.viewEffect.test { - - // When - // end auth flow - viewModel.handleOnNewIntent(successIntent()) - - // Then - assertThat(viewModel.state.value.authFlowActive).isFalse() - assertThat(awaitItem()).isEqualTo( - FinishWithResult( - result = FinancialConnectionsSheetActivityResult.Completed( - expectedSession - ) - ) - ) + val viewModel = createViewModel(defaultInitialState) + + // When + // end auth flow + viewModel.handleOnNewIntent(successIntent()) + + // Then + withState(viewModel) { + assertThat(it.authFlowActive).isFalse() + val viewEffect = it.viewEffect as FinishWithResult + assertThat(viewEffect.result).isEqualTo(Completed(expectedSession)) } } @@ -182,17 +182,17 @@ class FinancialConnectionsSheetViewModelTest { val apiException = APIException() whenever(generateFinancialConnectionsSessionManifest(any(), any())).thenReturn(manifest) whenever(fetchFinancialConnectionsSession.invoke(any())).thenAnswer { throw apiException } - val viewModel = createViewModel(configuration) - viewModel.viewEffect.test { - // When - // end auth flow - viewModel.handleOnNewIntent(successIntent()) - - // Then - assertThat(viewModel.state.value.authFlowActive).isFalse() - assertThat(awaitItem()).isEqualTo( - FinishWithResult(FinancialConnectionsSheetActivityResult.Failed(apiException)) - ) + val viewModel = createViewModel(defaultInitialState) + + // When + // end auth flow + viewModel.handleOnNewIntent(successIntent()) + + // Then + withState(viewModel) { + assertThat(it.authFlowActive).isFalse() + val viewEffect = it.viewEffect as FinishWithResult + assertThat(viewEffect.result).isEqualTo(Failed(apiException)) } } @@ -202,17 +202,17 @@ class FinancialConnectionsSheetViewModelTest { // Given whenever(generateFinancialConnectionsSessionManifest(any(), any())).thenReturn(manifest) whenever(fetchFinancialConnectionsSession(any())).thenAnswer { throw APIException() } - val viewModel = createViewModel(configuration) - viewModel.viewEffect.test { - // When - // end auth flow - viewModel.handleOnNewIntent(successIntent()) - - // Then - assertThat(viewModel.state.value.authFlowActive).isFalse() - assertThat(awaitItem()).isEqualTo( - FinishWithResult(FinancialConnectionsSheetActivityResult.Failed(APIException())) - ) + val viewModel = createViewModel(defaultInitialState) + // When + // end auth flow + viewModel.handleOnNewIntent(successIntent()) + + // Then + // Then + withState(viewModel) { + assertThat(it.authFlowActive).isFalse() + val viewEffect = it.viewEffect as FinishWithResult + assertThat(viewEffect.result).isEqualTo(Failed(APIException())) } } @@ -220,15 +220,18 @@ class FinancialConnectionsSheetViewModelTest { fun `onResume - when flow is still active and no config changes, finish with Result#Cancelled`() { runTest { // Given - val viewModel = createViewModel(configuration) - viewModel.viewEffect.test { - // When - // end auth flow (activity resumed without new intent received) - viewModel.onResume() - - // Then - assertThat(viewModel.state.value.authFlowActive).isTrue() - assertThat(awaitItem()).isEqualTo(FinishWithResult(FinancialConnectionsSheetActivityResult.Canceled)) + whenever(generateFinancialConnectionsSessionManifest(any(), any())).thenReturn(manifest) + val viewModel = createViewModel(defaultInitialState) + + // When + // end auth flow (activity resumed without new intent received) + viewModel.onResume() + + // Then + withState(viewModel) { + assertThat(it.authFlowActive).isTrue() + val viewEffect = it.viewEffect as FinishWithResult + assertThat(viewEffect.result).isEqualTo(Canceled) } } } @@ -237,17 +240,23 @@ class FinancialConnectionsSheetViewModelTest { fun `onActivityResult - when flow is still active and config changed, finish with Result#Cancelled`() { runTest { // Given - val viewModel = createViewModel(configuration) - viewModel.viewEffect.test { - // When - // configuration changes, changing lifecycle flow. - viewModel.onActivityRecreated() - // auth flow ends (activity received result without new intent received) - viewModel.onActivityResult() - - // Then - assertThat(viewModel.state.value.authFlowActive).isTrue() - assertThat(awaitItem()).isEqualTo(FinishWithResult(FinancialConnectionsSheetActivityResult.Canceled)) + // simulate a config change (initial state has a persisted manifest and ongoing authFlow) + val viewModel = createViewModel( + defaultInitialState.copy( + manifest = manifest, + authFlowActive = true + ) + ) + + // When + // auth flow ends (activity received result without new intent received) + viewModel.onActivityResult() + + // Then + withState(viewModel) { + assertThat(it.authFlowActive).isTrue() + val viewEffect = it.viewEffect as FinishWithResult + assertThat(viewEffect.result).isEqualTo(Canceled) } } } @@ -259,10 +268,10 @@ class FinancialConnectionsSheetViewModelTest { whenever(generateFinancialConnectionsSessionManifest(any(), any())).thenReturn(manifest) // When - val viewModel = createViewModel(configuration) + val viewModel = createViewModel(defaultInitialState) // Then - assertThat(viewModel.state.value.manifest).isEqualTo(manifest) + withState(viewModel) { assertThat(it.manifest).isEqualTo(manifest) } } } @@ -291,14 +300,11 @@ class FinancialConnectionsSheetViewModelTest { ) private fun createViewModel( - configuration: FinancialConnectionsSheet.Configuration, - savedStateHandle: SavedStateHandle = SavedStateHandle() + initialState: FinancialConnectionsSheetState, ): FinancialConnectionsSheetViewModel { - val args = FinancialConnectionsSheetActivityArgs.ForData(configuration) return FinancialConnectionsSheetViewModel( applicationId = "com.example.app", - starterArgs = args, - savedStateHandle = savedStateHandle, + initialState = initialState, generateFinancialConnectionsSessionManifest = generateFinancialConnectionsSessionManifest, fetchFinancialConnectionsSession = fetchFinancialConnectionsSession, fetchFinancialConnectionsSessionForToken = fetchFinancialConnectionsSessionForToken, From d7155259fe7106a15d49464833077e365295f6b5 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Thu, 12 May 2022 12:14:49 -0700 Subject: [PATCH 06/19] Ktlint fixes. --- .../financialconnections/FinancialConnectionsSheetFragment.kt | 1 - .../financialconnections/FinancialConnectionsSheetState.kt | 2 -- .../financialconnections/FinancialConnectionsSheetViewModel.kt | 1 - .../di/FinancialConnectionsSheetComponent.kt | 2 -- .../FinancialConnectionsSheetViewModelTest.kt | 1 - payments-core/build.gradle | 1 - 6 files changed, 8 deletions(-) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt index c37364c4ec2..7f2d1cf7eaf 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt @@ -60,4 +60,3 @@ internal class FinancialConnectionsSheetFragment : requireActivity().finish() } } - diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt index d5232b8e15d..03cefe79cb1 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetState.kt @@ -1,9 +1,7 @@ package com.stripe.android.financialconnections -import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.PersistState -import com.airbnb.mvrx.Uninitialized import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetForDataContract diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt index d7a96a7d458..1626c3fa25d 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt @@ -61,7 +61,6 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( } } } - } /** diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetComponent.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetComponent.kt index 1e347efc5ad..50502563f35 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetComponent.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/di/FinancialConnectionsSheetComponent.kt @@ -1,10 +1,8 @@ package com.stripe.android.financialconnections.di import android.app.Application -import androidx.lifecycle.SavedStateHandle import com.stripe.android.core.injection.CoroutineContextModule import com.stripe.android.core.injection.LoggingModule -import com.stripe.android.financialconnections.FinancialConnectionsSheet import com.stripe.android.financialconnections.FinancialConnectionsSheetState import com.stripe.android.financialconnections.FinancialConnectionsSheetViewModel import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs diff --git a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt index 4acc4520955..23cf313f791 100644 --- a/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt +++ b/financial-connections/src/test/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModelTest.kt @@ -152,7 +152,6 @@ class FinancialConnectionsSheetViewModelTest { } } - @Test fun `handleOnNewIntent - when intent with success, then finish with Result#Success`() = runTest { diff --git a/payments-core/build.gradle b/payments-core/build.gradle index 3b3dd5f2d8b..3dbf83ba7bd 100644 --- a/payments-core/build.gradle +++ b/payments-core/build.gradle @@ -51,7 +51,6 @@ dependencies { testImplementation "com.google.truth:truth:$truthVersion" testImplementation "androidx.arch.core:core-testing:$androidxArchCoreVersion" testImplementation "androidx.fragment:fragment-testing:$androidxFragmentVersion" - testImplementation testLibs.turbine androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation "androidx.test:rules:$androidTestVersion" From bd7005e9888cb3fcd9b45d8020116025ab30a6c8 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Thu, 12 May 2022 12:50:43 -0700 Subject: [PATCH 07/19] Updates comments. --- .../FinancialConnectionsSheetViewModel.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt index 1626c3fa25d..7f5e4177a64 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetViewModel.kt @@ -83,8 +83,10 @@ internal class FinancialConnectionsSheetViewModel @Inject constructor( /** * Activity recreation changes the lifecycle order: * - * - If config change happens while in web flow: onResume -> onNewIntent -> activityResult - * - If no config change happens: onActivityResult -> onNewIntent -> onResume + * If config change happens while in web flow: + * - onResume -> onNewIntent -> activityResult -> onResume(again) + * If no config change happens: + * - onActivityResult -> onNewIntent -> onResume * * (note [handleOnNewIntent] will just get called if user completed the web flow and clicked * the deeplink that redirects back to the app) From 86c1ac5915465267900ca9c0f98436d8046bb1e3 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Thu, 12 May 2022 13:17:39 -0700 Subject: [PATCH 08/19] Moves back logic to fragment. --- .../FinancialConnectionsSheetActivity.kt | 9 --------- .../FinancialConnectionsSheetFragment.kt | 9 +++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt index b9861af3582..5d879e48e55 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt @@ -38,13 +38,4 @@ internal class FinancialConnectionsSheetActivity : super.onNewIntent(intent) viewModel.handleOnNewIntent(intent) } - - /** - * If the back button is pressed during the manifest fetch or session fetch - * return canceled result - */ - override fun onBackPressed() { - setResult(Activity.RESULT_OK, Intent().putExtras(Canceled.toBundle())) - finish() - } } diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt index 7f2d1cf7eaf..3bd92e7a442 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetFragment.kt @@ -3,6 +3,8 @@ package com.stripe.android.financialconnections import android.app.Activity import android.content.Intent import android.net.Uri +import android.os.Bundle +import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.fragment.app.Fragment import com.airbnb.mvrx.MavericksView @@ -22,6 +24,13 @@ internal class FinancialConnectionsSheetFragment : private val viewModel: FinancialConnectionsSheetViewModel by activityViewModel() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requireActivity().onBackPressedDispatcher.addCallback(this) { + finishWithResult(FinancialConnectionsSheetActivityResult.Canceled) + } + } + /** * handle state changes here. */ From 10777719b765ec006506f59ebbb843a660a61130 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Thu, 12 May 2022 15:01:08 -0700 Subject: [PATCH 09/19] Removes unused imports. --- .../financialconnections/FinancialConnectionsSheetActivity.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt index 5d879e48e55..6fd78d689e7 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt @@ -1,13 +1,11 @@ package com.stripe.android.financialconnections -import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.airbnb.mvrx.asMavericksArgs import com.airbnb.mvrx.viewModel import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs -import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Canceled internal class FinancialConnectionsSheetActivity : AppCompatActivity(R.layout.activity_financialconnections_sheet) { From d55708df5d49439d20de6a5cf1f76cb892cd520a Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Thu, 12 May 2022 19:47:39 -0700 Subject: [PATCH 10/19] Reverts turbine deletion. --- dependencies.gradle | 1 + payments-core/build.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/dependencies.gradle b/dependencies.gradle index 32a9dc780da..dc04bbd9fda 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -16,5 +16,6 @@ ext.libs = [ ] ext.testLibs = [ + turbine: "app.cash.turbine:turbine:0.8.0", mavericks: "com.airbnb.android:mavericks-testing:$versions.mavericks" ] diff --git a/payments-core/build.gradle b/payments-core/build.gradle index 3dbf83ba7bd..3b3dd5f2d8b 100644 --- a/payments-core/build.gradle +++ b/payments-core/build.gradle @@ -51,6 +51,7 @@ dependencies { testImplementation "com.google.truth:truth:$truthVersion" testImplementation "androidx.arch.core:core-testing:$androidxArchCoreVersion" testImplementation "androidx.fragment:fragment-testing:$androidxFragmentVersion" + testImplementation testLibs.turbine androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation "androidx.test:rules:$androidTestVersion" From 72ee4d4a2615ce02420382524dea336d4472f6f2 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Thu, 12 May 2022 20:10:43 -0700 Subject: [PATCH 11/19] inlines args. --- .../FinancialConnectionsSheetActivity.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt index 6fd78d689e7..ff2d3692a4d 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheetActivity.kt @@ -12,15 +12,13 @@ internal class FinancialConnectionsSheetActivity : val viewModel: FinancialConnectionsSheetViewModel by viewModel() - private val starterArgs: FinancialConnectionsSheetActivityArgs? by lazy { - FinancialConnectionsSheetActivityArgs.fromIntent(intent) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) { val fragment = FinancialConnectionsSheetFragment().apply { - arguments = requireNotNull(starterArgs).asMavericksArgs() + arguments = requireNotNull( + FinancialConnectionsSheetActivityArgs.fromIntent(intent) + ).asMavericksArgs() } supportFragmentManager.beginTransaction() .setReorderingAllowed(true) From fd85581fa31b312fe110fc9b471faf4bff149bf7 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Thu, 12 May 2022 22:57:41 -0700 Subject: [PATCH 12/19] Just initialize mavericks if it hasn't been yet. --- .../FinancialConnectionsInitializer.kt | 26 +++++++++++++++++++ .../FinancialConnectionsSheet.kt | 5 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsInitializer.kt diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsInitializer.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsInitializer.kt new file mode 100644 index 00000000000..a2b26c0b07d --- /dev/null +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsInitializer.kt @@ -0,0 +1,26 @@ +package com.stripe.android.financialconnections + +import com.airbnb.mvrx.Mavericks + +/** + * Wrapper to handle any initialization needed before launching [FinancialConnectionsSheetActivity]. + */ +class FinancialConnectionsInitializer { + + fun initialize() { + initMavericks() + } + + /** + * Tries to retrieve [Mavericks.viewModelConfigFactory]. If Mavericks hasn't yet been + * initialized by the host app, it'll throw an [IllegalStateException]. In that case, + * initialize. + */ + private fun initMavericks() { + try { + Mavericks.viewModelConfigFactory + } catch (exception: IllegalStateException) { + Mavericks.initialize(debugMode = false) + } + } +} \ No newline at end of file diff --git a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt index e03dade835c..bdaa075ab61 100644 --- a/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt +++ b/financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsSheet.kt @@ -16,11 +16,12 @@ import kotlinx.parcelize.Parcelize * typically as a field initializer of an Activity or Fragment. */ class FinancialConnectionsSheet internal constructor( - private val financialConnectionsSheetLauncher: FinancialConnectionsSheetLauncher + private val financialConnectionsSheetLauncher: FinancialConnectionsSheetLauncher, + financialConnectionsInitializer: FinancialConnectionsInitializer = FinancialConnectionsInitializer() ) { init { - Mavericks.initialize(debugMode = false) + financialConnectionsInitializer.initialize() } /** From 8b531d2b2c7884e249b592849755cf708d3f9135 Mon Sep 17 00:00:00 2001 From: Carlos Munoz Date: Mon, 16 May 2022 11:24:45 -0700 Subject: [PATCH 13/19] Initialize mavericks via content provider. --- .../src/main/AndroidManifest.xml | 6 +++ .../FinancialConnectionsInitializer.kt | 26 ------------ .../FinancialConnectionsSheet.kt | 6 --- .../FinancialConnectionsInitializer.kt | 41 +++++++++++++++++++ 4 files changed, 47 insertions(+), 32 deletions(-) delete mode 100644 financial-connections/src/main/java/com/stripe/android/financialconnections/FinancialConnectionsInitializer.kt create mode 100644 financial-connections/src/main/java/com/stripe/android/financialconnections/appinitializer/FinancialConnectionsInitializer.kt diff --git a/financial-connections/src/main/AndroidManifest.xml b/financial-connections/src/main/AndroidManifest.xml index b303ce148d6..316e1fd1bd1 100644 --- a/financial-connections/src/main/AndroidManifest.xml +++ b/financial-connections/src/main/AndroidManifest.xml @@ -29,6 +29,12 @@ android:name="com.stripe.android.financialconnections.FinancialConnectionsSheetActivity" android:exported="false" android:theme="@style/StripeDefaultTheme" /> + +