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

Conversation

carlosmuvi-stripe
Copy link
Collaborator

@carlosmuvi-stripe carlosmuvi-stripe commented May 10, 2022

Summary

  • Introduces Airbnb's Mavericks on Financial Connections to make building screens as functions of state easier and faster.
  • Recommended video
  • Also removes unnecessary dependencies - ConstraintLayout and Turbine.
  • Removes viewBinding.

Motivation

Establish a consistent presentation architecture across the Financial Connections SDK where screens are modeled as a function of state. This is a useful concept because it's:

  • Thread safe
  • Easy for engineers to reason through
  • Renders the same independently of the order of events leading up to it
  • Powerful enough to render any type of screen
  • Easy to test

Also, we get additional features:

Testing

  • Added tests
  • Modified tests
  • Manually verified

@github-actions
Copy link
Contributor

github-actions bot commented May 10, 2022

Diffuse output:

OLD: paymentsheet-example-release-master.apk (signature: none)
NEW: paymentsheet-example-release-pr.apk (signature: none)

          │            compressed             │            uncompressed            
          ├───────────┬───────────┬───────────┼───────────┬───────────┬────────────
 APK      │ old       │ new       │ diff      │ old       │ new       │ diff       
──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼────────────
      dex │  13.9 MiB │    14 MiB │ +79.3 KiB │  46.4 MiB │  46.7 MiB │ +242.1 KiB 
     arsc │   1.7 MiB │   1.7 MiB │       0 B │   1.7 MiB │   1.7 MiB │        0 B 
 manifest │   3.5 KiB │   3.5 KiB │     +84 B │  15.7 KiB │  16.2 KiB │     +492 B 
      res │ 823.3 KiB │ 823.3 KiB │      +4 B │   1.3 MiB │   1.3 MiB │       +4 B 
   native │   5.4 MiB │   5.4 MiB │       0 B │  13.4 MiB │  13.4 MiB │        0 B 
    asset │     3 MiB │     3 MiB │    +229 B │     3 MiB │     3 MiB │     +229 B 
    other │  79.9 KiB │  79.9 KiB │       0 B │ 154.8 KiB │ 154.8 KiB │        0 B 
──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼────────────
    total │  24.9 MiB │  24.9 MiB │ +79.6 KiB │    66 MiB │  66.3 MiB │ +242.8 KiB 

         │           raw           │                unique                
         ├────────┬────────┬───────┼────────┬────────┬────────────────────
 DEX     │ old    │ new    │ diff  │ old    │ new    │ diff               
─────────┼────────┼────────┼───────┼────────┼────────┼────────────────────
   files │      4 │      4 │     0 │        │        │                    
 strings │ 225906 │ 227188 │ +1282 │ 196643 │ 197674 │ +1031 (+1091 -60)  
   types │  39411 │  39658 │  +247 │  35967 │  36165 │  +198 (+212 -14)   
 classes │  33330 │  33525 │  +195 │  33330 │  33525 │  +195 (+209 -14)   
 methods │ 202934 │ 203727 │  +793 │ 194807 │ 195730 │  +923 (+1049 -126) 
  fields │ 150266 │ 152407 │ +2141 │ 149262 │ 151387 │ +2125 (+2169 -44)  

 ARSC    │ old  │ new  │ diff 
─────────┼──────┼──────┼──────
 configs │  293 │  293 │  0   
 entries │ 5802 │ 5802 │  0
APK
      compressed       │     uncompressed      │                               
───────────┬───────────┼──────────┬────────────┤                               
 size      │ diff      │ size     │ diff       │ path                          
