@b3dotfun/sdk 0.0.63 → 0.0.64-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/global-account/react/components/B3DynamicModal.js +7 -3
- package/dist/cjs/global-account/react/components/ManageAccount/BalanceContent.js +3 -3
- package/dist/cjs/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +6 -0
- package/dist/cjs/global-account/react/components/ProfileEditor/ProfileEditor.js +141 -0
- package/dist/cjs/global-account/react/components/index.d.ts +2 -0
- package/dist/cjs/global-account/react/components/index.js +7 -2
- package/dist/cjs/global-account/react/hooks/useAuthentication.js +0 -11
- package/dist/cjs/global-account/react/stores/useModalStore.d.ts +7 -1
- package/dist/cjs/global-account/react/utils/profileDisplay.d.ts +6 -0
- package/dist/cjs/global-account/react/utils/profileDisplay.js +60 -4
- package/dist/esm/global-account/react/components/B3DynamicModal.js +7 -3
- package/dist/esm/global-account/react/components/ManageAccount/BalanceContent.js +3 -3
- package/dist/esm/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +6 -0
- package/dist/esm/global-account/react/components/ProfileEditor/ProfileEditor.js +135 -0
- package/dist/esm/global-account/react/components/index.d.ts +2 -0
- package/dist/esm/global-account/react/components/index.js +3 -0
- package/dist/esm/global-account/react/hooks/useAuthentication.js +0 -11
- package/dist/esm/global-account/react/stores/useModalStore.d.ts +7 -1
- package/dist/esm/global-account/react/utils/profileDisplay.d.ts +6 -0
- package/dist/esm/global-account/react/utils/profileDisplay.js +59 -4
- package/dist/types/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +6 -0
- package/dist/types/global-account/react/components/index.d.ts +2 -0
- package/dist/types/global-account/react/stores/useModalStore.d.ts +7 -1
- package/dist/types/global-account/react/utils/profileDisplay.d.ts +6 -0
- package/package.json +2 -2
- package/src/global-account/react/components/B3DynamicModal.tsx +7 -3
- package/src/global-account/react/components/ManageAccount/BalanceContent.tsx +4 -4
- package/src/global-account/react/components/ProfileEditor/ProfileEditor.tsx +265 -0
- package/src/global-account/react/components/index.ts +4 -0
- package/src/global-account/react/hooks/useAuthentication.ts +0 -12
- package/src/global-account/react/stores/useModalStore.ts +9 -1
- package/src/global-account/react/utils/profileDisplay.ts +67 -4
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Users } from "@b3dotfun/b3-api";
|
|
4
|
+
import app from "@b3dotfun/sdk/global-account/app";
|
|
5
|
+
import { Button, useB3, useProfile } from "@b3dotfun/sdk/global-account/react";
|
|
6
|
+
import { validateImageUrl } from "@b3dotfun/sdk/global-account/react/utils/profileDisplay";
|
|
7
|
+
import { cn } from "@b3dotfun/sdk/shared/utils/cn";
|
|
8
|
+
import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
|
|
9
|
+
import { getIpfsUrl } from "@b3dotfun/sdk/shared/utils/ipfs";
|
|
10
|
+
import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
|
|
11
|
+
import { Check, Loader2, Upload, X } from "lucide-react";
|
|
12
|
+
import { useRef, useState } from "react";
|
|
13
|
+
import { toast } from "sonner";
|
|
14
|
+
import { useActiveAccount } from "thirdweb/react";
|
|
15
|
+
import { upload } from "thirdweb/storage";
|
|
16
|
+
|
|
17
|
+
const debug = debugB3React("ProfileEditor");
|
|
18
|
+
|
|
19
|
+
interface ProfileEditorProps {
|
|
20
|
+
onSuccess?: () => void;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ProfileEditor({ onSuccess, className }: ProfileEditorProps) {
|
|
25
|
+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
26
|
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
27
|
+
const [username, setUsername] = useState<string>("");
|
|
28
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
29
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
30
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
31
|
+
const { user, setUser } = useB3();
|
|
32
|
+
|
|
33
|
+
const account = useActiveAccount();
|
|
34
|
+
const { data: profile, refetch: refreshProfile } = useProfile({
|
|
35
|
+
address: account?.address,
|
|
36
|
+
fresh: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const rawAvatarUrl = user?.avatar ? getIpfsUrl(user?.avatar) : profile?.avatar;
|
|
40
|
+
const avatarUrl = validateImageUrl(rawAvatarUrl);
|
|
41
|
+
const safePreviewUrl = validateImageUrl(previewUrl);
|
|
42
|
+
const hasAvatar = !!avatarUrl;
|
|
43
|
+
const currentUsername = user?.username || "";
|
|
44
|
+
|
|
45
|
+
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
46
|
+
const file = event.target.files?.[0];
|
|
47
|
+
if (file) {
|
|
48
|
+
// Validate file type
|
|
49
|
+
if (!file.type.startsWith("image/")) {
|
|
50
|
+
toast.error("Please select an image file");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate file size (max 5MB)
|
|
55
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
56
|
+
toast.error("File size must be less than 5MB");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setSelectedFile(file);
|
|
61
|
+
|
|
62
|
+
// Create preview URL
|
|
63
|
+
const url = URL.createObjectURL(file);
|
|
64
|
+
setPreviewUrl(url);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleRemoveFile = () => {
|
|
69
|
+
setSelectedFile(null);
|
|
70
|
+
if (previewUrl) {
|
|
71
|
+
URL.revokeObjectURL(previewUrl);
|
|
72
|
+
setPreviewUrl(null);
|
|
73
|
+
}
|
|
74
|
+
if (fileInputRef.current) {
|
|
75
|
+
fileInputRef.current.value = "";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleSave = async () => {
|
|
80
|
+
// Check if there are any changes
|
|
81
|
+
const hasAvatarChange = selectedFile !== null;
|
|
82
|
+
const hasUsernameChange = username.trim() !== "" && username !== currentUsername;
|
|
83
|
+
|
|
84
|
+
if (!hasAvatarChange && !hasUsernameChange) {
|
|
85
|
+
toast.error("Please make at least one change");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setIsSaving(true);
|
|
90
|
+
try {
|
|
91
|
+
let ipfsUrl: string | undefined;
|
|
92
|
+
|
|
93
|
+
// Upload avatar if selected
|
|
94
|
+
if (hasAvatarChange && selectedFile) {
|
|
95
|
+
debug("Starting upload to IPFS", selectedFile);
|
|
96
|
+
setIsUploading(true);
|
|
97
|
+
|
|
98
|
+
ipfsUrl = await upload({
|
|
99
|
+
client,
|
|
100
|
+
files: [selectedFile],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
debug("Upload successful", ipfsUrl);
|
|
104
|
+
setIsUploading(false);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Update user profile
|
|
108
|
+
let updatedUser = user as Users | undefined;
|
|
109
|
+
|
|
110
|
+
// If both avatar and username need updating, do them sequentially
|
|
111
|
+
// Update avatar first if uploaded
|
|
112
|
+
if (ipfsUrl) {
|
|
113
|
+
// @ts-expect-error this resolved fine, look into why expect-error needed
|
|
114
|
+
updatedUser = await app.service("users").setAvatar(
|
|
115
|
+
{
|
|
116
|
+
avatar: ipfsUrl,
|
|
117
|
+
},
|
|
118
|
+
// @ts-expect-error - our typed client is expecting context even though it's set elsewhere
|
|
119
|
+
{},
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Update username if changed (this will use the updated user from avatar change if both were updated)
|
|
124
|
+
if (hasUsernameChange && user?._id) {
|
|
125
|
+
// @ts-expect-error this resolved fine, look into why expect-error needed
|
|
126
|
+
updatedUser = await app.service("users").registerUsername(
|
|
127
|
+
{ username: username },
|
|
128
|
+
// @ts-expect-error - our typed client is expecting context even though it's set elsewhere
|
|
129
|
+
{},
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Update user state
|
|
134
|
+
setUser(updatedUser);
|
|
135
|
+
|
|
136
|
+
// Refresh profile to get updated data
|
|
137
|
+
await refreshProfile();
|
|
138
|
+
|
|
139
|
+
// Show success message
|
|
140
|
+
const changes = [];
|
|
141
|
+
if (hasAvatarChange) changes.push("avatar");
|
|
142
|
+
if (hasUsernameChange) changes.push("username");
|
|
143
|
+
toast.success(`Successfully updated ${changes.join(" and ")}!`);
|
|
144
|
+
|
|
145
|
+
onSuccess?.();
|
|
146
|
+
|
|
147
|
+
// Clean up
|
|
148
|
+
handleRemoveFile();
|
|
149
|
+
setUsername("");
|
|
150
|
+
} catch (error) {
|
|
151
|
+
debug("Error updating profile:", error);
|
|
152
|
+
toast.error("Failed to update profile. Please try again.");
|
|
153
|
+
} finally {
|
|
154
|
+
setIsUploading(false);
|
|
155
|
+
setIsSaving(false);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleFileInputClick = () => {
|
|
160
|
+
fileInputRef.current?.click();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const isLoading = isUploading || isSaving;
|
|
164
|
+
const hasChanges = selectedFile !== null || (username.trim() !== "" && username !== currentUsername);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div className={cn("flex flex-col items-center justify-center space-y-6 p-8", className)}>
|
|
168
|
+
<div className="space-y-2 text-center">
|
|
169
|
+
<h2 className="font-neue-montreal-semibold text-b3-grey text-2xl">Edit Your Profile</h2>
|
|
170
|
+
<p className="text-b3-foreground-muted font-neue-montreal-medium">Update your avatar and username</p>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Avatar Section */}
|
|
174
|
+
<div className="w-full max-w-md space-y-4">
|
|
175
|
+
<div className="space-y-2">
|
|
176
|
+
<label className="text-b3-grey font-neue-montreal-semibold text-sm">Avatar</label>
|
|
177
|
+
|
|
178
|
+
{/* Current/Preview Avatar */}
|
|
179
|
+
<div className="flex justify-center">
|
|
180
|
+
{safePreviewUrl || avatarUrl ? (
|
|
181
|
+
<div className="relative">
|
|
182
|
+
<div className="border-b3-primary-blue h-32 w-32 overflow-hidden rounded-full border-4">
|
|
183
|
+
<img
|
|
184
|
+
src={safePreviewUrl || avatarUrl || ""}
|
|
185
|
+
alt={safePreviewUrl ? "Preview" : "Current avatar"}
|
|
186
|
+
className="h-full w-full object-cover"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
{safePreviewUrl && (
|
|
190
|
+
<button
|
|
191
|
+
onClick={handleRemoveFile}
|
|
192
|
+
className="bg-b3-negative absolute -right-2 -top-2 flex h-8 w-8 items-center justify-center rounded-full text-white transition-colors hover:bg-red-600"
|
|
193
|
+
disabled={isLoading}
|
|
194
|
+
>
|
|
195
|
+
<X size={16} />
|
|
196
|
+
</button>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
<div className="bg-b3-primary-wash h-32 w-32 rounded-full" />
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* Upload Button */}
|
|
205
|
+
{!selectedFile && (
|
|
206
|
+
<Button variant="outline" onClick={handleFileInputClick} disabled={isLoading} className="w-full">
|
|
207
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
208
|
+
{hasAvatar ? "Change Avatar" : "Upload Avatar"}
|
|
209
|
+
</Button>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Hidden file input */}
|
|
213
|
+
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleFileSelect} className="hidden" />
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* Username Section */}
|
|
217
|
+
<div className="space-y-2">
|
|
218
|
+
<label htmlFor="username" className="text-b3-grey font-neue-montreal-semibold text-sm">
|
|
219
|
+
Username
|
|
220
|
+
</label>
|
|
221
|
+
<input
|
|
222
|
+
id="username"
|
|
223
|
+
type="text"
|
|
224
|
+
value={username}
|
|
225
|
+
onChange={e => setUsername(e.target.value)}
|
|
226
|
+
placeholder={currentUsername || "Enter username"}
|
|
227
|
+
className="border-b3-line bg-b3-background text-b3-grey placeholder:text-b3-foreground-muted font-neue-montreal-medium focus:border-b3-primary-blue w-full rounded-lg border px-4 py-3 transition-colors focus:outline-none"
|
|
228
|
+
disabled={isLoading}
|
|
229
|
+
/>
|
|
230
|
+
{currentUsername && (
|
|
231
|
+
<p className="text-b3-foreground-muted font-neue-montreal-medium text-xs">Current: {currentUsername}</p>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Action Buttons */}
|
|
237
|
+
<div className="flex w-full max-w-md gap-3">
|
|
238
|
+
<Button
|
|
239
|
+
onClick={handleSave}
|
|
240
|
+
disabled={isLoading || !hasChanges}
|
|
241
|
+
className="bg-b3-primary-blue hover:bg-b3-primary-blue/90 flex-1 text-white disabled:opacity-50"
|
|
242
|
+
>
|
|
243
|
+
{isLoading ? (
|
|
244
|
+
<>
|
|
245
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
246
|
+
{isUploading ? "Uploading..." : "Saving..."}
|
|
247
|
+
</>
|
|
248
|
+
) : (
|
|
249
|
+
<>
|
|
250
|
+
<Check className="mr-2 h-4 w-4" />
|
|
251
|
+
Save Changes
|
|
252
|
+
</>
|
|
253
|
+
)}
|
|
254
|
+
</Button>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Help Text */}
|
|
258
|
+
<div className="text-b3-foreground-muted font-neue-montreal-medium max-w-md text-center text-xs">
|
|
259
|
+
<p>
|
|
260
|
+
Your avatar will be uploaded to IPFS and stored securely. Make sure you have the rights to use this image.
|
|
261
|
+
</p>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
@@ -19,6 +19,10 @@ export { getConnectOptionsFromStrategy, isWalletType, type AllowedStrategy } fro
|
|
|
19
19
|
// ManageAccount Components
|
|
20
20
|
export { ManageAccount } from "./ManageAccount/ManageAccount";
|
|
21
21
|
|
|
22
|
+
// Profile Components
|
|
23
|
+
export { AvatarEditor } from "./AvatarEditor/AvatarEditor";
|
|
24
|
+
export { ProfileEditor } from "./ProfileEditor/ProfileEditor";
|
|
25
|
+
|
|
22
26
|
// RequestPermissions Components
|
|
23
27
|
export { RequestPermissions } from "./RequestPermissions/RequestPermissions";
|
|
24
28
|
export { RequestPermissionsButton } from "./RequestPermissions/RequestPermissionsButton";
|
|
@@ -48,18 +48,6 @@ export function useAuthentication(partnerId: string) {
|
|
|
48
48
|
const { switchAccount } = useSwitchAccount();
|
|
49
49
|
debug("@@activeWagmiAccount", activeWagmiAccount);
|
|
50
50
|
|
|
51
|
-
// Check localStorage version and clear if not found or mismatched
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (typeof localStorage !== "undefined") {
|
|
54
|
-
const version = localStorage.getItem("version");
|
|
55
|
-
if (version !== "1") {
|
|
56
|
-
debug("@@localStorage:clearing due to version mismatch", { version });
|
|
57
|
-
localStorage.clear();
|
|
58
|
-
localStorage.setItem("version", "1");
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}, []);
|
|
62
|
-
|
|
63
51
|
const wallet = ecosystemWallet(ecosystemWalletId, {
|
|
64
52
|
partnerId: partnerId,
|
|
65
53
|
});
|
|
@@ -373,6 +373,13 @@ export interface AvatarEditorModalProps extends BaseModalProps {
|
|
|
373
373
|
onSuccess?: () => void;
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
export interface ProfileEditorModalProps extends BaseModalProps {
|
|
377
|
+
/** Modal type identifier */
|
|
378
|
+
type: "profileEditor";
|
|
379
|
+
/** Callback function called when profile is successfully updated */
|
|
380
|
+
onSuccess?: () => void;
|
|
381
|
+
}
|
|
382
|
+
|
|
376
383
|
/**
|
|
377
384
|
* Union type of all possible modal content types
|
|
378
385
|
*/
|
|
@@ -395,7 +402,8 @@ export type ModalContentType =
|
|
|
395
402
|
| AnySpendBondKitProps
|
|
396
403
|
| LinkAccountModalProps
|
|
397
404
|
| AnySpendDepositHypeProps
|
|
398
|
-
| AvatarEditorModalProps
|
|
405
|
+
| AvatarEditorModalProps
|
|
406
|
+
| ProfileEditorModalProps;
|
|
399
407
|
// Add other modal types here like: | OtherModalProps | AnotherModalProps
|
|
400
408
|
|
|
401
409
|
/**
|
|
@@ -1,5 +1,68 @@
|
|
|
1
|
+
import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
|
|
1
2
|
import { type Profile } from "thirdweb/wallets";
|
|
2
3
|
|
|
4
|
+
const debug = debugB3React("profileDisplay");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates that an image URL uses an allowed schema
|
|
8
|
+
* @param url - The URL to validate
|
|
9
|
+
* @returns The URL if valid, null otherwise
|
|
10
|
+
*/
|
|
11
|
+
export function validateImageUrl(url: string | null | undefined): string | null {
|
|
12
|
+
if (!url) return null;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// For blob URLs (from createObjectURL)
|
|
16
|
+
if (url.startsWith("blob:")) {
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// For IPFS protocol URLs
|
|
21
|
+
if (url.startsWith("ipfs://")) {
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Parse URL to validate protocol and hostname
|
|
26
|
+
const parsedUrl = new URL(url);
|
|
27
|
+
|
|
28
|
+
// Only allow http and https protocols
|
|
29
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
30
|
+
debug("Rejected unsafe protocol:", parsedUrl.protocol, url);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Whitelist of allowed IPFS gateway hostnames
|
|
35
|
+
const allowedIpfsGateways = [
|
|
36
|
+
"ipfs.io",
|
|
37
|
+
"gateway.pinata.cloud",
|
|
38
|
+
"cloudflare-ipfs.com",
|
|
39
|
+
"dweb.link",
|
|
40
|
+
"nftstorage.link",
|
|
41
|
+
"w3s.link",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Check if hostname matches allowed IPFS gateways
|
|
45
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
46
|
+
const isAllowedIpfsGateway = allowedIpfsGateways.some(gateway => {
|
|
47
|
+
// Exact match or subdomain of the gateway
|
|
48
|
+
return hostname === gateway || hostname.endsWith(`.${gateway}`);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (isAllowedIpfsGateway) {
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// For standard HTTP(S) URLs from trusted sources
|
|
56
|
+
// Add additional hostname validation here if needed
|
|
57
|
+
// For now, allow all HTTP(S) URLs (can be restricted further if needed)
|
|
58
|
+
return url;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// Invalid URL format
|
|
61
|
+
debug("Invalid image URL format:", url, error);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
3
66
|
export interface ExtendedProfileDetails {
|
|
4
67
|
id?: string;
|
|
5
68
|
email?: string;
|
|
@@ -40,7 +103,7 @@ export function getProfileDisplayInfo(profile: ExtendedProfile): ProfileDisplayI
|
|
|
40
103
|
displayInfo = {
|
|
41
104
|
title: details.name || details.username || "Unknown",
|
|
42
105
|
subtitle: details.username ? `@${details.username}` : "X Account",
|
|
43
|
-
imageUrl: details.profileImageUrl
|
|
106
|
+
imageUrl: validateImageUrl(details.profileImageUrl),
|
|
44
107
|
initial: "X",
|
|
45
108
|
type,
|
|
46
109
|
};
|
|
@@ -49,7 +112,7 @@ export function getProfileDisplayInfo(profile: ExtendedProfile): ProfileDisplayI
|
|
|
49
112
|
displayInfo = {
|
|
50
113
|
title: details.name || details.username || "Unknown",
|
|
51
114
|
subtitle: details.username ? `@${details.username}` : "Farcaster Account",
|
|
52
|
-
imageUrl: details.profileImageUrl
|
|
115
|
+
imageUrl: validateImageUrl(details.profileImageUrl),
|
|
53
116
|
initial: "F",
|
|
54
117
|
type,
|
|
55
118
|
};
|
|
@@ -58,7 +121,7 @@ export function getProfileDisplayInfo(profile: ExtendedProfile): ProfileDisplayI
|
|
|
58
121
|
displayInfo = {
|
|
59
122
|
title: details.name || details.email || "Unknown",
|
|
60
123
|
subtitle: details.email || "Google Account",
|
|
61
|
-
imageUrl: details.profileImageUrl
|
|
124
|
+
imageUrl: validateImageUrl(details.profileImageUrl),
|
|
62
125
|
initial: "G",
|
|
63
126
|
type,
|
|
64
127
|
};
|
|
@@ -67,7 +130,7 @@ export function getProfileDisplayInfo(profile: ExtendedProfile): ProfileDisplayI
|
|
|
67
130
|
displayInfo = {
|
|
68
131
|
title: details.username || details.name || "Unknown",
|
|
69
132
|
subtitle: "Discord Account",
|
|
70
|
-
imageUrl: details.profileImageUrl
|
|
133
|
+
imageUrl: validateImageUrl(details.profileImageUrl),
|
|
71
134
|
initial: "D",
|
|
72
135
|
type,
|
|
73
136
|
};
|