diff --git a/.changeset/rabby-qr-overlay.md b/.changeset/rabby-qr-overlay.md new file mode 100644 index 00000000000..9e495b15ee0 --- /dev/null +++ b/.changeset/rabby-qr-overlay.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Fix Rabby mobile connections and keep the WalletConnect QR overlay interactive and unstacked 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..b0d737ed86a --- /dev/null +++ b/packages/thirdweb/src/wallets/wallet-connect/qr-overlay.test.ts @@ -0,0 +1,61 @@ +// @vitest-environment happy-dom +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createQROverlay } from "./qr-overlay.js"; + +const URI = "wc:1234@2?relay-protocol=irn&symKey=abcd"; + +function overlayElements(): HTMLElement[] { + return Array.from(document.body.children).filter( + (el): el is HTMLElement => + el instanceof HTMLElement && el.style.zIndex === "9999", + ); +} + +function pressEscape() { + document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); +} + +describe("createQROverlay", () => { + beforeEach(() => { + document.body.innerHTML = ""; + document.body.style.pointerEvents = ""; + }); + + it("stays interactive when pointer events are disabled on body", () => { + document.body.style.pointerEvents = "none"; + + const overlay = createQROverlay(URI); + + expect(overlayElements()[0]?.style.pointerEvents).toBe("auto"); + overlay.destroy(); + }); + + it("replaces the previous overlay instead of stacking", () => { + createQROverlay(URI); + const overlay = createQROverlay(URI); + + expect(overlayElements()).toHaveLength(1); + overlay.destroy(); + }); + + it("detaches the replaced overlay's listeners", () => { + const firstCancel = vi.fn(); + const secondCancel = vi.fn(); + createQROverlay(URI, { onCancel: firstCancel }); + createQROverlay(URI, { onCancel: secondCancel }); + + pressEscape(); + + expect(firstCancel).not.toHaveBeenCalled(); + expect(secondCancel).toHaveBeenCalledTimes(1); + }); + + it("does not remove a newer overlay when an older one is destroyed", () => { + const first = createQROverlay(URI); + createQROverlay(URI); + + first.destroy(); + + 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..2207299d1ee 100644 --- a/packages/thirdweb/src/wallets/wallet-connect/qr-overlay.ts +++ b/packages/thirdweb/src/wallets/wallet-connect/qr-overlay.ts @@ -53,6 +53,9 @@ export interface QROverlay { show: () => void; } +// Only one overlay is shown at a time; creating a new one replaces the previous one. +let removeActiveOverlay: (() => void) | undefined; + /** * Creates a QR code overlay for the given WalletConnect URI */ @@ -69,7 +72,10 @@ export function createQROverlay( onCancel, } = options; + removeActiveOverlay?.(); + // Create overlay backdrop + // pointer-events is explicit so the overlay stays interactive when the host page disables it on body const overlay = document.createElement("div"); overlay.style.cssText = ` position: fixed; @@ -77,6 +83,7 @@ export function createQROverlay( 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; @@ -252,9 +259,23 @@ export function createQROverlay( // Append to container container.appendChild(overlay); + function removeNow() { + document.removeEventListener("keydown", handleEscapeKey); + overlay.removeEventListener("click", handleOverlayClick); + overlay.remove(); + style.remove(); + if (removeActiveOverlay === removeNow) { + removeActiveOverlay = undefined; + } + } + removeActiveOverlay = removeNow; + function destroyOverlay(userInitiated = false) { document.removeEventListener("keydown", handleEscapeKey); overlay.removeEventListener("click", handleOverlayClick); + if (removeActiveOverlay === removeNow) { + removeActiveOverlay = undefined; + } // Call onCancel callback only if user initiated the close action if (userInitiated && onCancel) {