@b3dotfun/sdk 0.1.69-alpha.12 → 0.1.69-alpha.14
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/checkout/AnySpendCheckout.d.ts +3 -1
- package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.js +5 -1
- package/dist/cjs/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +3 -1
- package/dist/cjs/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.js +4 -2
- package/dist/cjs/global-account/react/components/ManageAccount/ProfileSection.js +15 -6
- package/dist/cjs/global-account/react/utils/profileDisplay.js +17 -18
- package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.d.ts +3 -1
- package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.js +5 -1
- package/dist/esm/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +3 -1
- package/dist/esm/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.js +4 -2
- package/dist/esm/global-account/react/components/ManageAccount/ProfileSection.js +13 -7
- package/dist/esm/global-account/react/utils/profileDisplay.js +17 -18
- package/dist/types/anyspend/react/components/checkout/AnySpendCheckout.d.ts +3 -1
- package/dist/types/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.d.ts +3 -1
- package/package.json +2 -1
- package/src/anyspend/react/components/checkout/AnySpendCheckout.tsx +6 -0
- package/src/global-account/react/components/IPFSMediaRenderer/IPFSMediaRenderer.tsx +17 -10
- package/src/global-account/react/components/ManageAccount/ProfileSection.tsx +29 -11
- package/src/global-account/react/utils/profileDisplay.ts +21 -19
|
@@ -121,5 +121,7 @@ export interface AnySpendCheckoutProps {
|
|
|
121
121
|
feeOnTop?: boolean;
|
|
122
122
|
/** When true, identity verification (KYC) is required before card payment. Defaults to false. */
|
|
123
123
|
kycEnabled?: boolean;
|
|
124
|
+
/** Extra metadata included under the 'callbackMetadata' key in the order's callbackMetadata (e.g. workflowId, orgId from checkout session) */
|
|
125
|
+
callbackMetadata?: Record<string, unknown>;
|
|
124
126
|
}
|
|
125
|
-
export declare function AnySpendCheckout({ mode, recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText, checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines, formSchema, formComponent, onFormSubmit, shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp, enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, variablePricing, feeOnTop, kycEnabled, }: AnySpendCheckoutProps): import("react/jsx-runtime").JSX.Element;
|
|
127
|
+
export declare function AnySpendCheckout({ mode, recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText, checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines, formSchema, formComponent, onFormSubmit, shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp, enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, variablePricing, feeOnTop, kycEnabled, callbackMetadata: callbackMetadataProp, }: AnySpendCheckoutProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -24,7 +24,7 @@ shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp,
|
|
|
24
24
|
// New discount props
|
|
25
25
|
enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
|
|
26
26
|
// Variable pricing
|
|
27
|
-
variablePricing, feeOnTop, kycEnabled = false, }) {
|
|
27
|
+
variablePricing, feeOnTop, kycEnabled = false, callbackMetadata: callbackMetadataProp, }) {
|
|
28
28
|
// ===== Variable pricing state =====
|
|
29
29
|
const [variablePricingAmount, setVariablePricingAmount] = (0, react_2.useState)("0");
|
|
30
30
|
const isVariablePricingActive = variablePricing?.enabled === true;
|
|
@@ -176,8 +176,12 @@ variablePricing, feeOnTop, kycEnabled = false, }) {
|
|
|
176
176
|
if (isVariablePricingActive && variablePricingAmount !== "0") {
|
|
177
177
|
meta.variablePricingAmount = variablePricingAmount;
|
|
178
178
|
}
|
|
179
|
+
// Namespace caller-supplied metadata to avoid collisions with internal keys
|
|
180
|
+
if (callbackMetadataProp)
|
|
181
|
+
meta.callbackMetadata = callbackMetadataProp;
|
|
179
182
|
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
180
183
|
}, [
|
|
184
|
+
callbackMetadataProp,
|
|
181
185
|
formData,
|
|
182
186
|
selectedShipping,
|
|
183
187
|
shippingAddress,
|
|
@@ -16,6 +16,8 @@ interface IPFSMediaRendererProps {
|
|
|
16
16
|
controls?: boolean;
|
|
17
17
|
/** Style object */
|
|
18
18
|
style?: React.CSSProperties;
|
|
19
|
+
/** Callback when media fails to load */
|
|
20
|
+
onError?: () => void;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* IPFSMediaRenderer - A wrapper around Thirdweb's MediaRenderer that configures
|
|
@@ -35,5 +37,5 @@ interface IPFSMediaRendererProps {
|
|
|
35
37
|
* />
|
|
36
38
|
* ```
|
|
37
39
|
*/
|
|
38
|
-
export declare function IPFSMediaRenderer({ src, alt, className, client, width, height, controls, style, }: IPFSMediaRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
export declare function IPFSMediaRenderer({ src, alt, className, client, width, height, controls, style, onError, }: IPFSMediaRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
39
41
|
export {};
|
|
@@ -24,7 +24,7 @@ const react_1 = require("thirdweb/react");
|
|
|
24
24
|
* />
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
|
-
function IPFSMediaRenderer({ src, alt = "Media", className, client = thirdweb_1.client, width, height, controls, style, }) {
|
|
27
|
+
function IPFSMediaRenderer({ src, alt = "Media", className, client = thirdweb_1.client, width, height, controls, style, onError, }) {
|
|
28
28
|
// If no source, render fallback
|
|
29
29
|
if (!src) {
|
|
30
30
|
return ((0, jsx_runtime_1.jsx)("div", { className: className, style: style, "aria-label": alt, children: (0, jsx_runtime_1.jsx)("div", { className: "bg-b3-primary-wash flex h-full w-full items-center justify-center rounded-full", children: (0, jsx_runtime_1.jsx)("span", { className: "text-b3-grey font-neue-montreal-semibold text-xs", children: alt.charAt(0).toUpperCase() }) }) }));
|
|
@@ -32,5 +32,7 @@ function IPFSMediaRenderer({ src, alt = "Media", className, client = thirdweb_1.
|
|
|
32
32
|
// Convert IPFS URLs to HTTP gateway URLs using our preferred gateway
|
|
33
33
|
// This avoids Thirdweb's default cloudflare-ipfs.com which can be unreliable
|
|
34
34
|
const resolvedSrc = src.startsWith("ipfs://") ? (0, ipfs_1.getIpfsUrl)(src) : src;
|
|
35
|
-
|
|
35
|
+
// Wrap in a span with onErrorCapture to catch img/video load errors from
|
|
36
|
+
// MediaRenderer, which doesn't expose an onError prop itself.
|
|
37
|
+
return ((0, jsx_runtime_1.jsx)("span", { onErrorCapture: onError, className: "contents", children: (0, jsx_runtime_1.jsx)(react_1.MediaRenderer, { src: resolvedSrc, client: client, alt: alt, className: className, width: width ? width.toString() : undefined, height: height ? height.toString() : undefined, controls: controls, style: style }) }));
|
|
36
38
|
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
7
|
const react_1 = require("../../../../global-account/react");
|
|
8
|
+
const profileDisplay_1 = require("../../../../global-account/react/utils/profileDisplay");
|
|
5
9
|
const utils_1 = require("../../../../shared/utils");
|
|
6
10
|
const number_1 = require("../../../../shared/utils/number");
|
|
11
|
+
const boring_avatars_1 = __importDefault(require("boring-avatars"));
|
|
7
12
|
const lucide_react_1 = require("lucide-react");
|
|
8
13
|
const react_2 = require("react");
|
|
9
14
|
const react_3 = require("thirdweb/react");
|
|
10
15
|
const useFirstEOA_1 = require("../../hooks/useFirstEOA");
|
|
11
16
|
const IPFSMediaRenderer_1 = require("../IPFSMediaRenderer/IPFSMediaRenderer");
|
|
17
|
+
const AVATAR_COLORS = ["#3368ef", "#272727", "#6366f1", "#06b6d4", "#eeb0d9", "#ba3fbf", "#ff777b", "#dfbb53"];
|
|
12
18
|
const ProfileSection = () => {
|
|
13
19
|
const account = (0, react_3.useActiveAccount)();
|
|
14
20
|
const { address: eoaAddress } = (0, useFirstEOA_1.useFirstEOA)();
|
|
@@ -22,7 +28,8 @@ const ProfileSection = () => {
|
|
|
22
28
|
const setB3ModalContentType = (0, react_1.useModalStore)(state => state.setB3ModalContentType);
|
|
23
29
|
const navigateBack = (0, react_1.useModalStore)(state => state.navigateBack);
|
|
24
30
|
const { data: simBalance } = (0, react_1.useSimBalance)(smartWalletAddress);
|
|
25
|
-
|
|
31
|
+
const [imgError, setImgError] = (0, react_2.useState)(false);
|
|
32
|
+
const handleImgError = (0, react_2.useCallback)(() => setImgError(true), []);
|
|
26
33
|
const totalBalanceUsd = (0, react_2.useMemo)(() => {
|
|
27
34
|
if (!simBalance?.balances)
|
|
28
35
|
return 0;
|
|
@@ -33,15 +40,17 @@ const ProfileSection = () => {
|
|
|
33
40
|
setB3ModalContentType({
|
|
34
41
|
type: "avatarEditor",
|
|
35
42
|
onSuccess: () => {
|
|
36
|
-
// navigate back on success
|
|
37
43
|
navigateBack();
|
|
38
44
|
},
|
|
39
45
|
});
|
|
40
46
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
const avatarSrc = (0, react_2.useMemo)(() => (0, profileDisplay_1.validateImageUrl)(user?.avatar) || (0, profileDisplay_1.validateImageUrl)(profile?.avatar), [user?.avatar, profile?.avatar]);
|
|
48
|
+
// Reset error state when avatar source changes (e.g. user uploads a new avatar)
|
|
49
|
+
(0, react_2.useEffect)(() => {
|
|
50
|
+
setImgError(false);
|
|
51
|
+
}, [avatarSrc]);
|
|
44
52
|
const currentUsername = user?.username || profile?.displayName || (0, utils_1.formatUsername)(profile?.name || "");
|
|
45
|
-
|
|
53
|
+
const avatarSeed = eoaAddress || account?.address || smartWalletAddress || currentUsername || "user";
|
|
54
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-between px-5 py-6", children: (0, jsx_runtime_1.jsxs)("div", { className: "global-account-profile flex items-center gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "global-account-profile-avatar relative", children: [(0, jsx_runtime_1.jsx)("div", { className: "border-b3-line border-1 bg-b3-primary-wash size-14 overflow-hidden rounded-full border", children: avatarSrc && !imgError ? ((0, jsx_runtime_1.jsx)(IPFSMediaRenderer_1.IPFSMediaRenderer, { src: avatarSrc, alt: "Profile Avatar", className: "h-full w-full object-cover", onError: handleImgError })) : ((0, jsx_runtime_1.jsx)(boring_avatars_1.default, { name: avatarSeed, variant: "beam", size: 56, colors: AVATAR_COLORS })) }), (0, jsx_runtime_1.jsx)("button", { onClick: handleEditAvatar, 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", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 10, className: "text-b3-background" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "global-account-profile-info flex flex-col gap-1", children: [(0, jsx_runtime_1.jsxs)("h2", { className: "text-b3-grey font-neue-montreal-semibold flex h-[38px] items-center gap-1 text-xl", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-b3-foreground-muted", children: " $" }), (0, jsx_runtime_1.jsx)("div", { className: "text-[30px]", children: (0, number_1.formatDisplayNumber)(totalBalanceUsd, { fractionDigits: 2 }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "b3-modal-username font-neue-montreal-semibold text-base leading-none text-[#0B57C2]", children: currentUsername })] })] }) }));
|
|
46
55
|
};
|
|
47
56
|
exports.default = ProfileSection;
|
|
@@ -4,6 +4,18 @@ exports.validateImageUrl = validateImageUrl;
|
|
|
4
4
|
exports.getProfileDisplayInfo = getProfileDisplayInfo;
|
|
5
5
|
const debug_1 = require("../../../shared/utils/debug");
|
|
6
6
|
const debug = (0, debug_1.debugB3React)("profileDisplay");
|
|
7
|
+
const ALLOWED_IPFS_GATEWAYS = [
|
|
8
|
+
"ipfs.io",
|
|
9
|
+
"gateway.pinata.cloud",
|
|
10
|
+
"cloudflare-ipfs.com",
|
|
11
|
+
"dweb.link",
|
|
12
|
+
"nftstorage.link",
|
|
13
|
+
"w3s.link",
|
|
14
|
+
];
|
|
15
|
+
const BLOCKED_HOSTNAMES = ["models.readyplayer.me", "readyplayer.me"];
|
|
16
|
+
function isHostnameInList(hostname, list) {
|
|
17
|
+
return list.some(entry => hostname === entry || hostname.endsWith(`.${entry}`));
|
|
18
|
+
}
|
|
7
19
|
/**
|
|
8
20
|
* Validates that an image URL uses an allowed schema
|
|
9
21
|
* @param url - The URL to validate
|
|
@@ -28,27 +40,14 @@ function validateImageUrl(url) {
|
|
|
28
40
|
debug("Rejected unsafe protocol:", parsedUrl.protocol, url);
|
|
29
41
|
return null;
|
|
30
42
|
}
|
|
31
|
-
// Whitelist of allowed IPFS gateway hostnames
|
|
32
|
-
const allowedIpfsGateways = [
|
|
33
|
-
"ipfs.io",
|
|
34
|
-
"gateway.pinata.cloud",
|
|
35
|
-
"cloudflare-ipfs.com",
|
|
36
|
-
"dweb.link",
|
|
37
|
-
"nftstorage.link",
|
|
38
|
-
"w3s.link",
|
|
39
|
-
];
|
|
40
|
-
// Check if hostname matches allowed IPFS gateways
|
|
41
43
|
const hostname = parsedUrl.hostname.toLowerCase();
|
|
42
|
-
|
|
43
|
-
// Exact match or subdomain of the gateway
|
|
44
|
-
return hostname === gateway || hostname.endsWith(`.${gateway}`);
|
|
45
|
-
});
|
|
46
|
-
if (isAllowedIpfsGateway) {
|
|
44
|
+
if (isHostnameInList(hostname, ALLOWED_IPFS_GATEWAYS)) {
|
|
47
45
|
return url;
|
|
48
46
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
if (isHostnameInList(hostname, BLOCKED_HOSTNAMES)) {
|
|
48
|
+
debug("Rejected deprecated avatar service:", hostname, url);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
52
51
|
return url;
|
|
53
52
|
}
|
|
54
53
|
catch (error) {
|
|
@@ -121,5 +121,7 @@ export interface AnySpendCheckoutProps {
|
|
|
121
121
|
feeOnTop?: boolean;
|
|
122
122
|
/** When true, identity verification (KYC) is required before card payment. Defaults to false. */
|
|
123
123
|
kycEnabled?: boolean;
|
|
124
|
+
/** Extra metadata included under the 'callbackMetadata' key in the order's callbackMetadata (e.g. workflowId, orgId from checkout session) */
|
|
125
|
+
callbackMetadata?: Record<string, unknown>;
|
|
124
126
|
}
|
|
125
|
-
export declare function AnySpendCheckout({ mode, recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText, checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines, formSchema, formComponent, onFormSubmit, shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp, enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, variablePricing, feeOnTop, kycEnabled, }: AnySpendCheckoutProps): import("react/jsx-runtime").JSX.Element;
|
|
127
|
+
export declare function AnySpendCheckout({ mode, recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText, checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines, formSchema, formComponent, onFormSubmit, shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp, enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, variablePricing, feeOnTop, kycEnabled, callbackMetadata: callbackMetadataProp, }: AnySpendCheckoutProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -21,7 +21,7 @@ shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp,
|
|
|
21
21
|
// New discount props
|
|
22
22
|
enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount,
|
|
23
23
|
// Variable pricing
|
|
24
|
-
variablePricing, feeOnTop, kycEnabled = false, }) {
|
|
24
|
+
variablePricing, feeOnTop, kycEnabled = false, callbackMetadata: callbackMetadataProp, }) {
|
|
25
25
|
// ===== Variable pricing state =====
|
|
26
26
|
const [variablePricingAmount, setVariablePricingAmount] = useState("0");
|
|
27
27
|
const isVariablePricingActive = variablePricing?.enabled === true;
|
|
@@ -173,8 +173,12 @@ variablePricing, feeOnTop, kycEnabled = false, }) {
|
|
|
173
173
|
if (isVariablePricingActive && variablePricingAmount !== "0") {
|
|
174
174
|
meta.variablePricingAmount = variablePricingAmount;
|
|
175
175
|
}
|
|
176
|
+
// Namespace caller-supplied metadata to avoid collisions with internal keys
|
|
177
|
+
if (callbackMetadataProp)
|
|
178
|
+
meta.callbackMetadata = callbackMetadataProp;
|
|
176
179
|
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
177
180
|
}, [
|
|
181
|
+
callbackMetadataProp,
|
|
178
182
|
formData,
|
|
179
183
|
selectedShipping,
|
|
180
184
|
shippingAddress,
|
|
@@ -16,6 +16,8 @@ interface IPFSMediaRendererProps {
|
|
|
16
16
|
controls?: boolean;
|
|
17
17
|
/** Style object */
|
|
18
18
|
style?: React.CSSProperties;
|
|
19
|
+
/** Callback when media fails to load */
|
|
20
|
+
onError?: () => void;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* IPFSMediaRenderer - A wrapper around Thirdweb's MediaRenderer that configures
|
|
@@ -35,5 +37,5 @@ interface IPFSMediaRendererProps {
|
|
|
35
37
|
* />
|
|
36
38
|
* ```
|
|
37
39
|
*/
|
|
38
|
-
export declare function IPFSMediaRenderer({ src, alt, className, client, width, height, controls, style, }: IPFSMediaRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
export declare function IPFSMediaRenderer({ src, alt, className, client, width, height, controls, style, onError, }: IPFSMediaRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
39
41
|
export {};
|
|
@@ -21,7 +21,7 @@ import { MediaRenderer } from "thirdweb/react";
|
|
|
21
21
|
* />
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
export function IPFSMediaRenderer({ src, alt = "Media", className, client = defaultClient, width, height, controls, style, }) {
|
|
24
|
+
export function IPFSMediaRenderer({ src, alt = "Media", className, client = defaultClient, width, height, controls, style, onError, }) {
|
|
25
25
|
// If no source, render fallback
|
|
26
26
|
if (!src) {
|
|
27
27
|
return (_jsx("div", { className: className, style: style, "aria-label": alt, children: _jsx("div", { className: "bg-b3-primary-wash flex h-full w-full items-center justify-center rounded-full", children: _jsx("span", { className: "text-b3-grey font-neue-montreal-semibold text-xs", children: alt.charAt(0).toUpperCase() }) }) }));
|
|
@@ -29,5 +29,7 @@ export function IPFSMediaRenderer({ src, alt = "Media", className, client = defa
|
|
|
29
29
|
// Convert IPFS URLs to HTTP gateway URLs using our preferred gateway
|
|
30
30
|
// This avoids Thirdweb's default cloudflare-ipfs.com which can be unreliable
|
|
31
31
|
const resolvedSrc = src.startsWith("ipfs://") ? getIpfsUrl(src) : src;
|
|
32
|
-
|
|
32
|
+
// Wrap in a span with onErrorCapture to catch img/video load errors from
|
|
33
|
+
// MediaRenderer, which doesn't expose an onError prop itself.
|
|
34
|
+
return (_jsx("span", { onErrorCapture: onError, className: "contents", children: _jsx(MediaRenderer, { src: resolvedSrc, client: client, alt: alt, className: className, width: width ? width.toString() : undefined, height: height ? height.toString() : undefined, controls: controls, style: style }) }));
|
|
33
35
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useAccountWallet, useModalStore, useProfile, useSimBalance, useUser, } from "../../../../global-account/react/index.js";
|
|
3
|
+
import { validateImageUrl } from "../../../../global-account/react/utils/profileDisplay.js";
|
|
3
4
|
import { formatUsername } from "../../../../shared/utils/index.js";
|
|
4
5
|
import { formatDisplayNumber } from "../../../../shared/utils/number.js";
|
|
6
|
+
import Avatar from "boring-avatars";
|
|
5
7
|
import { Pencil } from "lucide-react";
|
|
6
|
-
import { useMemo } from "react";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
7
9
|
import { useActiveAccount } from "thirdweb/react";
|
|
8
10
|
import { useFirstEOA } from "../../hooks/useFirstEOA.js";
|
|
9
11
|
import { IPFSMediaRenderer } from "../IPFSMediaRenderer/IPFSMediaRenderer.js";
|
|
12
|
+
const AVATAR_COLORS = ["#3368ef", "#272727", "#6366f1", "#06b6d4", "#eeb0d9", "#ba3fbf", "#ff777b", "#dfbb53"];
|
|
10
13
|
const ProfileSection = () => {
|
|
11
14
|
const account = useActiveAccount();
|
|
12
15
|
const { address: eoaAddress } = useFirstEOA();
|
|
@@ -20,7 +23,8 @@ const ProfileSection = () => {
|
|
|
20
23
|
const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
|
|
21
24
|
const navigateBack = useModalStore(state => state.navigateBack);
|
|
22
25
|
const { data: simBalance } = useSimBalance(smartWalletAddress);
|
|
23
|
-
|
|
26
|
+
const [imgError, setImgError] = useState(false);
|
|
27
|
+
const handleImgError = useCallback(() => setImgError(true), []);
|
|
24
28
|
const totalBalanceUsd = useMemo(() => {
|
|
25
29
|
if (!simBalance?.balances)
|
|
26
30
|
return 0;
|
|
@@ -31,15 +35,17 @@ const ProfileSection = () => {
|
|
|
31
35
|
setB3ModalContentType({
|
|
32
36
|
type: "avatarEditor",
|
|
33
37
|
onSuccess: () => {
|
|
34
|
-
// navigate back on success
|
|
35
38
|
navigateBack();
|
|
36
39
|
},
|
|
37
40
|
});
|
|
38
41
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
const avatarSrc = useMemo(() => validateImageUrl(user?.avatar) || validateImageUrl(profile?.avatar), [user?.avatar, profile?.avatar]);
|
|
43
|
+
// Reset error state when avatar source changes (e.g. user uploads a new avatar)
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setImgError(false);
|
|
46
|
+
}, [avatarSrc]);
|
|
42
47
|
const currentUsername = user?.username || profile?.displayName || formatUsername(profile?.name || "");
|
|
43
|
-
|
|
48
|
+
const avatarSeed = eoaAddress || account?.address || smartWalletAddress || currentUsername || "user";
|
|
49
|
+
return (_jsx("div", { className: "flex items-center justify-between px-5 py-6", children: _jsxs("div", { className: "global-account-profile flex items-center gap-4", children: [_jsxs("div", { className: "global-account-profile-avatar relative", children: [_jsx("div", { className: "border-b3-line border-1 bg-b3-primary-wash size-14 overflow-hidden rounded-full border", children: avatarSrc && !imgError ? (_jsx(IPFSMediaRenderer, { src: avatarSrc, alt: "Profile Avatar", className: "h-full w-full object-cover", onError: handleImgError })) : (_jsx(Avatar, { name: avatarSeed, variant: "beam", size: 56, colors: AVATAR_COLORS })) }), _jsx("button", { onClick: handleEditAvatar, 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", children: _jsx(Pencil, { size: 10, className: "text-b3-background" }) })] }), _jsxs("div", { className: "global-account-profile-info flex flex-col gap-1", children: [_jsxs("h2", { className: "text-b3-grey font-neue-montreal-semibold flex h-[38px] items-center gap-1 text-xl", children: [_jsx("div", { className: "text-b3-foreground-muted", children: " $" }), _jsx("div", { className: "text-[30px]", children: formatDisplayNumber(totalBalanceUsd, { fractionDigits: 2 }) })] }), _jsx("div", { className: "b3-modal-username font-neue-montreal-semibold text-base leading-none text-[#0B57C2]", children: currentUsername })] })] }) }));
|
|
44
50
|
};
|
|
45
51
|
export default ProfileSection;
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { debugB3React } from "../../../shared/utils/debug.js";
|
|
2
2
|
const debug = debugB3React("profileDisplay");
|
|
3
|
+
const ALLOWED_IPFS_GATEWAYS = [
|
|
4
|
+
"ipfs.io",
|
|
5
|
+
"gateway.pinata.cloud",
|
|
6
|
+
"cloudflare-ipfs.com",
|
|
7
|
+
"dweb.link",
|
|
8
|
+
"nftstorage.link",
|
|
9
|
+
"w3s.link",
|
|
10
|
+
];
|
|
11
|
+
const BLOCKED_HOSTNAMES = ["models.readyplayer.me", "readyplayer.me"];
|
|
12
|
+
function isHostnameInList(hostname, list) {
|
|
13
|
+
return list.some(entry => hostname === entry || hostname.endsWith(`.${entry}`));
|
|
14
|
+
}
|
|
3
15
|
/**
|
|
4
16
|
* Validates that an image URL uses an allowed schema
|
|
5
17
|
* @param url - The URL to validate
|
|
@@ -24,27 +36,14 @@ export function validateImageUrl(url) {
|
|
|
24
36
|
debug("Rejected unsafe protocol:", parsedUrl.protocol, url);
|
|
25
37
|
return null;
|
|
26
38
|
}
|
|
27
|
-
// Whitelist of allowed IPFS gateway hostnames
|
|
28
|
-
const allowedIpfsGateways = [
|
|
29
|
-
"ipfs.io",
|
|
30
|
-
"gateway.pinata.cloud",
|
|
31
|
-
"cloudflare-ipfs.com",
|
|
32
|
-
"dweb.link",
|
|
33
|
-
"nftstorage.link",
|
|
34
|
-
"w3s.link",
|
|
35
|
-
];
|
|
36
|
-
// Check if hostname matches allowed IPFS gateways
|
|
37
39
|
const hostname = parsedUrl.hostname.toLowerCase();
|
|
38
|
-
|
|
39
|
-
// Exact match or subdomain of the gateway
|
|
40
|
-
return hostname === gateway || hostname.endsWith(`.${gateway}`);
|
|
41
|
-
});
|
|
42
|
-
if (isAllowedIpfsGateway) {
|
|
40
|
+
if (isHostnameInList(hostname, ALLOWED_IPFS_GATEWAYS)) {
|
|
43
41
|
return url;
|
|
44
42
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
if (isHostnameInList(hostname, BLOCKED_HOSTNAMES)) {
|
|
44
|
+
debug("Rejected deprecated avatar service:", hostname, url);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
48
47
|
return url;
|
|
49
48
|
}
|
|
50
49
|
catch (error) {
|
|
@@ -121,5 +121,7 @@ export interface AnySpendCheckoutProps {
|
|
|
121
121
|
feeOnTop?: boolean;
|
|
122
122
|
/** When true, identity verification (KYC) is required before card payment. Defaults to false. */
|
|
123
123
|
kycEnabled?: boolean;
|
|
124
|
+
/** Extra metadata included under the 'callbackMetadata' key in the order's callbackMetadata (e.g. workflowId, orgId from checkout session) */
|
|
125
|
+
callbackMetadata?: Record<string, unknown>;
|
|
124
126
|
}
|
|
125
|
-
export declare function AnySpendCheckout({ mode, recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText, checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines, formSchema, formComponent, onFormSubmit, shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp, enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, variablePricing, feeOnTop, kycEnabled, }: AnySpendCheckoutProps): import("react/jsx-runtime").JSX.Element;
|
|
127
|
+
export declare function AnySpendCheckout({ mode, recipientAddress, destinationTokenAddress, destinationTokenChainId, items, totalAmount: totalAmountOverride, organizationName, organizationLogo, themeColor, buttonText, checkoutSessionId, onSuccess, onError, returnUrl, returnLabel, classes, footer, defaultPaymentMethod, senderAddress, slots, content, theme, showPoints, showOrderId, shipping: shippingProp, tax, discount: discountProp, summaryLines, formSchema, formComponent, onFormSubmit, shippingOptions, collectShippingAddress, onShippingChange: onShippingChangeProp, enableDiscountCode, onDiscountApplied: onDiscountAppliedProp, validateDiscount, variablePricing, feeOnTop, kycEnabled, callbackMetadata: callbackMetadataProp, }: AnySpendCheckoutProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -16,6 +16,8 @@ interface IPFSMediaRendererProps {
|
|
|
16
16
|
controls?: boolean;
|
|
17
17
|
/** Style object */
|
|
18
18
|
style?: React.CSSProperties;
|
|
19
|
+
/** Callback when media fails to load */
|
|
20
|
+
onError?: () => void;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* IPFSMediaRenderer - A wrapper around Thirdweb's MediaRenderer that configures
|
|
@@ -35,5 +37,5 @@ interface IPFSMediaRendererProps {
|
|
|
35
37
|
* />
|
|
36
38
|
* ```
|
|
37
39
|
*/
|
|
38
|
-
export declare function IPFSMediaRenderer({ src, alt, className, client, width, height, controls, style, }: IPFSMediaRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
export declare function IPFSMediaRenderer({ src, alt, className, client, width, height, controls, style, onError, }: IPFSMediaRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
39
41
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b3dotfun/sdk",
|
|
3
|
-
"version": "0.1.69-alpha.
|
|
3
|
+
"version": "0.1.69-alpha.14",
|
|
4
4
|
"source": "src/index.ts",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"react-native": "./dist/cjs/index.native.js",
|
|
@@ -330,6 +330,7 @@
|
|
|
330
330
|
"@thirdweb-dev/wagmi-adapter": "0.2.165",
|
|
331
331
|
"@web3icons/react": "3.16.0",
|
|
332
332
|
"big.js": "^7.0.1",
|
|
333
|
+
"boring-avatars": "^2.0.4",
|
|
333
334
|
"class-variance-authority": "0.7.0",
|
|
334
335
|
"clsx": "2.0.0",
|
|
335
336
|
"cmdk": "1.0.0",
|
|
@@ -138,6 +138,8 @@ export interface AnySpendCheckoutProps {
|
|
|
138
138
|
feeOnTop?: boolean;
|
|
139
139
|
/** When true, identity verification (KYC) is required before card payment. Defaults to false. */
|
|
140
140
|
kycEnabled?: boolean;
|
|
141
|
+
/** Extra metadata included under the 'callbackMetadata' key in the order's callbackMetadata (e.g. workflowId, orgId from checkout session) */
|
|
142
|
+
callbackMetadata?: Record<string, unknown>;
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
const emptyAddress: AddressData = { street: "", city: "", state: "", zip: "", country: "" };
|
|
@@ -187,6 +189,7 @@ export function AnySpendCheckout({
|
|
|
187
189
|
variablePricing,
|
|
188
190
|
feeOnTop,
|
|
189
191
|
kycEnabled = false,
|
|
192
|
+
callbackMetadata: callbackMetadataProp,
|
|
190
193
|
}: AnySpendCheckoutProps) {
|
|
191
194
|
// ===== Variable pricing state =====
|
|
192
195
|
const [variablePricingAmount, setVariablePricingAmount] = useState<string>("0");
|
|
@@ -342,8 +345,11 @@ export function AnySpendCheckout({
|
|
|
342
345
|
if (isVariablePricingActive && variablePricingAmount !== "0") {
|
|
343
346
|
meta.variablePricingAmount = variablePricingAmount;
|
|
344
347
|
}
|
|
348
|
+
// Namespace caller-supplied metadata to avoid collisions with internal keys
|
|
349
|
+
if (callbackMetadataProp) meta.callbackMetadata = callbackMetadataProp;
|
|
345
350
|
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
346
351
|
}, [
|
|
352
|
+
callbackMetadataProp,
|
|
347
353
|
formData,
|
|
348
354
|
selectedShipping,
|
|
349
355
|
shippingAddress,
|
|
@@ -22,6 +22,8 @@ interface IPFSMediaRendererProps {
|
|
|
22
22
|
controls?: boolean;
|
|
23
23
|
/** Style object */
|
|
24
24
|
style?: React.CSSProperties;
|
|
25
|
+
/** Callback when media fails to load */
|
|
26
|
+
onError?: () => void;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -51,6 +53,7 @@ export function IPFSMediaRenderer({
|
|
|
51
53
|
height,
|
|
52
54
|
controls,
|
|
53
55
|
style,
|
|
56
|
+
onError,
|
|
54
57
|
}: IPFSMediaRendererProps) {
|
|
55
58
|
// If no source, render fallback
|
|
56
59
|
if (!src) {
|
|
@@ -67,16 +70,20 @@ export function IPFSMediaRenderer({
|
|
|
67
70
|
// This avoids Thirdweb's default cloudflare-ipfs.com which can be unreliable
|
|
68
71
|
const resolvedSrc = src.startsWith("ipfs://") ? getIpfsUrl(src) : src;
|
|
69
72
|
|
|
73
|
+
// Wrap in a span with onErrorCapture to catch img/video load errors from
|
|
74
|
+
// MediaRenderer, which doesn't expose an onError prop itself.
|
|
70
75
|
return (
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
<span onErrorCapture={onError} className="contents">
|
|
77
|
+
<MediaRenderer
|
|
78
|
+
src={resolvedSrc}
|
|
79
|
+
client={client}
|
|
80
|
+
alt={alt}
|
|
81
|
+
className={className}
|
|
82
|
+
width={width ? width.toString() : undefined}
|
|
83
|
+
height={height ? height.toString() : undefined}
|
|
84
|
+
controls={controls}
|
|
85
|
+
style={style}
|
|
86
|
+
/>
|
|
87
|
+
</span>
|
|
81
88
|
);
|
|
82
89
|
}
|
|
@@ -5,14 +5,18 @@ import {
|
|
|
5
5
|
useSimBalance,
|
|
6
6
|
useUser,
|
|
7
7
|
} from "@b3dotfun/sdk/global-account/react";
|
|
8
|
+
import { validateImageUrl } from "@b3dotfun/sdk/global-account/react/utils/profileDisplay";
|
|
8
9
|
import { formatUsername } from "@b3dotfun/sdk/shared/utils";
|
|
9
10
|
import { formatDisplayNumber } from "@b3dotfun/sdk/shared/utils/number";
|
|
11
|
+
import Avatar from "boring-avatars";
|
|
10
12
|
import { Pencil } from "lucide-react";
|
|
11
|
-
import { useMemo } from "react";
|
|
13
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
12
14
|
import { useActiveAccount } from "thirdweb/react";
|
|
13
15
|
import { useFirstEOA } from "../../hooks/useFirstEOA";
|
|
14
16
|
import { IPFSMediaRenderer } from "../IPFSMediaRenderer/IPFSMediaRenderer";
|
|
15
17
|
|
|
18
|
+
const AVATAR_COLORS = ["#3368ef", "#272727", "#6366f1", "#06b6d4", "#eeb0d9", "#ba3fbf", "#ff777b", "#dfbb53"];
|
|
19
|
+
|
|
16
20
|
const ProfileSection = () => {
|
|
17
21
|
const account = useActiveAccount();
|
|
18
22
|
const { address: eoaAddress } = useFirstEOA();
|
|
@@ -27,8 +31,9 @@ const ProfileSection = () => {
|
|
|
27
31
|
const navigateBack = useModalStore(state => state.navigateBack);
|
|
28
32
|
|
|
29
33
|
const { data: simBalance } = useSimBalance(smartWalletAddress);
|
|
34
|
+
const [imgError, setImgError] = useState(false);
|
|
35
|
+
const handleImgError = useCallback(() => setImgError(true), []);
|
|
30
36
|
|
|
31
|
-
// Calculate total balance in USD
|
|
32
37
|
const totalBalanceUsd = useMemo(() => {
|
|
33
38
|
if (!simBalance?.balances) return 0;
|
|
34
39
|
return simBalance.balances.reduce((sum, token) => sum + (token.value_usd || 0), 0);
|
|
@@ -39,27 +44,40 @@ const ProfileSection = () => {
|
|
|
39
44
|
setB3ModalContentType({
|
|
40
45
|
type: "avatarEditor",
|
|
41
46
|
onSuccess: () => {
|
|
42
|
-
// navigate back on success
|
|
43
47
|
navigateBack();
|
|
44
48
|
},
|
|
45
49
|
});
|
|
46
50
|
};
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const avatarSrc = useMemo(
|
|
53
|
+
() => validateImageUrl(user?.avatar) || validateImageUrl(profile?.avatar),
|
|
54
|
+
[user?.avatar, profile?.avatar],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Reset error state when avatar source changes (e.g. user uploads a new avatar)
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setImgError(false);
|
|
60
|
+
}, [avatarSrc]);
|
|
50
61
|
|
|
51
|
-
// Get current username - prioritize user.username, fallback to profile data
|
|
52
62
|
const currentUsername = user?.username || profile?.displayName || formatUsername(profile?.name || "");
|
|
63
|
+
const avatarSeed = eoaAddress || account?.address || smartWalletAddress || currentUsername || "user";
|
|
53
64
|
|
|
54
65
|
return (
|
|
55
66
|
<div className="flex items-center justify-between px-5 py-6">
|
|
56
67
|
<div className="global-account-profile flex items-center gap-4">
|
|
57
68
|
<div className="global-account-profile-avatar relative">
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
<div className="border-b3-line border-1 bg-b3-primary-wash size-14 overflow-hidden rounded-full border">
|
|
70
|
+
{avatarSrc && !imgError ? (
|
|
71
|
+
<IPFSMediaRenderer
|
|
72
|
+
src={avatarSrc}
|
|
73
|
+
alt="Profile Avatar"
|
|
74
|
+
className="h-full w-full object-cover"
|
|
75
|
+
onError={handleImgError}
|
|
76
|
+
/>
|
|
77
|
+
) : (
|
|
78
|
+
<Avatar name={avatarSeed} variant="beam" size={56} colors={AVATAR_COLORS} />
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
63
81
|
|
|
64
82
|
<button
|
|
65
83
|
onClick={handleEditAvatar}
|
|
@@ -3,6 +3,21 @@ import { type Profile } from "thirdweb/wallets";
|
|
|
3
3
|
|
|
4
4
|
const debug = debugB3React("profileDisplay");
|
|
5
5
|
|
|
6
|
+
const ALLOWED_IPFS_GATEWAYS = [
|
|
7
|
+
"ipfs.io",
|
|
8
|
+
"gateway.pinata.cloud",
|
|
9
|
+
"cloudflare-ipfs.com",
|
|
10
|
+
"dweb.link",
|
|
11
|
+
"nftstorage.link",
|
|
12
|
+
"w3s.link",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const BLOCKED_HOSTNAMES = ["models.readyplayer.me", "readyplayer.me"];
|
|
16
|
+
|
|
17
|
+
function isHostnameInList(hostname: string, list: string[]): boolean {
|
|
18
|
+
return list.some(entry => hostname === entry || hostname.endsWith(`.${entry}`));
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
/**
|
|
7
22
|
* Validates that an image URL uses an allowed schema
|
|
8
23
|
* @param url - The URL to validate
|
|
@@ -31,30 +46,17 @@ export function validateImageUrl(url: string | null | undefined): string | null
|
|
|
31
46
|
return null;
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
// Whitelist of allowed IPFS gateway hostnames
|
|
35
|
-
const allowedIpfsGateways = [
|
|
36
|
-
"ipfs.io",
|
|
37
|
-
"gateway.pinata.cloud",
|
|
38
|
-
"cloudflare-ipfs.com",
|
|
39
|
-
"dweb.link",
|
|
40
|
-
"nftstorage.link",
|
|
41
|
-
"w3s.link",
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
// Check if hostname matches allowed IPFS gateways
|
|
45
49
|
const hostname = parsedUrl.hostname.toLowerCase();
|
|
46
|
-
const isAllowedIpfsGateway = allowedIpfsGateways.some(gateway => {
|
|
47
|
-
// Exact match or subdomain of the gateway
|
|
48
|
-
return hostname === gateway || hostname.endsWith(`.${gateway}`);
|
|
49
|
-
});
|
|
50
50
|
|
|
51
|
-
if (
|
|
51
|
+
if (isHostnameInList(hostname, ALLOWED_IPFS_GATEWAYS)) {
|
|
52
52
|
return url;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if (isHostnameInList(hostname, BLOCKED_HOSTNAMES)) {
|
|
56
|
+
debug("Rejected deprecated avatar service:", hostname, url);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
58
60
|
return url;
|
|
59
61
|
} catch (error) {
|
|
60
62
|
// Invalid URL format
|