From 4198c18da79b85cd801eb8d9f42e7c8d66f18622 Mon Sep 17 00:00:00 2001 From: ankit-tailor Date: Mon, 13 Jan 2025 16:55:17 +0530 Subject: [PATCH 1/2] feat: add identity component --- packages/composer-kit/src/identity/avatar.tsx | 39 ++++++++++ .../composer-kit/src/identity/balance.tsx | 26 +++++++ .../src/identity/hooks/use-social.ts | 55 ++++++++++++++ .../composer-kit/src/identity/indentity.tsx | 73 +++++++++++++++++++ packages/composer-kit/src/identity/index.ts | 5 ++ packages/composer-kit/src/identity/name.tsx | 24 ++++++ packages/composer-kit/src/identity/social.tsx | 27 +++++++ 7 files changed, 249 insertions(+) create mode 100644 packages/composer-kit/src/identity/avatar.tsx create mode 100644 packages/composer-kit/src/identity/balance.tsx create mode 100644 packages/composer-kit/src/identity/hooks/use-social.ts create mode 100644 packages/composer-kit/src/identity/indentity.tsx create mode 100644 packages/composer-kit/src/identity/index.ts create mode 100644 packages/composer-kit/src/identity/name.tsx create mode 100644 packages/composer-kit/src/identity/social.tsx diff --git a/packages/composer-kit/src/identity/avatar.tsx b/packages/composer-kit/src/identity/avatar.tsx new file mode 100644 index 0000000..e41cbe7 --- /dev/null +++ b/packages/composer-kit/src/identity/avatar.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { useIdentity } from "./indentity"; + +function DefaultAvatar(): JSX.Element { + return ( + + + + ); +} + +export function Avatar(): JSX.Element { + const { avatar, name: ensName } = useIdentity(); + + return ( + <> + {avatar ? ( + {`${ensName}-avatar`} + ) : ( + + )} + + ); +} diff --git a/packages/composer-kit/src/identity/balance.tsx b/packages/composer-kit/src/identity/balance.tsx new file mode 100644 index 0000000..3f6733d --- /dev/null +++ b/packages/composer-kit/src/identity/balance.tsx @@ -0,0 +1,26 @@ +import { useBalance } from "../core/internal/hooks/use-balance"; +import { UTILITY_TOKENS } from "../core/internal/utils/constants"; +import { cn } from "../utils/helper"; +import { useIdentity } from "./indentity"; + +interface BalanceProps extends React.HTMLAttributes { + precison?: number; +} + +export function Balance({ + className, + precison = 3, + ...props +}: BalanceProps): JSX.Element | null { + const { address, token } = useIdentity(); + const { balance } = useBalance({ + address, + tokenMetaData: UTILITY_TOKENS[token], + }); + + return ( +

+ {`${parseFloat(balance).toFixed(precison)} ${token}`} +

+ ); +} diff --git a/packages/composer-kit/src/identity/hooks/use-social.ts b/packages/composer-kit/src/identity/hooks/use-social.ts new file mode 100644 index 0000000..f47c2f7 --- /dev/null +++ b/packages/composer-kit/src/identity/hooks/use-social.ts @@ -0,0 +1,55 @@ +import { useState, useEffect } from "react"; +import { mainnet } from "viem/chains"; +import { normalize } from "viem/ens"; +import { getPublicClient } from "../../core/internal/config/viem-public-client"; + +interface SocialProps { + ensName: string; + tag: "github" | "twitter" | "url" | "farcaster"; +} + +const getSocialUrl = (tag: string, data: string) => { + switch (tag) { + case "github": + return `https://github.com/${data}`; + case "twitter": + return `https://x.com/${data}`; + case "url": + return data; + case "farcaster": + return `https://warpcast.com/${data}`; + default: + return null; + } +}; + +export const useSocial = ({ ensName, tag }: SocialProps) => { + const [socialData, setSocialData] = useState<{ + tag: string; + url: string | null; + } | null>(null); + + useEffect(() => { + const fetchSocialData = async () => { + const client = getPublicClient(mainnet); + + const data = await client.getEnsText({ + name: normalize(ensName), + key: `com.${tag}`, + }); + + if (data) { + setSocialData({ + url: getSocialUrl(tag, data), + tag, + }); + } else { + setSocialData(null); + } + }; + + fetchSocialData(); + }, [ensName]); + + return socialData; +}; diff --git a/packages/composer-kit/src/identity/indentity.tsx b/packages/composer-kit/src/identity/indentity.tsx new file mode 100644 index 0000000..cea66c9 --- /dev/null +++ b/packages/composer-kit/src/identity/indentity.tsx @@ -0,0 +1,73 @@ +import React, { + createContext, + useContext, + useMemo, + type HTMLAttributes, +} from "react"; +import { type Address } from "viem"; +import { mainnet } from "viem/chains"; +import { useEnsAvatar, useEnsName } from "wagmi"; + +interface IdentityProps extends HTMLAttributes { + address: Address; + token?: "CELO" | "cUSD" | "USDT"; +} + +interface IdentityContextType { + address: Address; + name: string; + avatar: string; + balance: string; + token: "CELO" | "cUSD" | "USDT"; +} + +const IdentityContext = createContext(null); + +export function Identity({ + children, + token = "CELO", + address, + ...props +}: IdentityProps): JSX.Element { + const { data: ensName, isLoading: isEnsLoading } = useEnsName({ + address, + chainId: mainnet.id, + query: { + enabled: true, + }, + }); + const { data: avatar, isLoading: isAvatarLoading } = useEnsAvatar({ + name: ensName ?? "", + chainId: mainnet.id, + }); + + const contextValue = useMemo(() => { + return { + address, + name: ensName ?? "", + avatar: avatar ?? "", + balance: "0", + token, + }; + }, [address, avatar, ensName, token]); + + if (isEnsLoading || isAvatarLoading) { + return
; + } + + return ( + +
{children}
+
+ ); +} + +export const useIdentity = (): IdentityContextType => { + const context = useContext(IdentityContext); + + if (!context) { + throw new Error("useIdentity must be used within an IdentityProvider"); + } + + return context; +}; diff --git a/packages/composer-kit/src/identity/index.ts b/packages/composer-kit/src/identity/index.ts new file mode 100644 index 0000000..19a3842 --- /dev/null +++ b/packages/composer-kit/src/identity/index.ts @@ -0,0 +1,5 @@ +export { Identity } from "./indentity"; +export { Avatar } from "./avatar"; +export { Name } from "./name"; +export { Balance } from "./balance"; +export { Social } from "./social"; diff --git a/packages/composer-kit/src/identity/name.tsx b/packages/composer-kit/src/identity/name.tsx new file mode 100644 index 0000000..6a632b9 --- /dev/null +++ b/packages/composer-kit/src/identity/name.tsx @@ -0,0 +1,24 @@ +import { cn } from "../utils/helper"; +import { useIdentity } from "./indentity"; + +interface NameProps extends React.HTMLAttributes { + isTruncated?: boolean; +} + +const getTruncatedAddress = (address: string): string => { + return `${address.slice(0, 6)}...${address.slice(-4)}`; +}; + +export function Name({ + isTruncated = true, + className, + ...props +}: NameProps): JSX.Element | null { + const { address, name } = useIdentity(); + + return ( +

+ {name || isTruncated ? getTruncatedAddress(address) : address} +

+ ); +} diff --git a/packages/composer-kit/src/identity/social.tsx b/packages/composer-kit/src/identity/social.tsx new file mode 100644 index 0000000..2e56a01 --- /dev/null +++ b/packages/composer-kit/src/identity/social.tsx @@ -0,0 +1,27 @@ +import { useSocial } from "./hooks/use-social"; +import { useIdentity } from "./indentity"; + +interface SocialProps extends React.HTMLAttributes { + tag: "github" | "twitter" | "url" | "farcaster"; +} + +export function Social({ + className, + tag, + children, + ...props +}: SocialProps): JSX.Element | null { + const { name } = useIdentity(); + const data = useSocial({ + ensName: name, + tag, + }); + + if (!data) return null; + + return ( + + {children ?? tag} + + ); +} From abea3ffd4547f330253c277f72ab69cd4be5be49 Mon Sep 17 00:00:00 2001 From: ankit-tailor Date: Mon, 13 Jan 2025 16:55:35 +0530 Subject: [PATCH 2/2] feat: add identity story --- apps/storybook/stories/identity.stories.tsx | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 apps/storybook/stories/identity.stories.tsx diff --git a/apps/storybook/stories/identity.stories.tsx b/apps/storybook/stories/identity.stories.tsx new file mode 100644 index 0000000..3344608 --- /dev/null +++ b/apps/storybook/stories/identity.stories.tsx @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + Avatar, + Balance, + Identity, + Name, + Social, +} from "@composer-kit/ui/identity"; + +const meta: Meta = { + component: Identity, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + render: () => { + return ( + + +
+ + +
+ +
+ ); + }, + name: "Indentity", + args: {}, +};