───────────┼───────────┼──────────┼────────────┼───────────────────────────────
 448.9 KiB │ +71.8 KiB │  1.1 MiB │ +227.1 KiB │ ∆ classes4.dex                
   3.3 MiB │  +4.3 KiB │    9 MiB │  +71.8 KiB │ ∆ classes2.dex                
   3.8 MiB │  +3.1 KiB │ 11.3 MiB │  -56.8 KiB │ ∆ classes3.dex                
   7.3 KiB │    +229 B │  7.2 KiB │     +229 B │ ∆ assets/dexopt/baseline.prof 
   3.5 KiB │     +84 B │ 16.2 KiB │     +492 B │ ∆ AndroidManifest.xml         
     607 B │      +4 B │  1.1 KiB │       +4 B │ ∆ res/ZJ.xml                  
───────────┼───────────┼──────────┼────────────┼───────────────────────────────
   7.6 MiB │ +79.6 KiB │ 21.4 MiB │ +242.8 KiB │ (total)
MANIFEST
@@ -217,2 +217,7 @@
     <activity
+        android:exported=false
+        android:name=com.stripe.android.stripe3ds2.views.ChallengeActivity
+        android:theme=@style/Stripe3DS2Theme
+        />
+    <activity
         android:exported=true
@@ -247,6 +252,7 @@
         />
-    <activity
+    <provider
+        android:authorities=com.stripe.android.paymentsheet.example.financialconnections-init
         android:exported=false
-        android:name=com.stripe.android.stripe3ds2.views.ChallengeActivity
-        android:theme=@style/Stripe3DS2Theme
+        android:multiprocess=true
+        android:name=com.stripe.android.financialconnections.appinitializer.FinancialConnectionsInitializer
         />
