Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Financial Connections] Adds Mavericks to handle state-based UIs. #4986

Merged
merged 23 commits into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e16f3be
Adds mavericks.
carlosmuvi-stripe May 10, 2022
1330e6d
Removes viewBinding.
carlosmuvi-stripe May 10, 2022
b09df88
Move args validation to manifest fetching.
carlosmuvi-stripe May 10, 2022
8cfa170
Makes viewEffect a nullable prop and handles it in invalidate.
carlosmuvi-stripe May 10, 2022
6e079b3
Removes redundant activity tests.
carlosmuvi-stripe May 11, 2022
3d62e47
Merge remote-tracking branch 'origin/master' into poc-mavericks
carlosmuvi-stripe May 12, 2022
d715525
Ktlint fixes.
carlosmuvi-stripe May 12, 2022
bd7005e
Updates comments.
carlosmuvi-stripe May 12, 2022
86c1ac5
Moves back logic to fragment.
carlosmuvi-stripe May 12, 2022
1077771
Removes unused imports.
carlosmuvi-stripe May 12, 2022
d55708d
Reverts turbine deletion.
carlosmuvi-stripe May 13, 2022
72ee4d4
inlines args.
carlosmuvi-stripe May 13, 2022
fd85581
Just initialize mavericks if it hasn't been yet.
carlosmuvi-stripe May 13, 2022
da1207b
Merge remote-tracking branch 'origin/master' into poc-mavericks
carlosmuvi-stripe May 13, 2022
041e91a
Merge remote-tracking branch 'origin/master' into poc-mavericks
carlosmuvi-stripe May 16, 2022
8b531d2
Initialize mavericks via content provider.
carlosmuvi-stripe May 16, 2022
e5e6fbc
Remove inject.
carlosmuvi-stripe May 20, 2022
ea7d683
Remove redundant launch function.
carlosmuvi-stripe May 20, 2022
aaa6b24
Removes fragment and unifies in activity.
carlosmuvi-stripe May 20, 2022
c80ac9d
Regenerates API.
carlosmuvi-stripe May 21, 2022
d116c39
Merge remote-tracking branch 'origin/master' into poc-mavericks
carlosmuvi-stripe Jun 3, 2022
4922ae7
Fixes activity recreation flow.
carlosmuvi-stripe Jun 3, 2022
62b7edc
Fix tests.
carlosmuvi-stripe Jun 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
ext.versions = [
detekt: "1.20.0",
mavericks: "2.6.1"
]

