@multiplatform.one/web3 7.17.0 → 7.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/dist/cjs/connectkit/components/Common/ConnectorList/index.cjs +33 -5
  2. package/dist/cjs/connectkit/components/Common/ConnectorList/index.native.js +38 -5
  3. package/dist/cjs/connectkit/components/Common/ConnectorList/index.native.js.map +1 -1
  4. package/dist/cjs/connectkit/components/ConnectButton/index.cjs +5 -0
  5. package/dist/cjs/connectkit/components/ConnectButton/index.native.js +5 -0
  6. package/dist/cjs/connectkit/components/ConnectButton/index.native.js.map +1 -1
  7. package/dist/cjs/connectkit/components/ConnectModal/ConnectWithInjector/index.cjs +9 -4
  8. package/dist/cjs/connectkit/components/ConnectModal/ConnectWithInjector/index.native.js +12 -7
  9. package/dist/cjs/connectkit/components/ConnectModal/ConnectWithInjector/index.native.js.map +1 -1
  10. package/dist/cjs/connectkit/index.cjs +5 -0
  11. package/dist/cjs/connectkit/index.native.js +5 -0
  12. package/dist/cjs/connectkit/index.native.js.map +1 -1
  13. package/dist/cjs/connectkit/openConnector/context.cjs +146 -0
  14. package/dist/cjs/connectkit/openConnector/context.native.js +188 -0
  15. package/dist/cjs/connectkit/openConnector/context.native.js.map +1 -0
  16. package/dist/cjs/connectkit/openConnector/device.cjs +42 -0
  17. package/dist/cjs/connectkit/openConnector/device.native.js +51 -0
  18. package/dist/cjs/connectkit/openConnector/device.native.js.map +1 -0
  19. package/dist/cjs/connectkit/openConnector/index.cjs +35 -0
  20. package/dist/cjs/connectkit/openConnector/index.native.js +38 -0
  21. package/dist/cjs/connectkit/openConnector/index.native.js.map +1 -0
  22. package/dist/cjs/connectkit/openConnector/launcher.cjs +133 -0
  23. package/dist/cjs/connectkit/openConnector/launcher.native.js +158 -0
  24. package/dist/cjs/connectkit/openConnector/launcher.native.js.map +1 -0
  25. package/dist/cjs/connectkit/screens.cjs +1 -10
  26. package/dist/cjs/connectkit/screens.native.js +1 -12
  27. package/dist/cjs/connectkit/screens.native.js.map +1 -1
  28. package/dist/cjs/connectkit/siwe/connectIntent.cjs +9 -5
  29. package/dist/cjs/connectkit/siwe/connectIntent.native.js +10 -3
  30. package/dist/cjs/connectkit/siwe/connectIntent.native.js.map +1 -1
  31. package/dist/cjs/connectkit/useWeb3Modal.cjs +21 -0
  32. package/dist/cjs/connectkit/useWeb3Modal.native.js +22 -0
  33. package/dist/cjs/connectkit/useWeb3Modal.native.js.map +1 -1
  34. package/dist/cjs/connectkit/web3Bridge.cjs +4 -1
  35. package/dist/cjs/connectkit/web3Bridge.native.js +4 -1
  36. package/dist/cjs/connectkit/web3Bridge.native.js.map +1 -1
  37. package/dist/cjs/connectkit/web3Provider.native.js.map +1 -1
  38. package/dist/esm/connectkit/components/Common/ConnectorList/index.mjs +34 -6
  39. package/dist/esm/connectkit/components/Common/ConnectorList/index.mjs.map +1 -1
  40. package/dist/esm/connectkit/components/Common/ConnectorList/index.native.js +39 -6
  41. package/dist/esm/connectkit/components/Common/ConnectorList/index.native.js.map +1 -1
  42. package/dist/esm/connectkit/components/ConnectButton/index.mjs +5 -0
  43. package/dist/esm/connectkit/components/ConnectButton/index.mjs.map +1 -1
  44. package/dist/esm/connectkit/components/ConnectButton/index.native.js +5 -0
  45. package/dist/esm/connectkit/components/ConnectButton/index.native.js.map +1 -1
  46. package/dist/esm/connectkit/components/ConnectModal/ConnectWithInjector/index.mjs +9 -4
  47. package/dist/esm/connectkit/components/ConnectModal/ConnectWithInjector/index.mjs.map +1 -1
  48. package/dist/esm/connectkit/components/ConnectModal/ConnectWithInjector/index.native.js +12 -7
  49. package/dist/esm/connectkit/components/ConnectModal/ConnectWithInjector/index.native.js.map +1 -1
  50. package/dist/esm/connectkit/index.mjs +3 -1
  51. package/dist/esm/connectkit/index.mjs.map +1 -1
  52. package/dist/esm/connectkit/index.native.js +3 -1
  53. package/dist/esm/connectkit/index.native.js.map +1 -1
  54. package/dist/esm/connectkit/openConnector/context.mjs +118 -0
  55. package/dist/esm/connectkit/openConnector/context.mjs.map +1 -0
  56. package/dist/esm/connectkit/openConnector/context.native.js +146 -0
  57. package/dist/esm/connectkit/openConnector/context.native.js.map +1 -0
  58. package/dist/esm/connectkit/openConnector/device.mjs +16 -0
  59. package/dist/esm/connectkit/openConnector/device.mjs.map +1 -0
  60. package/dist/esm/connectkit/openConnector/device.native.js +22 -0
  61. package/dist/esm/connectkit/openConnector/device.native.js.map +1 -0
  62. package/dist/esm/connectkit/openConnector/index.mjs +4 -0
  63. package/dist/esm/connectkit/openConnector/index.mjs.map +1 -0
  64. package/dist/esm/connectkit/openConnector/index.native.js +4 -0
  65. package/dist/esm/connectkit/openConnector/index.native.js.map +1 -0
  66. package/dist/esm/connectkit/openConnector/launcher.mjs +106 -0
  67. package/dist/esm/connectkit/openConnector/launcher.mjs.map +1 -0
  68. package/dist/esm/connectkit/openConnector/launcher.native.js +128 -0
  69. package/dist/esm/connectkit/openConnector/launcher.native.js.map +1 -0
  70. package/dist/esm/connectkit/screens.mjs +2 -11
  71. package/dist/esm/connectkit/screens.mjs.map +1 -1
  72. package/dist/esm/connectkit/screens.native.js +2 -13
  73. package/dist/esm/connectkit/screens.native.js.map +1 -1
  74. package/dist/esm/connectkit/siwe/connectIntent.mjs +9 -6
  75. package/dist/esm/connectkit/siwe/connectIntent.mjs.map +1 -1
  76. package/dist/esm/connectkit/siwe/connectIntent.native.js +10 -4
  77. package/dist/esm/connectkit/siwe/connectIntent.native.js.map +1 -1
  78. package/dist/esm/connectkit/useWeb3Modal.mjs +22 -1
  79. package/dist/esm/connectkit/useWeb3Modal.mjs.map +1 -1
  80. package/dist/esm/connectkit/useWeb3Modal.native.js +23 -1
  81. package/dist/esm/connectkit/useWeb3Modal.native.js.map +1 -1
  82. package/dist/esm/connectkit/web3Bridge.mjs +4 -1
  83. package/dist/esm/connectkit/web3Bridge.mjs.map +1 -1
  84. package/dist/esm/connectkit/web3Bridge.native.js +4 -1
  85. package/dist/esm/connectkit/web3Bridge.native.js.map +1 -1
  86. package/dist/esm/connectkit/web3Provider.mjs.map +1 -1
  87. package/dist/esm/connectkit/web3Provider.native.js.map +1 -1
  88. package/dist/jsx/connectkit/components/Common/ConnectorList/index.mjs +34 -6
  89. package/dist/jsx/connectkit/components/Common/ConnectorList/index.mjs.map +1 -1
  90. package/dist/jsx/connectkit/components/Common/ConnectorList/index.native.js +38 -5
  91. package/dist/jsx/connectkit/components/Common/ConnectorList/index.native.js.map +1 -1
  92. package/dist/jsx/connectkit/components/ConnectButton/index.mjs +5 -0
  93. package/dist/jsx/connectkit/components/ConnectButton/index.mjs.map +1 -1
  94. package/dist/jsx/connectkit/components/ConnectButton/index.native.js +5 -0
  95. package/dist/jsx/connectkit/components/ConnectButton/index.native.js.map +1 -1
  96. package/dist/jsx/connectkit/components/ConnectModal/ConnectWithInjector/index.mjs +9 -4
  97. package/dist/jsx/connectkit/components/ConnectModal/ConnectWithInjector/index.mjs.map +1 -1
  98. package/dist/jsx/connectkit/components/ConnectModal/ConnectWithInjector/index.native.js +12 -7
  99. package/dist/jsx/connectkit/components/ConnectModal/ConnectWithInjector/index.native.js.map +1 -1
  100. package/dist/jsx/connectkit/index.mjs +3 -1
  101. package/dist/jsx/connectkit/index.mjs.map +1 -1
  102. package/dist/jsx/connectkit/index.native.js +5 -0
  103. package/dist/jsx/connectkit/index.native.js.map +1 -1
  104. package/dist/jsx/connectkit/openConnector/context.mjs +118 -0
  105. package/dist/jsx/connectkit/openConnector/context.mjs.map +1 -0
  106. package/dist/jsx/connectkit/openConnector/context.native.js +188 -0
  107. package/dist/jsx/connectkit/openConnector/context.native.js.map +1 -0
  108. package/dist/jsx/connectkit/openConnector/device.mjs +16 -0
  109. package/dist/jsx/connectkit/openConnector/device.mjs.map +1 -0
  110. package/dist/jsx/connectkit/openConnector/device.native.js +51 -0
  111. package/dist/jsx/connectkit/openConnector/device.native.js.map +1 -0
  112. package/dist/jsx/connectkit/openConnector/index.mjs +4 -0
  113. package/dist/jsx/connectkit/openConnector/index.mjs.map +1 -0
  114. package/dist/jsx/connectkit/openConnector/index.native.js +38 -0
  115. package/dist/jsx/connectkit/openConnector/index.native.js.map +1 -0
  116. package/dist/jsx/connectkit/openConnector/launcher.mjs +106 -0
  117. package/dist/jsx/connectkit/openConnector/launcher.mjs.map +1 -0
  118. package/dist/jsx/connectkit/openConnector/launcher.native.js +158 -0
  119. package/dist/jsx/connectkit/openConnector/launcher.native.js.map +1 -0
  120. package/dist/jsx/connectkit/screens.mjs +2 -11
  121. package/dist/jsx/connectkit/screens.mjs.map +1 -1
  122. package/dist/jsx/connectkit/screens.native.js +1 -12
  123. package/dist/jsx/connectkit/screens.native.js.map +1 -1
  124. package/dist/jsx/connectkit/siwe/connectIntent.mjs +9 -6
  125. package/dist/jsx/connectkit/siwe/connectIntent.mjs.map +1 -1
  126. package/dist/jsx/connectkit/siwe/connectIntent.native.js +10 -3
  127. package/dist/jsx/connectkit/siwe/connectIntent.native.js.map +1 -1
  128. package/dist/jsx/connectkit/useWeb3Modal.mjs +22 -1
  129. package/dist/jsx/connectkit/useWeb3Modal.mjs.map +1 -1
  130. package/dist/jsx/connectkit/useWeb3Modal.native.js +22 -0
  131. package/dist/jsx/connectkit/useWeb3Modal.native.js.map +1 -1
  132. package/dist/jsx/connectkit/web3Bridge.mjs +4 -1
  133. package/dist/jsx/connectkit/web3Bridge.mjs.map +1 -1
  134. package/dist/jsx/connectkit/web3Bridge.native.js +4 -1
  135. package/dist/jsx/connectkit/web3Bridge.native.js.map +1 -1
  136. package/dist/jsx/connectkit/web3Provider.mjs.map +1 -1
  137. package/dist/jsx/connectkit/web3Provider.native.js.map +1 -1
  138. package/package.json +7 -6
  139. package/src/connectkit/components/Common/ConnectorList/index.tsx +30 -2
  140. package/src/connectkit/components/ConnectButton/index.tsx +5 -0
  141. package/src/connectkit/components/ConnectModal/ConnectWithInjector/index.tsx +13 -8
  142. package/src/connectkit/index.ts +8 -0
  143. package/src/connectkit/openConnector/context.tsx +176 -0
  144. package/src/connectkit/openConnector/device.spec.ts +86 -0
  145. package/src/connectkit/openConnector/device.ts +37 -0
  146. package/src/connectkit/openConnector/index.ts +18 -0
  147. package/src/connectkit/openConnector/launcher.spec.ts +325 -0
  148. package/src/connectkit/openConnector/launcher.ts +203 -0
  149. package/src/connectkit/openConnector/modal.spec.tsx +330 -0
  150. package/src/connectkit/screens.tsx +2 -11
  151. package/src/connectkit/siwe/connectIntent.spec.ts +90 -0
  152. package/src/connectkit/siwe/connectIntent.ts +19 -6
  153. package/src/connectkit/useWeb3Modal.ts +25 -1
  154. package/src/connectkit/web3Bridge.tsx +4 -1
  155. package/src/connectkit/web3Provider.tsx +4 -1
  156. package/types/connectkit/components/Common/ConnectorList/index.d.ts.map +1 -1
  157. package/types/connectkit/components/ConnectButton/index.d.ts.map +1 -1
  158. package/types/connectkit/components/ConnectModal/ConnectWithInjector/index.d.ts.map +1 -1
  159. package/types/connectkit/index.d.ts +2 -0
  160. package/types/connectkit/index.d.ts.map +1 -1
  161. package/types/connectkit/openConnector/context.d.ts +48 -0
  162. package/types/connectkit/openConnector/context.d.ts.map +1 -0
  163. package/types/connectkit/openConnector/device.d.ts +18 -0
  164. package/types/connectkit/openConnector/device.d.ts.map +1 -0
  165. package/types/connectkit/openConnector/index.d.ts +3 -0
  166. package/types/connectkit/openConnector/index.d.ts.map +1 -0
  167. package/types/connectkit/openConnector/launcher.d.ts +64 -0
  168. package/types/connectkit/openConnector/launcher.d.ts.map +1 -0
  169. package/types/connectkit/screens.d.ts.map +1 -1
  170. package/types/connectkit/siwe/connectIntent.d.ts +8 -3
  171. package/types/connectkit/siwe/connectIntent.d.ts.map +1 -1
  172. package/types/connectkit/useWeb3Modal.d.ts +16 -0
  173. package/types/connectkit/useWeb3Modal.d.ts.map +1 -1
  174. package/types/connectkit/web3Bridge.d.ts.map +1 -1
  175. package/types/connectkit/web3Provider.d.ts +4 -1
  176. package/types/connectkit/web3Provider.d.ts.map +1 -1
