@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
@@ -23,7 +23,6 @@ import { Deposit } from "./Deposit/Deposit";
23
23
  import { LinkAccount } from "./LinkAccount/LinkAccount";
24
24
  import { LinkNewAccount } from "./LinkAccount/LinkNewAccount";
25
25
  import { ManageAccount } from "./ManageAccount/ManageAccount";
26
- import { ProfileEditor } from "./ProfileEditor/ProfileEditor";
27
26
  import { RequestPermissions } from "./RequestPermissions/RequestPermissions";
28
27
  import { Send } from "./Send/Send";
29
28
  import { SignInWithB3Flow } from "./SignInWithB3/SignInWithB3Flow";
@@ -74,7 +73,6 @@ export function B3DynamicModal() {
74
73
  "avatarEditor",
75
74
  "deposit",
76
75
  "send",
77
- "profileEditor",
78
76
  ];
79
77
 
80
78
  const freestyleTypes = [
@@ -157,8 +155,6 @@ export function B3DynamicModal() {
157
155
  return <Deposit />;
158
156
  case "send":
159
157
  return <Send {...contentType} />;
160
- case "profileEditor":
161
- return <ProfileEditor onSuccess={contentType.onSuccess} />;
162
158
  // Add other modal types here
163
159
  default:
164
160
  return null;
@@ -182,11 +178,7 @@ export function B3DynamicModal() {
182
178
  contentType?.type === "send" ||
183
179
  contentType?.type === "avatarEditor") &&
184
180
  "p-0",
185
- "mx-auto w-full max-w-md sm:max-w-lg", // TODO CHECK THIS
186
- // Remove default width classes for avatar editor and profile editor
187
- contentType?.type === "avatarEditor" || contentType?.type === "profileEditor"
188
- ? "!w-[90vw] !max-w-none" // Use !important to override default styles
189
- : "mx-auto w-full max-w-md sm:max-w-lg",
181
+ "mx-auto w-full max-w-md sm:max-w-lg",
190
182
  )}
191
183
  hideCloseButton={hideCloseButton}
192
184
  >
