@gluwa/connect-kit 0.2.0-next.8 → 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.
- package/CHANGELOG.md +37 -0
- package/assets/wc.png +0 -0
- package/dist/{chunk-VE5LB7BN.js → chunk-CJANPJHY.js} +4 -2
- package/dist/credit-connect.js +1 -1
- package/dist/index.css +211 -156
- package/dist/index.d.ts +4 -1
- package/dist/index.js +521 -197
- package/dist/package.json +1 -1
- package/package.json +6 -6
- package/src/ConnectKitProvider.tsx +8 -1
- package/src/components/qrArea/index.tsx +28 -4
- package/src/components/qrArea/style.scss +85 -34
- package/src/components/toast/index.tsx +41 -0
- package/src/components/toast/style.scss +108 -0
- package/src/core/config.ts +23 -2
- package/src/core/deeplinkSignal.ts +25 -0
- package/src/creditConnectConnector.ts +15 -5
- package/src/events/account.ts +15 -6
- package/src/hooks/useConnectBanner.ts +79 -0
- package/src/hooks/useConnectTimeout.ts +21 -9
- package/src/hooks/useMetaMaskExtension.ts +30 -0
- package/src/hooks/useWCState.ts +14 -10
- package/src/hooks/useWagmiConnect.ts +42 -20
- package/src/index.ts +1 -0
- package/src/layout/allWallets/index.tsx +17 -3
- package/src/layout/allWallets/style.scss +2 -7
- package/src/layout/connectDialog/asset/warningIcon.tsx +21 -0
- package/src/layout/connectDialog/idle/assets/banner-placeholder.png +0 -0
- package/src/layout/connectDialog/idle/index.tsx +40 -16
- package/src/layout/connectDialog/idle/style.scss +42 -51
- package/src/layout/connectDialog/index.tsx +196 -47
- package/src/layout/connectDialog/metaMask/index.tsx +3 -2
- package/src/layout/connectDialog/qr/index.tsx +3 -1
- package/src/layout/connectDialog/style.scss +2 -49
- package/src/layout/connectDialog/timeout/index.tsx +0 -5
- package/src/layout/connectDialog/timeout/style.scss +0 -5
- package/src/layout/connectDialog/walletQR/index.tsx +3 -1
- package/src/layout/detailPanel/index.tsx +39 -6
- package/src/layout/detailPanel/style.scss +7 -1
- package/src/layout/selectorPanel/index.tsx +1 -0
- package/src/types.ts +20 -0
- package/src/utils/platform.ts +7 -4
- package/src/wallet-display-override.ts +15 -0
- package/src/components/snackbar/index.tsx +0 -25
- package/src/components/snackbar/style.scss +0 -51
|
@@ -2,6 +2,7 @@ import { type FC, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { useConfig } from 'wagmi';
|
|
3
3
|
import './style.scss';
|
|
4
4
|
import {
|
|
5
|
+
type ConnectBannerSiteKey,
|
|
5
6
|
type ConnectorId,
|
|
6
7
|
type ConnectKitTheme,
|
|
7
8
|
type WCWallet,
|
|
@@ -10,22 +11,22 @@ import {
|
|
|
10
11
|
import { type DetailContent } from '../detailPanel';
|
|
11
12
|
import { SelectorPanel } from '../selectorPanel';
|
|
12
13
|
import { DetailPanel } from '../detailPanel';
|
|
13
|
-
import {
|
|
14
|
-
import { resolveConnectorId } from '../../connector-meta';
|
|
14
|
+
import { Toast } from '../../components/toast';
|
|
15
|
+
import { CONNECTOR_META, resolveConnectorId } from '../../connector-meta';
|
|
16
|
+
import { useMetaMaskExtension } from '../../hooks/useMetaMaskExtension';
|
|
15
17
|
import { useWagmiConnect } from '../../hooks/useWagmiConnect';
|
|
16
18
|
import { useConnectTimeout } from '../../hooks/useConnectTimeout';
|
|
19
|
+
import { useConnectBanner } from '../../hooks/useConnectBanner';
|
|
17
20
|
import { useWCState } from '../../hooks/useWCState';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
isMobileDevice,
|
|
21
|
-
tryOpenDeepLink,
|
|
22
|
-
tryOpenCreditWalletDeeplink,
|
|
23
|
-
} from '../../utils/platform';
|
|
21
|
+
import { isMobileDevice, tryOpenDeepLink, tryOpenCreditWalletDeeplink } from '../../utils/platform';
|
|
22
|
+
import { hasDeeplinkOpened, markDeeplinkOpened } from '../../core/deeplinkSignal';
|
|
24
23
|
|
|
25
24
|
export type DialogStep = 'selector' | 'detail';
|
|
26
25
|
|
|
27
26
|
export interface ConnectDialogProps {
|
|
28
27
|
open: boolean;
|
|
28
|
+
siteKey: ConnectBannerSiteKey;
|
|
29
|
+
connectBannerUrl: string;
|
|
29
30
|
theme?: ConnectKitTheme;
|
|
30
31
|
connectors?: ConnectorId[];
|
|
31
32
|
selectedConnector?: ConnectorId | null;
|
|
@@ -49,11 +50,28 @@ const getContentForConnector = (id: ConnectorId): DetailContent => {
|
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
// MM 은 확장 감지 시 즉시 extension 연결, 아니면 QR. (select / retry 공통)
|
|
52
|
-
const getInitialContentForConnector = (
|
|
53
|
-
id
|
|
53
|
+
const getInitialContentForConnector = (
|
|
54
|
+
id: ConnectorId,
|
|
55
|
+
isMetaMaskExtensionInstalled: boolean,
|
|
56
|
+
): DetailContent =>
|
|
57
|
+
id === 'METAMASK' && !isMetaMaskExtensionInstalled ? 'metamask-qr' : getContentForConnector(id);
|
|
58
|
+
|
|
59
|
+
const getErrorCode = (error: unknown, depth = 0): number | undefined => {
|
|
60
|
+
if (depth > 4 || !error || typeof error !== 'object') return undefined;
|
|
61
|
+
|
|
62
|
+
const candidate = error as { code?: unknown; cause?: unknown; error?: unknown };
|
|
63
|
+
const code = Number(candidate.code);
|
|
64
|
+
if (Number.isFinite(code)) return code;
|
|
65
|
+
|
|
66
|
+
return getErrorCode(candidate.cause, depth + 1) ?? getErrorCode(candidate.error, depth + 1);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const isMetaMaskPendingRequestError = (error: unknown): boolean => getErrorCode(error) === -32002;
|
|
54
70
|
|
|
55
71
|
export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
56
72
|
open,
|
|
73
|
+
siteKey,
|
|
74
|
+
connectBannerUrl,
|
|
57
75
|
theme = 'dark',
|
|
58
76
|
connectors = ['CREDIT_CONNECT', 'METAMASK', 'WALLET_CONNECT'],
|
|
59
77
|
selectedConnector: selectedConnectorProp = null,
|
|
@@ -86,10 +104,13 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
86
104
|
* - 'wallet-qr' 개별 지갑 QR
|
|
87
105
|
*/
|
|
88
106
|
const [content, setContent] = useState<DetailContent>('idle');
|
|
89
|
-
const [
|
|
107
|
+
const [showCopyToast, setShowCopyToast] = useState(false);
|
|
108
|
+
const [showMetaMaskPendingRequestToast, setShowMetaMaskPendingRequestToast] = useState(false);
|
|
90
109
|
const [qrUri, setQrUri] = useState<string | null>(null);
|
|
91
110
|
// 외부 만료 신호 카운터 — CC(서버 push session_expired) / WC(connect reject) 수신 시 증가.
|
|
92
111
|
const [expiredSignal, setExpiredSignal] = useState(0);
|
|
112
|
+
// EIP-6963 기반 MetaMask 확장 설치 상태.
|
|
113
|
+
const isMetaMaskExtensionInstalled = useMetaMaskExtension();
|
|
93
114
|
|
|
94
115
|
// 현재 content 를 ref 로 추적 (effect deps 없이 최신값 참조용).
|
|
95
116
|
const contentRef = useRef<DetailContent>(content);
|
|
@@ -98,10 +119,15 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
98
119
|
const preTimeoutContentRef = useRef<DetailContent>('idle');
|
|
99
120
|
|
|
100
121
|
const config = useConfig();
|
|
122
|
+
const { banner: connectBanner, loading: connectBannerLoading } = useConnectBanner({
|
|
123
|
+
open,
|
|
124
|
+
siteKey,
|
|
125
|
+
url: connectBannerUrl,
|
|
126
|
+
});
|
|
101
127
|
|
|
102
128
|
// wagmi connect + display_uri 이벤트 → qrUri state.
|
|
103
129
|
// 모든 wagmi connector 의 message 이벤트를 구독하므로 CC connector 의 display_uri emit 도 자동 포착.
|
|
104
|
-
const { triggerConnect, cancelConnect } = useWagmiConnect({
|
|
130
|
+
const { triggerConnect, cancelConnect, isConnecting } = useWagmiConnect({
|
|
105
131
|
onConnect: (result) => {
|
|
106
132
|
// recent = 연결 성공 기준. wagmi 가 recentConnectorId 를 localStorage 에 저장하므로
|
|
107
133
|
// UI state 만 동기화. 다음 마운트 시 아래 effect 가 storage 에서 복원.
|
|
@@ -109,10 +135,25 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
109
135
|
onConnect?.(result);
|
|
110
136
|
},
|
|
111
137
|
onError: (err, connectorId) => {
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
// MetaMask는 pending 요청을 사전에 조회할 수 없어 새 요청의 -32002 응답으로만 판별한다.
|
|
139
|
+
if (
|
|
140
|
+
connectorId === 'METAMASK' &&
|
|
141
|
+
isMetaMaskExtensionInstalled &&
|
|
142
|
+
isMetaMaskPendingRequestError(err)
|
|
143
|
+
) {
|
|
144
|
+
setShowMetaMaskPendingRequestToast(true);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// MM/WC/CC connect reject·실패는 타이머를 기다리지 않고 즉시 실패 화면으로 전환.
|
|
148
|
+
// (CC 는 서버 push 만료와 별개로, 지갑 앱에서 거절하면 connect promise 가 reject 된다.)
|
|
149
|
+
if (
|
|
150
|
+
connectorId === 'METAMASK' ||
|
|
151
|
+
connectorId === 'WALLET_CONNECT' ||
|
|
152
|
+
connectorId === 'CREDIT_CONNECT'
|
|
153
|
+
) {
|
|
115
154
|
setExpiredSignal((n) => n + 1);
|
|
155
|
+
} else {
|
|
156
|
+
connectorId satisfies never;
|
|
116
157
|
}
|
|
117
158
|
onError?.(err, connectorId);
|
|
118
159
|
},
|
|
@@ -123,22 +164,45 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
123
164
|
|
|
124
165
|
// 연결 타임아웃 (CC=서버 push / MM=client 타이머 / WC=connect reject) → timedOut.
|
|
125
166
|
const timeoutActive = step === 'detail' && !!selectedConnector && content !== 'timeout';
|
|
126
|
-
const {
|
|
167
|
+
const {
|
|
168
|
+
timedOut,
|
|
169
|
+
reset: resetTimeout,
|
|
170
|
+
retry,
|
|
171
|
+
} = useConnectTimeout({
|
|
127
172
|
connectorId: selectedConnector,
|
|
128
173
|
active: timeoutActive,
|
|
174
|
+
isConnecting,
|
|
129
175
|
expiredSignal,
|
|
130
176
|
onRetry: () => {
|
|
131
177
|
if (!selectedConnector) return;
|
|
132
|
-
cancelConnect();
|
|
133
|
-
setQrUri(null);
|
|
134
178
|
// 타임아웃 직전 화면으로 복원 (예: WC 의 wallet-qr → 선택 지갑 QR 유지).
|
|
135
179
|
// idle/timeout 처럼 복원 불가한 값이면 커넥터 초기 화면으로.
|
|
136
180
|
const restore = preTimeoutContentRef.current;
|
|
137
181
|
const nextContent: DetailContent =
|
|
138
182
|
restore !== 'idle' && restore !== 'timeout'
|
|
139
183
|
? restore
|
|
140
|
-
: getInitialContentForConnector(selectedConnector);
|
|
184
|
+
: getInitialContentForConnector(selectedConnector, isMetaMaskExtensionInstalled);
|
|
185
|
+
|
|
186
|
+
// MetaMask 확장 pending 요청은 dApp에서 종료하거나 다시 요청할 수 없다.
|
|
187
|
+
// 실패 화면을 유지하고 기존 요청을 먼저 처리하도록 안내한다.
|
|
188
|
+
if (selectedConnector === 'METAMASK' && restore === 'metamask-ext' && isConnecting) {
|
|
189
|
+
setContent('timeout');
|
|
190
|
+
setShowMetaMaskPendingRequestToast(true);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
141
194
|
setContent(nextContent);
|
|
195
|
+
setQrUri(null);
|
|
196
|
+
|
|
197
|
+
// 재시도는 채널을 파괴하지 않는다.
|
|
198
|
+
// kit 의 타임아웃(DEFAULT_CONNECT_TIMEOUT_MS)은 '오래 걸린다'는 UI 신호일 뿐이고,
|
|
199
|
+
// 그 시점에 커넥터의 세션은 대체로 아직 유효하다. MM 의 경우 SDK 가 저장된 채널을
|
|
200
|
+
// validUntil 까지 재사용하므로(originatorSessionConnect) 같은 QR/URI 가 다시 나오는데,
|
|
201
|
+
// 채널이 살아있으니 스캔·딥링크·Open app 이 모두 정상 동작한다.
|
|
202
|
+
// 반대로 sdk.terminate() 로 채널을 지우면, 이전 시도가 미완료라 SDK 가 startConnection
|
|
203
|
+
// 을 타지 못하고 캐시된 옛 qrcodeLink 를 재방출한다 → 죽은 채널의 QR 이 그려진다.
|
|
204
|
+
// 그래서 여기서는 평범한 재시도만 한다. CC/WC 는 커넥터가 매 connect 마다 새 세션을 만든다.
|
|
205
|
+
cancelConnect();
|
|
142
206
|
triggerConnect(selectedConnector);
|
|
143
207
|
},
|
|
144
208
|
});
|
|
@@ -146,18 +210,44 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
146
210
|
// 타임아웃 발생 → 진행 중 연결 취소 + timeout 화면 표시.
|
|
147
211
|
// 전환 전 화면을 저장해 retry 시 복원 (WC wallet-qr 등).
|
|
148
212
|
useEffect(() => {
|
|
149
|
-
|
|
213
|
+
// selector로 돌아간 렌더에 이전 timedOut이 잠시 남아도 실패 화면을 다시 덮지 않는다.
|
|
214
|
+
if (!timedOut || !selectedConnector || step !== 'detail') return;
|
|
215
|
+
// cancelConnect로 isConnecting이 바뀌어 effect가 다시 실행되어도
|
|
216
|
+
// 기록한 이전 화면을 timeout으로 덮어쓰지 않는다.
|
|
217
|
+
if (contentRef.current === 'timeout') return;
|
|
150
218
|
preTimeoutContentRef.current = contentRef.current;
|
|
151
|
-
|
|
219
|
+
|
|
220
|
+
// MetaMask 확장 pending 요청만 보존한다. QR/Deeplink는 Retry에서 SDK를 재시작한다.
|
|
221
|
+
const shouldPreserveMetaMaskRequest =
|
|
222
|
+
selectedConnector === 'METAMASK' && contentRef.current === 'metamask-ext' && isConnecting;
|
|
223
|
+
// 확장 승인 요청은 dApp에서 종료할 수 없으므로 원래 요청을 유지한다.
|
|
224
|
+
if (!shouldPreserveMetaMaskRequest) {
|
|
225
|
+
cancelConnect();
|
|
226
|
+
}
|
|
152
227
|
setContent('timeout');
|
|
153
|
-
}, [timedOut, cancelConnect]);
|
|
228
|
+
}, [timedOut, selectedConnector, step, isConnecting, cancelConnect]);
|
|
229
|
+
|
|
230
|
+
// provider 응답이 늦게 도착하면 QR에서 확장 연결 화면으로 전환.
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (
|
|
233
|
+
isMetaMaskExtensionInstalled &&
|
|
234
|
+
selectedConnector === 'METAMASK' &&
|
|
235
|
+
content === 'metamask-qr'
|
|
236
|
+
) {
|
|
237
|
+
setContent('metamask-ext');
|
|
238
|
+
}
|
|
239
|
+
}, [isMetaMaskExtensionInstalled, selectedConnector, content]);
|
|
154
240
|
|
|
155
241
|
// 마운트 시 wagmi storage 의 recentConnectorId(연결 성공 시 wagmi 가 저장) 복원.
|
|
156
242
|
useEffect(() => {
|
|
157
243
|
const restoreRecent = async (): Promise<void> => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
244
|
+
try {
|
|
245
|
+
const wagmiId = await config.storage?.getItem('recentConnectorId');
|
|
246
|
+
const resolved = resolveConnectorId(wagmiId ?? undefined);
|
|
247
|
+
if (resolved) setRecentConnector(resolved);
|
|
248
|
+
} catch {
|
|
249
|
+
// 무시 — 실패해도 진행한다.
|
|
250
|
+
}
|
|
161
251
|
};
|
|
162
252
|
restoreRecent().catch(() => undefined);
|
|
163
253
|
}, [config]);
|
|
@@ -166,9 +256,16 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
166
256
|
const wc = useWCState();
|
|
167
257
|
|
|
168
258
|
const handleSelect = (id: ConnectorId) => {
|
|
259
|
+
// 다른 지갑 선택은 이전 시도와 실패 상태를 이어받지 않고 항상 새 연결로 시작한다.
|
|
260
|
+
cancelConnect();
|
|
261
|
+
resetTimeout();
|
|
262
|
+
setQrUri(null);
|
|
263
|
+
setSelectedWallet(null);
|
|
264
|
+
setShowCopyToast(false);
|
|
265
|
+
setShowMetaMaskPendingRequestToast(false);
|
|
169
266
|
setSelectedConnector(id);
|
|
170
267
|
// recent 는 클릭이 아니라 연결 성공(onConnect) 시점에 기록 — 위 useWagmiConnect onConnect 참조.
|
|
171
|
-
setContent(getInitialContentForConnector(id));
|
|
268
|
+
setContent(getInitialContentForConnector(id, isMetaMaskExtensionInstalled));
|
|
172
269
|
setStep('detail');
|
|
173
270
|
// wagmi connect 시작 (display_uri 도착 시 setQrUri 자동 호출)
|
|
174
271
|
triggerConnect(id);
|
|
@@ -218,37 +315,73 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
218
315
|
}, [open]);
|
|
219
316
|
|
|
220
317
|
// selectedWallet 가 먼저 set 됐고 qrUri 가 뒤늦게 도착한 경우 자동 deeplink.
|
|
221
|
-
//
|
|
222
|
-
|
|
318
|
+
// 자동 deeplink 는 best-effort 다 — 횟수 제한을 두지 않는다.
|
|
319
|
+
// iOS WebKit 은 사용자 제스처 없는 커스텀 스킴 이동을 억제한다. display_uri 는 네트워크
|
|
320
|
+
// 왕복 뒤에 도착하므로 지갑 탭의 제스처 활성 창(수백 ms)을 이미 넘긴 상태이고,
|
|
321
|
+
// 따라서 이 시도는 브라우저가 허용할 때만 통과한다. dApp 이 제어할 수 있는 영역이 아니다.
|
|
322
|
+
// 막힌 경우의 확실한 경로는 'Open app' 버튼(제스처) 이다.
|
|
223
323
|
useEffect(() => {
|
|
224
|
-
deeplinkTriedRef.current = false;
|
|
225
|
-
}, [selectedWallet]);
|
|
226
|
-
useEffect(() => {
|
|
227
|
-
if (deeplinkTriedRef.current) return;
|
|
228
324
|
if (!selectedWallet || !qrUri || content !== 'wallet-qr') return;
|
|
229
325
|
if (!isMobileDevice()) return;
|
|
230
326
|
const base = selectedWallet.mobileNative || selectedWallet.mobileUniversal;
|
|
231
327
|
if (!base) return;
|
|
232
|
-
deeplinkTriedRef.current = true;
|
|
233
328
|
tryOpenDeepLink(qrUri, base);
|
|
234
329
|
}, [selectedWallet, qrUri, content]);
|
|
235
330
|
|
|
236
331
|
// CC connector direct path: selector 의 'Continue with Credit Wallet' → content='qr'.
|
|
237
|
-
// wagmi MM connector 는 SDK 가 자체 deeplink 하지만 CC connector 는 emit display_uri 만 함 —
|
|
238
|
-
// kit 가 mobile redirect 책임. payload 는 full URL (`https://app.creditcoin.org/?creditconnect=...`).
|
|
239
|
-
const creditConnectDeeplinkTriedRef = useRef(false);
|
|
240
|
-
useEffect(() => {
|
|
241
|
-
creditConnectDeeplinkTriedRef.current = false;
|
|
242
|
-
}, [selectedConnector]);
|
|
243
332
|
useEffect(() => {
|
|
244
|
-
if (creditConnectDeeplinkTriedRef.current) return;
|
|
245
333
|
if (content !== 'qr' || !qrUri) return;
|
|
246
334
|
if (selectedConnector !== 'CREDIT_CONNECT') return;
|
|
247
335
|
if (!isMobileDevice()) return;
|
|
248
|
-
creditConnectDeeplinkTriedRef.current = true;
|
|
249
336
|
tryOpenCreditWalletDeeplink(qrUri);
|
|
250
337
|
}, [content, qrUri, selectedConnector]);
|
|
251
338
|
|
|
339
|
+
// MetaMask 는 기본적으로 SDK 가 앱을 연다 (core/config.ts 의 openDeeplink 콜백).
|
|
340
|
+
// 그런데 SDK 는 취소된 connect 가 남아있는 동안(initializationOngoing) 그 콜백을 호출하지 않는다.
|
|
341
|
+
// 그 래치를 dApp 이 강제로 풀 방법은 커넥터에 'rejected' 를 쏘는 것뿐인데, 그러면 SDK 가
|
|
342
|
+
// 4001 경로를 타며 handleDisconnect() 로 provider._state.isConnected 를 false 로 내리고,
|
|
343
|
+
// 이후 switchChain 등 모든 RPC 의 딥링크 리다이렉트가 write() 의 isMetaMaskInstalled()
|
|
344
|
+
// 가드에서 조용히 스킵된다. 그래서 SDK 상태는 건드리지 않는다.
|
|
345
|
+
// 대신 SDK 가 열지 않는 시도에서는 kit 이 직접 연다. 재방출되는 qrUri 는 terminate 를 하지
|
|
346
|
+
// 않는 한 살아있는 채널이므로('Open app' 이 같은 URI 로 정상 동작하는 것이 근거) 유효하다.
|
|
347
|
+
// 이중 발사는 deeplinkSignal 이 막는다 — connect 시도당 1회.
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
if (content !== 'metamask-qr' || !qrUri) return;
|
|
350
|
+
if (selectedConnector !== 'METAMASK') return;
|
|
351
|
+
if (!isMobileDevice()) return;
|
|
352
|
+
if (hasDeeplinkOpened()) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const base = CONNECTOR_META.METAMASK.deepLinkBase;
|
|
356
|
+
if (!base) return;
|
|
357
|
+
markDeeplinkOpened();
|
|
358
|
+
tryOpenDeepLink(qrUri, base);
|
|
359
|
+
}, [content, qrUri, selectedConnector]);
|
|
360
|
+
|
|
361
|
+
// 'Open app' 버튼 — 사용자 제스처이므로 iOS 의 비제스처 이동 억제에 걸리지 않는다.
|
|
362
|
+
// 자동 시도가 막혔을 때의 확실한 경로.
|
|
363
|
+
const handleOpenApp = (): void => {
|
|
364
|
+
if (!qrUri) return;
|
|
365
|
+
|
|
366
|
+
if (content === 'qr' && selectedConnector === 'CREDIT_CONNECT') {
|
|
367
|
+
tryOpenCreditWalletDeeplink(qrUri);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (content === 'metamask-qr' && selectedConnector === 'METAMASK') {
|
|
372
|
+
const base = CONNECTOR_META.METAMASK.deepLinkBase;
|
|
373
|
+
if (!base) return;
|
|
374
|
+
tryOpenDeepLink(qrUri, base);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (content === 'wallet-qr' && selectedWallet) {
|
|
379
|
+
const base = selectedWallet.mobileNative || selectedWallet.mobileUniversal;
|
|
380
|
+
if (!base) return;
|
|
381
|
+
tryOpenDeepLink(qrUri, base);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
252
385
|
// Prevent background scroll while dialog is open
|
|
253
386
|
useEffect(() => {
|
|
254
387
|
const prev = document.body.style.overflow;
|
|
@@ -266,7 +399,8 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
266
399
|
setSelectedConnector(null);
|
|
267
400
|
setContent('idle');
|
|
268
401
|
setStep('selector');
|
|
269
|
-
|
|
402
|
+
setShowCopyToast(false);
|
|
403
|
+
setShowMetaMaskPendingRequestToast(false);
|
|
270
404
|
}
|
|
271
405
|
}, [open, cancelConnect]);
|
|
272
406
|
|
|
@@ -276,7 +410,8 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
276
410
|
if (!dialog) return undefined;
|
|
277
411
|
const handleCancel = (e: Event) => {
|
|
278
412
|
e.preventDefault();
|
|
279
|
-
|
|
413
|
+
setShowCopyToast(false);
|
|
414
|
+
setShowMetaMaskPendingRequestToast(false);
|
|
280
415
|
onClose();
|
|
281
416
|
};
|
|
282
417
|
dialog.addEventListener('cancel', handleCancel);
|
|
@@ -302,6 +437,8 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
302
437
|
onClose={onClose}
|
|
303
438
|
/>
|
|
304
439
|
<DetailPanel
|
|
440
|
+
connectBanner={connectBanner}
|
|
441
|
+
connectBannerLoading={connectBannerLoading}
|
|
305
442
|
selectedConnector={selectedConnector}
|
|
306
443
|
selectedWallet={selectedWallet}
|
|
307
444
|
content={content}
|
|
@@ -310,6 +447,7 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
310
447
|
walletListLoading={wc.walletListLoading}
|
|
311
448
|
walletCount={wc.walletList.length}
|
|
312
449
|
onBack={handleBack}
|
|
450
|
+
onOpenApp={handleOpenApp}
|
|
313
451
|
onClose={onClose}
|
|
314
452
|
onRetry={retry}
|
|
315
453
|
onShowAllWallets={handleShowAllWallets}
|
|
@@ -319,16 +457,27 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
|
|
|
319
457
|
if (!qrUri) return;
|
|
320
458
|
navigator.clipboard
|
|
321
459
|
.writeText(qrUri)
|
|
322
|
-
.then(() =>
|
|
460
|
+
.then(() => {
|
|
461
|
+
setShowCopyToast(true);
|
|
462
|
+
})
|
|
323
463
|
.catch((err) => {
|
|
324
464
|
// eslint-disable-next-line no-console
|
|
325
465
|
console.warn('[copyLink] clipboard write failed', err);
|
|
326
466
|
});
|
|
327
467
|
}}
|
|
328
468
|
/>
|
|
329
|
-
{
|
|
330
|
-
<div className="ck-
|
|
331
|
-
|
|
469
|
+
{(showCopyToast || showMetaMaskPendingRequestToast) && (
|
|
470
|
+
<div className="ck-toast-stack">
|
|
471
|
+
{showCopyToast && (
|
|
472
|
+
<Toast message="Link copied" onClose={() => setShowCopyToast(false)} />
|
|
473
|
+
)}
|
|
474
|
+
{showMetaMaskPendingRequestToast && (
|
|
475
|
+
<Toast
|
|
476
|
+
variant="warning"
|
|
477
|
+
message="Clear the pending request in your wallet first."
|
|
478
|
+
onClose={() => setShowMetaMaskPendingRequestToast(false)}
|
|
479
|
+
/>
|
|
480
|
+
)}
|
|
332
481
|
</div>
|
|
333
482
|
)}
|
|
334
483
|
</dialog>
|
|
@@ -12,6 +12,7 @@ interface MetaMaskProps {
|
|
|
12
12
|
logoUrl?: string;
|
|
13
13
|
downloadUrl?: string;
|
|
14
14
|
onCopy?: () => void;
|
|
15
|
+
onOpenApp?: () => void;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export const MetaMask: FC<MetaMaskProps> = ({
|
|
@@ -19,7 +20,7 @@ export const MetaMask: FC<MetaMaskProps> = ({
|
|
|
19
20
|
uri,
|
|
20
21
|
logoUrl,
|
|
21
22
|
downloadUrl = '',
|
|
22
|
-
|
|
23
|
+
onOpenApp,
|
|
23
24
|
}) => {
|
|
24
25
|
if (variant === 'extension') {
|
|
25
26
|
return (
|
|
@@ -46,7 +47,7 @@ export const MetaMask: FC<MetaMaskProps> = ({
|
|
|
46
47
|
logoUrl={logoUrl}
|
|
47
48
|
hasBg
|
|
48
49
|
label="Scan this QR Code with your phone"
|
|
49
|
-
|
|
50
|
+
onOpenApp={onOpenApp}
|
|
50
51
|
/>
|
|
51
52
|
<ActionItem
|
|
52
53
|
as="a"
|
|
@@ -6,9 +6,10 @@ interface QRProps {
|
|
|
6
6
|
uri?: string | null;
|
|
7
7
|
logoUrl?: string;
|
|
8
8
|
downloadUrl?: string;
|
|
9
|
+
onOpenApp?: () => void;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export const QR: FC<QRProps> = ({ uri, logoUrl, downloadUrl }) => {
|
|
12
|
+
export const QR: FC<QRProps> = ({ uri, logoUrl, downloadUrl, onOpenApp }) => {
|
|
12
13
|
const status = !uri ? 'loading' : 'ready';
|
|
13
14
|
|
|
14
15
|
return (
|
|
@@ -18,6 +19,7 @@ export const QR: FC<QRProps> = ({ uri, logoUrl, downloadUrl }) => {
|
|
|
18
19
|
status={status}
|
|
19
20
|
logoUrl={logoUrl}
|
|
20
21
|
label="Open the PenguinWallet app and scan the QR"
|
|
22
|
+
onOpenApp={onOpenApp}
|
|
21
23
|
/>
|
|
22
24
|
<ActionItem
|
|
23
25
|
as="a"
|
|
@@ -37,59 +37,12 @@
|
|
|
37
37
|
width: 100%;
|
|
38
38
|
max-width: none;
|
|
39
39
|
grid-template-columns: 1fr;
|
|
40
|
+
grid-template-rows: minmax(0, 1fr);
|
|
40
41
|
max-height: 82dvh;
|
|
41
|
-
height:
|
|
42
|
+
height: auto;
|
|
42
43
|
border-radius: 12px 12px 0 0;
|
|
43
44
|
|
|
44
45
|
&[data-step='selector'] .ck-detail { display: none !important; }
|
|
45
46
|
&[data-step='detail'] .ck-selector { display: none !important; }
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
.ck-root .ck-dialog-snackbar {
|
|
50
|
-
position: fixed;
|
|
51
|
-
top: clamp(20px, 9.2vh, 99px);
|
|
52
|
-
left: 50%;
|
|
53
|
-
z-index: 1001;
|
|
54
|
-
width: min(calc(100vw - 40px), 400px);
|
|
55
|
-
transform: translateX(-50%);
|
|
56
|
-
animation: ck-dialog-snackbar-in 0.2s ease;
|
|
57
|
-
|
|
58
|
-
@include tablet {
|
|
59
|
-
top: clamp(
|
|
60
|
-
calc(env(safe-area-inset-top, 0px) + 20px),
|
|
61
|
-
calc(env(safe-area-inset-top, 0px) + 20px),
|
|
62
|
-
calc(env(safe-area-inset-top, 0px) + 20px)
|
|
63
|
-
);
|
|
64
|
-
left: 20px;
|
|
65
|
-
right: 20px;
|
|
66
|
-
width: auto;
|
|
67
|
-
transform: none;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
@keyframes ck-dialog-snackbar-in {
|
|
72
|
-
from {
|
|
73
|
-
opacity: 0;
|
|
74
|
-
transform: translateX(-50%) translateY(8px);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
to {
|
|
78
|
-
opacity: 1;
|
|
79
|
-
transform: translateX(-50%) translateY(0);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@include tablet {
|
|
84
|
-
@keyframes ck-dialog-snackbar-in {
|
|
85
|
-
from {
|
|
86
|
-
opacity: 0;
|
|
87
|
-
transform: translateY(8px);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
to {
|
|
91
|
-
opacity: 1;
|
|
92
|
-
transform: translateY(0);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
@@ -10,11 +10,6 @@ export const Timeout: FC<TimeoutProps> = ({ onRetry }) => {
|
|
|
10
10
|
<div className="ck-timeout">
|
|
11
11
|
<div className="ck-timeout-text">
|
|
12
12
|
<h3 className="ck-timeout-title">Connection Failed</h3>
|
|
13
|
-
<p className="ck-timeout-desc">
|
|
14
|
-
The connection took too long to respond.
|
|
15
|
-
<br />
|
|
16
|
-
Please check your network and try again.
|
|
17
|
-
</p>
|
|
18
13
|
</div>
|
|
19
14
|
<button type="button" className="ck-timeout-retry-btn" onClick={onRetry}>
|
|
20
15
|
Try Again
|
|
@@ -7,9 +7,10 @@ interface WalletQRProps {
|
|
|
7
7
|
wallet: WCWallet;
|
|
8
8
|
uri?: string | null;
|
|
9
9
|
onCopy?: () => void;
|
|
10
|
+
onOpenApp?: () => void;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export const WalletQR: FC<WalletQRProps> = ({ wallet, uri = null, onCopy }) => {
|
|
13
|
+
export const WalletQR: FC<WalletQRProps> = ({ wallet, uri = null, onCopy, onOpenApp }) => {
|
|
13
14
|
const downloadUrl = wallet.appIos || wallet.appAndroid || wallet.homepage;
|
|
14
15
|
|
|
15
16
|
return (
|
|
@@ -21,6 +22,7 @@ export const WalletQR: FC<WalletQRProps> = ({ wallet, uri = null, onCopy }) => {
|
|
|
21
22
|
hasBg
|
|
22
23
|
label={`Scan with ${wallet.name}`}
|
|
23
24
|
onCopy={onCopy}
|
|
25
|
+
onOpenApp={onOpenApp}
|
|
24
26
|
/>
|
|
25
27
|
{downloadUrl && (
|
|
26
28
|
<ActionItem
|
|
@@ -2,7 +2,7 @@ import { type FC } from 'react';
|
|
|
2
2
|
import { CloseIcon } from '../connectDialog/asset/closeIcon';
|
|
3
3
|
import { BackIcon } from '../connectDialog/asset/backIcon';
|
|
4
4
|
import './style.scss';
|
|
5
|
-
import { type ConnectorId, type WCWallet } from '../../types';
|
|
5
|
+
import { type ConnectBanner, type ConnectorId, type WCWallet } from '../../types';
|
|
6
6
|
import { CONNECTOR_META } from '../../connector-meta';
|
|
7
7
|
import { Idle } from '../connectDialog/idle';
|
|
8
8
|
import { Loading } from '../connectDialog/loading';
|
|
@@ -25,6 +25,8 @@ export type DetailContent =
|
|
|
25
25
|
| 'wallet-qr';
|
|
26
26
|
|
|
27
27
|
interface DetailPanelProps {
|
|
28
|
+
connectBanner?: ConnectBanner | null;
|
|
29
|
+
connectBannerLoading?: boolean;
|
|
28
30
|
selectedConnector: ConnectorId | null;
|
|
29
31
|
selectedWallet?: WCWallet | null;
|
|
30
32
|
content?: DetailContent;
|
|
@@ -40,6 +42,8 @@ interface DetailPanelProps {
|
|
|
40
42
|
onShowAllWallets?: () => void;
|
|
41
43
|
onSelectWallet?: (wallet: WCWallet) => void;
|
|
42
44
|
onCopyLink?: () => void;
|
|
45
|
+
/** 모바일 QR 화면의 'Open app' 버튼 — 해당 지갑 앱을 deeplink 로 연다. */
|
|
46
|
+
onOpenApp?: () => void;
|
|
43
47
|
/** timeout 화면의 "Try Again" 버튼 핸들러. */
|
|
44
48
|
onRetry?: () => void;
|
|
45
49
|
}
|
|
@@ -68,6 +72,8 @@ const getTitle = (content: DetailContent, connector: ConnectorId | null): string
|
|
|
68
72
|
};
|
|
69
73
|
|
|
70
74
|
export const DetailPanel: FC<DetailPanelProps> = ({
|
|
75
|
+
connectBanner = null,
|
|
76
|
+
connectBannerLoading = false,
|
|
71
77
|
selectedConnector,
|
|
72
78
|
selectedWallet = null,
|
|
73
79
|
content = 'idle',
|
|
@@ -80,6 +86,7 @@ export const DetailPanel: FC<DetailPanelProps> = ({
|
|
|
80
86
|
onShowAllWallets,
|
|
81
87
|
onSelectWallet,
|
|
82
88
|
onCopyLink,
|
|
89
|
+
onOpenApp,
|
|
83
90
|
onRetry,
|
|
84
91
|
}) => {
|
|
85
92
|
// CONNECTOR_META 에서 활성 connector 의 logo / downloadUrl 가져옴.
|
|
@@ -91,6 +98,10 @@ export const DetailPanel: FC<DetailPanelProps> = ({
|
|
|
91
98
|
? `Scan with ${selectedWallet.name}`
|
|
92
99
|
: getTitle(content, selectedConnector);
|
|
93
100
|
const showBack = content !== 'idle';
|
|
101
|
+
const closeColor =
|
|
102
|
+
content === 'idle' && !connectBannerLoading
|
|
103
|
+
? (connectBanner?.closeColor ?? '#FFFFFF')
|
|
104
|
+
: undefined;
|
|
94
105
|
|
|
95
106
|
return (
|
|
96
107
|
<section className="ck-detail">
|
|
@@ -105,15 +116,37 @@ export const DetailPanel: FC<DetailPanelProps> = ({
|
|
|
105
116
|
) : (
|
|
106
117
|
<span className="ck-detail-header-spacer" aria-hidden="true" />
|
|
107
118
|
)}
|
|
108
|
-
<button
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
className="ck-btn-close"
|
|
122
|
+
onClick={onClose}
|
|
123
|
+
aria-label="Close"
|
|
124
|
+
style={{ color: closeColor }}
|
|
125
|
+
>
|
|
109
126
|
<CloseIcon />
|
|
110
127
|
</button>
|
|
111
128
|
</header>
|
|
112
129
|
<div className="ck-detail-body">
|
|
113
|
-
{content === 'idle' &&
|
|
130
|
+
{content === 'idle' && !connectBannerLoading && (
|
|
131
|
+
<Idle
|
|
132
|
+
title={connectBanner?.title}
|
|
133
|
+
description={connectBanner?.subtitle}
|
|
134
|
+
linkText={connectBanner?.linkText}
|
|
135
|
+
linkUrl={connectBanner?.linkUrl}
|
|
136
|
+
imageUrl={connectBanner?.imageUrl}
|
|
137
|
+
titleColor={connectBanner?.titleColor}
|
|
138
|
+
descriptionColor={connectBanner?.subtitleColor}
|
|
139
|
+
linkTextColor={connectBanner?.linkTextColor}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
114
142
|
{content === 'loading' && <Loading />}
|
|
115
143
|
{content === 'qr' && (
|
|
116
|
-
<QR
|
|
144
|
+
<QR
|
|
145
|
+
uri={qrUri}
|
|
146
|
+
logoUrl={connectorLogoUrl}
|
|
147
|
+
downloadUrl={connectorDownloadUrl}
|
|
148
|
+
onOpenApp={onOpenApp}
|
|
149
|
+
/>
|
|
117
150
|
)}
|
|
118
151
|
{content === 'timeout' && <Timeout onRetry={onRetry} />}
|
|
119
152
|
{(content === 'metamask-ext' || content === 'metamask-qr') && (
|
|
@@ -122,7 +155,7 @@ export const DetailPanel: FC<DetailPanelProps> = ({
|
|
|
122
155
|
logoUrl={connectorLogoUrl}
|
|
123
156
|
uri={qrUri}
|
|
124
157
|
downloadUrl={connectorDownloadUrl}
|
|
125
|
-
|
|
158
|
+
onOpenApp={onOpenApp}
|
|
126
159
|
/>
|
|
127
160
|
)}
|
|
128
161
|
{content === 'walletconnect' && (
|
|
@@ -142,7 +175,7 @@ export const DetailPanel: FC<DetailPanelProps> = ({
|
|
|
142
175
|
/>
|
|
143
176
|
)}
|
|
144
177
|
{content === 'wallet-qr' && selectedWallet && (
|
|
145
|
-
<WalletQR wallet={selectedWallet} uri={qrUri} onCopy={onCopyLink} />
|
|
178
|
+
<WalletQR wallet={selectedWallet} uri={qrUri} onCopy={onCopyLink} onOpenApp={onOpenApp} />
|
|
146
179
|
)}
|
|
147
180
|
</div>
|
|
148
181
|
</section>
|