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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.js +1 -1
  2. package/dist/cjs/anyspend/react/components/common/CryptoPaymentMethod.js +1 -1
  3. package/dist/cjs/anyspend/react/components/common/PanelOnrampPayment.js +1 -1
  4. package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
  5. package/dist/cjs/global-account/react/components/AvatarEditor/AvatarEditor.js +71 -4
  6. package/dist/cjs/global-account/react/components/B3DynamicModal.js +1 -1
  7. package/dist/cjs/global-account/react/components/ManageAccount/HomeActions.js +1 -1
  8. package/dist/cjs/global-account/react/components/ManageAccount/ProfileSection.js +1 -1
  9. package/dist/cjs/global-account/react/components/ManageAccount/SettingsContent.js +1 -1
  10. package/dist/cjs/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
  11. package/dist/cjs/global-account/react/components/ModalHeader/ModalHeader.js +2 -2
  12. package/dist/cjs/global-account/react/components/ui/drawer.js +1 -1
  13. package/dist/esm/anyspend/react/components/AnySpend.js +1 -1
  14. package/dist/esm/anyspend/react/components/common/CryptoPaymentMethod.js +1 -1
  15. package/dist/esm/anyspend/react/components/common/PanelOnrampPayment.js +1 -1
  16. package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
  17. package/dist/esm/global-account/react/components/AvatarEditor/AvatarEditor.js +72 -5
  18. package/dist/esm/global-account/react/components/B3DynamicModal.js +1 -1
  19. package/dist/esm/global-account/react/components/ManageAccount/HomeActions.js +1 -1
  20. package/dist/esm/global-account/react/components/ManageAccount/ProfileSection.js +1 -1
  21. package/dist/esm/global-account/react/components/ManageAccount/SettingsContent.js +1 -1
  22. package/dist/esm/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
  23. package/dist/esm/global-account/react/components/ModalHeader/ModalHeader.js +2 -2
  24. package/dist/esm/global-account/react/components/ui/drawer.js +1 -1
  25. package/dist/styles/index.css +1 -1
  26. package/dist/types/global-account/react/components/AvatarEditor/AvatarEditor.d.ts +1 -0
  27. package/dist/types/global-account/react/components/ModalHeader/ModalHeader.d.ts +2 -1
  28. package/package.json +2 -1
  29. package/src/anyspend/react/components/AnySpend.tsx +1 -1
  30. package/src/anyspend/react/components/common/CryptoPaymentMethod.tsx +1 -1
  31. package/src/anyspend/react/components/common/PanelOnrampPayment.tsx +1 -1
  32. package/src/global-account/react/components/AvatarEditor/AvatarEditor.tsx +123 -6
  33. package/src/global-account/react/components/B3DynamicModal.tsx +1 -1
  34. package/src/global-account/react/components/ManageAccount/HomeActions.tsx +1 -1
  35. package/src/global-account/react/components/ManageAccount/ProfileSection.tsx +6 -1
  36. package/src/global-account/react/components/ManageAccount/SettingsContent.tsx +1 -1
  37. package/src/global-account/react/components/ModalHeader/ModalHeader.tsx +16 -8
  38. package/src/global-account/react/components/ui/drawer.tsx +1 -1
@@ -7,7 +7,10 @@ import { cn } from "@b3dotfun/sdk/shared/utils/cn";
7
7
  import { debugB3React } from "@b3dotfun/sdk/shared/utils/debug";
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;
@@ -33,6 +46,9 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
33
46
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
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);
@@ -50,6 +66,48 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
50
66
  const currentAvatar = validateImageUrl(rawCurrentAvatar);
51
67
  const safePreviewUrl = validateImageUrl(previewUrl);
52
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
+ };
110
+
53
111
  const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
54
112
  const file = event.target.files?.[0];
55
113
  if (file) {
@@ -87,6 +145,10 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
87
145
  if (fileInputRef.current) {
88
146
  fileInputRef.current.value = "";
89
147
  }
148
+ // Reset crop state
149
+ setCrop({ x: 0, y: 0 });
150
+ setZoom(1);
151
+ setCroppedAreaPixels(null);
90
152
  };
91
153
 
92
154
  const handleSaveChanges = async () => {
@@ -99,8 +161,20 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
99
161
  try {
100
162
  let fileToUpload: File | null = null;
101
163
 
102
- // If user uploaded a new file
103
- if (selectedFile) {
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
104
178
  fileToUpload = selectedFile;
105
179
  } else if (selectedProfileType && selectedAvatar) {
106
180
  // User selected from existing profile avatars
@@ -319,7 +393,7 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
319
393
  {/* Upload Image Button */}
320
394
  <button
321
395
  onClick={handleUploadImageClick}
322
- className="font-inter shadow-xs mb-6 flex w-full items-center justify-center gap-2 rounded-xl border border-[#e4e4e7] bg-white px-4 py-3 text-sm font-semibold text-[#18181b] transition-colors hover:bg-[#f4f4f5]"
396
+ className="font-inter mb-6 flex w-full items-center justify-center gap-2 rounded-xl border border-[#e4e4e7] bg-white px-4 py-3 text-sm font-semibold text-[#18181b] shadow-sm transition-colors hover:bg-[#f4f4f5]"
323
397
  >
324
398
  <Upload className="h-4 w-4" />
325
399
  Upload image
@@ -419,13 +493,56 @@ export function AvatarEditor({ onSetAvatar, className }: AvatarEditorProps) {
419
493
  </div>
420
494
  ) : (
421
495
  <div className="mb-6 w-full">
422
- <div className="aspect-square w-full overflow-hidden rounded-xl bg-[#f4f4f5]">
496
+ <div className="relative aspect-square w-full overflow-hidden rounded-xl bg-[#f4f4f5]">
423
497
  {safePreviewUrl ? (
424
- <img src={safePreviewUrl} alt="Preview" className="h-full w-full object-cover" />
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
+ </>
425
528
  ) : (
426
529
  <div className="bg-b3-primary-wash h-full w-full" />
427
530
  )}
428
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
+ )}
429
546
  </div>
430
547
  )}
