[SDK] Feature: Social Extension (Beta) (#4349)
## Problem solved
Short description of the bug fixed or feature added
<!-- start pr-codex -->
---
## PR-Codex overview
This PR adds support for fetching social profiles for Farcaster, Lens, and ENS.
### Detailed summary
- Added functions to fetch social profiles for different protocols
- Updated components to use social profile data
- Updated types for social profiles and profiles metadata
> The following files were skipped due to too many changes: `packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx`
> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
<!-- end pr-codex -->
This commit is contained in:
@@ -184,3 +184,6 @@ export { useConnectionManager } from "../react/core/providers/connection-manager
|
||||
export { Blobbie } from "../react/web/ui/ConnectWallet/Blobbie.js";
|
||||
|
||||
export { useSiweAuth } from "../react/core/hooks/auth/useSiweAuth.js";
|
||||
|
||||
// Social
|
||||
export { useSocialProfiles } from "../react/core/social/useSocialProfiles.js";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { getSocialProfiles } from "../social/profiles.js";
|
||||
export { type SocialProfiles } from "../social/types.js";
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import { getSocialProfiles } from "../../../social/profiles.js";
|
||||
|
||||
/**
|
||||
* Fetches the wallet's available social profiles.
|
||||
* @param options - The options to use when fetching the social profiles.
|
||||
* @param options.client - The Thirdweb client.
|
||||
* @param options.address - The wallet address to fetch the social profiles for.
|
||||
* @returns A React Query result containing the social profiles.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { useSocialProfiles } from "thirdweb/react";
|
||||
* const { data: profiles } = useSocialProfiles({
|
||||
* client,
|
||||
* address: "0x...",
|
||||
* });
|
||||
* ```
|
||||
* @beta
|
||||
*/
|
||||
export function useSocialProfiles(options: {
|
||||
client: ThirdwebClient;
|
||||
address: string | undefined;
|
||||
}) {
|
||||
const { client, address } = options;
|
||||
return useQuery({
|
||||
queryKey: ["social-profiles", address],
|
||||
enabled: !!address,
|
||||
retry: 3,
|
||||
queryFn: async () => {
|
||||
if (!address) {
|
||||
throw new Error(
|
||||
"Address is required, should not have reached this point.",
|
||||
);
|
||||
}
|
||||
return await getSocialProfiles({ address, client });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import type { ThirdwebClient } from "../../../client/client.js";
|
||||
import { resolveAvatar } from "../../../extensions/ens/resolve-avatar.js";
|
||||
import { resolveName } from "../../../extensions/ens/resolve-name.js";
|
||||
import { shortenAddress } from "../../../utils/address.js";
|
||||
import { parseAvatarRecord } from "../../../utils/ens/avatar.js";
|
||||
import { getWalletInfo } from "../../../wallets/__generated__/getWalletInfo.js";
|
||||
import type { Account, Wallet } from "../../../wallets/interfaces/wallet.js";
|
||||
import type { WalletInfo } from "../../../wallets/wallet-info.js";
|
||||
import type { WalletId } from "../../../wallets/wallet-types.js";
|
||||
import { useWalletBalance } from "../hooks/others/useWalletBalance.js";
|
||||
import { useSocialProfiles } from "../social/useSocialProfiles.js";
|
||||
|
||||
/**
|
||||
* Get the ENS name and avatar for an address
|
||||
@@ -98,6 +100,11 @@ export function useConnectedWalletDetails(
|
||||
ensName: ensNameQuery.data,
|
||||
});
|
||||
|
||||
const socialProfileQuery = useSocialProfiles({
|
||||
client,
|
||||
address: activeAccount?.address,
|
||||
});
|
||||
|
||||
const shortAddress = activeAccount?.address
|
||||
? shortenAddress(activeAccount.address, 4)
|
||||
: "";
|
||||
@@ -110,11 +117,31 @@ export function useConnectedWalletDetails(
|
||||
});
|
||||
|
||||
const addressOrENS = ensNameQuery.data || shortAddress;
|
||||
const pfpUnresolved = socialProfileQuery.data?.filter((p) => p.avatar)[0]
|
||||
?.avatar;
|
||||
|
||||
const { data: pfp } = useQuery({
|
||||
queryKey: ["ens-avatar", pfpUnresolved],
|
||||
queryFn: async () => {
|
||||
if (!pfpUnresolved) {
|
||||
return undefined;
|
||||
}
|
||||
return parseAvatarRecord({ client, uri: pfpUnresolved });
|
||||
},
|
||||
enabled: !!pfpUnresolved,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
const name =
|
||||
socialProfileQuery.data?.filter((p) => p.name)[0]?.name || addressOrENS;
|
||||
|
||||
return {
|
||||
socialProfileQuery,
|
||||
ensNameQuery,
|
||||
ensAvatarQuery,
|
||||
addressOrENS,
|
||||
pfp,
|
||||
name,
|
||||
shortAddress,
|
||||
balanceQuery,
|
||||
};
|
||||
|
||||
@@ -21,9 +21,9 @@ export const WalletImage = (props: {
|
||||
theme: Theme;
|
||||
wallet: Wallet;
|
||||
size: number;
|
||||
ensAvatar?: string | null;
|
||||
avatar?: string | null;
|
||||
}) => {
|
||||
const { wallet, ensAvatar, size } = props;
|
||||
const { wallet, avatar, size } = props;
|
||||
|
||||
const { data: imageData } = useQuery({
|
||||
queryKey: ["wallet-image", wallet.id, wallet.getAccount()?.address],
|
||||
@@ -56,10 +56,10 @@ export const WalletImage = (props: {
|
||||
|
||||
return WALLET_ICON;
|
||||
},
|
||||
enabled: !ensAvatar,
|
||||
enabled: !avatar,
|
||||
});
|
||||
|
||||
const data = ensAvatar || imageData || WALLET_ICON;
|
||||
const data = avatar || imageData || WALLET_ICON;
|
||||
return <RNImage theme={props.theme} data={data} size={size} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -22,13 +22,12 @@ export function ConnectedButton(
|
||||
const theme = parseTheme(props.theme);
|
||||
const { account, wallet } = props;
|
||||
const walletChain = useActiveWalletChain();
|
||||
const { ensAvatarQuery, addressOrENS, balanceQuery } =
|
||||
useConnectedWalletDetails(
|
||||
props.client,
|
||||
walletChain,
|
||||
account,
|
||||
props.detailsButton?.displayBalanceToken,
|
||||
);
|
||||
const { pfp, name, balanceQuery } = useConnectedWalletDetails(
|
||||
props.client,
|
||||
walletChain,
|
||||
account,
|
||||
props.detailsButton?.displayBalanceToken,
|
||||
);
|
||||
return (
|
||||
<ThemedButton
|
||||
theme={theme}
|
||||
@@ -41,12 +40,7 @@ export function ConnectedButton(
|
||||
}}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<WalletImage
|
||||
theme={theme}
|
||||
size={40}
|
||||
wallet={wallet}
|
||||
ensAvatar={ensAvatarQuery.data}
|
||||
/>
|
||||
<WalletImage theme={theme} size={40} wallet={wallet} avatar={pfp} />
|
||||
<View style={styles.col}>
|
||||
<ThemedText
|
||||
theme={theme}
|
||||
@@ -55,7 +49,7 @@ export function ConnectedButton(
|
||||
color: theme.colors.primaryButtonText,
|
||||
}}
|
||||
>
|
||||
{addressOrENS}
|
||||
{name}
|
||||
</ThemedText>
|
||||
{balanceQuery.data ? (
|
||||
<ThemedText
|
||||
|
||||
@@ -163,24 +163,18 @@ export function ConnectedModal(props: ConnectedModalProps) {
|
||||
const AccountHeader = (props: ConnectedModalProps) => {
|
||||
const { account, wallet, theme } = props;
|
||||
const walletChain = useActiveWalletChain();
|
||||
const { ensAvatarQuery, addressOrENS, balanceQuery } =
|
||||
useConnectedWalletDetails(
|
||||
props.client,
|
||||
walletChain,
|
||||
account,
|
||||
props.detailsButton?.displayBalanceToken,
|
||||
);
|
||||
const { pfp, name, balanceQuery } = useConnectedWalletDetails(
|
||||
props.client,
|
||||
walletChain,
|
||||
account,
|
||||
props.detailsButton?.displayBalanceToken,
|
||||
);
|
||||
return (
|
||||
<View style={styles.accountHeaderContainer}>
|
||||
<WalletImage
|
||||
theme={theme}
|
||||
size={70}
|
||||
wallet={wallet}
|
||||
ensAvatar={ensAvatarQuery.data}
|
||||
/>
|
||||
<WalletImage theme={theme} size={70} wallet={wallet} avatar={pfp} />
|
||||
<SmartAccountBadge client={props.client} theme={theme} />
|
||||
<Spacer size="smd" />
|
||||
<Address account={account} theme={theme} addressOrENS={addressOrENS} />
|
||||
<Address account={account} theme={theme} addressOrENS={name} />
|
||||
<Spacer size="xxs" />
|
||||
{balanceQuery.data ? (
|
||||
<ThemedText
|
||||
|
||||
@@ -131,13 +131,12 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
const activeAccount = useActiveAccount();
|
||||
const walletChain = useActiveWalletChain();
|
||||
|
||||
const { ensAvatarQuery, addressOrENS, balanceQuery } =
|
||||
useConnectedWalletDetails(
|
||||
client,
|
||||
walletChain,
|
||||
activeAccount,
|
||||
props.detailsButton?.displayBalanceToken,
|
||||
);
|
||||
const { pfp, name, balanceQuery } = useConnectedWalletDetails(
|
||||
client,
|
||||
walletChain,
|
||||
activeAccount,
|
||||
props.detailsButton?.displayBalanceToken,
|
||||
);
|
||||
|
||||
function closeModal() {
|
||||
setRootEl(null);
|
||||
@@ -185,8 +184,7 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
);
|
||||
}
|
||||
|
||||
const avatarSrc =
|
||||
props.detailsButton?.connectedAccountAvatarUrl ?? ensAvatarQuery.data;
|
||||
const avatarSrc = props.detailsButton?.connectedAccountAvatarUrl || pfp;
|
||||
|
||||
return (
|
||||
<WalletInfoButton
|
||||
@@ -235,7 +233,7 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
weight={500}
|
||||
className={`${TW_CONNECTED_WALLET}__address`}
|
||||
>
|
||||
{props.detailsButton?.connectedAccountName ?? addressOrENS}
|
||||
{props.detailsButton?.connectedAccountName ?? name}
|
||||
</Text>
|
||||
|
||||
{/* Balance */}
|
||||
@@ -280,13 +278,12 @@ function DetailsModal(props: {
|
||||
const { client, locale } = props;
|
||||
const walletChain = useActiveWalletChain();
|
||||
const activeAccount = useActiveAccount();
|
||||
const { ensAvatarQuery, addressOrENS, balanceQuery } =
|
||||
useConnectedWalletDetails(
|
||||
client,
|
||||
walletChain,
|
||||
activeAccount,
|
||||
props.displayBalanceToken,
|
||||
);
|
||||
const { pfp, name, balanceQuery } = useConnectedWalletDetails(
|
||||
client,
|
||||
walletChain,
|
||||
activeAccount,
|
||||
props.displayBalanceToken,
|
||||
);
|
||||
const theme = parseTheme(props.theme);
|
||||
|
||||
const activeWallet = useActiveWallet();
|
||||
@@ -370,8 +367,7 @@ function DetailsModal(props: {
|
||||
</MenuButton>
|
||||
);
|
||||
|
||||
const avatarSrc =
|
||||
props.detailsModal?.connectedAccountAvatarUrl ?? ensAvatarQuery.data;
|
||||
const avatarSrc = props.detailsModal?.connectedAccountAvatarUrl ?? pfp;
|
||||
|
||||
const { hideSendFunds, hideReceiveFunds, hideBuyFunds } =
|
||||
props.detailsModal || {};
|
||||
@@ -483,7 +479,7 @@ function DetailsModal(props: {
|
||||
}}
|
||||
>
|
||||
<Text color="primaryText" weight={500} size="md">
|
||||
{props.detailsModal?.connectedAccountName ?? addressOrENS}
|
||||
{props.detailsModal?.connectedAccountName ?? name}
|
||||
</Text>
|
||||
<IconButton>
|
||||
<CopyIcon
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ThirdwebClient } from "../client/client.js";
|
||||
import { getThirdwebBaseUrl } from "../utils/domains.js";
|
||||
import { getClientFetch } from "../utils/fetch.js";
|
||||
import type { SocialProfiles } from "./types.js";
|
||||
|
||||
/**
|
||||
* Fetches the wallet's available social profiles.
|
||||
* @param args - The arguments to use when fetching the social profiles.
|
||||
* @param args.address - The wallet address to fetch the social profiles for.
|
||||
* @param args.client - The Thirdweb client.
|
||||
* @returns A promise resolving to the retrieved social profiles for different protocols. If a profile is not available for a protocol, the value will be `null`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { getProfiles } from "thirdweb/social";
|
||||
* const profiles = await getProfiles({
|
||||
* address: "0x...",
|
||||
* client,
|
||||
* });
|
||||
* ```
|
||||
* @beta
|
||||
*/
|
||||
export async function getSocialProfiles(args: {
|
||||
address: string;
|
||||
client: ThirdwebClient;
|
||||
}): Promise<SocialProfiles> {
|
||||
const { address, client } = args;
|
||||
|
||||
const clientFetch = getClientFetch(client);
|
||||
const response = await clientFetch(
|
||||
`${getThirdwebBaseUrl("social")}/v1/profiles/${address}`,
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
try {
|
||||
const errorBody = await response.json();
|
||||
throw new Error(`Failed to fetch profile: ${errorBody.message}`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Failed to fetch profile: ${response.status}\n${await response.text()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (await response.json()).data as SocialProfiles;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export type FarcasterProfile = {
|
||||
fid?: number;
|
||||
bio?: string;
|
||||
pfp?: string;
|
||||
display?: string;
|
||||
username?: string;
|
||||
custodyAddress?: string;
|
||||
addresses?: string[];
|
||||
};
|
||||
|
||||
export type LensProfile = {
|
||||
name?: string;
|
||||
bio?: string;
|
||||
picture?: string;
|
||||
coverPicture?: string;
|
||||
};
|
||||
|
||||
export type EnsProfile = {
|
||||
name?: string;
|
||||
address?: string;
|
||||
avatar?: string;
|
||||
display?: string;
|
||||
description?: string;
|
||||
keywords?: string[];
|
||||
email?: string;
|
||||
mail?: string;
|
||||
notice?: string;
|
||||
location?: string;
|
||||
phone?: string;
|
||||
url?: string;
|
||||
twitter?: string;
|
||||
github?: string;
|
||||
discord?: string;
|
||||
telegram?: string;
|
||||
};
|
||||
|
||||
export type SocialProfiles = {
|
||||
type: "farcaster" | "lens" | "ens";
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
bio?: string;
|
||||
metadata?: FarcasterProfile | LensProfile | EnsProfile;
|
||||
}[];
|
||||
@@ -4,6 +4,11 @@ type DomainOverrides = {
|
||||
* @default "rpc.thirdweb.com"
|
||||
*/
|
||||
rpc?: string;
|
||||
/**
|
||||
* The base URL for the social service.
|
||||
* @default "social.thirdweb.com"
|
||||
*/
|
||||
social?: string;
|
||||
/**
|
||||
* The base URL for the in-app wallet service
|
||||
* @default "embedded-wallet.thirdweb.com"
|
||||
@@ -28,6 +33,7 @@ type DomainOverrides = {
|
||||
|
||||
export const DEFAULT_RPC_URL = "rpc.thirdweb.com";
|
||||
const DEFAULT_IN_APP_WALLET_URL = "embedded-wallet.thirdweb.com";
|
||||
const DEFAULT_SOCIAL_URL = "social.thirdweb.com";
|
||||
const DEFAULT_PAY_URL = "pay.thirdweb.com";
|
||||
const DEFAULT_STORAGE_URL = "storage.thirdweb.com";
|
||||
const DEFAULT_BUNDLER_URL = "bundler.thirdweb.com";
|
||||
@@ -35,6 +41,7 @@ const DEFAULT_BUNDLER_URL = "bundler.thirdweb.com";
|
||||
let domains: { [k in keyof DomainOverrides]-?: string } = {
|
||||
rpc: DEFAULT_RPC_URL,
|
||||
inAppWallet: DEFAULT_IN_APP_WALLET_URL,
|
||||
social: DEFAULT_SOCIAL_URL,
|
||||
pay: DEFAULT_PAY_URL,
|
||||
storage: DEFAULT_STORAGE_URL,
|
||||
bundler: DEFAULT_BUNDLER_URL,
|
||||
@@ -47,6 +54,7 @@ export const setThirdwebDomains = (DomainOverrides: DomainOverrides) => {
|
||||
domains = {
|
||||
rpc: DomainOverrides.rpc ?? DEFAULT_RPC_URL,
|
||||
inAppWallet: DomainOverrides.inAppWallet ?? DEFAULT_IN_APP_WALLET_URL,
|
||||
social: DomainOverrides.social ?? DEFAULT_SOCIAL_URL,
|
||||
pay: DomainOverrides.pay ?? DEFAULT_PAY_URL,
|
||||
storage: DomainOverrides.storage ?? DEFAULT_STORAGE_URL,
|
||||
bundler: DomainOverrides.bundler ?? DEFAULT_BUNDLER_URL,
|
||||
|
||||
Reference in New Issue
Block a user