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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.js +4 -2
  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 +4 -2
  7. package/dist/cjs/anyspend/react/hooks/useSigMint.d.ts +1 -1
  8. package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.js +80 -37
  9. package/dist/cjs/global-account/react/components/B3DynamicModal.js +1 -9
  10. package/dist/cjs/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
  11. package/dist/cjs/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.js +37 -0
  12. package/dist/cjs/global-account/react/components/ManageAccount/BalanceContent.js +4 -3
  13. package/dist/cjs/global-account/react/components/ManageAccount/HomeActions.js +1 -1
  14. package/dist/cjs/global-account/react/components/ManageAccount/ManageAccount.js +1 -1
  15. package/dist/cjs/global-account/react/components/ManageAccount/ProfileSection.js +6 -3
  16. package/dist/cjs/global-account/react/components/ManageAccount/SettingsProfileCard.js +77 -9
  17. package/dist/cjs/global-account/react/components/SignInWithB3/SignIn.js +3 -1
  18. package/dist/cjs/global-account/react/components/index.d.ts +1 -2
  19. package/dist/cjs/global-account/react/components/index.js +6 -8
  20. package/dist/cjs/global-account/react/hooks/useAccountWallet.d.ts +1 -0
  21. package/dist/cjs/global-account/react/hooks/useAccountWallet.js +18 -0
  22. package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +2 -2
  23. package/dist/cjs/global-account/react/hooks/useUserQuery.d.ts +2 -2
  24. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +1 -7
  25. package/dist/cjs/shared/constants/chains/supported.d.ts +1 -1
  26. package/dist/cjs/shared/utils/ipfs.js +10 -3
  27. package/dist/esm/anyspend/react/components/AnySpend.js +4 -2
  28. package/dist/esm/anyspend/react/components/AnySpendCustom.js +1 -1
  29. package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +1 -1
  30. package/dist/esm/anyspend/react/components/AnyspendDepositHype.js +1 -1
  31. package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
  32. package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.js +4 -2
  33. package/dist/esm/anyspend/react/hooks/useSigMint.d.ts +1 -1
  34. package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.js +81 -38
  35. package/dist/esm/global-account/react/components/B3DynamicModal.js +1 -9
  36. package/dist/esm/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
  37. package/dist/esm/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.js +34 -0
  38. package/dist/esm/global-account/react/components/ManageAccount/BalanceContent.js +4 -3
  39. package/dist/esm/global-account/react/components/ManageAccount/HomeActions.js +1 -1
  40. package/dist/esm/global-account/react/components/ManageAccount/ManageAccount.js +1 -1
  41. package/dist/esm/global-account/react/components/ManageAccount/ProfileSection.js +6 -3
  42. package/dist/esm/global-account/react/components/ManageAccount/SettingsProfileCard.js +74 -9
  43. package/dist/esm/global-account/react/components/SignInWithB3/SignIn.js +4 -2
  44. package/dist/esm/global-account/react/components/index.d.ts +1 -2
  45. package/dist/esm/global-account/react/components/index.js +3 -4
  46. package/dist/esm/global-account/react/hooks/useAccountWallet.d.ts +1 -0
  47. package/dist/esm/global-account/react/hooks/useAccountWallet.js +17 -0
  48. package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +2 -2
  49. package/dist/esm/global-account/react/hooks/useUserQuery.d.ts +2 -2
  50. package/dist/esm/global-account/react/stores/useModalStore.d.ts +1 -7
  51. package/dist/esm/shared/constants/chains/supported.d.ts +1 -1
  52. package/dist/esm/shared/utils/ipfs.js +10 -3
  53. package/dist/styles/index.css +1 -1
  54. package/dist/types/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
  55. package/dist/types/anyspend/react/hooks/useSigMint.d.ts +1 -1
  56. package/dist/types/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
  57. package/dist/types/global-account/react/components/index.d.ts +1 -2
  58. package/dist/types/global-account/react/hooks/useAccountWallet.d.ts +1 -0
  59. package/dist/types/global-account/react/hooks/useAuthentication.d.ts +2 -2
  60. package/dist/types/global-account/react/hooks/useUserQuery.d.ts +2 -2
  61. package/dist/types/global-account/react/stores/useModalStore.d.ts +1 -7
  62. package/dist/types/shared/constants/chains/supported.d.ts +1 -1
  63. package/package.json +1 -1
  64. package/src/anyspend/react/components/AnySpend.tsx +4 -3
  65. package/src/anyspend/react/components/AnySpendCustom.tsx +0 -2
  66. package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +0 -2
  67. package/src/anyspend/react/components/AnyspendDepositHype.tsx +0 -2
  68. package/src/anyspend/react/components/common/CryptoPaymentMethod.tsx +6 -13
  69. package/src/global-account/react/components/AvatarEditor/AvatarEditor.tsx +129 -74
  70. package/src/global-account/react/components/B3DynamicModal.tsx +2 -10
  71. package/src/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.tsx +84 -0
  72. package/src/global-account/react/components/ManageAccount/BalanceContent.tsx +4 -7
  73. package/src/global-account/react/components/ManageAccount/HomeActions.tsx +1 -1
  74. package/src/global-account/react/components/ManageAccount/ManageAccount.tsx +2 -2
  75. package/src/global-account/react/components/ManageAccount/ProfileSection.tsx +9 -11
  76. package/src/global-account/react/components/ManageAccount/SettingsProfileCard.tsx +129 -23
  77. package/src/global-account/react/components/SignInWithB3/SignIn.tsx +11 -7
  78. package/src/global-account/react/components/index.ts +3 -4
  79. package/src/global-account/react/hooks/useAccountWallet.tsx +26 -0
  80. package/src/global-account/react/stores/useModalStore.ts +1 -9
  81. package/src/shared/utils/ipfs.ts +10 -3
  82. package/dist/cjs/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
  83. package/dist/cjs/global-account/react/components/ProfileEditor/ProfileEditor.js +0 -141
  84. package/dist/esm/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
  85. package/dist/esm/global-account/react/components/ProfileEditor/ProfileEditor.js +0 -135
  86. package/dist/types/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
  87. package/src/global-account/react/components/ProfileEditor/ProfileEditor.tsx +0 -265
@@ -0,0 +1,39 @@
1
+ import type { ThirdwebClient } from "thirdweb";
2
+ interface IPFSMediaRendererProps {
3
+ /** The source URL - can be IPFS URL (ipfs://...) or HTTP URL */
4
+ src: string | null | undefined;
5
+ /** Alt text for the media */
6
+ alt?: string;
7
+ /** CSS class name */
8
+ className?: string;
9
+ /** Thirdweb client instance (optional, uses default if not provided) */
10
+ client?: ThirdwebClient;
11
+ /** Width of the media */
12
+ width?: string | number;
13
+ /** Height of the media */
14
+ height?: string | number;
15
+ /** Controls property for video/audio */
16
+ controls?: boolean;
17
+ /** Style object */
18
+ style?: React.CSSProperties;
19
+ }
20
+ /**
21
+ * IPFSMediaRenderer - A wrapper around Thirdweb's MediaRenderer that configures
22
+ * the IPFS gateway URL to use our validated gateway.
23
+ *
24
+ * Features:
25
+ * - Configures MediaRenderer to use cloudflare-ipfs.com gateway
26
+ * - Gateway matches our allowed list in profileDisplay.ts
27
+ * - Provides fallback for missing sources
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * <IPFSMediaRenderer
32
+ * src="ipfs://QmX..."
33
+ * alt="Profile Avatar"
34
+ * className="size-14 rounded-full"
35
+ * />
36
+ * ```
37
+ */
38
+ export declare function IPFSMediaRenderer({ src, alt, className, client, width, height, controls, style, }: IPFSMediaRendererProps): import("react/jsx-runtime").JSX.Element;
39
+ export {};
@@ -15,8 +15,7 @@ export { getConnectOptionsFromStrategy, isWalletType, type AllowedStrategy } fro
15
15
  export { ManageAccount } from "./ManageAccount/ManageAccount";
16
16
  export { Deposit } from "./Deposit/Deposit";
17
17
  export { Send } from "./Send/Send";
18
- export { AvatarEditor } from "./AvatarEditor/AvatarEditor";
19
- export { ProfileEditor } from "./ProfileEditor/ProfileEditor";
18
+ export { IPFSMediaRenderer } from "./IPFSMediaRenderer/IPFSMediaRenderer";
20
19
  export { RequestPermissions } from "./RequestPermissions/RequestPermissions";
21
20
  export { RequestPermissionsButton } from "./RequestPermissions/RequestPermissionsButton";
22
21
  export { AccountAssets } from "./AccountAssets/AccountAssets";
@@ -16,3 +16,4 @@ export declare function useAccountWallet(): {
16
16
  eoaWalletIcon?: string;
17
17
  smartWalletIcon?: string;
18
18
  };
19
+ export declare function useAccountWalletImage(): string;
@@ -13,8 +13,8 @@ export declare function useAuthentication(partnerId: string): {
13
13
  onConnect: (_walleAutoConnectedWith: Wallet, allConnectedWallets: Wallet[]) => Promise<void>;
14
14
  user: {
15
15
  email?: string | undefined;
16
- username?: string | undefined;
17
16
  telNumber?: string | undefined;
17
+ username?: string | undefined;
18
18
  ens?: string | undefined;
19
19
  avatar?: string | undefined;
20
20
  preferences?: {} | undefined;
@@ -41,8 +41,8 @@ export declare function useAuthentication(partnerId: string): {
41
41
  name?: string | undefined;
42
42
  address?: string | undefined;
43
43
  email?: string | undefined;
44
- phone?: string | undefined;
45
44
  username?: string | undefined;
45
+ phone?: string | undefined;
46
46
  fid?: string | undefined;
47
47
  };
48
48
  }[] | undefined;
@@ -8,8 +8,8 @@ import { Users } from "@b3dotfun/b3-api";
8
8
  export declare function useUserQuery(): {
9
9
  user: {
10
10
  email?: string | undefined;
11
- username?: string | undefined;
12
11
  telNumber?: string | undefined;
12
+ username?: string | undefined;
13
13
  ens?: string | undefined;
14
14
  avatar?: string | undefined;
15
15
  preferences?: {} | undefined;
@@ -36,8 +36,8 @@ export declare function useUserQuery(): {
36
36
  name?: string | undefined;
37
37
  address?: string | undefined;
38
38
  email?: string | undefined;
39
- phone?: string | undefined;
40
39
  username?: string | undefined;
40
+ phone?: string | undefined;
41
41
  fid?: string | undefined;
42
42
  };
43
43
  }[] | undefined;
@@ -360,12 +360,6 @@ export interface AvatarEditorModalProps extends BaseModalProps {
360
360
  /** Callback function called when avatar is successfully set */
361
361
  onSuccess?: () => void;
362
362
  }
363
- export interface ProfileEditorModalProps extends BaseModalProps {
364
- /** Modal type identifier */
365
- type: "profileEditor";
366
- /** Callback function called when profile is successfully updated */
367
- onSuccess?: () => void;
368
- }
369
363
  /**
370
364
  * Props for the Deposit modal
371
365
  * Allows users to deposit tokens into their global account
@@ -391,7 +385,7 @@ export interface SendModalProps extends BaseModalProps {
391
385
  /**
392
386
  * Union type of all possible modal content types
393
387
  */
394
- export type ModalContentType = SignInWithB3ModalProps | RequestPermissionsModalProps | ManageAccountModalProps | AnySpendModalProps | AnyspendOrderDetailsProps | AnySpendNftProps | AnySpendJoinTournamentProps | AnySpendFundTournamentProps | AnySpendOrderHistoryProps | AnySpendStakeB3Props | AnySpendStakeB3ExactInProps | AnySpendStakeUpsideProps | AnySpendStakeUpsideExactInProps | AnySpendBuySpinProps | AnySpendSignatureMintProps | AnySpendBondKitProps | LinkAccountModalProps | LinkNewAccountModalProps | AnySpendDepositHypeProps | AvatarEditorModalProps | DepositModalProps | SendModalProps | ProfileEditorModalProps;
388
+ export type ModalContentType = SignInWithB3ModalProps | RequestPermissionsModalProps | ManageAccountModalProps | AnySpendModalProps | AnyspendOrderDetailsProps | AnySpendNftProps | AnySpendJoinTournamentProps | AnySpendFundTournamentProps | AnySpendOrderHistoryProps | AnySpendStakeB3Props | AnySpendStakeB3ExactInProps | AnySpendStakeUpsideProps | AnySpendStakeUpsideExactInProps | AnySpendBuySpinProps | AnySpendSignatureMintProps | AnySpendBondKitProps | LinkAccountModalProps | LinkNewAccountModalProps | AnySpendDepositHypeProps | AvatarEditorModalProps | DepositModalProps | SendModalProps;
395
389
  /**
396
390
  * State interface for the modal store
397
391
  */
@@ -35,13 +35,13 @@ export declare const supportedChainNetworks: {
35
35
  uri: string;
36
36
  }[];
37
37
  };
38
+ _id: string | {};
38
39
  icon: {
39
40
  format: string;
40
41
  height: number;
41
42
  width: number;
42
43
  url: string;
43
44
  };
44
- _id: string | {};
45
45
  }[];
46
46
  export declare const coingeckoChains: Record<number, {
47
47
  coingecko_id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.0.65-test.1",
3
+ "version": "0.0.65-test.3",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -25,6 +25,7 @@ import {
25
25
  useTokenFromUrl,
26
26
  } from "@b3dotfun/sdk/global-account/react";
27
27
  import BottomNavigation from "@b3dotfun/sdk/global-account/react/components/ManageAccount/BottomNavigation";
28
+ import { useAccountWalletImage } from "@b3dotfun/sdk/global-account/react/hooks/useAccountWallet";
28
29
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
29
30
  import { formatTokenAmount } from "@b3dotfun/sdk/shared/utils/number";
30
31
  import invariant from "invariant";
@@ -474,6 +475,8 @@ function AnySpendInner({
474
475
  const recipientProfile = useProfile({ address: recipientAddress, fresh: true });
475
476
  const recipientName = recipientProfile.data?.name;
476
477
 
478
+ const globalWalletImage = useAccountWalletImage();
479
+
477
480
  // Auto-set active wallet from wagmi
478
481
  useAutoSetActiveWalletFromWagmi();
479
482
 
@@ -1248,7 +1251,7 @@ function AnySpendInner({
1248
1251
  }}
1249
1252
  onBack={navigateBack}
1250
1253
  recipientEnsName={globalWallet?.ensName}
1251
- recipientImageUrl={globalWallet?.meta?.icon}
1254
+ recipientImageUrl={globalWalletImage}
1252
1255
  />
1253
1256
  );
1254
1257
 
@@ -1265,8 +1268,6 @@ function AnySpendInner({
1265
1268
 
1266
1269
  const cryptoPaymentMethodView = (
1267
1270
  <CryptoPaymentMethod
1268
- globalAddress={globalAddress}
1269
- globalWallet={globalWallet}
1270
1271
  selectedPaymentMethod={selectedCryptoPaymentMethod}
1271
1272
  setSelectedPaymentMethod={setSelectedCryptoPaymentMethod}
1272
1273
  isCreatingOrder={isCreatingOrder}
@@ -1237,8 +1237,6 @@ function AnySpendCustomInner({
1237
1237
  const cryptoPaymentMethodView = (
1238
1238
  <div className={cn("bg-as-surface-primary mx-auto w-[460px] max-w-full rounded-xl p-4")}>
1239
1239
  <CryptoPaymentMethod
1240
- globalAddress={currentWallet?.wallet?.address}
1241
- globalWallet={currentWallet?.wallet}
1242
1240
  selectedPaymentMethod={selectedCryptoPaymentMethod}
1243
1241
  setSelectedPaymentMethod={setSelectedCryptoPaymentMethod}
1244
1242
  isCreatingOrder={isCreatingOrder}
@@ -499,8 +499,6 @@ function AnySpendCustomExactInInner({
499
499
 
500
500
  const cryptoPaymentMethodView = (
501
501
  <CryptoPaymentMethod
502
- globalAddress={globalAddress}
503
- globalWallet={undefined}
504
502
  selectedPaymentMethod={selectedCryptoPaymentMethod}
505
503
  setSelectedPaymentMethod={setSelectedCryptoPaymentMethod}
506
504
  isCreatingOrder={isCreatingOrder}
@@ -464,8 +464,6 @@ function AnySpendDepositHypeInner({
464
464
 
465
465
  const cryptoPaymentMethodView = (
466
466
  <CryptoPaymentMethod
467
- globalAddress={globalAddress}
468
- globalWallet={undefined}
469
467
  selectedPaymentMethod={selectedCryptoPaymentMethod}
470
468
  setSelectedPaymentMethod={setSelectedCryptoPaymentMethod}
471
469
  isCreatingOrder={isCreatingOrder}
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useAccountWallet } from "@b3dotfun/sdk/global-account/react";
4
+ import { useAccountWalletImage } from "@b3dotfun/sdk/global-account/react/hooks/useAccountWallet";
4
5
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
5
6
  import { shortenAddress } from "@b3dotfun/sdk/shared/utils/formatAddress";
6
7
  import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
@@ -22,12 +23,6 @@ export enum CryptoPaymentMethodType {
22
23
  }
23
24
 
24
25
  interface CryptoPaymentMethodProps {
25
- globalAddress?: string;
26
- globalWallet?: {
27
- meta?: {
28
- icon?: string;
29
- };
30
- };
31
26
  selectedPaymentMethod: CryptoPaymentMethodType;
32
27
  setSelectedPaymentMethod: (method: CryptoPaymentMethodType) => void;
33
28
  isCreatingOrder: boolean;
@@ -42,11 +37,7 @@ export function CryptoPaymentMethod({
42
37
  onBack,
43
38
  onSelectPaymentMethod,
44
39
  }: CryptoPaymentMethodProps) {
45
- const {
46
- wallet: globalWallet,
47
- connectedEOAWallet: connectedEOAWallet,
48
- connectedSmartWallet: connectedSmartWallet,
49
- } = useAccountWallet();
40
+ const { connectedEOAWallet: connectedEOAWallet, connectedSmartWallet: connectedSmartWallet } = useAccountWallet();
50
41
  const { connector, address } = useAccount();
51
42
  const { connect, connectors, isPending } = useConnect();
52
43
  const { disconnect } = useDisconnect();
@@ -58,6 +49,8 @@ export function CryptoPaymentMethod({
58
49
  const isConnected = !!connectedEOAWallet;
59
50
  const globalAddress = connectedSmartWallet?.getAccount()?.address;
60
51
 
52
+ const walletImage = useAccountWalletImage();
53
+
61
54
  // Use custom hook to determine wallet display logic
62
55
  const { shouldShowConnectedEOA, shouldShowWagmiWallet } = useConnectedWalletDisplay(selectedPaymentMethod);
63
56
 
@@ -341,8 +334,8 @@ export function CryptoPaymentMethod({
341
334
  >
342
335
  <div className="flex items-center justify-between">
343
336
  <div className="flex items-center gap-3">
344
- {globalWallet?.meta?.icon ? (
345
- <img src={globalWallet.meta.icon} alt="Global Account" className="h-10 w-10 rounded-full" />
337
+ {walletImage ? (
338
+ <img src={walletImage} alt="Global Account" className="h-10 w-10 rounded-full" />
346
339
  ) : (
347
340
  <div className="wallet-icon flex h-10 w-10 items-center justify-center rounded-full bg-purple-100">
348
341
  <Wallet className="h-5 w-5 text-purple-600" />
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import app from "@b3dotfun/sdk/global-account/app";
4
- import { Button, useB3, useProfile } from "@b3dotfun/sdk/global-account/react";
4
+ import { Button, IPFSMediaRenderer, useB3, useProfile } from "@b3dotfun/sdk/global-account/react";
5
+ import { validateImageUrl } from "@b3dotfun/sdk/global-account/react/utils/profileDisplay";
5
6
  import { cn } from "@b3dotfun/sdk/shared/utils/cn";
6
7
  import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
7
- import { getIpfsUrl } from "@b3dotfun/sdk/shared/utils/ipfs";
8
8
  import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
9
9
  import { Loader2, Upload, X } from "lucide-react";
10
10
  import { useRef, useState } from "react";
@@ -27,16 +27,15 @@ type ViewStep = "select" | "upload";
27
27
  export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
28
28
  const [viewStep, setViewStep] = useState<ViewStep>("select");
29
29
  const [selectedAvatar, setSelectedAvatar] = useState<string | null>(null);
30
+ const [selectedProfileType, setSelectedProfileType] = useState<string | null>(null); // Track which profile was selected
30
31
  const [hoveredProfile, setHoveredProfile] = useState<string | null>(null);
31
32
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
32
33
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
33
- const [isUploading, setIsUploading] = useState(false);
34
34
  const [isSaving, setIsSaving] = useState(false);
35
35
  const [isDragging, setIsDragging] = useState(false);
36
36
  const fileInputRef = useRef<HTMLInputElement>(null);
37
37
  const { setUser, user, partnerId } = useB3();
38
38
  const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
39
- const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen);
40
39
  const contentType = useModalStore(state => state.contentType);
41
40
  const { setPreference } = useProfileSettings();
42
41
 
@@ -46,11 +45,10 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
46
45
  fresh: true,
47
46
  });
48
47
 
49
- const currentAvatar = user?.avatar
50
- ? getIpfsUrl(user?.avatar)
51
- : profile?.avatar
52
- ? getIpfsUrl(profile.avatar)
53
- : undefined;
48
+ // Get raw avatar URLs, convert IPFS URLs, and validate them
49
+ const rawCurrentAvatar = user?.avatar || profile?.avatar;
50
+ const currentAvatar = validateImageUrl(rawCurrentAvatar);
51
+ const safePreviewUrl = validateImageUrl(previewUrl);
54
52
 
55
53
  const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
56
54
  const file = event.target.files?.[0];
@@ -68,6 +66,8 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
68
66
  }
69
67
 
70
68
  setSelectedFile(file);
69
+ // Clear profile type selection when uploading a new file
70
+ setSelectedProfileType(null);
71
71
 
72
72
  // Create preview URL
73
73
  const url = URL.createObjectURL(file);
@@ -78,6 +78,7 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
78
78
 
79
79
  const handleRemovePreview = () => {
80
80
  setSelectedAvatar(currentAvatar || null);
81
+ setSelectedProfileType(null);
81
82
  setSelectedFile(null);
82
83
  if (previewUrl) {
83
84
  URL.revokeObjectURL(previewUrl);
@@ -96,14 +97,58 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
96
97
 
97
98
  setIsSaving(true);
98
99
  try {
100
+ let fileToUpload: File | null = null;
101
+
99
102
  // If user uploaded a new file
100
103
  if (selectedFile) {
101
- debug("Starting upload to IPFS", selectedFile);
104
+ fileToUpload = selectedFile;
105
+ } else if (selectedProfileType && selectedAvatar) {
106
+ // User selected from existing profile avatars
107
+ // Fetch the image from the URL and convert to blob
108
+ debug("Fetching image from social profile:", selectedAvatar);
109
+
110
+ try {
111
+ const response = await fetch(selectedAvatar);
112
+ if (!response.ok) {
113
+ throw new Error("Failed to fetch image");
114
+ }
115
+
116
+ const blob = await response.blob();
117
+ debug("Fetched blob with type:", blob.type);
118
+
119
+ // Determine the correct extension from the blob's MIME type
120
+ // This handles URLs without extensions (like Farcaster images)
121
+ const mimeToExtension: Record<string, string> = {
122
+ "image/jpeg": "jpg",
123
+ "image/jpg": "jpg",
124
+ "image/png": "png",
125
+ "image/gif": "gif",
126
+ "image/webp": "webp",
127
+ "image/svg+xml": "svg",
128
+ };
129
+
130
+ const extension = blob.type ? mimeToExtension[blob.type.toLowerCase()] || "jpg" : "jpg";
131
+ const mimeType = blob.type || `image/${extension}`;
132
+
133
+ fileToUpload = new File([blob], `avatar-${selectedProfileType}.${extension}`, { type: mimeType });
134
+
135
+ debug("Successfully converted social profile image to file with extension:", extension);
136
+ } catch (fetchError) {
137
+ debug("Error fetching social profile image:", fetchError);
138
+ toast.error("Failed to fetch profile image. Please try uploading manually.");
139
+ setIsSaving(false);
140
+ return;
141
+ }
142
+ }
143
+
144
+ // Upload to IPFS if we have a file
145
+ if (fileToUpload) {
146
+ debug("Starting upload to IPFS", fileToUpload);
102
147
 
103
148
  // Upload to IPFS using Thirdweb
104
149
  const ipfsUrl = await upload({
105
150
  client,
106
- files: [selectedFile],
151
+ files: [fileToUpload],
107
152
  });
108
153
 
109
154
  debug("Upload successful", ipfsUrl);
@@ -121,23 +166,6 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
121
166
  setUser(user);
122
167
 
123
168
  toast.success("Looks great! Your avatar has been saved!");
124
- } else if (selectedAvatar && selectedAvatar !== currentAvatar) {
125
- // User selected from existing profile avatars
126
- // Find the profile that matches the selected avatar
127
- const selectedProfile = profile?.profiles?.find(p => p.avatar === selectedAvatar);
128
-
129
- if (selectedProfile && selectedProfile.type) {
130
- debug("Setting profile preference to:", selectedProfile.type);
131
-
132
- // Set preference for this profile type
133
- await setPreference(account.address, selectedProfile.type, account.address, async (message: string) => {
134
- // Sign the message using the active account
135
- const signature = await account.signMessage({ message });
136
- return signature;
137
- });
138
-
139
- toast.success("Avatar updated successfully!");
140
- }
141
169
  }
142
170
 
143
171
  // Refresh profile to get updated avatar
@@ -165,8 +193,15 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
165
193
  }
166
194
  };
167
195
 
168
- const handleProfileAvatarSelect = (avatarUrl: string) => {
196
+ const handleProfileAvatarSelect = (avatarUrl: string, profileType: string) => {
169
197
  setSelectedAvatar(avatarUrl);
198
+ setSelectedProfileType(profileType);
199
+ // Clear any selected file since we're selecting from profile
200
+ setSelectedFile(null);
201
+ if (previewUrl) {
202
+ URL.revokeObjectURL(previewUrl);
203
+ setPreviewUrl(null);
204
+ }
170
205
  };
171
206
 
172
207
  const handleUploadImageClick = () => {
@@ -214,6 +249,8 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
214
249
  }
215
250
 
216
251
  setSelectedFile(file);
252
+ // Clear profile type selection when uploading a new file
253
+ setSelectedProfileType(null);
217
254
 
218
255
  // Create preview URL
219
256
  const url = URL.createObjectURL(file);
@@ -230,17 +267,22 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
230
267
  });
231
268
  };
232
269
 
233
- const isLoading = isUploading || isSaving;
270
+ const isLoading = isSaving;
234
271
 
235
- // Get profile avatars
272
+ // Get profile avatars with validated URLs
236
273
  const profileAvatars =
237
274
  profile?.profiles
238
275
  ?.filter(p => p.avatar)
239
- .map(p => ({
240
- type: p.type,
241
- avatar: getIpfsUrl(p?.avatar || ""),
242
- name: p.name || p.type,
243
- })) || [];
276
+ .map(p => {
277
+ const rawAvatarUrl = p?.avatar || "";
278
+ const validatedUrl = validateImageUrl(rawAvatarUrl);
279
+ return {
280
+ type: p.type,
281
+ avatar: validatedUrl,
282
+ name: p.name || p.type,
283
+ };
284
+ })
285
+ .filter(p => p.avatar !== null) || []; // Filter out profiles with invalid avatars
244
286
 
245
287
  return (
246
288
  <div className={cn("flex w-full max-w-md flex-col bg-white", className)}>
@@ -254,13 +296,17 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
254
296
  {/* Avatar Preview */}
255
297
  <div className="relative mb-6">
256
298
  <div className="h-32 w-32 overflow-hidden rounded-full">
257
- <img
258
- src={selectedAvatar || currentAvatar || "https://via.placeholder.com/128"}
259
- alt="Avatar preview"
260
- className="h-full w-full object-cover"
261
- />
299
+ {safePreviewUrl || selectedAvatar || currentAvatar ? (
300
+ <IPFSMediaRenderer
301
+ src={safePreviewUrl || selectedAvatar || currentAvatar || ""}
302
+ alt="Avatar preview"
303
+ className="h-full w-full object-cover"
304
+ />
305
+ ) : (
306
+ <div className="bg-b3-primary-wash h-full w-full" />
307
+ )}
262
308
  </div>
263
- {selectedAvatar && (
309
+ {(selectedAvatar !== currentAvatar || selectedFile) && (
264
310
  <button
265
311
  onClick={handleRemovePreview}
266
312
  className="absolute -right-1 -top-1 flex h-8 w-8 items-center justify-center rounded-full bg-[#51525c] text-white transition-colors hover:bg-[#71717a]"
@@ -288,37 +334,42 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
288
334
 
289
335
  {/* Profile Avatars */}
290
336
  <div className="mb-4 flex gap-3">
291
- {profileAvatars.map((profileAvatar, index) => (
292
- <div
293
- key={index}
294
- className="relative"
295
- onMouseEnter={() => setHoveredProfile(profileAvatar.type)}
296
- onMouseLeave={() => setHoveredProfile(null)}
297
- >
298
- <button
299
- onClick={() => handleProfileAvatarSelect(profileAvatar.avatar)}
300
- className={cn(
301
- "h-16 w-16 overflow-hidden rounded-full border-2 transition-all",
302
- selectedAvatar === profileAvatar.avatar
303
- ? "border-[#3368ef] ring-2 ring-[#3368ef]/20"
304
- : "border-transparent hover:border-[#e4e4e7]",
305
- )}
337
+ {profileAvatars.map((profileAvatar, index) => {
338
+ // Skip if avatar is null (should not happen due to filter, but TypeScript doesn't know that)
339
+ if (!profileAvatar.avatar) return null;
340
+
341
+ return (
342
+ <div
343
+ key={index}
344
+ className="relative"
345
+ onMouseEnter={() => setHoveredProfile(profileAvatar.type)}
346
+ onMouseLeave={() => setHoveredProfile(null)}
306
347
  >
307
- <img
308
- src={profileAvatar.avatar}
309
- alt={`${profileAvatar.type} avatar`}
310
- className="h-full w-full object-cover"
311
- />
312
- </button>
313
-
314
- {/* Tooltip */}
315
- {hoveredProfile === profileAvatar.type && (
316
- <div className="absolute -top-10 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-[#18181b] px-3 py-1.5 text-xs text-white">
317
- {profileAvatar.name}
318
- </div>
319
- )}
320
- </div>
321
- ))}
348
+ <button
349
+ onClick={() => handleProfileAvatarSelect(profileAvatar.avatar || "", profileAvatar.type || "")}
350
+ className={cn(
351
+ "h-16 w-16 overflow-hidden rounded-full border-2 transition-all",
352
+ selectedProfileType === profileAvatar.type
353
+ ? "border-[#3368ef] ring-2 ring-[#3368ef]/20"
354
+ : "border-transparent hover:border-[#e4e4e7]",
355
+ )}
356
+ >
357
+ <img
358
+ src={profileAvatar.avatar}
359
+ alt={`${profileAvatar.type} avatar`}
360
+ className="h-full w-full object-cover"
361
+ />
362
+ </button>
363
+
364
+ {/* Tooltip */}
365
+ {hoveredProfile === profileAvatar.type && (
366
+ <div className="absolute -top-10 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-[#18181b] px-3 py-1.5 text-xs text-white">
367
+ {profileAvatar.name}
368
+ </div>
369
+ )}
370
+ </div>
371
+ );
372
+ })}
322
373
  </div>
323
374
 
324
375
  {/* Link More Account */}
@@ -369,7 +420,11 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
369
420
  ) : (
370
421
  <div className="mb-6 w-full">
371
422
  <div className="aspect-square w-full overflow-hidden rounded-xl bg-[#f4f4f5]">
372
- <img src={previewUrl || ""} alt="Preview" className="h-full w-full object-cover" />
423
+ {safePreviewUrl ? (
424
+ <img src={safePreviewUrl} alt="Preview" className="h-full w-full object-cover" />
425
+ ) : (
426
+ <div className="bg-b3-primary-wash h-full w-full" />
427
+ )}
373
428
  </div>
374
429
  </div>
375
430
  )}
@@ -392,7 +447,7 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
392
447
  </Button>
393
448
  <Button
394
449
  onClick={handleSaveChanges}
395
- disabled={isLoading || !selectedAvatar}
450
+ disabled={isLoading || (!selectedFile && !selectedProfileType)}
396
451
  className="flex-1 rounded-xl bg-[#3368ef] text-white hover:bg-[#2952cc]"
397
452
  >
398
453
  {isLoading ? (
@@ -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
  >
@@ -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"