@b3dotfun/sdk 0.0.35-alpha.2 → 0.0.35-alpha.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 (61) hide show
  1. package/dist/cjs/global-account/bsmnt.d.ts +2 -0
  2. package/dist/cjs/global-account/bsmnt.js +42 -1
  3. package/dist/cjs/global-account/react/components/AvatarCreator/AvatarCreator.d.ts +6 -0
  4. package/dist/cjs/global-account/react/components/AvatarCreator/AvatarCreator.js +55 -0
  5. package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +6 -0
  6. package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.js +108 -0
  7. package/dist/cjs/global-account/react/components/B3DynamicModal.js +9 -1
  8. package/dist/cjs/global-account/react/components/ManageAccount/BalanceContent.d.ts +3 -1
  9. package/dist/cjs/global-account/react/components/ManageAccount/BalanceContent.js +19 -5
  10. package/dist/cjs/global-account/react/components/ManageAccount/ManageAccount.d.ts +3 -1
  11. package/dist/cjs/global-account/react/components/ManageAccount/ManageAccount.js +6 -6
  12. package/dist/cjs/global-account/react/hooks/useAccountWallet.js +3 -2
  13. package/dist/cjs/global-account/react/hooks/useAuthentication.js +7 -0
  14. package/dist/cjs/global-account/react/hooks/useProfile.d.ts +1 -1
  15. package/dist/cjs/global-account/react/hooks/useRPMToken.d.ts +7 -0
  16. package/dist/cjs/global-account/react/hooks/useRPMToken.js +11 -0
  17. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +11 -1
  18. package/dist/cjs/global-account/react/utils/updateAvatar.d.ts +4 -0
  19. package/dist/cjs/global-account/react/utils/updateAvatar.js +54 -0
  20. package/dist/esm/global-account/bsmnt.d.ts +2 -0
  21. package/dist/esm/global-account/bsmnt.js +39 -0
  22. package/dist/esm/global-account/react/components/AvatarCreator/AvatarCreator.d.ts +6 -0
  23. package/dist/esm/global-account/react/components/AvatarCreator/AvatarCreator.js +52 -0
  24. package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +6 -0
  25. package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.js +102 -0
  26. package/dist/esm/global-account/react/components/B3DynamicModal.js +9 -1
  27. package/dist/esm/global-account/react/components/ManageAccount/BalanceContent.d.ts +3 -1
  28. package/dist/esm/global-account/react/components/ManageAccount/BalanceContent.js +20 -6
  29. package/dist/esm/global-account/react/components/ManageAccount/ManageAccount.d.ts +3 -1
  30. package/dist/esm/global-account/react/components/ManageAccount/ManageAccount.js +6 -6
  31. package/dist/esm/global-account/react/hooks/useAccountWallet.js +3 -2
  32. package/dist/esm/global-account/react/hooks/useAuthentication.js +7 -0
  33. package/dist/esm/global-account/react/hooks/useProfile.d.ts +1 -1
  34. package/dist/esm/global-account/react/hooks/useRPMToken.d.ts +7 -0
  35. package/dist/esm/global-account/react/hooks/useRPMToken.js +8 -0
  36. package/dist/esm/global-account/react/stores/useModalStore.d.ts +11 -1
  37. package/dist/esm/global-account/react/utils/updateAvatar.d.ts +4 -0
  38. package/dist/esm/global-account/react/utils/updateAvatar.js +18 -0
  39. package/dist/styles/index.css +1 -1
  40. package/dist/types/global-account/bsmnt.d.ts +2 -0
  41. package/dist/types/global-account/react/components/AvatarCreator/AvatarCreator.d.ts +6 -0
  42. package/dist/types/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +6 -0
  43. package/dist/types/global-account/react/components/ManageAccount/BalanceContent.d.ts +3 -1
  44. package/dist/types/global-account/react/components/ManageAccount/ManageAccount.d.ts +3 -1
  45. package/dist/types/global-account/react/hooks/useProfile.d.ts +1 -1
  46. package/dist/types/global-account/react/hooks/useRPMToken.d.ts +7 -0
  47. package/dist/types/global-account/react/stores/useModalStore.d.ts +11 -1
  48. package/dist/types/global-account/react/utils/updateAvatar.d.ts +4 -0
  49. package/package.json +6 -5
  50. package/src/global-account/bsmnt.ts +47 -0
  51. package/src/global-account/react/components/AvatarCreator/AvatarCreator.tsx +90 -0
  52. package/src/global-account/react/components/AvatarEditor/AvatarEditor.tsx +233 -0
  53. package/src/global-account/react/components/B3DynamicModal.tsx +27 -2
  54. package/src/global-account/react/components/ManageAccount/BalanceContent.tsx +63 -35
  55. package/src/global-account/react/components/ManageAccount/ManageAccount.tsx +106 -78
  56. package/src/global-account/react/hooks/useAccountWallet.tsx +3 -2
  57. package/src/global-account/react/hooks/useAuthentication.ts +9 -0
  58. package/src/global-account/react/hooks/useProfile.ts +1 -1
  59. package/src/global-account/react/hooks/useRPMToken.ts +17 -0
  60. package/src/global-account/react/stores/useModalStore.ts +13 -1
  61. package/src/global-account/react/utils/updateAvatar.ts +21 -0