431
548
  </>
@@ -186,7 +186,7 @@ export function B3DynamicModal() {
186
186
  <ModalDescription className="sr-only hidden">{contentType?.type || "Modal Body"}</ModalDescription>
187
187
 
188
188
  <div className={cn("no-scrollbar max-h-[90dvh] overflow-auto sm:max-h-[80dvh]")}>
189
- {(!hideCloseButton || contentType?.showBackButton) && (
189
+ {!hideCloseButton && (
190
190
  <button
191
191
  onClick={navigateBack}
192
192
  className="flex items-center gap-2 px-6 py-4 text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
@@ -41,7 +41,7 @@ const HomeActionButton = ({
41
41
  return (
42
42
  <Button
43
43
  className={cn(
44
- "border-b3-line hover:border-b3-primary-blue shadow-xs flex h-[84px] w-full flex-col items-center justify-center gap-2 rounded-2xl border-[1.5px] bg-[#FAFAFA] hover:bg-[#FAFAFA]",
44
+ "border-b3-line hover:border-b3-primary-blue flex h-[84px] w-full flex-col items-center justify-center gap-2 rounded-2xl border-[1.5px] bg-[#FAFAFA] shadow-[0_0_0_1px_rgba(10,13,18,0.18)_inset,0_-2px_0_0_rgba(10,13,18,0.05)_inset,0_1px_2px_0_rgba(10,13,18,0.05)] hover:bg-[#FAFAFA]",
45
45
  customClass,
46
46
  )}
47
47
  onClick={onClick}
@@ -49,7 +49,12 @@ const ProfileSection = () => {
49
49
  <div className="flex items-center justify-between px-5 py-6">
50
50
  <div className="global-account-profile flex items-center gap-4">
51
51
  <div className="global-account-profile-avatar relative">
52
- <IPFSMediaRenderer src={avatarSrc} alt="Profile Avatar" className="size-14 rounded-full" />
52
+ <IPFSMediaRenderer
53
+ src={avatarSrc}
54
+ alt="Profile Avatar"
55
+ className="border-b3-line border-1 bg-b3-primary-wash size-14 rounded-full border"
56
+ />
57
+
53
58
  <button
54
59
  onClick={handleEditAvatar}
55
60
  className="border-b3-background hover:bg-b3-grey/80 absolute -bottom-1 -right-1 flex size-6 items-center justify-center rounded-full border-4 bg-[#a0a0ab] transition-colors"
@@ -50,7 +50,7 @@ const SettingsContent = ({
50
50
 
51
51
  return (
52
52
  <div className="flex h-[470px] flex-col">
53
- <ModalHeader title="Settings" />
53
+ <ModalHeader showBackButton={false} showCloseButton={false} title="Settings" />
54
54
 
55
55
  {/* Profile Section */}
56
56
  <div className="p-5">
@@ -3,6 +3,7 @@ import { ChevronDown, X } from "lucide-react";
3
3
  import { useModalStore } from "../../stores";
4
4
 
5
5
  const ModalHeader = ({
6
+ showBackButton = true,
6
7
  handleBack,
7
8
  handleClose,
8
9
  title,
@@ -11,6 +12,7 @@ const ModalHeader = ({
11
12
  className,
12
13
  showBackWord = false,
13
14
  }: {
15
+ showBackButton?: boolean;
14
16
  handleBack?: () => void;
15
17
  handleClose?: () => void;
16
18
  title: string;
@@ -26,21 +28,27 @@ const ModalHeader = ({
26
28
  <div
27
29
  className={cn("flex h-16 items-center justify-between border-b border-[#e4e4e7] bg-white px-5 py-3", className)}
28
30
  >
29
- <button
30
- onClick={handleBack || navigateBack}
31
- className="flex h-6 w-6 items-center justify-center transition-opacity hover:opacity-70"
32
- >
33
- <ChevronDown className="h-6 w-6 rotate-90 text-[#51525c]" />
34
- {showBackWord && <span className="text-sm font-medium">Back</span>}
35
- </button>
31
+ {showBackButton ? (
32
+ <button
33
+ onClick={handleBack || navigateBack}
34
+ className="flex h-6 w-6 items-center justify-center transition-opacity hover:opacity-70"
35
+ >
36
+ <ChevronDown className="h-6 w-6 rotate-90 text-[#51525c]" />
37
+ {showBackWord && <span className="text-sm font-medium">Back</span>}
38
+ </button>
39
+ ) : (
40
+ <div className="w-2" />
41
+ )}
36
42
  <p className="font-inter text-lg font-semibold leading-7 text-[#18181b]">{title}</p>
37
- {showCloseButton && (
43
+ {showCloseButton ? (
38
44
  <button
39
45
  onClick={handleClose || (() => setB3ModalOpen(false))}
40
46
  className="flex h-6 w-6 items-center justify-center transition-opacity hover:opacity-70"
41
47
  >
42
48
  <X className="h-6 w-6 text-[#51525c]" />
43
49
  </button>
50
+ ) : (
51
+ <div className="w-2" />
44
52
  )}
45
53
  {children}
46
54
  </div>
@@ -35,7 +35,7 @@ const DrawerContent = React.forwardRef<
35
35
  <DrawerPrimitive.Content
36
36
  ref={ref}
37
37
  className={cn(
38
- "bg-b3-react-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border py-6",
38
+ "bg-b3-react-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border py-6 pt-5",
39
39
  className,
40
40
  )}
41
41
  {...props}