DEX
STRINGS:

   old    │ new    │ diff              
  ────────┼────────┼───────────────────
   196643 │ 197674 │ +1031 (+1091 -60) 
  + �
  
  ���
  ���0����� �H
  
  + �
  
  ���
  ���0�*�0�H
  ¢����
  + �
  ���
  ������H�����*�0�*�H�H
  
  + �
  ���
  ������H����������*�0�H�@
  + �
  ���
  ������H����������*�0�*�H�H
  
  + 
  
  ���
  
  ������0�2�������0�H
  
  + 
  
  ��
  
  ������0�2����
  ��������0�H
  
  + �
  
  ���
  ���
  ������0�����2�����0�H�@
  + �
  
  ���
  ���
  �����
   �*���0�0�����*�0�H
  
  + �
  ���
  ���
  ���
  ���H�����*�����H�0������*�0�2�����H�H
  
  + �
  
  ���
  
  ���
  ��������0�2�����0�H
  ¢������
  + �
  
  ���
  ���
  ���
  ���0�����2�����0�2�����H�H�@
  + �
  
  ���
  ���
  ���
  ���0����������*�0�2�����H�H�@
  + �
  
  ���
  ���
  ���
  ���0�����2��������H�0�H�@¨��
  + �
  
  ���
  
  ���
  
  ������0�����*�����H�0�2�������0�H�@
  + �
  
  ���
  
  ���
  
  ������0�����*�0�2���������H�����0�0�H�@
  + �
  
  ���
  ���
  ���
  ������0����������*�0�*�0�H�@
  + �
  ���
  ���
  ���
  
  ������H�����*�0��������*�����H�0�
  ������*�0�H
  
  + �
  ���
  ���
  
  ���
  ����2�0�B
  ������0�¢����R�����0�X��¢��
  ������¨��
  + �
  
  ���
  
  ���
  
  ���
  ���0�����*�0�2����
   �*���0�0�H
  
  + �
  
  ���
  
  ���
  
  ���
  ���0�����*�����H�0������*�0�2�����H�H
  
  + �
  ���
  ���
  ���
  
  ���
  ���H�����*�0��������*�����H�0�
  ������*�0�H
  ¨��
  + �
  ���
  ���
  ���
  ���
  ����Æ��2�0�B���¢����R�����0�X��¢��
  ������¨��
  + �
  ���
  ���
  ���
  ���
  ����Æ��2�����0�0�2�0�B���¢����¨��
  + �
  
  ���
  
  ��
  
  ���
  ������0�����*�0������*�0�*�0�H�@
  + �
  
  ���
  
  ���
  
  ���
  ������0�����*�0�2���������H�����H�0�¢����H�@
  + �
  ���
  ���
  
  ���
  ���
  ������H�����*�0�����*�0�*�0������*�����H�0�H
  
  + �
  
  ���
  ���
  ���
  ���
  
  ������0����������*�0�*�����H�0�2�����0�H�@
  + �
  ���
  
  ��
  ���
  
  ���
  ����f�*����*�0�2�����H�0�J�����0�2�����8H&¢����¨��
  + �
  ���
  ��
  ���
  ���
  
  ���
  ����2�0�B���¢����R�����0�X�T¢��
  R�����0�X��¢��
  ¨��
  + �
  
  ���
  
  ���
  
  ���
  ���*��
  �2�0�J�����0�2�����0�H�J�����0�2�����0�H�J�����0�2�����0�H�¨��
  + �
  
  ���
  ���
  ���
  ���
  ���*��
  �2�����0�����80�J����82��	��0�2
  �
  ������0�H��¢���R�������8X��¢��
  ��������������¨�
  + �
  
  ���
  
  ���
  
  ���
  ���
  ������0�����*�����H�0������*�0������2��������H�0�H�@
  + �
  
  ���
  
  ���
  
  ���
  ���
  ������0�����*�����H�0������*�0�����������2���������H�����H�0	H�@
  + �
  
  ���
  
  ���
  
  ���
  ���
  ������0�����*�����H�0������*�0����������������2��	������H�����H�����H�0
  H�@
  + �
  
  ���
  
  ���
  
  ���
  ���
  ������0�����*�����H�0������*�0��������������������	2��
  ������H�����H�����H�����H	0�H�@
  + �
  
  ���
  
  ���
  
  ���
  ���
  ������0�����*�����H�0������*�0��������������������	����
  2_��� ����H�����H�����H�����H	����H
  0H�@
  + �
  
  ���
  
  ���
  
  ���
  ���
  ������0�����*�����H�0������*�0��������������������	����
  �����2*��&����H�����H�����H�����H	����H
  ����H�0
  H�@
  + �
  
  ���
  
  ���
  
  ���
  ���
  ������0�����*�����H�0������*�0��������������������	����
  ���������20�
  �,����H�����H�����H�����H	����H
  ����H�����H0�H�@
  + �
  
  ���
  ���
  ���
  
  ���
  
  �����
   �*���0�0�����*�0��������*�����H�0�
  ������*�0�H
  
  + �
  
  ��_
  ���
  ��
  ���
  ���
  ���� ����0�2
  ��������0�2
  �	������0�H� �
  ��0�2
  ��������0�2
  �������0�H4�������������0���0���
  ������0�0�0�X��¢��
  ������¨�
  + �
  
  ���
  
  ���
  ���
  ���
  ���*��
  �2�����8����8�0�J(��������8�0�2�����82
  ��������0�H��¢����¨��
  + �
  
  ���
  
  ���
  ���
  ���
  ���*��
  �2�����8����8�0�J(��������8�0�2�����82
  ��������0�H��¢����¨��¸�
  + �
  
  ���
  
  ���
  ���
  
  ���
  
  ������0����*�0�*�0��������*�����H�0�
  ������*�0�H
  
  + �
  ���
  ���
  ���
  ���
  
  ���
  
  ������H����*�0�*�0��������*�����H�0�
  ������*�0�2���������H�����H�0	H
  
  + �
  ���
  ��
  ���
  ���
  ���
  ���
  �������2�0�B���¢����J�������H������*�����0�¢����J!����0������*�����0�2��	��H�¢���
  ¨��
  + �
  
  ���
  
  ���
  
  ���
  
  ���
  *��
  �2�����80�J ����0������*�0�2��������H�0�H�¨��
  + �
  
  ���

...✂

Comment on lines -17 to -25
/**
* 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,
)
}
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.

import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Named

@Suppress("LongParameterList", "TooManyFunctions")
internal class FinancialConnectionsSheetViewModel @Inject constructor(
@Named(APPLICATION_ID) private val applicationId: String,
private val starterArgs: FinancialConnectionsSheetActivityArgs,
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'll use the state at the only source of truth, and retrieve initial args from there.

onFatal(it)
}.onSuccess {
openAuthFlow(it)
withState { state ->
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

state is read via withState. It ensures all the pending state changes are commited.

// stores manifest in state for future references.
_state.updateAndPersist {
it.copy(
setState {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

state is set using setState.

@carlosmuvi-stripe carlosmuvi-stripe changed the title POC - Mavericks in FC [Financial Connections] Adds Mavericks to handle state-based UIs. May 12, 2022
) {
@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.

Comment on lines +27 to +29
constructor(args: FinancialConnectionsSheetActivityArgs) : this(
initialArgs = args
)
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.

fetchManifest()
} else {
onActivityRecreated()
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.

unrelated to mavericks - moved this out of the activity and handled in viewModel.

Comment on lines 34 to 47
/**
* handle state changes here.
*/
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()
}
}
}
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.