@@ -0,0 +1,4 @@
1
+ export declare function updateAvatar(avatar: string): Promise<{
2
+ success: boolean;
3
+ error?: string;
4
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.0.35-alpha.2",
3
+ "version": "0.0.35-alpha.4",
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.48",
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": "^2.16.6",
325
- "@readyplayerme/visage": "^6.10.0",
326
- "@tanstack/react-query": ">=5.55.0",
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
- "mx-auto w-full max-w-md",
124
- "sm:max-w-lg sm:rounded-b-none",
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
  }
@@ -2,6 +2,7 @@ import {
2
2
  Button,
3
3
  CopyToClipboard,
4
4
  useAuthentication,
5
+ useB3,
5
6
  useB3BalanceFromAddresses,
6
7
  useModalStore,
7
8
  useNativeBalance,
@@ -11,6 +12,7 @@ import { BankIcon } from "@b3dotfun/sdk/global-account/react/components/icons/Ba
11
12
  import { SignOutIcon } from "@b3dotfun/sdk/global-account/react/components/icons/SignOutIcon";
12
13
  import { SwapIcon } from "@b3dotfun/sdk/global-account/react/components/icons/SwapIcon";
13
14
  import { formatUsername } from "@b3dotfun/sdk/shared/utils";
15
+ import { getIpfsUrl } from "@b3dotfun/sdk/shared/utils/ipfs";
14
16
  import { Loader2, Pencil } from "lucide-react";
15
17
  import { useEffect, useRef, useState } from "react";
16
18
  import { useActiveAccount } from "thirdweb/react";
@@ -22,6 +24,8 @@ import { TokenBalanceRow } from "./TokenBalanceRow";
22
24
  interface BalanceContentProps {
23
25
  onLogout?: () => void;
24
26
  partnerId: string;
27
+ showDeposit?: boolean;
28
+ showSwap?: boolean;
25
29
  }
26
30
 
27
31
  function centerTruncate(str: string, length = 4) {
@@ -29,19 +33,34 @@ function centerTruncate(str: string, length = 4) {
29
33
  return `${str.slice(0, length)}...${str.slice(-length)}`;
30
34
  }
31
35
 
32
- export function BalanceContent({ onLogout, partnerId }: BalanceContentProps) {
36
+ export function BalanceContent({ onLogout, partnerId, showDeposit = true, showSwap = true }: BalanceContentProps) {
33
37
  const account = useActiveAccount();
34
38
  const { address: eoaAddress, info: eoaInfo } = useFirstEOA();
35
39
  const { data: profile } = useProfile({
36
40
  address: eoaAddress || account?.address,
37
41
  fresh: true,
38
42
  });
39
- const { setB3ModalOpen, setB3ModalContentType } = useModalStore();
43
+ const { user } = useB3();
44
+ const { setB3ModalOpen, setB3ModalContentType, navigateBack } = useModalStore();
40
45
  const { logout } = useAuthentication(partnerId);
41
46
  const [logoutLoading, setLogoutLoading] = useState(false);
42
47
  const [openAccordions, setOpenAccordions] = useState<string[]>([]);
43
48
  const hasExpandedRef = useRef(false);
44
49
 
50
+ const avatarUrl = user?.avatar ? getIpfsUrl(user?.avatar) : profile?.avatar;
51
+
52
+ const handleEditAvatar = () => {
53
+ setB3ModalOpen(true);
54
+ setB3ModalContentType({
55
+ type: "avatarEditor",
56
+ showBackButton: true,
57
+ onSuccess: () => {
58
+ // navigate back on success
59
+ navigateBack();
60
+ },
61
+ });
62
+ };
63
+
45
64
  console.log("eoaAddress", eoaAddress);
46
65
  console.log("account?.address", account?.address);
47
66
 
@@ -98,14 +117,17 @@ export function BalanceContent({ onLogout, partnerId }: BalanceContentProps) {
98
117
  <div className="flex items-center justify-between">
99
118
  <div className="global-account-profile flex items-center gap-4">
100
119
  <div className="global-account-profile-avatar relative">
101
- {profile?.avatar ? (
102
- <img src={profile?.avatar} alt="Profile" className="size-24 rounded-full" />
120
+ {avatarUrl ? (
121
+ <img src={avatarUrl} alt="Profile" className="size-24 rounded-full" />
103
122
  ) : (
104
123
  <div className="bg-b3-primary-wash size-24 rounded-full" />
105
124
  )}
106
- <div className="bg-b3-grey border-b3-background absolute -bottom-1 -right-1 flex size-8 items-center justify-center rounded-full border-4">
125
+ <button
126
+ onClick={handleEditAvatar}
127
+ className="bg-b3-grey border-b3-background hover:bg-b3-grey/80 absolute -bottom-1 -right-1 flex size-8 items-center justify-center rounded-full border-4 transition-colors"
128
+ >
107
129
  <Pencil size={16} className="text-b3-background" />
108
- </div>
130
+ </button>
109
131
  </div>
110
132
  <div className="global-account-profile-info">
111
133
  <h2 className="text-b3-grey text-xl font-semibold">
@@ -122,35 +144,41 @@ export function BalanceContent({ onLogout, partnerId }: BalanceContentProps) {
122
144
  </div>
123
145
 
124
146
  {/* Quick Actions */}
125
- <div className="grid grid-cols-2 gap-3">
126
- <Button
127
- className="manage-account-deposit bg-b3-primary-wash hover:bg-b3-primary-wash/70 h-[84px] w-full flex-col items-start gap-2 rounded-2xl"
128
- onClick={() => {
129
- setB3ModalOpen(true);
130
- setB3ModalContentType({
131
- type: "anySpend",
132
- defaultActiveTab: "fiat",
133
- showBackButton: true,
134
- });
135
- }}
136
- >
137
- <BankIcon size={24} className="text-b3-primary-blue shrink-0" />
138
- <div className="text-b3-grey font-neue-montreal-semibold">Deposit</div>
139
- </Button>
140
- <Button
141
- className="manage-account-swap bg-b3-primary-wash hover:bg-b3-primary-wash/70 flex h-[84px] w-full flex-col items-start gap-2 rounded-2xl"
142
- onClick={() => {
143
- setB3ModalOpen(true);
144
- setB3ModalContentType({
145
- type: "anySpend",
146
- showBackButton: true,
147
- });
148
- }}
149
- >
150
- <SwapIcon size={24} className="text-b3-primary-blue" />
151
- <div className="text-b3-grey font-neue-montreal-semibold">Swap</div>
152
- </Button>
153
- </div>
147
+ {(showDeposit || showSwap) && (
148
+ <div className="grid grid-cols-2 gap-3">
149
+ {showDeposit && (
150
+ <Button
151
+ className="manage-account-deposit bg-b3-primary-wash hover:bg-b3-primary-wash/70 h-[84px] w-full flex-col items-start gap-2 rounded-2xl"
152
+ onClick={() => {
153
+ setB3ModalOpen(true);
154
+ setB3ModalContentType({
155
+ type: "anySpend",
156
+ defaultActiveTab: "fiat",
157
+ showBackButton: true,
158
+ });
159
+ }}
160
+ >
161
+ <BankIcon size={24} className="text-b3-primary-blue shrink-0" />
162
+ <div className="text-b3-grey font-neue-montreal-semibold">Deposit</div>
163
+ </Button>
164
+ )}
165
+ {showSwap && (
166
+ <Button
167
+ className="manage-account-swap bg-b3-primary-wash hover:bg-b3-primary-wash/70 flex h-[84px] w-full flex-col items-start gap-2 rounded-2xl"
168
+ onClick={() => {
169
+ setB3ModalOpen(true);
170
+ setB3ModalContentType({
171
+ type: "anySpend",
172
+ showBackButton: true,
173
+ });
174
+ }}
175
+ >
176
+ <SwapIcon size={24} className="text-b3-primary-blue" />
177
+ <div className="text-b3-grey font-neue-montreal-semibold">Swap</div>
178
+ </Button>
179
+ )}
180
+ </div>
181
+ )}
154
182
 
155
183
  {/* Balance Sections with Accordions */}
156
184
  <Accordion type="multiple" value={openAccordions} onValueChange={setOpenAccordions} className="space-y-2">