@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
@@ -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.4",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -319,6 +319,7 @@
319
319
  "lucide-react": "0.424.0",
320
320
  "motion": "^12.23.11",
321
321
  "qrcode.react": "4.2.0",
322
+ "react-easy-crop": "^5.5.3",
322
323
  "react-intersection-observer": "9.16.0",
323
324
  "react-number-format": "5.4.3",
324
325
  "react-timeago": "8.2.0",
@@ -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
 
@@ -1012,7 +1015,7 @@ function AnySpendInner({
1012
1015
  );
1013
1016
 
1014
1017
  const mainView = (
1015
- <div className={"mx-auto flex w-[460px] max-w-full flex-col items-center gap-2"}>
1018
+ <div className={"mx-auto flex w-[460px] max-w-full flex-col items-center gap-2 pt-5"}>
1016
1019
  <div className={"flex w-full max-w-full flex-col items-center gap-2 px-5"}>
1017
1020
  {/* Token Header - Show when in buy mode */}
1018
1021
  {isBuyMode && (
@@ -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
 
@@ -192,7 +185,7 @@ export function CryptoPaymentMethod({
192
185
  };
193
186
 
194
187
  return (
195
- <div className="crypto-payment-method mx-auto h-fit w-[460px] max-w-full px-5 pb-5">
188
+ <div className="crypto-payment-method mx-auto h-fit w-[460px] max-w-full px-5 pb-5 pt-5 sm:px-0 sm:pt-5">
196
189
  <div className={cn("relative flex flex-col gap-10")}>
197
190
  {/* Header */}
198
191
  <button
@@ -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" />
@@ -150,7 +150,7 @@ function PanelOnrampPaymentInner(props: PanelOnrampPaymentProps) {
150
150
  };
151
151
 
152
152
  return (
153
- <div className="mx-auto flex w-full max-w-[460px] flex-col gap-6 px-5">
153
+ <div className="mx-auto flex w-full max-w-[460px] flex-col gap-6 px-5 pt-5">
154
154
  {/* Order Summary Section */}
155
155
  <>
156
156
  <h2 className="-mb-3 text-lg font-semibold">Order summary</h2>
@@ -1,13 +1,16 @@
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
- import { useRef, useState } from "react";
10
+ import { useCallback, useRef, useState } from "react";
11
+ import type { Area } from "react-easy-crop";
12
+ import Cropper from "react-easy-crop";
13
+ import "react-easy-crop/react-easy-crop.css";
11
14
  import { toast } from "sonner";
12
15
  import { useActiveAccount } from "thirdweb/react";
13
16
  import { upload } from "thirdweb/storage";
@@ -17,6 +20,16 @@ import ModalHeader from "../ModalHeader/ModalHeader";
17
20
 
18
21
  const debug = debugB3React("AvatarEditor");
19
22
 
23
+ // Helper function to create an image element from a URL
24
+ const createImage = (url: string): Promise<HTMLImageElement> =>
25
+ new Promise((resolve, reject) => {
26
+ const image = new Image();
27
+ image.addEventListener("load", () => resolve(image));
28
+ image.addEventListener("error", error => reject(error));
29
+ image.setAttribute("crossOrigin", "anonymous");
30
+ image.src = url;
31
+ });
32
+
20
33
  interface AvatarEditorProps {
21
34
  onSetAvatar?: () => void;
22
35
  className?: string;
@@ -27,16 +40,18 @@ type ViewStep = "select" | "upload";
27
40
  export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
28
41
  const [viewStep, setViewStep] = useState<ViewStep>("select");
29
42
  const [selectedAvatar, setSelectedAvatar] = useState<string | null>(null);
43
+ const [selectedProfileType, setSelectedProfileType] = useState<string | null>(null); // Track which profile was selected
30
44
  const [hoveredProfile, setHoveredProfile] = useState<string | null>(null);
31
45
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
32
46
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
33
- const [isUploading, setIsUploading] = useState(false);
34
47
  const [isSaving, setIsSaving] = useState(false);
35
48
  const [isDragging, setIsDragging] = useState(false);
49
+ const [crop, setCrop] = useState({ x: 0, y: 0 });
50
+ const [zoom, setZoom] = useState(1);
51
+ const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
36
52
  const fileInputRef = useRef<HTMLInputElement>(null);
37
53
  const { setUser, user, partnerId } = useB3();
38
54
  const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
39
- const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen);
40
55
  const contentType = useModalStore(state => state.contentType);
41
56
  const { setPreference } = useProfileSettings();
42
57
 
@@ -46,11 +61,52 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
46
61
  fresh: true,
47
62
  });
48
63
 
49
- const currentAvatar = user?.avatar
50
- ? getIpfsUrl(user?.avatar)
51
- : profile?.avatar
52
- ? getIpfsUrl(profile.avatar)
53
- : undefined;
64
+ // Get raw avatar URLs, convert IPFS URLs, and validate them
65
+ const rawCurrentAvatar = user?.avatar || profile?.avatar;
66
+ const currentAvatar = validateImageUrl(rawCurrentAvatar);
67
+ const safePreviewUrl = validateImageUrl(previewUrl);
68
+
69
+ const onCropComplete = useCallback((_croppedArea: Area, croppedAreaPixels: Area) => {
70
+ setCroppedAreaPixels(croppedAreaPixels);
71
+ }, []);
72
+
73
+ const createCroppedImage = async (imageSrc: string, pixelCrop: Area): Promise<Blob> => {
74
+ const image = await createImage(imageSrc);
75
+ const canvas = document.createElement("canvas");
76
+ const ctx = canvas.getContext("2d");
77
+
78
+ if (!ctx) {
79
+ throw new Error("Failed to get canvas context");
80
+ }
81
+
82
+ // Set canvas size to the crop area
83
+ canvas.width = pixelCrop.width;
84
+ canvas.height = pixelCrop.height;
85
+
86
+ // Draw the cropped image
87
+ ctx.drawImage(
88
+ image,
89
+ pixelCrop.x,
90
+ pixelCrop.y,
91
+ pixelCrop.width,
92
+ pixelCrop.height,
93
+ 0,
94
+ 0,
95
+ pixelCrop.width,
96
+ pixelCrop.height,
97
+ );
98
+
99
+ // Return as blob
100
+ return new Promise((resolve, reject) => {
101
+ canvas.toBlob(blob => {
102
+ if (!blob) {
103
+ reject(new Error("Canvas is empty"));
104
+ return;
105
+ }
106
+ resolve(blob);
107
+ }, "image/jpeg");
108
+ });
109
+ };
54
110
 
55
111
  const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
56
112
  const file = event.target.files?.[0];
@@ -68,6 +124,8 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
68
124
  }
69
125
 
70
126
  setSelectedFile(file);
127
+ // Clear profile type selection when uploading a new file
128
+ setSelectedProfileType(null);
71
129
 
72
130
  // Create preview URL
73
131
  const url = URL.createObjectURL(file);
@@ -78,6 +136,7 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
78
136
 
79
137
  const handleRemovePreview = () => {
80
138
  setSelectedAvatar(currentAvatar || null);
139
+ setSelectedProfileType(null);
81
140
  setSelectedFile(null);
82
141
  if (previewUrl) {
83
142
  URL.revokeObjectURL(previewUrl);
@@ -86,6 +145,10 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
86
145
  if (fileInputRef.current) {
87
146
  fileInputRef.current.value = "";
88
147
  }
148
+ // Reset crop state
149
+ setCrop({ x: 0, y: 0 });
150
+ setZoom(1);
151
+ setCroppedAreaPixels(null);
89
152
  };
90
153
 
91
154
  const handleSaveChanges = async () => {
@@ -96,14 +159,70 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
96
159
 
97
160
  setIsSaving(true);
98
161
  try {
99
- // If user uploaded a new file
100
- if (selectedFile) {
101
- debug("Starting upload to IPFS", selectedFile);
162
+ let fileToUpload: File | null = null;
163
+
164
+ // If user uploaded a new file and cropped it
165
+ if (selectedFile && previewUrl && croppedAreaPixels) {
166
+ try {
167
+ const croppedBlob = await createCroppedImage(previewUrl, croppedAreaPixels);
168
+ const extension = selectedFile.name.split(".").pop() || "jpg";
169
+ fileToUpload = new File([croppedBlob], `avatar-cropped.${extension}`, { type: "image/jpeg" });
170
+ } catch (error) {
171
+ debug("Error cropping image:", error);
172
+ toast.error("Failed to crop image. Please try again.");
173
+ setIsSaving(false);
174
+ return;
175
+ }
176
+ } else if (selectedFile) {
177
+ // Fallback if no crop was made
178
+ fileToUpload = selectedFile;
179
+ } else if (selectedProfileType && selectedAvatar) {
180
+ // User selected from existing profile avatars
181
+ // Fetch the image from the URL and convert to blob
182
+ debug("Fetching image from social profile:", selectedAvatar);
183
+
184
+ try {
185
+ const response = await fetch(selectedAvatar);
186
+ if (!response.ok) {
187
+ throw new Error("Failed to fetch image");
188
+ }
189
+
190
+ const blob = await response.blob();
191
+ debug("Fetched blob with type:", blob.type);
192
+
193
+ // Determine the correct extension from the blob's MIME type
194
+ // This handles URLs without extensions (like Farcaster images)
195
+ const mimeToExtension: Record<string, string> = {
196
+ "image/jpeg": "jpg",
197
+ "image/jpg": "jpg",
198
+ "image/png": "png",
199
+ "image/gif": "gif",
200
+ "image/webp": "webp",
201
+ "image/svg+xml": "svg",
202
+ };
203
+
204
+ const extension = blob.type ? mimeToExtension[blob.type.toLowerCase()] || "jpg" : "jpg";
205
+ const mimeType = blob.type || `image/${extension}`;
206
+
207
+ fileToUpload = new File([blob], `avatar-${selectedProfileType}.${extension}`, { type: mimeType });
208
+
209
+ debug("Successfully converted social profile image to file with extension:", extension);
210
+ } catch (fetchError) {
211
+ debug("Error fetching social profile image:", fetchError);
212
+ toast.error("Failed to fetch profile image. Please try uploading manually.");
213
+ setIsSaving(false);
214
+ return;
215
+ }
216
+ }
217
+
218
+ // Upload to IPFS if we have a file
219
+ if (fileToUpload) {
220
+ debug("Starting upload to IPFS", fileToUpload);
102
221
 
103
222
  // Upload to IPFS using Thirdweb
104
223
  const ipfsUrl = await upload({
105
224
  client,
106
- files: [selectedFile],
225
+ files: [fileToUpload],
107
226
  });
108
227
 
109
228
  debug("Upload successful", ipfsUrl);
@@ -121,23 +240,6 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
121
240
  setUser(user);
122
241
 
123
242
  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
243
  }
142
244
 
143
245
  // Refresh profile to get updated avatar
@@ -165,8 +267,15 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
165
267
  }
166
268
  };
167
269
 
168
- const handleProfileAvatarSelect = (avatarUrl: string) => {
270
+ const handleProfileAvatarSelect = (avatarUrl: string, profileType: string) => {
169
271
  setSelectedAvatar(avatarUrl);
272
+ setSelectedProfileType(profileType);
273
+ // Clear any selected file since we're selecting from profile
274
+ setSelectedFile(null);
275
+ if (previewUrl) {
276
+ URL.revokeObjectURL(previewUrl);
277
+ setPreviewUrl(null);
278
+ }
170
279
  };
171
280
 
172
281
  const handleUploadImageClick = () => {
@@ -214,6 +323,8 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
214
323
  }
215
324
 
216
325
  setSelectedFile(file);
326
+ // Clear profile type selection when uploading a new file
327
+ setSelectedProfileType(null);
217
328
 
218
329
  // Create preview URL
219
330
  const url = URL.createObjectURL(file);
@@ -230,17 +341,22 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
230
341
  });
231
342
  };
232
343
 
233
- const isLoading = isUploading || isSaving;
344
+ const isLoading = isSaving;
234
345
 
235
- // Get profile avatars
346
+ // Get profile avatars with validated URLs
236
347
  const profileAvatars =
237
348
  profile?.profiles
238
349
  ?.filter(p => p.avatar)
239
- .map(p => ({
240
- type: p.type,
241
- avatar: getIpfsUrl(p?.avatar || ""),
242
- name: p.name || p.type,
243
- })) || [];
350
+ .map(p => {
351
+ const rawAvatarUrl = p?.avatar || "";
352
+ const validatedUrl = validateImageUrl(rawAvatarUrl);
353
+ return {
354
+ type: p.type,
355
+ avatar: validatedUrl,
356
+ name: p.name || p.type,
357
+ };
358
+ })
359
+ .filter(p => p.avatar !== null) || []; // Filter out profiles with invalid avatars
244
360
 
245
361
  return (
246
362
  <div className={cn("flex w-full max-w-md flex-col bg-white", className)}>
@@ -254,13 +370,17 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
254
370
  {/* Avatar Preview */}
255
371
  <div className="relative mb-6">
256
372
  <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
- />
373
+ {safePreviewUrl || selectedAvatar || currentAvatar ? (
374
+ <IPFSMediaRenderer
375
+ src={safePreviewUrl || selectedAvatar || currentAvatar || ""}
376
+ alt="Avatar preview"
377
+ className="h-full w-full object-cover"
378
+ />
379
+ ) : (
380
+ <div className="bg-b3-primary-wash h-full w-full" />
381
+ )}
262
382
  </div>
263
- {selectedAvatar && (
383
+ {(selectedAvatar !== currentAvatar || selectedFile) && (
264
384
  <button
265
385
  onClick={handleRemovePreview}
266
386
  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]"
@@ -273,7 +393,7 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
273
393
  {/* Upload Image Button */}
274
394
  <button
275
395
  onClick={handleUploadImageClick}
276
- className="font-inter shadow-xs mb-6 flex w-full items-center justify-center gap-2 rounded-xl border border-[#e4e4e7] bg-white px-4 py-3 text-sm font-semibold text-[#18181b] transition-colors hover:bg-[#f4f4f5]"
396
+ className="font-inter mb-6 flex w-full items-center justify-center gap-2 rounded-xl border border-[#e4e4e7] bg-white px-4 py-3 text-sm font-semibold text-[#18181b] shadow-sm transition-colors hover:bg-[#f4f4f5]"
277
397
  >
278
398
  <Upload className="h-4 w-4" />
279
399
  Upload image
@@ -288,37 +408,42 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
288
408
 
289
409
  {/* Profile Avatars */}
290
410
  <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
- )}
411
+ {profileAvatars.map((profileAvatar, index) => {
412
+ // Skip if avatar is null (should not happen due to filter, but TypeScript doesn't know that)
413
+ if (!profileAvatar.avatar) return null;
414
+
415
+ return (
416
+ <div
417
+ key={index}
418
+ className="relative"
419
+ onMouseEnter={() => setHoveredProfile(profileAvatar.type)}
420
+ onMouseLeave={() => setHoveredProfile(null)}
306
421
  >
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
- ))}
422
+ <button
423
+ onClick={() => handleProfileAvatarSelect(profileAvatar.avatar || "", profileAvatar.type || "")}
424
+ className={cn(
425
+ "h-16 w-16 overflow-hidden rounded-full border-2 transition-all",
426
+ selectedProfileType === profileAvatar.type
427
+ ? "border-[#3368ef] ring-2 ring-[#3368ef]/20"
428
+ : "border-transparent hover:border-[#e4e4e7]",
429
+ )}
430
+ >
431
+ <img
432
+ src={profileAvatar.avatar}
433
+ alt={`${profileAvatar.type} avatar`}
434
+ className="h-full w-full object-cover"
435
+ />
436
+ </button>
437
+
438
+ {/* Tooltip */}
439
+ {hoveredProfile === profileAvatar.type && (
440
+ <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">
441
+ {profileAvatar.name}
442
+ </div>
443
+ )}
444
+ </div>
445
+ );
446
+ })}
322
447
  </div>
323
448
 
324
449
  {/* Link More Account */}
@@ -368,9 +493,56 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
368
493
  </div>
369
494
  ) : (
370
495
  <div className="mb-6 w-full">
371
- <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" />
496
+ <div className="relative aspect-square w-full overflow-hidden rounded-xl bg-[#f4f4f5]">
497
+ {safePreviewUrl ? (
498
+ <>
499
+ <Cropper
500
+ image={safePreviewUrl}
501
+ crop={crop}
502
+ zoom={zoom}
503
+ aspect={1}
504
+ onCropChange={setCrop}
505
+ onCropComplete={onCropComplete}
506
+ onZoomChange={setZoom}
507
+ cropShape="rect"
508
+ showGrid={false}
509
+ style={{
510
+ containerStyle: {
511
+ width: "100%",
512
+ height: "100%",
513
+ backgroundColor: "#f4f4f5",
514
+ },
515
+ cropAreaStyle: {
516
+ border: "2px solid #3368ef",
517
+ borderRadius: "0px",
518
+ },
519
+ }}
520
+ />
521
+ <button
522
+ onClick={handleRemovePreview}
523
+ className="absolute right-4 top-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-[#51525c] text-white transition-colors hover:bg-[#71717a]"
524
+ >
525
+ <X className="h-4 w-4" />
526
+ </button>
527
+ </>
528
+ ) : (
529
+ <div className="bg-b3-primary-wash h-full w-full" />
530
+ )}
373
531
  </div>
532
+ {safePreviewUrl && (
533
+ <div className="mt-4 flex items-center gap-3">
534
+ <label className="flex-shrink-0 text-sm font-semibold text-[#475467]">Zoom</label>
535
+ <input
536
+ type="range"
537
+ min={1}
538
+ max={3}
539
+ step={0.1}
540
+ value={zoom}
541
+ onChange={e => setZoom(Number(e.target.value))}
542
+ className="flex-1 accent-[#3368ef]"
543
+ />
544
+ </div>
545
+ )}
374
546
  </div>
375
547
  )}
376
548
  </>
@@ -392,7 +564,7 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
392
564
  </Button>
393
565
  <Button
394
566
  onClick={handleSaveChanges}
395
- disabled={isLoading || !selectedAvatar}
567
+ disabled={isLoading || (!selectedFile && !selectedProfileType)}
396
568
  className="flex-1 rounded-xl bg-[#3368ef] text-white hover:bg-[#2952cc]"
397
569
  >
398
570
  {isLoading ? (