@b3dotfun/sdk 0.0.65-test.1 → 0.0.65-test.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.js +5 -3
  2. package/dist/cjs/anyspend/react/components/AnySpendCustom.js +1 -1
  3. package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.js +1 -1
  4. package/dist/cjs/anyspend/react/components/AnyspendDepositHype.js +1 -1
  5. package/dist/cjs/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
  6. package/dist/cjs/anyspend/react/components/common/CryptoPaymentMethod.js +5 -3
  7. package/dist/cjs/anyspend/react/components/common/PanelOnrampPayment.js +1 -1
  8. package/dist/cjs/anyspend/react/hooks/useSigMint.d.ts +1 -1
  9. package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
  10. package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.js +149 -39
  11. package/dist/cjs/global-account/react/components/B3DynamicModal.js +1 -9
  12. package/dist/cjs/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
  13. package/dist/cjs/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.js +37 -0
  14. package/dist/cjs/global-account/react/components/ManageAccount/BalanceContent.js +4 -3
  15. package/dist/cjs/global-account/react/components/ManageAccount/HomeActions.js +1 -1
  16. package/dist/cjs/global-account/react/components/ManageAccount/ManageAccount.js +1 -1
  17. package/dist/cjs/global-account/react/components/ManageAccount/ProfileSection.js +6 -3
  18. package/dist/cjs/global-account/react/components/ManageAccount/SettingsContent.js +1 -1
  19. package/dist/cjs/global-account/react/components/ManageAccount/SettingsProfileCard.js +77 -9
  20. package/dist/cjs/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
  21. package/dist/cjs/global-account/react/components/ModalHeader/ModalHeader.js +2 -2
  22. package/dist/cjs/global-account/react/components/SignInWithB3/SignIn.js +3 -1
  23. package/dist/cjs/global-account/react/components/index.d.ts +1 -2
  24. package/dist/cjs/global-account/react/components/index.js +6 -8
  25. package/dist/cjs/global-account/react/components/ui/drawer.js +1 -1
  26. package/dist/cjs/global-account/react/hooks/useAccountWallet.d.ts +1 -0
  27. package/dist/cjs/global-account/react/hooks/useAccountWallet.js +18 -0
  28. package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +2 -2
  29. package/dist/cjs/global-account/react/hooks/useUserQuery.d.ts +2 -2
  30. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +1 -7
  31. package/dist/cjs/shared/constants/chains/supported.d.ts +1 -1
  32. package/dist/cjs/shared/utils/ipfs.js +10 -3
  33. package/dist/esm/anyspend/react/components/AnySpend.js +5 -3
  34. package/dist/esm/anyspend/react/components/AnySpendCustom.js +1 -1
  35. package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +1 -1
  36. package/dist/esm/anyspend/react/components/AnyspendDepositHype.js +1 -1
  37. package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
  38. package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.js +5 -3
  39. package/dist/esm/anyspend/react/components/common/PanelOnrampPayment.js +1 -1
  40. package/dist/esm/anyspend/react/hooks/useSigMint.d.ts +1 -1
  41. package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
  42. package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.js +151 -41
  43. package/dist/esm/global-account/react/components/B3DynamicModal.js +1 -9
  44. package/dist/esm/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
  45. package/dist/esm/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.js +34 -0
  46. package/dist/esm/global-account/react/components/ManageAccount/BalanceContent.js +4 -3
  47. package/dist/esm/global-account/react/components/ManageAccount/HomeActions.js +1 -1
  48. package/dist/esm/global-account/react/components/ManageAccount/ManageAccount.js +1 -1
  49. package/dist/esm/global-account/react/components/ManageAccount/ProfileSection.js +6 -3
  50. package/dist/esm/global-account/react/components/ManageAccount/SettingsContent.js +1 -1
  51. package/dist/esm/global-account/react/components/ManageAccount/SettingsProfileCard.js +74 -9
  52. package/dist/esm/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
  53. package/dist/esm/global-account/react/components/ModalHeader/ModalHeader.js +2 -2
  54. package/dist/esm/global-account/react/components/SignInWithB3/SignIn.js +4 -2
  55. package/dist/esm/global-account/react/components/index.d.ts +1 -2
  56. package/dist/esm/global-account/react/components/index.js +3 -4
  57. package/dist/esm/global-account/react/components/ui/drawer.js +1 -1
  58. package/dist/esm/global-account/react/hooks/useAccountWallet.d.ts +1 -0
  59. package/dist/esm/global-account/react/hooks/useAccountWallet.js +17 -0
  60. package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +2 -2
  61. package/dist/esm/global-account/react/hooks/useUserQuery.d.ts +2 -2
  62. package/dist/esm/global-account/react/stores/useModalStore.d.ts +1 -7
  63. package/dist/esm/shared/constants/chains/supported.d.ts +1 -1
  64. package/dist/esm/shared/utils/ipfs.js +10 -3
  65. package/dist/styles/index.css +1 -1
  66. package/dist/types/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
  67. package/dist/types/anyspend/react/hooks/useSigMint.d.ts +1 -1
  68. package/dist/types/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
  69. package/dist/types/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
  70. package/dist/types/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
  71. package/dist/types/global-account/react/components/index.d.ts +1 -2
  72. package/dist/types/global-account/react/hooks/useAccountWallet.d.ts +1 -0
  73. package/dist/types/global-account/react/hooks/useAuthentication.d.ts +2 -2
  74. package/dist/types/global-account/react/hooks/useUserQuery.d.ts +2 -2
  75. package/dist/types/global-account/react/stores/useModalStore.d.ts +1 -7
  76. package/dist/types/shared/constants/chains/supported.d.ts +1 -1
  77. package/package.json +2 -1
  78. package/src/anyspend/react/components/AnySpend.tsx +5 -4
  79. package/src/anyspend/react/components/AnySpendCustom.tsx +0 -2
  80. package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +0 -2
  81. package/src/anyspend/react/components/AnyspendDepositHype.tsx +0 -2
  82. package/src/anyspend/react/components/common/CryptoPaymentMethod.tsx +7 -14
  83. package/src/anyspend/react/components/common/PanelOnrampPayment.tsx +1 -1
  84. package/src/global-account/react/components/AvatarEditor/AvatarEditor.tsx +251 -79
  85. package/src/global-account/react/components/B3DynamicModal.tsx +3 -11
  86. package/src/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.tsx +84 -0
  87. package/src/global-account/react/components/ManageAccount/BalanceContent.tsx +4 -7
  88. package/src/global-account/react/components/ManageAccount/HomeActions.tsx +1 -1
  89. package/src/global-account/react/components/ManageAccount/ManageAccount.tsx +2 -2
  90. package/src/global-account/react/components/ManageAccount/ProfileSection.tsx +14 -11
  91. package/src/global-account/react/components/ManageAccount/SettingsContent.tsx +1 -1
  92. package/src/global-account/react/components/ManageAccount/SettingsProfileCard.tsx +129 -23
  93. package/src/global-account/react/components/ModalHeader/ModalHeader.tsx +16 -8
  94. package/src/global-account/react/components/SignInWithB3/SignIn.tsx +11 -7
  95. package/src/global-account/react/components/index.ts +3 -4
  96. package/src/global-account/react/components/ui/drawer.tsx +1 -1
  97. package/src/global-account/react/hooks/useAccountWallet.tsx +26 -0
  98. package/src/global-account/react/stores/useModalStore.ts +1 -9
  99. package/src/shared/utils/ipfs.ts +10 -3
  100. package/dist/cjs/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
  101. package/dist/cjs/global-account/react/components/ProfileEditor/ProfileEditor.js +0 -141
  102. package/dist/esm/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
  103. package/dist/esm/global-account/react/components/ProfileEditor/ProfileEditor.js +0 -135
  104. package/dist/types/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
  105. package/src/global-account/react/components/ProfileEditor/ProfileEditor.tsx +0 -265
