Skip to content

topPresenter selects detached RCTFabricModalHostViewController → drop-in silently fails to present on repeat checkout (iOS, Fabric) #1140

Description

@adibfirman

Describe the bug

On iOS new architecture (Fabric), the card drop-in presents fine on the first checkout but silently fails on every subsequent checkout in the same app session. No JS error, no callback — the pay button appears to do nothing.

Device console shows:

Attempt to present <Adyen.DropInNavigationController: 0x...>
on <RCTFabricModalHostViewController: 0x...>
(from <RCTFabricModalHostViewController: 0x...>)
whose view is not in the window hierarchy.

Same error string as #905, but different cause. #905 is a first-present timing race (Modal onShow). This one is a stale-presenter bug on repeat present: after a prior flow calls router.dismissAll(), its RCTFabricModalHostViewController is torn down on the React side but left presented-yet-detached at the UIKit level. On the next present(), UIViewController.topPresenter walks presentedViewController to the deepest VC and returns that stale, out-of-window host. iOS silently refuses to present on it.

Root cause (library side)

topPresenter's descent loop walks blindly to the deepest presentedViewController with no check that the VC is still live / in-window. Confirmed still present in latest 2.11.1 (loop adopted scene-aware key-window lookup but no detached/dismissing guard).

Steps to reproduce

  1. RN new arch (Fabric) + expo-router (or any RN modal host).
  2. Complete/attempt one card checkout (type: 'scheme').
  3. Tear down the flow via router.dismissAll().
  4. Start a second checkout in the same session → drop-in never appears; console logs the error above.

Physical device only — not reproducible on simulator.

Suggested fix

Guard topPresenter's descent loop to skip detached/dismissing VCs:

while let presented = topController?.presentedViewController,
      !presented.isBeingDismissed,
      presented.viewIfLoaded?.window != nil {
    topController = presented
}

Optionally, in BaseModule.present(), dismiss a leaked presented VC before presenting (belt-and-suspenders):

if let stale = presenter.presentedViewController,
   stale.viewIfLoaded?.window == nil || stale.isBeingDismissed {
    presenter.dismiss(animated: false) { [weak self] in
        self?.present(component)
    }
    return
}

Environment

  • @adyen/react-native 2.7.3 (also affects 2.11.1)
  • iOS, RN new architecture / Fabric
  • Physical device (simulator does not reproduce)

Happy to open a PR with the topPresenter guard if welcome.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug reportwhen a bug report is created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions