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 all 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
8 changes: 7 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,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.8.0"
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
16 changes: 3 additions & 13 deletions financial-connections/api/financial-connections.api
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,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 @@ -135,19 +135,9 @@ 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 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,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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">
Expand All @@ -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"
Expand All @@ -17,4 +18,4 @@
app:layout_constraintTop_toTopOf="parent"
app:trackThickness="@dimen/stripe_connectionssheet_loading_indicator_stroke_width" />

</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
6 changes: 6 additions & 0 deletions financial-connections/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
android:name="com.stripe.android.financialconnections.FinancialConnectionsSheetActivity"
android:exported="false"
android:theme="@style/StripeDefaultTheme" />

<provider
android:name=".appinitializer.FinancialConnectionsInitializer"
android:authorities="${applicationId}.financialconnections-init"
android:exported="false"
android:multiprocess="true" />
</application>

<!--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ 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,
) {

/**
* Configuration for a [FinancialConnectionsSheet]
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,126 +1,75 @@
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.airbnb.mvrx.MavericksView
import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import com.stripe.android.financialconnections.FinancialConnectionsSheetViewEffect.FinishWithResult
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.presentation.CreateBrowserIntentForUrl
import java.security.InvalidParameterException

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

val viewModel: FinancialConnectionsSheetViewModel by viewModel()

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 }

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

setupObservers()
viewModel.onEach { postInvalidate() }
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()
}

override fun onBackPressed() {
super.onBackPressed()
finishWithResult(FinancialConnectionsSheetActivityResult.Canceled)
}

/**
* 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
* handle state changes here.
*/
override fun onBackPressed() {
finishWithResult(FinancialConnectionsSheetActivityResult.Canceled)
override fun invalidate() {
withState(viewModel) { state ->
state.viewEffect?.let { viewEffect ->
when (viewEffect) {
is OpenAuthFlowWithUrl -> startForResult.launch(
CreateBrowserIntentForUrl(
context = this,
uri = Uri.parse(viewEffect.url),
)
)
is FinishWithResult -> finishWithResult(
viewEffect.result
)
}
viewModel.onViewEffectLaunched()
}
}
}

private fun finishWithResult(result: FinancialConnectionsSheetActivityResult) {
setResult(
Activity.RESULT_OK,
Intent().putExtras(result.toBundle())
)
setResult(RESULT_OK, Intent().putExtras(result.toBundle()))
finish()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.stripe.android.financialconnections

import androidx.lifecycle.SavedStateHandle
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.PersistState
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
Expand All @@ -9,38 +11,22 @@ 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
) {
@PersistState val manifest: FinancialConnectionsSessionManifest? = null,
@PersistState val authFlowActive: Boolean = false,
val viewEffect: FinancialConnectionsSheetViewEffect? = null
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.

Now viewEffects are part of the state, ensuring we commit to an Unidirectional Data Flow.

) : MavericksState {

/**
* 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,
)
}
Comment on lines -17 to -25
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There's no need to handle savedStateHandle with the @PersistState annotation.

val sessionSecret: String
get() = initialArgs.configuration.financialConnectionsSessionClientSecret

/**
* Saves the persistable fields of this state that changed to the given [SavedStateHandle]
* Constructor used by Mavericks to build the initial state.
*/
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"
}
constructor(args: FinancialConnectionsSheetActivityArgs) : this(
initialArgs = args
)
Comment on lines +27 to +29
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

instead of having to handle fragment args manually, Mavericks calls this constructor passing down the args so state can be used as the only source of truth.

}

/**
Expand Down
Loading