@b3dotfun/sdk 0.0.65-test.1 → 0.0.65-test.3
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/dist/cjs/anyspend/react/components/AnySpend.js +4 -2
- package/dist/cjs/anyspend/react/components/AnySpendCustom.js +1 -1
- package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.js +1 -1
- package/dist/cjs/anyspend/react/components/AnyspendDepositHype.js +1 -1
- package/dist/cjs/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
- package/dist/cjs/anyspend/react/components/common/CryptoPaymentMethod.js +4 -2
- package/dist/cjs/anyspend/react/hooks/useSigMint.d.ts +1 -1
- package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.js +80 -37
- package/dist/cjs/global-account/react/components/B3DynamicModal.js +1 -9
- package/dist/cjs/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
- package/dist/cjs/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.js +37 -0
- package/dist/cjs/global-account/react/components/ManageAccount/BalanceContent.js +4 -3
- package/dist/cjs/global-account/react/components/ManageAccount/HomeActions.js +1 -1
- package/dist/cjs/global-account/react/components/ManageAccount/ManageAccount.js +1 -1
- package/dist/cjs/global-account/react/components/ManageAccount/ProfileSection.js +6 -3
- package/dist/cjs/global-account/react/components/ManageAccount/SettingsProfileCard.js +77 -9
- package/dist/cjs/global-account/react/components/SignInWithB3/SignIn.js +3 -1
- package/dist/cjs/global-account/react/components/index.d.ts +1 -2
- package/dist/cjs/global-account/react/components/index.js +6 -8
- package/dist/cjs/global-account/react/hooks/useAccountWallet.d.ts +1 -0
- package/dist/cjs/global-account/react/hooks/useAccountWallet.js +18 -0
- package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +2 -2
- package/dist/cjs/global-account/react/hooks/useUserQuery.d.ts +2 -2
- package/dist/cjs/global-account/react/stores/useModalStore.d.ts +1 -7
- package/dist/cjs/shared/constants/chains/supported.d.ts +1 -1
- package/dist/cjs/shared/utils/ipfs.js +10 -3
- package/dist/esm/anyspend/react/components/AnySpend.js +4 -2
- package/dist/esm/anyspend/react/components/AnySpendCustom.js +1 -1
- package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +1 -1
- package/dist/esm/anyspend/react/components/AnyspendDepositHype.js +1 -1
- package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
- package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.js +4 -2
- package/dist/esm/anyspend/react/hooks/useSigMint.d.ts +1 -1
- package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.js +81 -38
- package/dist/esm/global-account/react/components/B3DynamicModal.js +1 -9
- package/dist/esm/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
- package/dist/esm/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.js +34 -0
- package/dist/esm/global-account/react/components/ManageAccount/BalanceContent.js +4 -3
- package/dist/esm/global-account/react/components/ManageAccount/HomeActions.js +1 -1
- package/dist/esm/global-account/react/components/ManageAccount/ManageAccount.js +1 -1
- package/dist/esm/global-account/react/components/ManageAccount/ProfileSection.js +6 -3
- package/dist/esm/global-account/react/components/ManageAccount/SettingsProfileCard.js +74 -9
- package/dist/esm/global-account/react/components/SignInWithB3/SignIn.js +4 -2
- package/dist/esm/global-account/react/components/index.d.ts +1 -2
- package/dist/esm/global-account/react/components/index.js +3 -4
- package/dist/esm/global-account/react/hooks/useAccountWallet.d.ts +1 -0
- package/dist/esm/global-account/react/hooks/useAccountWallet.js +17 -0
- package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +2 -2
- package/dist/esm/global-account/react/hooks/useUserQuery.d.ts +2 -2
- package/dist/esm/global-account/react/stores/useModalStore.d.ts +1 -7
- package/dist/esm/shared/constants/chains/supported.d.ts +1 -1
- package/dist/esm/shared/utils/ipfs.js +10 -3
- package/dist/styles/index.css +1 -1
- package/dist/types/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
- package/dist/types/anyspend/react/hooks/useSigMint.d.ts +1 -1
- package/dist/types/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
- package/dist/types/global-account/react/components/index.d.ts +1 -2
- package/dist/types/global-account/react/hooks/useAccountWallet.d.ts +1 -0
- package/dist/types/global-account/react/hooks/useAuthentication.d.ts +2 -2
- package/dist/types/global-account/react/hooks/useUserQuery.d.ts +2 -2
- package/dist/types/global-account/react/stores/useModalStore.d.ts +1 -7
- package/dist/types/shared/constants/chains/supported.d.ts +1 -1
- package/package.json +1 -1
- package/src/anyspend/react/components/AnySpend.tsx +4 -3
- package/src/anyspend/react/components/AnySpendCustom.tsx +0 -2
- package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +0 -2
- package/src/anyspend/react/components/AnyspendDepositHype.tsx +0 -2
- package/src/anyspend/react/components/common/CryptoPaymentMethod.tsx +6 -13
- package/src/global-account/react/components/AvatarEditor/AvatarEditor.tsx +129 -74
- package/src/global-account/react/components/B3DynamicModal.tsx +2 -10
- package/src/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.tsx +84 -0
- package/src/global-account/react/components/ManageAccount/BalanceContent.tsx +4 -7
- package/src/global-account/react/components/ManageAccount/HomeActions.tsx +1 -1
- package/src/global-account/react/components/ManageAccount/ManageAccount.tsx +2 -2
- package/src/global-account/react/components/ManageAccount/ProfileSection.tsx +9 -11
- package/src/global-account/react/components/ManageAccount/SettingsProfileCard.tsx +129 -23
- package/src/global-account/react/components/SignInWithB3/SignIn.tsx +11 -7
- package/src/global-account/react/components/index.ts +3 -4
- package/src/global-account/react/hooks/useAccountWallet.tsx +26 -0
- package/src/global-account/react/stores/useModalStore.ts +1 -9
- package/src/shared/utils/ipfs.ts +10 -3
- package/dist/cjs/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
- package/dist/cjs/global-account/react/components/ProfileEditor/ProfileEditor.js +0 -141
- package/dist/esm/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
- package/dist/esm/global-account/react/components/ProfileEditor/ProfileEditor.js +0 -135
- package/dist/types/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
- package/src/global-account/react/components/ProfileEditor/ProfileEditor.tsx +0 -265
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import app from "../../../../global-account/app.js";
|
|
4
|
-
import { Button, useB3, useProfile } from "../../../../global-account/react/index.js";
|
|
4
|
+
import { Button, IPFSMediaRenderer, useB3, useProfile } from "../../../../global-account/react/index.js";
|
|
5
|
+
import { validateImageUrl } from "../../../../global-account/react/utils/profileDisplay.js";
|
|
5
6
|
import { cn } from "../../../../shared/utils/cn.js";
|
|
6
7
|
import { debugB3React } from "../../../../shared/utils/debug.js";
|
|
7
|
-
import { getIpfsUrl } from "../../../../shared/utils/ipfs.js";
|
|
8
8
|
import { client } from "../../../../shared/utils/thirdweb.js";
|
|
9
9
|
import { Loader2, Upload, X } from "lucide-react";
|
|
10
10
|
import { useRef, useState } from "react";
|
|
@@ -18,16 +18,15 @@ const debug = debugB3React("AvatarEditor");
|
|
|
18
18
|
export function AvatarEditor({ onSetAvatar, className }) {
|
|
19
19
|
const [viewStep, setViewStep] = useState("select");
|
|
20
20
|
const [selectedAvatar, setSelectedAvatar] = useState(null);
|
|
21
|
+
const [selectedProfileType, setSelectedProfileType] = useState(null); // Track which profile was selected
|
|
21
22
|
const [hoveredProfile, setHoveredProfile] = useState(null);
|
|
22
23
|
const [selectedFile, setSelectedFile] = useState(null);
|
|
23
24
|
const [previewUrl, setPreviewUrl] = useState(null);
|
|
24
|
-
const [isUploading, setIsUploading] = useState(false);
|
|
25
25
|
const [isSaving, setIsSaving] = useState(false);
|
|
26
26
|
const [isDragging, setIsDragging] = useState(false);
|
|
27
27
|
const fileInputRef = useRef(null);
|
|
28
28
|
const { setUser, user, partnerId } = useB3();
|
|
29
29
|
const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
|
|
30
|
-
const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen);
|
|
31
30
|
const contentType = useModalStore(state => state.contentType);
|
|
32
31
|
const { setPreference } = useProfileSettings();
|
|
33
32
|
const account = useActiveAccount();
|
|
@@ -35,11 +34,10 @@ export function AvatarEditor({ onSetAvatar, className }) {
|
|
|
35
34
|
address: account?.address,
|
|
36
35
|
fresh: true,
|
|
37
36
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
: undefined;
|
|
37
|
+
// Get raw avatar URLs, convert IPFS URLs, and validate them
|
|
38
|
+
const rawCurrentAvatar = user?.avatar || profile?.avatar;
|
|
39
|
+
const currentAvatar = validateImageUrl(rawCurrentAvatar);
|
|
40
|
+
const safePreviewUrl = validateImageUrl(previewUrl);
|
|
43
41
|
const handleFileSelect = (event) => {
|
|
44
42
|
const file = event.target.files?.[0];
|
|
45
43
|
if (file) {
|
|
@@ -54,6 +52,8 @@ export function AvatarEditor({ onSetAvatar, className }) {
|
|
|
54
52
|
return;
|
|
55
53
|
}
|
|
56
54
|
setSelectedFile(file);
|
|
55
|
+
// Clear profile type selection when uploading a new file
|
|
56
|
+
setSelectedProfileType(null);
|
|
57
57
|
// Create preview URL
|
|
58
58
|
const url = URL.createObjectURL(file);
|
|
59
59
|
setPreviewUrl(url);
|
|
@@ -62,6 +62,7 @@ export function AvatarEditor({ onSetAvatar, className }) {
|
|
|
62
62
|
};
|
|
63
63
|
const handleRemovePreview = () => {
|
|
64
64
|
setSelectedAvatar(currentAvatar || null);
|
|
65
|
+
setSelectedProfileType(null);
|
|
65
66
|
setSelectedFile(null);
|
|
66
67
|
if (previewUrl) {
|
|
67
68
|
URL.revokeObjectURL(previewUrl);
|
|
@@ -78,13 +79,51 @@ export function AvatarEditor({ onSetAvatar, className }) {
|
|
|
78
79
|
}
|
|
79
80
|
setIsSaving(true);
|
|
80
81
|
try {
|
|
82
|
+
let fileToUpload = null;
|
|
81
83
|
// If user uploaded a new file
|
|
82
84
|
if (selectedFile) {
|
|
83
|
-
|
|
85
|
+
fileToUpload = selectedFile;
|
|
86
|
+
}
|
|
87
|
+
else if (selectedProfileType && selectedAvatar) {
|
|
88
|
+
// User selected from existing profile avatars
|
|
89
|
+
// Fetch the image from the URL and convert to blob
|
|
90
|
+
debug("Fetching image from social profile:", selectedAvatar);
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(selectedAvatar);
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new Error("Failed to fetch image");
|
|
95
|
+
}
|
|
96
|
+
const blob = await response.blob();
|
|
97
|
+
debug("Fetched blob with type:", blob.type);
|
|
98
|
+
// Determine the correct extension from the blob's MIME type
|
|
99
|
+
// This handles URLs without extensions (like Farcaster images)
|
|
100
|
+
const mimeToExtension = {
|
|
101
|
+
"image/jpeg": "jpg",
|
|
102
|
+
"image/jpg": "jpg",
|
|
103
|
+
"image/png": "png",
|
|
104
|
+
"image/gif": "gif",
|
|
105
|
+
"image/webp": "webp",
|
|
106
|
+
"image/svg+xml": "svg",
|
|
107
|
+
};
|
|
108
|
+
const extension = blob.type ? mimeToExtension[blob.type.toLowerCase()] || "jpg" : "jpg";
|
|
109
|
+
const mimeType = blob.type || `image/${extension}`;
|
|
110
|
+
fileToUpload = new File([blob], `avatar-${selectedProfileType}.${extension}`, { type: mimeType });
|
|
111
|
+
debug("Successfully converted social profile image to file with extension:", extension);
|
|
112
|
+
}
|
|
113
|
+
catch (fetchError) {
|
|
114
|
+
debug("Error fetching social profile image:", fetchError);
|
|
115
|
+
toast.error("Failed to fetch profile image. Please try uploading manually.");
|
|
116
|
+
setIsSaving(false);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Upload to IPFS if we have a file
|
|
121
|
+
if (fileToUpload) {
|
|
122
|
+
debug("Starting upload to IPFS", fileToUpload);
|
|
84
123
|
// Upload to IPFS using Thirdweb
|
|
85
124
|
const ipfsUrl = await upload({
|
|
86
125
|
client,
|
|
87
|
-
files: [
|
|
126
|
+
files: [fileToUpload],
|
|
88
127
|
});
|
|
89
128
|
debug("Upload successful", ipfsUrl);
|
|
90
129
|
// Save avatar URL using profiles service
|
|
@@ -98,21 +137,6 @@ export function AvatarEditor({ onSetAvatar, className }) {
|
|
|
98
137
|
setUser(user);
|
|
99
138
|
toast.success("Looks great! Your avatar has been saved!");
|
|
100
139
|
}
|
|
101
|
-
else if (selectedAvatar && selectedAvatar !== currentAvatar) {
|
|
102
|
-
// User selected from existing profile avatars
|
|
103
|
-
// Find the profile that matches the selected avatar
|
|
104
|
-
const selectedProfile = profile?.profiles?.find(p => p.avatar === selectedAvatar);
|
|
105
|
-
if (selectedProfile && selectedProfile.type) {
|
|
106
|
-
debug("Setting profile preference to:", selectedProfile.type);
|
|
107
|
-
// Set preference for this profile type
|
|
108
|
-
await setPreference(account.address, selectedProfile.type, account.address, async (message) => {
|
|
109
|
-
// Sign the message using the active account
|
|
110
|
-
const signature = await account.signMessage({ message });
|
|
111
|
-
return signature;
|
|
112
|
-
});
|
|
113
|
-
toast.success("Avatar updated successfully!");
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
140
|
// Refresh profile to get updated avatar
|
|
117
141
|
await refreshProfile();
|
|
118
142
|
onSetAvatar?.();
|
|
@@ -138,8 +162,15 @@ export function AvatarEditor({ onSetAvatar, className }) {
|
|
|
138
162
|
});
|
|
139
163
|
}
|
|
140
164
|
};
|
|
141
|
-
const handleProfileAvatarSelect = (avatarUrl) => {
|
|
165
|
+
const handleProfileAvatarSelect = (avatarUrl, profileType) => {
|
|
142
166
|
setSelectedAvatar(avatarUrl);
|
|
167
|
+
setSelectedProfileType(profileType);
|
|
168
|
+
// Clear any selected file since we're selecting from profile
|
|
169
|
+
setSelectedFile(null);
|
|
170
|
+
if (previewUrl) {
|
|
171
|
+
URL.revokeObjectURL(previewUrl);
|
|
172
|
+
setPreviewUrl(null);
|
|
173
|
+
}
|
|
143
174
|
};
|
|
144
175
|
const handleUploadImageClick = () => {
|
|
145
176
|
setViewStep("upload");
|
|
@@ -178,6 +209,8 @@ export function AvatarEditor({ onSetAvatar, className }) {
|
|
|
178
209
|
return;
|
|
179
210
|
}
|
|
180
211
|
setSelectedFile(file);
|
|
212
|
+
// Clear profile type selection when uploading a new file
|
|
213
|
+
setSelectedProfileType(null);
|
|
181
214
|
// Create preview URL
|
|
182
215
|
const url = URL.createObjectURL(file);
|
|
183
216
|
setPreviewUrl(url);
|
|
@@ -191,18 +224,28 @@ export function AvatarEditor({ onSetAvatar, className }) {
|
|
|
191
224
|
partnerId: partnerId,
|
|
192
225
|
});
|
|
193
226
|
};
|
|
194
|
-
const isLoading =
|
|
195
|
-
// Get profile avatars
|
|
227
|
+
const isLoading = isSaving;
|
|
228
|
+
// Get profile avatars with validated URLs
|
|
196
229
|
const profileAvatars = profile?.profiles
|
|
197
230
|
?.filter(p => p.avatar)
|
|
198
|
-
.map(p =>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
231
|
+
.map(p => {
|
|
232
|
+
const rawAvatarUrl = p?.avatar || "";
|
|
233
|
+
const validatedUrl = validateImageUrl(rawAvatarUrl);
|
|
234
|
+
return {
|
|
235
|
+
type: p.type,
|
|
236
|
+
avatar: validatedUrl,
|
|
237
|
+
name: p.name || p.type,
|
|
238
|
+
};
|
|
239
|
+
})
|
|
240
|
+
.filter(p => p.avatar !== null) || []; // Filter out profiles with invalid avatars
|
|
241
|
+
return (_jsxs("div", { className: cn("flex w-full max-w-md flex-col bg-white", className), children: [viewStep === "upload" && _jsx(ModalHeader, { title: "Upload Image" }), _jsxs("div", { className: "flex flex-col items-center p-6", children: [viewStep === "select" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative mb-6", children: [_jsx("div", { className: "h-32 w-32 overflow-hidden rounded-full", children: safePreviewUrl || selectedAvatar || currentAvatar ? (_jsx(IPFSMediaRenderer, { src: safePreviewUrl || selectedAvatar || currentAvatar || "", alt: "Avatar preview", className: "h-full w-full object-cover" })) : (_jsx("div", { className: "bg-b3-primary-wash h-full w-full" })) }), (selectedAvatar !== currentAvatar || selectedFile) && (_jsx("button", { onClick: handleRemovePreview, className: "absolute -right-1 -top-1 flex h-8 w-8 items-center justify-center rounded-full bg-[#51525c] text-white transition-colors hover:bg-[#71717a]", children: _jsx(X, { className: "h-4 w-4" }) }))] }), _jsxs("button", { onClick: handleUploadImageClick, className: "font-inter shadow-xs mb-6 flex w-full items-center justify-center gap-2 rounded-xl border border-[#e4e4e7] bg-white px-4 py-3 text-sm font-semibold text-[#18181b] transition-colors hover:bg-[#f4f4f5]", children: [_jsx(Upload, { className: "h-4 w-4" }), "Upload image"] }), _jsxs("div", { className: "w-full", children: [_jsx("h3", { className: "mb-2 text-base font-semibold text-[#18181b]", children: "Select your profile image" }), _jsx("p", { className: "mb-4 text-sm font-semibold text-[#475467]", children: "Pick an avatar from your linked profiles, ENS or upload a new one." }), _jsx("div", { className: "mb-4 flex gap-3", children: profileAvatars.map((profileAvatar, index) => {
|
|
242
|
+
// Skip if avatar is null (should not happen due to filter, but TypeScript doesn't know that)
|
|
243
|
+
if (!profileAvatar.avatar)
|
|
244
|
+
return null;
|
|
245
|
+
return (_jsxs("div", { className: "relative", onMouseEnter: () => setHoveredProfile(profileAvatar.type), onMouseLeave: () => setHoveredProfile(null), children: [_jsx("button", { onClick: () => handleProfileAvatarSelect(profileAvatar.avatar || "", profileAvatar.type || ""), className: cn("h-16 w-16 overflow-hidden rounded-full border-2 transition-all", selectedProfileType === profileAvatar.type
|
|
246
|
+
? "border-[#3368ef] ring-2 ring-[#3368ef]/20"
|
|
247
|
+
: "border-transparent hover:border-[#e4e4e7]"), children: _jsx("img", { src: profileAvatar.avatar, alt: `${profileAvatar.type} avatar`, className: "h-full w-full object-cover" }) }), hoveredProfile === profileAvatar.type && (_jsx("div", { className: "absolute -top-10 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-[#18181b] px-3 py-1.5 text-xs text-white", children: profileAvatar.name }))] }, index));
|
|
248
|
+
}) }), _jsxs("button", { onClick: handleLinkMoreAccount, className: "font-inter flex items-center gap-2 text-sm font-semibold text-[#3368ef] hover:underline", children: [_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: "h-4 w-4", children: _jsx("path", { d: "M8.75 2.75C8.75 2.33579 8.41421 2 8 2C7.58579 2 7.25 2.33579 7.25 2.75V7.25H2.75C2.33579 7.25 2 7.58579 2 8C2 8.41421 2.33579 8.75 2.75 8.75H7.25V13.25C7.25 13.6642 7.58579 14 8 14C8.41421 14 8.75 13.6642 8.75 13.25V8.75H13.25C13.6642 8.75 14 8.41421 14 8C14 7.58579 13.6642 7.25 13.25 7.25H8.75V2.75Z", fill: "currentColor" }) }), "Link more account"] })] })] })) : (_jsx(_Fragment, { children: !selectedFile ? (_jsxs("div", { onClick: handleOpenFilePicker, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, className: cn("mb-6 flex w-full cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-16 transition-colors", isDragging
|
|
206
249
|
? "border-[#3368ef] bg-[#f0f5ff]"
|
|
207
|
-
: "border-[#e4e4e7] hover:border-[#3368ef] hover:bg-[#f0f5ff]"), children: [_jsxs("p", { className: "font-inter mb-1 text-sm", children: [_jsx("span", { className: "font-semibold text-[#3368ef]", children: "Click to upload" }), _jsx("span", { className: "text-[#71717a]", children: " or drag and drop" })] }), _jsx("p", { className: "text-xs text-[#71717a]", children: "PNG, JPG or GIF (up to 5MB)" })] })) : (_jsx("div", { className: "mb-6 w-full", children: _jsx("div", { className: "aspect-square w-full overflow-hidden rounded-xl bg-[#f4f4f5]", children: _jsx("img", { src:
|
|
250
|
+
: "border-[#e4e4e7] hover:border-[#3368ef] hover:bg-[#f0f5ff]"), children: [_jsxs("p", { className: "font-inter mb-1 text-sm", children: [_jsx("span", { className: "font-semibold text-[#3368ef]", children: "Click to upload" }), _jsx("span", { className: "text-[#71717a]", children: " or drag and drop" })] }), _jsx("p", { className: "text-xs text-[#71717a]", children: "PNG, JPG or GIF (up to 5MB)" })] })) : (_jsx("div", { className: "mb-6 w-full", children: _jsx("div", { className: "aspect-square w-full overflow-hidden rounded-xl bg-[#f4f4f5]", children: safePreviewUrl ? (_jsx("img", { src: safePreviewUrl, alt: "Preview", className: "h-full w-full object-cover" })) : (_jsx("div", { className: "bg-b3-primary-wash h-full w-full" })) }) })) })), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleFileSelect, className: "hidden" })] }), _jsxs("div", { className: "font-inter flex gap-3 border-t border-[#e4e4e7] p-6 font-semibold", children: [_jsx(Button, { onClick: handleCancel, variant: "outline", disabled: isLoading, className: "flex-1 rounded-xl border-[#e4e4e7] text-[#18181b] hover:bg-[#f4f4f5]", children: "Cancel" }), _jsx(Button, { onClick: handleSaveChanges, disabled: isLoading || (!selectedFile && !selectedProfileType), className: "flex-1 rounded-xl bg-[#3368ef] text-white hover:bg-[#2952cc]", children: isLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Saving..."] })) : ("Save changes") })] })] }));
|
|
208
251
|
}
|
|
@@ -14,7 +14,6 @@ import { Deposit } from "./Deposit/Deposit.js";
|
|
|
14
14
|
import { LinkAccount } from "./LinkAccount/LinkAccount.js";
|
|
15
15
|
import { LinkNewAccount } from "./LinkAccount/LinkNewAccount.js";
|
|
16
16
|
import { ManageAccount } from "./ManageAccount/ManageAccount.js";
|
|
17
|
-
import { ProfileEditor } from "./ProfileEditor/ProfileEditor.js";
|
|
18
17
|
import { RequestPermissions } from "./RequestPermissions/RequestPermissions.js";
|
|
19
18
|
import { Send } from "./Send/Send.js";
|
|
20
19
|
import { SignInWithB3Flow } from "./SignInWithB3/SignInWithB3Flow.js";
|
|
@@ -60,7 +59,6 @@ export function B3DynamicModal() {
|
|
|
60
59
|
"avatarEditor",
|
|
61
60
|
"deposit",
|
|
62
61
|
"send",
|
|
63
|
-
"profileEditor",
|
|
64
62
|
];
|
|
65
63
|
const freestyleTypes = [
|
|
66
64
|
"anySpendNft",
|
|
@@ -129,8 +127,6 @@ export function B3DynamicModal() {
|
|
|
129
127
|
return _jsx(Deposit, {});
|
|
130
128
|
case "send":
|
|
131
129
|
return _jsx(Send, { ...contentType });
|
|
132
|
-
case "profileEditor":
|
|
133
|
-
return _jsx(ProfileEditor, { onSuccess: contentType.onSuccess });
|
|
134
130
|
// Add other modal types here
|
|
135
131
|
default:
|
|
136
132
|
return null;
|
|
@@ -144,9 +140,5 @@ export function B3DynamicModal() {
|
|
|
144
140
|
contentType?.type === "deposit" ||
|
|
145
141
|
contentType?.type === "send" ||
|
|
146
142
|
contentType?.type === "avatarEditor") &&
|
|
147
|
-
"p-0", "mx-auto w-full max-w-md sm:max-w-lg",
|
|
148
|
-
// Remove default width classes for avatar editor and profile editor
|
|
149
|
-
contentType?.type === "avatarEditor" || contentType?.type === "profileEditor"
|
|
150
|
-
? "!w-[90vw] !max-w-none" // Use !important to override default styles
|
|
151
|
-
: "mx-auto w-full max-w-md sm:max-w-lg"), hideCloseButton: hideCloseButton, children: [_jsx(ModalTitle, { className: "sr-only hidden", children: contentType?.type || "Modal" }), _jsx(ModalDescription, { className: "sr-only hidden", children: contentType?.type || "Modal Body" }), _jsxs("div", { className: cn("no-scrollbar max-h-[90dvh] overflow-auto sm:max-h-[80dvh]"), children: [(!hideCloseButton || contentType?.showBackButton) && (_jsxs("button", { onClick: navigateBack, className: "flex items-center gap-2 px-6 py-4 text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white", children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M15.8337 10H4.16699", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M10.0003 15.8334L4.16699 10L10.0003 4.16669", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })] }), _jsx("span", { className: "font-inter text-sm font-semibold", children: "Back" })] })), renderContent()] })] }), (contentType?.type === "avatarEditor" || contentType?.type === "profileEditor") && (_jsx("button", { onClick: () => setB3ModalOpen(false), className: "fixed right-5 top-5 z-[100] cursor-pointer text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white", children: _jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M18 6L6 18M6 6L18 18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }))] }));
|
|
143
|
+
"p-0", "mx-auto w-full max-w-md sm:max-w-lg"), hideCloseButton: hideCloseButton, children: [_jsx(ModalTitle, { className: "sr-only hidden", children: contentType?.type || "Modal" }), _jsx(ModalDescription, { className: "sr-only hidden", children: contentType?.type || "Modal Body" }), _jsxs("div", { className: cn("no-scrollbar max-h-[90dvh] overflow-auto sm:max-h-[80dvh]"), children: [(!hideCloseButton || contentType?.showBackButton) && (_jsxs("button", { onClick: navigateBack, className: "flex items-center gap-2 px-6 py-4 text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white", children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M15.8337 10H4.16699", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M10.0003 15.8334L4.16699 10L10.0003 4.16669", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })] }), _jsx("span", { className: "font-inter text-sm font-semibold", children: "Back" })] })), renderContent()] })] }), contentType?.type === "avatarEditor" && (_jsx("button", { onClick: () => setB3ModalOpen(false), className: "fixed right-5 top-5 z-[100] cursor-pointer text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white", children: _jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M18 6L6 18M6 6L18 18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }))] }));
|
|
152
144
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ThirdwebClient } from "thirdweb";
|
|
2
|
+
interface IPFSMediaRendererProps {
|
|
3
|
+
/** The source URL - can be IPFS URL (ipfs://...) or HTTP URL */
|
|
4
|
+
src: string | null | undefined;
|
|
5
|
+
/** Alt text for the media */
|
|
6
|
+
alt?: string;
|
|
7
|
+
/** CSS class name */
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Thirdweb client instance (optional, uses default if not provided) */
|
|
10
|
+
client?: ThirdwebClient;
|
|
11
|
+
/** Width of the media */
|
|
12
|
+
width?: string | number;
|
|
13
|
+
/** Height of the media */
|
|
14
|
+
height?: string | number;
|
|
15
|
+
/** Controls property for video/audio */
|
|
16
|
+
controls?: boolean;
|
|
17
|
+
/** Style object */
|
|
18
|
+
style?: React.CSSProperties;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* IPFSMediaRenderer - A wrapper around Thirdweb's MediaRenderer that configures
|
|
22
|
+
* the IPFS gateway URL to use our validated gateway.
|
|
23
|
+
*
|
|
24
|
+
* Features:
|
|
25
|
+
* - Configures MediaRenderer to use cloudflare-ipfs.com gateway
|
|
26
|
+
* - Gateway matches our allowed list in profileDisplay.ts
|
|
27
|
+
* - Provides fallback for missing sources
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <IPFSMediaRenderer
|
|
32
|
+
* src="ipfs://QmX..."
|
|
33
|
+
* alt="Profile Avatar"
|
|
34
|
+
* className="size-14 rounded-full"
|
|
35
|
+
* />
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function IPFSMediaRenderer({ src, alt, className, client, width, height, controls, style, }: IPFSMediaRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { client as defaultClient } from "../../../../shared/utils/thirdweb.js";
|
|
4
|
+
import { MediaRenderer } from "thirdweb/react";
|
|
5
|
+
// Primary IPFS gateway URL - matches our allowed list in profileDisplay.ts
|
|
6
|
+
// Note: MediaRenderer expects the base gateway URL without /ipfs path
|
|
7
|
+
const IPFS_GATEWAY_URL = "https://cloudflare-ipfs.com";
|
|
8
|
+
/**
|
|
9
|
+
* IPFSMediaRenderer - A wrapper around Thirdweb's MediaRenderer that configures
|
|
10
|
+
* the IPFS gateway URL to use our validated gateway.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Configures MediaRenderer to use cloudflare-ipfs.com gateway
|
|
14
|
+
* - Gateway matches our allowed list in profileDisplay.ts
|
|
15
|
+
* - Provides fallback for missing sources
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <IPFSMediaRenderer
|
|
20
|
+
* src="ipfs://QmX..."
|
|
21
|
+
* alt="Profile Avatar"
|
|
22
|
+
* className="size-14 rounded-full"
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function IPFSMediaRenderer({ src, alt = "Media", className, client = defaultClient, width, height, controls, style, }) {
|
|
27
|
+
// If no source, render fallback
|
|
28
|
+
if (!src) {
|
|
29
|
+
return (_jsx("div", { className: className, style: style, "aria-label": alt, children: _jsx("div", { className: "bg-b3-primary-wash flex h-full w-full items-center justify-center", children: _jsx("span", { className: "text-b3-grey font-neue-montreal-semibold text-xs", children: alt.charAt(0).toUpperCase() }) }) }));
|
|
30
|
+
}
|
|
31
|
+
// Convert IPFS URLs to HTTP gateway URLs if needed
|
|
32
|
+
// This handles both ipfs:// URLs and existing HTTP gateway URLs
|
|
33
|
+
return (_jsx(MediaRenderer, { src: src, client: client, alt: alt, className: className, width: width ? width.toString() : undefined, height: height ? height.toString() : undefined, controls: controls, style: style }));
|
|
34
|
+
}
|
|
@@ -4,11 +4,11 @@ import { BankIcon } from "../../../../global-account/react/components/icons/Bank
|
|
|
4
4
|
import { SignOutIcon } from "../../../../global-account/react/components/icons/SignOutIcon.js";
|
|
5
5
|
import { SwapIcon } from "../../../../global-account/react/components/icons/SwapIcon.js";
|
|
6
6
|
import { formatUsername } from "../../../../shared/utils/index.js";
|
|
7
|
-
import { getIpfsUrl } from "../../../../shared/utils/ipfs.js";
|
|
8
7
|
import { Loader2, Pencil } from "lucide-react";
|
|
9
8
|
import { useEffect, useRef, useState } from "react";
|
|
10
9
|
import { useActiveAccount } from "thirdweb/react";
|
|
11
10
|
import { useFirstEOA } from "../../hooks/useFirstEOA.js";
|
|
11
|
+
import { IPFSMediaRenderer } from "../IPFSMediaRenderer/IPFSMediaRenderer.js";
|
|
12
12
|
import { B3TokenIcon, EthereumTokenIcon } from "../TokenIcon.js";
|
|
13
13
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../ui/accordion.js";
|
|
14
14
|
import { TokenBalanceRow } from "./TokenBalanceRow.js";
|
|
@@ -32,7 +32,8 @@ export function BalanceContent({ onLogout, showDeposit = true, showSwap = true }
|
|
|
32
32
|
const [logoutLoading, setLogoutLoading] = useState(false);
|
|
33
33
|
const [openAccordions, setOpenAccordions] = useState([]);
|
|
34
34
|
const hasExpandedRef = useRef(false);
|
|
35
|
-
|
|
35
|
+
// IPFSMediaRenderer will handle IPFS URL conversion and validation
|
|
36
|
+
const avatarSrc = user?.avatar || profile?.avatar;
|
|
36
37
|
const handleEditProfile = () => {
|
|
37
38
|
setB3ModalOpen(true);
|
|
38
39
|
setB3ModalContentType({
|
|
@@ -87,7 +88,7 @@ export function BalanceContent({ onLogout, showDeposit = true, showSwap = true }
|
|
|
87
88
|
setB3ModalOpen(false);
|
|
88
89
|
setLogoutLoading(false);
|
|
89
90
|
};
|
|
90
|
-
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx("div", { className: "flex items-center justify-between", children: _jsxs("div", { className: "global-account-profile flex items-center gap-4", children: [_jsxs("div", { className: "global-account-profile-avatar relative", children: [
|
|
91
|
+
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx("div", { className: "flex items-center justify-between", children: _jsxs("div", { className: "global-account-profile flex items-center gap-4", children: [_jsxs("div", { className: "global-account-profile-avatar relative", children: [_jsx(IPFSMediaRenderer, { src: avatarSrc, alt: "Profile", className: "size-24 rounded-full" }), _jsx("button", { onClick: handleEditProfile, className: "bg-b3-grey border-b3-background hover:bg-b3-grey/80 absolute -bottom-1 -right-1 flex size-8 items-center justify-center rounded-full border-4 transition-colors", children: _jsx(Pencil, { size: 16, className: "text-b3-background" }) })] }), _jsxs("div", { className: "global-account-profile-info", children: [_jsx("h2", { className: "text-b3-grey text-xl font-semibold", children: user?.username || profile?.displayName || formatUsername(profile?.name || "") }), _jsxs("div", { className: "address-button border-b3-line bg-b3-line/20 hover:bg-b3-line/40 flex w-fit items-center gap-2 rounded-full border px-3 py-1 transition-colors", children: [_jsx("span", { className: "text-b3-foreground-muted font-mono text-xs", children: centerTruncate(account?.address || "", 6) }), _jsx(CopyToClipboard, { text: account?.address || "" })] })] })] }) }), (showDeposit || showSwap) && (_jsxs("div", { className: "grid grid-cols-2 gap-3", children: [showDeposit && (_jsxs(Button, { className: "manage-account-deposit bg-b3-primary-wash hover:bg-b3-primary-wash/70 h-[84px] w-full flex-col items-start gap-2 rounded-2xl", onClick: () => {
|
|
91
92
|
setB3ModalOpen(true);
|
|
92
93
|
setB3ModalContentType({
|
|
93
94
|
type: "anySpend",
|
|
@@ -6,7 +6,7 @@ import { cn } from "../../../../shared/utils/index.js";
|
|
|
6
6
|
const SendIcon = () => (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: _jsx("path", { d: "M17.9958 1.21467C17.666 1.10449 17.359 1.18417 17.2063 1.22851C17.04 1.27683 16.8417 1.35425 16.6428 1.43191L2.51187 6.94641C2.28951 7.03315 2.07413 7.11717 1.9079 7.19936C1.76427 7.27038 1.45899 7.42845 1.28859 7.75646C1.10067 8.11818 1.10092 8.54881 1.28927 8.9103C1.46006 9.23811 1.76552 9.39582 1.90924 9.46667C2.07556 9.54867 2.29099 9.63241 2.51345 9.71889L6.4468 11.2485C6.74023 11.3626 6.88695 11.4197 7.03291 11.4232C7.16193 11.4263 7.2899 11.3994 7.40674 11.3446C7.53893 11.2826 7.65024 11.1713 7.87287 10.9487L11.9107 6.91083C12.2361 6.58539 12.7638 6.58539 13.0892 6.91083C13.4147 7.23626 13.4147 7.7639 13.0892 8.08934L9.05138 12.1272C8.82875 12.3498 8.71744 12.4611 8.65545 12.5933C8.60065 12.7101 8.57374 12.8381 8.57684 12.9671C8.58035 13.1131 8.6374 13.2598 8.75152 13.5532L10.2811 17.4865C10.3676 17.709 10.4514 17.9245 10.5334 18.0908C10.6042 18.2345 10.7619 18.54 11.0897 18.7108C11.4512 18.8991 11.8819 18.8994 12.2436 18.7115C12.5716 18.541 12.7297 18.2358 12.8007 18.0921C12.8829 17.9259 12.9669 17.7106 13.0536 17.4882L18.5681 3.35726C18.6458 3.15833 18.7232 2.96007 18.7715 2.79371C18.8159 2.64105 18.8955 2.334 18.7854 2.00419C18.6609 1.63157 18.3685 1.33915 17.9958 1.21467Z", fill: "#0C68E9" }) }));
|
|
7
7
|
const BuyIcon = () => (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [_jsx("path", { d: "M9.99998 8.75C9.30962 8.75 8.74998 9.30964 8.74998 10C8.74998 10.6904 9.30962 11.25 9.99998 11.25C10.6903 11.25 11.25 10.6904 11.25 10C11.25 9.30964 10.6903 8.75 9.99998 8.75Z", fill: "#0C68E9" }), _jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M14.1666 2.5C12.3642 2.5 10.8013 2.95205 9.44241 3.37018L9.18862 3.44844C7.90485 3.84476 6.86213 4.16667 5.83332 4.16667C5.09512 4.16667 4.43564 4.08819 3.88041 3.98151L3.85421 3.97647C3.44746 3.89831 3.10585 3.83267 2.84038 3.79758C2.60748 3.76679 2.26236 3.7302 1.94675 3.84126C1.78134 3.89947 1.60242 3.98249 1.43327 4.12224C1.26411 4.26199 1.14882 4.42202 1.06045 4.57348C0.898119 4.85168 0.864329 5.167 0.849347 5.3925C0.833297 5.63407 0.833306 5.94132 0.833317 6.29588L0.833315 15.2922C0.833263 15.4565 0.833201 15.6516 0.932434 15.9263C0.981719 16.0628 1.06288 16.1945 1.10974 16.2664C1.15659 16.3383 1.24436 16.4657 1.34932 16.5659C1.41718 16.6306 1.49456 16.6992 1.58722 16.76C1.67809 16.8196 1.76087 16.8559 1.79699 16.8717L1.80356 16.8746C2.48669 17.1759 3.71203 17.5 5.83332 17.5C7.63579 17.5 9.19862 17.0479 10.5576 16.6298L10.8113 16.5516C12.0951 16.1552 13.1378 15.8333 14.1666 15.8333C14.9048 15.8333 15.5643 15.9118 16.1196 16.0185L16.1458 16.0235C16.5525 16.1017 16.8941 16.1673 17.1596 16.2024C17.3925 16.2332 17.7376 16.2698 18.0532 16.1587C18.2186 16.1005 18.3975 16.0175 18.5667 15.8778C18.7359 15.738 18.8511 15.578 18.9395 15.4265C19.1018 15.1483 19.1356 14.833 19.1506 14.6075C19.1667 14.3659 19.1667 14.0587 19.1666 13.7041L19.1667 4.70776C19.1667 4.54347 19.1668 4.34839 19.0675 4.07365C19.0182 3.9372 18.9371 3.80547 18.8902 3.73359C18.8434 3.6617 18.7556 3.53428 18.6507 3.43412C18.5828 3.36935 18.5054 3.30084 18.4127 3.24003C18.3219 3.1804 18.2391 3.14413 18.203 3.1283L18.1964 3.12542C17.5133 2.82409 16.2879 2.5 14.1666 2.5ZM15.8333 7.5C15.8333 7.03976 15.4602 6.66667 15 6.66667C14.5397 6.66667 14.1666 7.03976 14.1666 7.5V10.8333C14.1666 11.2936 14.5397 11.6667 15 11.6667C15.4602 11.6667 15.8333 11.2936 15.8333 10.8333V7.5ZM7.08331 10C7.08331 8.38917 8.38915 7.08333 9.99998 7.08333C11.6108 7.08333 12.9166 8.38917 12.9166 10C12.9166 11.6108 11.6108 12.9167 9.99998 12.9167C8.38915 12.9167 7.08331 11.6108 7.08331 10ZM4.99998 8.33333C5.46022 8.33333 5.83331 8.70643 5.83331 9.16667V12.5C5.83331 12.9602 5.46022 13.3333 4.99998 13.3333C4.53974 13.3333 4.16665 12.9602 4.16665 12.5V9.16667C4.16665 8.70643 4.53974 8.33333 4.99998 8.33333Z", fill: "#0C68E9" })] }));
|
|
8
8
|
const HomeActionButton = ({ customClass, icon, label, onClick, }) => {
|
|
9
|
-
return (_jsxs(Button, { className: cn("border-b3-line hover:border-b3-primary-blue flex h-[84px] w-full flex-col items-center justify-center gap-2 rounded-2xl border-[1.5px] bg-[#FAFAFA] hover:bg-[#FAFAFA]", customClass), onClick: onClick, children: [icon, _jsx("div", { className: "text-b3-grey font-neue-montreal-semibold", children: label })] }));
|
|
9
|
+
return (_jsxs(Button, { className: cn("border-b3-line hover:border-b3-primary-blue shadow-xs flex h-[84px] w-full flex-col items-center justify-center gap-2 rounded-2xl border-[1.5px] bg-[#FAFAFA] hover:bg-[#FAFAFA]", customClass), onClick: onClick, children: [icon, _jsx("div", { className: "text-b3-grey font-neue-montreal-semibold", children: label })] }));
|
|
10
10
|
};
|
|
11
11
|
const HomeActions = ({ showDeposit, showSwap }) => {
|
|
12
12
|
const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen);
|
|
@@ -27,5 +27,5 @@ export function ManageAccount({ onLogout, onSwap: _onSwap, onDeposit: _onDeposit
|
|
|
27
27
|
else if (["home", "tokens", "nfts", "apps", "settings"].includes(tab)) {
|
|
28
28
|
setActiveTab?.(tab);
|
|
29
29
|
}
|
|
30
|
-
}, children: [_jsxs("div", { className: "p-0", children: [_jsx(TabsContentPrimitive, { value: "home", className: "
|
|
30
|
+
}, children: [_jsxs("div", { className: "p-0", children: [_jsx(TabsContentPrimitive, { value: "home", className: "m-0 p-0 pb-2", children: _jsx(HomeContent, { showDeposit: showDeposit, showSwap: showSwap }) }), _jsx(TabsContentPrimitive, { value: "swap", className: "hidden" }), _jsx(TabsContentPrimitive, { value: "settings", className: "m-0 p-0 pb-2", children: _jsx(SettingsContent, { partnerId: partnerId, onLogout: onLogout, chain: chain }) })] }), _jsx(BottomNavigation, {})] }) }));
|
|
31
31
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useAccountWallet, useB3, useModalStore, useProfile, useSimBalance } from "../../../../global-account/react/index.js";
|
|
3
3
|
import { formatUsername } from "../../../../shared/utils/index.js";
|
|
4
|
-
import { getIpfsUrl } from "../../../../shared/utils/ipfs.js";
|
|
5
4
|
import { formatDisplayNumber } from "../../../../shared/utils/number.js";
|
|
6
5
|
import { Pencil } from "lucide-react";
|
|
7
6
|
import { useMemo } from "react";
|
|
8
7
|
import { useActiveAccount } from "thirdweb/react";
|
|
9
8
|
import { useFirstEOA } from "../../hooks/useFirstEOA.js";
|
|
9
|
+
import { IPFSMediaRenderer } from "../IPFSMediaRenderer/IPFSMediaRenderer.js";
|
|
10
10
|
const ProfileSection = () => {
|
|
11
11
|
const account = useActiveAccount();
|
|
12
12
|
const { address: eoaAddress } = useFirstEOA();
|
|
@@ -26,7 +26,6 @@ const ProfileSection = () => {
|
|
|
26
26
|
return 0;
|
|
27
27
|
return simBalance.balances.reduce((sum, token) => sum + (token.value_usd || 0), 0);
|
|
28
28
|
}, [simBalance]);
|
|
29
|
-
const avatarUrl = user?.avatar ? getIpfsUrl(user?.avatar) : profile?.avatar;
|
|
30
29
|
const handleEditAvatar = () => {
|
|
31
30
|
setB3ModalOpen(true);
|
|
32
31
|
setB3ModalContentType({
|
|
@@ -37,6 +36,10 @@ const ProfileSection = () => {
|
|
|
37
36
|
},
|
|
38
37
|
});
|
|
39
38
|
};
|
|
40
|
-
|
|
39
|
+
// IPFSMediaRenderer will handle IPFS URL conversion and validation
|
|
40
|
+
const avatarSrc = user?.avatar || profile?.avatar;
|
|
41
|
+
// Get current username - prioritize user.username, fallback to profile data
|
|
42
|
+
const currentUsername = user?.username || profile?.displayName || formatUsername(profile?.name || "");
|
|
43
|
+
return (_jsx("div", { className: "flex items-center justify-between px-5 py-6", children: _jsxs("div", { className: "global-account-profile flex items-center gap-4", children: [_jsxs("div", { className: "global-account-profile-avatar relative", children: [_jsx(IPFSMediaRenderer, { src: avatarSrc, alt: "Profile Avatar", className: "size-14 rounded-full" }), _jsx("button", { onClick: handleEditAvatar, className: "border-b3-background hover:bg-b3-grey/80 absolute -bottom-1 -right-1 flex size-6 items-center justify-center rounded-full border-4 bg-[#a0a0ab] transition-colors", children: _jsx(Pencil, { size: 10, className: "text-b3-background" }) })] }), _jsxs("div", { className: "global-account-profile-info flex flex-col gap-1", children: [_jsxs("h2", { className: "text-b3-grey font-neue-montreal-semibold flex h-[38px] items-center gap-1 text-xl", children: [_jsx("div", { className: "text-b3-foreground-muted", children: " $" }), _jsx("div", { className: "text-[30px]", children: formatDisplayNumber(totalBalanceUsd, { fractionDigits: 2 }) })] }), _jsx("div", { className: "font-neue-montreal-semibold text-base leading-none text-[#0B57C2]", children: currentUsername })] })] }) }));
|
|
41
44
|
};
|
|
42
45
|
export default ProfileSection;
|
|
@@ -1,22 +1,40 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import app from "../../../../global-account/app.js";
|
|
2
3
|
import { useB3, useModalStore, useProfile } from "../../../../global-account/react/index.js";
|
|
3
4
|
import { formatUsername } from "../../../../shared/utils/index.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { Check, Loader2, Pencil, X } from "lucide-react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
import { toast } from "sonner";
|
|
6
8
|
import { useActiveAccount } from "thirdweb/react";
|
|
7
9
|
import { useFirstEOA } from "../../hooks/useFirstEOA.js";
|
|
10
|
+
import { IPFSMediaRenderer } from "../IPFSMediaRenderer/IPFSMediaRenderer.js";
|
|
8
11
|
const SettingsProfileCard = () => {
|
|
9
12
|
const account = useActiveAccount();
|
|
10
13
|
const { address: eoaAddress } = useFirstEOA();
|
|
11
|
-
const { data: profile } = useProfile({
|
|
14
|
+
const { data: profile, refetch: refreshProfile } = useProfile({
|
|
12
15
|
address: eoaAddress || account?.address,
|
|
13
16
|
fresh: true,
|
|
14
17
|
});
|
|
15
|
-
const { user } = useB3();
|
|
18
|
+
const { user, setUser } = useB3();
|
|
16
19
|
const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen);
|
|
17
20
|
const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
|
|
18
21
|
const navigateBack = useModalStore(state => state.navigateBack);
|
|
19
|
-
|
|
22
|
+
// State for inline username editing
|
|
23
|
+
const [isEditingUsername, setIsEditingUsername] = useState(false);
|
|
24
|
+
const [editedUsername, setEditedUsername] = useState("");
|
|
25
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
26
|
+
const inputRef = useRef(null);
|
|
27
|
+
// IPFSMediaRenderer will handle IPFS URL conversion and validation
|
|
28
|
+
const avatarSrc = user?.avatar || profile?.avatar;
|
|
29
|
+
// Get current username - prioritize user.username, fallback to profile data
|
|
30
|
+
const currentUsername = user?.username || profile?.displayName || formatUsername(profile?.name || "");
|
|
31
|
+
// Focus input when entering edit mode
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (isEditingUsername && inputRef.current) {
|
|
34
|
+
inputRef.current.focus();
|
|
35
|
+
inputRef.current.select();
|
|
36
|
+
}
|
|
37
|
+
}, [isEditingUsername]);
|
|
20
38
|
const handleEditAvatar = () => {
|
|
21
39
|
setB3ModalOpen(true);
|
|
22
40
|
setB3ModalContentType({
|
|
@@ -28,9 +46,56 @@ const SettingsProfileCard = () => {
|
|
|
28
46
|
});
|
|
29
47
|
};
|
|
30
48
|
const handleEditUsername = () => {
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
setEditedUsername(currentUsername || "");
|
|
50
|
+
setIsEditingUsername(true);
|
|
33
51
|
};
|
|
34
|
-
|
|
52
|
+
const handleCancelEdit = () => {
|
|
53
|
+
setIsEditingUsername(false);
|
|
54
|
+
setEditedUsername("");
|
|
55
|
+
};
|
|
56
|
+
const handleSaveUsername = async () => {
|
|
57
|
+
if (!editedUsername.trim()) {
|
|
58
|
+
toast.error("Username cannot be empty");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (editedUsername === currentUsername) {
|
|
62
|
+
// No change, just exit edit mode
|
|
63
|
+
handleCancelEdit();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setIsSaving(true);
|
|
67
|
+
try {
|
|
68
|
+
const updatedUser = await app.service("users").registerUsername({ username: editedUsername.trim() },
|
|
69
|
+
// @ts-expect-error - our typed client is expecting context even though it's set elsewhere
|
|
70
|
+
{});
|
|
71
|
+
// Update user state - registerUsername returns an array with single user
|
|
72
|
+
setUser(Array.isArray(updatedUser) ? updatedUser[0] : updatedUser);
|
|
73
|
+
// Refresh profile to get updated data
|
|
74
|
+
await refreshProfile();
|
|
75
|
+
toast.success("Username updated successfully!");
|
|
76
|
+
setIsEditingUsername(false);
|
|
77
|
+
setEditedUsername("");
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error("Error updating username:", error);
|
|
81
|
+
toast.error("Failed to update username. Please try again.");
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
setIsSaving(false);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const handleKeyDown = (e) => {
|
|
88
|
+
if (e.key === "Enter") {
|
|
89
|
+
handleSaveUsername();
|
|
90
|
+
}
|
|
91
|
+
else if (e.key === "Escape") {
|
|
92
|
+
handleCancelEdit();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
return (_jsxs("div", { className: "flex w-full items-center gap-3", children: [_jsxs("div", { className: "relative shrink-0", children: [_jsx(IPFSMediaRenderer, { src: avatarSrc, alt: "Profile", className: "border-black/8 size-14 rounded-full border object-cover" }), _jsx("button", { onClick: handleEditAvatar, className: "absolute -bottom-0.5 -right-0.5 flex size-[18px] items-center justify-center rounded-full border-[1.5px] border-white bg-[#a0a0ab] transition-colors hover:bg-[#a0a0ab]/80", "aria-label": "Edit avatar", children: _jsx(Pencil, { size: 10, className: "text-white", strokeWidth: 2.5 }) })] }), _jsx("div", { className: "flex shrink-0 flex-col gap-1", children: isEditingUsername ? (
|
|
96
|
+
/* Edit mode - inline input */
|
|
97
|
+
_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { ref: inputRef, type: "text", value: editedUsername, onChange: e => setEditedUsername(e.target.value), onKeyDown: handleKeyDown, disabled: isSaving, className: "border-b3-line bg-b3-background text-b3-grey placeholder:text-b3-foreground-muted font-neue-montreal-semibold focus:border-b3-primary-blue w-full rounded-md border px-2 py-1 text-lg leading-none transition-colors focus:outline-none disabled:opacity-50", placeholder: "Enter username" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { onClick: handleSaveUsername, disabled: isSaving, className: "text-b3-primary-blue hover:text-b3-primary-blue/80 flex items-center justify-center rounded-md p-1 transition-colors disabled:opacity-50", "aria-label": "Save username", children: isSaving ? _jsx(Loader2, { size: 18, className: "animate-spin" }) : _jsx(Check, { size: 18, strokeWidth: 2.5 }) }), _jsx("button", { onClick: handleCancelEdit, disabled: isSaving, className: "text-b3-foreground-muted hover:text-b3-grey flex items-center justify-center rounded-md p-1 transition-colors disabled:opacity-50", "aria-label": "Cancel editing", children: _jsx(X, { size: 18, strokeWidth: 2.5 }) })] })] })) : (
|
|
98
|
+
/* Display mode */
|
|
99
|
+
_jsxs(_Fragment, { children: [_jsx("div", { className: "flex items-center gap-1", children: _jsx("p", { className: "font-neue-montreal-semibold text-lg leading-none text-[#0B57C2]", children: currentUsername }) }), _jsx("button", { onClick: handleEditUsername, className: "flex items-center justify-center gap-1 text-left transition-opacity hover:opacity-80", children: _jsx("p", { className: "font-inter text-sm font-semibold leading-5 text-[#51525C]", children: "Edit Username" }) })] })) })] }));
|
|
35
100
|
};
|
|
36
101
|
export default SettingsProfileCard;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { SignInWithB3, StyleRoot, useAccountWallet, useAuthentication, useB3, useIsMobile, } from "../../../../global-account/react/index.js";
|
|
2
|
+
import { IPFSMediaRenderer, SignInWithB3, StyleRoot, useAccountWallet, useAuthentication, useB3, useIsMobile, } from "../../../../global-account/react/index.js";
|
|
3
3
|
import Icon from "../../../../global-account/react/components/custom/Icon.js";
|
|
4
4
|
import { ecosystemWalletId } from "../../../../shared/constants/index.js";
|
|
5
5
|
import { cn, truncateAddress } from "../../../../shared/utils/index.js";
|
|
6
6
|
import { Menu, MenuButton, MenuItems, Transition } from "@headlessui/react";
|
|
7
7
|
import { useEffect } from "react";
|
|
8
8
|
import { useConnectedWallets, useSetActiveWallet, useWalletInfo } from "thirdweb/react";
|
|
9
|
+
import { useAccountWalletImage } from "../../hooks/useAccountWallet.js";
|
|
9
10
|
import { ManageAccountButton } from "../custom/ManageAccountButton.js";
|
|
10
11
|
export function SignIn(props) {
|
|
11
12
|
const { className } = props;
|
|
@@ -34,8 +35,9 @@ export function SignIn(props) {
|
|
|
34
35
|
setActiveWallet(connectedEOAWallet);
|
|
35
36
|
}
|
|
36
37
|
}, [connectedEOAWallet, isActiveEOAWallet, setActiveWallet, automaticallySetFirstEoa]);
|
|
38
|
+
const walletImage = useAccountWalletImage();
|
|
37
39
|
// Desktop version - original dropdown menu
|
|
38
|
-
return (_jsx(StyleRoot, { children: _jsx(Menu, { className: `relative flex items-center ${className || ""}`, as: "div", children: globalAddress ? (_jsxs(_Fragment, { children: [_jsxs(MenuButton, { className: "bg-b3-react-background group flex h-10 items-center gap-1 rounded-xl px-3", children: [!!
|
|
40
|
+
return (_jsx(StyleRoot, { children: _jsx(Menu, { className: `relative flex items-center ${className || ""}`, as: "div", children: globalAddress ? (_jsxs(_Fragment, { children: [_jsxs(MenuButton, { className: "bg-b3-react-background group flex h-10 items-center gap-1 rounded-xl px-3 focus:outline-none", children: [!!walletImage && (_jsx(IPFSMediaRenderer, { src: walletImage, alt: "Wallet Image", className: "bg-b3-react-primary h-6 w-6 rounded-full object-cover opacity-100" })), _jsx("div", { className: "text-as-primary", children: ensName ? ensName : truncateAddress(globalAddress) })] }), _jsx(Transition, { enter: "duration-200 ease-out", enterFrom: "scale-95 opacity-0", enterTo: "scale-100 opacity-100", leave: "duration-300 ease-out", leaveFrom: "scale-100 opacity-100", leaveTo: "scale-95 opacity-0", children: _jsx(MenuItems, { className: "b3-root absolute -right-4 top-full min-w-64 rounded-2xl border focus:outline-none lg:right-0", modal: false,
|
|
39
41
|
// TODO: Figure out why setting anchor on mobile causes z-index issues where it appears under elements
|
|
40
42
|
anchor: isMobile ? "top end" : undefined, children: _jsxs("div", { className: "bg-b3-react-background", children: [connectedEOAWallet ? (_jsxs("div", { className: cn("border-b3-react-subtle bg-b3-react-background flex cursor-pointer items-center justify-between rounded-xl p-3"), onClick: () => handleSetActiveAccount(connectedEOAWallet?.id), children: [_jsxs("div", { className: "flex items-center", children: [_jsx("img", { className: "bg-b3-react-primary h-16 w-16 rounded-full opacity-100", src: eoaWalletIcon, alt: connectedEOAWallet?.id }), _jsxs("div", { className: "ml-4 grow", children: [ensName && _jsx("div", { children: ensName }), _jsx("div", { children: truncateAddress(globalAddress) }), _jsx("div", { children: walletInfo?.name })] })] }), isActiveEOAWallet && _jsx(Icon, { className: "fill-b3-react-primary", name: "check" })] })) : (connectedSmartWallet && (_jsxs("div", { className: cn("mb-2 flex cursor-pointer items-center justify-between rounded-xl p-3", isActiveSmartWallet
|
|
41
43
|
? "bg-b3-react-background"
|
|
@@ -15,8 +15,7 @@ export { getConnectOptionsFromStrategy, isWalletType, type AllowedStrategy } fro
|
|
|
15
15
|
export { ManageAccount } from "./ManageAccount/ManageAccount";
|
|
16
16
|
export { Deposit } from "./Deposit/Deposit";
|
|
17
17
|
export { Send } from "./Send/Send";
|
|
18
|
-
export {
|
|
19
|
-
export { ProfileEditor } from "./ProfileEditor/ProfileEditor";
|
|
18
|
+
export { IPFSMediaRenderer } from "./IPFSMediaRenderer/IPFSMediaRenderer";
|
|
20
19
|
export { RequestPermissions } from "./RequestPermissions/RequestPermissions";
|
|
21
20
|
export { RequestPermissionsButton } from "./RequestPermissions/RequestPermissionsButton";
|
|
22
21
|
export { AccountAssets } from "./AccountAssets/AccountAssets";
|