@@ -0,0 +1,86 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+
4
+ import { defaultOpenConnectorFallbackMs, isTouchDevice } from "./device";
5
+ import {
6
+ DEFAULT_DESKTOP_OPEN_CONNECTOR_FALLBACK_MS,
7
+ DEFAULT_OPEN_CONNECTOR_FALLBACK_MS,
8
+ } from "./launcher";
9
+
10
+ const IPADOS_SAFARI_UA =
11
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15";
12
+ const ANDROID_DESKTOP_SITE_UA =
13
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
14
+ const ANDROID_UA =
15
+ "Mozilla/5.0 (Linux; Android 16; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36";
16
+ const WINDOWS_UA =
17
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
18
+
19
+ const coarse = () => ({ matches: true });
20
+ const fine = () => ({ matches: false });
21
+
22
+ function setNavigator(userAgent: string, maxTouchPoints: number) {
23
+ Object.defineProperty(window.navigator, "userAgent", { value: userAgent, configurable: true });
24
+ Object.defineProperty(window.navigator, "maxTouchPoints", {
25
+ value: maxTouchPoints,
26
+ configurable: true,
27
+ });
28
+ }
29
+
30
+ afterEach(() => {
31
+ setNavigator(WINDOWS_UA, 0);
32
+ });
33
+
34
+ describe("isTouchDevice", () => {
35
+ it("counts iPadOS Safari, which sends a Mac user agent", () => {
36
+ expect(isTouchDevice({ userAgent: IPADOS_SAFARI_UA, maxTouchPoints: 5 }, fine)).toBe(true);
37
+ });
38
+
39
+ it("does not count a Mac", () => {
40
+ expect(isTouchDevice({ userAgent: IPADOS_SAFARI_UA, maxTouchPoints: 0 }, coarse)).toBe(false);
41
+ });
42
+
43
+ it("counts Android's Desktop site: touch points and a coarse primary pointer", () => {
44
+ expect(isTouchDevice({ userAgent: ANDROID_DESKTOP_SITE_UA, maxTouchPoints: 5 }, coarse)).toBe(
45
+ true,
46
+ );
47
+ });
48
+
49
+ it("does not count a touch-screen laptop, whose primary pointer is fine", () => {
50
+ expect(isTouchDevice({ userAgent: WINDOWS_UA, maxTouchPoints: 10 }, fine)).toBe(false);
51
+ });
52
+
53
+ it("is false without a navigator (SSR) or a matchMedia", () => {
54
+ expect(isTouchDevice(null, coarse)).toBe(false);
55
+ expect(isTouchDevice({ userAgent: ANDROID_DESKTOP_SITE_UA, maxTouchPoints: 5 }, null)).toBe(
56
+ false,
57
+ );
58
+ });
59
+
60
+ it("is false when matchMedia throws", () => {
61
+ const throws = () => {
62
+ throw new Error("no media queries");
63
+ };
64
+ expect(isTouchDevice({ userAgent: ANDROID_DESKTOP_SITE_UA, maxTouchPoints: 5 }, throws)).toBe(
65
+ false,
66
+ );
67
+ });
68
+ });
69
+
70
+ describe("defaultOpenConnectorFallbackMs", () => {
71
+ it("is the phone default on a phone", () => {
72
+ setNavigator(ANDROID_UA, 5);
73
+ expect(defaultOpenConnectorFallbackMs()).toBe(DEFAULT_OPEN_CONNECTOR_FALLBACK_MS);
74
+ });
75
+
76
+ it("is the phone default on an iPad that asks for the desktop site", () => {
77
+ setNavigator(IPADOS_SAFARI_UA, 5);
78
+ expect(defaultOpenConnectorFallbackMs()).toBe(DEFAULT_OPEN_CONNECTOR_FALLBACK_MS);
79
+ });
80
+
81
+ it("is still a fallback on desktop, so an explicit call never spins forever", () => {
82
+ setNavigator(WINDOWS_UA, 0);
83
+ expect(defaultOpenConnectorFallbackMs()).toBe(DEFAULT_DESKTOP_OPEN_CONNECTOR_FALLBACK_MS);
84
+ expect(DEFAULT_DESKTOP_OPEN_CONNECTOR_FALLBACK_MS).toBeGreaterThan(0);
85
+ });
86
+ });
@@ -0,0 +1,37 @@
1
+ import { isMobile } from "@multiplatform.one/connectkit";
2
+
3
+ import {
4
+ DEFAULT_DESKTOP_OPEN_CONNECTOR_FALLBACK_MS,
5
+ DEFAULT_OPEN_CONNECTOR_FALLBACK_MS,
6
+ } from "./launcher";
7
+
8
+ type DeviceNavigator = { userAgent?: string; maxTouchPoints?: number };
9
+ type MatchMedia = (query: string) => { matches: boolean };
10
+
11
+ /**
12
+ * A touch device whose browser sends a desktop user agent, which connectkit's
13
+ * `isMobile()` reads as desktop: iPadOS Safari sends a Mac one, and Android's
14
+ * "Desktop site" a Linux one. A Mac reports no touch points, and a laptop's touch
15
+ * screen is not its primary pointer, so neither counts.
16
+ */
17
+ export function isTouchDevice(
18
+ nav: DeviceNavigator | null = typeof navigator === "undefined" ? null : navigator,
19
+ matchMedia: MatchMedia | null = typeof window === "undefined" || !window.matchMedia
20
+ ? null
21
+ : (query) => window.matchMedia(query),
22
+ ): boolean {
23
+ if (!nav || (nav.maxTouchPoints ?? 0) < 2) return false;
24
+ if (/Macintosh/.test(nav.userAgent ?? "")) return true;
25
+ try {
26
+ return matchMedia?.("(pointer: coarse)").matches ?? false;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /** The `fallbackMs` an `openConnector` call without one gets on this device. */
33
+ export function defaultOpenConnectorFallbackMs(): number {
34
+ return isMobile() || isTouchDevice()
35
+ ? DEFAULT_OPEN_CONNECTOR_FALLBACK_MS
36
+ : DEFAULT_DESKTOP_OPEN_CONNECTOR_FALLBACK_MS;
37
+ }
@@ -0,0 +1,18 @@
1
+ export {
2
+ createConnectorLauncher,
3
+ DEFAULT_DESKTOP_OPEN_CONNECTOR_FALLBACK_MS,
4
+ DEFAULT_OPEN_CONNECTOR_FALLBACK_MS,
5
+ type ConnectorLauncher,
6
+ type LauncherDeps,
7
+ type OpenConnectorOptions,
8
+ type PendingConnect,
9
+ } from "./launcher";
10
+ export {
11
+ ConnectorLauncherProvider,
12
+ getMobileOptions,
13
+ resolveWallet,
14
+ useConnectorLauncher,
15
+ type ConnectorLauncherValue,
16
+ type MobileConnectOptions,
17
+ type Web3ConnectKitOptions,
18
+ } from "./context";
@@ -0,0 +1,325 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { Connector } from "wagmi";
3
+
4
+ import { createConnectorLauncher, type LauncherDeps } from "./launcher";
5
+
6
+ type Message = { type: string; data?: unknown };
7
+
8
+ function fakeConnector(uid: string) {
9
+ const handlers = new Set<(message: Message) => void>();
10
+ const emitter = {
11
+ on: (_event: string, fn: (message: Message) => void) => handlers.add(fn),
12
+ off: (_event: string, fn: (message: Message) => void) => handlers.delete(fn),
13
+ emit: (message: Message) => handlers.forEach((fn) => fn(message)),
14
+ listeners: () => handlers.size,
15
+ };
16
+ return { uid, id: uid, name: uid, emitter } as unknown as Connector & {
17
+ emitter: typeof emitter;
18
+ };
19
+ }
20
+
21
+ function deferred() {
22
+ let resolve!: (value?: unknown) => void;
23
+ let reject!: (error: unknown) => void;
24
+ const promise = new Promise((res, rej) => {
25
+ resolve = res;
26
+ reject = rej;
27
+ });
28
+ return { promise, resolve, reject };
29
+ }
30
+
31
+ function setup() {
32
+ const calls: string[] = [];
33
+ const win = new EventTarget();
34
+ const doc = Object.assign(new EventTarget(), { visibilityState: "visible" });
35
+ const connects: ReturnType<typeof deferred>[] = [];
36
+ const deps: LauncherDeps = {
37
+ connect: vi.fn((connector: Connector) => {
38
+ calls.push(`connect:${connector.uid}`);
39
+ const d = deferred();
40
+ connects.push(d);
41
+ return d.promise;
42
+ }),
43
+ showConnecting: vi.fn((walletId: string) => calls.push(`connecting:${walletId}`)),
44
+ showList: vi.fn(() => calls.push("list")),
45
+ onFailed: vi.fn(),
46
+ win,
47
+ doc,
48
+ };
49
+ const launcher = createConnectorLauncher(() => deps);
50
+ return { launcher, deps, calls, win, doc, connects };
51
+ }
52
+
53
+ // Let promise callbacks run under fake timers.
54
+ const flush = () => Promise.resolve().then(() => undefined);
55
+
56
+ describe("createConnectorLauncher", () => {
57
+ beforeEach(() => {
58
+ vi.useFakeTimers();
59
+ });
60
+ afterEach(() => {
61
+ vi.useRealTimers();
62
+ });
63
+
64
+ it("calls connect() synchronously, before open() returns and before any UI change", () => {
65
+ const { launcher, deps, calls } = setup();
66
+ const mm = fakeConnector("metaMaskSDK");
67
+
68
+ launcher.open("metaMaskSDK", mm, { fallbackMs: 6000 });
69
+
70
+ expect(deps.connect).toHaveBeenCalledTimes(1);
71
+ expect(deps.connect).toHaveBeenCalledWith(mm);
72
+ expect(calls).toEqual(["connect:metaMaskSDK", "connecting:metaMaskSDK"]);
73
+ expect(launcher.pending()?.walletId).toBe("metaMaskSDK");
74
+ });
75
+
76
+ it("falls back to the list when the page never loses focus within fallbackMs", () => {
77
+ const { launcher, deps } = setup();
78
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
79
+
80
+ vi.advanceTimersByTime(5999);
81
+ expect(deps.showList).not.toHaveBeenCalled();
82
+
83
+ vi.advanceTimersByTime(1);
84
+ expect(deps.showList).toHaveBeenCalledTimes(1);
85
+ expect(deps.onFailed).not.toHaveBeenCalled();
86
+ expect(launcher.pending()).toBeNull();
87
+ });
88
+
89
+ it("keeps waiting when the wallet app took focus (blur)", () => {
90
+ const { launcher, deps, win } = setup();
91
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
92
+
93
+ win.dispatchEvent(new Event("blur"));
94
+ vi.advanceTimersByTime(10_000);
95
+
96
+ expect(deps.showList).not.toHaveBeenCalled();
97
+ expect(launcher.pending()?.walletId).toBe("metaMaskSDK");
98
+ });
99
+
100
+ it("keeps waiting when the page went hidden", () => {
101
+ const { launcher, deps, doc } = setup();
102
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
103
+
104
+ doc.visibilityState = "hidden";
105
+ doc.dispatchEvent(new Event("visibilitychange"));
106
+ vi.advanceTimersByTime(10_000);
107
+
108
+ expect(deps.showList).not.toHaveBeenCalled();
109
+ });
110
+
111
+ it("falls back to the list when connect fails", async () => {
112
+ const { launcher, deps, connects } = setup();
113
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
114
+
115
+ const error = new Error("User rejected request");
116
+ connects[0].reject(error);
117
+ await flush();
118
+
119
+ expect(deps.onFailed).toHaveBeenCalledWith(error);
120
+ expect(deps.showList).toHaveBeenCalledTimes(1);
121
+ expect(launcher.pending()).toBeNull();
122
+ vi.advanceTimersByTime(10_000);
123
+ expect(deps.showList).toHaveBeenCalledTimes(1);
124
+ });
125
+
126
+ it("ends quietly when connect lands", async () => {
127
+ const { launcher, deps, connects } = setup();
128
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
129
+
130
+ connects[0].resolve();
131
+ await flush();
132
+ vi.advanceTimersByTime(10_000);
133
+
134
+ expect(deps.showList).not.toHaveBeenCalled();
135
+ expect(launcher.pending()).toBeNull();
136
+ expect(vi.getTimerCount()).toBe(0);
137
+ });
138
+
139
+ it("re-tap re-issues the pending request on the same connector and keeps one session", async () => {
140
+ const { launcher, deps, connects } = setup();
141
+ const mm = fakeConnector("metaMaskSDK");
142
+ launcher.open("metaMaskSDK", mm, { fallbackMs: 6000 });
143
+ const first = launcher.pending();
144
+ vi.advanceTimersByTime(3000);
145
+
146
+ launcher.open("metaMaskSDK", mm, { fallbackMs: 6000 });
147
+
148
+ expect(deps.connect).toHaveBeenCalledTimes(2);
149
+ expect(deps.connect).toHaveBeenLastCalledWith(mm);
150
+ expect(launcher.pending()).toBe(first);
151
+ expect(vi.getTimerCount()).toBe(1);
152
+
153
+ // The ORIGINAL timer still decides: 3000 ms more, not 6000.
154
+ vi.advanceTimersByTime(3000);
155
+ expect(deps.showList).toHaveBeenCalledTimes(1);
156
+
157
+ // The re-issued request failing on its own does not fall back a second time.
158
+ connects[1].reject(new Error("already pending"));
159
+ await flush();
160
+ expect(deps.showList).toHaveBeenCalledTimes(1);
161
+ });
162
+
163
+ it("re-tap after the fallback starts a fresh connect", () => {
164
+ const { launcher, deps } = setup();
165
+ const mm = fakeConnector("metaMaskSDK");
166
+ launcher.open("metaMaskSDK", mm, { fallbackMs: 6000 });
167
+ vi.advanceTimersByTime(6000);
168
+
169
+ launcher.open("metaMaskSDK", mm, { fallbackMs: 6000 });
170
+
171
+ expect(deps.connect).toHaveBeenCalledTimes(2);
172
+ expect(launcher.pending()?.walletId).toBe("metaMaskSDK");
173
+ expect(vi.getTimerCount()).toBe(1);
174
+ });
175
+
176
+ it("opening another connector ends the pending one without falling back", () => {
177
+ const { launcher, deps } = setup();
178
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
179
+ launcher.open("coinbaseWalletSDK", fakeConnector("coinbaseWalletSDK"), { fallbackMs: 6000 });
180
+
181
+ expect(launcher.pending()?.walletId).toBe("coinbaseWalletSDK");
182
+ expect(vi.getTimerCount()).toBe(1);
183
+ expect(deps.showList).not.toHaveBeenCalled();
184
+ });
185
+
186
+ it("captures the wallet deeplink the connector announces", () => {
187
+ const { launcher } = setup();
188
+ const mm = fakeConnector("metaMaskSDK");
189
+ const seen: Array<string | undefined> = [];
190
+ launcher.subscribe(() => seen.push(launcher.pending()?.deeplink));
191
+
192
+ launcher.open("metaMaskSDK", mm, { fallbackMs: 6000 });
193
+ mm.emitter.emit({ type: "display_uri", data: "metamask://connect/mwp?p=abc" });
194
+
195
+ expect(launcher.pending()?.deeplink).toBe("metamask://connect/mwp?p=abc");
196
+ expect(seen).toContain("metamask://connect/mwp?p=abc");
197
+
198
+ launcher.cancel();
199
+ expect(mm.emitter.listeners()).toBe(0);
200
+ });
201
+
202
+ it("runs no timer without fallbackMs", () => {
203
+ const { launcher, deps, win } = setup();
204
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"));
205
+
206
+ expect(vi.getTimerCount()).toBe(0);
207
+ win.dispatchEvent(new Event("blur"));
208
+ win.dispatchEvent(new Event("focus"));
209
+ expect(vi.getTimerCount()).toBe(0);
210
+ vi.advanceTimersByTime(60_000);
211
+ expect(deps.showList).not.toHaveBeenCalled();
212
+ });
213
+
214
+ it("re-arms the fallback when the page comes back with the connect still pending", () => {
215
+ const { launcher, deps, win } = setup();
216
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
217
+
218
+ win.dispatchEvent(new Event("blur"));
219
+ vi.advanceTimersByTime(20_000);
220
+ expect(deps.showList).not.toHaveBeenCalled();
221
+
222
+ // A dismissed chooser, or a relay answer lost in the background.
223
+ win.dispatchEvent(new Event("focus"));
224
+ vi.advanceTimersByTime(5999);
225
+ expect(deps.showList).not.toHaveBeenCalled();
226
+ vi.advanceTimersByTime(1);
227
+ expect(deps.showList).toHaveBeenCalledTimes(1);
228
+ expect(launcher.pending()).toBeNull();
229
+ });
230
+
231
+ it("restarts the wait from the return, not from the tap", () => {
232
+ const { launcher, deps, win } = setup();
233
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
234
+
235
+ vi.advanceTimersByTime(1000);
236
+ win.dispatchEvent(new Event("blur"));
237
+ vi.advanceTimersByTime(4500);
238
+ win.dispatchEvent(new Event("focus"));
239
+
240
+ vi.advanceTimersByTime(5999);
241
+ expect(deps.showList).not.toHaveBeenCalled();
242
+ expect(vi.getTimerCount()).toBe(1);
243
+ vi.advanceTimersByTime(1);
244
+ expect(deps.showList).toHaveBeenCalledTimes(1);
245
+ });
246
+
247
+ it("re-arms when the page turns visible again", () => {
248
+ const { launcher, deps, doc } = setup();
249
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
250
+
251
+ doc.visibilityState = "hidden";
252
+ doc.dispatchEvent(new Event("visibilitychange"));
253
+ vi.advanceTimersByTime(20_000);
254
+ expect(deps.showList).not.toHaveBeenCalled();
255
+
256
+ doc.visibilityState = "visible";
257
+ doc.dispatchEvent(new Event("visibilitychange"));
258
+ vi.advanceTimersByTime(6000);
259
+ expect(deps.showList).toHaveBeenCalledTimes(1);
260
+ });
261
+
262
+ it("a page that leaves again after coming back keeps waiting", () => {
263
+ const { launcher, deps, win } = setup();
264
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
265
+
266
+ win.dispatchEvent(new Event("blur"));
267
+ win.dispatchEvent(new Event("focus"));
268
+ vi.advanceTimersByTime(2000);
269
+ win.dispatchEvent(new Event("blur"));
270
+ vi.advanceTimersByTime(20_000);
271
+
272
+ expect(deps.showList).not.toHaveBeenCalled();
273
+ expect(launcher.pending()?.walletId).toBe("metaMaskSDK");
274
+ });
275
+
276
+ it("a focus with no blur before it leaves the original timer alone", () => {
277
+ const { launcher, deps, win } = setup();
278
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
279
+
280
+ vi.advanceTimersByTime(3000);
281
+ win.dispatchEvent(new Event("focus"));
282
+ vi.advanceTimersByTime(3000);
283
+
284
+ expect(deps.showList).toHaveBeenCalledTimes(1);
285
+ });
286
+
287
+ it("cancel() ends the session and stops watching; the wagmi connect is left alone", async () => {
288
+ const { launcher, deps, win, connects } = setup();
289
+ const mm = fakeConnector("metaMaskSDK");
290
+ const seen: Array<string | null> = [];
291
+ launcher.subscribe(() => seen.push(launcher.pending()?.walletId ?? null));
292
+ launcher.open("metaMaskSDK", mm, { fallbackMs: 6000 });
293
+ win.dispatchEvent(new Event("blur"));
294
+
295
+ launcher.cancel();
296
+
297
+ expect(launcher.pending()).toBeNull();
298
+ expect(seen[seen.length - 1]).toBeNull();
299
+ expect(vi.getTimerCount()).toBe(0);
300
+ expect(mm.emitter.listeners()).toBe(0);
301
+ win.dispatchEvent(new Event("focus"));
302
+ expect(vi.getTimerCount()).toBe(0);
303
+
304
+ connects[0].reject(new Error("User rejected request"));
305
+ await flush();
306
+ vi.advanceTimersByTime(60_000);
307
+ expect(deps.showList).not.toHaveBeenCalled();
308
+ expect(deps.onFailed).not.toHaveBeenCalled();
309
+ });
310
+
311
+ it("a return after the connect landed arms nothing", async () => {
312
+ const { launcher, deps, win, connects } = setup();
313
+ launcher.open("metaMaskSDK", fakeConnector("metaMaskSDK"), { fallbackMs: 6000 });
314
+ win.dispatchEvent(new Event("blur"));
315
+
316
+ connects[0].resolve();
317
+ await flush();
318
+ win.dispatchEvent(new Event("focus"));
319
+
320
+ expect(vi.getTimerCount()).toBe(0);
321
+ expect(launcher.pending()).toBeNull();
322
+ vi.advanceTimersByTime(60_000);
323
+ expect(deps.showList).not.toHaveBeenCalled();
324
+ });
325
+ });
@@ -0,0 +1,203 @@
1
+ import type { Connector } from "wagmi";
2
+
3
+ // Opening ONE connector straight from an app's button, without showing the wallet
4
+ // list first. A phone that goes straight to MetaMask saves the user a tap, but only
5
+ // if the connect fires inside that tap: a browser opens the wallet's deeplink only
6
+ // while the tap's user activation is live, so anything that defers connect() (a
7
+ // route change, an effect, a timer) loses it. The launcher therefore calls connect()
8
+ // before it touches any UI, and hands the UI updates to its caller.
9
+ //
10
+ // When the wallet never comes up (no MetaMask app: the deeplink goes nowhere and the
11
+ // page never loses focus) or the connect fails, the user lands on the wallet list,
12
+ // so a straight-to-wallet button is never a dead end. The same holds when the user
13
+ // comes back from the wallet app and its answer never arrives (a dismissed chooser,
14
+ // a relay reply lost while the page was in the background): the page being in front
15
+ // again restarts the wait.
16
+
17
+ /** A page still in front this long after the tap never reached the wallet app. */
18
+ export const DEFAULT_OPEN_CONNECTOR_FALLBACK_MS = 6_000;
19
+
20
+ /**
21
+ * The desktop default. An extension's popup window blurs the page, but a wallet that
22
+ * answers in the page (a QR code to scan) does not, so desktop waits longer.
23
+ */
24
+ export const DEFAULT_DESKTOP_OPEN_CONNECTOR_FALLBACK_MS = 30_000;
25
+
26
+ export type OpenConnectorOptions = {
27
+ /**
28
+ * How long the page may stay in front with the connect unanswered before the
29
+ * wallet list shows: after the tap, and again each time the page comes back from
30
+ * the wallet app. Defaults to 6000 ms on a phone or tablet and to 30000 ms on
31
+ * desktop. `0` turns it off, and the connecting screen then waits for the connect
32
+ * to settle however long that takes.
33
+ */
34
+ fallbackMs?: number;
35
+ };
36
+
37
+ export type PendingConnect = {
38
+ /** The wallet-list id the connect was opened for. */
39
+ walletId: string;
40
+ connector: Connector;
41
+ /** The wallet deeplink, once the connector announces one (`display_uri`). */
42
+ deeplink?: string;
43
+ };
44
+
45
+ type Listener = (event: Event) => void;
46
+
47
+ export type LauncherDeps = {
48
+ connect: (connector: Connector) => Promise<unknown>;
49
+ /** Show the connecting state for this wallet. */
50
+ showConnecting: (walletId: string) => void;
51
+ /** Show the wallet list. */
52
+ showList: () => void;
53
+ /** A connect settled with an error; runs before the list shows. */
54
+ onFailed?: (error: unknown) => void;
55
+ win?: {
56
+ addEventListener: (type: string, listener: Listener) => void;
57
+ removeEventListener: (type: string, listener: Listener) => void;
58
+ };
59
+ doc?: {
60
+ addEventListener: (type: string, listener: Listener) => void;
61
+ removeEventListener: (type: string, listener: Listener) => void;
62
+ visibilityState?: string;
63
+ };
64
+ };
65
+
66
+ type Session = {
67
+ pending: PendingConnect;
68
+ stop: () => void;
69
+ };
70
+
71
+ export type ConnectorLauncher = {
72
+ /**
73
+ * Connect `connector` now, inside the caller's gesture. A second call for the
74
+ * connector already connecting re-issues the request inside the new tap (MetaMask
75
+ * Connect answers it by re-opening the pending request) and keeps the one
76
+ * session: no second timer, no reset of the connecting state.
77
+ */
78
+ open: (walletId: string, connector: Connector, options?: OpenConnectorOptions) => void;
79
+ pending: () => PendingConnect | null;
80
+ subscribe: (listener: () => void) => () => void;
81
+ /**
82
+ * End the pending session without falling back: the user left the connecting
83
+ * screen, a wallet connected, or the provider unmounted. The wagmi connect itself
84
+ * carries on.
85
+ */
86
+ cancel: () => void;
87
+ };
88
+
89
+ export function createConnectorLauncher(getDeps: () => LauncherDeps): ConnectorLauncher {
90
+ let session: Session | null = null;
91
+ const listeners = new Set<() => void>();
92
+ const emit = () => listeners.forEach((listener) => listener());
93
+
94
+ const end = (s: Session) => {
95
+ if (session !== s) return;
96
+ s.stop();
97
+ session = null;
98
+ emit();
99
+ };
100
+
101
+ const open: ConnectorLauncher["open"] = (walletId, connector, options = {}) => {
102
+ const deps = getDeps();
103
+
104
+ const current = session;
105
+ if (current && current.pending.connector.uid === connector.uid) {
106
+ deps.connect(connector).then(
107
+ () => end(current),
108
+ () => undefined,
109
+ );
110
+ deps.showConnecting(current.pending.walletId);
111
+ return;
112
+ }
113
+ if (current) end(current);
114
+
115
+ const win = deps.win ?? (typeof window !== "undefined" ? window : undefined);
116
+ const doc = deps.doc ?? (typeof document !== "undefined" ? document : undefined);
117
+
118
+ const fallbackMs = options.fallbackMs ?? 0;
119
+ let left = false;
120
+ let timer: ReturnType<typeof setTimeout> | undefined;
121
+ const disarm = () => {
122
+ if (timer !== undefined) clearTimeout(timer);
123
+ timer = undefined;
124
+ };
125
+ const arm = () => {
126
+ disarm();
127
+ if (fallbackMs <= 0) return;
128
+ timer = setTimeout(() => {
129
+ timer = undefined;
130
+ // While the wallet app has the page, wait for the page to come back.
131
+ if (!left) fallBack();
132
+ }, fallbackMs);
133
+ };
134
+ const onLeave = () => {
135
+ left = true;
136
+ };
137
+ const onReturn = () => {
138
+ if (!left) return;
139
+ left = false;
140
+ arm();
141
+ };
142
+ const onVisibility = () => {
143
+ if (doc?.visibilityState === "hidden") onLeave();
144
+ else onReturn();
145
+ };
146
+ const onMessage = (message: { type: string; data?: unknown }) => {
147
+ if (session !== s || message.type !== "display_uri" || typeof message.data !== "string") {
148
+ return;
149
+ }
150
+ s.pending = { ...s.pending, deeplink: message.data };
151
+ emit();
152
+ };
153
+
154
+ const s: Session = {
155
+ pending: { walletId, connector },
156
+ stop: () => {
157
+ disarm();
158
+ win?.removeEventListener("blur", onLeave);
159
+ win?.removeEventListener("pagehide", onLeave);
160
+ win?.removeEventListener("focus", onReturn);
161
+ win?.removeEventListener("pageshow", onReturn);
162
+ doc?.removeEventListener("visibilitychange", onVisibility);
163
+ connector.emitter?.off?.("message", onMessage);
164
+ },
165
+ };
166
+ session = s;
167
+
168
+ const fallBack = (error?: unknown) => {
169
+ if (session !== s) return;
170
+ end(s);
171
+ if (error !== undefined) getDeps().onFailed?.(error);
172
+ getDeps().showList();
173
+ };
174
+
175
+ connector.emitter?.on?.("message", onMessage);
176
+ deps.connect(connector).then(
177
+ () => end(s),
178
+ (error: unknown) => fallBack(error ?? new Error("connect failed")),
179
+ );
180
+
181
+ win?.addEventListener("blur", onLeave);
182
+ win?.addEventListener("pagehide", onLeave);
183
+ win?.addEventListener("focus", onReturn);
184
+ win?.addEventListener("pageshow", onReturn);
185
+ doc?.addEventListener("visibilitychange", onVisibility);
186
+ arm();
187
+
188
+ deps.showConnecting(walletId);
189
+ emit();
190
+ };
191
+
192
+ return {
193
+ open,
194
+ pending: () => session?.pending ?? null,
195
+ subscribe: (listener) => {
196
+ listeners.add(listener);
197
+ return () => listeners.delete(listener);
198
+ },
199
+ cancel: () => {
200
+ if (session) end(session);
201
+ },
202
+ };
203
+ }