-
Notifications
You must be signed in to change notification settings - Fork 657
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
Changes from all commits
e16f3be
1330e6d
b09df88
8cfa170
6e079b3
3d62e47
d715525
bd7005e
86c1ac5
1077771
d55708d
72ee4d4
fd85581
da1207b
041e91a
8b531d2
e5e6fbc
ea7d683
aaa6b24
c80ac9d
d116c39
4922ae7
62b7edc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
|
@@ -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 | ||
) : 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
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.