Bug: Modal actions hidden on some Shopify Mobile Android devices

import { Modal } from "@shopify/polaris";

@shopify/polaris version 13.9.5

On Samsung Galaxy S24 running Shopify Mobile for Android v10.2618.0 everything appears to be fine and the “Cancel” and “Okay” buttons are clickable:

On the Google Pixel Phone running the same version of Shopify Mobile for Android, the “Cancel” and “Okay” buttons are inaccessible and obstructed by the Sidekick button:

+1 on this. We have a merchant reporting missing modal buttons in our app on their iPhone 14 Pro Max in the Shopify mobile app.

Never had any previous reports of this problem, no recent app changes on our end, and haven’t been able to replicate the issue on any of our own test devices.

We’ve had a few users reach out with the same issue, and I’m personally running into it on a Pixel 8 device as well when accessing modals within third-party apps; it also seems possibly tied to the state of the Sidekick at the time? Unsure of that specifically, but I can reproduce as follows:

Android: When I clear storage & clear cache for the Shopify app, login again, and access a third-party app, modals render properly (below, left); tapping into the Sidekick brings up a full-screen chat window (below, right):

If I close the app, re-open (without cache/storage clearing), modals now render behind the nav bar (below, left), and Sidekick chats open just within the nav bar itself (below, right); no amount of tapping/editing settings in this bar seems to resolve the issue:

I’ve also reproduced the same issue on an iPhone 12 Pro Max - instead of cache/storage clearing, I uninstalled & reinstalled the app, logged in again, and opened it for the first time - modals render properly, same behaviour as Android with full-screen Sidekick chat.

If I then close the Shopify app (swipe up, swipe up again to remove from the app carousel), then re-open it (without re-installing), modal now renders behind the nav bar, same behaviour as Android with nav bar-only Sidekick chat.

Both devices running the latest version (10.2619.0) of the Shopify app, but the issue was present in at least one version prior to that.

Just bumping this thread.

Bumping this again as the bug has returned in Shopify mobile version 10.2621.0, on both iOS and Android devices. Same repro steps as described above.

As a work-around we had Claude write us a substitute called AppModal and AppModal.Section for Polaris Modal and Modal.Section.

On small displays where Shopify Mobile displays the floating toolbar, this component displays our own full screen modal using a div with the action buttons at the bottom padded with space. On larger displays it falls back to using the Polaris Modal component.

To use import AppModal.js and replace all Modal / Modal.Section with AppModal / AppModal.Section

import AppModal from "AppModal";

AppModal.js

// On condensed (mobile) displays the Modal footer buttons are currently 
// buggy, so we render the same content inside a full screen <Page> instead.
// Use Polaris Modal on non-condensed displays.
//
// Supported props mirror the subset of Polaris <Modal>:
//   open, title, sectioned, size, primaryAction, secondaryActions, onClose, children.
// Wrap children in <AppModal.Section> the same way you'd use <Modal.Section>.

import React, { useEffect, useCallback, createContext, useContext } from "react";
import { Modal, Page, Button, InlineStack, useBreakpoints } from "@shopify/polaris";

const ModeContext = createContext("modal"); // "modal" | "page"

const AppModal = ({
  open,
  title,
  sectioned = false,
  size = "medium",
  primaryAction,
  secondaryActions = [],
  onClose,
  children,
}) => {
  const { mdDown } = useBreakpoints();
  const useFullscreenPage = mdDown;

  const handleKey = useCallback(
    (e) => {
      if (e.key === "Escape" && onClose) onClose();
    },
    [onClose],
  );

  useEffect(() => {
    if (!open || !useFullscreenPage) return undefined;
    document.addEventListener("keydown", handleKey);
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", handleKey);
      document.body.style.overflow = prevOverflow;
    };
  }, [open, useFullscreenPage, handleKey]);

  if (!open) return null;

  if (!useFullscreenPage) {
    return (
      <ModeContext.Provider value="modal">
        <Modal
          open={open}
          title={title}
          sectioned={sectioned}
          size={size}
          primaryAction={primaryAction}
          secondaryActions={secondaryActions}
          onClose={onClose}
        >
          {children}
        </Modal>
      </ModeContext.Provider>
    );
  }

  return (
    <ModeContext.Provider value="page">
      <div
        role="dialog"
        aria-modal="true"
        aria-label={typeof title === "string" ? title : undefined}
        style={{
          position: "fixed",
          inset: 0,
          zIndex: 510,
          background: "var(--p-color-bg, #f6f6f7)",
          overflowY: "auto",
          WebkitOverflowScrolling: "touch",
        }}
      >
        <Page
          title={title}
          backAction={
            onClose ? { content: "Close", onAction: onClose } : undefined
          }
          fullWidth
        >
          <div style={{ padding: sectioned ? "16px 0" : 0 }}>{children}</div>
          {(primaryAction ||
            (Array.isArray(secondaryActions) &&
              secondaryActions.length > 0)) && (
              <div style={{ padding: "16px 12px 80px" }}>
                <InlineStack gap="200" wrap align="end">
                  {Array.isArray(secondaryActions) &&
                    secondaryActions.map((action, i) => {
                      if (!action) return null;
                      const {
                        content,
                        onAction,
                        loading,
                        disabled,
                        destructive,
                        icon,
                        size,
                      } = action;
                      return (
                        <Button
                          key={i}
                          onClick={onAction}
                          loading={loading}
                          disabled={disabled}
                          tone={destructive ? "critical" : undefined}
                          icon={icon}
                          size={size}
                        >
                          {content}
                        </Button>
                      );
                    })}
                  {primaryAction &&
                    (() => {
                      const {
                        content,
                        onAction,
                        loading,
                        disabled,
                        destructive,
                        icon,
                        size,
                      } = primaryAction;
                      return (
                        <Button
                          variant="primary"
                          onClick={onAction}
                          loading={loading}
                          disabled={disabled}
                          tone={destructive ? "critical" : undefined}
                          icon={icon}
                          size={size}
                        >
                          {content}
                        </Button>
                      );
                    })()}
                </InlineStack>
              </div>
            )}
        </Page>
      </div>
    </ModeContext.Provider>
  );
};

const Section = ({ children }) => {
  const mode = useContext(ModeContext);
  if (mode === "page") {
    return (
      <div
        style={{
          padding: "16px 16px",
          borderBottom: "1px solid var(--p-color-border-secondary, #e1e3e5)",
        }}
      >
        {children}
      </div>
    );
  }
  return <Modal.Section>{children}</Modal.Section>;
};

AppModal.Section = Section;

export default AppModal;