@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.
- package/dist/cjs/anyspend/react/components/AnySpend.js +5 -3
- package/dist/cjs/anyspend/react/components/AnySpendCustom.js +1 -1
- package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.js +1 -1
- package/dist/cjs/anyspend/react/components/AnyspendDepositHype.js +1 -1
- package/dist/cjs/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
- package/dist/cjs/anyspend/react/components/common/CryptoPaymentMethod.js +5 -3
- package/dist/cjs/anyspend/react/components/common/PanelOnrampPayment.js +1 -1
- package/dist/cjs/anyspend/react/hooks/useSigMint.d.ts +1 -1
- package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
- package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.js +149 -39
- package/dist/cjs/global-account/react/components/B3DynamicModal.js +1 -9
- package/dist/cjs/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
- package/dist/cjs/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.js +37 -0
- package/dist/cjs/global-account/react/components/ManageAccount/BalanceContent.js +4 -3
- package/dist/cjs/global-account/react/components/ManageAccount/HomeActions.js +1 -1
- package/dist/cjs/global-account/react/components/ManageAccount/ManageAccount.js +1 -1
- package/dist/cjs/global-account/react/components/ManageAccount/ProfileSection.js +6 -3
- package/dist/cjs/global-account/react/components/ManageAccount/SettingsContent.js +1 -1
- package/dist/cjs/global-account/react/components/ManageAccount/SettingsProfileCard.js +77 -9
- package/dist/cjs/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
- package/dist/cjs/global-account/react/components/ModalHeader/ModalHeader.js +2 -2
- package/dist/cjs/global-account/react/components/SignInWithB3/SignIn.js +3 -1
- package/dist/cjs/global-account/react/components/index.d.ts +1 -2
- package/dist/cjs/global-account/react/components/index.js +6 -8
- package/dist/cjs/global-account/react/components/ui/drawer.js +1 -1
- package/dist/cjs/global-account/react/hooks/useAccountWallet.d.ts +1 -0
- package/dist/cjs/global-account/react/hooks/useAccountWallet.js +18 -0
- package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +2 -2
- package/dist/cjs/global-account/react/hooks/useUserQuery.d.ts +2 -2
- package/dist/cjs/global-account/react/stores/useModalStore.d.ts +1 -7
- package/dist/cjs/shared/constants/chains/supported.d.ts +1 -1
- package/dist/cjs/shared/utils/ipfs.js +10 -3
- package/dist/esm/anyspend/react/components/AnySpend.js +5 -3
- package/dist/esm/anyspend/react/components/AnySpendCustom.js +1 -1
- package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +1 -1
- package/dist/esm/anyspend/react/components/AnyspendDepositHype.js +1 -1
- package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
- package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.js +5 -3
- package/dist/esm/anyspend/react/components/common/PanelOnrampPayment.js +1 -1
- package/dist/esm/anyspend/react/hooks/useSigMint.d.ts +1 -1
- package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
- package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.js +151 -41
- package/dist/esm/global-account/react/components/B3DynamicModal.js +1 -9
- package/dist/esm/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
- package/dist/esm/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.js +34 -0
- package/dist/esm/global-account/react/components/ManageAccount/BalanceContent.js +4 -3
- package/dist/esm/global-account/react/components/ManageAccount/HomeActions.js +1 -1
- package/dist/esm/global-account/react/components/ManageAccount/ManageAccount.js +1 -1
- package/dist/esm/global-account/react/components/ManageAccount/ProfileSection.js +6 -3
- package/dist/esm/global-account/react/components/ManageAccount/SettingsContent.js +1 -1
- package/dist/esm/global-account/react/components/ManageAccount/SettingsProfileCard.js +74 -9
- package/dist/esm/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
- package/dist/esm/global-account/react/components/ModalHeader/ModalHeader.js +2 -2
- package/dist/esm/global-account/react/components/SignInWithB3/SignIn.js +4 -2
- package/dist/esm/global-account/react/components/index.d.ts +1 -2
- package/dist/esm/global-account/react/components/index.js +3 -4
- package/dist/esm/global-account/react/components/ui/drawer.js +1 -1
- package/dist/esm/global-account/react/hooks/useAccountWallet.d.ts +1 -0
- package/dist/esm/global-account/react/hooks/useAccountWallet.js +17 -0
- package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +2 -2
- package/dist/esm/global-account/react/hooks/useUserQuery.d.ts +2 -2
- package/dist/esm/global-account/react/stores/useModalStore.d.ts +1 -7
- package/dist/esm/shared/constants/chains/supported.d.ts +1 -1
- package/dist/esm/shared/utils/ipfs.js +10 -3
- package/dist/styles/index.css +1 -1
- package/dist/types/anyspend/react/components/common/CryptoPaymentMethod.d.ts +0 -6
- package/dist/types/anyspend/react/hooks/useSigMint.d.ts +1 -1
- package/dist/types/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
- package/dist/types/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +39 -0
- package/dist/types/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
- package/dist/types/global-account/react/components/index.d.ts +1 -2
- package/dist/types/global-account/react/hooks/useAccountWallet.d.ts +1 -0
- package/dist/types/global-account/react/hooks/useAuthentication.d.ts +2 -2
- package/dist/types/global-account/react/hooks/useUserQuery.d.ts +2 -2
- package/dist/types/global-account/react/stores/useModalStore.d.ts +1 -7
- package/dist/types/shared/constants/chains/supported.d.ts +1 -1
- package/package.json +2 -1
- package/src/anyspend/react/components/AnySpend.tsx +5 -4
- package/src/anyspend/react/components/AnySpendCustom.tsx +0 -2
- package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +0 -2
- package/src/anyspend/react/components/AnyspendDepositHype.tsx +0 -2
- package/src/anyspend/react/components/common/CryptoPaymentMethod.tsx +7 -14
- package/src/anyspend/react/components/common/PanelOnrampPayment.tsx +1 -1
- package/src/global-account/react/components/AvatarEditor/AvatarEditor.tsx +251 -79
- package/src/global-account/react/components/B3DynamicModal.tsx +3 -11
- package/src/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.tsx +84 -0
- package/src/global-account/react/components/ManageAccount/BalanceContent.tsx +4 -7
- package/src/global-account/react/components/ManageAccount/HomeActions.tsx +1 -1
- package/src/global-account/react/components/ManageAccount/ManageAccount.tsx +2 -2
- package/src/global-account/react/components/ManageAccount/ProfileSection.tsx +14 -11
- package/src/global-account/react/components/ManageAccount/SettingsContent.tsx +1 -1
- package/src/global-account/react/components/ManageAccount/SettingsProfileCard.tsx +129 -23
- package/src/global-account/react/components/ModalHeader/ModalHeader.tsx +16 -8
- package/src/global-account/react/components/SignInWithB3/SignIn.tsx +11 -7
- package/src/global-account/react/components/index.ts +3 -4
- package/src/global-account/react/components/ui/drawer.tsx +1 -1
- package/src/global-account/react/hooks/useAccountWallet.tsx +26 -0
- package/src/global-account/react/stores/useModalStore.ts +1 -9
- package/src/shared/utils/ipfs.ts +10 -3
- package/dist/cjs/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
- package/dist/cjs/global-account/react/components/ProfileEditor/ProfileEditor.js +0 -141
- package/dist/esm/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
- package/dist/esm/global-account/react/components/ProfileEditor/ProfileEditor.js +0 -135
- package/dist/types/global-account/react/components/ProfileEditor/ProfileEditor.d.ts +0 -6
- 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.
|
|
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={
|
|
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
|
-
{
|
|
345
|
-
<img src={
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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: [
|
|
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 =
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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 || !
|
|
567
|
+
disabled={isLoading || (!selectedFile && !selectedProfileType)}
|
|
396
568
|
className="flex-1 rounded-xl bg-[#3368ef] text-white hover:bg-[#2952cc]"
|
|
397
569
|
>
|
|
398
570
|
{isLoading ? (
|