ext.buildLibs = [
Expand All @@ -10,6 +11,10 @@ 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.8.0"
mavericks: "com.airbnb.android:mavericks-testing:$versions.mavericks"
]
1 change: 0 additions & 1 deletion financial-connections-example/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
17 changes: 4 additions & 13 deletions financial-connections/api/financial-connections.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (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 <init> (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 {
Expand All @@ -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 {
Expand Down
10 changes: 2 additions & 8 deletions financial-connections/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ dependencies {
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

kapt "com.google.dagger:dagger-compiler:$daggerVersion"

testImplementation 'app.cash.turbine:turbine:0.8.0'
testImplementation "androidx.arch.core:core-testing:$androidxArchCoreVersion"
testImplementation "androidx.fragment:fragment-testing:$androidxFragmentVersion"
testImplementation "androidx.test.ext:junit-ktx:$androidTestJunitVersion"
Expand All @@ -47,6 +47,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"
Expand All @@ -56,13 +57,6 @@ dependencies {
ktlint "com.pinterest:ktlint:$ktlintVersion"
}

android {
buildFeatures {
viewBinding true
}
}


ext {
artifactId = "financial-connections"
artifactName = "financial-connections"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:indicatorColor="@color/stripe_toolbar_color_default"
app:indicatorSize="@dimen/stripe_connectionssheet_loading_indicator_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:trackThickness="@dimen/stripe_connectionssheet_loading_indicator_stroke_width" />

</androidx.constraintlayout.widget.ConstraintLayout>
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".FinancialConnectionsSheetActivity" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.google.android.material.progressindicator.CircularProgressIndicator
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"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:trackThickness="@dimen/stripe_connectionssheet_loading_indicator_stroke_width" />

</FrameLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.stripe.android.financialconnections
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
Expand All @@ -17,6 +18,11 @@ import kotlinx.parcelize.Parcelize
class FinancialConnectionsSheet internal constructor(
private val financialConnectionsSheetLauncher: FinancialConnectionsSheetLauncher
) {

init {
Mavericks.initialize(debugMode = false)
}

/**
* Configuration for a [FinancialConnectionsSheet]
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,125 +2,40 @@ 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 androidx.lifecycle.lifecycleScope
import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult
import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.OpenAuthFlowWithUrl
import com.stripe.android.financialconnections.databinding.ActivityFinancialconnectionsSheetBinding
import com.airbnb.mvrx.asMavericksArgs
import com.airbnb.mvrx.viewModel
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityArgs
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult
import com.stripe.android.financialconnections.presentation.CreateBrowserIntentForUrl
import java.security.InvalidParameterException
import com.stripe.android.financialconnections.launcher.FinancialConnectionsSheetActivityResult.Canceled

internal class FinancialConnectionsSheetActivity : AppCompatActivity() {
internal class FinancialConnectionsSheetActivity :
AppCompatActivity(R.layout.activity_financialconnections_sheet) {

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)
}

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
if (savedInstanceState == null) {
val fragment = FinancialConnectionsSheetFragment().apply {
arguments = requireNotNull(starterArgs).asMavericksArgs()
}
supportFragmentManager.beginTransaction()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since there's only one fragment, is it possible to declare it deirectly in xml?

    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_host_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:name="com.stripe.android.financialconnections.FinancialConnectionsSheetFragment"
    tools:context=".FinancialConnectionsSheetActivity" />

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also why do we change the Activity to Fragment? Is it required by Mavericks?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is supported, but they recommend using fragments, as "Putting UI directly inside of an Activity is no longer recommended in general for Android." (airbnb/mavericks#616 (comment))

However if Compose will be used to build the UI, the activity is still a recommended approach - https://github.com/airbnb/mavericks/blob/main/sample-compose/src/main/java/com/airbnb/mvrx/compose/sample/ComposeSampleActivity.kt so the fragment would be removed in that case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, rethinking about it, the fragment doesn't bring any value on this case as there's no UI currently. Moved things back to the activity - aaa6b24

.setReorderingAllowed(true)
.add(R.id.nav_host_fragment, fragment, null)
.commit()
}

setupObservers()
if (savedInstanceState != null) viewModel.onActivityRecreated()
}

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()
}

/**
* 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)
}

/**
* If the back button is pressed during the manifest fetch or session fetch
* return canceled result
*/
override fun onBackPressed() {
finishWithResult(FinancialConnectionsSheetActivityResult.Canceled)
}

private fun finishWithResult(result: FinancialConnectionsSheetActivityResult) {
setResult(
Activity.RESULT_OK,
Intent().putExtras(result.toBundle())
)
finish()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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
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
import com.stripe.android.financialconnections.presentation.CreateBrowserIntentForUrl

internal class FinancialConnectionsSheetFragment :
Fragment(R.layout.fragment_financial_connections_sheet), MavericksView {

private val startForResult = registerForActivityResult(StartActivityForResult()) {
viewModel.onActivityResult()
}

private val viewModel: FinancialConnectionsSheetViewModel by activityViewModel()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would a fragmentViewModel or navGraphViewModel be applicable here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need an activityViewModel to receive the onNewIntent callback, captured by the activity, on this ViewModel shared with the fragment.


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(this) {
finishWithResult(FinancialConnectionsSheetActivityResult.Canceled)
}
}

/**
* handle state changes here.
*/
override fun invalidate() {
withState(viewModel) { state ->
if (state.viewEffect != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use state.viewEffect?.let {} or add an else block to when(state.viewEffect) for the null case

when (state.viewEffect) {
is OpenAuthFlowWithUrl -> state.viewEffect.launch()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can directly define the launch() function inside class OpenAuthFlowWithUrl instead of an extension, the when/is check will be able to tell the type and no downcast is needed here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean by adding a launch abstract function to the ViewEffect sealed class?

each viewEffect uses different dependencies hosted by the fragment, so we can't have a unified launch. However I don't think we need that much level of function extraction, we can simplify here removing the launch entirely: ea7d683

is FinishWithResult -> finishWithResult(state.viewEffect.result)
}
viewModel.onViewEffectLaunched()
}
}
}
Copy link
Collaborator Author

@carlosmuvi-stripe carlosmuvi-stripe May 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When state changes, invalidate gets triggered. State updates in the UI should be handled here.


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()
}
}
Loading