@@ -1,7 +1,14 @@
1
+ /**
2
+ * List of IPFS gateways to use for converting ipfs:// URLs to HTTP URLs
3
+ * These gateways must match the allowed list in profileDisplay.ts validateImageUrl()
4
+ */
1
5
  const IPFS_GATEWAYS = [
2
- "https://cloudflare-ipfs.com/ipfs",
3
- "https://ipfs.io/ipfs",
4
- // Can add more gateways as needed
6
+ "https://cloudflare-ipfs.com/ipfs", // Primary gateway - fast and reliable
7
+ "https://ipfs.io/ipfs", // Fallback gateway
8
+ "https://gateway.pinata.cloud/ipfs", // Additional option
9
+ "https://dweb.link/ipfs", // Additional option
10
+ "https://nftstorage.link/ipfs", // Additional option
11
+ "https://w3s.link/ipfs", // Additional option
5
12
  ] as const;
6
13
 
7
14
  /**
@@ -1,6 +0,0 @@
1
- interface ProfileEditorProps {
2
- onSuccess?: () => void;
3
- className?: string;
4
- }
5
- export declare function ProfileEditor({ onSuccess, className }: ProfileEditorProps): import("react/jsx-runtime").JSX.Element;
6
- export {};
@@ -1,141 +0,0 @@
1
- "use strict";
2
- "use client";
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.ProfileEditor = ProfileEditor;
8
- const jsx_runtime_1 = require("react/jsx-runtime");
9
- const app_1 = __importDefault(require("../../../../global-account/app"));
10
- const react_1 = require("../../../../global-account/react");
11
- const profileDisplay_1 = require("../../../../global-account/react/utils/profileDisplay");
12
- const cn_1 = require("../../../../shared/utils/cn");
13
- const debug_1 = require("../../../../shared/utils/debug");
14
- const ipfs_1 = require("../../../../shared/utils/ipfs");
15
- const thirdweb_1 = require("../../../../shared/utils/thirdweb");
16
- const lucide_react_1 = require("lucide-react");
17
- const react_2 = require("react");
18
- const sonner_1 = require("sonner");
19
- const react_3 = require("thirdweb/react");
20
- const storage_1 = require("thirdweb/storage");
21
- const debug = (0, debug_1.debugB3React)("ProfileEditor");
22
- function ProfileEditor({ onSuccess, className }) {
23
- const [selectedFile, setSelectedFile] = (0, react_2.useState)(null);
24
- const [previewUrl, setPreviewUrl] = (0, react_2.useState)(null);
25
- const [username, setUsername] = (0, react_2.useState)("");
26
- const [isUploading, setIsUploading] = (0, react_2.useState)(false);
27
- const [isSaving, setIsSaving] = (0, react_2.useState)(false);
28
- const fileInputRef = (0, react_2.useRef)(null);
29
- const { user, setUser } = (0, react_1.useB3)();
30
- const account = (0, react_3.useActiveAccount)();
31
- const { data: profile, refetch: refreshProfile } = (0, react_1.useProfile)({
32
- address: account?.address,
33
- fresh: true,
34
- });
35
- const rawAvatarUrl = user?.avatar ? (0, ipfs_1.getIpfsUrl)(user?.avatar) : profile?.avatar;
36
- const avatarUrl = (0, profileDisplay_1.validateImageUrl)(rawAvatarUrl);
37
- const safePreviewUrl = (0, profileDisplay_1.validateImageUrl)(previewUrl);
38
- const hasAvatar = !!avatarUrl;
39
- const currentUsername = user?.username || "";
40
- const handleFileSelect = (event) => {
41
- const file = event.target.files?.[0];
42
- if (file) {
43
- // Validate file type
44
- if (!file.type.startsWith("image/")) {
45
- sonner_1.toast.error("Please select an image file");
46
- return;
47
- }
48
- // Validate file size (max 5MB)
49
- if (file.size > 5 * 1024 * 1024) {
50
- sonner_1.toast.error("File size must be less than 5MB");
51
- return;
52
- }
53
- setSelectedFile(file);
54
- // Create preview URL
55
- const url = URL.createObjectURL(file);
56
- setPreviewUrl(url);
57
- }
58
- };
59
- const handleRemoveFile = () => {
60
- setSelectedFile(null);
61
- if (previewUrl) {
62
- URL.revokeObjectURL(previewUrl);
63
- setPreviewUrl(null);
64
- }
65
- if (fileInputRef.current) {
66
- fileInputRef.current.value = "";
67
- }
68
- };
69
- const handleSave = async () => {
70
- // Check if there are any changes
71
- const hasAvatarChange = selectedFile !== null;
72
- const hasUsernameChange = username.trim() !== "" && username !== currentUsername;
73
- if (!hasAvatarChange && !hasUsernameChange) {
74
- sonner_1.toast.error("Please make at least one change");
75
- return;
76
- }
77
- setIsSaving(true);
78
- try {
79
- let ipfsUrl;
80
- // Upload avatar if selected
81
- if (hasAvatarChange && selectedFile) {
82
- debug("Starting upload to IPFS", selectedFile);
83
- setIsUploading(true);
84
- ipfsUrl = await (0, storage_1.upload)({
85
- client: thirdweb_1.client,
86
- files: [selectedFile],
87
- });
88
- debug("Upload successful", ipfsUrl);
89
- setIsUploading(false);
90
- }
91
- // Update user profile
92
- let updatedUser = user;
93
- // If both avatar and username need updating, do them sequentially
94
- // Update avatar first if uploaded
95
- if (ipfsUrl) {
96
- // @ts-expect-error this resolved fine, look into why expect-error needed
97
- updatedUser = await app_1.default.service("users").setAvatar({
98
- avatar: ipfsUrl,
99
- },
100
- // @ts-expect-error - our typed client is expecting context even though it's set elsewhere
101
- {});
102
- }
103
- // Update username if changed (this will use the updated user from avatar change if both were updated)
104
- if (hasUsernameChange && user?._id) {
105
- // @ts-expect-error this resolved fine, look into why expect-error needed
106
- updatedUser = await app_1.default.service("users").registerUsername({ username: username },
107
- // @ts-expect-error - our typed client is expecting context even though it's set elsewhere
108
- {});
109
- }
110
- // Update user state
111
- setUser(updatedUser);
112
- // Refresh profile to get updated data
113
- await refreshProfile();
114
- // Show success message
115
- const changes = [];
116
- if (hasAvatarChange)
117
- changes.push("avatar");
118
- if (hasUsernameChange)
119
- changes.push("username");
120
- sonner_1.toast.success(`Successfully updated ${changes.join(" and ")}!`);
121
- onSuccess?.();
122
- // Clean up
123
- handleRemoveFile();
124
- setUsername("");
125
- }
126
- catch (error) {
127
- debug("Error updating profile:", error);
128
- sonner_1.toast.error("Failed to update profile. Please try again.");
129
- }
130
- finally {
131
- setIsUploading(false);
132
- setIsSaving(false);
133
- }
134
- };
135
- const handleFileInputClick = () => {
136
- fileInputRef.current?.click();
137
- };
138
- const isLoading = isUploading || isSaving;
139
- const hasChanges = selectedFile !== null || (username.trim() !== "" && username !== currentUsername);
140
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex flex-col items-center justify-center space-y-6 p-8", className), children: [(0, jsx_runtime_1.jsxs)("div", { className: "space-y-2 text-center", children: [(0, jsx_runtime_1.jsx)("h2", { className: "font-neue-montreal-semibold text-b3-grey text-2xl", children: "Edit Your Profile" }), (0, jsx_runtime_1.jsx)("p", { className: "text-b3-foreground-muted font-neue-montreal-medium", children: "Update your avatar and username" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "w-full max-w-md space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "space-y-2", children: [(0, jsx_runtime_1.jsx)("label", { className: "text-b3-grey font-neue-montreal-semibold text-sm", children: "Avatar" }), (0, jsx_runtime_1.jsx)("div", { className: "flex justify-center", children: safePreviewUrl || avatarUrl ? ((0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("div", { className: "border-b3-primary-blue h-32 w-32 overflow-hidden rounded-full border-4", children: (0, jsx_runtime_1.jsx)("img", { src: safePreviewUrl || avatarUrl || "", alt: safePreviewUrl ? "Preview" : "Current avatar", className: "h-full w-full object-cover" }) }), safePreviewUrl && ((0, jsx_runtime_1.jsx)("button", { onClick: handleRemoveFile, 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", disabled: isLoading, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16 }) }))] })) : ((0, jsx_runtime_1.jsx)("div", { className: "bg-b3-primary-wash h-32 w-32 rounded-full" })) }), !selectedFile && ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "outline", onClick: handleFileInputClick, disabled: isLoading, className: "w-full", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { className: "mr-2 h-4 w-4" }), hasAvatar ? "Change Avatar" : "Upload Avatar"] })), (0, jsx_runtime_1.jsx)("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleFileSelect, className: "hidden" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "space-y-2", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "username", className: "text-b3-grey font-neue-montreal-semibold text-sm", children: "Username" }), (0, jsx_runtime_1.jsx)("input", { id: "username", type: "text", value: username, onChange: e => setUsername(e.target.value), placeholder: currentUsername || "Enter username", 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", disabled: isLoading }), currentUsername && ((0, jsx_runtime_1.jsxs)("p", { className: "text-b3-foreground-muted font-neue-montreal-medium text-xs", children: ["Current: ", currentUsername] }))] })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex w-full max-w-md gap-3", children: (0, jsx_runtime_1.jsx)(react_1.Button, { onClick: handleSave, disabled: isLoading || !hasChanges, className: "bg-b3-primary-blue hover:bg-b3-primary-blue/90 flex-1 text-white disabled:opacity-50", children: isLoading ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isUploading ? "Uploading..." : "Saving..."] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "mr-2 h-4 w-4" }), "Save Changes"] })) }) }), (0, jsx_runtime_1.jsx)("div", { className: "text-b3-foreground-muted font-neue-montreal-medium max-w-md text-center text-xs", children: (0, jsx_runtime_1.jsx)("p", { children: "Your avatar will be uploaded to IPFS and stored securely. Make sure you have the rights to use this image." }) })] }));
141
- }
@@ -1,6 +0,0 @@
1
- interface ProfileEditorProps {
2
- onSuccess?: () => void;
3
- className?: string;
4
- }
5
- export declare function ProfileEditor({ onSuccess, className }: ProfileEditorProps): import("react/jsx-runtime").JSX.Element;
6
- export {};
@@ -1,135 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import app from "../../../../global-account/app.js";
4
- import { Button, useB3, useProfile } from "../../../../global-account/react/index.js";
5
- import { validateImageUrl } from "../../../../global-account/react/utils/profileDisplay.js";
6
- import { cn } from "../../../../shared/utils/cn.js";
7
- import { debugB3React } from "../../../../shared/utils/debug.js";
8
- import { getIpfsUrl } from "../../../../shared/utils/ipfs.js";
9
- import { client } from "../../../../shared/utils/thirdweb.js";
10
- import { Check, Loader2, Upload, X } from "lucide-react";
11
- import { useRef, useState } from "react";
12
- import { toast } from "sonner";
13
- import { useActiveAccount } from "thirdweb/react";
14
- import { upload } from "thirdweb/storage";
15
- const debug = debugB3React("ProfileEditor");
16
- export function ProfileEditor({ onSuccess, className }) {
17
- const [selectedFile, setSelectedFile] = useState(null);
18
- const [previewUrl, setPreviewUrl] = useState(null);
19
- const [username, setUsername] = useState("");
20
- const [isUploading, setIsUploading] = useState(false);
21
- const [isSaving, setIsSaving] = useState(false);
22
- const fileInputRef = useRef(null);
23
- const { user, setUser } = useB3();
24
- const account = useActiveAccount();
25
- const { data: profile, refetch: refreshProfile } = useProfile({
26
- address: account?.address,
27
- fresh: true,
28
- });
29
- const rawAvatarUrl = user?.avatar ? getIpfsUrl(user?.avatar) : profile?.avatar;
30
- const avatarUrl = validateImageUrl(rawAvatarUrl);
31
- const safePreviewUrl = validateImageUrl(previewUrl);
32
- const hasAvatar = !!avatarUrl;
33
- const currentUsername = user?.username || "";
34
- const handleFileSelect = (event) => {
35
- const file = event.target.files?.[0];
36
- if (file) {
37
- // Validate file type
38
- if (!file.type.startsWith("image/")) {
39
- toast.error("Please select an image file");
40
- return;
41
- }
42
- // Validate file size (max 5MB)
43
- if (file.size > 5 * 1024 * 1024) {
44
- toast.error("File size must be less than 5MB");
45
- return;
46
- }
47
- setSelectedFile(file);
48
- // Create preview URL
49
- const url = URL.createObjectURL(file);
50
- setPreviewUrl(url);
51
- }
52
- };
53
- const handleRemoveFile = () => {
54
- setSelectedFile(null);
55
- if (previewUrl) {
56
- URL.revokeObjectURL(previewUrl);
57
- setPreviewUrl(null);
58
- }
59
- if (fileInputRef.current) {
60
- fileInputRef.current.value = "";
61
- }
62
- };
63
- const handleSave = async () => {
64
- // Check if there are any changes
65
- const hasAvatarChange = selectedFile !== null;
66
- const hasUsernameChange = username.trim() !== "" && username !== currentUsername;
67
- if (!hasAvatarChange && !hasUsernameChange) {
68
- toast.error("Please make at least one change");
69
- return;
70
- }
71
- setIsSaving(true);
72
- try {
73
- let ipfsUrl;
74
- // Upload avatar if selected
75
- if (hasAvatarChange && selectedFile) {
76
- debug("Starting upload to IPFS", selectedFile);
77
- setIsUploading(true);
78
- ipfsUrl = await upload({
79
- client,
80
- files: [selectedFile],
81
- });
82
- debug("Upload successful", ipfsUrl);
83
- setIsUploading(false);
84
- }
85
- // Update user profile
86
- let updatedUser = user;
87
- // If both avatar and username need updating, do them sequentially
88
- // Update avatar first if uploaded
89
- if (ipfsUrl) {
90
- // @ts-expect-error this resolved fine, look into why expect-error needed
91
- updatedUser = await app.service("users").setAvatar({
92
- avatar: ipfsUrl,
93
- },
94
- // @ts-expect-error - our typed client is expecting context even though it's set elsewhere
95
- {});
96
- }
97
- // Update username if changed (this will use the updated user from avatar change if both were updated)
98
- if (hasUsernameChange && user?._id) {
99
- // @ts-expect-error this resolved fine, look into why expect-error needed
100
- updatedUser = await app.service("users").registerUsername({ username: username },
101
- // @ts-expect-error - our typed client is expecting context even though it's set elsewhere
102
- {});
103
- }
104
- // Update user state
105
- setUser(updatedUser);
106
- // Refresh profile to get updated data
107
- await refreshProfile();
108
- // Show success message
109
- const changes = [];
110
- if (hasAvatarChange)
111
- changes.push("avatar");
112
- if (hasUsernameChange)
113
- changes.push("username");
114
- toast.success(`Successfully updated ${changes.join(" and ")}!`);
115
- onSuccess?.();
116
- // Clean up
117
- handleRemoveFile();
118
- setUsername("");
119
- }
120
- catch (error) {
121
- debug("Error updating profile:", error);
122
- toast.error("Failed to update profile. Please try again.");
123
- }
124
- finally {
125
- setIsUploading(false);
126
- setIsSaving(false);
127
- }
128
- };
129
- const handleFileInputClick = () => {
130
- fileInputRef.current?.click();
131
- };
132
- const isLoading = isUploading || isSaving;
133
- const hasChanges = selectedFile !== null || (username.trim() !== "" && username !== currentUsername);
134
- return (_jsxs("div", { className: cn("flex flex-col items-center justify-center space-y-6 p-8", className), children: [_jsxs("div", { className: "space-y-2 text-center", children: [_jsx("h2", { className: "font-neue-montreal-semibold text-b3-grey text-2xl", children: "Edit Your Profile" }), _jsx("p", { className: "text-b3-foreground-muted font-neue-montreal-medium", children: "Update your avatar and username" })] }), _jsxs("div", { className: "w-full max-w-md space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-b3-grey font-neue-montreal-semibold text-sm", children: "Avatar" }), _jsx("div", { className: "flex justify-center", children: safePreviewUrl || avatarUrl ? (_jsxs("div", { className: "relative", children: [_jsx("div", { className: "border-b3-primary-blue h-32 w-32 overflow-hidden rounded-full border-4", children: _jsx("img", { src: safePreviewUrl || avatarUrl || "", alt: safePreviewUrl ? "Preview" : "Current avatar", className: "h-full w-full object-cover" }) }), safePreviewUrl && (_jsx("button", { onClick: handleRemoveFile, 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", disabled: isLoading, children: _jsx(X, { size: 16 }) }))] })) : (_jsx("div", { className: "bg-b3-primary-wash h-32 w-32 rounded-full" })) }), !selectedFile && (_jsxs(Button, { variant: "outline", onClick: handleFileInputClick, disabled: isLoading, className: "w-full", children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), hasAvatar ? "Change Avatar" : "Upload Avatar"] })), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleFileSelect, className: "hidden" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { htmlFor: "username", className: "text-b3-grey font-neue-montreal-semibold text-sm", children: "Username" }), _jsx("input", { id: "username", type: "text", value: username, onChange: e => setUsername(e.target.value), placeholder: currentUsername || "Enter username", 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", disabled: isLoading }), currentUsername && (_jsxs("p", { className: "text-b3-foreground-muted font-neue-montreal-medium text-xs", children: ["Current: ", currentUsername] }))] })] }), _jsx("div", { className: "flex w-full max-w-md gap-3", children: _jsx(Button, { onClick: handleSave, disabled: isLoading || !hasChanges, className: "bg-b3-primary-blue hover:bg-b3-primary-blue/90 flex-1 text-white disabled:opacity-50", children: isLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isUploading ? "Uploading..." : "Saving..."] })) : (_jsxs(_Fragment, { children: [_jsx(Check, { className: "mr-2 h-4 w-4" }), "Save Changes"] })) }) }), _jsx("div", { className: "text-b3-foreground-muted font-neue-montreal-medium max-w-md text-center text-xs", children: _jsx("p", { children: "Your avatar will be uploaded to IPFS and stored securely. Make sure you have the rights to use this image." }) })] }));
135
- }
@@ -1,6 +0,0 @@
1
- interface ProfileEditorProps {
2
- onSuccess?: () => void;
3
- className?: string;
4
- }
5
- export declare function ProfileEditor({ onSuccess, className }: ProfileEditorProps): import("react/jsx-runtime").JSX.Element;
6
- export {};
@@ -1,265 +0,0 @@
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
- }