@b3dotfun/sdk 0.0.35-alpha.3 → 0.0.35-alpha.5
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/global-account/bsmnt.d.ts +2 -0
- package/dist/cjs/global-account/bsmnt.js +42 -1
- package/dist/cjs/global-account/react/components/AvatarCreator/AvatarCreator.d.ts +6 -0
- package/dist/cjs/global-account/react/components/AvatarCreator/AvatarCreator.js +55 -0
- package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +6 -0
- package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.js +108 -0
- package/dist/cjs/global-account/react/components/B3DynamicModal.js +9 -1
- package/dist/cjs/global-account/react/components/LinkAccount/LinkAccount.d.ts +3 -1
- package/dist/cjs/global-account/react/components/LinkAccount/LinkAccount.js +21 -11
- package/dist/cjs/global-account/react/components/ManageAccount/BalanceContent.js +16 -2
- package/dist/cjs/global-account/react/components/icons/AppleIcon.d.ts +9 -0
- package/dist/cjs/global-account/react/components/icons/AppleIcon.js +8 -0
- package/dist/cjs/global-account/react/components/icons/DiscordIcon.d.ts +9 -0
- package/dist/cjs/global-account/react/components/icons/DiscordIcon.js +8 -0
- package/dist/cjs/global-account/react/components/icons/FarcasterIcon.d.ts +9 -0
- package/dist/cjs/global-account/react/components/icons/FarcasterIcon.js +8 -0
- package/dist/cjs/global-account/react/components/icons/GoogleIcon.d.ts +8 -0
- package/dist/cjs/global-account/react/components/icons/GoogleIcon.js +8 -0
- package/dist/cjs/global-account/react/components/icons/XIcon.d.ts +9 -0
- package/dist/cjs/global-account/react/components/icons/XIcon.js +8 -0
- package/dist/cjs/global-account/react/hooks/useAccountWallet.js +3 -2
- package/dist/cjs/global-account/react/hooks/useAuthentication.js +7 -0
- package/dist/cjs/global-account/react/hooks/useProfile.d.ts +1 -1
- package/dist/cjs/global-account/react/hooks/useRPMToken.d.ts +7 -0
- package/dist/cjs/global-account/react/hooks/useRPMToken.js +11 -0
- package/dist/cjs/global-account/react/stores/useModalStore.d.ts +7 -1
- package/dist/cjs/global-account/react/utils/updateAvatar.d.ts +4 -0
- package/dist/cjs/global-account/react/utils/updateAvatar.js +54 -0
- package/dist/esm/global-account/bsmnt.d.ts +2 -0
- package/dist/esm/global-account/bsmnt.js +39 -0
- package/dist/esm/global-account/react/components/AvatarCreator/AvatarCreator.d.ts +6 -0
- package/dist/esm/global-account/react/components/AvatarCreator/AvatarCreator.js +52 -0
- package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +6 -0
- package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.js +102 -0
- package/dist/esm/global-account/react/components/B3DynamicModal.js +9 -1
- package/dist/esm/global-account/react/components/LinkAccount/LinkAccount.d.ts +3 -1
- package/dist/esm/global-account/react/components/LinkAccount/LinkAccount.js +22 -12
- package/dist/esm/global-account/react/components/ManageAccount/BalanceContent.js +17 -3
- package/dist/esm/global-account/react/components/icons/AppleIcon.d.ts +9 -0
- package/dist/esm/global-account/react/components/icons/AppleIcon.js +5 -0
- package/dist/esm/global-account/react/components/icons/DiscordIcon.d.ts +9 -0
- package/dist/esm/global-account/react/components/icons/DiscordIcon.js +5 -0
- package/dist/esm/global-account/react/components/icons/FarcasterIcon.d.ts +9 -0
- package/dist/esm/global-account/react/components/icons/FarcasterIcon.js +5 -0
- package/dist/esm/global-account/react/components/icons/GoogleIcon.d.ts +8 -0
- package/dist/esm/global-account/react/components/icons/GoogleIcon.js +5 -0
- package/dist/esm/global-account/react/components/icons/XIcon.d.ts +9 -0
- package/dist/esm/global-account/react/components/icons/XIcon.js +5 -0
- package/dist/esm/global-account/react/hooks/useAccountWallet.js +3 -2
- package/dist/esm/global-account/react/hooks/useAuthentication.js +7 -0
- package/dist/esm/global-account/react/hooks/useProfile.d.ts +1 -1
- package/dist/esm/global-account/react/hooks/useRPMToken.d.ts +7 -0
- package/dist/esm/global-account/react/hooks/useRPMToken.js +8 -0
- package/dist/esm/global-account/react/stores/useModalStore.d.ts +7 -1
- package/dist/esm/global-account/react/utils/updateAvatar.d.ts +4 -0
- package/dist/esm/global-account/react/utils/updateAvatar.js +18 -0
- package/dist/styles/index.css +1 -1
- package/dist/types/global-account/bsmnt.d.ts +2 -0
- package/dist/types/global-account/react/components/AvatarCreator/AvatarCreator.d.ts +6 -0
- package/dist/types/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +6 -0
- package/dist/types/global-account/react/components/LinkAccount/LinkAccount.d.ts +3 -1
- package/dist/types/global-account/react/components/icons/AppleIcon.d.ts +9 -0
- package/dist/types/global-account/react/components/icons/DiscordIcon.d.ts +9 -0
- package/dist/types/global-account/react/components/icons/FarcasterIcon.d.ts +9 -0
- package/dist/types/global-account/react/components/icons/GoogleIcon.d.ts +8 -0
- package/dist/types/global-account/react/components/icons/XIcon.d.ts +9 -0
- package/dist/types/global-account/react/hooks/useProfile.d.ts +1 -1
- package/dist/types/global-account/react/hooks/useRPMToken.d.ts +7 -0
- package/dist/types/global-account/react/stores/useModalStore.d.ts +7 -1
- package/dist/types/global-account/react/utils/updateAvatar.d.ts +4 -0
- package/package.json +6 -5
- package/src/global-account/bsmnt.ts +47 -0
- package/src/global-account/react/components/AvatarCreator/AvatarCreator.tsx +90 -0
- package/src/global-account/react/components/AvatarEditor/AvatarEditor.tsx +233 -0
- package/src/global-account/react/components/B3DynamicModal.tsx +27 -2
- package/src/global-account/react/components/LinkAccount/LinkAccount.tsx +40 -18
- package/src/global-account/react/components/ManageAccount/BalanceContent.tsx +25 -5
- package/src/global-account/react/components/ManageAccount/ManageAccount.tsx +0 -1
- package/src/global-account/react/components/icons/AppleIcon.tsx +33 -0
- package/src/global-account/react/components/icons/DiscordIcon.tsx +32 -0
- package/src/global-account/react/components/icons/FarcasterIcon.tsx +37 -0
- package/src/global-account/react/components/icons/GoogleIcon.tsx +40 -0
- package/src/global-account/react/components/icons/XIcon.tsx +28 -0
- package/src/global-account/react/hooks/useAccountWallet.tsx +3 -2
- package/src/global-account/react/hooks/useAuthentication.ts +9 -0
- package/src/global-account/react/hooks/useProfile.ts +1 -1
- package/src/global-account/react/hooks/useRPMToken.ts +17 -0
- package/src/global-account/react/stores/useModalStore.ts +9 -1
- package/src/global-account/react/utils/updateAvatar.ts +21 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface XIconProps {
|
|
3
|
+
className?: string;
|
|
4
|
+
size?: number;
|
|
5
|
+
color?: string;
|
|
6
|
+
style?: React.CSSProperties;
|
|
7
|
+
}
|
|
8
|
+
export declare function XIcon({ className, size, color, style }: XIconProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export default XIcon;
|
|
@@ -291,10 +291,16 @@ export interface AnySpendDepositHypeProps extends BaseModalProps {
|
|
|
291
291
|
/** Callback function called when the deposit is successful */
|
|
292
292
|
onSuccess?: (amount?: string) => void;
|
|
293
293
|
}
|
|
294
|
+
export interface AvatarEditorModalProps extends BaseModalProps {
|
|
295
|
+
/** Modal type identifier */
|
|
296
|
+
type: "avatarEditor";
|
|
297
|
+
/** Callback function called when avatar is successfully set */
|
|
298
|
+
onSuccess?: () => void;
|
|
299
|
+
}
|
|
294
300
|
/**
|
|
295
301
|
* Union type of all possible modal content types
|
|
296
302
|
*/
|
|
297
|
-
export type ModalContentType = SignInWithB3ModalProps | RequestPermissionsModalProps | ManageAccountModalProps | AnySpendModalProps | AnyspendOrderDetailsProps | AnySpendNftProps | AnySpendJoinTournamentProps | AnySpendFundTournamentProps | AnySpendOrderHistoryProps | AnySpendStakeB3Props | AnySpendBuySpinProps | AnySpendSignatureMintProps | AnySpendBondKitProps | LinkAccountModalProps | AnySpendDepositHypeProps;
|
|
303
|
+
export type ModalContentType = SignInWithB3ModalProps | RequestPermissionsModalProps | ManageAccountModalProps | AnySpendModalProps | AnyspendOrderDetailsProps | AnySpendNftProps | AnySpendJoinTournamentProps | AnySpendFundTournamentProps | AnySpendOrderHistoryProps | AnySpendStakeB3Props | AnySpendBuySpinProps | AnySpendSignatureMintProps | AnySpendBondKitProps | LinkAccountModalProps | AnySpendDepositHypeProps | AvatarEditorModalProps;
|
|
298
304
|
/**
|
|
299
305
|
* State interface for the modal store
|
|
300
306
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b3dotfun/sdk",
|
|
3
|
-
"version": "0.0.35-alpha.
|
|
3
|
+
"version": "0.0.35-alpha.5",
|
|
4
4
|
"source": "src/index.ts",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"react-native": "./dist/cjs/index.native.js",
|
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
"constants"
|
|
238
238
|
],
|
|
239
239
|
"dependencies": {
|
|
240
|
-
"@b3dotfun/b3-api": "0.0.
|
|
240
|
+
"@b3dotfun/b3-api": "0.0.49",
|
|
241
241
|
"@b3dotfun/basement-api": "0.0.11",
|
|
242
242
|
"@feathersjs/authentication-client": "5.0.33",
|
|
243
243
|
"@feathersjs/feathers": "5.0.33",
|
|
@@ -321,9 +321,10 @@
|
|
|
321
321
|
"peerDependencies": {
|
|
322
322
|
"@fingerprintjs/fingerprintjs-pro-react": "^2.7.0",
|
|
323
323
|
"@privy-io/react-auth": "^2.8.0",
|
|
324
|
-
"@react-three/postprocessing": "
|
|
325
|
-
"@readyplayerme/
|
|
326
|
-
"@
|
|
324
|
+
"@react-three/postprocessing": "2.16.6",
|
|
325
|
+
"@readyplayerme/react-avatar-creator": "0.5.0",
|
|
326
|
+
"@readyplayerme/visage": "6.10.0",
|
|
327
|
+
"@tanstack/react-query": "5.55.0",
|
|
327
328
|
"react": "^18.0.0 || ^19.0.0",
|
|
328
329
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
329
330
|
"react-native-mmkv": "^3.2.0",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { createClient } from "@b3dotfun/basement-api";
|
|
2
|
+
import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
|
|
2
3
|
import { AuthenticationClient } from "@feathersjs/authentication-client";
|
|
3
4
|
import socketio from "@feathersjs/socketio-client";
|
|
4
5
|
import Cookies from "js-cookie";
|
|
5
6
|
import io from "socket.io-client";
|
|
6
7
|
|
|
8
|
+
const debug = debugB3React("bsmnt");
|
|
9
|
+
|
|
7
10
|
// Also use b3 auth token since we are using b3-jwt strategy
|
|
8
11
|
const BSMNT_AUTH_COOKIE_NAME = "b3-auth";
|
|
9
12
|
|
|
@@ -70,4 +73,48 @@ export const resetSocket = () => {
|
|
|
70
73
|
// reset the socket connection
|
|
71
74
|
};
|
|
72
75
|
|
|
76
|
+
export function extractAvatarIdFromUrl(url: string): string | null {
|
|
77
|
+
const regex = /https:\/\/models\.readyplayer\.me\/([a-f0-9]{24})\.[a-zA-Z0-9]+/;
|
|
78
|
+
const match = url.match(regex);
|
|
79
|
+
return match ? match[1] : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const authenticateWithB3JWT = async (fullToken: string, params?: Record<string, any>) => {
|
|
83
|
+
// Do not authenticate if there is no token
|
|
84
|
+
if (!fullToken) {
|
|
85
|
+
console.log("No token found, not authenticating");
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
debug("Authenticating with token:12", fullToken);
|
|
90
|
+
try {
|
|
91
|
+
const response = await app.authenticate(
|
|
92
|
+
{
|
|
93
|
+
strategy: "b3-jwt",
|
|
94
|
+
accessToken: fullToken,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
query: params || {},
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
debug("Authenticated", response);
|
|
101
|
+
|
|
102
|
+
// Store streamToken if it exists in the response
|
|
103
|
+
if (response?.streamToken) {
|
|
104
|
+
Cookies.set("stream-token", response.streamToken, {
|
|
105
|
+
expires: new Date(response.authExpires),
|
|
106
|
+
secure: process.env.NODE_ENV === "production",
|
|
107
|
+
sameSite: "strict",
|
|
108
|
+
});
|
|
109
|
+
debug("Stream token stored in cookies");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return response;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
debug(`Authentication error:5`, error);
|
|
115
|
+
debug("Authentication JWT", fullToken);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
73
120
|
export default app;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useProfile } from "@b3dotfun/sdk/global-account/react";
|
|
4
|
+
import { useRPMToken } from "@b3dotfun/sdk/global-account/react/hooks/useRPMToken";
|
|
5
|
+
import { updateAvatar } from "@b3dotfun/sdk/global-account/react/utils/updateAvatar";
|
|
6
|
+
import { cn } from "@b3dotfun/sdk/shared/utils/cn";
|
|
7
|
+
import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
|
|
8
|
+
import {
|
|
9
|
+
AvatarCreatorConfig,
|
|
10
|
+
AvatarCreator as AvatarCreatorRPM,
|
|
11
|
+
AvatarExportedEvent,
|
|
12
|
+
} from "@readyplayerme/react-avatar-creator";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { toast } from "sonner";
|
|
15
|
+
import { useActiveAccount } from "thirdweb/react";
|
|
16
|
+
|
|
17
|
+
const debug = debugB3React("AvatarCreator");
|
|
18
|
+
|
|
19
|
+
const config: AvatarCreatorConfig = {
|
|
20
|
+
clearCache: true,
|
|
21
|
+
bodyType: "fullbody",
|
|
22
|
+
quickStart: true,
|
|
23
|
+
language: "en",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
interface AvatarCreatorProps {
|
|
27
|
+
onSetAvatar?: () => void;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function AvatarCreator({ onSetAvatar, className }: AvatarCreatorProps) {
|
|
32
|
+
const { token, refetch: refetchRPMToken } = useRPMToken();
|
|
33
|
+
const [loading, setIsLoading] = useState(false);
|
|
34
|
+
const account = useActiveAccount();
|
|
35
|
+
const { data: profile, refetch: refreshProfile } = useProfile({
|
|
36
|
+
address: account?.address,
|
|
37
|
+
fresh: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const hasAvatar = profile?.avatar;
|
|
41
|
+
|
|
42
|
+
const handleOnAvatarExported = async (event: AvatarExportedEvent) => {
|
|
43
|
+
setIsLoading(true);
|
|
44
|
+
debug("@@AvatarExportedEvent", event);
|
|
45
|
+
try {
|
|
46
|
+
const avatarUpload = await updateAvatar(event.data.url);
|
|
47
|
+
debug("@@avatarUpload", avatarUpload);
|
|
48
|
+
|
|
49
|
+
await refreshProfile();
|
|
50
|
+
toast.success(
|
|
51
|
+
hasAvatar ? "Nice look! Your avatar has been updated!" : "Looks great! Your avatar has been saved!",
|
|
52
|
+
);
|
|
53
|
+
onSetAvatar?.();
|
|
54
|
+
await refetchRPMToken(undefined);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
debug("@@error:AvatarCreator", e);
|
|
57
|
+
toast.error("Failed to update avatar. Please try again.");
|
|
58
|
+
}
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (loading) {
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex h-[80vh] w-full flex-col items-center justify-center gap-4">
|
|
65
|
+
<div className="border-primary h-8 w-8 animate-spin rounded-full border-4 border-t-transparent" />
|
|
66
|
+
<p className="text-muted-foreground text-sm font-medium">Saving your avatar</p>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!token) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex h-[80vh] w-full flex-col items-center justify-center gap-4">
|
|
74
|
+
<div className="border-primary h-8 w-8 animate-spin rounded-full border-4 border-t-transparent" />
|
|
75
|
+
<p className="text-muted-foreground text-sm font-medium">Loading avatar creator</p>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className={cn("h-[calc(90vh-2px)] w-full", className)}>
|
|
82
|
+
<AvatarCreatorRPM
|
|
83
|
+
className="h-full w-full"
|
|
84
|
+
subdomain="b3"
|
|
85
|
+
config={{ ...config, token }}
|
|
86
|
+
onAvatarExported={handleOnAvatarExported}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import app from "@b3dotfun/sdk/global-account/app";
|
|
4
|
+
import { Button, useB3, useProfile } from "@b3dotfun/sdk/global-account/react";
|
|
5
|
+
import { cn } from "@b3dotfun/sdk/shared/utils/cn";
|
|
6
|
+
import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
|
|
7
|
+
import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
|
|
8
|
+
import { Check, Loader2, Upload, X } from "lucide-react";
|
|
9
|
+
import { useRef, useState } from "react";
|
|
10
|
+
import { toast } from "sonner";
|
|
11
|
+
import { useActiveAccount } from "thirdweb/react";
|
|
12
|
+
import { upload } from "thirdweb/storage";
|
|
13
|
+
|
|
14
|
+
const debug = debugB3React("AvatarEditor");
|
|
15
|
+
|
|
16
|
+
interface AvatarEditorProps {
|
|
17
|
+
onSetAvatar?: () => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
|
|
22
|
+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
23
|
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
24
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
25
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
26
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
27
|
+
const { setUser } = useB3();
|
|
28
|
+
|
|
29
|
+
const account = useActiveAccount();
|
|
30
|
+
const { data: profile, refetch: refreshProfile } = useProfile({
|
|
31
|
+
address: account?.address,
|
|
32
|
+
fresh: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Thirdweb upload function
|
|
36
|
+
|
|
37
|
+
const hasAvatar = profile?.avatar;
|
|
38
|
+
|
|
39
|
+
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
40
|
+
const file = event.target.files?.[0];
|
|
41
|
+
if (file) {
|
|
42
|
+
// Validate file type
|
|
43
|
+
if (!file.type.startsWith("image/")) {
|
|
44
|
+
toast.error("Please select an image file");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validate file size (max 5MB)
|
|
49
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
50
|
+
toast.error("File size must be less than 5MB");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setSelectedFile(file);
|
|
55
|
+
|
|
56
|
+
// Create preview URL
|
|
57
|
+
const url = URL.createObjectURL(file);
|
|
58
|
+
setPreviewUrl(url);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleRemoveFile = () => {
|
|
63
|
+
setSelectedFile(null);
|
|
64
|
+
if (previewUrl) {
|
|
65
|
+
URL.revokeObjectURL(previewUrl);
|
|
66
|
+
setPreviewUrl(null);
|
|
67
|
+
}
|
|
68
|
+
if (fileInputRef.current) {
|
|
69
|
+
fileInputRef.current.value = "";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleUpload = async () => {
|
|
74
|
+
if (!selectedFile) {
|
|
75
|
+
toast.error("Please select an image first");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setIsUploading(true);
|
|
80
|
+
try {
|
|
81
|
+
debug("Starting upload to IPFS", selectedFile);
|
|
82
|
+
|
|
83
|
+
// Upload to IPFS using Thirdweb
|
|
84
|
+
const ipfsUrl = await upload({
|
|
85
|
+
client,
|
|
86
|
+
files: [selectedFile],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
debug("Upload successful", ipfsUrl);
|
|
90
|
+
|
|
91
|
+
// Save avatar URL using profiles service
|
|
92
|
+
setIsSaving(true);
|
|
93
|
+
const user = await app.service("users").setAvatar(
|
|
94
|
+
{
|
|
95
|
+
avatar: ipfsUrl,
|
|
96
|
+
},
|
|
97
|
+
// @ts-expect-error - our typed client is expecting context even though it's set elsewhere
|
|
98
|
+
{},
|
|
99
|
+
);
|
|
100
|
+
// update user
|
|
101
|
+
// @ts-expect-error this resolved fine, look into why expect-error needed
|
|
102
|
+
setUser(user);
|
|
103
|
+
|
|
104
|
+
// Refresh profile to get updated avatar
|
|
105
|
+
await refreshProfile();
|
|
106
|
+
|
|
107
|
+
toast.success(
|
|
108
|
+
hasAvatar ? "Nice look! Your avatar has been updated!" : "Looks great! Your avatar has been saved!",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
onSetAvatar?.();
|
|
112
|
+
|
|
113
|
+
// Clean up
|
|
114
|
+
handleRemoveFile();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
debug("Error uploading avatar:", error);
|
|
117
|
+
toast.error("Failed to upload avatar. Please try again.");
|
|
118
|
+
} finally {
|
|
119
|
+
setIsUploading(false);
|
|
120
|
+
setIsSaving(false);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleFileInputClick = () => {
|
|
125
|
+
fileInputRef.current?.click();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const isLoading = isUploading || isSaving;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className={cn("flex flex-col items-center justify-center space-y-6 p-8", className)}>
|
|
132
|
+
<div className="space-y-2 text-center">
|
|
133
|
+
<h2 className="font-neue-montreal-semibold text-b3-grey text-2xl">
|
|
134
|
+
{hasAvatar ? "Update Your Avatar" : "Set Your Avatar"}
|
|
135
|
+
</h2>
|
|
136
|
+
<p className="text-b3-foreground-muted font-neue-montreal-medium">
|
|
137
|
+
Upload an image to personalize your profile
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Current Avatar Display */}
|
|
142
|
+
{hasAvatar && !previewUrl && (
|
|
143
|
+
<div className="relative">
|
|
144
|
+
<div className="border-b3-primary-blue h-32 w-32 overflow-hidden rounded-full border-4">
|
|
145
|
+
<img src={profile.avatar} alt="Current avatar" className="h-full w-full object-cover" />
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{/* File Upload Area */}
|
|
151
|
+
<div className="w-full max-w-md">
|
|
152
|
+
{!selectedFile ? (
|
|
153
|
+
<div
|
|
154
|
+
onClick={handleFileInputClick}
|
|
155
|
+
className="border-b3-line hover:border-b3-primary-blue hover:bg-b3-primary-wash/20 cursor-pointer rounded-xl border-2 border-dashed p-8 text-center transition-colors"
|
|
156
|
+
>
|
|
157
|
+
<Upload className="text-b3-grey mx-auto mb-4 h-12 w-12" />
|
|
158
|
+
<p className="text-b3-grey font-neue-montreal-semibold mb-2">Click to select an image</p>
|
|
159
|
+
<p className="text-b3-foreground-muted font-neue-montreal-medium text-sm">PNG, JPG, or GIF up to 5MB</p>
|
|
160
|
+
</div>
|
|
161
|
+
) : (
|
|
162
|
+
<div className="space-y-4">
|
|
163
|
+
{/* Preview */}
|
|
164
|
+
<div className="relative">
|
|
165
|
+
<div className="border-b3-primary-blue mx-auto h-32 w-32 overflow-hidden rounded-full border-4">
|
|
166
|
+
{previewUrl ? (
|
|
167
|
+
<img src={previewUrl} alt="Preview" className="h-full w-full object-cover" />
|
|
168
|
+
) : (
|
|
169
|
+
<div className="bg-b3-primary-wash flex h-full w-full items-center justify-center rounded-full">
|
|
170
|
+
<p className="text-b3-grey font-neue-montreal-semibold text-sm">No file selected</p>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
<button
|
|
175
|
+
onClick={handleRemoveFile}
|
|
176
|
+
className="bg-b3-negative absolute -right-2 -top-2 flex h-8 w-8 items-center justify-center rounded-full text-white transition-colors hover:bg-red-600"
|
|
177
|
+
disabled={isLoading}
|
|
178
|
+
>
|
|
179
|
+
<X size={16} />
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* File Info */}
|
|
184
|
+
<div className="space-y-1 text-center">
|
|
185
|
+
<p className="text-b3-grey font-neue-montreal-semibold text-sm">{selectedFile.name}</p>
|
|
186
|
+
<p className="text-b3-foreground-muted font-neue-montreal-medium text-xs">
|
|
187
|
+
{(selectedFile.size / 1024 / 1024).toFixed(2)} MB
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{/* Hidden file input */}
|
|
194
|
+
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleFileSelect} className="hidden" />
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Action Buttons */}
|
|
198
|
+
<div className="flex w-full max-w-md gap-3">
|
|
199
|
+
{selectedFile && (
|
|
200
|
+
<Button
|
|
201
|
+
onClick={handleUpload}
|
|
202
|
+
disabled={isLoading}
|
|
203
|
+
className="bg-b3-primary-blue hover:bg-b3-primary-blue/90 flex-1 text-white"
|
|
204
|
+
>
|
|
205
|
+
{isLoading ? (
|
|
206
|
+
<>
|
|
207
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
208
|
+
{isUploading ? "Uploading..." : "Saving..."}
|
|
209
|
+
</>
|
|
210
|
+
) : (
|
|
211
|
+
<>
|
|
212
|
+
<Check className="mr-2 h-4 w-4" />
|
|
213
|
+
{hasAvatar ? "Update Avatar" : "Set Avatar"}
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
</Button>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
<Button variant="outline" onClick={handleFileInputClick} disabled={isLoading} className="flex-1">
|
|
220
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
221
|
+
{selectedFile ? "Change Image" : "Select Image"}
|
|
222
|
+
</Button>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* Help Text */}
|
|
226
|
+
<div className="text-b3-foreground-muted font-neue-montreal-medium max-w-md text-center text-xs">
|
|
227
|
+
<p>
|
|
228
|
+
Your avatar will be uploaded to IPFS and stored securely. Make sure you have the rights to use this image.
|
|
229
|
+
</p>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
@@ -12,6 +12,7 @@ import { AnySpendDepositHype } from "@b3dotfun/sdk/anyspend/react/components/Any
|
|
|
12
12
|
import { useIsMobile, useModalStore } from "@b3dotfun/sdk/global-account/react";
|
|
13
13
|
import { cn } from "@b3dotfun/sdk/shared/utils/cn";
|
|
14
14
|
import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
|
|
15
|
+
import { AvatarEditor } from "./AvatarEditor/AvatarEditor";
|
|
15
16
|
import { useB3 } from "./B3Provider/useB3";
|
|
16
17
|
import { LinkAccount } from "./LinkAccount/LinkAccount";
|
|
17
18
|
import { ManageAccount } from "./ManageAccount/ManageAccount";
|
|
@@ -40,6 +41,7 @@ export function B3DynamicModal() {
|
|
|
40
41
|
"anySpendSignatureMint",
|
|
41
42
|
"anySpendBondKit",
|
|
42
43
|
"linkAccount",
|
|
44
|
+
"avatarEditor",
|
|
43
45
|
];
|
|
44
46
|
|
|
45
47
|
const freestyleTypes = [
|
|
@@ -65,6 +67,9 @@ export function B3DynamicModal() {
|
|
|
65
67
|
isFreestyleType && "b3-modal-freestyle",
|
|
66
68
|
contentType?.type === "signInWithB3" && "p-0",
|
|
67
69
|
contentType?.type === "anySpend" && "md:px-6",
|
|
70
|
+
// Add specific styles for avatar editor
|
|
71
|
+
// contentType?.type === "avatarEditor_disabled" &&
|
|
72
|
+
// "h-[90dvh] w-[90vw] bg-black p-0 overflow-y-auto overflow-x-hidden max-md:-mt-8 max-md:rounded-t-xl",
|
|
68
73
|
);
|
|
69
74
|
|
|
70
75
|
debug("contentType", contentType);
|
|
@@ -102,6 +107,8 @@ export function B3DynamicModal() {
|
|
|
102
107
|
return <LinkAccount {...contentType} />;
|
|
103
108
|
case "anySpendDepositHype":
|
|
104
109
|
return <AnySpendDepositHype {...contentType} mode="modal" />;
|
|
110
|
+
case "avatarEditor":
|
|
111
|
+
return <AvatarEditor onSetAvatar={contentType.onSuccess} />;
|
|
105
112
|
// Add other modal types here
|
|
106
113
|
default:
|
|
107
114
|
return null;
|
|
@@ -120,8 +127,10 @@ export function B3DynamicModal() {
|
|
|
120
127
|
contentClass,
|
|
121
128
|
"rounded-2xl bg-white shadow-xl dark:bg-gray-900",
|
|
122
129
|
"border border-gray-200 dark:border-gray-800",
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
// Remove default width classes for avatar editor
|
|
131
|
+
contentType?.type === "avatarEditor"
|
|
132
|
+
? "!w-[90vw] !max-w-none" // Use !important to override default styles
|
|
133
|
+
: "mx-auto w-full max-w-md sm:max-w-lg",
|
|
125
134
|
)}
|
|
126
135
|
hideCloseButton={hideCloseButton}
|
|
127
136
|
>
|
|
@@ -155,6 +164,22 @@ export function B3DynamicModal() {
|
|
|
155
164
|
{renderContent()}
|
|
156
165
|
</div>
|
|
157
166
|
</ModalContent>
|
|
167
|
+
{contentType?.type === "avatarEditor" && (
|
|
168
|
+
<button
|
|
169
|
+
onClick={() => setB3ModalOpen(false)}
|
|
170
|
+
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"
|
|
171
|
+
>
|
|
172
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
173
|
+
<path
|
|
174
|
+
d="M18 6L6 18M6 6L18 18"
|
|
175
|
+
stroke="currentColor"
|
|
176
|
+
strokeWidth="2"
|
|
177
|
+
strokeLinecap="round"
|
|
178
|
+
strokeLinejoin="round"
|
|
179
|
+
/>
|
|
180
|
+
</svg>
|
|
181
|
+
</button>
|
|
182
|
+
)}
|
|
158
183
|
</ModalComponent>
|
|
159
184
|
);
|
|
160
185
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import app from "@b3dotfun/sdk/global-account/app";
|
|
1
2
|
import { ecosystemWalletId } from "@b3dotfun/sdk/shared/constants";
|
|
2
3
|
import { client } from "@b3dotfun/sdk/shared/utils/thirdweb";
|
|
3
|
-
import { Loader2 } from "lucide-react";
|
|
4
|
+
import { Loader2, Mail, Phone } from "lucide-react";
|
|
4
5
|
import { useCallback, useEffect, useState } from "react";
|
|
5
6
|
import { toast } from "sonner";
|
|
6
7
|
import { useLinkProfile, useProfiles } from "thirdweb/react";
|
|
@@ -8,8 +9,12 @@ import { preAuthenticate } from "thirdweb/wallets";
|
|
|
8
9
|
import { LinkAccountModalProps, useModalStore } from "../../stores/useModalStore";
|
|
9
10
|
import { getProfileDisplayInfo } from "../../utils/profileDisplay";
|
|
10
11
|
import { useB3 } from "../B3Provider/useB3";
|
|
12
|
+
import { AppleIcon } from "../icons/AppleIcon";
|
|
13
|
+
import { DiscordIcon } from "../icons/DiscordIcon";
|
|
14
|
+
import { FarcasterIcon } from "../icons/FarcasterIcon";
|
|
15
|
+
import { GoogleIcon } from "../icons/GoogleIcon";
|
|
16
|
+
import { XIcon } from "../icons/XIcon";
|
|
11
17
|
import { Button } from "../ui/button";
|
|
12
|
-
import app from "@b3dotfun/sdk/global-account/app";
|
|
13
18
|
type OTPStrategy = "email" | "phone";
|
|
14
19
|
type SocialStrategy = "google" | "x" | "discord" | "apple" | "farcaster";
|
|
15
20
|
type Strategy = OTPStrategy | SocialStrategy;
|
|
@@ -18,17 +23,22 @@ interface AuthMethod {
|
|
|
18
23
|
id: Strategy;
|
|
19
24
|
label: string;
|
|
20
25
|
enabled: boolean;
|
|
21
|
-
icon
|
|
26
|
+
icon: React.ReactNode;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
const AUTH_METHODS: AuthMethod[] = [
|
|
25
|
-
{ id: "email", label: "Email", enabled: true },
|
|
26
|
-
{ id: "phone", label: "Phone", enabled: true },
|
|
27
|
-
{ id: "google", label: "Google", enabled: true },
|
|
28
|
-
{ id: "x", label: "X (Twitter)", enabled: true },
|
|
29
|
-
{ id: "discord", label: "Discord", enabled: true },
|
|
30
|
-
{ id: "apple", label: "Apple", enabled: true },
|
|
31
|
-
{
|
|
30
|
+
{ id: "email", label: "Email", enabled: true, icon: <Mail className="text-b3-primary-blue size-6" /> },
|
|
31
|
+
{ id: "phone", label: "Phone", enabled: true, icon: <Phone className="text-b3-primary-blue size-6" /> },
|
|
32
|
+
{ id: "google", label: "Google", enabled: true, icon: <GoogleIcon className="size-6" /> },
|
|
33
|
+
{ id: "x", label: "X (Twitter)", enabled: true, icon: <XIcon className="size-6" /> },
|
|
34
|
+
{ id: "discord", label: "Discord", enabled: true, icon: <DiscordIcon className="size-6" /> },
|
|
35
|
+
{ id: "apple", label: "Apple", enabled: true, icon: <AppleIcon className="size-6" /> },
|
|
36
|
+
{
|
|
37
|
+
id: "farcaster",
|
|
38
|
+
label: "Farcaster",
|
|
39
|
+
enabled: true,
|
|
40
|
+
icon: <FarcasterIcon className="size-6" />,
|
|
41
|
+
},
|
|
32
42
|
];
|
|
33
43
|
|
|
34
44
|
export function LinkAccount({
|
|
@@ -37,7 +47,8 @@ export function LinkAccount({
|
|
|
37
47
|
onClose,
|
|
38
48
|
chain,
|
|
39
49
|
partnerId,
|
|
40
|
-
|
|
50
|
+
className,
|
|
51
|
+
}: LinkAccountModalProps & { className?: string }) {
|
|
41
52
|
const { isLinking, linkingMethod, setLinkingState, navigateBack, setB3ModalContentType } = useModalStore();
|
|
42
53
|
const [selectedMethod, setSelectedMethod] = useState<Strategy | null>(null);
|
|
43
54
|
const [email, setEmail] = useState("");
|
|
@@ -288,9 +299,9 @@ export function LinkAccount({
|
|
|
288
299
|
}
|
|
289
300
|
|
|
290
301
|
return (
|
|
291
|
-
<div className=
|
|
292
|
-
<div className="flex items-center justify-between">
|
|
293
|
-
<h2 className="text-b3-grey font-neue-montreal-semibold text-2xl">Link New Account</h2>
|
|
302
|
+
<div className={`b3-link-account space-y-6 p-6 ${className || ""}`} data-testid="link-account">
|
|
303
|
+
<div className="b3-link-account-header flex items-center justify-between">
|
|
304
|
+
<h2 className="b3-link-account-title text-b3-grey font-neue-montreal-semibold text-2xl">Link New Account</h2>
|
|
294
305
|
{selectedMethod && (
|
|
295
306
|
<Button variant="ghost" className="text-b3-grey hover:text-b3-grey/80" onClick={handleBack}>
|
|
296
307
|
Backs
|
|
@@ -299,11 +310,13 @@ export function LinkAccount({
|
|
|
299
310
|
</div>
|
|
300
311
|
|
|
301
312
|
{!selectedMethod ? (
|
|
302
|
-
<div className="grid gap-3">
|
|
313
|
+
<div className="b3-link-account-methods grid gap-3">
|
|
303
314
|
{availableAuthMethods.map(method => (
|
|
304
315
|
<Button
|
|
305
316
|
key={method.id}
|
|
306
|
-
|
|
317
|
+
variant="outline"
|
|
318
|
+
className="b3-link-account-method-button border-b3-line hover:border-b3-primary-blue/30 hover:bg-b3-primary-blue/5 text-b3-grey font-neue-montreal-medium h-14 justify-start bg-transparent px-6 text-base transition-all duration-200"
|
|
319
|
+
data-method={method.id}
|
|
307
320
|
onClick={() => {
|
|
308
321
|
if (method.id === "email" || method.id === "phone") {
|
|
309
322
|
setSelectedMethod(method.id);
|
|
@@ -313,7 +326,16 @@ export function LinkAccount({
|
|
|
313
326
|
}}
|
|
314
327
|
disabled={linkingMethod === method.id}
|
|
315
328
|
>
|
|
316
|
-
{isLinking && linkingMethod === method.id ?
|
|
329
|
+
{isLinking && linkingMethod === method.id ? (
|
|
330
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
331
|
+
) : (
|
|
332
|
+
<div className="b3-link-account-method-content flex items-center gap-4">
|
|
333
|
+
<div className="b3-link-account-method-icon flex items-center justify-center rounded-full">
|
|
334
|
+
{method.icon}
|
|
335
|
+
</div>
|
|
336
|
+
<span className="b3-link-account-method-label font-medium">{method.label}</span>
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
317
339
|
</Button>
|
|
318
340
|
))}
|
|
319
341
|
{availableAuthMethods.length === 0 && (
|
|
@@ -323,7 +345,7 @@ export function LinkAccount({
|
|
|
323
345
|
)}
|
|
324
346
|
</div>
|
|
325
347
|
) : (
|
|
326
|
-
<div className="space-y-4">
|
|
348
|
+
<div className="b3-link-account-form space-y-4">
|
|
327
349
|
{selectedMethod === "email" && (
|
|
328
350
|
<div className="space-y-2">
|
|
329
351
|
<label className="text-b3-grey font-neue-montreal-medium text-sm">Email Address</label>
|