@gluwa/connect-kit 0.2.0-next.1 → 0.2.0-next.11

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 (99) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/assets/fonts/Inter-Medium.ttf +0 -0
  3. package/assets/fonts/Inter-Medium.woff2 +0 -0
  4. package/assets/fonts/Inter-Regular.ttf +0 -0
  5. package/assets/fonts/Inter-Regular.woff2 +0 -0
  6. package/assets/fonts/Inter-SemiBold.ttf +0 -0
  7. package/assets/fonts/Inter-SemiBold.woff2 +0 -0
  8. package/assets/graphic.png +0 -0
  9. package/assets/metamask.png +0 -0
  10. package/assets/penguinwallet.png +0 -0
  11. package/assets/wc-logo.svg +3 -0
  12. package/assets/wc-search.png +0 -0
  13. package/dist/chunk-VE5LB7BN.js +405 -0
  14. package/dist/credit-connect.js +2 -409
  15. package/dist/index.css +1903 -389
  16. package/dist/index.d.ts +21 -10
  17. package/dist/index.js +1507 -922
  18. package/dist/package.json +7 -6
  19. package/package.json +8 -8
  20. package/src/ConnectKitProvider.tsx +97 -65
  21. package/src/api/asset.ts +10 -0
  22. package/src/assets.d.ts +5 -0
  23. package/src/components/actionItem/index.tsx +50 -0
  24. package/src/components/actionItem/style.scss +47 -0
  25. package/src/components/copyLink/index.tsx +16 -0
  26. package/src/components/copyLink/style.scss +16 -0
  27. package/src/components/qrArea/index.tsx +72 -0
  28. package/src/components/qrArea/style.scss +122 -0
  29. package/src/components/searchBar/asset/Search.tsx +20 -0
  30. package/src/components/searchBar/asset/XMarkCircle.tsx +24 -0
  31. package/src/components/searchBar/index.tsx +44 -0
  32. package/src/components/searchBar/style.scss +58 -0
  33. package/src/components/toast/index.tsx +41 -0
  34. package/src/components/toast/style.scss +108 -0
  35. package/src/components/toggle/index.tsx +27 -0
  36. package/src/components/toggle/style.scss +53 -0
  37. package/src/components/walletGrid/index.tsx +64 -0
  38. package/src/components/walletGrid/style.scss +108 -0
  39. package/src/components/walletItem/index.tsx +49 -0
  40. package/src/components/walletItem/style.scss +70 -0
  41. package/src/connector-meta.ts +16 -11
  42. package/src/core/config.ts +20 -1
  43. package/src/creditConnectConnector.ts +23 -25
  44. package/src/events/account.ts +30 -3
  45. package/src/hooks/useConnectBanner.ts +79 -0
  46. package/src/hooks/useConnectTimeout.ts +92 -0
  47. package/src/hooks/useMetaMaskExtension.ts +29 -0
  48. package/src/hooks/useWCState.ts +21 -5
  49. package/src/hooks/useWagmiConnect.ts +35 -9
  50. package/src/index.ts +5 -2
  51. package/src/layout/allWallets/index.tsx +52 -0
  52. package/src/layout/allWallets/style.scss +57 -0
  53. package/src/layout/connectDialog/asset/backIcon.tsx +24 -0
  54. package/src/layout/connectDialog/asset/checkIcon.tsx +28 -0
  55. package/src/layout/connectDialog/asset/closeIcon.tsx +21 -0
  56. package/src/layout/connectDialog/asset/copyIcon.tsx +28 -0
  57. package/src/layout/connectDialog/asset/linkIcon.tsx +24 -0
  58. package/src/layout/connectDialog/asset/spinner.tsx +21 -0
  59. package/src/layout/connectDialog/asset/warningIcon.tsx +21 -0
  60. package/src/layout/connectDialog/idle/assets/banner-placeholder.png +0 -0
  61. package/src/layout/connectDialog/idle/index.tsx +55 -0
  62. package/src/layout/connectDialog/idle/style.scss +52 -0
  63. package/src/layout/connectDialog/index.tsx +432 -0
  64. package/src/layout/connectDialog/loading/index.tsx +13 -0
  65. package/src/layout/connectDialog/loading/style.scss +19 -0
  66. package/src/layout/connectDialog/metaMask/index.tsx +61 -0
  67. package/src/layout/connectDialog/metaMask/style.scss +65 -0
  68. package/src/layout/connectDialog/qr/index.tsx +33 -0
  69. package/src/layout/connectDialog/style.scss +48 -0
  70. package/src/layout/connectDialog/timeout/index.tsx +31 -0
  71. package/src/layout/connectDialog/timeout/style.scss +64 -0
  72. package/src/layout/connectDialog/walletConnect/index.tsx +45 -0
  73. package/src/layout/connectDialog/walletConnect/style.scss +48 -0
  74. package/src/layout/connectDialog/walletQR/index.tsx +37 -0
  75. package/src/layout/detailPanel/index.tsx +175 -0
  76. package/src/layout/detailPanel/style.scss +70 -0
  77. package/src/layout/selectorPanel/index.tsx +100 -0
  78. package/src/layout/selectorPanel/style.scss +113 -0
  79. package/src/style/color.scss +417 -0
  80. package/src/style/font.scss +28 -0
  81. package/src/style/index.scss +5 -0
  82. package/src/style/mixin.scss +217 -0
  83. package/src/style/reset.scss +110 -0
  84. package/src/style/variables.scss +13 -0
  85. package/src/types.ts +32 -8
  86. package/src/utils/platform.ts +5 -3
  87. package/src/wallet-display-override.ts +15 -0
  88. package/tsconfig.json +5 -1
  89. package/tsup.config.ts +32 -2
  90. package/assets/creditwallet.png +0 -0
  91. package/dist/chunk-DDA6FP6U.js +0 -7
  92. package/src/ConnectModal.scss +0 -665
  93. package/src/ConnectModal.tsx +0 -573
  94. package/src/components/QRFrame.tsx +0 -43
  95. package/src/views/CreditWalletView.tsx +0 -58
  96. package/src/views/IdleView.tsx +0 -10
  97. package/src/views/MetaMaskView.tsx +0 -66
  98. package/src/views/SwitchChainView.tsx +0 -72
  99. package/src/views/WalletConnectView.tsx +0 -206
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gluwa/connect-kit",
3
- "version": "0.2.0-next.1",
3
+ "version": "0.2.0-next.11",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -11,6 +11,7 @@
11
11
  "types": "./src/credit-connect.ts",
