-
Notifications
You must be signed in to change notification settings - Fork 663
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
Improve animation in payment sheet screen #10524
base: master
Are you sure you want to change the base?
Improve animation in payment sheet screen #10524
Conversation
Diffuse output:
APK
|
paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetActivityTest.kt
Show resolved
Hide resolved
paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentSheetScreen.kt
Outdated
Show resolved
Hide resolved
update = { | ||
button?.updateUiState(currentState) | ||
}, |
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.
This seems necessary to sync the state instead of updating within the LaunchedEffect
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.
Can you explain what you mean here?
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.
Originally it was updated async with
LaunchedEffect(viewModel, button) {
viewModel.primaryButtonUiState.collect { uiState ->
withContext(Dispatchers.Main) {
button?.updateUiState(uiState)
}
}
}
Updating the android view along with recomposition by using this update
function can solve the janky animation. I am not sure if this is 100% correct.
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.
I'm still not quite following, but the code as written doesn't seem dangerous.
paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentSheetScreen.kt
Outdated
Show resolved
Hide resolved
paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentSheetScreen.kt
Outdated
Show resolved
Hide resolved
2192d48
to
1cc7b25
Compare
return if (isWalletEnabled || isCompleteFlow) { | ||
null | ||
} else { | ||
if (interactor.state.value.supportedPaymentMethods.singleOrNull()?.code == |
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.
How do we ensure this is called again once the interactor state has changed?
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.
It works because supportedPaymentMethods
does not change with the state. It is determined when the interactor is initialized (if I am not wrong). I can refactor this if we decide to go this path.
The one within ManageSavedPaymentMethods
would worry me more. That works because interactor.state.title
and walletsState
share a same dependency interactor.editing
, but we shouldn't have implicit dependency like this.
I did not really find a solution for ManageSavedPaymentMethods
. If I roll back the change and collect title
as a StateFlow
in PaymentSheetScreen
, the animation would still look janky (but maybe acceptable?)
Screen_recording_20250402_142456.webm
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.
I don't see any animations in the screen recording (maybe I'm missing something).
If we can make progress on this, without making implicit assumptions, I think we should do that.
If the only way to do it is to make implicit assumptions, we could write tests to validate the behavior that exists today in a more end to end/integration style test.
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.
If you look closer, the title/headerText actually blinks. But it seems not quite noticeable! I'll roll back and use StateFlow
for title, as no implicit assumption is needed in this case.
paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentSheetScreen.kt
Outdated
Show resolved
Hide resolved
paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentSheetScreen.kt
Outdated
Show resolved
Hide resolved
paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentSheetScreen.kt
Outdated
Show resolved
Hide resolved
update = { | ||
button?.updateUiState(currentState) | ||
}, |
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.
Can you explain what you mean here?
paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetActivityTest.kt
Show resolved
Hide resolved
override fun showsWalletsHeader(isCompleteFlow: Boolean): StateFlow<Boolean> { | ||
return interactor.showsWalletsHeader | ||
} | ||
override fun showsWalletsHeader(isCompleteFlow: Boolean, walletsState: WalletsState?) = |
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.
Is this change necessary?
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.
Ideally we should use !interactor.showsWalletsInline(walletsState)
instead. But the current showsWalletsHeader
is used in many tests. I can create another PR for this so we are not introducing more complexity here.
* | ||
*/ | ||
@Composable | ||
internal fun rememberPaymentSheetScreenContentState( |
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.
Should this method take in the viewModel instead?
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.
Yes, I think so! And actually we might want to facilitate combineAsStateFlow
and mapAsStateFlow
in that case. wdyt? Is there a reason that we should avoid these two helpers?
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.
I think we should avoid combineAsStateFlow and mapAsStateFlow.
Those helpers are hacks! We should use compose to do what they do instead. Combine is just a bunch of collectAsState + manual combination with "normal" code. Map is basically the same story. Just collect + do the transformation as "normal" code.
I don't see a use for them when we have the utilities compose provides.
The hacks are basically how we generate the initial value is not consistent with the mental model of how a state flow works. Calling StateFlow.value
should just return a value that's already been calculated. Our hacks for combineAsStateFlow and mapAsStateFlow recompute the value instead 😬
paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentSheetScreen.kt
Outdated
Show resolved
Hide resolved
…screen # Conflicts: # paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/PaymentSheetScreen.kt
// supportedPaymentMethods does not change | ||
if (interactor.state.value.supportedPaymentMethods.singleOrNull()?.code == |
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.
I'll refactor interactor.state.value.supportedPaymentMethods
to interactor.supportedPaymentMethods
in another PR.
override fun showsWalletsHeader(isCompleteFlow: Boolean): StateFlow<Boolean> { | ||
return interactor.showsWalletsHeader | ||
} | ||
override fun showsWalletsHeader(isCompleteFlow: Boolean, walletsState: WalletsState?) = |
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.
Ideally we should use !interactor.showsWalletsInline(walletsState)
instead. But the current showsWalletsHeader
is used in many tests. I can create another PR for this so we are not introducing more complexity here.
override fun title(isCompleteFlow: Boolean, isWalletEnabled: Boolean): ResolvableString? { | ||
// Should never be used | ||
// headerText of this screen is set in paymentSheetScreenContentState | ||
return interactor.state.value.title | ||
} |
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.
Is there a better way to avoid calling this function? I don't think we want to throw an error though.
override fun paymentSheetScreenContentState( | ||
type: PaymentSheetFlowType, | ||
walletsState: WalletsState?, | ||
primaryButtonUiState: PrimaryButton.UIState? | ||
): StateFlow<PaymentSheetScreenContentState> { | ||
return interactor.state.mapAsStateFlow { state -> | ||
state.title | ||
PaymentSheetScreenContentState( | ||
showsWalletsHeader = false, | ||
actualWalletsState = null, | ||
headerText = state.title, | ||
currentScreen = this, | ||
primaryButtonUiState = primaryButtonUiState | ||
) | ||
} | ||
} |
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.
Most of the screen calls the parent paymentSheetScreenContentState
. But we rely on interactor.state
here so we need to override the function.
Espresso.onIdle() | ||
assertThat(activity.buyButton.isEnabled) | ||
.isTrue() | ||
composeTestRule.waitUntil { activity.buyButton.isEnabled } |
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.
The weird thing here is that we still need Espresso.onIdle()
to pass the test. I suspect this is related to the fact that our PrimaryButton
is an AndroidView
wrapped by composable.
a3294ee
to
1efb380
Compare
* PaymentSheetScreenContentState | ||
* | ||
*/ | ||
fun paymentSheetScreenContentState( |
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.
I think there was a misunderstanding here. What I meant was for this to be abstract, and for each of the screens to implement it. I also meant we should remove the other StateFlows and use only this one.
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.
As is, this implementation has the same issues mentioned here.
1efb380
to
bc6cfd3
Compare
Summary
Sync state updates in
PaymenSheetScreenContent
to prevent glitchy animation.Motivation
https://jira.corp.stripe.com/browse/MOBILESDK-2857
Root cause is the async state updates in
PaymenSheetScreenContent
. See the following recording for detailed step through:Screen_recording_20250327_231246.webm
Testing
Screenshots
Changelog
[Fixed] Improve animation in payment sheet screen