@gluwa/connect-kit 0.1.0-next.0 → 0.1.0-next.2
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/CHANGELOG.md +12 -0
- package/dist/index.d.ts +52 -4
- package/dist/index.js +651 -308
- package/dist/package.json +3 -1
- package/package.json +3 -1
- package/src/ConnectKitProvider.tsx +169 -0
- package/src/ConnectModal.scss +38 -0
- package/src/ConnectModal.tsx +246 -199
- package/src/components/QRFrame.tsx +43 -0
- package/src/creditConnectConnector.ts +61 -13
- package/src/hooks/useWCState.ts +150 -0
- package/src/hooks/useWagmiConnect.ts +68 -45
- package/src/index.ts +13 -1
- package/src/types.ts +22 -4
- package/src/views/CreditWalletView.tsx +4 -17
- package/src/views/MetaMaskView.tsx +5 -30
- package/src/views/SwitchChainView.tsx +67 -0
- package/src/views/WalletConnectView.tsx +10 -70
- package/tsup.config.ts +12 -1
- package/dist/_esm-PE6HOEBI.js +0 -3909
- package/dist/ccip-UBX2BH3T.js +0 -15
- package/dist/chunk-6KUZ225H.js +0 -5259
- package/dist/chunk-EVEWD66F.js +0 -447
- package/dist/chunk-U2IU7TQD.js +0 -45
- package/dist/secp256k1-FYSVLDVL.js +0 -2311
|
@@ -17,11 +17,23 @@ export type CreditConnectConnectorOptions = {
|
|
|
17
17
|
|
|
18
18
|
type CreditConnectProvider = EIP1193Provider;
|
|
19
19
|
type ProviderListener = (...args: unknown[]) => void;
|
|
20
|
+
type CreditConnectProviderWithEvents = CreditConnectProvider & {
|
|
21
|
+
on: (event: string, listener: ProviderListener) => CreditConnectProviderWithEvents;
|
|
22
|
+
removeListener: (event: string, listener: ProviderListener) => CreditConnectProviderWithEvents;
|
|
23
|
+
};
|
|
20
24
|
|
|
21
25
|
const CONNECTOR_ID = 'credit-connect';
|
|
22
26
|
const CONNECTOR_NAME = 'Credit Wallet';
|
|
23
27
|
const CONNECTOR_TYPE = 'credit-connect';
|
|
24
28
|
|
|
29
|
+
const castConnectResult = <withCapabilities extends boolean>(value: unknown) =>
|
|
30
|
+
value as {
|
|
31
|
+
accounts: withCapabilities extends true
|
|
32
|
+
? ReadonlyArray<{ address: Address; capabilities: Record<string, unknown> }>
|
|
33
|
+
: readonly Address[];
|
|
34
|
+
chainId: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
25
37
|
export const creditConnectConnector = (
|
|
26
38
|
options: CreditConnectConnectorOptions,
|
|
27
39
|
): ReturnType<typeof createConnector> => {
|
|
@@ -157,7 +169,20 @@ export const creditConnectConnector = (
|
|
|
157
169
|
const chainId = Number.parseInt(getEvmAddressInfo(session)[0].networkIdentifier, 10);
|
|
158
170
|
if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
|
|
159
171
|
selectedChainId = chainId;
|
|
160
|
-
|
|
172
|
+
if (parameters?.withCapabilities === true) {
|
|
173
|
+
return castConnectResult<withCapabilities>({
|
|
174
|
+
accounts: accounts.map((address) => ({
|
|
175
|
+
address,
|
|
176
|
+
capabilities: {} as Record<string, unknown>,
|
|
177
|
+
})),
|
|
178
|
+
chainId,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return castConnectResult<withCapabilities>({
|
|
183
|
+
accounts,
|
|
184
|
+
chainId,
|
|
185
|
+
});
|
|
161
186
|
}
|
|
162
187
|
|
|
163
188
|
if (m.state.status === 'CONNECTING' && m.state.connectingPayload) {
|
|
@@ -202,7 +227,20 @@ export const creditConnectConnector = (
|
|
|
202
227
|
if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
|
|
203
228
|
|
|
204
229
|
selectedChainId = chainId;
|
|
205
|
-
|
|
230
|
+
if (parameters?.withCapabilities === true) {
|
|
231
|
+
return castConnectResult<withCapabilities>({
|
|
232
|
+
accounts: accounts.map((address) => ({
|
|
233
|
+
address,
|
|
234
|
+
capabilities: {} as Record<string, unknown>,
|
|
235
|
+
})),
|
|
236
|
+
chainId,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return castConnectResult<withCapabilities>({
|
|
241
|
+
accounts,
|
|
242
|
+
chainId,
|
|
243
|
+
});
|
|
206
244
|
} catch (error) {
|
|
207
245
|
resetState();
|
|
208
246
|
throw error;
|
|
@@ -277,9 +315,17 @@ export const creditConnectConnector = (
|
|
|
277
315
|
if (provider) return provider;
|
|
278
316
|
await ensureManagerInitialized();
|
|
279
317
|
|
|
280
|
-
|
|
281
|
-
request: async (args
|
|
282
|
-
const { method, params } = args as {
|
|
318
|
+
const createdProvider = {
|
|
319
|
+
request: async (args) => {
|
|
320
|
+
const { method, params: rawParams } = args as {
|
|
321
|
+
method: string;
|
|
322
|
+
params?: unknown[] | object;
|
|
323
|
+
};
|
|
324
|
+
const params = Array.isArray(rawParams)
|
|
325
|
+
? [...rawParams]
|
|
326
|
+
: rawParams === undefined
|
|
327
|
+
? []
|
|
328
|
+
: [rawParams];
|
|
283
329
|
const session = getCurrentSession();
|
|
284
330
|
|
|
285
331
|
const getSelectedAddressInfo = (address?: string) => {
|
|
@@ -409,20 +455,22 @@ export const creditConnectConnector = (
|
|
|
409
455
|
},
|
|
410
456
|
|
|
411
457
|
on: (event: string, listener: ProviderListener) => {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
458
|
+
let listeners = providerListeners.get(event);
|
|
459
|
+
if (!listeners) {
|
|
460
|
+
listeners = new Set();
|
|
461
|
+
providerListeners.set(event, listeners);
|
|
462
|
+
}
|
|
463
|
+
listeners.add(listener);
|
|
464
|
+
return createdProvider;
|
|
417
465
|
},
|
|
418
466
|
|
|
419
467
|
removeListener: (event: string, listener: ProviderListener) => {
|
|
420
468
|
providerListeners.get(event)?.delete(listener);
|
|
421
|
-
|
|
422
|
-
return provider!;
|
|
469
|
+
return createdProvider;
|
|
423
470
|
},
|
|
424
|
-
} as
|
|
471
|
+
} as CreditConnectProviderWithEvents;
|
|
425
472
|
|
|
473
|
+
provider = createdProvider;
|
|
426
474
|
return provider;
|
|
427
475
|
},
|
|
428
476
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from 'react';
|
|
2
|
+
import type { WCWallet, WCSubView } from '../types';
|
|
3
|
+
|
|
4
|
+
const fetchWCWalletList = async (projectId: string): Promise<WCWallet[]> => {
|
|
5
|
+
const all: WCWallet[] = [];
|
|
6
|
+
let page = 1;
|
|
7
|
+
const entries = 100;
|
|
8
|
+
|
|
9
|
+
while (page <= 10) {
|
|
10
|
+
const url = new URL('https://explorer-api.walletconnect.com/v3/wallets');
|
|
11
|
+
url.searchParams.set('projectId', projectId);
|
|
12
|
+
url.searchParams.set('entries', String(entries));
|
|
13
|
+
url.searchParams.set('page', String(page));
|
|
14
|
+
|
|
15
|
+
const res = await fetch(url.toString());
|
|
16
|
+
if (!res.ok) throw new Error(`Failed to fetch wallet list: ${res.status}`);
|
|
17
|
+
|
|
18
|
+
const json = (await res.json()) as {
|
|
19
|
+
listings: Record<
|
|
20
|
+
string,
|
|
21
|
+
{
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
homepage?: string;
|
|
25
|
+
image_url?: { sm?: string; md?: string };
|
|
26
|
+
mobile?: { native?: string; universal?: string };
|
|
27
|
+
desktop?: { native?: string; universal?: string };
|
|
28
|
+
app?: { ios?: string; android?: string };
|
|
29
|
+
}
|
|
30
|
+
>;
|
|
31
|
+
total: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const listings = Object.values(json.listings ?? {});
|
|
35
|
+
for (const w of listings) {
|
|
36
|
+
all.push({
|
|
37
|
+
id: w.id,
|
|
38
|
+
name: w.name,
|
|
39
|
+
imageUrl: w.image_url?.md ?? w.image_url?.sm ?? '',
|
|
40
|
+
mobileNative: w.mobile?.native ?? '',
|
|
41
|
+
mobileUniversal: w.mobile?.universal ?? '',
|
|
42
|
+
desktopNative: w.desktop?.native ?? '',
|
|
43
|
+
desktopUniversal: w.desktop?.universal ?? '',
|
|
44
|
+
appIos: w.app?.ios ?? '',
|
|
45
|
+
appAndroid: w.app?.android ?? '',
|
|
46
|
+
homepage: w.homepage ?? '',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (listings.length < entries || all.length >= json.total) break;
|
|
51
|
+
page += 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return all;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export interface WCState {
|
|
58
|
+
subView: WCSubView;
|
|
59
|
+
walletList: WCWallet[];
|
|
60
|
+
walletListLoading: boolean;
|
|
61
|
+
walletListSearch: string;
|
|
62
|
+
filterActive: boolean;
|
|
63
|
+
selectedWallet: WCWallet | null;
|
|
64
|
+
loadWalletList: (projectId: string) => Promise<void>;
|
|
65
|
+
onShowList: () => void;
|
|
66
|
+
onSelectWallet: (wallet: WCWallet) => void;
|
|
67
|
+
onSearchChange: (q: string) => void;
|
|
68
|
+
onFilterToggle: () => void;
|
|
69
|
+
// Returns true if WC handled the back navigation, false if the parent should handle it
|
|
70
|
+
handleBack: () => boolean;
|
|
71
|
+
resetView: () => void;
|
|
72
|
+
reset: () => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const useWCState = (): WCState => {
|
|
76
|
+
const [subView, setSubView] = useState<WCSubView>('qr');
|
|
77
|
+
const [walletList, setWalletList] = useState<WCWallet[]>([]);
|
|
78
|
+
const [walletListLoading, setWalletListLoading] = useState(false);
|
|
79
|
+
const [walletListSearch, setWalletListSearch] = useState('');
|
|
80
|
+
const [filterActive, setFilterActive] = useState(false);
|
|
81
|
+
const [selectedWallet, setSelectedWallet] = useState<WCWallet | null>(null);
|
|
82
|
+
const loadedRef = useRef(false);
|
|
83
|
+
|
|
84
|
+
const loadWalletList = useCallback(async (projectId: string): Promise<void> => {
|
|
85
|
+
if (loadedRef.current) return;
|
|
86
|
+
loadedRef.current = true;
|
|
87
|
+
setWalletListLoading(true);
|
|
88
|
+
try {
|
|
89
|
+
const list = await fetchWCWalletList(projectId);
|
|
90
|
+
setWalletList(list);
|
|
91
|
+
} catch {
|
|
92
|
+
loadedRef.current = false;
|
|
93
|
+
} finally {
|
|
94
|
+
setWalletListLoading(false);
|
|
95
|
+
}
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const onShowList = useCallback((): void => setSubView('list'), []);
|
|
99
|
+
|
|
100
|
+
const onSelectWallet = useCallback((wallet: WCWallet): void => {
|
|
101
|
+
setSelectedWallet(wallet);
|
|
102
|
+
setSubView('wallet');
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const onSearchChange = useCallback((q: string): void => setWalletListSearch(q), []);
|
|
106
|
+
|
|
107
|
+
const onFilterToggle = useCallback((): void => setFilterActive((prev) => !prev), []);
|
|
108
|
+
|
|
109
|
+
const handleBack = useCallback((): boolean => {
|
|
110
|
+
if (subView === 'wallet') {
|
|
111
|
+
setSelectedWallet(null);
|
|
112
|
+
setSubView('list');
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (subView === 'list') {
|
|
116
|
+
setSubView('qr');
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}, [subView]);
|
|
121
|
+
|
|
122
|
+
const resetView = useCallback((): void => {
|
|
123
|
+
setSubView('qr');
|
|
124
|
+
setSelectedWallet(null);
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
const reset = useCallback((): void => {
|
|
128
|
+
setSubView('qr');
|
|
129
|
+
setWalletListSearch('');
|
|
130
|
+
setFilterActive(false);
|
|
131
|
+
setSelectedWallet(null);
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
subView,
|
|
136
|
+
walletList,
|
|
137
|
+
walletListLoading,
|
|
138
|
+
walletListSearch,
|
|
139
|
+
filterActive,
|
|
140
|
+
selectedWallet,
|
|
141
|
+
loadWalletList,
|
|
142
|
+
onShowList,
|
|
143
|
+
onSelectWallet,
|
|
144
|
+
onSearchChange,
|
|
145
|
+
onFilterToggle,
|
|
146
|
+
handleBack,
|
|
147
|
+
resetView,
|
|
148
|
+
reset,
|
|
149
|
+
};
|
|
150
|
+
};
|
|
@@ -1,12 +1,54 @@
|
|
|
1
1
|
import { useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
import { useConnect,
|
|
2
|
+
import { useConnect, useConfig } from 'wagmi';
|
|
3
3
|
import type { ConnectorId, ConnectResult } from '../types';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
type WagmiConnectorLike = {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type?: string;
|
|
9
|
+
rdns?: string | readonly string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const hasMetaMaskKeyword = (value: string): boolean => value.toLowerCase().includes('metamask');
|
|
13
|
+
const hasWalletConnectKeyword = (value: string): boolean =>
|
|
14
|
+
value.toLowerCase().includes('walletconnect');
|
|
15
|
+
|
|
16
|
+
const isMetaMaskConnector = (connector: WagmiConnectorLike): boolean => {
|
|
17
|
+
if (connector.id === 'injected' || connector.id === 'metaMaskSDK') return true;
|
|
18
|
+
if (hasMetaMaskKeyword(connector.id) || hasMetaMaskKeyword(connector.name)) return true;
|
|
19
|
+
|
|
20
|
+
if (connector.rdns) {
|
|
21
|
+
const rdnsList = Array.isArray(connector.rdns) ? connector.rdns : [connector.rdns];
|
|
22
|
+
if (rdnsList.some((rdns) => hasMetaMaskKeyword(rdns))) return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const isWalletConnectConnector = (connector: WagmiConnectorLike): boolean => {
|
|
29
|
+
if (connector.id === 'walletConnect') return true;
|
|
30
|
+
if (hasWalletConnectKeyword(connector.id) || hasWalletConnectKeyword(connector.name)) return true;
|
|
31
|
+
return false;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const resolveWagmiConnector = <T extends WagmiConnectorLike>(
|
|
35
|
+
connectors: readonly T[],
|
|
36
|
+
connectorId: ConnectorId,
|
|
37
|
+
): T | undefined => {
|
|
38
|
+
switch (connectorId) {
|
|
39
|
+
case 'CREDIT_CONNECT':
|
|
40
|
+
return connectors.find((connector) => connector.id === 'credit-connect');
|
|
41
|
+
|
|
42
|
+
case 'METAMASK':
|
|
43
|
+
return connectors.find(isMetaMaskConnector);
|
|
44
|
+
|
|
45
|
+
case 'CREDIT_WALLET':
|
|
46
|
+
case 'WALLET_CONNECT':
|
|
47
|
+
return connectors.find(isWalletConnectConnector);
|
|
48
|
+
|
|
49
|
+
default:
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
10
52
|
};
|
|
11
53
|
|
|
12
54
|
interface Options {
|
|
@@ -27,7 +69,6 @@ export const useWagmiConnect = ({
|
|
|
27
69
|
onQrUri,
|
|
28
70
|
}: Options): UseWagmiConnectResult => {
|
|
29
71
|
const config = useConfig();
|
|
30
|
-
const { address, status } = useAccount();
|
|
31
72
|
const pendingConnectorId = useRef<ConnectorId | null>(null);
|
|
32
73
|
|
|
33
74
|
// 콜백 참조
|
|
@@ -36,26 +77,12 @@ export const useWagmiConnect = ({
|
|
|
36
77
|
const onQrUriRef = useRef(onQrUri);
|
|
37
78
|
useEffect(() => {
|
|
38
79
|
onConnectRef.current = onConnect;
|
|
39
|
-
}, [onConnect]);
|
|
40
|
-
useEffect(() => {
|
|
41
80
|
onErrorRef.current = onError;
|
|
42
|
-
}, [onError]);
|
|
43
|
-
useEffect(() => {
|
|
44
81
|
onQrUriRef.current = onQrUri;
|
|
45
|
-
}, [onQrUri]);
|
|
82
|
+
}, [onConnect, onError, onQrUri]);
|
|
46
83
|
|
|
47
84
|
// 연결 시도
|
|
48
|
-
const {
|
|
49
|
-
mutation: {
|
|
50
|
-
onError(error) {
|
|
51
|
-
const id = pendingConnectorId.current;
|
|
52
|
-
if (id) {
|
|
53
|
-
onErrorRef.current(error as Error, id);
|
|
54
|
-
pendingConnectorId.current = null;
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
});
|
|
85
|
+
const { connectAsync, reset, isPending } = useConnect();
|
|
59
86
|
|
|
60
87
|
// QR 코드 표시
|
|
61
88
|
useEffect(() => {
|
|
@@ -80,38 +107,34 @@ export const useWagmiConnect = ({
|
|
|
80
107
|
};
|
|
81
108
|
}, [config.connectors]);
|
|
82
109
|
|
|
83
|
-
// 연결 완료
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
if (status === 'connected' && address && pendingConnectorId.current) {
|
|
86
|
-
const id = pendingConnectorId.current;
|
|
87
|
-
pendingConnectorId.current = null;
|
|
88
|
-
onConnectRef.current({ address, connectorId: id });
|
|
89
|
-
}
|
|
90
|
-
}, [status, address]);
|
|
91
|
-
|
|
92
110
|
// 연결 트리거
|
|
93
111
|
const triggerConnect = useCallback(
|
|
94
112
|
(connectorId: ConnectorId): void => {
|
|
95
|
-
const
|
|
96
|
-
if (!wagmiId) {
|
|
97
|
-
onErrorRef.current(new Error(`No wagmi connector mapped for ${connectorId}`), connectorId);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const connector = config.connectors.find((c) => c.id === wagmiId);
|
|
113
|
+
const connector = resolveWagmiConnector(config.connectors, connectorId);
|
|
102
114
|
if (!connector) {
|
|
103
|
-
onErrorRef.current(
|
|
104
|
-
new Error(`Wagmi connector '${wagmiId}' not found in config`),
|
|
105
|
-
connectorId,
|
|
106
|
-
);
|
|
115
|
+
onErrorRef.current(new Error(`No wagmi connector found for ${connectorId}`), connectorId);
|
|
107
116
|
return;
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
onQrUriRef.current(null);
|
|
111
120
|
pendingConnectorId.current = connectorId;
|
|
112
|
-
|
|
121
|
+
connectAsync({ connector })
|
|
122
|
+
.then((result) => {
|
|
123
|
+
if (pendingConnectorId.current !== connectorId) return;
|
|
124
|
+
const address = result.accounts?.[0];
|
|
125
|
+
if (!address) {
|
|
126
|
+
throw new Error('Connected but no account returned');
|
|
127
|
+
}
|
|
128
|
+
pendingConnectorId.current = null;
|
|
129
|
+
onConnectRef.current({ address, connectorId });
|
|
130
|
+
})
|
|
131
|
+
.catch((error) => {
|
|
132
|
+
if (pendingConnectorId.current !== connectorId) return;
|
|
133
|
+
pendingConnectorId.current = null;
|
|
134
|
+
onErrorRef.current(error as Error, connectorId);
|
|
135
|
+
});
|
|
113
136
|
},
|
|
114
|
-
[config.connectors,
|
|
137
|
+
[config.connectors, connectAsync],
|
|
115
138
|
);
|
|
116
139
|
|
|
117
140
|
// 연결 취소
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
export { ConnectModal } from './ConnectModal';
|
|
2
|
+
export { ConnectKitProvider, useConnectKit } from './ConnectKitProvider';
|
|
3
|
+
export type { ConnectKitContextValue, ConnectKitProviderProps } from './ConnectKitProvider';
|
|
2
4
|
export { creditConnectConnector } from './creditConnectConnector';
|
|
3
5
|
export type { CreditConnectConnectorOptions } from './creditConnectConnector';
|
|
4
|
-
export type {
|
|
6
|
+
export type {
|
|
7
|
+
ConnectErrorContext,
|
|
8
|
+
ConnectErrorReason,
|
|
9
|
+
ConnectModalProps,
|
|
10
|
+
Connectors,
|
|
11
|
+
ConnectResult,
|
|
12
|
+
ConnectorId,
|
|
13
|
+
CreditWalletStrategy,
|
|
14
|
+
WCSubView,
|
|
15
|
+
WCWallet,
|
|
16
|
+
} from './types';
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
1
3
|
export type ConnectorId = 'CREDIT_WALLET' | 'CREDIT_CONNECT' | 'METAMASK' | 'WALLET_CONNECT';
|
|
2
4
|
|
|
3
|
-
export type
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
export type CreditWalletStrategy = 'walletConnect' | 'creditConnect';
|
|
6
|
+
|
|
7
|
+
export interface Connectors {
|
|
8
|
+
creditWallet?: CreditWalletStrategy | false;
|
|
9
|
+
metamask?: boolean;
|
|
10
|
+
walletConnect?: boolean;
|
|
11
|
+
}
|
|
6
12
|
|
|
7
13
|
export interface WCWallet {
|
|
8
14
|
id: string;
|
|
@@ -24,11 +30,23 @@ export interface ConnectResult {
|
|
|
24
30
|
connectorId: ConnectorId;
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
export type ConnectErrorReason = 'rejected' | 'unsupported' | 'unknown';
|
|
34
|
+
|
|
35
|
+
export interface ConnectErrorContext {
|
|
36
|
+
connectorId: ConnectorId;
|
|
37
|
+
reason: ConnectErrorReason;
|
|
38
|
+
error: Error;
|
|
39
|
+
retry: () => void;
|
|
40
|
+
dismiss: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
export interface ConnectModalProps {
|
|
28
44
|
open: boolean;
|
|
29
|
-
connectors:
|
|
45
|
+
connectors: Connectors;
|
|
30
46
|
onConnect: (result: ConnectResult) => void;
|
|
31
47
|
onClose: () => void;
|
|
32
48
|
onLog?: (message: string, error?: Error) => void;
|
|
33
49
|
wcProjectId?: string;
|
|
50
|
+
requiredChainId?: number;
|
|
51
|
+
renderConnectError?: (ctx: ConnectErrorContext) => ReactNode;
|
|
34
52
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type FC, useEffect } from 'react';
|
|
2
|
-
import { QRCodeSVG } from 'qrcode.react';
|
|
1
|
+
import { type FC, useEffect, useMemo } from 'react';
|
|
3
2
|
import { isMobileDevice, tryOpenDeepLink } from '../utils/platform';
|
|
4
3
|
import { CONNECTOR_META } from '../connector-meta';
|
|
5
4
|
import { type ConnectorId } from '../types';
|
|
5
|
+
import { QRFrame } from '../components/QRFrame';
|
|
6
6
|
|
|
7
7
|
interface CreditWalletViewProps {
|
|
8
8
|
connectorId: Extract<ConnectorId, 'CREDIT_CONNECT' | 'CREDIT_WALLET'>;
|
|
@@ -11,7 +11,7 @@ interface CreditWalletViewProps {
|
|
|
11
11
|
|
|
12
12
|
export const CreditWalletView: FC<CreditWalletViewProps> = ({ connectorId, qrUri }) => {
|
|
13
13
|
const { downloadUrl, logoUrl, deepLinkBase } = CONNECTOR_META[connectorId];
|
|
14
|
-
const isMobile = isMobileDevice();
|
|
14
|
+
const isMobile = useMemo(() => isMobileDevice(), []);
|
|
15
15
|
|
|
16
16
|
useEffect(() => {
|
|
17
17
|
if (isMobile && qrUri && deepLinkBase) {
|
|
@@ -29,20 +29,7 @@ export const CreditWalletView: FC<CreditWalletViewProps> = ({ connectorId, qrUri
|
|
|
29
29
|
|
|
30
30
|
return (
|
|
31
31
|
<div className="ck-view ck-view--qr">
|
|
32
|
-
<
|
|
33
|
-
{qrUri ? (
|
|
34
|
-
<QRCodeSVG
|
|
35
|
-
value={qrUri}
|
|
36
|
-
size={196}
|
|
37
|
-
level="H"
|
|
38
|
-
imageSettings={
|
|
39
|
-
logoUrl ? { src: logoUrl, width: 40, height: 40, excavate: true } : undefined
|
|
40
|
-
}
|
|
41
|
-
/>
|
|
42
|
-
) : (
|
|
43
|
-
<div className="ck-qr-spinner" aria-label="Loading QR code" />
|
|
44
|
-
)}
|
|
45
|
-
</div>
|
|
32
|
+
<QRFrame qrUri={qrUri} logoUrl={logoUrl} />
|
|
46
33
|
|
|
47
34
|
<p className="ck-view__caption">Open the Credit Wallet app and scan the QR</p>
|
|
48
35
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type FC, useEffect } from 'react';
|
|
2
|
-
import { QRCodeSVG } from 'qrcode.react';
|
|
1
|
+
import { type FC, useEffect, useMemo } from 'react';
|
|
3
2
|
import { isMobileDevice, tryOpenDeepLink } from '../utils/platform';
|
|
4
3
|
import { CONNECTOR_META } from '../connector-meta';
|
|
4
|
+
import { QRFrame, CopyLinkButton } from '../components/QRFrame';
|
|
5
5
|
|
|
6
6
|
const { downloadUrl, deepLinkBase } = CONNECTOR_META.METAMASK;
|
|
7
7
|
|
|
@@ -12,7 +12,7 @@ interface MetaMaskViewProps {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const MetaMaskView: FC<MetaMaskViewProps> = ({ qrUri, hasExtension, logoUrl }) => {
|
|
15
|
-
const isMobile = isMobileDevice();
|
|
15
|
+
const isMobile = useMemo(() => isMobileDevice(), []);
|
|
16
16
|
|
|
17
17
|
useEffect(() => {
|
|
18
18
|
if (isMobile && qrUri && deepLinkBase) {
|
|
@@ -41,33 +41,8 @@ export const MetaMaskView: FC<MetaMaskViewProps> = ({ qrUri, hasExtension, logoU
|
|
|
41
41
|
|
|
42
42
|
return (
|
|
43
43
|
<div className="ck-view ck-view--qr">
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
<QRCodeSVG
|
|
47
|
-
value={qrUri}
|
|
48
|
-
size={196}
|
|
49
|
-
level="H"
|
|
50
|
-
imageSettings={
|
|
51
|
-
logoUrl ? { src: logoUrl, width: 40, height: 40, excavate: true } : undefined
|
|
52
|
-
}
|
|
53
|
-
/>
|
|
54
|
-
) : (
|
|
55
|
-
<div className="ck-qr-spinner" aria-label="Loading QR code" />
|
|
56
|
-
)}
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
{qrUri && (
|
|
60
|
-
<button
|
|
61
|
-
type="button"
|
|
62
|
-
className="ck-copy-link"
|
|
63
|
-
onClick={() => {
|
|
64
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
65
|
-
navigator.clipboard.writeText(qrUri);
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
Copy link
|
|
69
|
-
</button>
|
|
70
|
-
)}
|
|
44
|
+
<QRFrame qrUri={qrUri} logoUrl={logoUrl} />
|
|
45
|
+
<CopyLinkButton qrUri={qrUri} />
|
|
71
46
|
|
|
72
47
|
{downloadUrl && (
|
|
73
48
|
<div className="ck-download-card">
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState, type FC } from 'react';
|
|
2
|
+
import { useSwitchChain } from 'wagmi';
|
|
3
|
+
|
|
4
|
+
interface SwitchChainViewProps {
|
|
5
|
+
currentChainId: number | undefined;
|
|
6
|
+
requiredChainId: number;
|
|
7
|
+
requiredChainName?: string;
|
|
8
|
+
onDisconnect: () => void;
|
|
9
|
+
onLog?: (message: string, error?: Error) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const SwitchChainView: FC<SwitchChainViewProps> = ({
|
|
13
|
+
currentChainId,
|
|
14
|
+
requiredChainId,
|
|
15
|
+
requiredChainName,
|
|
16
|
+
onDisconnect,
|
|
17
|
+
onLog,
|
|
18
|
+
}) => {
|
|
19
|
+
const { switchChainAsync, isPending } = useSwitchChain();
|
|
20
|
+
const [error, setError] = useState<Error | null>(null);
|
|
21
|
+
|
|
22
|
+
const targetLabel = requiredChainName ?? `chain ${requiredChainId}`;
|
|
23
|
+
|
|
24
|
+
const handleSwitch = (): void => {
|
|
25
|
+
setError(null);
|
|
26
|
+
switchChainAsync({ chainId: requiredChainId }).catch((err: unknown) => {
|
|
27
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
28
|
+
// wagmi's MetaMask connector handles 4902 (chain not added) via wallet_addEthereumChain
|
|
29
|
+
// automatically when chain metadata is configured. We only surface what reaches us.
|
|
30
|
+
const code =
|
|
31
|
+
typeof err === 'object' && err !== null && 'code' in err
|
|
32
|
+
? (err as { code: unknown }).code
|
|
33
|
+
: undefined;
|
|
34
|
+
if (code === 4902) {
|
|
35
|
+
onLog?.('[connect-kit] chain not registered in wallet (4902)', normalized);
|
|
36
|
+
} else {
|
|
37
|
+
onLog?.(`[connect-kit] switch chain failed: ${normalized.message}`, normalized);
|
|
38
|
+
}
|
|
39
|
+
setError(normalized);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="ck-view ck-view--switch-chain">
|
|
45
|
+
<h4 className="ck-view__title">Wrong network</h4>
|
|
46
|
+
<p className="ck-view__description">
|
|
47
|
+
{currentChainId != null
|
|
48
|
+
? `Connected to chain ${currentChainId}. Please switch to ${targetLabel} to continue.`
|
|
49
|
+
: `Please switch to ${targetLabel} to continue.`}
|
|
50
|
+
</p>
|
|
51
|
+
<div className="ck-view__actions">
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
className="ck-btn-primary"
|
|
55
|
+
onClick={handleSwitch}
|
|
56
|
+
disabled={isPending}
|
|
57
|
+
>
|
|
58
|
+
{isPending ? 'Waiting for wallet…' : `Switch to ${targetLabel}`}
|
|
59
|
+
</button>
|
|
60
|
+
<button type="button" className="ck-btn-secondary" onClick={onDisconnect}>
|
|
61
|
+
Disconnect
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
{error && <p className="ck-view__caption ck-view__caption--error">{error.message}</p>}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|