@multiplatform.one/web3 7.36.0 → 7.38.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.
- package/dist/cjs/connectkit/connectors/metaMaskConnectConnector.cjs +79 -8
- package/dist/cjs/connectkit/connectors/metaMaskConnectConnector.native.js +90 -5
- package/dist/cjs/connectkit/connectors/metaMaskConnectConnector.native.js.map +1 -1
- package/dist/cjs/connectkit/connectors/restoredSession.cjs +136 -0
- package/dist/cjs/connectkit/connectors/restoredSession.native.js +297 -0
- package/dist/cjs/connectkit/connectors/restoredSession.native.js.map +1 -0
- package/dist/cjs/connectkit/defaultConfig.cjs +3 -1
- package/dist/cjs/connectkit/defaultConfig.native.js +3 -1
- package/dist/cjs/connectkit/defaultConfig.native.js.map +1 -1
- package/dist/cjs/connectkit/defaultConnectors.cjs +5 -1
- package/dist/cjs/connectkit/defaultConnectors.native.js +5 -1
- package/dist/cjs/connectkit/defaultConnectors.native.js.map +1 -1
- package/dist/cjs/connectkit/index.cjs +3 -0
- package/dist/cjs/connectkit/index.native.js +3 -0
- package/dist/cjs/connectkit/index.native.js.map +1 -1
- package/dist/cjs/keycloak/KeycloakWalletLogin.cjs +16 -2
- package/dist/cjs/keycloak/useWalletLogin.cjs +9 -2
- package/dist/esm/connectkit/connectors/metaMaskConnectConnector.mjs +79 -8
- package/dist/esm/connectkit/connectors/metaMaskConnectConnector.mjs.map +1 -1
- package/dist/esm/connectkit/connectors/metaMaskConnectConnector.native.js +90 -5
- package/dist/esm/connectkit/connectors/metaMaskConnectConnector.native.js.map +1 -1
- package/dist/esm/connectkit/connectors/restoredSession.mjs +107 -0
- package/dist/esm/connectkit/connectors/restoredSession.mjs.map +1 -0
- package/dist/esm/connectkit/connectors/restoredSession.native.js +265 -0
- package/dist/esm/connectkit/connectors/restoredSession.native.js.map +1 -0
- package/dist/esm/connectkit/defaultConfig.mjs +3 -1
- package/dist/esm/connectkit/defaultConfig.mjs.map +1 -1
- package/dist/esm/connectkit/defaultConfig.native.js +3 -1
- package/dist/esm/connectkit/defaultConfig.native.js.map +1 -1
- package/dist/esm/connectkit/defaultConnectors.mjs +5 -1
- package/dist/esm/connectkit/defaultConnectors.mjs.map +1 -1
- package/dist/esm/connectkit/defaultConnectors.native.js +5 -1
- package/dist/esm/connectkit/defaultConnectors.native.js.map +1 -1
- package/dist/esm/connectkit/index.mjs +2 -1
- package/dist/esm/connectkit/index.mjs.map +1 -1
- package/dist/esm/connectkit/index.native.js +2 -1
- package/dist/esm/connectkit/index.native.js.map +1 -1
- package/dist/esm/keycloak/KeycloakWalletLogin.mjs +16 -2
- package/dist/esm/keycloak/KeycloakWalletLogin.mjs.map +1 -1
- package/dist/esm/keycloak/useWalletLogin.mjs +10 -3
- package/dist/esm/keycloak/useWalletLogin.mjs.map +1 -1
- package/dist/jsx/connectkit/connectors/metaMaskConnectConnector.mjs +79 -8
- package/dist/jsx/connectkit/connectors/metaMaskConnectConnector.mjs.map +1 -1
- package/dist/jsx/connectkit/connectors/metaMaskConnectConnector.native.js +90 -5
- package/dist/jsx/connectkit/connectors/metaMaskConnectConnector.native.js.map +1 -1
- package/dist/jsx/connectkit/connectors/restoredSession.mjs +107 -0
- package/dist/jsx/connectkit/connectors/restoredSession.mjs.map +1 -0
- package/dist/jsx/connectkit/connectors/restoredSession.native.js +297 -0
- package/dist/jsx/connectkit/connectors/restoredSession.native.js.map +1 -0
- package/dist/jsx/connectkit/defaultConfig.mjs +3 -1
- package/dist/jsx/connectkit/defaultConfig.mjs.map +1 -1
- package/dist/jsx/connectkit/defaultConfig.native.js +3 -1
- package/dist/jsx/connectkit/defaultConfig.native.js.map +1 -1
- package/dist/jsx/connectkit/defaultConnectors.mjs +5 -1
- package/dist/jsx/connectkit/defaultConnectors.mjs.map +1 -1
- package/dist/jsx/connectkit/defaultConnectors.native.js +5 -1
- package/dist/jsx/connectkit/defaultConnectors.native.js.map +1 -1
- package/dist/jsx/connectkit/index.mjs +2 -1
- package/dist/jsx/connectkit/index.mjs.map +1 -1
- package/dist/jsx/connectkit/index.native.js +3 -0
- package/dist/jsx/connectkit/index.native.js.map +1 -1
- package/dist/jsx/keycloak/KeycloakWalletLogin.mjs +16 -2
- package/dist/jsx/keycloak/KeycloakWalletLogin.mjs.map +1 -1
- package/dist/jsx/keycloak/useWalletLogin.mjs +10 -3
- package/dist/jsx/keycloak/useWalletLogin.mjs.map +1 -1
- package/dist/keycloak/login.iife.js +356 -356
- package/package.json +6 -6
- package/src/connectkit/connectors/metaMaskConnectConnector.spec.ts +316 -0
- package/src/connectkit/connectors/metaMaskConnectConnector.ts +143 -3
- package/src/connectkit/connectors/restoredSession.spec.ts +206 -0
- package/src/connectkit/connectors/restoredSession.ts +165 -0
- package/src/connectkit/defaultConfig.ts +8 -0
- package/src/connectkit/defaultConnectors.ts +6 -0
- package/src/connectkit/index.ts +5 -0
- package/src/keycloak/KeycloakWalletLogin.spec.tsx +51 -1
- package/src/keycloak/KeycloakWalletLogin.tsx +12 -1
- package/src/keycloak/useWalletLogin.ts +18 -2
- package/types/connectkit/connectors/metaMaskConnectConnector.d.ts +9 -0
- package/types/connectkit/connectors/metaMaskConnectConnector.d.ts.map +1 -1
- package/types/connectkit/connectors/restoredSession.d.ts +45 -0
- package/types/connectkit/connectors/restoredSession.d.ts.map +1 -0
- package/types/connectkit/defaultConfig.d.ts +7 -1
- package/types/connectkit/defaultConfig.d.ts.map +1 -1
- package/types/connectkit/defaultConnectors.d.ts +3 -1
- package/types/connectkit/defaultConnectors.d.ts.map +1 -1
- package/types/connectkit/index.d.ts +1 -0
- package/types/connectkit/index.d.ts.map +1 -1
- package/types/keycloak/KeycloakWalletLogin.d.ts.map +1 -1
- package/types/keycloak/useWalletLogin.d.ts +6 -0
- package/types/keycloak/useWalletLogin.d.ts.map +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { IDBFactory } from "fake-indexeddb";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
metaMaskConnectKV,
|
|
5
|
+
multichainBrowserBuild,
|
|
6
|
+
SessionStore,
|
|
7
|
+
storedSession,
|
|
8
|
+
} from "../../../tests/fixtures/metaMaskConnectStore";
|
|
9
|
+
import {
|
|
10
|
+
forgetStoredSessions,
|
|
11
|
+
MetaMaskSessionLostError,
|
|
12
|
+
metaMaskConnectSessionStore,
|
|
13
|
+
type PageLike,
|
|
14
|
+
type RestoredSessionTimeout,
|
|
15
|
+
untilSilent,
|
|
16
|
+
} from "./restoredSession";
|
|
17
|
+
|
|
18
|
+
const timeout: RestoredSessionTimeout = { afterReturnMs: 20_000, withoutTripMs: 45_000 };
|
|
19
|
+
|
|
20
|
+
function page(): PageLike & { show(state: "hidden" | "visible"): void } {
|
|
21
|
+
const listeners = new Set<() => void>();
|
|
22
|
+
const p = {
|
|
23
|
+
visibilityState: "visible",
|
|
24
|
+
addEventListener: (_: "visibilitychange", listener: () => void) => void listeners.add(listener),
|
|
25
|
+
removeEventListener: (_: "visibilitychange", listener: () => void) =>
|
|
26
|
+
void listeners.delete(listener),
|
|
27
|
+
show(state: "hidden" | "visible") {
|
|
28
|
+
p.visibilityState = state;
|
|
29
|
+
for (const listener of [...listeners]) listener();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function deferred<T>() {
|
|
36
|
+
let resolve!: (value: T) => void;
|
|
37
|
+
let reject!: (error: unknown) => void;
|
|
38
|
+
const promise = new Promise<T>((res, rej) => {
|
|
39
|
+
resolve = res;
|
|
40
|
+
reject = rej;
|
|
41
|
+
});
|
|
42
|
+
return { promise, resolve, reject };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function track<T>(promise: Promise<T>) {
|
|
46
|
+
const state: { value?: T; error?: unknown; settled: boolean } = { settled: false };
|
|
47
|
+
promise.then(
|
|
48
|
+
(value) => Object.assign(state, { value, settled: true }),
|
|
49
|
+
(error: unknown) => Object.assign(state, { error, settled: true }),
|
|
50
|
+
);
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("untilSilent", () => {
|
|
55
|
+
beforeEach(() => vi.useFakeTimers());
|
|
56
|
+
afterEach(() => vi.useRealTimers());
|
|
57
|
+
|
|
58
|
+
it("drops the session once the page has been back from MetaMask for afterReturnMs with no answer", async () => {
|
|
59
|
+
const tab = page();
|
|
60
|
+
const drop = vi.fn(async () => undefined);
|
|
61
|
+
const result = track(untilSilent(deferred().promise, timeout, drop, tab));
|
|
62
|
+
|
|
63
|
+
tab.show("hidden");
|
|
64
|
+
await vi.advanceTimersByTimeAsync(120_000);
|
|
65
|
+
tab.show("visible");
|
|
66
|
+
await vi.advanceTimersByTimeAsync(timeout.afterReturnMs - 1);
|
|
67
|
+
expect(drop).not.toHaveBeenCalled();
|
|
68
|
+
expect(result.settled).toBe(false);
|
|
69
|
+
|
|
70
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
71
|
+
expect(drop).toHaveBeenCalledOnce();
|
|
72
|
+
expect(result.error).toBeInstanceOf(MetaMaskSessionLostError);
|
|
73
|
+
expect(result.error).toMatchObject({ code: 4900 });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("drops it after withoutTripMs when the page never left for MetaMask", async () => {
|
|
77
|
+
const drop = vi.fn(async () => undefined);
|
|
78
|
+
const result = track(untilSilent(deferred().promise, timeout, drop, page()));
|
|
79
|
+
|
|
80
|
+
await vi.advanceTimersByTimeAsync(timeout.withoutTripMs - 1);
|
|
81
|
+
expect(drop).not.toHaveBeenCalled();
|
|
82
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
83
|
+
expect(drop).toHaveBeenCalledOnce();
|
|
84
|
+
expect(result.error).toBeInstanceOf(MetaMaskSessionLostError);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("restarts the wait when the user goes back to MetaMask", async () => {
|
|
88
|
+
const tab = page();
|
|
89
|
+
const drop = vi.fn(async () => undefined);
|
|
90
|
+
untilSilent(deferred().promise, timeout, drop, tab).catch(() => undefined);
|
|
91
|
+
|
|
92
|
+
tab.show("hidden");
|
|
93
|
+
tab.show("visible");
|
|
94
|
+
await vi.advanceTimersByTimeAsync(timeout.afterReturnMs - 5_000);
|
|
95
|
+
tab.show("hidden");
|
|
96
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
97
|
+
expect(drop).not.toHaveBeenCalled();
|
|
98
|
+
tab.show("visible");
|
|
99
|
+
await vi.advanceTimersByTimeAsync(timeout.afterReturnMs - 1);
|
|
100
|
+
expect(drop).not.toHaveBeenCalled();
|
|
101
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
102
|
+
expect(drop).toHaveBeenCalledOnce();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("passes the wallet's answer through and drops nothing", async () => {
|
|
106
|
+
const tab = page();
|
|
107
|
+
const answer = deferred<string>();
|
|
108
|
+
const drop = vi.fn(async () => undefined);
|
|
109
|
+
const result = track(untilSilent(answer.promise, timeout, drop, tab));
|
|
110
|
+
|
|
111
|
+
tab.show("hidden");
|
|
112
|
+
tab.show("visible");
|
|
113
|
+
answer.resolve("0x5167");
|
|
114
|
+
await vi.advanceTimersByTimeAsync(timeout.withoutTripMs * 2);
|
|
115
|
+
expect(result.value).toBe("0x5167");
|
|
116
|
+
expect(drop).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("passes the wallet's rejection through", async () => {
|
|
120
|
+
const answer = deferred<string>();
|
|
121
|
+
const result = track(untilSilent(answer.promise, timeout, vi.fn(), page()));
|
|
122
|
+
const rejected = Object.assign(new Error("User rejected the request."), { code: 4001 });
|
|
123
|
+
answer.reject(rejected);
|
|
124
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
125
|
+
expect(result.error).toBe(rejected);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("still rejects when dropping the session fails", async () => {
|
|
129
|
+
const drop = vi.fn(async () => {
|
|
130
|
+
throw new Error("relay gone");
|
|
131
|
+
});
|
|
132
|
+
const result = track(untilSilent(deferred().promise, timeout, drop, page()));
|
|
133
|
+
await vi.advanceTimersByTimeAsync(timeout.withoutTripMs);
|
|
134
|
+
expect(result.error).toBeInstanceOf(MetaMaskSessionLostError);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("with no document, waits withoutTripMs", async () => {
|
|
138
|
+
const drop = vi.fn(async () => undefined);
|
|
139
|
+
const result = track(untilSilent(deferred().promise, timeout, drop));
|
|
140
|
+
await vi.advanceTimersByTimeAsync(timeout.withoutTripMs);
|
|
141
|
+
expect(result.error).toBeInstanceOf(MetaMaskSessionLostError);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("forgetStoredSessions", () => {
|
|
146
|
+
it("deletes every session MetaMask Connect could resume, and nothing else it stored", async () => {
|
|
147
|
+
const idb = new IDBFactory();
|
|
148
|
+
const kv = metaMaskConnectKV(idb);
|
|
149
|
+
const sessions = await SessionStore.create(kv);
|
|
150
|
+
await sessions.set(storedSession("stale"));
|
|
151
|
+
await sessions.set(storedSession("older"));
|
|
152
|
+
await kv.set("multichain-transport", "mwp");
|
|
153
|
+
await kv.set("cache_eth_accounts", '["0xaa"]');
|
|
154
|
+
expect((await sessions.list()).map((s) => s.id)).toEqual(["stale", "older"]);
|
|
155
|
+
|
|
156
|
+
await forgetStoredSessions(idb);
|
|
157
|
+
|
|
158
|
+
expect(await (await SessionStore.create(kv)).list()).toEqual([]);
|
|
159
|
+
expect(await kv.get("session:stale")).toBeNull();
|
|
160
|
+
expect(await kv.get("multichain-transport")).toBe("mwp");
|
|
161
|
+
expect(await kv.get("cache_eth_accounts")).toBe('["0xaa"]');
|
|
162
|
+
await kv.close();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("creates no database where MetaMask Connect has none", async () => {
|
|
166
|
+
const idb = new IDBFactory();
|
|
167
|
+
await forgetStoredSessions(idb);
|
|
168
|
+
expect(await idb.databases()).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("settles when IndexedDB fails inside its callbacks", async () => {
|
|
172
|
+
const open: { result?: unknown; onsuccess?: () => void } = {};
|
|
173
|
+
const idb = { open: () => open } as unknown as IDBFactory;
|
|
174
|
+
const forgotten = forgetStoredSessions(idb);
|
|
175
|
+
open.result = {
|
|
176
|
+
objectStoreNames: {
|
|
177
|
+
contains() {
|
|
178
|
+
throw new Error("InvalidStateError");
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
close: () => undefined,
|
|
182
|
+
};
|
|
183
|
+
open.onsuccess?.();
|
|
184
|
+
await expect(forgotten).resolves.toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("does nothing without IndexedDB", async () => {
|
|
188
|
+
await expect(forgetStoredSessions(undefined)).resolves.toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("names the store the installed MetaMask Connect keeps and resumes its sessions from", () => {
|
|
192
|
+
// StoreAdapterWeb opens `${DB_NAME}${dbNameSuffix}` and uses stores[0].
|
|
193
|
+
const build = multichainBrowserBuild();
|
|
194
|
+
expect(build).toContain('_StoreAdapterWeb.DB_NAME = "mmconnect";');
|
|
195
|
+
expect(build).toContain(
|
|
196
|
+
'constructor(dbNameSuffix = "-kv-store", storeName = _StoreAdapterWeb.stores[0])',
|
|
197
|
+
);
|
|
198
|
+
expect(build).toContain(`_StoreAdapterWeb.stores = ["${metaMaskConnectSessionStore.store}"`);
|
|
199
|
+
expect(build).toContain("const adapter = new StoreAdapterWeb2();");
|
|
200
|
+
expect(metaMaskConnectSessionStore.database).toBe("mmconnect-kv-store");
|
|
201
|
+
// MWPTransport.connect resumes the first session SessionStore lists there.
|
|
202
|
+
expect(build).toContain("const session = yield this.getActiveSession();");
|
|
203
|
+
expect(build).toContain("const sessionStore = yield SessionStore.create(kvstore);");
|
|
204
|
+
expect(build).toContain("const [activeSession] = yield sessionStore.list();");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// MetaMask forgets a connection on "Disconnect all" (or a reinstall) and tells
|
|
2
|
+
// the page nothing: no message reaches the session's relay channel. The page
|
|
3
|
+
// resumes the session it stored, sends each request on a channel no wallet
|
|
4
|
+
// reads, and deeplinks a session id MetaMask answers with "Connection Not
|
|
5
|
+
// Found". The request then waits out the relay timer, ten minutes with
|
|
6
|
+
// metaMaskConnectTimeouts. A connect is no way out either: MetaMask Connect
|
|
7
|
+
// resumes any stored session and answers it from its cache (FZ-7655).
|
|
8
|
+
|
|
9
|
+
export type RestoredSessionTimeout = {
|
|
10
|
+
/** How long the page may be back in front after a trip to MetaMask with no answer. */
|
|
11
|
+
afterReturnMs: number;
|
|
12
|
+
/** How long a request may go unanswered when the page never left for MetaMask. */
|
|
13
|
+
withoutTripMs: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const defaultRestoredSessionTimeout: RestoredSessionTimeout = {
|
|
17
|
+
afterReturnMs: 20_000,
|
|
18
|
+
withoutTripMs: 45_000,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The request a dropped session failed. viem reports it to callers as
|
|
23
|
+
* ProviderDisconnectedError (4900), with this as its `cause`.
|
|
24
|
+
*/
|
|
25
|
+
export class MetaMaskSessionLostError extends Error {
|
|
26
|
+
/** EIP-1193 "Disconnected". */
|
|
27
|
+
readonly code = 4900;
|
|
28
|
+
constructor() {
|
|
29
|
+
super(
|
|
30
|
+
"MetaMask never answered on the stored connection, so it was dropped. Connect your wallet again.",
|
|
31
|
+
);
|
|
32
|
+
this.name = "MetaMaskSessionLostError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Where MetaMask Connect keeps its Mobile Wallet Protocol sessions: the IndexedDB
|
|
37
|
+
// store of @metamask/connect-multichain's StoreAdapterWeb, under the keys of
|
|
38
|
+
// @metamask/mobile-wallet-protocol-core's SessionStore. A connect resumes the
|
|
39
|
+
// first session listed there.
|
|
40
|
+
export const metaMaskConnectSessionStore = {
|
|
41
|
+
database: "mmconnect-kv-store",
|
|
42
|
+
store: "sdk-kv-store",
|
|
43
|
+
sessionsKey: "sessions:master-list",
|
|
44
|
+
sessionKey: (id: string) => `session:${id}`,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Deletes every Mobile Wallet Protocol session MetaMask Connect stored, so no
|
|
49
|
+
* connect can resume one. MetaMask Connect's own disconnect deletes the session
|
|
50
|
+
* only once it has closed the relay socket. Never rejects; with no IndexedDB, or
|
|
51
|
+
* nothing stored, there is nothing to do.
|
|
52
|
+
*/
|
|
53
|
+
export function forgetStoredSessions(
|
|
54
|
+
idb: IDBFactory | undefined = (globalThis as { indexedDB?: IDBFactory }).indexedDB,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
const { database, store, sessionsKey, sessionKey } = metaMaskConnectSessionStore;
|
|
57
|
+
return new Promise<void>((resolve) => {
|
|
58
|
+
let open: IDBOpenDBRequest;
|
|
59
|
+
try {
|
|
60
|
+
if (!idb) throw new Error("no IndexedDB");
|
|
61
|
+
open = idb.open(database);
|
|
62
|
+
} catch {
|
|
63
|
+
resolve();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// A database MetaMask Connect never created stays uncreated: it creates its
|
|
67
|
+
// object stores only when it makes the database.
|
|
68
|
+
open.onupgradeneeded = () => open.transaction?.abort();
|
|
69
|
+
open.onerror = () => resolve();
|
|
70
|
+
open.onsuccess = () => {
|
|
71
|
+
const db = open.result;
|
|
72
|
+
const done = () => {
|
|
73
|
+
db.close();
|
|
74
|
+
resolve();
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
if (!db.objectStoreNames.contains(store)) return done();
|
|
78
|
+
const tx = db.transaction(store, "readwrite");
|
|
79
|
+
tx.oncomplete = done;
|
|
80
|
+
tx.onabort = done;
|
|
81
|
+
const sessions = tx.objectStore(store);
|
|
82
|
+
const listed = sessions.get(sessionsKey);
|
|
83
|
+
listed.onsuccess = () => {
|
|
84
|
+
try {
|
|
85
|
+
let ids: unknown;
|
|
86
|
+
try {
|
|
87
|
+
ids = JSON.parse(String(listed.result ?? "[]"));
|
|
88
|
+
} catch {
|
|
89
|
+
ids = [];
|
|
90
|
+
}
|
|
91
|
+
for (const id of Array.isArray(ids) ? ids : [])
|
|
92
|
+
if (typeof id === "string") sessions.delete(sessionKey(id));
|
|
93
|
+
sessions.delete(sessionsKey);
|
|
94
|
+
} catch {
|
|
95
|
+
done();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
done();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface PageLike {
|
|
106
|
+
visibilityState: string;
|
|
107
|
+
addEventListener(type: "visibilitychange", listener: () => void): void;
|
|
108
|
+
removeEventListener(type: "visibilitychange", listener: () => void): void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Settles as `request` does unless the wallet stays silent: `timeout.afterReturnMs`
|
|
113
|
+
* once the page is back from MetaMask (leaving again restarts the wait), or
|
|
114
|
+
* `timeout.withoutTripMs` when it never left. Then `drop` runs and the promise
|
|
115
|
+
* rejects with MetaMaskSessionLostError. The page cannot ask the wallet whether
|
|
116
|
+
* it still holds the session, so a live one whose user comes back before
|
|
117
|
+
* approving, or whose relay takes longer than afterReturnMs to reconnect, is
|
|
118
|
+
* dropped too and costs a fresh connect.
|
|
119
|
+
*/
|
|
120
|
+
export function untilSilent<T>(
|
|
121
|
+
request: Promise<T>,
|
|
122
|
+
timeout: RestoredSessionTimeout,
|
|
123
|
+
drop: () => Promise<void>,
|
|
124
|
+
page: PageLike | undefined = (globalThis as { document?: PageLike }).document,
|
|
125
|
+
): Promise<T> {
|
|
126
|
+
return new Promise<T>((resolve, reject) => {
|
|
127
|
+
let settled = false;
|
|
128
|
+
let leftPage = page?.visibilityState === "hidden";
|
|
129
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
130
|
+
const settle = () => {
|
|
131
|
+
settled = true;
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
page?.removeEventListener("visibilitychange", arm);
|
|
134
|
+
};
|
|
135
|
+
const silent = () => {
|
|
136
|
+
if (settled) return;
|
|
137
|
+
settle();
|
|
138
|
+
void drop()
|
|
139
|
+
.catch(() => undefined)
|
|
140
|
+
.then(() => reject(new MetaMaskSessionLostError()));
|
|
141
|
+
};
|
|
142
|
+
function arm() {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
if (page?.visibilityState === "hidden") {
|
|
145
|
+
leftPage = true;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
timer = setTimeout(silent, leftPage ? timeout.afterReturnMs : timeout.withoutTripMs);
|
|
149
|
+
}
|
|
150
|
+
page?.addEventListener("visibilitychange", arm);
|
|
151
|
+
arm();
|
|
152
|
+
request.then(
|
|
153
|
+
(value) => {
|
|
154
|
+
if (settled) return;
|
|
155
|
+
settle();
|
|
156
|
+
resolve(value);
|
|
157
|
+
},
|
|
158
|
+
(error: unknown) => {
|
|
159
|
+
if (settled) return;
|
|
160
|
+
settle();
|
|
161
|
+
reject(error);
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -5,6 +5,7 @@ import { EthereumProviderOptions as AaveAccountOptions } from "@aave/account";
|
|
|
5
5
|
|
|
6
6
|
import { setAppMeta } from "@multiplatform.one/connectkit";
|
|
7
7
|
import defaultConnectors from "./defaultConnectors";
|
|
8
|
+
import type { RestoredSessionTimeout } from "./connectors/restoredSession";
|
|
8
9
|
import { discoverWallets, type WalletEcosystem } from "./adapters/discover";
|
|
9
10
|
import { getWalletConnectors } from "./connectors/getWalletConnectors";
|
|
10
11
|
import { ecosystemBySyntheticChainId, syntheticChains } from "./connectors/syntheticChains";
|
|
@@ -26,6 +27,11 @@ type DefaultConfigProps = {
|
|
|
26
27
|
// Aave Account options
|
|
27
28
|
enableAaveAccount?: boolean;
|
|
28
29
|
aaveAccountOptions?: AaveAccountOptions;
|
|
30
|
+
/**
|
|
31
|
+
* On a phone, drop a MetaMask session restored from storage that leaves a
|
|
32
|
+
* wallet request unanswered this long, so the next connect starts fresh.
|
|
33
|
+
*/
|
|
34
|
+
metaMaskRestoredSessionTimeout?: RestoredSessionTimeout;
|
|
29
35
|
// wallet: append shim-connectors for any detected non-EVM wallet (Solana/Sui/
|
|
30
36
|
// Bitcoin) alongside the EVM connectors. No-op when none are installed. A list
|
|
31
37
|
// limits it to those ecosystems.
|
|
@@ -52,6 +58,7 @@ const defaultConfig = ({
|
|
|
52
58
|
client,
|
|
53
59
|
enableAaveAccount = false,
|
|
54
60
|
aaveAccountOptions,
|
|
61
|
+
metaMaskRestoredSessionTimeout,
|
|
55
62
|
enableWallet = true,
|
|
56
63
|
...props
|
|
57
64
|
}: DefaultConfigProps): CreateConfigParameters => {
|
|
@@ -87,6 +94,7 @@ const defaultConfig = ({
|
|
|
87
94
|
coinbaseWalletPreference,
|
|
88
95
|
enableAaveAccount,
|
|
89
96
|
aaveAccountOptions,
|
|
97
|
+
metaMaskRestoredSessionTimeout,
|
|
90
98
|
});
|
|
91
99
|
|
|
92
100
|
// Append wallet shim-connectors for detected non-EVM wallets. discoverWallets()
|
|
@@ -4,6 +4,7 @@ import { CreateConnectorFn } from "wagmi";
|
|
|
4
4
|
import type { CoinbaseWalletParameters } from "wagmi/connectors/coinbaseWallet";
|
|
5
5
|
import { safe } from "wagmi/connectors/safe";
|
|
6
6
|
import { metaMaskConnect } from "./connectors/metaMaskConnectConnector";
|
|
7
|
+
import type { RestoredSessionTimeout } from "./connectors/restoredSession";
|
|
7
8
|
import {
|
|
8
9
|
isEmbeddedDesktopWebview,
|
|
9
10
|
metaMaskOptionsForEmbeddedDesktop,
|
|
@@ -23,6 +24,7 @@ type DefaultConnectorsProps = {
|
|
|
23
24
|
coinbaseWalletPreference?: CoinbaseWalletParameters<"4">["preference"];
|
|
24
25
|
enableAaveAccount?: boolean;
|
|
25
26
|
aaveAccountOptions?: AaveAccountOptions;
|
|
27
|
+
metaMaskRestoredSessionTimeout?: RestoredSessionTimeout;
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
const defaultConnectors = ({
|
|
@@ -30,6 +32,7 @@ const defaultConnectors = ({
|
|
|
30
32
|
coinbaseWalletPreference,
|
|
31
33
|
enableAaveAccount,
|
|
32
34
|
aaveAccountOptions,
|
|
35
|
+
metaMaskRestoredSessionTimeout,
|
|
33
36
|
}: DefaultConnectorsProps): CreateConnectorFn[] => {
|
|
34
37
|
const shouldUseSafeConnector = !(typeof window === "undefined") && window?.parent !== window;
|
|
35
38
|
|
|
@@ -58,6 +61,9 @@ const defaultConnectors = ({
|
|
|
58
61
|
url: app.url,
|
|
59
62
|
iconUrl: app.icon,
|
|
60
63
|
},
|
|
64
|
+
...(metaMaskRestoredSessionTimeout
|
|
65
|
+
? { restoredSessionTimeout: metaMaskRestoredSessionTimeout }
|
|
66
|
+
: {}),
|
|
61
67
|
...(isEmbeddedDesktopWebview() ? metaMaskOptionsForEmbeddedDesktop() : {}),
|
|
62
68
|
}),
|
|
63
69
|
coinbaseWalletConnector({
|
package/src/connectkit/index.ts
CHANGED
|
@@ -10,6 +10,11 @@ export {
|
|
|
10
10
|
reopenMetaMask,
|
|
11
11
|
type MetaMaskConnectParameters,
|
|
12
12
|
} from "./connectors/metaMaskConnectConnector";
|
|
13
|
+
export {
|
|
14
|
+
defaultRestoredSessionTimeout,
|
|
15
|
+
MetaMaskSessionLostError,
|
|
16
|
+
type RestoredSessionTimeout,
|
|
17
|
+
} from "./connectors/restoredSession";
|
|
13
18
|
|
|
14
19
|
// web3's wrapper (generic connectkit useModal + wallet/SIWE openers) keeps the
|
|
15
20
|
// consumer-facing `useModal` API. The agnostic base lives in ./hooks/useModal.
|
|
@@ -3,7 +3,7 @@ import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/re
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { createConfig, createStorage, http } from "wagmi";
|
|
5
5
|
import { mainnet } from "wagmi/chains";
|
|
6
|
-
import { connect } from "wagmi/actions";
|
|
6
|
+
import { connect, disconnect } from "wagmi/actions";
|
|
7
7
|
import { mock } from "wagmi/connectors";
|
|
8
8
|
import { verifyMessage } from "viem";
|
|
9
9
|
import { resetConnectIntentPage, setConnectIntent } from "../connectkit/siwe/connectIntent";
|
|
@@ -205,6 +205,56 @@ describe("KeycloakWalletLogin wallet sign-in", () => {
|
|
|
205
205
|
});
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
+
describe("KeycloakWalletLogin when the wallet connection drops during the signature", () => {
|
|
209
|
+
it("says so, and the next tap connects afresh to a clean sign-in step", async () => {
|
|
210
|
+
setUserAgent(ANDROID_UA);
|
|
211
|
+
setUserActivation(true);
|
|
212
|
+
let failSign!: (error: Error) => void;
|
|
213
|
+
vi.stubGlobal(
|
|
214
|
+
"fetch",
|
|
215
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
216
|
+
if (String(input).endsWith("/nonce")) return new Response("nonce123abc");
|
|
217
|
+
const body = String(
|
|
218
|
+
init?.body ?? (input instanceof Request ? await input.clone().text() : ""),
|
|
219
|
+
);
|
|
220
|
+
if (/"method":"(eth_sign|personal_sign)"/.test(body))
|
|
221
|
+
return new Promise<Response>((_, reject) => (failSign = reject));
|
|
222
|
+
return new Response("not found", { status: 404 });
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
// A session restored from storage, as on a page MetaMask has since forgotten.
|
|
226
|
+
const { config, connectSpy, walletButton } = renderLogin({
|
|
227
|
+
mobile: { preferredConnector: "mock" },
|
|
228
|
+
wagmiConfig: makeConfig(memoryStorage(), true),
|
|
229
|
+
});
|
|
230
|
+
await waitFor(() => expect(config.state.status).toBe("connected"));
|
|
231
|
+
connectSpy.mockClear();
|
|
232
|
+
|
|
233
|
+
fireEvent.click(walletButton);
|
|
234
|
+
await waitFor(() => expect(walletButton.textContent).toBe("Signing…"));
|
|
235
|
+
await waitFor(() => expect(failSign).toBeTypeOf("function"));
|
|
236
|
+
|
|
237
|
+
// What the MetaMask connector does once MetaMask stays silent: wagmi lets go,
|
|
238
|
+
// then the signature request fails.
|
|
239
|
+
await disconnect(config);
|
|
240
|
+
failSign(Object.assign(new Error("MetaMask never answered"), { code: 4900 }));
|
|
241
|
+
|
|
242
|
+
const notice = await screen.findByRole("status");
|
|
243
|
+
expect(notice.id).toBe("mpo-wallet-login-dropped");
|
|
244
|
+
expect(notice.textContent).toContain("Tap Sign in with Wallet to connect again");
|
|
245
|
+
await waitFor(() => expect(walletButton.textContent).toBe("Sign in with Wallet"));
|
|
246
|
+
expect(walletButton.disabled).toBe(false);
|
|
247
|
+
|
|
248
|
+
setUserActivation(false);
|
|
249
|
+
fireEvent.click(walletButton);
|
|
250
|
+
await waitFor(() => expect(connectSpy).toHaveBeenCalledTimes(1));
|
|
251
|
+
expect(document.getElementById("mpo-wallet-login-dropped")).toBeNull();
|
|
252
|
+
await screen.findByText("Sign In With Wallet");
|
|
253
|
+
expect(screen.queryByText("Unknown Error")).toBeNull();
|
|
254
|
+
expect(screen.getByText("Sign In")).not.toBeNull();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
208
258
|
describe("KeycloakWalletLogin target", () => {
|
|
209
259
|
it("{ action } posts the credential to a keycloakify page's login action", async () => {
|
|
210
260
|
const submit = vi
|
|
@@ -4,6 +4,7 @@ import { type CSSProperties, useMemo } from "react";
|
|
|
4
4
|
import type { Chain } from "viem";
|
|
5
5
|
import { type Config, WagmiProvider, createConfig } from "wagmi";
|
|
6
6
|
import { arbitrum, mainnet, optimism, polygon } from "wagmi/chains";
|
|
7
|
+
import { defaultRestoredSessionTimeout } from "../connectkit/connectors/restoredSession";
|
|
7
8
|
import getDefaultConfig from "../connectkit/defaultConfig";
|
|
8
9
|
import { SIWEProvider } from "../connectkit/siwe";
|
|
9
10
|
import { StatusState } from "../connectkit/siwe/SIWEContext";
|
|
@@ -69,6 +70,9 @@ export function createKeycloakWalletConfig({
|
|
|
69
70
|
// Aave Account signs through an app.family.co iframe, which a Keycloak
|
|
70
71
|
// page's default CSP (frame-src 'self') blocks.
|
|
71
72
|
enableAaveAccount: false,
|
|
73
|
+
// A login MetaMask forgot ("Disconnect all") would otherwise sit on
|
|
74
|
+
// "Signing…" for the ten-minute relay timer (FZ-7655).
|
|
75
|
+
metaMaskRestoredSessionTimeout: defaultRestoredSessionTimeout,
|
|
72
76
|
}),
|
|
73
77
|
);
|
|
74
78
|
}
|
|
@@ -88,6 +92,8 @@ const buttonStyle: CSSProperties = {
|
|
|
88
92
|
background: "#fff",
|
|
89
93
|
};
|
|
90
94
|
|
|
95
|
+
const noticeStyle: CSSProperties = { margin: "8px 0 0", fontSize: 13 };
|
|
96
|
+
|
|
91
97
|
/**
|
|
92
98
|
* Wallet and GPG sign-in for a Keycloak login page, on the browser flow: the
|
|
93
99
|
* signed credential posts to the page's authenticator. Carries its own wagmi,
|
|
@@ -169,7 +175,7 @@ function LoginButtons({
|
|
|
169
175
|
className,
|
|
170
176
|
buttonClassName,
|
|
171
177
|
}: KeycloakWalletLoginProps) {
|
|
172
|
-
const { login, status } = useWalletLogin();
|
|
178
|
+
const { login, status, dropped } = useWalletLogin();
|
|
173
179
|
const { openGpg } = useWeb3Modal();
|
|
174
180
|
const style = buttonClassName ? undefined : buttonStyle;
|
|
175
181
|
const busy = status === StatusState.LOADING || status === StatusState.SUCCESS;
|
|
@@ -189,6 +195,11 @@ function LoginButtons({
|
|
|
189
195
|
<WalletIcon />
|
|
190
196
|
<span>{busy ? "Signing…" : walletLabel}</span>
|
|
191
197
|
</button>
|
|
198
|
+
{dropped && !busy && (
|
|
199
|
+
<p id="mpo-wallet-login-dropped" role="status" style={noticeStyle}>
|
|
200
|
+
The wallet connection was lost. Tap {walletLabel} to connect again.
|
|
201
|
+
</p>
|
|
202
|
+
)}
|
|
192
203
|
{gpg && (
|
|
193
204
|
<button
|
|
194
205
|
type="button"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useModal } from "@multiplatform.one/connectkit";
|
|
2
|
-
import { useCallback, useContext, useEffect, useRef } from "react";
|
|
2
|
+
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { useAccount } from "wagmi";
|
|
4
4
|
import { useConnectorLauncher } from "../connectkit/openConnector";
|
|
5
5
|
import { SIWEContext, StatusState } from "../connectkit/siwe/SIWEContext";
|
|
@@ -23,6 +23,12 @@ export interface UseWalletLoginResult {
|
|
|
23
23
|
status: StatusState;
|
|
24
24
|
isConnected: boolean;
|
|
25
25
|
address?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The wallet connection went away while a signature waited on it, as when
|
|
28
|
+
* MetaMask never answered a session it no longer holds. The next `login()`
|
|
29
|
+
* connects afresh and clears it.
|
|
30
|
+
*/
|
|
31
|
+
dropped: boolean;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
function readIntent(key: string, ttl: number): boolean {
|
|
@@ -72,6 +78,7 @@ export function useWalletLogin(opts: UseWalletLoginOptions = {}): UseWalletLogin
|
|
|
72
78
|
const isConnected = onStore ? connection.status === "connected" : wagmiAccount.isConnected;
|
|
73
79
|
const address = onStore ? (connection.address ?? undefined) : wagmiAccount.address;
|
|
74
80
|
const signingRef = useRef(false);
|
|
81
|
+
const [dropped, setDropped] = useState(false);
|
|
75
82
|
|
|
76
83
|
const sign = useCallback(async () => {
|
|
77
84
|
const signer = signIn ?? siwe;
|
|
@@ -88,6 +95,10 @@ export function useWalletLogin(opts: UseWalletLoginOptions = {}): UseWalletLogin
|
|
|
88
95
|
}, [signIn, siwe]);
|
|
89
96
|
|
|
90
97
|
const login = useCallback(async () => {
|
|
98
|
+
setDropped(false);
|
|
99
|
+
// A failed signature leaves SIWE on ERROR, which the sign-in step would
|
|
100
|
+
// show the next attempt as "Unknown Error".
|
|
101
|
+
if (!signIn && siwe?.status === StatusState.ERROR) siwe.resetStatus();
|
|
91
102
|
if (isConnected) {
|
|
92
103
|
await sign();
|
|
93
104
|
return;
|
|
@@ -96,7 +107,7 @@ export function useWalletLogin(opts: UseWalletLoginOptions = {}): UseWalletLogin
|
|
|
96
107
|
// No await above: a phone's preferred wallet opens from inside the tap.
|
|
97
108
|
if (launcher?.openPreferredConnector()) return;
|
|
98
109
|
setOpen(true);
|
|
99
|
-
}, [isConnected, sign, intentKey, setOpen, launcher]);
|
|
110
|
+
}, [isConnected, sign, intentKey, setOpen, launcher, signIn, siwe]);
|
|
100
111
|
|
|
101
112
|
useEffect(() => {
|
|
102
113
|
if (!isConnected || !readIntent(intentKey, intentTtlMs)) return;
|
|
@@ -107,11 +118,16 @@ export function useWalletLogin(opts: UseWalletLoginOptions = {}): UseWalletLogin
|
|
|
107
118
|
|
|
108
119
|
useEffect(() => () => writeIntent(intentKey, null), [intentKey]);
|
|
109
120
|
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!isConnected && signingRef.current) setDropped(true);
|
|
123
|
+
}, [isConnected]);
|
|
124
|
+
|
|
110
125
|
return {
|
|
111
126
|
login,
|
|
112
127
|
// SignInStatus holds StatusState's values, string for string.
|
|
113
128
|
status: signIn ? (signIn.status as StatusState) : (siwe?.status ?? StatusState.ERROR),
|
|
114
129
|
isConnected,
|
|
115
130
|
address,
|
|
131
|
+
dropped,
|
|
116
132
|
};
|
|
117
133
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EIP1193Provider } from "@metamask/connect-evm";
|
|
2
2
|
import { type CreateConnectorFn } from "wagmi";
|
|
3
3
|
import { type Chain, type Hex } from "viem";
|
|
4
|
+
import { type RestoredSessionTimeout } from "./restoredSession";
|
|
4
5
|
export type MetaMaskConnectParameters = {
|
|
5
6
|
dapp: {
|
|
6
7
|
name: string;
|
|
@@ -26,6 +27,14 @@ export type MetaMaskConnectParameters = {
|
|
|
26
27
|
* rather than to switch to it. Defaults to `metaMaskMayLackChain`.
|
|
27
28
|
*/
|
|
28
29
|
addChainFirst?: (chain: Chain) => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* On a phone, give up on a session restored from storage once a wallet
|
|
32
|
+
* request goes unanswered this long (see restoredSession.ts): the request
|
|
33
|
+
* fails with MetaMaskSessionLostError, wagmi disconnects, the stored session is
|
|
34
|
+
* deleted and MetaMask Connect disconnects, so the next connect starts a new
|
|
35
|
+
* one. Off by default.
|
|
36
|
+
*/
|
|
37
|
+
restoredSessionTimeout?: RestoredSessionTimeout;
|
|
29
38
|
};
|
|
30
39
|
type ConnectorConfig = Parameters<CreateConnectorFn>[0];
|
|
31
40
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metaMaskConnectConnector.d.ts","sourceRoot":"","sources":["../../../src/connectkit/connectors/metaMaskConnectConnector.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,uBAAuB,CAAC;AAEjF,OAAO,EAA2B,KAAK,iBAAiB,EAAmB,MAAM,OAAO,CAAC;AACzF,OAAO,EAEL,KAAK,KAAK,EAEV,KAAK,GAAG,EAKT,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"metaMaskConnectConnector.d.ts","sourceRoot":"","sources":["../../../src/connectkit/connectors/metaMaskConnectConnector.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,uBAAuB,CAAC;AAEjF,OAAO,EAA2B,KAAK,iBAAiB,EAAmB,MAAM,OAAO,CAAC;AACzF,OAAO,EAEL,KAAK,KAAK,EAEV,KAAK,GAAG,EAKT,MAAM,MAAM,CAAC;AAGd,OAAO,EAAwB,KAAK,sBAAsB,EAAe,MAAM,mBAAmB,CAAC;AAGnG,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvD,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,6EAA6E;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IAC1C;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;CACjD,CAAC;AAEF,KAAK,eAAe,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;AAGxD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,KAAK,EAAE,EACxB,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,GACzC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAQrB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,EAC7B,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,GACzC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAOrB;AAsGD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAE1D;AAOD,wGAAwG;AACxG,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,GAAG,EAAE,CAK9F;AAmGD;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAIxC;AAmFD,oFAAoF;AACpF,wBAAgB,0BAA0B,IAAI,IAAI,CAQjD;AAWD,wBAAgB,eAAe,CAAC,UAAU,EAAE,yBAAyB,wFAsMpE;yBAtMe,eAAe"}
|