@multiplatform.one/web3 7.34.3 → 7.34.4
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.
- package/README.md +8 -2
- package/dist/cjs/bundler/metaMaskConnectTimeouts.cjs +15 -6
- package/dist/cjs/bundler/metaMaskConnectTimeouts.native.js +15 -6
- package/dist/cjs/bundler/metaMaskConnectTimeouts.native.js.map +1 -1
- package/dist/cjs/connectkit/openConnector/device.native.js.map +1 -1
- package/dist/cjs/keycloak/device.cjs +3 -1
- package/dist/cjs/keycloak/device.native.js +4 -2
- package/dist/cjs/keycloak/device.native.js.map +1 -1
- package/dist/cjs/vite.native.js.map +1 -1
- package/dist/esm/bundler/metaMaskConnectTimeouts.mjs +15 -6
- package/dist/esm/bundler/metaMaskConnectTimeouts.mjs.map +1 -1
- package/dist/esm/bundler/metaMaskConnectTimeouts.native.js +15 -6
- package/dist/esm/bundler/metaMaskConnectTimeouts.native.js.map +1 -1
- package/dist/esm/connectkit/openConnector/device.mjs.map +1 -1
- package/dist/esm/connectkit/openConnector/device.native.js.map +1 -1
- package/dist/esm/keycloak/device.mjs +3 -1
- package/dist/esm/keycloak/device.mjs.map +1 -1
- package/dist/esm/keycloak/device.native.js +4 -2
- package/dist/esm/keycloak/device.native.js.map +1 -1
- package/dist/esm/vite.mjs.map +1 -1
- package/dist/esm/vite.native.js.map +1 -1
- package/dist/jsx/bundler/metaMaskConnectTimeouts.mjs +15 -6
- package/dist/jsx/bundler/metaMaskConnectTimeouts.mjs.map +1 -1
- package/dist/jsx/bundler/metaMaskConnectTimeouts.native.js +15 -6
- package/dist/jsx/bundler/metaMaskConnectTimeouts.native.js.map +1 -1
- package/dist/jsx/connectkit/openConnector/device.mjs.map +1 -1
- package/dist/jsx/connectkit/openConnector/device.native.js.map +1 -1
- package/dist/jsx/keycloak/device.mjs +3 -1
- package/dist/jsx/keycloak/device.mjs.map +1 -1
- package/dist/jsx/keycloak/device.native.js +4 -2
- package/dist/jsx/keycloak/device.native.js.map +1 -1
- package/dist/jsx/vite.mjs.map +1 -1
- package/dist/jsx/vite.native.js.map +1 -1
- package/dist/keycloak/login.iife.js +277 -277
- package/package.json +6 -6
- package/src/bundler/metaMaskConnectTimeouts.spec.ts +82 -7
- package/src/bundler/metaMaskConnectTimeouts.ts +29 -10
- package/src/connectkit/connectors/metaMaskConnectConnector.spec.ts +32 -0
- package/src/connectkit/openConnector/context.tsx +3 -3
- package/src/connectkit/openConnector/device.spec.ts +27 -0
- package/src/connectkit/openConnector/device.ts +5 -4
- package/src/esbuild.ts +3 -3
- package/src/keycloak/device.spec.ts +39 -0
- package/src/keycloak/device.ts +5 -1
- package/src/vite.ts +5 -3
- package/types/bundler/metaMaskConnectTimeouts.d.ts +6 -4
- package/types/bundler/metaMaskConnectTimeouts.d.ts.map +1 -1
- package/types/connectkit/openConnector/context.d.ts +3 -3
- package/types/connectkit/openConnector/device.d.ts +5 -4
- package/types/connectkit/openConnector/device.d.ts.map +1 -1
- package/types/esbuild.d.ts +3 -3
- package/types/keycloak/device.d.ts +1 -0
- package/types/keycloak/device.d.ts.map +1 -1
- package/types/vite.d.ts +5 -3
- package/types/vite.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/web3",
|
|
3
|
-
"version": "7.34.
|
|
3
|
+
"version": "7.34.4",
|
|
4
4
|
"description": "Web3 wallet integration for multiplatform.one ecosystem with ConnectKit and wagmi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"connectkit",
|
|
@@ -120,10 +120,10 @@
|
|
|
120
120
|
"@aave/account": "^0.2.1",
|
|
121
121
|
"@coinbase/wallet-sdk": "^4.4.0",
|
|
122
122
|
"@metamask/connect-evm": "^2.1.1",
|
|
123
|
-
"@multiplatform.one/connectkit": "^7.34.
|
|
124
|
-
"@multiplatform.one/gpg": "^7.34.
|
|
125
|
-
"@multiplatform.one/platform": "^7.34.
|
|
126
|
-
"@multiplatform.one/theme": "^7.34.
|
|
123
|
+
"@multiplatform.one/connectkit": "^7.34.4",
|
|
124
|
+
"@multiplatform.one/gpg": "^7.34.4",
|
|
125
|
+
"@multiplatform.one/platform": "^7.34.4",
|
|
126
|
+
"@multiplatform.one/theme": "^7.34.4",
|
|
127
127
|
"@noble/ciphers": "^1.3.0",
|
|
128
128
|
"@noble/curves": "^1.9.7",
|
|
129
129
|
"@solana/kit": "^8.5.0",
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"wagmi": "^3.7.1"
|
|
147
147
|
},
|
|
148
148
|
"devDependencies": {
|
|
149
|
-
"@multiplatform.one/config": "7.34.
|
|
149
|
+
"@multiplatform.one/config": "7.34.4",
|
|
150
150
|
"@tamagui/build": "2.7.6",
|
|
151
151
|
"@tanstack/react-query": "^5.104.3",
|
|
152
152
|
"@testing-library/jest-dom": "^7.0.1",
|
|
@@ -49,6 +49,9 @@ const multichainFiles = exportedFiles(multichainDir);
|
|
|
49
49
|
const dappClientFiles = exportedFiles(dappClientDir);
|
|
50
50
|
const read = (file: string) => readFileSync(file, "utf8");
|
|
51
51
|
|
|
52
|
+
const iPadCheck =
|
|
53
|
+
/if \(window\.navigator\.maxTouchPoints > 1 && \/Macintosh\/\.test\(window\.navigator\.userAgent\)\) return true;/g;
|
|
54
|
+
|
|
52
55
|
const pageLoadResume =
|
|
53
56
|
/resumeSession_fn = function\(session, options\)[\s\S]*?isContinuingPriorAttempt \? this\.options\.resumeTimeout : options \? this\.options\.connectionTimeout : 120 \* 1e3/;
|
|
54
57
|
const stockConnectionTimer =
|
|
@@ -96,6 +99,18 @@ describe("the timer patches on the installed MetaMask Connect", () => {
|
|
|
96
99
|
}
|
|
97
100
|
});
|
|
98
101
|
|
|
102
|
+
it("adds the iPad touch check to the platform check once", () => {
|
|
103
|
+
for (const file of multichainFiles) {
|
|
104
|
+
const code = read(file);
|
|
105
|
+
expect(code).not.toContain("maxTouchPoints");
|
|
106
|
+
const patched = patchMetaMaskConnectTimeouts(file, code)!.code;
|
|
107
|
+
expect(patched.match(iPadCheck)).toHaveLength(1);
|
|
108
|
+
expect(patched).toMatch(
|
|
109
|
+
/function isMobile\d*\(\) \{\n {2}if \(window\.navigator\.maxTouchPoints > 1/,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
99
114
|
it("fails the build when a package renames the marker it looks for", () => {
|
|
100
115
|
const [multichain] = multichainFiles;
|
|
101
116
|
const [dappClient] = dappClientFiles;
|
|
@@ -109,9 +124,7 @@ describe("the timer patches on the installed MetaMask Connect", () => {
|
|
|
109
124
|
),
|
|
110
125
|
).toBeUndefined();
|
|
111
126
|
renamedGrace.patch(dappClient!, read(dappClient!));
|
|
112
|
-
expect(renamedGrace.problem()).toMatch(
|
|
113
|
-
/left the timers in @metamask\/connect-multichain unpatched/,
|
|
114
|
-
);
|
|
127
|
+
expect(renamedGrace.problem()).toMatch(/left @metamask\/connect-multichain unpatched/);
|
|
115
128
|
|
|
116
129
|
const renamedHandshake = createMetaMaskConnectTimeoutTracker();
|
|
117
130
|
renamedHandshake.see(connectEvmEntry);
|
|
@@ -121,7 +134,7 @@ describe("the timer patches on the installed MetaMask Connect", () => {
|
|
|
121
134
|
read(dappClient!).replaceAll("HANDSHAKE_TIMEOUT", "HANDSHAKE_GRACE"),
|
|
122
135
|
);
|
|
123
136
|
expect(renamedHandshake.problem()).toMatch(
|
|
124
|
-
/left
|
|
137
|
+
/left @metamask\/mobile-wallet-protocol-dapp-client unpatched/,
|
|
125
138
|
);
|
|
126
139
|
});
|
|
127
140
|
|
|
@@ -142,6 +155,12 @@ describe("the timer patches on the installed MetaMask Connect", () => {
|
|
|
142
155
|
read(dappClient!).replace("HANDSHAKE_TIMEOUT = 60 * 1e3;", "HANDSHAKE_TIMEOUT = 45 * 1e3;"),
|
|
143
156
|
),
|
|
144
157
|
).toThrow(/timer HANDSHAKE_TIMEOUT not found/);
|
|
158
|
+
expect(() =>
|
|
159
|
+
patchMetaMaskConnectTimeouts(
|
|
160
|
+
multichain!,
|
|
161
|
+
code.replace(/function isMobile(\d*)\(\)/, "function isPhone$1()"),
|
|
162
|
+
),
|
|
163
|
+
).toThrow(/platform check isMobile not found .* would lose the MetaMask app on an iPad/);
|
|
145
164
|
// A resume that no longer reads the connection timer must not send the
|
|
146
165
|
// rewrite on to startSession's copy of the expression.
|
|
147
166
|
expect(() =>
|
|
@@ -259,6 +278,61 @@ describe("the patched MetaMask Connect transport", () => {
|
|
|
259
278
|
});
|
|
260
279
|
});
|
|
261
280
|
|
|
281
|
+
const userAgents = {
|
|
282
|
+
iPadSafari:
|
|
283
|
+
"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",
|
|
284
|
+
iPhone:
|
|
285
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
286
|
+
windows:
|
|
287
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/** connect-multichain's getPlatformType from its node build, with the patch applied or not. */
|
|
291
|
+
function loadGetPlatformType(patched: boolean): () => string {
|
|
292
|
+
const file = requireHere.resolve(multichainPkg);
|
|
293
|
+
const code = patched ? patchMetaMaskConnectTimeouts(file, read(file))!.code : read(file);
|
|
294
|
+
const wrapper = runInThisContext(
|
|
295
|
+
`(function (module, exports, require, __filename, __dirname) {${code}\nreturn (init_platform(), getPlatformType);\n})`,
|
|
296
|
+
{ filename: file },
|
|
297
|
+
);
|
|
298
|
+
const module = { exports: {} };
|
|
299
|
+
return wrapper(module, module.exports, createRequire(file), file, dirname(file));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
describe("the patched MetaMask Connect platform check", () => {
|
|
303
|
+
const stock = loadGetPlatformType(false);
|
|
304
|
+
const patched = loadGetPlatformType(true);
|
|
305
|
+
|
|
306
|
+
function on(userAgent: string, maxTouchPoints: number) {
|
|
307
|
+
const navigator = { userAgent, maxTouchPoints, product: "Gecko" };
|
|
308
|
+
vi.stubGlobal("navigator", navigator);
|
|
309
|
+
vi.stubGlobal("window", { navigator });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
313
|
+
|
|
314
|
+
it("takes an iPad on Safari's desktop site for mobile web, which stock MetaMask Connect does not", () => {
|
|
315
|
+
on(userAgents.iPadSafari, 5);
|
|
316
|
+
expect(stock()).toBe("web-desktop");
|
|
317
|
+
expect(patched()).toBe("web-mobile");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("keeps a Mac, which reports no touch points, on desktop web", () => {
|
|
321
|
+
on(userAgents.iPadSafari, 0);
|
|
322
|
+
expect(patched()).toBe("web-desktop");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("keeps a touch-screen Windows laptop on desktop web", () => {
|
|
326
|
+
on(userAgents.windows, 10);
|
|
327
|
+
expect(patched()).toBe("web-desktop");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("still takes an iPhone for mobile web", () => {
|
|
331
|
+
on(userAgents.iPhone, 5);
|
|
332
|
+
expect(patched()).toBe("web-mobile");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
262
336
|
const entryId = "mpo-web3-timer-entry";
|
|
263
337
|
const entrySource = 'export { createEVMClient } from "@metamask/connect-evm";';
|
|
264
338
|
|
|
@@ -285,6 +359,7 @@ function expectLongTimers(code: string) {
|
|
|
285
359
|
expect(code).toMatch(/resumeTimeout : [\w$]+ \? this\.options\.connectionTimeout : 120 \* 1e3/);
|
|
286
360
|
expect(code).not.toMatch(/DEFAULT_REQUEST_TIMEOUT[\w$]* = 60 \* 1e3;\s+CONNECTION_GRACE_PERIOD/);
|
|
287
361
|
expect(code).not.toMatch(/HANDSHAKE_TIMEOUT[\w$]* = 60 \* 1e3;/);
|
|
362
|
+
expect(code.match(iPadCheck)).toHaveLength(1);
|
|
288
363
|
}
|
|
289
364
|
|
|
290
365
|
function viteBuild(plugins: Plugin[]) {
|
|
@@ -315,7 +390,7 @@ describe("the bundler plugins", () => {
|
|
|
315
390
|
|
|
316
391
|
it("fails a vite build that bundles MetaMask Connect with a marker gone", async () => {
|
|
317
392
|
await expect(viteBuild([entry, renameGracePeriod, metaMaskConnectTimeouts()])).rejects.toThrow(
|
|
318
|
-
/left
|
|
393
|
+
/left @metamask\/connect-multichain unpatched/,
|
|
319
394
|
);
|
|
320
395
|
}, 120_000);
|
|
321
396
|
|
|
@@ -351,7 +426,7 @@ describe("the bundler plugins", () => {
|
|
|
351
426
|
|
|
352
427
|
it("fails the dev server's pre-bundle when a marker is gone", async () => {
|
|
353
428
|
await expect(preBundle([renameGracePeriod])).rejects.toThrow(
|
|
354
|
-
/left
|
|
429
|
+
/left @metamask\/connect-multichain unpatched/,
|
|
355
430
|
);
|
|
356
431
|
}, 120_000);
|
|
357
432
|
|
|
@@ -387,7 +462,7 @@ describe("the bundler plugins", () => {
|
|
|
387
462
|
},
|
|
388
463
|
};
|
|
389
464
|
await expect(esbuildBundle([renamed])).rejects.toThrow(
|
|
390
|
-
/left
|
|
465
|
+
/left @metamask\/connect-multichain unpatched/,
|
|
391
466
|
);
|
|
392
467
|
}, 120_000);
|
|
393
468
|
});
|
|
@@ -13,16 +13,25 @@
|
|
|
13
13
|
// MetaMask and connector calls wait on the resume. The resume made without
|
|
14
14
|
// connect options (the page-load one) keeps the stock 120 s.
|
|
15
15
|
//
|
|
16
|
+
// The same patch makes MetaMask Connect's platform check count an iPad. iPadOS
|
|
17
|
+
// Safari sends a Mac user agent by default, so MetaMask Connect took it for a
|
|
18
|
+
// desktop and showed its install/QR modal instead of opening the MetaMask app.
|
|
19
|
+
// A Mac reports no touch points, so it keeps the extension and QR path.
|
|
20
|
+
//
|
|
16
21
|
// A pattern that stops matching fails the build instead of quietly shipping the
|
|
17
|
-
// short timers again.
|
|
22
|
+
// short timers, or the desktop iPad, again.
|
|
18
23
|
|
|
19
24
|
export type MetaMaskConnectTimerRewrite = {
|
|
20
|
-
/**
|
|
21
|
-
|
|
25
|
+
/** What the rewrite changes, as a build error names it. */
|
|
26
|
+
target: string;
|
|
27
|
+
/** What a build without the rewrite would lose, as a build error names it. */
|
|
28
|
+
lost: string;
|
|
22
29
|
pattern: RegExp;
|
|
23
30
|
replacement: string;
|
|
24
31
|
};
|
|
25
32
|
|
|
33
|
+
const longTimers = "the long Android timers";
|
|
34
|
+
|
|
26
35
|
export type MetaMaskConnectTimeoutPatch = {
|
|
27
36
|
pkg: string;
|
|
28
37
|
marker: string;
|
|
@@ -35,16 +44,25 @@ export const metaMaskConnectTimeoutPatches: readonly MetaMaskConnectTimeoutPatch
|
|
|
35
44
|
marker: "CONNECTION_GRACE_PERIOD",
|
|
36
45
|
rewrites: [
|
|
37
46
|
{
|
|
38
|
-
|
|
47
|
+
target: "timer DEFAULT_REQUEST_TIMEOUT",
|
|
48
|
+
lost: longTimers,
|
|
39
49
|
pattern: /(DEFAULT_REQUEST_TIMEOUT\d*) = 60 \* 1e3;(\s+CONNECTION_GRACE_PERIOD =)/,
|
|
40
50
|
replacement: "$1 = 600 * 1e3;$2",
|
|
41
51
|
},
|
|
42
52
|
{
|
|
43
|
-
|
|
53
|
+
target: "timer the page-load session resume",
|
|
54
|
+
lost: longTimers,
|
|
44
55
|
pattern:
|
|
45
56
|
/(resumeSession_fn = function\((\w+), (\w+)\)(?:(?!startSession_fn)[\s\S])*?isContinuingPriorAttempt \? this\.options\.resumeTimeout : )this\.options\.connectionTimeout/,
|
|
46
57
|
replacement: "$1$3 ? this.options.connectionTimeout : 120 * 1e3",
|
|
47
58
|
},
|
|
59
|
+
{
|
|
60
|
+
target: "platform check isMobile",
|
|
61
|
+
lost: "the MetaMask app on an iPad",
|
|
62
|
+
pattern: /(function isMobile\d*\(\) \{)(?=[^}]*?\.parse\(window\.navigator\.userAgent\))/,
|
|
63
|
+
replacement:
|
|
64
|
+
"$1\n if (window.navigator.maxTouchPoints > 1 && /Macintosh/.test(window.navigator.userAgent)) return true;",
|
|
65
|
+
},
|
|
48
66
|
],
|
|
49
67
|
},
|
|
50
68
|
{
|
|
@@ -52,7 +70,8 @@ export const metaMaskConnectTimeoutPatches: readonly MetaMaskConnectTimeoutPatch
|
|
|
52
70
|
marker: "HANDSHAKE_TIMEOUT",
|
|
53
71
|
rewrites: [
|
|
54
72
|
{
|
|
55
|
-
|
|
73
|
+
target: "timer HANDSHAKE_TIMEOUT",
|
|
74
|
+
lost: longTimers,
|
|
56
75
|
pattern: /(HANDSHAKE_TIMEOUT) = 60 \* 1e3;/,
|
|
57
76
|
replacement: "$1 = 540 * 1e3;",
|
|
58
77
|
},
|
|
@@ -89,7 +108,7 @@ export function patchMetaMaskConnectTimeouts(
|
|
|
89
108
|
for (const rewrite of patch.rewrites) {
|
|
90
109
|
if (!rewrite.pattern.test(patched))
|
|
91
110
|
throw new Error(
|
|
92
|
-
`MetaMask Connect
|
|
111
|
+
`MetaMask Connect ${rewrite.target} not found in ${id}: ${patch.pkg} changed its source, and a build without this rewrite would lose ${rewrite.lost}`,
|
|
93
112
|
);
|
|
94
113
|
patched = patched.replace(rewrite.pattern, rewrite.replacement);
|
|
95
114
|
}
|
|
@@ -98,7 +117,7 @@ export function patchMetaMaskConnectTimeouts(
|
|
|
98
117
|
|
|
99
118
|
/**
|
|
100
119
|
* Counts what one build patched, so the build can refuse to ship MetaMask Connect
|
|
101
|
-
* with a
|
|
120
|
+
* with a patch left out (a package renamed the marker the patch looks for).
|
|
102
121
|
*/
|
|
103
122
|
export function createMetaMaskConnectTimeoutTracker() {
|
|
104
123
|
const patched = new Map<string, number>();
|
|
@@ -113,14 +132,14 @@ export function createMetaMaskConnectTimeoutTracker() {
|
|
|
113
132
|
if (result) patched.set(result.pkg, (patched.get(result.pkg) ?? 0) + 1);
|
|
114
133
|
return result;
|
|
115
134
|
},
|
|
116
|
-
/** Error text when the build bundles MetaMask Connect with a
|
|
135
|
+
/** Error text when the build bundles MetaMask Connect with a patch left out. */
|
|
117
136
|
problem(): string | undefined {
|
|
118
137
|
if (!bundlesMetaMaskConnect) return undefined;
|
|
119
138
|
const missing = metaMaskConnectTimeoutPatches
|
|
120
139
|
.map((patch) => patch.pkg)
|
|
121
140
|
.filter((pkg) => !patched.get(pkg));
|
|
122
141
|
if (!missing.length) return undefined;
|
|
123
|
-
return `the build bundles ${metaMaskConnectPkg} but left
|
|
142
|
+
return `the build bundles ${metaMaskConnectPkg} but left ${missing.join(", ")} unpatched`;
|
|
124
143
|
},
|
|
125
144
|
};
|
|
126
145
|
}
|
|
@@ -620,6 +620,38 @@ describe("metaMaskConnect switchChain on a phone", () => {
|
|
|
620
620
|
expect(addCalls()).toHaveLength(1);
|
|
621
621
|
});
|
|
622
622
|
|
|
623
|
+
describe("without an explicit mobile", () => {
|
|
624
|
+
const macUserAgent =
|
|
625
|
+
"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";
|
|
626
|
+
|
|
627
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
628
|
+
|
|
629
|
+
it("takes the phone path on an iPad on Safari's desktop site, which sends a Mac user agent", async () => {
|
|
630
|
+
vi.stubGlobal("navigator", { userAgent: macUserAgent, maxTouchPoints: 5 });
|
|
631
|
+
const connector = build([baseSepolia, bscTestnet]);
|
|
632
|
+
await connector.connect();
|
|
633
|
+
|
|
634
|
+
await connector.switchChain!({ chainId: baseSepolia.id });
|
|
635
|
+
|
|
636
|
+
expect(client.switchChain).not.toHaveBeenCalled();
|
|
637
|
+
expect(addCalls()).toHaveLength(1);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("keeps a Mac, which reports no touch points, on the desktop path", async () => {
|
|
641
|
+
vi.stubGlobal("navigator", { userAgent: macUserAgent, maxTouchPoints: 0 });
|
|
642
|
+
const connector = build([baseSepolia, bscTestnet]);
|
|
643
|
+
await connector.connect();
|
|
644
|
+
|
|
645
|
+
await connector.switchChain!({ chainId: baseSepolia.id });
|
|
646
|
+
|
|
647
|
+
expect(addCalls()).toHaveLength(0);
|
|
648
|
+
expect(client.switchChain).toHaveBeenCalledWith({
|
|
649
|
+
chainId: hex(baseSepolia.id),
|
|
650
|
+
chainConfiguration: expect.objectContaining({ chainId: hex(baseSepolia.id) }),
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
623
655
|
it("keeps the desktop switch as MetaMask Connect does it, with the chain configuration", async () => {
|
|
624
656
|
const connector = build([baseSepolia], { mobile: false });
|
|
625
657
|
await connector.connect();
|
|
@@ -31,9 +31,9 @@ export type MobileConnectOptions = {
|
|
|
31
31
|
/**
|
|
32
32
|
* Connector id a phone connects straight to (e.g. `"metaMaskSDK"`): `openSIWE()`
|
|
33
33
|
* and the default ConnectKit button skip the wallet list for it. Desktop keeps
|
|
34
|
-
* the list. A phone here is what connectkit's `isMobile()` reads
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* the list. A phone here is what connectkit's `isMobile()` reads: an iPad on
|
|
35
|
+
* Safari's default desktop site counts, a Mac does not, and Android's
|
|
36
|
+
* "Desktop site" keeps the list.
|
|
37
37
|
*/
|
|
38
38
|
preferredConnector?: string;
|
|
39
39
|
/** Passed to `openConnector` for the preferred connector. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
|
+
import { isMobile } from "@multiplatform.one/connectkit";
|
|
2
3
|
import { afterEach, describe, expect, it } from "vitest";
|
|
3
4
|
|
|
4
5
|
import { defaultOpenConnectorFallbackMs, isTouchDevice } from "./device";
|
|
@@ -11,6 +12,8 @@ const IPADOS_SAFARI_UA =
|
|
|
11
12
|
"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
13
|
const ANDROID_DESKTOP_SITE_UA =
|
|
13
14
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
|
|
15
|
+
const IPHONE_UA =
|
|
16
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1";
|
|
14
17
|
const ANDROID_UA =
|
|
15
18
|
"Mozilla/5.0 (Linux; Android 16; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36";
|
|
16
19
|
const WINDOWS_UA =
|
|
@@ -84,3 +87,27 @@ describe("defaultOpenConnectorFallbackMs", () => {
|
|
|
84
87
|
expect(DEFAULT_DESKTOP_OPEN_CONNECTOR_FALLBACK_MS).toBeGreaterThan(0);
|
|
85
88
|
});
|
|
86
89
|
});
|
|
90
|
+
|
|
91
|
+
describe("connectkit's isMobile", () => {
|
|
92
|
+
it("counts an iPad on Safari's default desktop site, which sends a Mac user agent", () => {
|
|
93
|
+
setNavigator(IPADOS_SAFARI_UA, 5);
|
|
94
|
+
expect(isMobile()).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not count a Mac, which reports no touch points", () => {
|
|
98
|
+
setNavigator(IPADOS_SAFARI_UA, 0);
|
|
99
|
+
expect(isMobile()).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("counts an iPhone and an Android phone", () => {
|
|
103
|
+
setNavigator(IPHONE_UA, 5);
|
|
104
|
+
expect(isMobile()).toBe(true);
|
|
105
|
+
setNavigator(ANDROID_UA, 5);
|
|
106
|
+
expect(isMobile()).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("does not count a touch-screen Windows laptop", () => {
|
|
110
|
+
setNavigator(WINDOWS_UA, 10);
|
|
111
|
+
expect(isMobile()).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -9,10 +9,11 @@ type DeviceNavigator = { userAgent?: string; maxTouchPoints?: number };
|
|
|
9
9
|
type MatchMedia = (query: string) => { matches: boolean };
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* A touch device whose browser sends a desktop user agent
|
|
13
|
-
* `isMobile()`
|
|
14
|
-
*
|
|
15
|
-
* screen is not its primary pointer, so neither
|
|
12
|
+
* A touch device whose browser sends a desktop user agent: iPadOS Safari sends a
|
|
13
|
+
* Mac one, which connectkit's `isMobile()` also counts, and Android's "Desktop
|
|
14
|
+
* site" a Linux one, which `isMobile()` reads as desktop. A Mac reports no touch
|
|
15
|
+
* points, and a laptop's touch screen is not its primary pointer, so neither
|
|
16
|
+
* counts.
|
|
16
17
|
*/
|
|
17
18
|
export function isTouchDevice(
|
|
18
19
|
nav: DeviceNavigator | null = typeof navigator === "undefined" ? null : navigator,
|
package/src/esbuild.ts
CHANGED
|
@@ -14,9 +14,9 @@ export type MetaMaskConnectTimeoutsEsbuildBuild = {
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* esbuild counterpart of `@multiplatform.one/web3/vite`'s metaMaskConnectTimeouts:
|
|
17
|
-
* raises MetaMask Connect's relay timers to ten minutes
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* raises MetaMask Connect's relay timers to ten minutes and makes it count an
|
|
18
|
+
* iPad as a tablet in a bundle that carries the wallet modal, and fails the
|
|
19
|
+
* build if it bundles MetaMask Connect with a patch left out.
|
|
20
20
|
*/
|
|
21
21
|
export function metaMaskConnectTimeoutsEsbuild() {
|
|
22
22
|
return {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { isMobileBrowser } from "./device";
|
|
4
|
+
|
|
5
|
+
const macUserAgent =
|
|
6
|
+
"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";
|
|
7
|
+
const iPhoneUserAgent =
|
|
8
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1";
|
|
9
|
+
const windowsUserAgent =
|
|
10
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
|
|
11
|
+
|
|
12
|
+
describe("isMobileBrowser", () => {
|
|
13
|
+
it("counts an iPad on Safari's default desktop site, which sends a Mac user agent", () => {
|
|
14
|
+
expect(isMobileBrowser({ userAgent: macUserAgent, maxTouchPoints: 5 })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("does not count a Mac, which reports no touch points", () => {
|
|
18
|
+
expect(isMobileBrowser({ userAgent: macUserAgent, maxTouchPoints: 0 })).toBe(false);
|
|
19
|
+
expect(isMobileBrowser({ userAgent: macUserAgent })).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("counts an iPhone", () => {
|
|
23
|
+
expect(isMobileBrowser({ userAgent: iPhoneUserAgent, maxTouchPoints: 5 })).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("does not count a touch-screen Windows laptop", () => {
|
|
27
|
+
expect(isMobileBrowser({ userAgent: windowsUserAgent, maxTouchPoints: 10 })).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("trusts the browser's own mobile hint where it gives one", () => {
|
|
31
|
+
expect(
|
|
32
|
+
isMobileBrowser({
|
|
33
|
+
userAgent: windowsUserAgent,
|
|
34
|
+
maxTouchPoints: 10,
|
|
35
|
+
userAgentData: { mobile: true },
|
|
36
|
+
}),
|
|
37
|
+
).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/keycloak/device.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type NavigatorLike = {
|
|
2
2
|
userAgent?: string;
|
|
3
|
+
maxTouchPoints?: number;
|
|
3
4
|
userAgentData?: { mobile?: boolean };
|
|
4
5
|
userActivation?: { isActive: boolean };
|
|
5
6
|
};
|
|
@@ -8,7 +9,10 @@ const nav = () => (globalThis as { navigator?: NavigatorLike }).navigator;
|
|
|
8
9
|
|
|
9
10
|
export function isMobileBrowser(navigator: NavigatorLike | undefined = nav()): boolean {
|
|
10
11
|
if (typeof navigator?.userAgentData?.mobile === "boolean") return navigator.userAgentData.mobile;
|
|
11
|
-
|
|
12
|
+
const userAgent = navigator?.userAgent ?? "";
|
|
13
|
+
if (/Android|iPhone|iPad|iPod/i.test(userAgent)) return true;
|
|
14
|
+
// iPadOS Safari sends a Mac user agent; a Mac reports no touch points.
|
|
15
|
+
return /Macintosh/.test(userAgent) && (navigator?.maxTouchPoints ?? 0) > 1;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
/** Whether the page holds a user gesture; true where the browser does not report it. */
|
package/src/vite.ts
CHANGED
|
@@ -8,10 +8,12 @@ const name = "mpo-web3:metamask-connect-timeouts";
|
|
|
8
8
|
* Raises MetaMask Connect's relay timers to ten minutes in every bundle that
|
|
9
9
|
* carries @multiplatform.one/web3's MetaMask connector. MetaMask Connect has no
|
|
10
10
|
* option for them, and on Android 16 a user who stays in MetaMask past 60 s
|
|
11
|
-
* otherwise loses the connect or the signature.
|
|
11
|
+
* otherwise loses the connect or the signature. It also makes MetaMask Connect
|
|
12
|
+
* take an iPad on Safari's default desktop site for the tablet it is, so the
|
|
13
|
+
* MetaMask app opens instead of the desktop QR. Add it to any vite (or rollup /
|
|
12
14
|
* rolldown) build that bundles the wallet modal; the build, and the dev server's
|
|
13
|
-
* dependency pre-bundle, fail if they bundle MetaMask Connect and a
|
|
14
|
-
* not be
|
|
15
|
+
* dependency pre-bundle, fail if they bundle MetaMask Connect and a patch could
|
|
16
|
+
* not be applied.
|
|
15
17
|
*/
|
|
16
18
|
export function metaMaskConnectTimeouts(): Plugin {
|
|
17
19
|
const tracker = createMetaMaskConnectTimeoutTracker();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type MetaMaskConnectTimerRewrite = {
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/** What the rewrite changes, as a build error names it. */
|
|
3
|
+
target: string;
|
|
4
|
+
/** What a build without the rewrite would lose, as a build error names it. */
|
|
5
|
+
lost: string;
|
|
4
6
|
pattern: RegExp;
|
|
5
7
|
replacement: string;
|
|
6
8
|
};
|
|
@@ -22,7 +24,7 @@ export declare function patchMetaMaskConnectTimeouts(id: string, code: string):
|
|
|
22
24
|
} | undefined;
|
|
23
25
|
/**
|
|
24
26
|
* Counts what one build patched, so the build can refuse to ship MetaMask Connect
|
|
25
|
-
* with a
|
|
27
|
+
* with a patch left out (a package renamed the marker the patch looks for).
|
|
26
28
|
*/
|
|
27
29
|
export declare function createMetaMaskConnectTimeoutTracker(): {
|
|
28
30
|
see(id: string): void;
|
|
@@ -30,7 +32,7 @@ export declare function createMetaMaskConnectTimeoutTracker(): {
|
|
|
30
32
|
pkg: string;
|
|
31
33
|
code: string;
|
|
32
34
|
} | undefined;
|
|
33
|
-
/** Error text when the build bundles MetaMask Connect with a
|
|
35
|
+
/** Error text when the build bundles MetaMask Connect with a patch left out. */
|
|
34
36
|
problem(): string | undefined;
|
|
35
37
|
};
|
|
36
38
|
//# sourceMappingURL=metaMaskConnectTimeouts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metaMaskConnectTimeouts.d.ts","sourceRoot":"","sources":["../../src/bundler/metaMaskConnectTimeouts.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"metaMaskConnectTimeouts.d.ts","sourceRoot":"","sources":["../../src/bundler/metaMaskConnectTimeouts.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,2BAA2B,GAAG;IACxC,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAIF,MAAM,MAAM,2BAA2B,GAAG;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,2BAA2B,EAAE,CAAC;CAClD,CAAC;AAEF,eAAO,MAAM,6BAA6B,EAAE,SAAS,2BAA2B,EAuC/E,CAAC;AAMF,2EAA2E;AAC3E,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,GACX,2BAA2B,GAAG,SAAS,CAKzC;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,GACX;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAY3C;AAED;;;GAGG;AACH,wBAAgB,mCAAmC;YAIvC,MAAM;cAGJ,MAAM,QAAQ,MAAM;aAzBxB,MAAM;cAAQ,MAAM;;IA+B1B,gFAAgF;eACrE,MAAM,GAAG,SAAS;EAShC"}
|
|
@@ -7,9 +7,9 @@ export type MobileConnectOptions = {
|
|
|
7
7
|
/**
|
|
8
8
|
* Connector id a phone connects straight to (e.g. `"metaMaskSDK"`): `openSIWE()`
|
|
9
9
|
* and the default ConnectKit button skip the wallet list for it. Desktop keeps
|
|
10
|
-
* the list. A phone here is what connectkit's `isMobile()` reads
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* the list. A phone here is what connectkit's `isMobile()` reads: an iPad on
|
|
11
|
+
* Safari's default desktop site counts, a Mac does not, and Android's
|
|
12
|
+
* "Desktop site" keeps the list.
|
|
13
13
|
*/
|
|
14
14
|
preferredConnector?: string;
|
|
15
15
|
/** Passed to `openConnector` for the preferred connector. */
|
|
@@ -6,10 +6,11 @@ type MatchMedia = (query: string) => {
|
|
|
6
6
|
matches: boolean;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
|
-
* A touch device whose browser sends a desktop user agent
|
|
10
|
-
* `isMobile()`
|
|
11
|
-
*
|
|
12
|
-
* screen is not its primary pointer, so neither
|
|
9
|
+
* A touch device whose browser sends a desktop user agent: iPadOS Safari sends a
|
|
10
|
+
* Mac one, which connectkit's `isMobile()` also counts, and Android's "Desktop
|
|
11
|
+
* site" a Linux one, which `isMobile()` reads as desktop. A Mac reports no touch
|
|
12
|
+
* points, and a laptop's touch screen is not its primary pointer, so neither
|
|
13
|
+
* counts.
|
|
13
14
|
*/
|
|
14
15
|
export declare function isTouchDevice(nav?: DeviceNavigator | null, matchMedia?: MatchMedia | null): boolean;
|
|
15
16
|
/** The `fallbackMs` an `openConnector` call without one gets on this device. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../../src/connectkit/openConnector/device.ts"],"names":[],"mappings":"AAOA,KAAK,eAAe,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AACvE,KAAK,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAE1D
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../../src/connectkit/openConnector/device.ts"],"names":[],"mappings":"AAOA,KAAK,eAAe,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AACvE,KAAK,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,GAAG,GAAE,eAAe,GAAG,IAA0D,EACjF,UAAU,GAAE,UAAU,GAAG,IAEc,GACtC,OAAO,CAQT;AAED,gFAAgF;AAChF,wBAAgB,8BAA8B,IAAI,MAAM,CAIvD"}
|
package/types/esbuild.d.ts
CHANGED
|
@@ -19,9 +19,9 @@ export type MetaMaskConnectTimeoutsEsbuildBuild = {
|
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
21
|
* esbuild counterpart of `@multiplatform.one/web3/vite`'s metaMaskConnectTimeouts:
|
|
22
|
-
* raises MetaMask Connect's relay timers to ten minutes
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* raises MetaMask Connect's relay timers to ten minutes and makes it count an
|
|
23
|
+
* iPad as a tablet in a bundle that carries the wallet modal, and fails the
|
|
24
|
+
* build if it bundles MetaMask Connect with a patch left out.
|
|
25
25
|
*/
|
|
26
26
|
export declare function metaMaskConnectTimeoutsEsbuild(): {
|
|
27
27
|
name: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../src/keycloak/device.ts"],"names":[],"mappings":"AAAA,KAAK,aAAa,GAAG;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACrC,cAAc,CAAC,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;CACxC,CAAC;AAIF,wBAAgB,eAAe,CAAC,SAAS,GAAE,aAAa,GAAG,SAAiB,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../src/keycloak/device.ts"],"names":[],"mappings":"AAAA,KAAK,aAAa,GAAG;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACrC,cAAc,CAAC,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;CACxC,CAAC;AAIF,wBAAgB,eAAe,CAAC,SAAS,GAAE,aAAa,GAAG,SAAiB,GAAG,OAAO,CAMrF;AAED,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,aAAa,GAAG,SAAiB,GAAG,OAAO,CAEvF"}
|
package/types/vite.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ import type { Plugin } from "vite";
|
|
|
3
3
|
* Raises MetaMask Connect's relay timers to ten minutes in every bundle that
|
|
4
4
|
* carries @multiplatform.one/web3's MetaMask connector. MetaMask Connect has no
|
|
5
5
|
* option for them, and on Android 16 a user who stays in MetaMask past 60 s
|
|
6
|
-
* otherwise loses the connect or the signature.
|
|
6
|
+
* otherwise loses the connect or the signature. It also makes MetaMask Connect
|
|
7
|
+
* take an iPad on Safari's default desktop site for the tablet it is, so the
|
|
8
|
+
* MetaMask app opens instead of the desktop QR. Add it to any vite (or rollup /
|
|
7
9
|
* rolldown) build that bundles the wallet modal; the build, and the dev server's
|
|
8
|
-
* dependency pre-bundle, fail if they bundle MetaMask Connect and a
|
|
9
|
-
* not be
|
|
10
|
+
* dependency pre-bundle, fail if they bundle MetaMask Connect and a patch could
|
|
11
|
+
* not be applied.
|
|
10
12
|
*/
|
|
11
13
|
export declare function metaMaskConnectTimeouts(): Plugin;
|
|
12
14
|
//# sourceMappingURL=vite.d.ts.map
|
package/types/vite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAMnC
|
|
1
|
+
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAMnC;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CA4ChD"}
|