@carlosmuvi-stripe carlosmuvi-stripe removed the request for review from skyler-stripe May 13, 2022 02:44
Copy link
Contributor

@ccen-stripe ccen-stripe left a comment

Choose a reason for hiding this comment

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

LGTM, left some Mavericks specific questions :)

One possible caveat is that this would prevent the user of Financial Connections SDK using a different version of Mavericks, we might need to communicate that somewhere

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.

}
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

@@ -21,15 +21,15 @@ import javax.inject.Singleton
internal interface FinancialConnectionsSheetComponent {
val viewModel: FinancialConnectionsSheetViewModel

fun inject(factory: FinancialConnectionsSheetViewModel.Factory)
fun inject(factory: FinancialConnectionsSheetViewModel.Companion)
Copy link
Contributor

Choose a reason for hiding this comment

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

The reason we have the inject method here is to ensure the factory was injectable when a fallback component injection is created, it needs to be explicitly called(e.g here), since fallback is not yet implemented, we could remove this method

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Forgot to remove it e5e6fbc


internal class FinancialConnectionsInitializer : ContentProvider() {
override fun onCreate(): Boolean {
Mavericks.initialize(context = requireNotNull(context))
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 I wonder since this is already here if we could start move daggers here too(not needed in this PR)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah we actually can!

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

withState(viewModel) { state ->
if (state.viewEffect != null) {
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

@carlosmuvi
Copy link
Contributor

Thanks for taking a look at this @ccen-stripe !

One possible caveat is that this would prevent the user of Financial Connections SDK using a different version of Mavericks, we might need to communicate that somewhere.

That's a good point that I still need to investigate. We might need to communicate that we're integrating Mavericks in our docs, probably mentioning how to configure resolutionStrategy in Gradle, etc. which was something I was trying to avoid.

I opened this issue in Mavericks asking for SDK integration concerns. Might followup with this!

@carlosmuvi-stripe carlosmuvi-stripe requested review from ccen-stripe and mshafrir-stripe and removed request for jameswoo-stripe May 24, 2022 14:05
ccen-stripe
ccen-stripe previously approved these changes May 27, 2022
@carlosmuvi-stripe carlosmuvi-stripe enabled auto-merge (squash) June 3, 2022 21:13
@carlosmuvi-stripe carlosmuvi-stripe merged commit 5b62878 into master Jun 3, 2022
@carlosmuvi-stripe carlosmuvi-stripe deleted the poc-mavericks branch June 3, 2022 21:31
@carlosmuvi-stripe carlosmuvi-stripe added the financial-connections Relates to the Financial Connections SDK label Jul 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
financial-connections Relates to the Financial Connections SDK
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants