diff --git a/.changeset/qr-overlay-pointer-events.md b/.changeset/qr-overlay-pointer-events.md new file mode 100644 index 00000000000..b390df76c1f --- /dev/null +++ b/.changeset/qr-overlay-pointer-events.md @@ -0,0 +1,9 @@ +--- +"thirdweb": patch +--- + +Keep the WalletConnect QR overlay usable when it is opened from inside another modal, and stop overlays stacking. + +The vanilla `createQROverlay` did not declare `pointer-events` on its root. Because the overlay is appended to `document.body`, and modal libraries commonly set `pointer-events: none` there while a dialog is open, the overlay inherited that value and became unclickable — visible at `z-index: 9999`, but with clicks passing through to the dialog behind it. It now sets `pointer-events: auto` explicitly. + +Separately, each connect attempt only tracked the overlay it created, so an abandoned attempt — or a pairing that expired and re-emitted its URI — left its overlay in the DOM and the next attempt stacked another on top. Overlays are now tagged and any stale one is removed before a new one is appended. diff --git a/.changeset/rabby-mobile-registry.md b/.changeset/rabby-mobile-registry.md new file mode 100644 index 00000000000..d6eb85d8832 --- /dev/null +++ b/.changeset/rabby-mobile-registry.md @@ -0,0 +1,7 @@ +--- +"thirdweb": patch +--- + +Fix Rabby connections on mobile and in-modal QR on desktop by refreshing its stale wallet registry entry. + +The generated entry for `io.rabby` recorded no iOS app, no Android app and no deep link, even though the WalletConnect registry has published all three for some time. As a result Rabby had no mobile hand-off available and was excluded from `WCSupportedWalletIds`, so mobile users could not reach the Rabby app and desktop users were routed onto the generic external-connect fallback instead of the in-modal WalletConnect QR screen. diff --git a/packages/thirdweb/src/wallets/__generated__/wallet-ids.ts b/packages/thirdweb/src/wallets/__generated__/wallet-ids.ts index e54b5ace2df..05e810c8103 100644 --- a/packages/thirdweb/src/wallets/__generated__/wallet-ids.ts +++ b/packages/thirdweb/src/wallets/__generated__/wallet-ids.ts @@ -2,7 +2,7 @@ // This file is auto-generated by the `scripts/wallets/generate.ts` script. // Do not modify this file manually. -// 476 wallets +// 477 wallets export type WCSupportedWalletIds = | "io.1inch.wallet" | "com.binance.wallet" @@ -32,6 +32,7 @@ export type WCSupportedWalletIds = | "com.roninchain.wallet" | "com.okex.wallet" | "com.wemixplay" + | "io.rabby" | "com.tangem" | "com.exodus" | "com.hashpack.wallet" diff --git a/packages/thirdweb/src/wallets/__generated__/wallet-infos.ts b/packages/thirdweb/src/wallets/__generated__/wallet-infos.ts index 4ab85132870..ecddffb1d18 100644 --- a/packages/thirdweb/src/wallets/__generated__/wallet-infos.ts +++ b/packages/thirdweb/src/wallets/__generated__/wallet-infos.ts @@ -2357,7 +2357,7 @@ const ALL_MINIMAL_WALLET_INFOS = [ { id: "io.rabby", name: "Rabby", - hasMobileSupport: false, + hasMobileSupport: true, }, { id: "com.brave.wallet", diff --git a/packages/thirdweb/src/wallets/__generated__/wallet/io.rabby/index.ts b/packages/thirdweb/src/wallets/__generated__/wallet/io.rabby/index.ts index ffcad3849f3..a8baed10a5a 100644 --- a/packages/thirdweb/src/wallets/__generated__/wallet/io.rabby/index.ts +++ b/packages/thirdweb/src/wallets/__generated__/wallet/io.rabby/index.ts @@ -9,25 +9,26 @@ export const wallet = { app: { browser: "https://chrome.google.com/webstore/detail/rabby/acmacodkjbdgmoleebolmdjonilkdbch", - ios: null, - android: null, - mac: null, - windows: null, - linux: null, + ios: "https://apps.apple.com/us/app/rabby-wallet-crypto-evm/id6474381673", + android: + "https://play.google.com/store/apps/details?id=com.debank.rabbymobile", + mac: "", + windows: "", + linux: "", chrome: "https://chrome.google.com/webstore/detail/rabby-wallet/acmacodkjbdgmoleebolmdjonilkdbch", - firefox: null, - safari: null, - edge: null, - opera: null, + firefox: "", + safari: "", + edge: "", + opera: "", }, rdns: "io.rabby", mobile: { - native: null, - universal: null, + native: "rabby://", + universal: "", }, desktop: { - native: null, + native: "", universal: "https://chrome.google.com/webstore/detail/rabby/acmacodkjbdgmoleebolmdjonilkdbch", }, diff --git a/packages/thirdweb/src/wallets/wallet-connect/qr-overlay.test.ts b/packages/thirdweb/src/wallets/wallet-connect/qr-overlay.test.ts new file mode 100644 index 00000000000..f4b5f1ee675 --- /dev/null +++ b/packages/thirdweb/src/wallets/wallet-connect/qr-overlay.test.ts @@ -0,0 +1,44 @@ +// @vitest-environment happy-dom +import { beforeEach, describe, expect, it } from "vitest"; +import { createQROverlay } from "./qr-overlay.js"; + +const URI = "wc:1234@2?relay-protocol=irn&symKey=abcd"; + +/** + * The overlay is identified by its own styling rather than by any attribute it + * sets, so these tests describe observable behaviour rather than implementation. + */ +function overlayElements(): HTMLElement[] { + return Array.from(document.body.children).filter( + (el): el is HTMLElement => + el instanceof HTMLElement && el.style.zIndex === "9999", + ); +} + +describe("createQROverlay", () => { + beforeEach(() => { + document.body.innerHTML = ""; + }); + + it("keeps the overlay clickable when the host page disables pointer events on body", () => { + // Modal libraries commonly do this while a dialog is open. + document.body.style.pointerEvents = "none"; + + const overlay = createQROverlay(URI); + + const [root] = overlayElements(); + expect(root).toBeDefined(); + // Without an explicit value the overlay inherits `none` from body and + // becomes unclickable despite being visible. + expect(root?.style.pointerEvents).toBe("auto"); + + overlay.destroy(); + }); + + it("replaces an overlay left behind by a previous attempt instead of stacking", () => { + createQROverlay(URI); + createQROverlay(URI); + + expect(overlayElements()).toHaveLength(1); + }); +}); diff --git a/packages/thirdweb/src/wallets/wallet-connect/qr-overlay.ts b/packages/thirdweb/src/wallets/wallet-connect/qr-overlay.ts index 24a665fe6d0..8cdf0b0cc58 100644 --- a/packages/thirdweb/src/wallets/wallet-connect/qr-overlay.ts +++ b/packages/thirdweb/src/wallets/wallet-connect/qr-overlay.ts @@ -53,6 +53,12 @@ export interface QROverlay { show: () => void; } +/** + * Marks overlays created by this module so stale ones can be cleaned up. + * @internal + */ +const QR_OVERLAY_ATTRIBUTE = "data-tw-qr-overlay"; + /** * Creates a QR code overlay for the given WalletConnect URI */ @@ -69,14 +75,26 @@ export function createQROverlay( onCancel, } = options; + // Remove overlays left behind by earlier connect attempts. Each call only + // tracks the overlay it created, so an abandoned attempt - or a pairing that + // expired and re-emitted its URI - would otherwise stack another one on top. + document.querySelectorAll(`[${QR_OVERLAY_ATTRIBUTE}]`).forEach((stale) => { + stale.remove(); + }); + // Create overlay backdrop const overlay = document.createElement("div"); + overlay.setAttribute(QR_OVERLAY_ATTRIBUTE, ""); + // pointer-events is set explicitly because the overlay is appended to + // document.body, and modal libraries commonly disable pointer events there + // while a dialog is open. Inheriting that would render the overlay unclickable. overlay.style.cssText = ` position: fixed; inset: 0; background-color: ${theme === "dark" ? "rgba(0, 0, 0, 0.8)" : "rgba(0, 0, 0, 0.5)"}; backdrop-filter: blur(10px); z-index: 9999; + pointer-events: auto; display: flex; align-items: center; justify-content: center;