@@ -194,7 +186,7 @@ export function B3DynamicModal() {
194
186
  <ModalDescription className="sr-only hidden">{contentType?.type || "Modal Body"}</ModalDescription>
195
187
 
196
188
  <div className={cn("no-scrollbar max-h-[90dvh] overflow-auto sm:max-h-[80dvh]")}>
197
- {(!hideCloseButton || contentType?.showBackButton) && (
189
+ {!hideCloseButton && (
198
190
  <button
199
191
  onClick={navigateBack}
200
192
  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"
@@ -221,7 +213,7 @@ export function B3DynamicModal() {
221
213
  {renderContent()}
222
214
  </div>
223
215
  </ModalContent>
224
- {(contentType?.type === "avatarEditor" || contentType?.type === "profileEditor") && (
216
+ {contentType?.type === "avatarEditor" && (
225
217
  <button
226
218
  onClick={() => setB3ModalOpen(false)}
227
219
  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"
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { client as defaultClient } from "@b3dotfun/sdk/shared/utils/thirdweb";
4
+ import type { ThirdwebClient } from "thirdweb";
5
+ import { MediaRenderer } from "thirdweb/react";
6
+
7
+ // Primary IPFS gateway URL - matches our allowed list in profileDisplay.ts
8
+ // Note: MediaRenderer expects the base gateway URL without /ipfs path
9
+ const IPFS_GATEWAY_URL = "https://cloudflare-ipfs.com";
10
+
11
+ interface IPFSMediaRendererProps {
12
+ /** The source URL - can be IPFS URL (ipfs://...) or HTTP URL */
13
+ src: string | null | undefined;
14
+ /** Alt text for the media */
15
+ alt?: string;
16
+ /** CSS class name */
17
+ className?: string;
18
+ /** Thirdweb client instance (optional, uses default if not provided) */
19
+ client?: ThirdwebClient;
20
+ /** Width of the media */
21
+ width?: string | number;
22
+ /** Height of the media */
23
+ height?: string | number;
24
+ /** Controls property for video/audio */
25
+ controls?: boolean;
26
+ /** Style object */
27
+ style?: React.CSSProperties;
28
+ }
29
+
30
+ /**
31
+ * IPFSMediaRenderer - A wrapper around Thirdweb's MediaRenderer that configures
32
+ * the IPFS gateway URL to use our validated gateway.
33
+ *
34
+ * Features:
35
+ * - Configures MediaRenderer to use cloudflare-ipfs.com gateway
36
+ * - Gateway matches our allowed list in profileDisplay.ts
37
+ * - Provides fallback for missing sources
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * <IPFSMediaRenderer
42
+ * src="ipfs://QmX..."
43
+ * alt="Profile Avatar"
44
+ * className="size-14 rounded-full"
45
+ * />
46
+ * ```
47
+ */
48
+ export function IPFSMediaRenderer({
49
+ src,
50
+ alt = "Media",
51
+ className,
52
+ client = defaultClient,
53
+ width,
54
+ height,
55
+ controls,
56
+ style,
57
+ }: IPFSMediaRendererProps) {
58
+ // If no source, render fallback
59
+ if (!src) {
60
+ return (
61
+ <div className={className} style={style} aria-label={alt}>
62
+ <div className="bg-b3-primary-wash flex h-full w-full items-center justify-center">
63
+ <span className="text-b3-grey font-neue-montreal-semibold text-xs">{alt.charAt(0).toUpperCase()}</span>
64
+ </div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ // Convert IPFS URLs to HTTP gateway URLs if needed
70
+ // This handles both ipfs:// URLs and existing HTTP gateway URLs
71
+
72
+ return (
73
+ <MediaRenderer
74
+ src={src}
75
+ client={client}
76
+ alt={alt}
77
+ className={className}
78
+ width={width ? width.toString() : undefined}
79
+ height={height ? height.toString() : undefined}
80
+ controls={controls}
81
+ style={style}
82
+ />
83
+ );
84
+ }
@@ -13,11 +13,11 @@ import { BankIcon } from "@b3dotfun/sdk/global-account/react/components/icons/Ba
13
13
  import { SignOutIcon } from "@b3dotfun/sdk/global-account/react/components/icons/SignOutIcon";
14
14
  import { SwapIcon } from "@b3dotfun/sdk/global-account/react/components/icons/SwapIcon";
15
15
  import { formatUsername } from "@b3dotfun/sdk/shared/utils";
16
- import { getIpfsUrl } from "@b3dotfun/sdk/shared/utils/ipfs";
17
16
  import { Loader2, Pencil } from "lucide-react";
18
17
  import { useEffect, useRef, useState } from "react";
19
18
  import { useActiveAccount } from "thirdweb/react";
20
19
  import { useFirstEOA } from "../../hooks/useFirstEOA";
20
+ import { IPFSMediaRenderer } from "../IPFSMediaRenderer/IPFSMediaRenderer";
21
21
  import { B3TokenIcon, EthereumTokenIcon } from "../TokenIcon";
22
22
  import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../ui/accordion";
23
23
  import { TokenBalanceRow } from "./TokenBalanceRow";
@@ -48,7 +48,8 @@ export function BalanceContent({ onLogout, showDeposit = true, showSwap = true }
48
48
  const [openAccordions, setOpenAccordions] = useState<string[]>([]);
49
49
  const hasExpandedRef = useRef(false);
50
50
 
51
- const avatarUrl = user?.avatar ? getIpfsUrl(user?.avatar) : profile?.avatar;
51
+ // IPFSMediaRenderer will handle IPFS URL conversion and validation
52
+ const avatarSrc = user?.avatar || profile?.avatar;
52
53
 
53
54
  const handleEditProfile = () => {
54
55
  setB3ModalOpen(true);
@@ -118,11 +119,7 @@ export function BalanceContent({ onLogout, showDeposit = true, showSwap = true }
118
119
  <div className="flex items-center justify-between">
119
120
  <div className="global-account-profile flex items-center gap-4">
120
121
  <div className="global-account-profile-avatar relative">
121
- {avatarUrl ? (
122
- <img src={avatarUrl} alt="Profile" className="size-24 rounded-full" />
123
- ) : (
124
- <div className="bg-b3-primary-wash size-24 rounded-full" />
125
- )}
122
+ <IPFSMediaRenderer src={avatarSrc} alt="Profile" className="size-24 rounded-full" />
126
123
  <button
127
124
  onClick={handleEditProfile}
128
125
  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"
@@ -41,7 +41,7 @@ const HomeActionButton = ({
41
41
  return (
42
42
  <Button
43
43
  className={cn(
44
- "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]",
44
+ "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] shadow-[0_0_0_1px_rgba(10,13,18,0.18)_inset,0_-2px_0_0_rgba(10,13,18,0.05)_inset,0_1px_2px_0_rgba(10,13,18,0.05)] hover:bg-[#FAFAFA]",
45
45
  customClass,
46
46
  )}
47
47
  onClick={onClick}
@@ -93,7 +93,7 @@ export function ManageAccount({
93
93
  }}
94
94
  >
95
95
  <div className="p-0">
96
- <TabsContentPrimitive value="home" className="px-0 pb-4 pt-2">
96
+ <TabsContentPrimitive value="home" className="m-0 p-0 pb-2">
97
97
  <HomeContent showDeposit={showDeposit} showSwap={showSwap} />
98
98
  </TabsContentPrimitive>
99
99
 
@@ -108,7 +108,7 @@ export function ManageAccount({
108
108
  {/* Swap tab content is handled by modal, so this is empty */}
109
109
  <TabsContentPrimitive value="swap" className="hidden" />
110
110
 
111
- <TabsContentPrimitive value="settings" className="pb-4 pt-2">
111
+ <TabsContentPrimitive value="settings" className="m-0 p-0 pb-2">
112
112
  <SettingsContent partnerId={partnerId} onLogout={onLogout} chain={chain} />
113
113
  </TabsContentPrimitive>
114
114
  </div>
@@ -1,11 +1,11 @@
1
1
  import { useAccountWallet, useB3, useModalStore, useProfile, useSimBalance } from "@b3dotfun/sdk/global-account/react";
2
2
  import { formatUsername } from "@b3dotfun/sdk/shared/utils";
3
- import { getIpfsUrl } from "@b3dotfun/sdk/shared/utils/ipfs";
4
3
  import { formatDisplayNumber } from "@b3dotfun/sdk/shared/utils/number";
5
4
  import { Pencil } from "lucide-react";
6
5
  import { useMemo } from "react";
7
6
  import { useActiveAccount } from "thirdweb/react";
8
7
  import { useFirstEOA } from "../../hooks/useFirstEOA";
8
+ import { IPFSMediaRenderer } from "../IPFSMediaRenderer/IPFSMediaRenderer";
9
9
 
10
10
  const ProfileSection = () => {
11
11
  const account = useActiveAccount();
@@ -28,8 +28,6 @@ const ProfileSection = () => {
28
28
  return simBalance.balances.reduce((sum, token) => sum + (token.value_usd || 0), 0);
29
29
  }, [simBalance]);
30
30
 
31
- const avatarUrl = user?.avatar ? getIpfsUrl(user?.avatar) : profile?.avatar;
32
-
33
31
  const handleEditAvatar = () => {
34
32
  setB3ModalOpen(true);
35
33
  setB3ModalContentType({
@@ -41,15 +39,22 @@ const ProfileSection = () => {
41
39
  });
42
40
  };
43
41
 
42
+ // IPFSMediaRenderer will handle IPFS URL conversion and validation
43
+ const avatarSrc = user?.avatar || profile?.avatar;
44
+
45
+ // Get current username - prioritize user.username, fallback to profile data
46
+ const currentUsername = user?.username || profile?.displayName || formatUsername(profile?.name || "");
47
+
44
48
  return (
45
49
  <div className="flex items-center justify-between px-5 py-6">
46
50
  <div className="global-account-profile flex items-center gap-4">
47
51
  <div className="global-account-profile-avatar relative">
48
- {avatarUrl ? (
49
- <img src={avatarUrl} alt="Profile" className="size-14 rounded-full" />
50
- ) : (
51
- <div className="bg-b3-primary-wash size-14 rounded-full" />
52
- )}
52
+ <IPFSMediaRenderer
53
+ src={avatarSrc}
54
+ alt="Profile Avatar"
55
+ className="border-b3-line border-1 bg-b3-primary-wash size-14 rounded-full border"
56
+ />
57
+
53
58
  <button
54
59
  onClick={handleEditAvatar}
55
60
  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"
@@ -62,9 +67,7 @@ const ProfileSection = () => {
62
67
  <div className="text-b3-foreground-muted"> $</div>
63
68
  <div className="text-[30px]">{formatDisplayNumber(totalBalanceUsd, { fractionDigits: 2 })}</div>
64
69
  </h2>
65
- <div className="font-neue-montreal-semibold text-base leading-none text-[#0B57C2]">
66
- {profile?.displayName || formatUsername(profile?.name || "")}
67
- </div>
70
+ <div className="font-neue-montreal-semibold text-base leading-none text-[#0B57C2]">{currentUsername}</div>
68
71
  </div>
69
72
  </div>
70
73
  </div>
@@ -50,7 +50,7 @@ const SettingsContent = ({
50
50
 
51
51
  return (
52
52
  <div className="flex h-[470px] flex-col">
53
- <ModalHeader title="Settings" />
53
+ <ModalHeader showBackButton={false} showCloseButton={false} title="Settings" />
54
54
 
55
55
  {/* Profile Section */}
56
56
  <div className="p-5">
@@ -1,23 +1,44 @@
1
+ import app from "@b3dotfun/sdk/global-account/app";
1
2
  import { useB3, useModalStore, useProfile } from "@b3dotfun/sdk/global-account/react";
2
3
  import { formatUsername } from "@b3dotfun/sdk/shared/utils";
3
- import { getIpfsUrl } from "@b3dotfun/sdk/shared/utils/ipfs";
4
- import { Pencil } from "lucide-react";
4
+ import { Check, Loader2, Pencil, X } from "lucide-react";
5
+ import { useEffect, useRef, useState } from "react";
6
+ import { toast } from "sonner";
5
7
  import { useActiveAccount } from "thirdweb/react";
6
8
  import { useFirstEOA } from "../../hooks/useFirstEOA";
9
+ import { IPFSMediaRenderer } from "../IPFSMediaRenderer/IPFSMediaRenderer";
7
10
 
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
 
20
- const avatarUrl = user?.avatar ? getIpfsUrl(user?.avatar) : profile?.avatar;
23
+ // State for inline username editing
24
+ const [isEditingUsername, setIsEditingUsername] = useState(false);
25
+ const [editedUsername, setEditedUsername] = useState("");
26
+ const [isSaving, setIsSaving] = useState(false);
27
+ const inputRef = useRef<HTMLInputElement>(null);
28
+
29
+ // IPFSMediaRenderer will handle IPFS URL conversion and validation
30
+ const avatarSrc = user?.avatar || profile?.avatar;
31
+
32
+ // Get current username - prioritize user.username, fallback to profile data
33
+ const currentUsername = user?.username || profile?.displayName || formatUsername(profile?.name || "");
34
+
35
+ // Focus input when entering edit mode
36
+ useEffect(() => {
37
+ if (isEditingUsername && inputRef.current) {
38
+ inputRef.current.focus();
39
+ inputRef.current.select();
40
+ }
41
+ }, [isEditingUsername]);
21
42
 
22
43
  const handleEditAvatar = () => {
23
44
  setB3ModalOpen(true);
@@ -31,19 +52,69 @@ const SettingsProfileCard = () => {
31
52
  };
32
53
 
33
54
  const handleEditUsername = () => {
34
- // TODO: Implement edit username functionality
35
- console.log("Edit username clicked");
55
+ setEditedUsername(currentUsername || "");
56
+ setIsEditingUsername(true);
57
+ };
58
+
59
+ const handleCancelEdit = () => {
60
+ setIsEditingUsername(false);
61
+ setEditedUsername("");
62
+ };
63
+
64
+ const handleSaveUsername = async () => {
65
+ if (!editedUsername.trim()) {
66
+ toast.error("Username cannot be empty");
67
+ return;
68
+ }
69
+
70
+ if (editedUsername === currentUsername) {
71
+ // No change, just exit edit mode
72
+ handleCancelEdit();
73
+ return;
74
+ }
75
+
76
+ setIsSaving(true);
77
+ try {
78
+ const updatedUser = await app.service("users").registerUsername(
79
+ { username: editedUsername.trim() },
80
+ // @ts-expect-error - our typed client is expecting context even though it's set elsewhere
81
+ {},
82
+ );
83
+
84
+ // Update user state - registerUsername returns an array with single user
85
+ setUser(Array.isArray(updatedUser) ? updatedUser[0] : updatedUser);
86
+
87
+ // Refresh profile to get updated data
88
+ await refreshProfile();
89
+
90
+ toast.success("Username updated successfully!");
91
+ setIsEditingUsername(false);
92
+ setEditedUsername("");
93
+ } catch (error) {
94
+ console.error("Error updating username:", error);
95
+ toast.error("Failed to update username. Please try again.");
96
+ } finally {
97
+ setIsSaving(false);
98
+ }
99
+ };
100
+
101
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
102
+ if (e.key === "Enter") {
103
+ handleSaveUsername();
104
+ } else if (e.key === "Escape") {
105
+ handleCancelEdit();
106
+ }
36
107
  };
37
108
 
38
109
  return (
39
110
  <div className="flex w-full items-center gap-3">
40
111
  {/* Avatar with edit badge */}
41
112
  <div className="relative shrink-0">
42
- {avatarUrl ? (
43
- <img src={avatarUrl} alt="Profile" className="border-black/8 size-14 rounded-full border object-cover" />
44
- ) : (
45
- <div className="bg-b3-primary-wash border-black/8 size-14 rounded-full border" />
46
- )}
113
+ <IPFSMediaRenderer
114
+ src={avatarSrc}
115
+ alt="Profile"
116
+ className="border-black/8 size-14 rounded-full border object-cover"
117
+ />
47
118
  <button
48
119
  onClick={handleEditAvatar}
49
120
  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"
@@ -55,17 +126,52 @@ const SettingsProfileCard = () => {
55
126
 
56
127
  {/* Username and edit link */}
57
128
  <div className="flex shrink-0 flex-col gap-1">
58
- <div className="flex items-center gap-1">
59
- <p className="font-neue-montreal-semibold text-lg leading-none text-[#0B57C2]">
60
- {profile?.displayName || formatUsername(profile?.name || "")}
61
- </p>
62
- </div>
63
- <button
64
- onClick={handleEditUsername}
65
- className="flex items-center justify-center gap-1 text-left transition-opacity hover:opacity-80"
66
- >
67
- <p className="font-inter text-sm font-semibold leading-5 text-[#51525C]">Edit Username</p>
68
- </button>
129
+ {isEditingUsername ? (
130
+ /* Edit mode - inline input */
131
+ <div className="flex items-center gap-2">
132
+ <input
133
+ ref={inputRef}
134
+ type="text"
135
+ value={editedUsername}
136
+ onChange={e => setEditedUsername(e.target.value)}
137
+ onKeyDown={handleKeyDown}
138
+ disabled={isSaving}
139
+ 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"
140
+ placeholder="Enter username"
141
+ />
142
+ <div className="flex items-center gap-1">
143
+ <button
144
+ onClick={handleSaveUsername}
145
+ disabled={isSaving}
146
+ 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"
147
+ aria-label="Save username"
148
+ >
149
+ {isSaving ? <Loader2 size={18} className="animate-spin" /> : <Check size={18} strokeWidth={2.5} />}
150
+ </button>
151
+ <button
152
+ onClick={handleCancelEdit}
153
+ disabled={isSaving}
154
+ className="text-b3-foreground-muted hover:text-b3-grey flex items-center justify-center rounded-md p-1 transition-colors disabled:opacity-50"
155
+ aria-label="Cancel editing"
156
+ >
157
+ <X size={18} strokeWidth={2.5} />
158
+ </button>
159
+ </div>
160
+ </div>
161
+ ) : (
162
+ /* Display mode */
163
+ <>
164
+ <div className="flex items-center gap-1">
165
+ <p className="font-neue-montreal-semibold text-lg leading-none text-[#0B57C2]">{currentUsername}</p>
166
+ </div>
167
+ <button
168
+ onClick={handleEditUsername}
169
+ className="flex items-center justify-center gap-1 text-left transition-opacity hover:opacity-80"
170
+ >
171
+ <p className="font-inter text-sm font-semibold leading-5 text-[#51525C]">Edit Username</p>
172
+ </button>
173
+ </>
174
+ )}
69
175
  </div>
70
176
  </div>
71
177
  );
@@ -3,6 +3,7 @@ import { ChevronDown, X } from "lucide-react";
3
3
  import { useModalStore } from "../../stores";
4
4
 
5
5
  const ModalHeader = ({
6
+ showBackButton = true,
6
7
  handleBack,
7
8
  handleClose,
8
9
  title,
@@ -11,6 +12,7 @@ const ModalHeader = ({
11
12
  className,
12
13
  showBackWord = false,
13
14
  }: {
15
+ showBackButton?: boolean;
14
16
  handleBack?: () => void;
15
17
  handleClose?: () => void;
16
18
  title: string;
@@ -26,21 +28,27 @@ const ModalHeader = ({
26
28
  <div
27
29
  className={cn("flex h-16 items-center justify-between border-b border-[#e4e4e7] bg-white px-5 py-3", className)}
28
30
  >
29
- <button
30
- onClick={handleBack || navigateBack}
31
- className="flex h-6 w-6 items-center justify-center transition-opacity hover:opacity-70"
32
- >
33
- <ChevronDown className="h-6 w-6 rotate-90 text-[#51525c]" />
34
- {showBackWord && <span className="text-sm font-medium">Back</span>}
35
- </button>
31
+ {showBackButton ? (
32
+ <button
33
+ onClick={handleBack || navigateBack}
34
+ className="flex h-6 w-6 items-center justify-center transition-opacity hover:opacity-70"
35
+ >
36
+ <ChevronDown className="h-6 w-6 rotate-90 text-[#51525c]" />
37
+ {showBackWord && <span className="text-sm font-medium">Back</span>}
38
+ </button>
39
+ ) : (
40
+ <div className="w-2" />
41
+ )}
36
42
  <p className="font-inter text-lg font-semibold leading-7 text-[#18181b]">{title}</p>
37
- {showCloseButton && (
43
+ {showCloseButton ? (
38
44
  <button
39
45
  onClick={handleClose || (() => setB3ModalOpen(false))}
40
46
  className="flex h-6 w-6 items-center justify-center transition-opacity hover:opacity-70"
41
47
  >
42
48
  <X className="h-6 w-6 text-[#51525c]" />
43
49
  </button>
50
+ ) : (
51
+ <div className="w-2" />
44
52
  )}
45
53
  {children}
46
54
  </div>
@@ -1,4 +1,5 @@
1
1
  import {
2
+ IPFSMediaRenderer,
2
3
  SignInWithB3,
3
4
  SignInWithB3ModalProps,
4
5
  StyleRoot,
@@ -13,6 +14,7 @@ import { cn, truncateAddress } from "@b3dotfun/sdk/shared/utils";
13
14
  import { Menu, MenuButton, MenuItems, Transition } from "@headlessui/react";
14
15
  import { ReactNode, useEffect } from "react";
15
16
  import { useConnectedWallets, useSetActiveWallet, useWalletInfo } from "thirdweb/react";
17
+ import { useAccountWalletImage } from "../../hooks/useAccountWallet";
16
18
  import { ManageAccountButton } from "../custom/ManageAccountButton";
17
19
 
18
20
  type SignInProps = {
@@ -44,7 +46,7 @@ export function SignIn(props: SignInWithB3Props) {
44
46
 
45
47
  const isMobile = useIsMobile();
46
48
  const { logout } = useAuthentication(partnerId);
47
- const onDisconnect = async () => {
49
+ const onDisconnect = async (): Promise<void> => {
48
50
  await logout();
49
51
  };
50
52
 
@@ -73,17 +75,19 @@ export function SignIn(props: SignInWithB3Props) {
73
75
  }
74
76
  }, [connectedEOAWallet, isActiveEOAWallet, setActiveWallet, automaticallySetFirstEoa]);
75
77
 
78
+ const walletImage = useAccountWalletImage();
79
+
76
80
  // Desktop version - original dropdown menu
77
81
  return (
78
82
  <StyleRoot>
79
83
  <Menu className={`relative flex items-center ${className || ""}`} as="div">
80
84
  {globalAddress ? (
81
85
  <>
82
- <MenuButton className="bg-b3-react-background group flex h-10 items-center gap-1 rounded-xl px-3">
83
- {!!wallet.meta?.icon && (
84
- <img
85
- src={wallet.meta.icon}
86
- alt={wallet.meta.icon}
86
+ <MenuButton className="bg-b3-react-background group flex h-10 items-center gap-1 rounded-xl px-3 focus:outline-none">
87
+ {!!walletImage && (
88
+ <IPFSMediaRenderer
89
+ src={walletImage}
90
+ alt="Wallet Image"
87
91
  className="bg-b3-react-primary h-6 w-6 rounded-full object-cover opacity-100"
88
92
  />
89
93
  )}
@@ -98,7 +102,7 @@ export function SignIn(props: SignInWithB3Props) {
98
102
  leaveTo="scale-95 opacity-0"
99
103
  >
100
104
  <MenuItems
101
- className="b3-root absolute -right-4 top-full min-w-64 rounded-2xl border lg:right-0"
105
+ className="b3-root absolute -right-4 top-full min-w-64 rounded-2xl border focus:outline-none lg:right-0"
102
106
  modal={false}
103
107
  // TODO: Figure out why setting anchor on mobile causes z-index issues where it appears under elements
104
108
  anchor={isMobile ? "top end" : undefined}
@@ -1,3 +1,4 @@
1
+ // TODO woj: Barrel file for all components, this might be reason of bundle size issues
1
2
  // Core Components
2
3
  export { B3DynamicModal } from "./B3DynamicModal";
3
4
  export { B3Provider, InnerProvider } from "./B3Provider/B3Provider";
@@ -25,10 +26,8 @@ export { Deposit } from "./Deposit/Deposit";
25
26
  // Send Components
26
27
  export { Send } from "./Send/Send";
27
28
 
28
- // B3 Global Branding Wrapper
29
- // Profile Components
30
- export { AvatarEditor } from "./AvatarEditor/AvatarEditor";
31
- export { ProfileEditor } from "./ProfileEditor/ProfileEditor";
29
+ // Media Components
30
+ export { IPFSMediaRenderer } from "./IPFSMediaRenderer/IPFSMediaRenderer";
32
31
 
33
32
  // RequestPermissions Components
34
33
  export { RequestPermissions } from "./RequestPermissions/RequestPermissions";
@@ -35,7 +35,7 @@ const DrawerContent = React.forwardRef<
35
35
  <DrawerPrimitive.Content
36
36
  ref={ref}
37
37
  className={cn(
38
- "bg-b3-react-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border py-6",
38
+ "bg-b3-react-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border py-6 pt-5",
39
39
  className,
40
40
  )}
41
41
  {...props}
@@ -112,3 +112,29 @@ export function useAccountWallet(): {
112
112
 
113
113
  return res;
114
114
  }
115
+
116
+ export function useAccountWalletImage(): string {
117
+ const { account, user } = useB3();
118
+
119
+ const activeWallet = useActiveWallet();
120
+ const connectedWallets = useConnectedWallets();
121
+
122
+ const connectedSmartWallet = connectedWallets.find(wallet => wallet.id === ecosystemWalletId);
123
+ const connectedEOAWallet = connectedWallets.find(wallet => wallet.id !== ecosystemWalletId);
124
+ const isActiveSmartWallet = activeWallet?.id === connectedSmartWallet?.id;
125
+
126
+ const { data: walletImage } = useWalletImage(connectedEOAWallet?.id);
127
+
128
+ // If not EOA sign in, then we need to show the smart wallet icon
129
+ const lastAuthProvider = useLastAuthProvider();
130
+
131
+ const smartWalletIcon =
132
+ lastAuthProvider && !connectedEOAWallet
133
+ ? socialIcons[lastAuthProvider as keyof typeof socialIcons]
134
+ : "https://gradvatar.com/0x0000000000000000000000000000000000000000"; // show smart wallet of eoa wallet is gradvatar
135
+
136
+ const { data: profileData } = useProfile({ address: account?.address });
137
+ const avatarUrl = user?.avatar || profileData?.avatar;
138
+
139
+ return avatarUrl || (isActiveSmartWallet ? smartWalletIcon : walletImage) || "";
140
+ }
@@ -383,13 +383,6 @@ export interface AvatarEditorModalProps extends BaseModalProps {
383
383
  onSuccess?: () => void;
384
384
  }
385
385
 
386
- export interface ProfileEditorModalProps extends BaseModalProps {
387
- /** Modal type identifier */
388
- type: "profileEditor";
389
- /** Callback function called when profile is successfully updated */
390
- onSuccess?: () => void;
391
- }
392
-
393
386
  /**
394
387
  * Props for the Deposit modal
395
388
  * Allows users to deposit tokens into their global account
@@ -439,8 +432,7 @@ export type ModalContentType =
439
432
  | AnySpendDepositHypeProps
440
433
  | AvatarEditorModalProps
441
434
  | DepositModalProps
442
- | SendModalProps
443
- | ProfileEditorModalProps;
435
+ | SendModalProps;
444
436
  // Add other modal types here like: | OtherModalProps | AnotherModalProps
445
437
 
446
438
  /**