Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { Identity } from "@namehash/namekit-react/client";

const meta: Meta<typeof Identity.Root> = {
title: "Namekit/Identity",
component: Identity.Root,
argTypes: {
address: { control: "text" },
network: {
control: {
type: "select",
options: ["mainnet", "sepolia"],
},
},
className: { control: "text" },
},
};

export default meta;

type Story = StoryObj<typeof Identity.Root>;

const IdentityCard: React.FC<{
address: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check all places in the code where you have an address param. These should all use the Address type defined in viem.

network?: "mainnet" | "sepolia";
}> = ({ address, network }) => (
<Identity.Root address={address} network={network}>
<Identity.Avatar />
<Identity.Name />
<Identity.Address />
<Identity.NameGuardShield />
</Identity.Root>
);

export const Default: Story = {
args: {
address: "0x838aD0EAE54F99F1926dA7C3b6bFbF617389B4D9",
network: "mainnet",
className: "rounded-xl",
},
render: (args) => <IdentityCard {...args} />,
};

export const MultipleCards: Story = {
render: () => (
<>
<IdentityCard address="0x838aD0EAE54F99F1926dA7C3b6bFbF617389B4D9" />
<IdentityCard address="0x123456789abcdef123456789abcdef123456789a" />
<IdentityCard address="0xabcdef123456789abcdef123456789abcdef1234" />
<IdentityCard address="0x987654321fedcba987654321fedcba987654321f" />
<IdentityCard
address="0xfedcba987654321fedcba987654321fedcba9876"
network="sepolia"
/>
<IdentityCard address="0x111222333444555666777888999000aaabbbcccd" />
</>
),
};
1 change: 1 addition & 0 deletions packages/namekit-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@headlessui/react": "1.7.17",
"@namehash/ens-utils": "workspace:*",
"@namehash/ens-webfont": "workspace:*",
"@namehash/nameguard": "workspace:*",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your suggestion to make this a peer dependency sounds good 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This hasn't been changed yet.

"classcat": "5.0.5"
},
"devDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions packages/namekit-react/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import "@namehash/ens-webfont";
import "./styles.css";

Expand All @@ -12,3 +14,5 @@ export {
CurrencySymbolSize,
} from "./components/CurrencySymbol/CurrencySymbol";
export { TruncatedText } from "./components/TruncatedText";

export { Identity } from "./components/Identity";
145 changes: 145 additions & 0 deletions packages/namekit-react/src/components/Identity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from "react";
import {
createClient,
Network,
type SecurePrimaryNameResult,
} from "@namehash/nameguard";

interface IdentityContextType extends SecurePrimaryNameResult {
address: string;
}

const IdentityContext = createContext<IdentityContextType | null>(null);

const useIdentity = () => {
const context = useContext(IdentityContext);

if (!context) {
throw new Error("useIdentity must be used within an IdentityProvider");
}

return context;
};

interface SubComponentProps {
className?: string;
children?: ReactNode;
}

interface RootProps {
address: string;
network?: Network;
className?: string;
children: ReactNode;
}

const Root = ({
address,
network = "mainnet",
className,
children,
...props
}: RootProps) => {
const [data, setData] = useState<IdentityContextType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);

const nameguard = createClient({ network });

const result = await nameguard.getSecurePrimaryName(address, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest that we add ReactQuery as a dependency / peer dependency for NameKit React.

The web3 community has generally adopted ReactQuery as an unofficial standard. There's a ton of various libraries and packages already using it or built on it.

For example, strongly suggest having a look at the following:

  1. wagmi. https://wagmi.sh/ The same team that built wagmi built viem https://viem.sh/. Pretty much everyone is using both libraries in their web3 apps. viem is a more low-level library that's suitable for use on a backend or frontend. wagmi is a frontend library that wraps viem. wagmi uses ReactQuery for all their network requests.
  2. onchainkit. https://onchainkit.xyz/ This is built as a layer on top of wagmi.
  3. rainbowkit. https://www.rainbowkit.com/ This is also built as a layer on top of wagmi.
  4. Etc..

In fact, suggest that we not only add ReactQuery as a dependency, but we also add wagmi as a dependency. We shouldn't try to reinvent the whole universe. Pretty much everyone who might use NameKit React is also going to be using wagmi, so we better make it convenient for everything to work together.

returnNameGuardReport: true,
});

setData({ ...result, address });
} catch (err) {
setError(
err instanceof Error ? err.message : "An unknown error occurred",
);
} finally {
setLoading(false);
}
};

fetchData();
}, [address, network]);

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest a few updates here:

  1. The Root and all of its children should always be rendered, even if data is still loading or failed to load. The responsibility for handling these cases should be pushed down into child components. Ex: Avatar should know what to do when it is loading or when there was a loading error, etc..
  2. ... continuing on from above: Suggest updating the interface that is returned by useIdentity so that all of the following are true:
    1. Independent of any loading state, it is always guaranteed to be able to get the following values: address and network (these props should never be undefined).
    2. There's some distinct field(s) representing the loading state / request error state for the identity.
    3. We shouldn't have the returned value ... extends SecurePrimaryNameResult (because this value might still be loading, etc..). Therefore we should have this as an optional property of the returned value that is undefined unless the loading state field is some value representing it has been successfully loaded.

Copy link
Member Author

@notrab notrab Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have addressed most of this in a recent update. I need to double check 2.2


return (
<IdentityContext.Provider value={data}>
<div className={`namekit-identity ${className}`} {...props}>
{children}
</div>
</IdentityContext.Provider>
);
};

const Avatar = ({ className, children, ...props }: SubComponentProps) => {
const { display_name } = useIdentity();

return (
<div className={`namekit-avatar ${className}`} {...props}>
<img
src={`https://avatar.example.com/${display_name}`}
alt={display_name}
/>
{children}
</div>
);
};

const Name = ({ className, ...props }: SubComponentProps) => {
const { display_name } = useIdentity();

return (
<div className={`namekit-name ${className}`} {...props}>
{display_name}
</div>
);
};

const Address = ({ className, ...props }: SubComponentProps) => {
const { address } = useIdentity();

return (
<div className={`namekit-address ${className}`} {...props}>
{address}
</div>
);
};

const NameGuardShield = ({ className, ...props }: SubComponentProps) => {
const { nameguard_report } = useIdentity();

return (
<div className={`namekit-nameguard-shield ${className}`} {...props}>
<div className="namekit-nameguard-rating">
Rating: {nameguard_report?.rating}
</div>
<div className="namekit-nameguard-risk-count">
Risks: {nameguard_report?.risk_count}
</div>
</div>
);
};

export const Identity = {
Root,
Avatar,
Name,
Address,
NameGuardShield,
};
Loading