12
12
  "default": "./src/credit-connect.ts"
13
13
  },
14
+ "./index.css": "./dist/index.css",
14
15
  "./package.json": "./package.json"
15
16
  },
16
17
  "publishConfig": {
@@ -45,7 +46,11 @@
45
46
  "dependencies": {
46
47
  "qrcode.react": "^4.2.0",
47
48
  "wagmi": "^2.15.6",
48
- "@wagmi/core": "^2.16.7"
49
+ "@wagmi/core": "^2.16.7",
50
+ "@gluwa/credit-connect-sdk": "workspace:*",
51
+ "@gluwa/credit-connect-storage-local": "workspace:*",
52
+ "@gluwa/credit-connect-e2ee-tweetnacl": "workspace:*",
53
+ "@gluwa/credit-connect-hub-node-ws": "workspace:*"
49
54
  },
50
55
  "devDependencies": {
51
56
  "@gluwa/credit-connect-sdk": "workspace:*",
@@ -62,15 +67,11 @@
62
67
  "wagmi": "^2.15.6"
63
68
  },
64
69
  "peerDependencies": {
65
- "@gluwa/credit-connect-sdk": "workspace:*",
66
70
  "@tanstack/react-query": ">=5.0.0",
67
71
  "react": "^18.0.0 || ^19.0.0",
68
72
  "viem": ">=2.0.0"
69
73
  },
70
74
  "peerDependenciesMeta": {
71
- "@gluwa/credit-connect-sdk": {
72
- "optional": true
73
- },
74
75
  "@tanstack/react-query": {
75
76
  "optional": true
76
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gluwa/connect-kit",
3
- "version": "0.2.0-next.1",
3
+ "version": "0.2.0-next.11",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -25,7 +25,11 @@
25
25
  "dependencies": {
26
26
  "qrcode.react": "^4.2.0",
27
27
  "wagmi": "^2.15.6",
28
- "@wagmi/core": "^2.16.7"
28
+ "@wagmi/core": "^2.16.7",
29
+ "@gluwa/credit-connect-storage-local": "0.2.0-next.6",
30
+ "@gluwa/credit-connect-sdk": "0.2.0-next.6",
31
+ "@gluwa/credit-connect-e2ee-tweetnacl": "0.2.0-next.6",
32
+ "@gluwa/credit-connect-hub-node-ws": "0.1.0-next.6"
29
33
  },
30
34
  "devDependencies": {
31
35
  "@tanstack/react-query": "^5.62.0",
@@ -39,18 +43,14 @@
39
43
  "typescript-eslint": "^8.18.2",
40
44
  "viem": "^2.31.7",
41
45
  "wagmi": "^2.15.6",
42
- "@gluwa/credit-connect-sdk": "0.2.0-next.1"
46
+ "@gluwa/credit-connect-sdk": "0.2.0-next.6"
43
47
  },
44
48
  "peerDependencies": {
45
49
  "@tanstack/react-query": ">=5.0.0",
46
50
  "react": "^18.0.0 || ^19.0.0",
47
- "viem": ">=2.0.0",
48
- "@gluwa/credit-connect-sdk": "0.2.0-next.1"
51
+ "viem": ">=2.0.0"
49
52
  },
50
53
  "peerDependenciesMeta": {
51
- "@gluwa/credit-connect-sdk": {
52
- "optional": true
53
- },
54
54
  "@tanstack/react-query": {
55
55
  "optional": true
56
56
  }
@@ -9,15 +9,21 @@ import {
9
9
  type FC,
10
10
  type ReactNode,
11
11
  } from 'react';
12
- import { WagmiProvider, useAccount, useDisconnect, useSwitchChain } from 'wagmi';
12
+ import { WagmiProvider, useAccount, useConfig, useDisconnect, useSwitchChain } from 'wagmi';
13
+ import { getAccount } from '@wagmi/core';
13
14
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
14
- import { ConnectModal } from './ConnectModal';
15
+ // import { ConnectModal } from './ConnectModal';
16
+ import { ConnectDialog } from './layout/connectDialog';
15
17
  import { getConfig, getKitConfig, initConfig } from './core/config';
18
+ import { resolveConnectorId } from './connector-meta';
16
19
  import type {
17
20
  ConnectErrorContext,
21
+ ConnectBannerSiteKey,
18
22
  ConnectKitConfig,
23
+ ConnectKitTheme,
19
24
  ConnectResult,
20
25
  ConnectorId,
26
+ ConnectorKey,
21
27
  Connectors,
22
28
  } from './types';
23
29
 
@@ -25,19 +31,26 @@ const internalQueryClient = new QueryClient();
25
31
 
26
32
  export interface ConnectKitProviderProps {
27
33
  children: ReactNode;
34
+ siteKey: ConnectBannerSiteKey;
35
+ connectBannerUrl: string;
28
36
  kitConfig?: ConnectKitConfig;
29
37
  connectors: Connectors;
30
38
  requiredChainId?: number;
39
+ theme: ConnectKitTheme;
31
40
  onConnect?: (result: ConnectResult) => void;
32
41
  onLog?: (message: string, error?: Error) => void;
33
42
  renderConnectError?: (ctx: ConnectErrorContext) => ReactNode;
34
43
  }
35
-
36
44
  export interface ConnectKitContextValue {
37
45
  isOpen: boolean;
38
46
  open: () => void;
39
47
  close: () => void;
40
48
  isConnected: boolean;
49
+ needsChainSwitch: boolean;
50
+ requiredChainId: number | undefined;
51
+ currentChainId: number | undefined;
52
+ isSwitchingChain: boolean;
53
+ switchToRequiredChain: () => Promise<void>;
41
54
  address: `0x${string}` | undefined;
42
55
  connectorId: ConnectorId | null;
43
56
  disconnect: () => Promise<void>;
@@ -45,14 +58,16 @@ export interface ConnectKitContextValue {
45
58
 
46
59
  const ConnectKitContext = createContext<ConnectKitContextValue | null>(null);
47
60
 
48
- const resolveConnectorId = (wagmiConnectorId: string | undefined): ConnectorId | null => {
49
- if (!wagmiConnectorId) return null;
50
- if (wagmiConnectorId === 'credit-connect') return 'CREDIT_CONNECT';
51
- if (wagmiConnectorId === 'walletConnect') return 'WALLET_CONNECT';
52
- const lower = wagmiConnectorId.toLowerCase();
53
- if (lower.includes('metamask') || wagmiConnectorId === 'injected') return 'METAMASK';
54
- if (lower.includes('walletconnect')) return 'WALLET_CONNECT';
55
- return null;
61
+ // 외부 API Connectors (ConnectorKey 배열) 내부 ConnectorId[] 매핑.
62
+ // 입력 순서 그대로 selector 표시 순서.
63
+ const CONNECTOR_KEY_TO_ID: Record<ConnectorKey, ConnectorId> = {
64
+ creditWallet: 'CREDIT_CONNECT',
65
+ metamask: 'METAMASK',
66
+ walletConnect: 'WALLET_CONNECT',
67
+ };
68
+ const resolveConnectorsFromConfig = (connectorsConfig?: Connectors): ConnectorId[] => {
69
+ if (!connectorsConfig) return [];
70
+ return connectorsConfig.map((key) => CONNECTOR_KEY_TO_ID[key]);
56
71
  };
57
72
 
58
73
  export const ConnectKitProvider: FC<ConnectKitProviderProps> = (props) => {
@@ -78,17 +93,22 @@ type ConnectKitProviderInnerProps = Omit<ConnectKitProviderProps, 'kitConfig'> &
78
93
 
79
94
  const ConnectKitProviderInner: FC<ConnectKitProviderInnerProps> = ({
80
95
  children,
96
+ siteKey,
97
+ connectBannerUrl,
81
98
  connectors,
82
99
  kitConfig,
83
100
  requiredChainId,
101
+ theme,
84
102
  onConnect,
85
103
  onLog,
86
- renderConnectError,
87
104
  }) => {
105
+ // NOTE: renderConnectError 는 ConnectDialog 가 아직 error UI customization 을 받지 않으므로
106
+ // destructure 에서 제외. 추후 ConnectDialog 의 timeout/error content 에 wire 필요 시 복원.
88
107
  const [isOpen, setIsOpen] = useState(false);
89
108
  const [pendingResult, setPendingResult] = useState<ConnectResult | null>(null);
90
109
 
91
110
  const account = useAccount();
111
+ const wagmiConfig = useConfig();
92
112
  const { disconnectAsync } = useDisconnect();
93
113
  const { switchChainAsync } = useSwitchChain();
94
114
 
@@ -97,14 +117,6 @@ const ConnectKitProviderInner: FC<ConnectKitProviderInnerProps> = ({
97
117
  onConnectRef.current = onConnect;
98
118
  }, [onConnect]);
99
119
 
100
- const chainMismatch =
101
- requiredChainId != null &&
102
- account.status === 'connected' &&
103
- account.chainId != null &&
104
- account.chainId !== requiredChainId;
105
-
106
- const [autoSwitchFailedKey, setAutoSwitchFailedKey] = useState<string | null>(null);
107
- const autoSwitchInFlightRef = useRef(false);
108
120
  const isMountedRef = useRef(true);
109
121
  useEffect(() => {
110
122
  isMountedRef.current = true;
@@ -113,39 +125,15 @@ const ConnectKitProviderInner: FC<ConnectKitProviderInnerProps> = ({
113
125
  };
114
126
  }, []);
115
127
 
116
- const mismatchKey =
117
- chainMismatch && account.chainId != null && requiredChainId != null
118
- ? `${account.chainId}->${requiredChainId}`
119
- : null;
128
+ const needsChainSwitch =
129
+ requiredChainId != null &&
130
+ account.status === 'connected' &&
131
+ account.chainId != null &&
132
+ account.chainId !== requiredChainId;
120
133
 
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]);
134
+ const [isSwitchingChain, setIsSwitchingChain] = useState(false);
140
135
 
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);
136
+ const effectiveOpen = isOpen;
149
137
 
150
138
  const open = useCallback((): void => {
151
139
  setIsOpen(true);
@@ -153,29 +141,62 @@ const ConnectKitProviderInner: FC<ConnectKitProviderInnerProps> = ({
153
141
 
154
142
  const close = useCallback((): void => {
155
143
  setIsOpen(false);
156
- }, []);
144
+ const status = getAccount(wagmiConfig).status;
145
+ if (status === 'connecting' || status === 'reconnecting') {
146
+ disconnectAsync().catch(() => undefined);
147
+ }
148
+ }, [wagmiConfig, disconnectAsync]);
157
149
 
158
150
  const disconnect = useCallback(async (): Promise<void> => {
159
151
  await disconnectAsync();
160
152
  }, [disconnectAsync]);
161
153
 
162
- const handleModalConnect = useCallback((result: ConnectResult): void => {
154
+ const switchToRequiredChain = useCallback(async (): Promise<void> => {
155
+ if (requiredChainId == null) return;
156
+ setIsSwitchingChain(true);
157
+ try {
158
+ await switchChainAsync({ chainId: requiredChainId });
159
+ } finally {
160
+ if (isMountedRef.current) setIsSwitchingChain(false);
161
+ }
162
+ }, [requiredChainId, switchChainAsync]);
163
+
164
+ // ConnectDialog 의 wagmi connect 성공 → pendingResult set → 아래 useEffect 가
165
+ // account.status === 'connected' 확인 후 외부 onConnect 호출 + modal close.
166
+ // wagmi state 가 비동기로 'connected' 로 전환되는 race 흡수용.
167
+ const handleDialogConnect = useCallback((result: ConnectResult): void => {
163
168
  setPendingResult(result);
164
169
  }, []);
165
170
 
166
- useEffect(() => {
171
+ const handleDialogError = useCallback(
172
+ (error: Error, connectorId: ConnectorId): void => {
173
+ onLog?.(`[connect-kit] connect failed: ${connectorId} — ${error.message}`, error);
174
+ },
175
+ [onLog],
176
+ );
177
+
178
+ // 연결 성공 시 modal 자동 close — wagmi state 만 기준으로.
179
+ // 이전 구현은 pendingResult 가 set 된 경우만 close 했는데, `setPendingResult` 는
180
+ // ConnectDialog 의 onConnect (`connectAsync.then`) 안에서만 호출됨. MM deeplink 같은
181
+ // 시나리오 (앱 이동 → 승인 → 복귀) 에서 wagmi 가 자체적으로 'connected' 로 전이하지만
182
+ // connectAsync 의 promise 는 resolve 안 될 수 있음 → pendingResult set 안 됨 → modal 안 닫힘.
183
+ // 모든 connector (WC/CC/MM) 의 connected 가 일관된 close 트리거가 되도록 단순화.
184
+ useEffect((): void => {
185
+ if (!effectiveOpen) return;
186
+ if (account.status !== 'connected') return;
187
+ setIsOpen(false);
188
+ }, [effectiveOpen, account.status]);
189
+
190
+ // 외부 onConnect 콜백은 best-effort — pendingResult 가 set 된 경우만 호출.
191
+ useEffect((): void => {
167
192
  if (!pendingResult) return;
168
193
  if (account.status !== 'connected') {
169
194
  setPendingResult(null);
170
195
  return;
171
196
  }
172
- if (requiredChainId != null && account.chainId !== requiredChainId) {
173
- return;
174
- }
175
197
  onConnectRef.current?.(pendingResult);
176
198
  setPendingResult(null);
177
- setIsOpen(false);
178
- }, [pendingResult, account.status, account.chainId, requiredChainId]);
199
+ }, [pendingResult, account.status]);
179
200
 
180
201
  const value = useMemo<ConnectKitContextValue>(
181
202
  () => ({
@@ -183,6 +204,11 @@ const ConnectKitProviderInner: FC<ConnectKitProviderInnerProps> = ({
183
204
  open,
184
205
  close,
185
206
  isConnected: account.status === 'connected',
207
+ needsChainSwitch,
208
+ requiredChainId,
209
+ currentChainId: account.chainId,
210
+ isSwitchingChain,
211
+ switchToRequiredChain,
186
212
  address: account.address,
187
213
  connectorId: resolveConnectorId(account.connector?.id),
188
214
  disconnect,
@@ -192,6 +218,11 @@ const ConnectKitProviderInner: FC<ConnectKitProviderInnerProps> = ({
192
218
  open,
193
219
  close,
194
220
  account.status,
221
+ needsChainSwitch,
222
+ requiredChainId,
223
+ account.chainId,
224
+ isSwitchingChain,
225
+ switchToRequiredChain,
195
226
  account.address,
196
227
  account.connector?.id,
197
228
  disconnect,
@@ -201,15 +232,16 @@ const ConnectKitProviderInner: FC<ConnectKitProviderInnerProps> = ({
201
232
  return (
202
233
  <ConnectKitContext.Provider value={value}>
203
234
  {children}
204
- <ConnectModal
235
+ <ConnectDialog
205
236
  open={effectiveOpen}
206
- connectors={connectors}
237
+ siteKey={siteKey}
238
+ connectBannerUrl={connectBannerUrl}
239
+ theme={theme}
240
+ connectors={resolveConnectorsFromConfig(connectors)}
207
241
  wcProjectId={kitConfig.wcProjectId}
208
- requiredChainId={requiredChainId}
209
- renderConnectError={renderConnectError}
210
- onConnect={handleModalConnect}
211
242
  onClose={close}
212
- onLog={onLog}
243
+ onConnect={handleDialogConnect}
244
+ onError={handleDialogError}
213
245
  />
214
246
  </ConnectKitContext.Provider>
215
247
  );
@@ -0,0 +1,10 @@
1
+ import {
2
+ watchAsset as wagmiWatchAsset,
3
+ type WatchAssetParameters,
4
+ type WatchAssetReturnType,
5
+ } from '@wagmi/core';
6
+ import { getConfig } from '../core/config';
7
+
8
+ export const watchAsset = (params: WatchAssetParameters): Promise<WatchAssetReturnType> => {
9
+ return wagmiWatchAsset(getConfig(), params);
10
+ };
package/src/assets.d.ts CHANGED
@@ -2,3 +2,8 @@ declare module '*.png' {
2
2
  const src: string;
3
3
  export default src;
4
4
  }
5
+
6
+ declare module '*.svg' {
7
+ const src: string;
8
+ export default src;
9
+ }
@@ -0,0 +1,50 @@
1
+ import { type ReactNode, type AnchorHTMLAttributes, type ButtonHTMLAttributes } from 'react';
2
+ import './style.scss';
3
+
4
+ interface ActionItemContent {
5
+ label?: string;
6
+ description?: string;
7
+ action?: string;
8
+ children?: ReactNode;
9
+ }
10
+
11
+ type ActionItemAsAnchor = { as: 'a' } & AnchorHTMLAttributes<HTMLAnchorElement> & ActionItemContent;
12
+ type ActionItemAsButton = { as: 'button' } & ButtonHTMLAttributes<HTMLButtonElement> &
13
+ ActionItemContent;
14
+
15
+ type ActionItemProps = ActionItemAsAnchor | ActionItemAsButton;
16
+
17
+ const ActionItemBody = ({ label, description, action, children }: ActionItemContent): ReactNode => {
18
+ if (children) return <>{children}</>;
19
+ return (
20
+ <>
21
+ <span className="ck-action-item-content">
22
+ <span className="ck-action-item-label">{label}</span>
23
+ {description && <span className="ck-action-item-desc">{description}</span>}
24
+ </span>
25
+ {action && <span className="ck-action-item-action">{action}</span>}
26
+ </>
27
+ );
28
+ };
29
+
30
+ export const ActionItem = (props: ActionItemProps) => {
31
+ if (props.as === 'a') {
32
+ const { as: _, label, description, action, children, className, ...rest } = props;
33
+ return (
34
+ <a className={`ck-action-item${className ? ` ${className}` : ''}`} {...rest}>
35
+ <ActionItemBody label={label} description={description} action={action}>
36
+ {children}
37
+ </ActionItemBody>
38
+ </a>
39
+ );
40
+ }
41
+
42
+ const { as: _, label, description, action, children, className, ...rest } = props;
43
+ return (
44
+ <button className={`ck-action-item${className ? ` ${className}` : ''}`} {...rest}>
45
+ <ActionItemBody label={label} description={description} action={action}>
46
+ {children}
47
+ </ActionItemBody>
48
+ </button>
49
+ );
50
+ };
@@ -0,0 +1,47 @@
1
+ @use '../../style/mixin.scss' as *;
2
+
3
+ .ck-root .ck-action-item {
4
+ display: flex;
5
+ align-items: center;
6
+ gap: 16px;
7
+ width: 100%;
8
+ padding: 16px 20px;
9
+ border-radius: 12px;
10
+ background: var(--scale-gray-100);
11
+ cursor: pointer;
12
+ margin-top: auto;
13
+ flex-shrink: 0;
14
+ text-decoration: none;
15
+ }
16
+
17
+ .ck-action-item-content {
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: 2px;
21
+ flex: 1;
22
+ min-width: 0;
23
+ }
24
+
25
+ .ck-action-item-label {
26
+ @include textToken('subtitle2-bold');
27
+ color: var(--scale-gray-900);
28
+ }
29
+
30
+ .ck-action-item-desc {
31
+ @include textToken('label5-regular');
32
+ color: var(--scale-gray-600);
33
+ }
34
+
35
+ .ck-action-item-action {
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ padding: 0 20px;
40
+ height: 44px;
41
+ border-radius: 6px;
42
+ background: var(--scale-blue2-alpha-100);
43
+ @include textToken('label5-bold');
44
+ color: var(--semantic-brand-trugi-primary);
45
+ white-space: nowrap;
46
+ flex-shrink: 0;
47
+ }
@@ -0,0 +1,16 @@
1
+ import { type FC } from 'react';
2
+ import './style.scss';
3
+ import { CopyIcon } from '../../layout/connectDialog/asset/copyIcon';
4
+
5
+ interface CopyLinkProps {
6
+ onCopy?: () => void;
7
+ }
8
+
9
+ export const CopyLink: FC<CopyLinkProps> = ({ onCopy }) => {
10
+ return (
11
+ <button type="button" className="ck-copy-link" onClick={onCopy}>
12
+ <CopyIcon className="ck-copy-link-icon" />
13
+ Copy link
14
+ </button>
15
+ );
16
+ };
@@ -0,0 +1,16 @@
1
+ @use '../../style/mixin.scss' as *;
2
+
3
+ .ck-root .ck-copy-link {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ align-self: center;
7
+ gap: 6px;
8
+ @include textToken('body3-l1-regular');
9
+ color: var(--scale-gray-500);
10
+ cursor: pointer;
11
+ }
12
+
13
+ .ck-copy-link-icon {
14
+ flex-shrink: 0;
15
+ color: var(--scale-gray-500);
16
+ }
@@ -0,0 +1,72 @@
1
+ import { type FC, type CSSProperties } from 'react';
2
+ import { QRCodeSVG } from 'qrcode.react';
3
+ import { CopyLink } from '../copyLink';
4
+ import './style.scss';
5
+
6
+ export type QrAreaStatus = 'loading' | 'ready' | 'timeout';
7
+
8
+ interface QrAreaProps {
9
+ uri?: string | null;
10
+ status: QrAreaStatus;
11
+ logoUrl?: string;
12
+ hasBg?: boolean;
13
+ label?: string;
14
+ onCopy?: () => void;
15
+ }
16
+
17
+ const BG_COLOR = '#ffffff';
18
+ const FG_COLOR = '#000000';
19
+ const FG_GRADIENT = 'url(#ck-qr-gradient)';
20
+
21
+ export const QrArea: FC<QrAreaProps> = ({ uri, status, logoUrl, hasBg = false, label, onCopy }) => {
22
+ const bgColor = hasBg ? BG_COLOR : 'transparent';
23
+ const fgColor = hasBg ? FG_COLOR : FG_GRADIENT;
24
+
25
+ return (
26
+ <div className="ck-qr-area">
27
+ <div className="ck-qr-group">
28
+ <figure className={`ck-qr ck-qr-${status}`} aria-label="QR Code">
29
+ {status === 'loading' && (
30
+ <div className="ck-qr-skeleton" aria-busy="true" aria-label="Loading QR code">
31
+ <div className="ck-qr-skeleton-inner" />
32
+ </div>
33
+ )}
34
+ {status === 'ready' && uri && (
35
+ <div
36
+ className="ck-qr-frame"
37
+ style={{ 'background': bgColor, '--ck-qr-bg': bgColor } as CSSProperties}
38
+ >
39
+ {!hasBg && (
40
+ <svg width="0" height="0" style={{ position: 'absolute' }} aria-hidden="true">
41
+ <defs>
42
+ <linearGradient id="ck-qr-gradient" x1="67%" y1="11%" x2="45%" y2="99%">
43
+ <stop offset="0%" stopColor="#7B91FF" />
44
+ <stop offset="100%" stopColor="#61BCFF" />
45
+ </linearGradient>
46
+ </defs>
47
+ </svg>
48
+ )}
49
+ <QRCodeSVG
50
+ value={uri}
51
+ size={288}
52
+ bgColor={bgColor}
53
+ fgColor={fgColor}
54
+ level="M"
55
+ marginSize={0}
56
+ imageSettings={
57
+ logoUrl ? { src: logoUrl, width: 68, height: 68, excavate: true } : undefined
58
+ }
59
+ />
60
+ </div>
61
+ )}
62
+ </figure>
63
+ {(label || onCopy) && (
64
+ <div className="ck-qr-text">
65
+ {label && <p className="ck-qr-label">{label}</p>}
66
+ {onCopy && <CopyLink onCopy={onCopy} />}
67
+ </div>
68
+ )}
69
+ </div>
70
+ </div>
71
+ );
72
+ };