@gluwa/connect-kit 0.1.0-next.0 → 0.1.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.
@@ -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, useAccount, useConfig } from 'wagmi';
2
+ import { useConnect, useConfig } from 'wagmi';
3
3
  import type { ConnectorId, ConnectResult } from '../types';
4
4
 
5
- export const WAGMI_CONNECTOR_ID: Partial<Record<ConnectorId, string>> = {
6
- METAMASK: 'metaMaskSDK',
7
- WALLET_CONNECT: 'walletConnect',
8
- CREDIT_WALLET: 'walletConnect',
9
- CREDIT_CONNECT: 'credit-connect',
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 { connect, reset, isPending } = useConnect({
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 wagmiId = WAGMI_CONNECTOR_ID[connectorId];
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
- connect({ connector });
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, connect],
137
+ [config.connectors, connectAsync],
115
138
  );
116
139
 
117
140
  // 연결 취소
package/src/index.ts CHANGED
@@ -1,4 +1,12 @@
1
1
  export { ConnectModal } from './ConnectModal';
2
2
  export { creditConnectConnector } from './creditConnectConnector';
3
3
  export type { CreditConnectConnectorOptions } from './creditConnectConnector';
4
- export type { ConnectModalProps, ConnectResult, ConnectorId, WCWallet, WCSubView } from './types';
4
+ export type {
5
+ ConnectModalProps,
6
+ Connectors,
7
+ ConnectResult,
8
+ ConnectorId,
9
+ CreditWalletStrategy,
10
+ WCSubView,
11
+ WCWallet,
12
+ } from './types';
package/src/types.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  export type ConnectorId = 'CREDIT_WALLET' | 'CREDIT_CONNECT' | 'METAMASK' | 'WALLET_CONNECT';
2
2
 
3
- export type ValidConnectors =
4
- | Array<Exclude<ConnectorId, 'WALLET_CONNECT'>>
5
- | Array<Exclude<ConnectorId, 'CREDIT_WALLET'>>;
3
+ export type CreditWalletStrategy = 'walletConnect' | 'creditConnect';
4
+
5
+ export interface Connectors {
6
+ creditWallet?: CreditWalletStrategy | false;
7
+ metamask?: boolean;
8
+ walletConnect?: boolean;
9
+ }
6
10
 
7
11
  export interface WCWallet {
8
12
  id: string;
@@ -26,7 +30,7 @@ export interface ConnectResult {
26
30
 
27
31
  export interface ConnectModalProps {
28
32
  open: boolean;
29
- connectors: ValidConnectors;
33
+ connectors: Connectors;
30
34
  onConnect: (result: ConnectResult) => void;
31
35
  onClose: () => void;
32
36
  onLog?: (message: string, error?: Error) => void;
@@ -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
- <div className={`ck-qr-frame ${!qrUri ? 'ck-qr-frame--loading' : ''}`}>
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
- <div className={`ck-qr-frame ${!qrUri ? 'ck-qr-frame--loading' : ''}`}>
45
- {qrUri ? (
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">
@@ -1,8 +1,9 @@
1
1
  import { type FC, useMemo } from 'react';
2
- import { QRCodeSVG } from 'qrcode.react';
3
2
  import { isMobileDevice, tryOpenDeepLink } from '../utils/platform';
4
3
  import { type WCWallet, type WCSubView } from '../types';
5
4
  import deeplink from '../../assets/deeplink.png';
5
+ import { QRFrame, CopyLinkButton } from '../components/QRFrame';
6
+ import { CONNECTOR_META } from '../connector-meta';
6
7
 
7
8
  interface WalletConnectViewProps {
8
9
  subView: WCSubView;
@@ -30,42 +31,11 @@ export const WalletConnectView: FC<WalletConnectViewProps> = (props) => {
30
31
 
31
32
  const WCQRView: FC<WalletConnectViewProps> = ({ qrUri, logoUrl, walletList, onShowList }) => (
32
33
  <div className="ck-view ck-view--qr">
33
- <div className={`ck-qr-frame ${!qrUri ? 'ck-qr-frame--loading' : ''}`}>
34
- {qrUri ? (
35
- <QRCodeSVG
36
- value={qrUri}
37
- size={196}
38
- level="H"
39
- imageSettings={
40
- logoUrl
41
- ? {
42
- src: logoUrl,
43
- width: 40,
44
- height: 40,
45
- excavate: true,
46
- }
47
- : undefined
48
- }
49
- />
50
- ) : (
51
- <div className="ck-qr-spinner" aria-label="Loading QR code" />
52
- )}
53
- </div>
34
+ <QRFrame qrUri={qrUri} logoUrl={logoUrl} />
54
35
 
55
36
  <p className="ck-view__caption">Scan this QR Code with your phone</p>
56
37
 
57
- {qrUri && (
58
- <button
59
- type="button"
60
- className="ck-copy-link"
61
- onClick={() => {
62
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
63
- navigator.clipboard.writeText(qrUri);
64
- }}
65
- >
66
- Copy link
67
- </button>
68
- )}
38
+ <CopyLinkButton qrUri={qrUri} />
69
39
 
70
40
  <button type="button" className="ck-all-wallets-btn" onClick={onShowList}>
71
41
  <span className="ck-all-wallets-btn__icon" aria-hidden="true" />
@@ -77,7 +47,7 @@ const WCQRView: FC<WalletConnectViewProps> = ({ qrUri, logoUrl, walletList, onSh
77
47
  </div>
78
48
  );
79
49
 
80
- const CREDIT_WALLET_NAME = 'Credit Wallet';
50
+ const CREDIT_WALLET_NAME = CONNECTOR_META.CREDIT_WALLET.label;
81
51
 
82
52
  const WalletListView: FC<WalletConnectViewProps> = ({
83
53
  walletList,
@@ -90,7 +60,7 @@ const WalletListView: FC<WalletConnectViewProps> = ({
90
60
  onSearchChange,
91
61
  onFilterToggle,
92
62
  }) => {
93
- const isMobile = isMobileDevice();
63
+ const isMobile = useMemo(() => isMobileDevice(), []);
94
64
 
95
65
  const filtered = useMemo(() => {
96
66
  const q = walletListSearch.trim().toLowerCase();
@@ -111,11 +81,10 @@ const WalletListView: FC<WalletConnectViewProps> = ({
111
81
  }, [walletList, walletListSearch, walletListFilterActive]);
112
82
 
113
83
  const handleClickWallet = (wallet: WCWallet): void => {
114
- const uri = qrUri;
115
- if (isMobile && uri) {
84
+ if (isMobile && qrUri) {
116
85
  const target = wallet.mobileNative || wallet.mobileUniversal;
117
86
  if (target) {
118
- tryOpenDeepLink(uri, target);
87
+ tryOpenDeepLink(qrUri, target);
119
88
  return;
120
89
  }
121
90
  }
@@ -211,40 +180,11 @@ const WalletQRView: FC<{ wallet: WCWallet; qrUri: string | null }> = ({ wallet,
211
180
 
212
181
  return (
213
182
  <div className="ck-view ck-view--qr">
214
- <div className={`ck-qr-frame ${!qrUri ? 'ck-qr-frame--loading' : ''}`}>
215
- {qrUri ? (
216
- <QRCodeSVG
217
- value={qrUri}
218
- size={196}
219
- level="H"
220
- {...(logoSrc && {
221
- imageSettings: {
222
- src: logoSrc,
223
- width: 40,
224
- height: 40,
225
- excavate: true,
226
- },
227
- })}
228
- />
229
- ) : (
230
- <div className="ck-qr-spinner" aria-label="Loading QR code" />
231
- )}
232
- </div>
183
+ <QRFrame qrUri={qrUri} logoUrl={logoSrc} />
233
184
 
234
185
  <p className="ck-view__caption">Scan this QR Code with your phone</p>
235
186
 
236
- {qrUri && (
237
- <button
238
- type="button"
239
- className="ck-copy-link"
240
- onClick={() => {
241
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
242
- navigator.clipboard.writeText(qrUri);
243
- }}
244
- >
245
- Copy link
246
- </button>
247
- )}
187
+ <CopyLinkButton qrUri={qrUri} />
248
188
 
249
189
  {downloadUrl && (
250
190
  <div className="ck-download-card">
package/tsup.config.ts CHANGED
@@ -7,7 +7,18 @@ export default defineConfig({
7
7
  dts: true,
8
8
  clean: true,
9
9
  target: 'es2019',
10
- external: ['react', 'react-native'],
10
+ external: [
11
+ 'react',
12
+ 'react-native',
13
+ 'wagmi',
14
+ '@wagmi/core',
15
+ 'viem',
16
+ '@gluwa/credit-connect-sdk',
17
+ '@gluwa/credit-connect-sdk/dapp',
18
+ '@gluwa/credit-connect-sdk/storage',
19
+ '@gluwa/credit-connect-sdk/e2ee',
20
+ '@gluwa/credit-connect-sdk/hub',
21
+ ],
11
22
  banner: {
12
23
  js: 'import React from "react";',
13
24
  },