Skip to content

Commit

Permalink
Fix PayNow animation jank (stripe#2934)
Browse files Browse the repository at this point in the history
## Summary
When the PaymentSheetViewController appears, it refreshes its UI,
including the ConfirmButton's animation state. A race condition arises
when the PaymentSheetViewController and the completion of an action
simultaneously update the UI, resulting in occasional animation
glitches. These occur when we start new animations before the
PaymentSheet animations have completed. To resolve this issue, we delay
the action's evolution to succeeded/cancelled until 0.2 seconds after
the dismissal of the PollingViewController. This allows sufficient time
for the animations to finish, preventing conflicts.

## Motivation
- Polish

## Testing
- Manually ran it 25 times and no longer observed the animation jank

## Changelog
N/A
  • Loading branch information
porter-stripe authored Sep 15, 2023
1 parent a66df4a commit 04cd715
Show file tree
Hide file tree
Showing 5 changed files with 22 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,8 @@ class AuthenticationContext: NSObject, PaymentSheetAuthenticationContext {
presentingViewController.present(pollingVC, animated: true, completion: nil)
}

func dismiss(_ authenticationViewController: UIViewController) {
authenticationViewController.dismiss(animated: true, completion: nil)
func dismiss(_ authenticationViewController: UIViewController, completion: (() -> Void)?) {
authenticationViewController.dismiss(animated: true, completion: completion)
}

let presentingViewController: UIViewController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,12 @@ extension BottomSheetViewController: PaymentSheetAuthenticationContext {
pushContentViewController(pollingVC)
}

func dismiss(_ authenticationViewController: UIViewController) {
func dismiss(_ authenticationViewController: UIViewController, completion: (() -> Void)?) {
guard contentViewController is BottomSheet3DS2ViewController || contentViewController is PollingViewController else {
return
}
_ = popContentViewController()
completion?()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,18 @@ class PollingViewController: UIViewController {
// MARK: Handlers

@objc func didTapCancel() {
currentAction.complete(with: .canceled, error: nil)
dismiss()
dismiss {
// Wait a short amount of time before completing the action to ensure smooth animations
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.currentAction.complete(with: .canceled, error: nil)
}
}
}

private func dismiss() {
private func dismiss(completion: (() -> Void)? = nil) {
if let authContext = currentAction.authenticationContext as? PaymentSheetAuthenticationContext {
authContext.authenticationContextWillDismiss?(self)
authContext.dismiss(self)
authContext.dismiss(self, completion: completion)
}

oneSecondTimer?.invalidate()
Expand Down Expand Up @@ -344,8 +348,12 @@ extension PollingViewController: IntentStatusPollerDelegate {
if paymentIntent.status == .succeeded {
setErrorStateWorkItem.cancel() // cancel the error work item incase it was scheduled
currentAction.paymentIntent = paymentIntent // update the local copy of the intent with the latest from the server
currentAction.complete(with: .succeeded, error: nil)
dismiss()
dismiss {
// Wait a short amount of time before completing the action to ensure smooth animations
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.currentAction.complete(with: .succeeded, error: nil)
}
}
} else if paymentIntent.status != .requiresAction {
// an error occured to take the intent out of requires action
// update polling state to indicate that we have encountered an error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,8 @@ extension PaymentSheet_LPM_ConfirmFlowTests: PaymentSheetAuthenticationContext {
// no-op
}

func dismiss(_ authenticationViewController: UIViewController) {
// no-op
func dismiss(_ authenticationViewController: UIViewController, completion: (() -> Void)?) {
completion?()
}

func presentPollingVCForAction(action: STPPaymentHandlerActionParams, type: STPPaymentMethodType, safariViewController: SFSafariViewController?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2331,7 +2331,7 @@ extension STPPaymentHandler {
if let paymentSheet = currentAction.authenticationContext
.authenticationPresentingViewController() as? PaymentSheetAuthenticationContext
{
paymentSheet.dismiss(challengeViewController)
paymentSheet.dismiss(challengeViewController, completion: nil)
} else {
challengeViewController.dismiss(animated: true, completion: nil)
}
Expand All @@ -2349,7 +2349,7 @@ extension STPPaymentHandler {
/// Internal authentication context for PaymentSheet magic
@_spi(STP) public protocol PaymentSheetAuthenticationContext: STPAuthenticationContext {
func present(_ authenticationViewController: UIViewController, completion: @escaping () -> Void)
func dismiss(_ authenticationViewController: UIViewController)
func dismiss(_ authenticationViewController: UIViewController, completion: (() -> Void)?)
func presentPollingVCForAction(action: STPPaymentHandlerActionParams, type: STPPaymentMethodType, safariViewController: SFSafariViewController?)
}

Expand Down

0 comments on commit 04cd715

Please sign in to comment.