@hfunlabs/hypurr-connect 0.1.12 → 0.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hfunlabs/hypurr-connect",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -0,0 +1,344 @@
1
+ import { AnimatePresence, motion } from "framer-motion";
2
+ import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
3
+ import {
4
+ useCallback,
5
+ useState,
6
+ type CSSProperties,
7
+ type ReactNode,
8
+ } from "react";
9
+ import {
10
+ AlertTriangle,
11
+ Loader2,
12
+ SpinKeyframes,
13
+ Trash2,
14
+ X,
15
+ } from "./icons/lucide";
16
+ import {
17
+ closeBtnStyle as makeCloseBtnStyle,
18
+ dangerOutlineButtonStyle,
19
+ fontFamily,
20
+ modalBackdropStyle,
21
+ modalHeaderStyle,
22
+ modalPanelStyle,
23
+ modalWrapperStyle,
24
+ profileColors,
25
+ titleStyle,
26
+ upperLabelStyle,
27
+ } from "./profileStyles";
28
+
29
+ export interface DeleteWalletModalProps {
30
+ isOpen: boolean;
31
+ onClose: () => void;
32
+ wallet: HyperliquidWallet | null;
33
+ onConfirm: (walletId: number) => Promise<void>;
34
+ /** Optional toast callback. Fires `{type:"success"}` on delete; errors are shown inline. */
35
+ onNotify?: (n: { type: "success" | "error"; message: string }) => void;
36
+ }
37
+
38
+ const DANGER_BG = "rgba(248,113,113,0.07)";
39
+ const DANGER_BORDER = "rgba(248,113,113,0.2)";
40
+
41
+ const backdropStyle = modalBackdropStyle(110);
42
+ const wrapperStyle = modalWrapperStyle(111, 16);
43
+ const panelStyle: CSSProperties = {
44
+ ...modalPanelStyle(true),
45
+ border: `1px solid ${profileColors.surfaceBd}`,
46
+ };
47
+ const headerStyle: CSSProperties = {
48
+ ...modalHeaderStyle,
49
+ borderBottom: "1px solid rgba(255,255,255,0.06)",
50
+ };
51
+
52
+ const bodyStyle: CSSProperties = {
53
+ padding: "20px 24px",
54
+ display: "flex",
55
+ flexDirection: "column",
56
+ gap: 16,
57
+ };
58
+
59
+ const warningBoxStyle: CSSProperties = {
60
+ display: "flex",
61
+ alignItems: "flex-start",
62
+ gap: 12,
63
+ padding: 14,
64
+ background: DANGER_BG,
65
+ border: `1px solid ${DANGER_BORDER}`,
66
+ borderRadius: 8,
67
+ };
68
+
69
+ const infoBoxStyle: CSSProperties = {
70
+ padding: "10px 12px",
71
+ background: "rgba(255,255,255,0.03)",
72
+ border: "1px solid rgba(255,255,255,0.06)",
73
+ borderRadius: 8,
74
+ };
75
+
76
+ const labelStyle: CSSProperties = {
77
+ display: "block",
78
+ fontSize: 12.5,
79
+ lineHeight: "1rem",
80
+ color: profileColors.muted,
81
+ marginBottom: 8,
82
+ };
83
+
84
+ const inputStyle = (disabled: boolean): CSSProperties => ({
85
+ width: "100%",
86
+ background: "rgba(13,18,25,0.9)",
87
+ border: `1px solid ${profileColors.surfaceBd}`,
88
+ borderRadius: 8,
89
+ padding: "10px 12px",
90
+ color: profileColors.text,
91
+ fontFamily: fontFamily.mono,
92
+ fontSize: 12.5,
93
+ lineHeight: "1rem",
94
+ outline: "none",
95
+ opacity: disabled ? 0.5 : 1,
96
+ boxSizing: "border-box",
97
+ transition: "border-color 150ms, background-color 150ms",
98
+ });
99
+
100
+ const footerStyle: CSSProperties = {
101
+ padding: "0 24px 24px",
102
+ };
103
+
104
+ const deleteButtonStyle = (
105
+ enabled: boolean,
106
+ hovered: boolean,
107
+ ): CSSProperties => ({
108
+ ...dangerOutlineButtonStyle(enabled, hovered),
109
+ width: "100%",
110
+ padding: "8px 0",
111
+ borderRadius: 8,
112
+ fontSize: 12.5,
113
+ lineHeight: "1rem",
114
+ fontWeight: 500,
115
+ display: "flex",
116
+ alignItems: "center",
117
+ justifyContent: "center",
118
+ gap: 8,
119
+ transition: "background-color 150ms, color 150ms, border-color 150ms",
120
+ });
121
+
122
+ export function DeleteWalletModal({
123
+ isOpen,
124
+ onClose,
125
+ wallet,
126
+ onConfirm,
127
+ onNotify,
128
+ }: DeleteWalletModalProps): ReactNode {
129
+ const [confirmName, setConfirmName] = useState("");
130
+ const [isDeleting, setIsDeleting] = useState(false);
131
+ const [error, setError] = useState<string | null>(null);
132
+ const [deleteHovered, setDeleteHovered] = useState(false);
133
+
134
+ const walletName = wallet?.name || "Unnamed Wallet";
135
+ const isNameMatch = confirmName === walletName;
136
+ const canDelete = isNameMatch && !isDeleting;
137
+
138
+ const handleClose = useCallback(() => {
139
+ if (isDeleting) return;
140
+ setConfirmName("");
141
+ setError(null);
142
+ onClose();
143
+ }, [isDeleting, onClose]);
144
+
145
+ const handleDelete = useCallback(async () => {
146
+ if (!wallet || !isNameMatch) return;
147
+ setError(null);
148
+ setIsDeleting(true);
149
+ try {
150
+ await onConfirm(wallet.id);
151
+ setConfirmName("");
152
+ onNotify?.({ type: "success", message: "Wallet deleted successfully" });
153
+ onClose();
154
+ } catch (e: unknown) {
155
+ setError(e instanceof Error ? e.message : "Failed to delete wallet");
156
+ } finally {
157
+ setIsDeleting(false);
158
+ }
159
+ }, [wallet, isNameMatch, onConfirm, onClose, onNotify]);
160
+
161
+ return (
162
+ <AnimatePresence>
163
+ {isOpen && wallet && (
164
+ <>
165
+ <SpinKeyframes />
166
+ <motion.div
167
+ key="backdrop"
168
+ style={backdropStyle}
169
+ initial={{ opacity: 0 }}
170
+ animate={{ opacity: 1 }}
171
+ exit={{ opacity: 0 }}
172
+ transition={{ duration: 0.15 }}
173
+ onClick={handleClose}
174
+ />
175
+ <div style={wrapperStyle}>
176
+ <motion.div
177
+ key="panel"
178
+ style={panelStyle}
179
+ initial={{ opacity: 0, y: 8 }}
180
+ animate={{ opacity: 1, y: 0 }}
181
+ exit={{ opacity: 0, y: 8 }}
182
+ transition={{ duration: 0.18, ease: "easeOut" }}
183
+ onClick={(e) => e.stopPropagation()}
184
+ >
185
+ <div style={headerStyle}>
186
+ <h3 style={titleStyle}>Delete Wallet</h3>
187
+ <button
188
+ onClick={handleClose}
189
+ disabled={isDeleting}
190
+ style={makeCloseBtnStyle(isDeleting)}
191
+ aria-label="Close"
192
+ >
193
+ <X size={16} />
194
+ </button>
195
+ </div>
196
+
197
+ <div style={bodyStyle}>
198
+ <div style={warningBoxStyle}>
199
+ <AlertTriangle
200
+ size={15}
201
+ color={profileColors.danger}
202
+ style={{ flexShrink: 0, marginTop: 2 }}
203
+ />
204
+ <div>
205
+ <p
206
+ style={{
207
+ margin: 0,
208
+ fontSize: 12.5,
209
+ lineHeight: "1rem",
210
+ color: profileColors.danger,
211
+ fontWeight: 500,
212
+ }}
213
+ >
214
+ This action cannot be undone
215
+ </p>
216
+ <p
217
+ style={{
218
+ margin: "2px 0 0",
219
+ fontSize: 12.5,
220
+ lineHeight: "1rem",
221
+ color: "rgba(248,113,113,0.7)",
222
+ }}
223
+ >
224
+ The private key will be permanently deleted. Any remaining
225
+ funds will be inaccessible.
226
+ </p>
227
+ </div>
228
+ </div>
229
+
230
+ <div style={infoBoxStyle}>
231
+ <p
232
+ style={{
233
+ margin: "0 0 4px",
234
+ color: profileColors.muted,
235
+ ...upperLabelStyle,
236
+ }}
237
+ >
238
+ Wallet to delete
239
+ </p>
240
+ <p
241
+ style={{
242
+ margin: 0,
243
+ fontSize: 12.5,
244
+ lineHeight: "1rem",
245
+ fontWeight: 500,
246
+ color: profileColors.text,
247
+ }}
248
+ >
249
+ {walletName}
250
+ </p>
251
+ <p
252
+ style={{
253
+ margin: "2px 0 0",
254
+ fontSize: 12.5,
255
+ lineHeight: "1rem",
256
+ color: profileColors.muted,
257
+ fontFamily: fontFamily.mono,
258
+ wordBreak: "break-all",
259
+ }}
260
+ >
261
+ {wallet.ethereumAddress}
262
+ </p>
263
+ </div>
264
+
265
+ <div>
266
+ <label style={labelStyle}>
267
+ Type{" "}
268
+ <span
269
+ style={{
270
+ color: profileColors.text,
271
+ fontWeight: 500,
272
+ }}
273
+ >
274
+ &quot;{walletName}&quot;
275
+ </span>{" "}
276
+ to confirm
277
+ </label>
278
+ <input
279
+ type="text"
280
+ value={confirmName}
281
+ onChange={(e) => setConfirmName(e.target.value)}
282
+ placeholder={walletName}
283
+ disabled={isDeleting}
284
+ style={inputStyle(isDeleting)}
285
+ />
286
+ </div>
287
+
288
+ {error && (
289
+ <div
290
+ style={{
291
+ display: "flex",
292
+ alignItems: "flex-start",
293
+ gap: 8,
294
+ padding: 12,
295
+ background: "rgba(248,113,113,0.08)",
296
+ border: `1px solid ${DANGER_BORDER}`,
297
+ borderRadius: 8,
298
+ }}
299
+ >
300
+ <AlertTriangle
301
+ size={14}
302
+ color={profileColors.danger}
303
+ style={{ flexShrink: 0, marginTop: 2 }}
304
+ />
305
+ <p
306
+ style={{
307
+ margin: 0,
308
+ fontSize: 12.5,
309
+ lineHeight: "1rem",
310
+ color: profileColors.danger,
311
+ }}
312
+ >
313
+ {error}
314
+ </p>
315
+ </div>
316
+ )}
317
+ </div>
318
+
319
+ <div style={footerStyle}>
320
+ <button
321
+ onClick={handleDelete}
322
+ disabled={!canDelete}
323
+ onMouseEnter={() => setDeleteHovered(true)}
324
+ onMouseLeave={() => setDeleteHovered(false)}
325
+ style={deleteButtonStyle(canDelete, deleteHovered)}
326
+ >
327
+ {isDeleting ? (
328
+ <>
329
+ <Loader2 size={14} /> Deleting...
330
+ </>
331
+ ) : (
332
+ <>
333
+ <Trash2 size={14} /> Delete Wallet
334
+ </>
335
+ )}
336
+ </button>
337
+ </div>
338
+ </motion.div>
339
+ </div>
340
+ </>
341
+ )}
342
+ </AnimatePresence>
343
+ );
344
+ }
@@ -52,8 +52,6 @@ import type {
52
52
  /** @internal context value — extends the public type with fields used only by library internals */
53
53
  interface InternalConnectState extends HypurrConnectState {
54
54
  loginTelegram: () => void;
55
- botUsername: string;
56
- useWidget: boolean;
57
55
  }
58
56
 
59
57
  const TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-jwt";
@@ -504,9 +502,12 @@ export function HypurrConnectProvider({
504
502
  displayName: tgUser.telegramUsername
505
503
  ? `@${tgUser.telegramUsername}`
506
504
  : `Telegram ${tgUser.telegramId}`,
507
- photoUrl: tgUser.pictureFileId
508
- ? `${mediaUrl}/${tgUser.pictureFileId}`
509
- : undefined,
505
+ photoUrl: (() => {
506
+ // `pictureFileId` is read at runtime; the schema may not type it.
507
+ const fileId = (tgUser as unknown as { pictureFileId?: string })
508
+ .pictureFileId;
509
+ return fileId ? `${mediaUrl}/${fileId}` : undefined;
510
+ })(),
510
511
  authMethod: "telegram",
511
512
  telegramId: String(tgUser.telegramId),
512
513
  hfunScore: tgUser?.reputation?.hfunScore,
@@ -912,6 +913,21 @@ export function HypurrConnectProvider({
912
913
  [tgClient, telegramRpcOptions, selectedWalletId, wallets, refreshWallets],
913
914
  );
914
915
 
916
+ const renameWallet = useCallback(
917
+ async (walletId: number, name: string): Promise<void> => {
918
+ await tgClient.hyperliquidWalletUpdate(
919
+ {
920
+ authData: {},
921
+ walletId,
922
+ name,
923
+ },
924
+ telegramRpcOptions,
925
+ );
926
+ refreshWallets();
927
+ },
928
+ [tgClient, telegramRpcOptions, refreshWallets],
929
+ );
930
+
915
931
  const createWalletPack = useCallback(
916
932
  async (name: string): Promise<number> => {
917
933
  const { response } = await tgClient.telegramChatWalletPackCreate(
@@ -1133,16 +1149,18 @@ export function HypurrConnectProvider({
1133
1149
  const state = randomState();
1134
1150
  sessionStorage.setItem(TELEGRAM_AUTH_STATE_KEY, state);
1135
1151
 
1136
- const configuredReturnTo = config.telegram.returnTo;
1152
+ const configuredReturnTo = config.telegram?.returnTo;
1137
1153
  const returnTo =
1138
1154
  typeof configuredReturnTo === "function"
1139
1155
  ? configuredReturnTo()
1140
1156
  : configuredReturnTo || currentReturnTo();
1141
1157
 
1142
- const authUrl = new URL(config.telegram.authHubUrl || DEFAULT_AUTH_HUB_URL);
1158
+ const authUrl = new URL(
1159
+ config.telegram?.authHubUrl || DEFAULT_AUTH_HUB_URL,
1160
+ );
1143
1161
  authUrl.searchParams.set("return_to", returnTo);
1144
1162
  authUrl.searchParams.set("state", state);
1145
- authUrl.searchParams.set("scope", normalizeScopes(config.telegram.scope));
1163
+ authUrl.searchParams.set("scope", normalizeScopes(config.telegram?.scope));
1146
1164
 
1147
1165
  const width = 520;
1148
1166
  const height = 720;
@@ -1168,9 +1186,9 @@ export function HypurrConnectProvider({
1168
1186
 
1169
1187
  window.location.assign(authUrl.toString());
1170
1188
  }, [
1171
- config.telegram.authHubUrl,
1172
- config.telegram.returnTo,
1173
- config.telegram.scope,
1189
+ config.telegram?.authHubUrl,
1190
+ config.telegram?.returnTo,
1191
+ config.telegram?.scope,
1174
1192
  ]);
1175
1193
 
1176
1194
  const connectEoa = useCallback(
@@ -1333,6 +1351,7 @@ export function HypurrConnectProvider({
1333
1351
 
1334
1352
  createWallet,
1335
1353
  deleteWallet,
1354
+ renameWallet,
1336
1355
  refreshWallets,
1337
1356
 
1338
1357
  packs,
@@ -1362,10 +1381,6 @@ export function HypurrConnectProvider({
1362
1381
  agentReady,
1363
1382
  clearAgent: handleClearAgent,
1364
1383
 
1365
- botId: config.telegram?.botId ?? "",
1366
- botUsername: config.telegram?.botUsername ?? "",
1367
- useWidget: config.telegram?.useWidget ?? false,
1368
-
1369
1384
  authDataMap,
1370
1385
  authToken: tgAuthToken,
1371
1386
  telegramRpcOptions,
@@ -1385,6 +1400,7 @@ export function HypurrConnectProvider({
1385
1400
  selectWallet,
1386
1401
  createWallet,
1387
1402
  deleteWallet,
1403
+ renameWallet,
1388
1404
  refreshWallets,
1389
1405
  packs,
1390
1406
  createWalletPack,
@@ -1407,9 +1423,6 @@ export function HypurrConnectProvider({
1407
1423
  agent,
1408
1424
  agentReady,
1409
1425
  handleClearAgent,
1410
- config.telegram?.botId,
1411
- config.telegram?.botUsername,
1412
- config.telegram?.useWidget,
1413
1426
  authDataMap,
1414
1427
  tgAuthToken,
1415
1428
  telegramRpcOptions,
@@ -17,10 +17,14 @@ import { TelegramColorIcon } from "./icons/TelegramColorIcon";
17
17
  export interface LoginModalProps {
18
18
  onConnectWallet: () => void;
19
19
  walletIcon?: ReactNode;
20
+ /** CSS color used as the modal/drawer background. Defaults to `rgba(20,20,20,0.95)`. */
21
+ backgroundColor?: string;
20
22
  }
21
23
 
22
24
  const MOBILE_BREAKPOINT = 640;
23
25
 
26
+ const DEFAULT_BACKGROUND_COLOR = "rgba(20,20,20,0.95)";
27
+
24
28
  const btnStyle: CSSProperties = {
25
29
  display: "flex",
26
30
  height: 53,
@@ -70,7 +74,6 @@ const modalBoxStyle: CSSProperties = {
70
74
  overflow: "hidden",
71
75
  borderRadius: 12,
72
76
  border: "1px solid rgba(255,255,255,0.1)",
73
- background: "#282828",
74
77
  padding: 24,
75
78
  };
76
79
 
@@ -127,7 +130,11 @@ function HoverButton({
127
130
  );
128
131
  }
129
132
 
130
- export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
133
+ export function LoginModal({
134
+ onConnectWallet,
135
+ walletIcon,
136
+ backgroundColor = DEFAULT_BACKGROUND_COLOR,
137
+ }: LoginModalProps) {
131
138
  const { loginTelegram, loginModalOpen, closeLoginModal } =
132
139
  useHypurrConnectInternal();
133
140
 
@@ -174,7 +181,11 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
174
181
  <AnimatePresence>
175
182
  {loginModalOpen &&
176
183
  (isMobile ? (
177
- <MobileDrawer key="drawer" onClose={closeLoginModal}>
184
+ <MobileDrawer
185
+ key="drawer"
186
+ onClose={closeLoginModal}
187
+ backgroundColor={backgroundColor}
188
+ >
178
189
  {modalContent}
179
190
  </MobileDrawer>
180
191
  ) : (
@@ -198,7 +209,7 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
198
209
  onClick={closeLoginModal}
199
210
  >
200
211
  <motion.div
201
- style={modalBoxStyle}
212
+ style={{ ...modalBoxStyle, background: backgroundColor }}
202
213
  initial={{ scale: 0.96, opacity: 0, y: 8 }}
203
214
  animate={{ scale: 1, opacity: 1, y: 0 }}
204
215
  exit={{ scale: 0.96, opacity: 0, y: 8 }}
@@ -230,7 +241,6 @@ const drawerSheetStyle: CSSProperties = {
230
241
  borderLeft: "1px solid rgba(255,255,255,0.1)",
231
242
  borderRight: "1px solid rgba(255,255,255,0.1)",
232
243
  borderTop: "1px solid rgba(255,255,255,0.1)",
233
- background: "#282828",
234
244
  padding: "12px 24px max(24px, env(safe-area-inset-bottom))",
235
245
  };
236
246
 
@@ -241,7 +251,6 @@ const drawerBgStyle: CSSProperties = {
241
251
  top: 0,
242
252
  bottom: "-100vh",
243
253
  zIndex: -1,
244
- background: "#282828",
245
254
  borderTopLeftRadius: 12,
246
255
  borderTopRightRadius: 12,
247
256
  };
@@ -263,9 +272,11 @@ const grabHandleStyle: CSSProperties = {
263
272
  function MobileDrawer({
264
273
  children,
265
274
  onClose,
275
+ backgroundColor,
266
276
  }: {
267
277
  children: ReactNode;
268
278
  onClose: () => void;
279
+ backgroundColor: string;
269
280
  }) {
270
281
  const controls = useAnimationControls();
271
282
 
@@ -294,7 +305,7 @@ function MobileDrawer({
294
305
 
295
306
  <motion.div
296
307
  key="drawer-sheet"
297
- style={drawerSheetStyle}
308
+ style={{ ...drawerSheetStyle, background: backgroundColor }}
298
309
  initial={{ y: "100%" }}
299
310
  animate={{ y: 0 }}
300
311
  exit={{ y: "100%" }}
@@ -304,7 +315,7 @@ function MobileDrawer({
304
315
  dragElastic={{ top: 0, bottom: 0.4 }}
305
316
  onDragEnd={handleDragEnd}
306
317
  >
307
- <div style={drawerBgStyle} />
318
+ <div style={{ ...drawerBgStyle, background: backgroundColor }} />
308
319
 
309
320
  <div style={grabHandleAreaStyle}>
310
321
  <div style={grabHandleStyle} />