@gluwa/connect-kit 0.1.0-next.3 → 0.2.0-next.1

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/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@gluwa/connect-kit",
3
- "version": "0.1.0-next.3",
3
+ "version": "0.2.0-next.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
7
+ "types": "./src/index.ts",
7
8
  "default": "./src/index.ts"
8
9
  },
9
10
  "./credit-connect": {
11
+ "types": "./src/credit-connect.ts",
10
12
  "default": "./src/credit-connect.ts"
11
13
  },
12
14
  "./package.json": "./package.json"
@@ -27,7 +29,10 @@
27
29
  "./index.css": "./dist/index.css"
28
30
  }
29
31
  },
30
- "sideEffects": true,
32
+ "sideEffects": [
33
+ "**/*.scss",
34
+ "**/*.css"
35
+ ],
31
36
  "scripts": {
32
37
  "prepack": "pnpm build && cp package.json dist/ && cp README.md dist/",
33
38
  "build": "tsup",
@@ -38,7 +43,9 @@
38
43
  "author": "",
39
44
  "license": "ISC",
40
45
  "dependencies": {
41
- "qrcode.react": "^3.2.0"
46
+ "qrcode.react": "^4.2.0",
47
+ "wagmi": "^2.15.6",
48
+ "@wagmi/core": "^2.16.7"
42
49
  },
43
50
  "devDependencies": {
44
51
  "@gluwa/credit-connect-sdk": "workspace:*",
@@ -57,10 +64,8 @@
57
64
  "peerDependencies": {
58
65
  "@gluwa/credit-connect-sdk": "workspace:*",
59
66
  "@tanstack/react-query": ">=5.0.0",
60
- "@wagmi/core": ">=2.0.0",
61
67
  "react": "^18.0.0 || ^19.0.0",
62
- "viem": ">=2.0.0",
63
- "wagmi": ">=2.0.0"
68
+ "viem": ">=2.0.0"
64
69
  },
65
70
  "peerDependenciesMeta": {
66
71
  "@gluwa/credit-connect-sdk": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gluwa/connect-kit",
3
- "version": "0.1.0-next.3",
3
+ "version": "0.2.0-next.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -16,11 +16,16 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
- "sideEffects": true,
19
+ "sideEffects": [
20
+ "**/*.scss",
21
+ "**/*.css"
22
+ ],
20
23
  "author": "",
21
24
  "license": "ISC",
22
25
  "dependencies": {
23
- "qrcode.react": "^3.2.0"
26
+ "qrcode.react": "^4.2.0",
27
+ "wagmi": "^2.15.6",
28
+ "@wagmi/core": "^2.16.7"
24
29
  },
25
30
  "devDependencies": {
26
31
  "@tanstack/react-query": "^5.62.0",
@@ -38,10 +43,8 @@
38
43
  },
39
44
  "peerDependencies": {
40
45
  "@tanstack/react-query": ">=5.0.0",
41
- "@wagmi/core": ">=2.0.0",
42
46
  "react": "^18.0.0 || ^19.0.0",
43
47
  "viem": ">=2.0.0",
44
- "wagmi": ">=2.0.0",
45
48
  "@gluwa/credit-connect-sdk": "0.2.0-next.1"
46
49
  },
47
50
  "peerDependenciesMeta": {
@@ -9,46 +9,28 @@ import {
9
9
  type FC,
10
10
  type ReactNode,
11
11
  } from 'react';
12
- import { WagmiProvider, useAccount, useDisconnect } from 'wagmi';
13
- import type { Config } from 'wagmi';
12
+ import { WagmiProvider, useAccount, useDisconnect, useSwitchChain } from 'wagmi';
14
13
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
15
14
  import { ConnectModal } from './ConnectModal';
16
- import type { ConnectErrorContext, ConnectResult, ConnectorId, Connectors } from './types';
15
+ import { getConfig, getKitConfig, initConfig } from './core/config';
16
+ import type {
17
+ ConnectErrorContext,
18
+ ConnectKitConfig,
19
+ ConnectResult,
20
+ ConnectorId,
21
+ Connectors,
22
+ } from './types';
17
23
 
18
- // wagmiConfig prop으로 기존 프로젝트 통합 시 내부에서 사용하는 QueryClient.
19
- // 모듈 레벨에 한 번만 생성 — 연결 플로우용으로만 쓰이므로 공유해도 무방.
20
24
  const internalQueryClient = new QueryClient();
21
25
 
22
26
  export interface ConnectKitProviderProps {
23
27
  children: ReactNode;
28
+ kitConfig?: ConnectKitConfig;
24
29
  connectors: Connectors;
25
- wcProjectId?: string;
26
- /**
27
- * 연결돼 있는 동안 강제할 체인 ID. 다른 체인에 연결돼 있으면
28
- * 자동으로 스위치 모달이 떠서 사용자에게 전환 또는 disconnect를 요구.
29
- */
30
30
  requiredChainId?: number;
31
- /**
32
- * 연결이 정상적으로 끝나고(체인 검증까지 통과) "사용 가능한 상태"가 됐을 때 한 번 호출.
33
- * 재연결(reconnect)에서는 호출되지 않음 — 그쪽은 dApp의 watchAccount 책임.
34
- */
35
31
  onConnect?: (result: ConnectResult) => void;
36
32
  onLog?: (message: string, error?: Error) => void;
37
- /**
38
- * 연결 실패(거절/미설치/기타) 시 모달 안에 표시할 fallback UI를 dApp이 직접 그리고 싶을 때.
39
- * 미제공 시 기본 동작은 selector로 복귀.
40
- */
41
33
  renderConnectError?: (ctx: ConnectErrorContext) => ReactNode;
42
- /**
43
- * 기존 프로젝트 통합용. wagmi config를 넘기면 ConnectKitProvider 내부에서
44
- * WagmiProvider를 직접 세운다.
45
- *
46
- * - 기존 프로젝트: 앱에 WagmiProvider가 없으므로 wagmiConfig를 넘긴다.
47
- * ConnectKitProvider가 같은 config 인스턴스로 WagmiProvider를 셀프 마운트하므로
48
- * 기존 wagmi 상태 store와 단일 config를 공유한다.
49
- * - 신규 프로젝트: 앱이 이미 WagmiProvider를 감싸므로 이 prop은 불필요하다.
50
- */
51
- wagmiConfig?: Config;
52
34
  }
53
35
 
54
36
  export interface ConnectKitContextValue {
@@ -74,22 +56,30 @@ const resolveConnectorId = (wagmiConnectorId: string | undefined): ConnectorId |
74
56
  };
75
57
 
76
58
  export const ConnectKitProvider: FC<ConnectKitProviderProps> = (props) => {
77
- if (props.wagmiConfig) {
78
- return (
79
- <WagmiProvider config={props.wagmiConfig}>
80
- <QueryClientProvider client={internalQueryClient}>
81
- <ConnectKitProviderInner {...props} />
82
- </QueryClientProvider>
83
- </WagmiProvider>
84
- );
85
- }
86
- return <ConnectKitProviderInner {...props} />;
59
+ const wagmiConfig = useMemo(
60
+ () => (props.kitConfig ? initConfig(props.kitConfig) : getConfig()),
61
+ [props.kitConfig],
62
+ );
63
+
64
+ const resolvedKitConfig = props.kitConfig ?? getKitConfig();
65
+
66
+ return (
67
+ <WagmiProvider config={wagmiConfig} reconnectOnMount={false}>
68
+ <QueryClientProvider client={internalQueryClient}>
69
+ <ConnectKitProviderInner {...props} kitConfig={resolvedKitConfig} />
70
+ </QueryClientProvider>
71
+ </WagmiProvider>
72
+ );
73
+ };
74
+
75
+ type ConnectKitProviderInnerProps = Omit<ConnectKitProviderProps, 'kitConfig'> & {
76
+ kitConfig: ConnectKitConfig;
87
77
  };
88
78
 
89
- const ConnectKitProviderInner: FC<ConnectKitProviderProps> = ({
79
+ const ConnectKitProviderInner: FC<ConnectKitProviderInnerProps> = ({
90
80
  children,
91
81
  connectors,
92
- wcProjectId,
82
+ kitConfig,
93
83
  requiredChainId,
94
84
  onConnect,
95
85
  onLog,
@@ -100,21 +90,62 @@ const ConnectKitProviderInner: FC<ConnectKitProviderProps> = ({
100
90
 
101
91
  const account = useAccount();
102
92
  const { disconnectAsync } = useDisconnect();
93
+ const { switchChainAsync } = useSwitchChain();
103
94
 
104
- // dApp의 onConnect는 ref로 잡아 effect 재실행 노이즈 차단
105
95
  const onConnectRef = useRef(onConnect);
106
96
  useEffect(() => {
107
97
  onConnectRef.current = onConnect;
108
98
  }, [onConnect]);
109
99
 
110
- // chain mismatch invariant: 연결돼 있지만 요구 체인이 아니면 modal force-open
111
100
  const chainMismatch =
112
101
  requiredChainId != null &&
113
102
  account.status === 'connected' &&
114
103
  account.chainId != null &&
115
104
  account.chainId !== requiredChainId;
116
105
 
117
- const effectiveOpen = isOpen || chainMismatch;
106
+ const [autoSwitchFailedKey, setAutoSwitchFailedKey] = useState<string | null>(null);
107
+ const autoSwitchInFlightRef = useRef(false);
108
+ const isMountedRef = useRef(true);
109
+ useEffect(() => {
110
+ isMountedRef.current = true;
111
+ return () => {
112
+ isMountedRef.current = false;
113
+ };
114
+ }, []);
115
+
116
+ const mismatchKey =
117
+ chainMismatch && account.chainId != null && requiredChainId != null
118
+ ? `${account.chainId}->${requiredChainId}`
119
+ : null;
120
+
121
+ useEffect(() => {
122
+ if (!chainMismatch || requiredChainId == null) return;
123
+ if (autoSwitchInFlightRef.current) return;
124
+ if (mismatchKey != null && mismatchKey === autoSwitchFailedKey) return;
125
+
126
+ autoSwitchInFlightRef.current = true;
127
+ switchChainAsync({ chainId: requiredChainId })
128
+ .catch((err: unknown) => {
129
+ if (!isMountedRef.current) return;
130
+ const normalized = err instanceof Error ? err : new Error(String(err));
131
+ onLog?.(`[connect-kit] silent switch chain failed: ${normalized.message}`, normalized);
132
+ if (mismatchKey != null) {
133
+ setAutoSwitchFailedKey(mismatchKey);
134
+ }
135
+ })
136
+ .finally(() => {
137
+ autoSwitchInFlightRef.current = false;
138
+ });
139
+ }, [chainMismatch, mismatchKey, requiredChainId, autoSwitchFailedKey, switchChainAsync, onLog]);
140
+
141
+ useEffect(() => {
142
+ if (!chainMismatch && autoSwitchFailedKey != null) {
143
+ setAutoSwitchFailedKey(null);
144
+ }
145
+ }, [chainMismatch, autoSwitchFailedKey]);
146
+
147
+ const autoSwitchFailedForCurrent = mismatchKey != null && mismatchKey === autoSwitchFailedKey;
148
+ const effectiveOpen = isOpen || (chainMismatch && autoSwitchFailedForCurrent);
118
149
 
119
150
  const open = useCallback((): void => {
120
151
  setIsOpen(true);
@@ -128,22 +159,17 @@ const ConnectKitProviderInner: FC<ConnectKitProviderProps> = ({
128
159
  await disconnectAsync();
129
160
  }, [disconnectAsync]);
130
161
 
131
- // ConnectModal → Provider: 연결 결과를 일단 보류했다가
132
- // chain 검증까지 통과한 후에만 dApp으로 forward
133
162
  const handleModalConnect = useCallback((result: ConnectResult): void => {
134
163
  setPendingResult(result);
135
164
  }, []);
136
165
 
137
- // pendingResult가 있고, 연결 + chain 매칭이 충족되면 dApp으로 발화
138
166
  useEffect(() => {
139
167
  if (!pendingResult) return;
140
168
  if (account.status !== 'connected') {
141
- // 연결이 끊겼거나 진행 중 — pending 폐기
142
169
  setPendingResult(null);
143
170
  return;
144
171
  }
145
172
  if (requiredChainId != null && account.chainId !== requiredChainId) {
146
- // chain 아직 안 맞음 — switch 후 다시 평가
147
173
  return;
148
174
  }
149
175
  onConnectRef.current?.(pendingResult);
@@ -178,7 +204,7 @@ const ConnectKitProviderInner: FC<ConnectKitProviderProps> = ({
178
204
  <ConnectModal
179
205
  open={effectiveOpen}
180
206
  connectors={connectors}
181
- wcProjectId={wcProjectId}
207
+ wcProjectId={kitConfig.wcProjectId}
182
208
  requiredChainId={requiredChainId}
183
209
  renderConnectError={renderConnectError}
184
210
  onConnect={handleModalConnect}
@@ -21,28 +21,60 @@ import { WalletConnectView } from './views/WalletConnectView';
21
21
  import { SwitchChainView } from './views/SwitchChainView';
22
22
 
23
23
  const RECENT_KEY = 'connect-kit:recent';
24
+ const VALID_CONNECTOR_IDS: ReadonlySet<ConnectorId> = new Set<ConnectorId>([
25
+ 'CREDIT_CONNECT',
26
+ 'CREDIT_WALLET',
27
+ 'METAMASK',
28
+ 'WALLET_CONNECT',
29
+ ]);
30
+
24
31
  const readRecentConnector = (): ConnectorId | null => {
25
32
  try {
26
- return localStorage.getItem(RECENT_KEY) as ConnectorId | null;
33
+ const raw = localStorage.getItem(RECENT_KEY);
34
+ if (raw == null) return null;
35
+ return VALID_CONNECTOR_IDS.has(raw as ConnectorId) ? (raw as ConnectorId) : null;
27
36
  } catch {
28
37
  return null;
29
38
  }
30
39
  };
31
40
 
32
- const writeRecentConnector = (id: ConnectorId): void => {
41
+ const writeRecentConnector = (
42
+ id: ConnectorId,
43
+ onLog?: (message: string, error?: Error) => void,
44
+ ): void => {
33
45
  try {
34
46
  localStorage.setItem(RECENT_KEY, id);
35
- } catch {}
47
+ } catch (err) {
48
+ const normalized = err instanceof Error ? err : new Error(String(err));
49
+ onLog?.(`[connect-kit] writeRecentConnector failed: ${normalized.message}`, normalized);
50
+ }
36
51
  };
37
52
 
53
+ const REJECTED_ERROR_CODES = new Set<unknown>([
54
+ 4001, // EIP-1193 user rejected
55
+ 'ACTION_REJECTED', // ethers v6
56
+ ]);
57
+
38
58
  const classifyError = (error: Error): ConnectErrorReason => {
39
59
  const code =
40
60
  typeof error === 'object' && error !== null && 'code' in error
41
61
  ? (error as unknown as { code: unknown }).code
42
62
  : undefined;
43
- if (code === 4001) return 'rejected';
63
+ if (code != null && REJECTED_ERROR_CODES.has(code)) return 'rejected';
44
64
  const message = error.message?.toLowerCase() ?? '';
45
- if (message.includes('user rejected') || message.includes('user denied')) return 'rejected';
65
+ if (
66
+ message.includes('user rejected') ||
67
+ message.includes('user denied') ||
68
+ message.includes('user cancel') ||
69
+ message.includes('rejected by user') ||
70
+ message.includes('action_rejected') ||
71
+ message.includes('사용자가 거부') ||
72
+ message.includes('사용자가 취소') ||
73
+ message.includes('ユーザーが拒否') ||
74
+ message.includes('ユーザーがキャンセル')
75
+ ) {
76
+ return 'rejected';
77
+ }
46
78
  if (message.includes('unsupported') || message.includes('not installed')) return 'unsupported';
47
79
  return 'unknown';
48
80
  };
@@ -270,8 +302,6 @@ const resolveConnectorsFromConfig = (connectorsConfig?: Connectors): ConnectorId
270
302
  };
271
303
 
272
304
  export const ConnectModal: FC<ConnectModalProps> = (props) => {
273
- // open=false면 내부 컴포넌트를 마운트하지 않음
274
- // → useConfig 등 wagmi hooks가 실행되지 않아 WagmiProvider 없이도 렌더링 가능
275
305
  if (!props.open) return null;
276
306
 
277
307
  const resolvedConnectors = resolveConnectorsFromConfig(props.connectors);
@@ -312,7 +342,6 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
312
342
 
313
343
  const wc = useWCState();
314
344
 
315
- // wagmi 상태 — chain mismatch invariant 판정용
316
345
  const account = useAccount();
317
346
  const { disconnectAsync } = useDisconnect();
318
347
  const wagmiConfig = useConfig();
@@ -328,24 +357,19 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
328
357
  return wagmiConfig.chains.find((chain) => chain.id === requiredChainId)?.name;
329
358
  }, [wagmiConfig.chains, requiredChainId]);
330
359
 
331
- // QR URI 수신
332
360
  const handleQrUri = useCallback((uri: string | null) => {
333
361
  setQrUri(uri);
334
362
  }, []);
335
363
 
336
- // 연결 완료
337
364
  const handleConnect = useCallback(
338
365
  (result: Parameters<typeof onConnect>[0]) => {
339
366
  setErrorState(null);
340
367
  onConnect(result);
341
- // chain mismatch가 있으면 모달은 SwitchChainView로 자동 전환됨 (닫지 않음)
342
- // Provider 측에서 requiredChainId가 매칭될 때까지 force-open 유지
343
368
  onClose();
344
369
  },
345
370
  [onConnect, onClose],
346
371
  );
347
372
 
348
- // 연결 실패
349
373
  const handleError = useCallback(
350
374
  (error: unknown, connectorId: ConnectorId): void => {
351
375
  const normalizedError = error instanceof Error ? error : new Error(String(error));
@@ -364,14 +388,12 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
364
388
  [onLog, wc.resetView, renderConnectError],
365
389
  );
366
390
 
367
- // 연결 시도
368
391
  const { triggerConnect, cancelConnect } = useWagmiConnect({
369
392
  onConnect: handleConnect,
370
393
  onError: handleError,
371
394
  onQrUri: handleQrUri,
372
395
  });
373
396
 
374
- // wagmi connector icon 매핑
375
397
  const connectorLogoMap = useMemo(() => {
376
398
  const map: Partial<Record<ConnectorId, string>> = {};
377
399
  const connectorIds: ConnectorId[] = [
@@ -387,13 +409,11 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
387
409
  return map;
388
410
  }, [wagmiConfig.connectors]);
389
411
 
390
- // 최근 연결 기록 로드
391
412
  useEffect(() => {
392
413
  setRecentConnector(readRecentConnector());
393
414
  setHasMetaMaskExtension(detectMetaMaskExtension());
394
415
  }, []);
395
416
 
396
- // 모달 닫힐 때 상태 초기화
397
417
  useEffect(() => {
398
418
  if (!open) {
399
419
  setSelectedConnector(null);
@@ -404,7 +424,6 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
404
424
  }
405
425
  }, [open, cancelConnect, wc.reset]);
406
426
 
407
- // 커넥터 선택 — 선택할 때마다 QR을 초기화해 새 세션 URI가 표시되도록 함
408
427
  const handleSelectConnector = useCallback(
409
428
  (id: ConnectorId): void => {
410
429
  if (id === selectedConnector && !errorState) return;
@@ -412,7 +431,7 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
412
431
  setQrUri(null);
413
432
  setErrorState(null);
414
433
  wc.resetView();
415
- writeRecentConnector(id);
434
+ writeRecentConnector(id, onLog);
416
435
  setRecentConnector(id);
417
436
 
418
437
  if (id === 'WALLET_CONNECT' && wcProjectId) {
@@ -430,10 +449,10 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
430
449
  wcProjectId,
431
450
  wc.resetView,
432
451
  wc.loadWalletList,
452
+ onLog,
433
453
  ],
434
454
  );
435
455
 
436
- // 뒤로가기
437
456
  const handleBack = useCallback((): void => {
438
457
  if (errorState) {
439
458
  setErrorState(null);
@@ -446,7 +465,6 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
446
465
  cancelConnect();
447
466
  }, [selectedConnector, errorState, wc.handleBack, cancelConnect]);
448
467
 
449
- // chain mismatch 상태에서의 disconnect (Switch chain pane의 cancel)
450
468
  const handleSwitchChainDisconnect = useCallback((): void => {
451
469
  disconnectAsync().catch((err: unknown) => {
452
470
  const normalized = err instanceof Error ? err : new Error(String(err));
@@ -454,13 +472,11 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
454
472
  });
455
473
  }, [disconnectAsync, onLog]);
456
474
 
457
- // overlay 클릭 — chain mismatch 상태에선 무시 (실수 클릭 방어)
458
475
  const handleOverlayClick = useCallback((): void => {
459
476
  if (chainMismatch) return;
460
477
  onClose();
461
478
  }, [chainMismatch, onClose]);
462
479
 
463
- // close 버튼 — chain mismatch 상태에선 disconnect
464
480
  const handleCloseClick = useCallback((): void => {
465
481
  if (chainMismatch) {
466
482
  handleSwitchChainDisconnect();
@@ -490,8 +506,6 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
490
506
  return renderConnectError(ctx);
491
507
  }, [errorState, renderConnectError, triggerConnect, cancelConnect]);
492
508
 
493
- // chain mismatch면 단일 pane (selector/detail 숨김)
494
- // requiredChainId != null 재확인은 TS narrowing용 — chainMismatch true면 이미 보장됨
495
509
  if (chainMismatch && requiredChainId != null) {
496
510
  return (
497
511
  <div className="ck-root">
@@ -0,0 +1,21 @@
1
+ import {
2
+ getAccount as wagmiGetAccount,
3
+ signMessage as wagmiSignMessage,
4
+ signTypedData as wagmiSignTypedData,
5
+ type GetAccountReturnType,
6
+ type SignMessageParameters,
7
+ type SignTypedDataParameters,
8
+ } from '@wagmi/core';
9
+ import { getConfig } from '../core/config';
10
+
11
+ export const getAccount = (): GetAccountReturnType => {
12
+ return wagmiGetAccount(getConfig());
13
+ };
14
+
15
+ export const signMessage = (params: SignMessageParameters): Promise<`0x${string}`> => {
16
+ return wagmiSignMessage(getConfig(), params);
17
+ };
18
+
19
+ export const signTypedData = (params: SignTypedDataParameters): Promise<`0x${string}`> => {
20
+ return wagmiSignTypedData(getConfig(), params);
21
+ };
@@ -0,0 +1,10 @@
1
+ import {
2
+ getBalance as wagmiGetBalance,
3
+ type GetBalanceParameters,
4
+ type GetBalanceReturnType,
5
+ } from '@wagmi/core';
6
+ import { getConfig } from '../core/config';
7
+
8
+ export const getBalance = (params: GetBalanceParameters): Promise<GetBalanceReturnType> => {
9
+ return wagmiGetBalance(getConfig(), params);
10
+ };
@@ -0,0 +1,17 @@
1
+ import {
2
+ switchChain as wagmiSwitchChain,
3
+ getPublicClient as wagmiGetPublicClient,
4
+ type SwitchChainParameters,
5
+ type GetPublicClientParameters,
6
+ type GetPublicClientReturnType,
7
+ } from '@wagmi/core';
8
+ import type { Chain } from 'viem';
9
+ import { getConfig } from '../core/config';
10
+
11
+ export const switchChain = (params: SwitchChainParameters): Promise<Chain> => {
12
+ return wagmiSwitchChain(getConfig(), params);
13
+ };
14
+
15
+ export const getPublicClient = (params?: GetPublicClientParameters): GetPublicClientReturnType => {
16
+ return wagmiGetPublicClient(getConfig(), params);
17
+ };
@@ -0,0 +1,33 @@
1
+ import {
2
+ readContract as wagmiReadContract,
3
+ writeContract as wagmiWriteContract,
4
+ type ReadContractParameters,
5
+ type ReadContractReturnType,
6
+ type WriteContractParameters,
7
+ type WriteContractReturnType,
8
+ type Config,
9
+ } from '@wagmi/core';
10
+ import type { Abi, ContractFunctionArgs, ContractFunctionName } from 'viem';
11
+ import { getConfig } from '../core/config';
12
+
13
+ export function readContract<
14
+ const abi extends Abi | readonly unknown[],
15
+ functionName extends ContractFunctionName<abi, 'pure' | 'view'>,
16
+ args extends ContractFunctionArgs<abi, 'pure' | 'view', functionName>,
17
+ >(
18
+ parameters: ReadContractParameters<abi, functionName, args, Config>,
19
+ ): Promise<ReadContractReturnType<abi, functionName, args>> {
20
+ return wagmiReadContract(getConfig(), parameters as any) as Promise<
21
+ ReadContractReturnType<abi, functionName, args>
22
+ >;
23
+ }
24
+
25
+ export function writeContract<
26
+ const abi extends Abi | readonly unknown[],
27
+ functionName extends ContractFunctionName<abi, 'nonpayable' | 'payable'>,
28
+ args extends ContractFunctionArgs<abi, 'nonpayable' | 'payable', functionName>,
29
+ >(
30
+ parameters: WriteContractParameters<abi, functionName, args, Config>,
31
+ ): Promise<WriteContractReturnType> {
32
+ return wagmiWriteContract(getConfig(), parameters as any);
33
+ }
@@ -0,0 +1,12 @@
1
+ import {
2
+ waitForTransactionReceipt as wagmiWaitForTransactionReceipt,
3
+ type WaitForTransactionReceiptParameters,
4
+ type WaitForTransactionReceiptReturnType,
5
+ } from '@wagmi/core';
6
+ import { getConfig } from '../core/config';
7
+
8
+ export const waitForTransactionReceipt = (
9
+ params: WaitForTransactionReceiptParameters,
10
+ ): Promise<WaitForTransactionReceiptReturnType> => {
11
+ return wagmiWaitForTransactionReceipt(getConfig(), params);
12
+ };
@@ -14,7 +14,7 @@ export const QRFrame: FC<QRFrameProps> = ({ qrUri, logoUrl }) => (
14
14
  size={196}
15
15
  level="H"
16
16
  imageSettings={
17
- logoUrl ? { src: logoUrl, width: 40, height: 40, excavate: true } : undefined
17
+ logoUrl ? { src: logoUrl, width: 50, height: 50, excavate: true } : undefined
18
18
  }
19
19
  />
20
20
  ) : (
@@ -34,7 +34,7 @@ export const CopyLinkButton: FC<CopyLinkButtonProps> = ({ qrUri }) => {
34
34
  type="button"
35
35
  className="ck-copy-link"
36
36
  onClick={() => {
37
- navigator.clipboard.writeText(qrUri);
37
+ navigator.clipboard?.writeText(qrUri).catch(() => {});
38
38
  }}
39
39
  >
40
40
  Copy link
@@ -0,0 +1,83 @@
1
+ import { createConfig, createStorage } from 'wagmi';
2
+ import { metaMask, walletConnect, injected } from 'wagmi/connectors';
3
+ import type { Config } from 'wagmi';
4
+ import type { ConnectKitConfig } from '../types';
5
+
6
+ type KitSlot = {
7
+ config: Config | null;
8
+ kitConfig: ConnectKitConfig | null;
9
+ };
10
+
11
+ const SLOT_KEY = Symbol.for('@gluwa/connect-kit:slot');
12
+
13
+ type GlobalWithSlot = typeof globalThis & {
14
+ [SLOT_KEY]?: KitSlot;
15
+ };
16
+
17
+ const getSlot = (): KitSlot => {
18
+ const g = globalThis as GlobalWithSlot;
19
+ if (!g[SLOT_KEY]) {
20
+ g[SLOT_KEY] = { config: null, kitConfig: null };
21
+ }
22
+ return g[SLOT_KEY];
23
+ };
24
+
25
+ export const initConfig = (kitConfig: ConnectKitConfig): Config => {
26
+ const slot = getSlot();
27
+ if (slot.config) {
28
+ if (slot.kitConfig !== kitConfig) {
29
+ // eslint-disable-next-line no-console
30
+ console.warn(
31
+ '[connect-kit] initConfig called again with a different kitConfig — new chains/transports ignored. Call resetConfig() first to re-init.',
32
+ );
33
+ }
34
+ return slot.config;
35
+ }
36
+
37
+ const connectors = [];
38
+
39
+ if (kitConfig.wcProjectId) {
40
+ connectors.push(walletConnect({ projectId: kitConfig.wcProjectId, showQrModal: false }));
41
+ }
42
+
43
+ connectors.push(metaMask({ storage: { enabled: true } }), injected({ shimDisconnect: false }));
44
+
45
+ if (kitConfig.extraConnectors?.length) {
46
+ connectors.push(...kitConfig.extraConnectors);
47
+ }
48
+
49
+ const isBrowser = typeof window !== 'undefined';
50
+
51
+ slot.config = createConfig({
52
+ ssr: true,
53
+ chains: kitConfig.chains,
54
+ connectors,
55
+ transports: kitConfig.transports,
56
+ storage: isBrowser ? createStorage({ storage: window.localStorage }) : undefined,
57
+ });
58
+ slot.kitConfig = kitConfig;
59
+
60
+ return slot.config;
61
+ };
62
+
63
+ export const getConfig = (): Config => {
64
+ const slot = getSlot();
65
+ if (!slot.config) {
66
+ throw new Error('ConnectKit config not initialized. Make sure ConnectKitProvider is mounted.');
67
+ }
68
+ return slot.config;
69
+ };
70
+
71
+ export const getKitConfig = (): ConnectKitConfig => {
72
+ const slot = getSlot();
73
+ if (!slot.kitConfig) {
74
+ throw new Error('ConnectKit config not initialized. Make sure ConnectKitProvider is mounted.');
75
+ }
76
+ return slot.kitConfig;
77
+ };
78
+
79
+ export const resetConfig = (): void => {
80
+ const slot = getSlot();
81
+ slot.config = null;
82
+ slot.kitConfig = null;
83
+ };