@gluwa/connect-kit 0.1.0-next.0

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,451 @@
1
+ import { createConnector } from '@wagmi/core';
2
+ import type { Address, EIP1193Provider, Hex } from 'viem';
3
+ import { getAddress } from 'viem';
4
+ import { DappSessionManager, type DappSession } from '@gluwa/credit-connect-sdk/dapp';
5
+ import type { StorageProvider } from '@gluwa/credit-connect-sdk/storage';
6
+ import type { E2EEProvider } from '@gluwa/credit-connect-sdk/e2ee';
7
+ import type { HubFactoryProvider } from '@gluwa/credit-connect-sdk/hub';
8
+
9
+ export type CreditConnectConnectorOptions = {
10
+ projectKey: string;
11
+ serverUrl: string;
12
+ storage: StorageProvider;
13
+ e2ee: E2EEProvider;
14
+ createHub: HubFactoryProvider['createHubFromDapp'];
15
+ onDisplayUri?: (uri: string | null) => void;
16
+ };
17
+
18
+ type CreditConnectProvider = EIP1193Provider;
19
+ type ProviderListener = (...args: unknown[]) => void;
20
+
21
+ const CONNECTOR_ID = 'credit-connect';
22
+ const CONNECTOR_NAME = 'Credit Wallet';
23
+ const CONNECTOR_TYPE = 'credit-connect';
24
+
25
+ export const creditConnectConnector = (
26
+ options: CreditConnectConnectorOptions,
27
+ ): ReturnType<typeof createConnector> => {
28
+ let manager: DappSessionManager | null = null;
29
+ let selectedChainId: number | null = null;
30
+ let connectPromise: Promise<boolean> | null = null;
31
+ let provider: CreditConnectProvider | null = null;
32
+ let isLocalDisconnect = false;
33
+
34
+ let emitWagmiMessage: ((type: string, data: unknown) => void) | null = null;
35
+ let emitWagmiDisconnect: (() => void) | null = null;
36
+
37
+ const providerListeners = new Map<string, Set<ProviderListener>>();
38
+
39
+ const emitProviderEvent = (event: string, ...args: unknown[]): void => {
40
+ const listeners = providerListeners.get(event);
41
+ if (listeners) for (const fn of listeners) fn(...args);
42
+ };
43
+
44
+ const getCurrentSession = (): DappSession | null => {
45
+ if (!manager || manager.state.status !== 'CONNECTED' || !manager.state.session) return null;
46
+ return manager.state.session;
47
+ };
48
+
49
+ const getEvmAddressInfo = (session: DappSession): DappSession['addressInfoList'] =>
50
+ session.addressInfoList.filter((info) => info.networkType === 'eip155');
51
+
52
+ const getChecksummedAccounts = (session: DappSession): Address[] =>
53
+ getEvmAddressInfo(session).map((info) => getAddress(info.address));
54
+
55
+ const resetState = (): void => {
56
+ connectPromise = null;
57
+ selectedChainId = null;
58
+ };
59
+
60
+ const createManagerIfNeeded = (): DappSessionManager => {
61
+ if (manager) return manager;
62
+
63
+ manager = new DappSessionManager({
64
+ projectKey: options.projectKey,
65
+ serverUrl: options.serverUrl,
66
+ storage: options.storage,
67
+ e2ee: options.e2ee,
68
+ createHub: options.createHub,
69
+ enableReconnect: true,
70
+ autoInit: true,
71
+ });
72
+
73
+ manager.on('changeState', (state) => {
74
+ if (state.status === 'CONNECTING' && state.connectingPayload) {
75
+ options.onDisplayUri?.(state.connectingPayload);
76
+ emitWagmiMessage?.('display_uri', state.connectingPayload);
77
+ }
78
+
79
+ if (state.status === 'DISCONNECTED' && !isLocalDisconnect) {
80
+ resetState();
81
+ emitProviderEvent('disconnect');
82
+ emitWagmiDisconnect?.();
83
+ }
84
+ });
85
+
86
+ return manager;
87
+ };
88
+
89
+ const ensureManagerInitialized = async (): Promise<DappSessionManager> => {
90
+ const m = createManagerIfNeeded();
91
+
92
+ if (m.state.status === 'PENDING') {
93
+ await m.initSession();
94
+ return m;
95
+ }
96
+
97
+ if (m.state.status === 'INITIALIZING') {
98
+ await new Promise<void>((resolve) => {
99
+ const unsub = m.on('changeState', (state) => {
100
+ if (state.status !== 'INITIALIZING') {
101
+ unsub();
102
+ resolve();
103
+ }
104
+ });
105
+ });
106
+ }
107
+
108
+ return m;
109
+ };
110
+
111
+ return createConnector((config) => {
112
+ emitWagmiMessage = (type: string, data: unknown): void => {
113
+ config.emitter.emit('message', { type, data });
114
+ };
115
+ emitWagmiDisconnect = (): void => {
116
+ config.emitter.emit('disconnect');
117
+ };
118
+
119
+ return {
120
+ id: CONNECTOR_ID,
121
+ name: CONNECTOR_NAME,
122
+ type: CONNECTOR_TYPE,
123
+
124
+ async setup() {
125
+ await ensureManagerInitialized();
126
+ },
127
+
128
+ async connect<withCapabilities extends boolean = false>(parameters?: {
129
+ chainId?: number;
130
+ isReconnecting?: boolean;
131
+ withCapabilities?: withCapabilities | boolean;
132
+ }): Promise<{
133
+ accounts: withCapabilities extends true
134
+ ? ReadonlyArray<{ address: Address; capabilities: Record<string, unknown> }>
135
+ : readonly Address[];
136
+ chainId: number;
137
+ }> {
138
+ try {
139
+ const m = createManagerIfNeeded();
140
+
141
+ if (m.state.status === 'INITIALIZING') {
142
+ await new Promise<void>((resolve) => {
143
+ const unsub = m.on('changeState', (state) => {
144
+ if (state.status !== 'INITIALIZING') {
145
+ unsub();
146
+ resolve();
147
+ }
148
+ });
149
+ });
150
+ }
151
+
152
+ if (parameters?.isReconnecting) {
153
+ const session = getCurrentSession();
154
+ if (!session) throw new Error('No active session to reconnect');
155
+ const accounts = getChecksummedAccounts(session);
156
+ if (accounts.length === 0) throw new Error('No EVM address found');
157
+ const chainId = Number.parseInt(getEvmAddressInfo(session)[0].networkIdentifier, 10);
158
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
159
+ selectedChainId = chainId;
160
+ return { accounts, chainId } as any;
161
+ }
162
+
163
+ if (m.state.status === 'CONNECTING' && m.state.connectingPayload) {
164
+ options.onDisplayUri?.(m.state.connectingPayload);
165
+
166
+ if (!connectPromise) {
167
+ connectPromise = new Promise<boolean>((resolve) => {
168
+ const unsub = m.on('changeState', (state) => {
169
+ if (state.status === 'CONNECTED') {
170
+ unsub();
171
+ resolve(true);
172
+ } else if (state.status === 'DISCONNECTED') {
173
+ unsub();
174
+ resolve(false);
175
+ }
176
+ });
177
+ });
178
+ }
179
+ }
180
+
181
+ if (m.state.status !== 'CONNECTED') {
182
+ if (!connectPromise) {
183
+ if (m.state.status !== 'DISCONNECTED') {
184
+ throw new Error(`Cannot connect from ${m.state.status} state`);
185
+ }
186
+ connectPromise = m.connectNewSession();
187
+ }
188
+
189
+ const connected = await connectPromise;
190
+ connectPromise = null;
191
+
192
+ if (!connected) throw new Error('Credit Connect session failed');
193
+ }
194
+
195
+ const session = getCurrentSession();
196
+ if (!session) throw new Error('Credit Connect session failed');
197
+
198
+ const accounts = getChecksummedAccounts(session);
199
+ if (accounts.length === 0) throw new Error('No EVM address found');
200
+
201
+ const chainId = Number.parseInt(getEvmAddressInfo(session)[0].networkIdentifier, 10);
202
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
203
+
204
+ selectedChainId = chainId;
205
+ return { accounts, chainId } as any;
206
+ } catch (error) {
207
+ resetState();
208
+ throw error;
209
+ }
210
+ },
211
+
212
+ async switchChain({ chainId }) {
213
+ const session = getCurrentSession();
214
+ if (!session) throw new Error('No active session');
215
+
216
+ const supported = getEvmAddressInfo(session).some(
217
+ (info) => Number.parseInt(info.networkIdentifier, 10) === chainId,
218
+ );
219
+
220
+ if (!supported) {
221
+ const err = new Error(`Unsupported chain: ${chainId}`) as Error & { code?: number };
222
+ err.code = 4902;
223
+ throw err;
224
+ }
225
+
226
+ selectedChainId = chainId;
227
+ config.emitter.emit('change', { chainId });
228
+
229
+ const chain = config.chains.find((c) => c.id === chainId);
230
+ if (!chain) throw new Error(`Chain ${chainId} not in config`);
231
+ return chain;
232
+ },
233
+
234
+ async isAuthorized() {
235
+ await ensureManagerInitialized();
236
+ const session = getCurrentSession();
237
+ if (!session) return false;
238
+ return session.addressInfoList.some((info) => info.networkType === 'eip155');
239
+ },
240
+
241
+ async disconnect() {
242
+ isLocalDisconnect = true;
243
+ try {
244
+ if (manager?.state.status === 'CONNECTED') {
245
+ await manager.disconnectSession();
246
+ }
247
+ } finally {
248
+ isLocalDisconnect = false;
249
+ }
250
+ resetState();
251
+ config.emitter.emit('disconnect');
252
+ },
253
+
254
+ async getAccounts() {
255
+ await ensureManagerInitialized();
256
+ const session = getCurrentSession();
257
+ if (!session) return [];
258
+ return getChecksummedAccounts(session);
259
+ },
260
+
261
+ async getChainId() {
262
+ await ensureManagerInitialized();
263
+ const session = getCurrentSession();
264
+ if (!session) throw new Error('No active session');
265
+ if (selectedChainId !== null) return selectedChainId;
266
+
267
+ const evmAddressInfo = getEvmAddressInfo(session);
268
+ if (evmAddressInfo.length === 0) throw new Error('No EVM address found');
269
+
270
+ const chainId = Number.parseInt(evmAddressInfo[0].networkIdentifier, 10);
271
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
272
+ selectedChainId = chainId;
273
+ return chainId;
274
+ },
275
+
276
+ async getProvider() {
277
+ if (provider) return provider;
278
+ await ensureManagerInitialized();
279
+
280
+ provider = {
281
+ request: async (args: unknown) => {
282
+ const { method, params } = args as { method: string; params?: unknown[] };
283
+ const session = getCurrentSession();
284
+
285
+ const getSelectedAddressInfo = (address?: string) => {
286
+ if (!session) throw new Error('No active session');
287
+ const evmInfo = getEvmAddressInfo(session);
288
+ if (evmInfo.length === 0) throw new Error('No EVM address found');
289
+ if (!address) return evmInfo[0];
290
+ const found = evmInfo.find(
291
+ (info) => info.address.toLowerCase() === address.toLowerCase(),
292
+ );
293
+ if (!found) throw new Error(`Address not in session: ${address}`);
294
+ return found;
295
+ };
296
+
297
+ if (method === 'eth_accounts') {
298
+ if (!session) return [];
299
+ return getChecksummedAccounts(session);
300
+ }
301
+
302
+ if (method === 'eth_chainId') {
303
+ if (!session) throw new Error('No active session');
304
+ if (selectedChainId !== null) return `0x${selectedChainId.toString(16)}`;
305
+ const evmInfo = getEvmAddressInfo(session);
306
+ if (evmInfo.length === 0) throw new Error('No EVM address found');
307
+ const chainId = Number.parseInt(evmInfo[0].networkIdentifier, 10);
308
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
309
+ selectedChainId = chainId;
310
+ return `0x${chainId.toString(16)}`;
311
+ }
312
+
313
+ if (method === 'wallet_switchEthereumChain') {
314
+ const [{ chainId: rawChainId }] = (params ?? []) as [{ chainId?: string }];
315
+ if (!rawChainId) throw new Error('wallet_switchEthereumChain missing chainId param');
316
+ const chainId = Number.parseInt(rawChainId, 16);
317
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${rawChainId}`);
318
+ if (!session) throw new Error('No active session');
319
+
320
+ const supported = getEvmAddressInfo(session).some(
321
+ (info) => Number.parseInt(info.networkIdentifier, 10) === chainId,
322
+ );
323
+ if (!supported) {
324
+ const err = new Error(`Unsupported chain: ${chainId}`) as Error & { code?: number };
325
+ err.code = 4902;
326
+ throw err;
327
+ }
328
+ selectedChainId = chainId;
329
+ config.emitter.emit('change', { chainId });
330
+ return null;
331
+ }
332
+
333
+ if (method === 'personal_sign') {
334
+ const [messageParam, addressParam] = (params ?? []) as [unknown, unknown];
335
+ const message =
336
+ typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
337
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
338
+ const addressInfo = getSelectedAddressInfo(address);
339
+ const response = await session?.jsonRpc.request('signMessage', {
340
+ message,
341
+ addressInfo,
342
+ });
343
+ return response?.signature;
344
+ }
345
+
346
+ if (method === 'eth_sign') {
347
+ const [addressParam, messageParam] = (params ?? []) as [unknown, unknown];
348
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
349
+ const message =
350
+ typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
351
+ const addressInfo = getSelectedAddressInfo(address);
352
+ const response = await session?.jsonRpc.request('signMessage', {
353
+ message,
354
+ addressInfo,
355
+ });
356
+ return response?.signature;
357
+ }
358
+
359
+ if (method === 'eth_signTypedData' || method === 'eth_signTypedData_v4') {
360
+ const [addressParam, typedDataParam] = (params ?? []) as [unknown, unknown];
361
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
362
+ const typedData =
363
+ typeof typedDataParam === 'string'
364
+ ? typedDataParam
365
+ : JSON.stringify(typedDataParam ?? '');
366
+ const addressInfo = getSelectedAddressInfo(address);
367
+ const response = await session?.jsonRpc.request('signTypedData', {
368
+ typedData,
369
+ addressInfo,
370
+ });
371
+ return response?.signature;
372
+ }
373
+
374
+ if (method === 'eth_sendTransaction') {
375
+ const [txParams] = (params ?? []) as [Record<string, string | undefined>];
376
+ const addressInfo = getSelectedAddressInfo(txParams?.from);
377
+ const response = await session?.jsonRpc.request('sendTransaction', {
378
+ addressInfo,
379
+ to: txParams?.to,
380
+ value: txParams?.value,
381
+ data: txParams?.data,
382
+ gas: txParams?.gas,
383
+ gasPrice: txParams?.gasPrice,
384
+ maxFeePerGas: txParams?.maxFeePerGas,
385
+ maxPriorityFeePerGas: txParams?.maxPriorityFeePerGas,
386
+ nonce: txParams?.nonce,
387
+ chainId: txParams?.chainId,
388
+ });
389
+ return response?.txHash;
390
+ }
391
+
392
+ if (method === 'credit_connect_send_message') {
393
+ if (!session) throw new Error('No active session');
394
+ const [messageParam] = (params ?? []) as [unknown];
395
+ const message =
396
+ typeof messageParam === 'string'
397
+ ? messageParam
398
+ : JSON.stringify(messageParam ?? '');
399
+ await session.msg.sendMessage(message);
400
+ return true;
401
+ }
402
+
403
+ if (method === 'credit_connect_jsonrpc_ping') {
404
+ if (!session) throw new Error('No active session');
405
+ return await session.jsonRpc.request('pingPong', {});
406
+ }
407
+
408
+ throw new Error(`Unsupported provider method: ${method}`);
409
+ },
410
+
411
+ on: (event: string, listener: ProviderListener) => {
412
+ if (!providerListeners.has(event)) providerListeners.set(event, new Set());
413
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
414
+ providerListeners.get(event)!.add(listener);
415
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
416
+ return provider!;
417
+ },
418
+
419
+ removeListener: (event: string, listener: ProviderListener) => {
420
+ providerListeners.get(event)?.delete(listener);
421
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
422
+ return provider!;
423
+ },
424
+ } as unknown as CreditConnectProvider;
425
+
426
+ return provider;
427
+ },
428
+
429
+ onAccountsChanged(accounts) {
430
+ if (!accounts || accounts.length === 0) {
431
+ config.emitter.emit('disconnect');
432
+ return;
433
+ }
434
+ config.emitter.emit('change', { accounts: accounts.map(getAddress) });
435
+ },
436
+
437
+ onChainChanged(chainId) {
438
+ const normalized =
439
+ typeof chainId === 'string' ? Number.parseInt(chainId as Hex, 16) : Number(chainId);
440
+ if (!Number.isFinite(normalized)) return;
441
+ selectedChainId = normalized;
442
+ config.emitter.emit('change', { chainId: normalized });
443
+ },
444
+
445
+ onDisconnect() {
446
+ resetState();
447
+ config.emitter.emit('disconnect');
448
+ },
449
+ };
450
+ });
451
+ };
@@ -0,0 +1,125 @@
1
+ import { useEffect, useRef, useCallback } from 'react';
2
+ import { useConnect, useAccount, useConfig } from 'wagmi';
3
+ import type { ConnectorId, ConnectResult } from '../types';
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',
10
+ };
11
+
12
+ interface Options {
13
+ onConnect: (result: ConnectResult) => void;
14
+ onError: (error: Error, connectorId: ConnectorId) => void;
15
+ onQrUri: (uri: string | null) => void;
16
+ }
17
+
18
+ interface UseWagmiConnectResult {
19
+ triggerConnect: (connectorId: ConnectorId) => void;
20
+ cancelConnect: () => void;
21
+ isConnecting: boolean;
22
+ }
23
+
24
+ export const useWagmiConnect = ({
25
+ onConnect,
26
+ onError,
27
+ onQrUri,
28
+ }: Options): UseWagmiConnectResult => {
29
+ const config = useConfig();
30
+ const { address, status } = useAccount();
31
+ const pendingConnectorId = useRef<ConnectorId | null>(null);
32
+
33
+ // 콜백 참조
34
+ const onConnectRef = useRef(onConnect);
35
+ const onErrorRef = useRef(onError);
36
+ const onQrUriRef = useRef(onQrUri);
37
+ useEffect(() => {
38
+ onConnectRef.current = onConnect;
39
+ }, [onConnect]);
40
+ useEffect(() => {
41
+ onErrorRef.current = onError;
42
+ }, [onError]);
43
+ useEffect(() => {
44
+ onQrUriRef.current = onQrUri;
45
+ }, [onQrUri]);
46
+
47
+ // 연결 시도
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
+ });
59
+
60
+ // QR 코드 표시
61
+ useEffect(() => {
62
+ const handler = (data: unknown): void => {
63
+ const msg = data as { type?: string; data?: unknown };
64
+ if (msg?.type === 'display_uri' && typeof msg.data === 'string') {
65
+ onQrUriRef.current(msg.data);
66
+ }
67
+ };
68
+
69
+ const unsubscribers = config.connectors.map((connector) => {
70
+ connector.emitter.on('message', handler);
71
+ return (): void => {
72
+ connector.emitter.off('message', handler);
73
+ };
74
+ });
75
+
76
+ return (): void => {
77
+ unsubscribers.forEach((fn) => {
78
+ fn();
79
+ });
80
+ };
81
+ }, [config.connectors]);
82
+
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
+ // 연결 트리거
93
+ const triggerConnect = useCallback(
94
+ (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);
102
+ if (!connector) {
103
+ onErrorRef.current(
104
+ new Error(`Wagmi connector '${wagmiId}' not found in config`),
105
+ connectorId,
106
+ );
107
+ return;
108
+ }
109
+
110
+ onQrUriRef.current(null);
111
+ pendingConnectorId.current = connectorId;
112
+ connect({ connector });
113
+ },
114
+ [config.connectors, connect],
115
+ );
116
+
117
+ // 연결 취소
118
+ const cancelConnect = useCallback((): void => {
119
+ pendingConnectorId.current = null;
120
+ onQrUriRef.current(null);
121
+ reset();
122
+ }, [reset]);
123
+
124
+ return { triggerConnect, cancelConnect, isConnecting: isPending };
125
+ };
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { ConnectModal } from './ConnectModal';
2
+ export { creditConnectConnector } from './creditConnectConnector';
3
+ export type { CreditConnectConnectorOptions } from './creditConnectConnector';
4
+ export type { ConnectModalProps, ConnectResult, ConnectorId, WCWallet, WCSubView } from './types';
package/src/types.ts ADDED
@@ -0,0 +1,34 @@
1
+ export type ConnectorId = 'CREDIT_WALLET' | 'CREDIT_CONNECT' | 'METAMASK' | 'WALLET_CONNECT';
2
+
3
+ export type ValidConnectors =
4
+ | Array<Exclude<ConnectorId, 'WALLET_CONNECT'>>
5
+ | Array<Exclude<ConnectorId, 'CREDIT_WALLET'>>;
6
+
7
+ export interface WCWallet {
8
+ id: string;
9
+ name: string;
10
+ imageUrl: string;
11
+ mobileNative: string;
12
+ mobileUniversal: string;
13
+ desktopNative: string;
14
+ desktopUniversal: string;
15
+ appIos: string;
16
+ appAndroid: string;
17
+ homepage: string;
18
+ }
19
+
20
+ export type WCSubView = 'qr' | 'list' | 'wallet';
21
+
22
+ export interface ConnectResult {
23
+ address: `0x${string}`;
24
+ connectorId: ConnectorId;
25
+ }
26
+
27
+ export interface ConnectModalProps {
28
+ open: boolean;
29
+ connectors: ValidConnectors;
30
+ onConnect: (result: ConnectResult) => void;
31
+ onClose: () => void;
32
+ onLog?: (message: string, error?: Error) => void;
33
+ wcProjectId?: string;
34
+ }
@@ -0,0 +1,16 @@
1
+ // 모바일 기기 감지
2
+ export const isMobileDevice = (): boolean => {
3
+ if (typeof window === 'undefined') return false;
4
+ return /android|iphone|ipad|ipod/i.test(navigator.userAgent);
5
+ };
6
+
7
+ // 딥링크 열기 (walletConnect가 지원하는 지갑앱)
8
+ export const tryOpenDeepLink = (uri: string, deepLinkBase: string): void => {
9
+ const sep = deepLinkBase.endsWith('/') ? '' : '/';
10
+ window.location.href = `${deepLinkBase}${sep}wc?uri=${encodeURIComponent(uri)}`;
11
+ };
12
+
13
+ // metaMask extension 감지
14
+ export const detectMetaMaskExtension = (): boolean =>
15
+ typeof window !== 'undefined' &&
16
+ Boolean((window as Window & { ethereum?: { isMetaMask?: boolean } }).ethereum?.isMetaMask);
@@ -0,0 +1,69 @@
1
+ import { type FC, useEffect } from 'react';
2
+ import { QRCodeSVG } from 'qrcode.react';
3
+ import { isMobileDevice, tryOpenDeepLink } from '../utils/platform';
4
+ import { CONNECTOR_META } from '../connector-meta';
5
+ import { type ConnectorId } from '../types';
6
+
7
+ interface CreditWalletViewProps {
8
+ connectorId: Extract<ConnectorId, 'CREDIT_CONNECT' | 'CREDIT_WALLET'>;
9
+ qrUri: string | null;
10
+ }
11
+
12
+ export const CreditWalletView: FC<CreditWalletViewProps> = ({ connectorId, qrUri }) => {
13
+ const { downloadUrl, logoUrl, deepLinkBase } = CONNECTOR_META[connectorId];
14
+ const isMobile = isMobileDevice();
15
+
16
+ useEffect(() => {
17
+ if (isMobile && qrUri && deepLinkBase) {
18
+ tryOpenDeepLink(qrUri, deepLinkBase);
19
+ }
20
+ }, [isMobile, qrUri, deepLinkBase]);
21
+
22
+ if (isMobile && deepLinkBase) {
23
+ return (
24
+ <div className="ck-view ck-view--mobile-redirect">
25
+ <p>Opening Credit Wallet...</p>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ return (
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>
46
+
47
+ <p className="ck-view__caption">Open the Credit Wallet app and scan the QR</p>
48
+
49
+ {downloadUrl && (
50
+ <div className="ck-download-card">
51
+ <div className="ck-download-card__text">
52
+ <p className="ck-download-card__title">Get Credit Wallet app</p>
53
+ <p className="ck-download-card__sub">
54
+ Once connected, you can log in with it next time
55
+ </p>
56
+ </div>
57
+ <a
58
+ href={downloadUrl}
59
+ target="_blank"
60
+ rel="nofollow noopener noreferrer"
61
+ className="ck-btn-primary"
62
+ >
63
+ Download
64
+ </a>
65
+ </div>
66
+ )}
67
+ </div>
68
+ );
69
+ };