@gluwa/connect-kit 0.2.0-next.9 → 0.2.1-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.
Files changed (43) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/assets/wc.png +0 -0
  3. package/dist/{chunk-VE5LB7BN.js → chunk-CJANPJHY.js} +4 -2
  4. package/dist/credit-connect.js +1 -1
  5. package/dist/index.css +178 -122
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.js +471 -182
  8. package/dist/package.json +1 -1
  9. package/package.json +6 -6
  10. package/src/ConnectKitProvider.tsx +8 -1
  11. package/src/components/qrArea/index.tsx +25 -3
  12. package/src/components/qrArea/style.scss +61 -2
  13. package/src/components/toast/index.tsx +41 -0
  14. package/src/components/toast/style.scss +108 -0
  15. package/src/core/config.ts +23 -2
  16. package/src/core/deeplinkSignal.ts +25 -0
  17. package/src/creditConnectConnector.ts +15 -5
  18. package/src/events/account.ts +15 -6
  19. package/src/hooks/useConnectBanner.ts +79 -0
  20. package/src/hooks/useConnectTimeout.ts +21 -9
  21. package/src/hooks/useMetaMaskExtension.ts +2 -1
  22. package/src/hooks/useWCState.ts +7 -2
  23. package/src/hooks/useWagmiConnect.ts +41 -14
  24. package/src/index.ts +1 -0
  25. package/src/layout/allWallets/index.tsx +16 -2
  26. package/src/layout/connectDialog/asset/warningIcon.tsx +21 -0
  27. package/src/layout/connectDialog/idle/assets/banner-placeholder.png +0 -0
  28. package/src/layout/connectDialog/idle/index.tsx +40 -16
  29. package/src/layout/connectDialog/idle/style.scss +42 -51
  30. package/src/layout/connectDialog/index.tsx +174 -37
  31. package/src/layout/connectDialog/metaMask/index.tsx +3 -2
  32. package/src/layout/connectDialog/qr/index.tsx +3 -1
  33. package/src/layout/connectDialog/style.scss +0 -48
  34. package/src/layout/connectDialog/timeout/index.tsx +0 -5
  35. package/src/layout/connectDialog/timeout/style.scss +0 -5
  36. package/src/layout/connectDialog/walletQR/index.tsx +3 -1
  37. package/src/layout/detailPanel/index.tsx +39 -6
  38. package/src/layout/detailPanel/style.scss +2 -1
  39. package/src/layout/selectorPanel/index.tsx +1 -0
  40. package/src/types.ts +20 -0
  41. package/src/utils/platform.ts +7 -0
  42. package/src/components/snackbar/index.tsx +0 -25
  43. package/src/components/snackbar/style.scss +0 -51
@@ -3,16 +3,19 @@ import { type ConnectorId } from '../types';
3
3
 
4
4
  // MM / CC 폴백용 기본 타임아웃. (CC 는 원칙적으로 서버 push 로 만료가 오지만,
5
5
  // 서버 값이 없거나 신호가 유실된 경우를 대비한 폴백 용도로도 쓸 수 있음.)
6
- export const DEFAULT_CONNECT_TIMEOUT_MS = 600_000;
6
+ export const DEFAULT_CONNECT_TIMEOUT_MS = 300_000;
7
7
 
