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

[BUG]: 3D Secure Authentication behind top layer elements #599

Open
tinleym opened this issue Apr 12, 2024 · 19 comments
Open

[BUG]: 3D Secure Authentication behind top layer elements #599

tinleym opened this issue Apr 12, 2024 · 19 comments
Labels
bug Something isn't working pinned

Comments

@tinleym
Copy link

tinleym commented Apr 12, 2024

What happened?

3D Secure Authentication modals rely on a high z-index, which regardless of number is trumped by top layer elements (e.g. a dialog opened with .showModal()). This can make the modal inaccessible (if, for example, it's called from a dialog element opened with showModal).

https://developer.mozilla.org/en-US/docs/Glossary/Top_layer

Environment

No response

Reproduction

No response

@tinleym tinleym added the bug Something isn't working label Apr 12, 2024
@brendanm-stripe
Copy link
Contributor

Can you share a basic reproduction of this to illustrate? What would you expect to happen vs what you observe?

@tinleym
Copy link
Author

tinleym commented Apr 20, 2024

Here's a codesandbox: https://codesandbox.io/p/devbox/goofy-mountain-wwzgsc?file=%2Fapp%2F_lib%2Fmain.tsx%3A37%2C16

The card number in the dialog will trigger 3d secure. I'd expect the 3d secure modal to appear over the form modal, but it's behind, because top layer is above any z-index.

@brendanm-stripe
Copy link
Contributor

Thanks for sharing that repro -- we're investigating this.

@benkingcode
Copy link

Also a big issue for us! Our checkout flow is in a native dialog.showModal() and 3D Secure totally breaks...

Copy link

stale bot commented Jun 1, 2024

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Jun 1, 2024
@tinleym
Copy link
Author

tinleym commented Jun 1, 2024

Some further activity

@stale stale bot removed the stale label Jun 1, 2024
@wesbos
Copy link

wesbos commented Jun 13, 2024

Hitting this issue as well. I have a checkout in a dialog:

<dialog><StripePaymentElement/><dialog>

When using .showModal() on the dialog, the browser add its to the "Top Layer", which cannot be overwritten with z-index.

Because of this, the 3d secure popup is under my dialog. Paypal has the same issue with their embedded login.

The fix is for stripe to move their modals to a modally shown element.

Right now for paypal I am closing my <dialog>, but I'm not finding an event for the PaymentElement

Screenshot 2024-06-13 at 12 54 41 PM

I've put my modal at 20% opacity here to show the issue:

Screenshot 2024-06-13 at 12 55 33 PM

@wesbos
Copy link

wesbos commented Jun 13, 2024

@tinleym @benkingcode - did you find any workaround to this? I'm trying to find a good way to detect if the 3DS modal is open so I can close mine. Doesn't look like Stripe offers an even for this, and detecting the iframe being inejcted seems tricky as there are no classes or src that I can grab onto..

@tinleym
Copy link
Author

tinleym commented Jun 14, 2024

@wesbos I'm setting the dialog display to block, and then on submit I call dialog.close (it stays visible because of display: block, but is removed from the top layer), I then await my stripe calls, and then I call dialog.showModal to get the dialog back to its presubmission state.

In my case, I want the dialog visible and inert during a submission, whether 3DS is called or not, so that's as far as I got.

@wesbos
Copy link

wesbos commented Jun 14, 2024

ohhhh interesting trick - display block on the closed dialog. thank you - might help me out

@benkingcode
Copy link

@brendanm-stripe any update?

@Neophen
Copy link

Neophen commented Aug 5, 2024

This is going to become more and more of an issue as more and more people will start using dialog elements.
We're running into this issue now also

@brendanm-stripe
Copy link
Contributor

We have no updates to share about this yet.

An interim alternative Stripe payment flow would be generating confirmation tokens to confirm on your server, specifying use_stripe_sdk=true (ref) for handling actions, then if required you can call handleNextAction (ref) to display the 3ds flow with your modal/dialog dismissed/hidden (possible using an approach like this).

@Lutymane
Copy link

I faced the same issue. My current work around is to remove Stripe frame from its original wrapper <div> and place it in its own top <dialog> and then use that old <div> to clean up the <dialog>.

Really this behavior should be default in Stripe itself already, since showModal() is a baseline feature and easy to fall back with existing logic.

{
  /**
   * [wrap, dialog]
   */
  const map = new WeakMap<HTMLElement, HTMLDialogElement>();

  const mo = new MutationObserver(mutations => {
    for (let { addedNodes, removedNodes } of mutations) {
      for (let addedNode of addedNodes) {
        // @note node should be element
        if (!(addedNode instanceof HTMLElement)) continue;

        // @note element is not a dialog (fired when we insert dialog with stripe frame)
        if (addedNode.tagName === "DIALOG") continue;

        let stripeFrame = addedNode.querySelector("[name^=__privateStripeFrame]");

        if (stripeFrame) {
          const stripeDialog = document.createElement("dialog");

          stripeDialog.style.width = "100%";
          stripeDialog.style.height = "100%";

          // @note keep its original div wrapper for dialog clean up reasons
          map.set(stripeFrame.parentElement!, stripeDialog);

          // @note append stripe frame itself to our new dialog and then we
          stripeDialog.append(stripeFrame);
          document.body.append(stripeDialog);

          stripeDialog.showModal();
        }
      }

      // @note stripe.js::this.unmount removes wrapper element from <body> so we can track that too
      for (let removedNode of removedNodes) {
        const dialog = map.get(removedNode as HTMLElement);

        if (!dialog) continue;

        dialog.remove();
      }
    }
  });

  mo.observe(document.body, { childList: true });
}

@wesbos
Copy link

wesbos commented Aug 28, 2024

The fix via stripe support was to move the generation of tokens to the server, but I was too far in to change my solutions, so I made this brutal hook to check for the modal. Hopefully this is fixed soon

/**
 * Check if the Stripe modal is open.
 * @link https://github.com/stripe/stripe-js/issues/599
 * Stripe has no way to tell if their modal is open, and when using a dialog .showmodal(), it opens overtop of the Stripe modal.
 * This hook checks if the Stripe modal is open
 */
export function useStripeModal() {
  const [open, setOpen] = useState(false);
  const [checkoutNeedsToBeReopened, setCheckoutNeedsToBeReopened] = useState(false);
  const dialog = document.querySelector<HTMLDialogElement>('dialog.checkout');
  // Continually check if there is a div with the class of .StripeElement
  useInterval(() => {
    const stripeModal = document.querySelector(
      'div[tabindex="-1"] > iframe[name*="__privateStripeFrame"]'
    );
    setOpen(!!stripeModal);
  }, 250);

  useEffect(() => {
    console.log('Stripe modal status changed', open);
    // If the dialog is open and the stripe modal is open, close the dialog
    if(dialog?.open && open) {
      dialog.close();
      setCheckoutNeedsToBeReopened(true);
    } else if (checkoutNeedsToBeReopened && !open) {
      // If the dialog was closed and the stripe modal is closed, reopen the dialog
      dialog?.showModal();
    }
    // If the stripe modal is closed
  }, [open])
  return open;
}

@ohnotnow
Copy link

ohnotnow commented Oct 8, 2024

Somewhat tangential - but possibly of 👀 for @wesbos . Caleb Porzio was recently talking about the weirdness with the top level 'dialog' layer vs. z-index vs. manual vs. ..... stuff for his new component library.

https://notesonwork.transistor.fm/episodes/modals-and-popover-woes

(Not an affiliate link, blah)

@Magellol
Copy link

Magellol commented Oct 10, 2024

We also use the dialog element and ultimately faced the same issue with the 3D secure frame rendering below our modal that sits on the top layer.

I implemented something similar as shared by @Lutymane. It works well but I'm a bit worried about it relying on some implementation details from this library. For instance if the __privateStripeFrame name changes, it could break all of a sudden. I guess we need to be extra careful upgrading this lib since we rely on internals.

If we can't solve the z-index issue right away, it would be great if stripe would let us know when such frames like 3D secures are being displayed so we don't have to rely on the lib internals.

Note: this fix also works for stripe captcha challenges. It uses the same mechanism.

@L-Blondy
Copy link

L-Blondy commented Nov 9, 2024

Also an issue for us

@rzv-me
Copy link

rzv-me commented Nov 18, 2024

the same for us. it would be great if stripe would at least emit events for when this happens

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working pinned
Projects
None yet
Development

No branches or pull requests