8
8
  interface UseConnectTimeoutParams {
9
9
  /** 현재 연결 시도 중인 커넥터. null = 유휴 상태. */
10
10
  connectorId: ConnectorId | null;
11
11
  /** 타임아웃 감시 활성화 여부 — pending/QR 표시 중일 때 true. */
12
12
  active: boolean;
13
+ /** Wagmi connect promise가 실제로 대기 중인지 여부. */
14
+ isConnecting: boolean;
13
15
  /**
14
- * 외부 이벤트 기반 만료 신호 (MM 제외). 값이 증가할 때마다 "만료 발생" 으로 간주.
16
+ * 외부 이벤트 기반 실패/만료 신호. 값이 증가할 때마다 연결 실패로 간주.
15
17
  * - CC: dialog 가 connector 의 `session_expired`(서버 push) 를 받아 올림.
18
+ * - MM: connect promise reject.
16
19
  * - WC: dialog 가 WC connect promise reject(프로토콜 ~5분 만료 등) 를 받아 올림.
17
20
  */
18
21
  expiredSignal?: number;
@@ -23,6 +26,8 @@ interface UseConnectTimeoutParams {
23
26
  interface UseConnectTimeoutResult {
24
27
  /** 타임아웃 발생 여부 — true 면 dialog 가 <Timeout> 을 렌더. */
25
28
  timedOut: boolean;
29
+ /** 다른 지갑 연결을 시작하기 전에 이전 타임아웃 상태를 즉시 초기화. */
30
+ reset: () => void;
26
31
  /** "Try Again" 버튼에 연결할 함수 — 상태 리셋 + onRetry 실행. */
27
32
  retry: () => void;
28
33
  }
@@ -36,6 +41,7 @@ interface UseConnectTimeoutResult {
36
41
  export const useConnectTimeout = ({
37
42
  connectorId,
38
43
  active,
44
+ isConnecting,
39
45
  expiredSignal = 0,
40
46
  onRetry,
41
47
  }: UseConnectTimeoutParams): UseConnectTimeoutResult => {
@@ -54,16 +60,18 @@ export const useConnectTimeout = ({
54
60
  // MM 만 client 타이머로 강제 (승인 대기에 네이티브 상한이 없음).
55
61
  // CC(서버 push) / WC(connect reject) 는 아래 expiredSignal effect 에서 처리.
56
62
  useEffect(() => {
57
- if (!active || connectorId !== 'METAMASK') return;
58
- const timer = setTimeout(fire, DEFAULT_CONNECT_TIMEOUT_MS);
63
+ if (!active || !isConnecting || connectorId !== 'METAMASK') return;
64
+ const timer = setTimeout(() => {
65
+ fire();
66
+ }, DEFAULT_CONNECT_TIMEOUT_MS);
59
67
  return () => clearTimeout(timer);
60
- }, [active, connectorId, fire]);
68
+ }, [active, isConnecting, connectorId, fire]);
61
69
 
62
- // 외부 만료 신호 처리 (CC 서버 push / WC connect reject) — MM 은 client 타이머라 제외.
70
+ // 외부 실패/만료 신호 처리 (CC 서버 push / MM·WC connect reject).
63
71
  // signal 값이 올라가면(새 만료 이벤트) 발화.
64
72
  const prevSignal = useRef(expiredSignal);
65
73
  useEffect(() => {
66
- if (!active || connectorId === 'METAMASK' || !connectorId) {
74
+ if (!active || !connectorId) {
67
75
  prevSignal.current = expiredSignal;
68
76
  return;
69
77
  }
@@ -76,7 +84,11 @@ export const useConnectTimeout = ({
76
84
  const retry = useCallback(() => {
77
85
  setTimedOut(false);
78
86
  onRetry?.();
79
- }, [onRetry]);
87
+ }, [connectorId, onRetry]);
88
+
89
+ const reset = useCallback(() => {
90
+ setTimedOut(false);
91
+ }, []);
80
92
 
81
- return { timedOut, retry };
93
+ return { timedOut, reset, retry };
82
94
  };
@@ -13,7 +13,8 @@ export const useMetaMaskExtension = (): boolean => {
13
13
 
14
14
  useEffect(() => {
15
15
  const handleAnnouncement = (event: Event): void => {
16
- if (isMetaMaskAnnouncement(event)) setIsInstalled(true);
16
+ const isMetaMask = isMetaMaskAnnouncement(event);
17
+ if (isMetaMask) setIsInstalled(true);
17
18
  };
18
19
 
19
20
  // 응답 listener를 먼저 등록한 뒤 설치된 provider 목록을 요청.
@@ -11,7 +11,10 @@ const sortCreditWalletFirst = (list: WCWallet[]): WCWallet[] =>
11
11
  return 0;
12
12
  });
13
13
 
14
- const fetchWCWalletList = async (projectId: string, signal?: AbortSignal): Promise<WCWallet[]> => {
14
+ const fetchWCWalletList = async (
15
+ projectId: string,
16
+ signal: AbortSignal | undefined,
17
+ ): Promise<WCWallet[]> => {
15
18
  // WC Explorer API 가 page 경계에서 같은 wallet 을 중복 emit 하는 케이스 관찰됨
16
19
  // (paginate 사이 sort/dedup 보장 X). id 기준 first-write-wins dedup.
17
20
  const byId = new Map<string, WCWallet>();
@@ -106,7 +109,9 @@ export const useWCState = (): WCState => {
106
109
  }, []);
107
110
 
108
111
  const loadWalletList = useCallback(async (projectId: string): Promise<void> => {
109
- if (loadedRef.current) return;
112
+ if (loadedRef.current) {
113
+ return;
114
+ }
110
115
  loadedRef.current = true;
111
116
  const controller = new AbortController();
112
117
  abortControllerRef.current = controller;
@@ -1,6 +1,9 @@
1
1
  import { useEffect, useRef, useCallback } from 'react';
2
2
  import { useConnect, useConfig } from 'wagmi';
3
+ import { getAccount } from '@wagmi/core';
3
4
  import type { ConnectorId, ConnectResult } from '../types';
5
+ import { resolveConnectorId } from '../connector-meta';
6
+ import { resetDeeplinkSignal } from '../core/deeplinkSignal';
4
7
 
5
8
  type WagmiConnectorLike = {
6
9
  id: string;
@@ -73,6 +76,7 @@ export const useWagmiConnect = ({
73
76
  }: Options): UseWagmiConnectResult => {
74
77
  const config = useConfig();
75
78
  const pendingConnectorId = useRef<ConnectorId | null>(null);
79
+ const pendingAttemptId = useRef<string | null>(null);
76
80
  const attemptCounter = useRef(0);
77
81
 
78
82
  const onConnectRef = useRef(onConnect);
@@ -89,17 +93,27 @@ export const useWagmiConnect = ({
89
93
  const { connectAsync, reset, isPending } = useConnect();
90
94
 
91
95
  useEffect(() => {
92
- const handler = (data: unknown): void => {
93
- const msg = data as { type?: string; data?: unknown };
94
- if (msg?.type === 'display_uri' && typeof msg.data === 'string') {
95
- onQrUriRef.current(msg.data);
96
- }
97
- if (msg?.type === 'session_expired') {
98
- onSessionExpiredRef.current?.();
99
- }
100
- };
101
-
102
96
  const unsubscribers = config.connectors.map((connector) => {
97
+ const emitterConnectorId = resolveConnectorId(connector.id);
98
+ const handler = (data: unknown): void => {
99
+ const msg = data as { type?: string; data?: unknown };
100
+ const pendingId = pendingConnectorId.current;
101
+ const belongsToPendingAttempt =
102
+ pendingId !== null && emitterConnectorId !== null && pendingId === emitterConnectorId;
103
+
104
+ if (msg?.type === 'display_uri' && typeof msg.data === 'string') {
105
+ if (!belongsToPendingAttempt) {
106
+ return;
107
+ }
108
+ onQrUriRef.current(msg.data);
109
+ }
110
+ if (msg?.type === 'session_expired') {
111
+ if (pendingId !== null && !belongsToPendingAttempt) {
112
+ return;
113
+ }
114
+ onSessionExpiredRef.current?.();
115
+ }
116
+ };
103
117
  connector.emitter.on('message', handler);
104
118
  return (): void => {
105
119
  connector.emitter.off('message', handler);
@@ -117,26 +131,38 @@ export const useWagmiConnect = ({
117
131
  (connectorId: ConnectorId): void => {
118
132
  const connector = resolveWagmiConnector(config.connectors, connectorId);
119
133
  if (!connector) {
134
+ pendingAttemptId.current = null;
120
135
  onErrorRef.current(new Error(`No wagmi connector found for ${connectorId}`), connectorId);
121
136
  return;
122
137
  }
123
138
 
124
139
  onQrUriRef.current(null);
140
+ resetDeeplinkSignal();
125
141
  const attemptId = ++attemptCounter.current;
126
142
  pendingConnectorId.current = connectorId;
127
143
  connectAsync({ connector })
128
144
  .then((result) => {
129
- if (attemptCounter.current !== attemptId) return;
130
- const address = result.accounts?.[0];
145
+ if (attemptCounter.current !== attemptId) {
146
+ return;
147
+ }
148
+ // connectAsync 의 resolve 값에 계정이 비어 오는 경우가 있다 (MetaMask 모바일에서
149
+ // authorized 직후 accounts 채워짐이 한 tick 늦는 race). 그때도 wagmi 스토어에는
150
+ // 이미 주소가 들어와 있으므로 '연결됐는가'의 원본인 스토어를 fallback 으로 쓴다.
151
+ // 이 fallback 이 없으면 성공한 연결 위에 실패/타임아웃 화면이 덮인다.
152
+ const address = result.accounts?.[0] ?? getAccount(config).address;
131
153
  if (!address) {
132
154
  throw new Error('Connected but no account returned');
133
155
  }
134
156
  pendingConnectorId.current = null;
157
+ pendingAttemptId.current = null;
135
158
  onConnectRef.current({ address, connectorId });
136
159
  })
137
160
  .catch((error) => {
138
- if (attemptCounter.current !== attemptId) return;
161
+ if (attemptCounter.current !== attemptId) {
162
+ return;
163
+ }
139
164
  pendingConnectorId.current = null;
165
+ pendingAttemptId.current = null;
140
166
  onErrorRef.current(error as Error, connectorId);
141
167
  });
142
168
  },
@@ -146,9 +172,10 @@ export const useWagmiConnect = ({
146
172
  const cancelConnect = useCallback((): void => {
147
173
  attemptCounter.current += 1;
148
174
  pendingConnectorId.current = null;
175
+ pendingAttemptId.current = null;
149
176
  onQrUriRef.current(null);
150
177
  reset();
151
- }, [reset]);
178
+ }, [isPending, reset]);
152
179
 
153
180
  return { triggerConnect, cancelConnect, isConnecting: isPending };
154
181
  };
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export { initConfig } from './core/config';
14
14
  export type {
15
15
  ConnectErrorContext,
16
16
  ConnectErrorReason,
17
+ ConnectBannerSiteKey,
17
18
  ConnectModalProps,
18
19
  ConnectKitConfig,
19
20
  ConnectKitTheme,
@@ -34,10 +34,24 @@ export const AllWallets: FC<AllWalletsProps> = ({ wallets, isLoading, onSelectWa
34
34
  return (
35
35
  <div className="ck-all-wallets">
36
36
  <div className="ck-all-wallets-controls">
37
- <SearchBar value={search} onChange={setSearch} onClear={() => setSearch('')} />
37
+ <SearchBar
38
+ value={search}
39
+ onChange={(value) => {
40
+ setSearch(value);
41
+ }}
42
+ onClear={() => {
43
+ setSearch('');
44
+ }}
45
+ />
38
46
  <div className="ck-wc-filter-btn">
39
47
  <img className="ck-wc-filter-logo" src={wcLogo} alt="" aria-hidden="true" />
40
- <Toggle checked={filterInstalled} label="" onChange={setFilterInstalled} />
48
+ <Toggle
49
+ checked={filterInstalled}
50
+ label=""
51
+ onChange={(checked) => {
52
+ setFilterInstalled(checked);
53
+ }}
54
+ />
41
55
  </div>
42
56
  </div>
43
57
  {!isLoading && filtered.length === 0 ? (
@@ -0,0 +1,21 @@
1
+ import { type FC, type SVGProps } from 'react';
2
+
3
+ export const WarningIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
4
+ return (
5
+ <svg
6
+ width={20}
7
+ height={18}
8
+ viewBox="0 0 20 18"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ aria-hidden="true"
12
+ focusable="false"
13
+ {...props}
14
+ >
15
+ <path
16
+ d="M9.05688 0.833252C9.47596 0.107398 10.5234 0.107541 10.9426 0.833252L19.6663 15.9417C20.0852 16.6675 19.561 17.5744 18.7229 17.5745H1.27759C0.43938 17.5745 -0.0848319 16.6676 0.334229 15.9417L9.05688 0.833252ZM9.26685 13.0491V14.5149H10.7327V13.0491H9.26685ZM9.26685 6.65747V12.1018H10.7327V6.65747H9.26685Z"
17
+ fill="currentColor"
18
+ />
19
+ </svg>
20
+ );
21
+ };
@@ -1,32 +1,56 @@
1
1
  import { type FC } from 'react';
2
- import './style.scss';
3
- import graphic from '../../../../assets/graphic.png';
4
2
  import { LinkIcon } from '../asset/linkIcon';
3
+ import placeholderImage from './assets/banner-placeholder.png';
4
+ import './style.scss';
5
+
6
+ interface IdleProps {
7
+ title?: string;
8
+ description?: string;
9
+ linkText?: string;
10
+ linkUrl?: string;
11
+ imageUrl?: string;
12
+ titleColor?: string;
13
+ descriptionColor?: string;
14
+ linkTextColor?: string;
15
+ }
5
16
 
6
- export const Idle: FC = () => {
17
+ export const Idle: FC<IdleProps> = ({
18
+ title = 'PenguinWallet',
19
+ description = `Your non-custodial gateway to the Creditcoin network.
20
+ Hold tokens, swap, and play ecosystem games in one app.`,
21
+ linkText = 'Download',
22
+ // TODO: Change to Penguin Wallet
23
+ linkUrl = 'https://creditcoin.org/Credit-Wallet',
24
+ imageUrl = placeholderImage,
25
+ titleColor = '#FFFFFF',
26
+ descriptionColor = '#888A8C',
27
+ linkTextColor = '#3374FF',
28
+ }) => {
7
29
  return (
8
30
  <div className="ck-idle">
9
- <figure className="ck-idle-graphic" aria-hidden="true">
10
- <img src={graphic} alt="" />
11
- </figure>
12
- <div className="ck-idle-text">
13
- <h3 className="ck-idle-title">Swap anytime, anywhere</h3>
14
- <p className="ck-idle-desc">
15
- Swap, earn, and build on the leading decentralized
16
- <br />
17
- crypto trading protocol.
31
+ <img className="ck-idle-bg" src={imageUrl} alt="" />
32
+ {title && (
33
+ <h3 className="ck-idle-title" style={{ color: titleColor }}>
34
+ {title}
35
+ </h3>
36
+ )}
37
+ {description && (
38
+ <p className="ck-idle-desc" style={{ color: descriptionColor }}>
39
+ {description}
18
40
  </p>
41
+ )}
42
+ {linkText && linkUrl && (
19
43
  <a
20
44
  className="ck-idle-link"
21
- href="#"
45
+ href={linkUrl}
22
46
  target="_blank"
23
47
  rel="noopener noreferrer"
24
- aria-label="Learn more about Credit Connect"
48
+ style={{ color: linkTextColor }}
25
49
  >
26
- <span>Learn more</span>
50
+ <span>{linkText}</span>
27
51
  <LinkIcon />
28
52
  </a>
29
- </div>
53
+ )}
30
54
  </div>
31
55
  );
32
56
  };
@@ -1,61 +1,52 @@
1
1
  @use '../../../style/mixin.scss' as *;
2
+ .ck-root {
3
+ .ck-idle {
4
+ display: flex;
5
+ flex-direction: column;
6
+ align-items: flex-start;
7
+ gap: 6px;
8
+ position: absolute;
9
+ inset: 0;
10
+ z-index: 0;
11
+ padding: 60px;
12
+ box-sizing: border-box;
13
+ }
2
14
 
3
- .ck-idle {
4
- display: flex;
5
- flex-direction: column;
6
- align-items: center;
7
- justify-content: center;
8
- gap: 20px;
9
- height: 100%;
10
- padding: 0px 40px;
11
- margin-top: -30px;
12
- }
13
-
14
- .ck-idle-graphic {
15
- max-width: 440px;
16
- height: auto;
17
- max-height: 240px;
18
- flex-shrink: 0;
19
- margin: 0;
20
-
21
- img {
15
+ .ck-idle-bg {
16
+ position: absolute;
17
+ inset: 0;
22
18
  width: 100%;
23
19
  height: 100%;
24
- object-fit: contain;
20
+ object-fit: cover;
21
+ z-index: -1;
25
22
  }
26
- }
27
-
28
- .ck-idle-text {
29
- display: flex;
30
- flex-direction: column;
31
- align-items: center;
32
- gap: 6px;
33
- width: 100%;
34
- }
35
-
36
- .ck-idle-title {
37
- @include textToken('title1-bold');
38
- color: var(--scale-gray-900);
39
- text-align: center;
40
- width: 100%;
41
- letter-spacing: -0.96px;
42
- }
43
23
 
44
- .ck-idle-desc {
45
- @include textToken('body4-l1-regular');
46
- color: var(--scale-gray-600);
47
- text-align: center;
48
- width: 100%;
49
- }
24
+ .ck-idle-title {
25
+ @include textToken('title1-bold');
26
+ color: var(--scale-gray-900);
27
+ }
50
28
 
51
- .ck-root .ck-idle-link {
52
- display: inline-flex;
53
- align-items: center;
54
- color: var(--scale-gray-600);
55
- span {
29
+ .ck-idle-desc {
30
+ color: var(--scale-gray-600);
31
+ margin-top: 6px;
32
+ white-space: pre-line;
56
33
  @include textToken('body4-l1-regular');
57
34
  }
58
- svg {
59
- flex-shrink: 0;
35
+
36
+ .ck-idle-link {
37
+ display: flex;
38
+ align-items: center;
39
+ margin: -2px -8px -8px -8px;
40
+ padding: 8px;
41
+
42
+ span {
43
+ @include textToken('body4-l1-bold');
44
+ color: inherit;
45
+ }
46
+
47
+ svg {
48
+ flex-shrink: 0;
49
+ color: inherit;
50
+ }
60
51
  }
61
- }
52
+ }