Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,39 @@ function App() {
}
```

### Web3 Chain Configuration

`BreadUIKitProvider` now supports explicit chain configuration.

```tsx
import { BreadUIKitProvider } from "@breadcoop/ui";
import { erc20Abi } from "viem";

<BreadUIKitProvider
app="fund"
authProvider="general"
chainId={11155111}
supportedChainIds={[11155111]}
tokenConfig={{
BREAD: {
address: "0xYourSepoliaBreadTokenAddress",
abi: erc20Abi,
},
}}
>
{children}
</BreadUIKitProvider>;
```

`chainId` is the primary target network used by login/switch-chain and reads.
`supportedChainIds` controls which chains are treated as connected vs unsupported.

### Migration Notes

- Preferred: pass `chainId` (and optionally `supportedChainIds`) explicitly.
- Legacy fallback: `isProd` is still accepted for backward compatibility (`true -> 100`, `false -> 31337`).
- For Sepolia, set `chainId={11155111}` to avoid automatic switching to Gnosis.

## Components

### Typography
Expand Down
14 changes: 6 additions & 8 deletions src/components/auth/login-button-privy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,22 @@ import { ReactNode } from "react";
import LiftedButton from "../LiftedButton/LiftedButton";
import { ButtonShell } from "./button-shell";
import { App } from "../../interface/app";
import { gnosis } from "viem/chains";
import { useBreadUIKitContext } from "../../context/lib";

export interface LoginButtonPrivyProps {
app: App;
status: "CONNECTED" | "LOADING" | "UNSUPPORTED_CHAIN" | "NOT_CONNECTED";
label?: string;
rightIcon?: ReactNode;
isProd?: boolean;
}

export const LoginButtonPrivy = ({
app,
status,
label = "Sign In",
rightIcon,
isProd = true,
}: LoginButtonPrivyProps) => {
const { chainId } = useBreadUIKitContext();
const className =
app === "fund"
? "bg-primary-orange"
Expand All @@ -42,7 +41,7 @@ export const LoginButtonPrivy = ({
return (
<SwitchNetwork
activeWallet={activeWallet}
isProd={isProd}
chainId={chainId}
className={className}
/>
);
Expand All @@ -63,11 +62,11 @@ export const LoginButtonPrivy = ({

function SwitchNetwork({
activeWallet,
isProd,
chainId,
className,
}: {
activeWallet: ConnectedWallet;
isProd: boolean;
chainId: number;
className?: string;
}) {
return (
Expand All @@ -77,8 +76,7 @@ function SwitchNetwork({
if (!activeWallet) return;

try {
const targetChainId = isProd ? gnosis.id : 31337;
await activeWallet.switchChain(targetChainId);
await activeWallet.switchChain(chainId);
} catch (error) {
console.error("Failed to switch chain:", error);
}
Expand Down
10 changes: 7 additions & 3 deletions src/components/auth/login-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ import {
import { LoginButtonGeneral } from "./login-button-general";
import { useAuthProvider } from "../../context/lib";

export interface LoginButtonProps extends LoginButtonPrivyProps {
/** @deprecated Chain behavior is controlled via BreadUIKitProvider config. */
isProd?: boolean;
}

export const LoginButton = ({
label = "Sign In",
isProd,
isProd: _isProd,
...props
}: LoginButtonPrivyProps) => {
}: LoginButtonProps) => {
const authProvider = useAuthProvider();

if (authProvider === "privy") {
return (
<LoginButtonPrivy
{...props}
isProd={isProd}
label={label}
/>
);
Expand Down
32 changes: 18 additions & 14 deletions src/components/connected-user/privy-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
"use client";

import { ReactNode, useMemo } from "react";
import { anvil, gnosis } from "viem/chains";
import { type Hex } from "viem";
import { usePrivy, useWallets } from "@privy-io/react-auth";
import { TConnectedUserState, TUserConnected } from ".";
import { ConnectedUserContext } from "./context";
import { useBreadUIKitContext } from "../../context/lib";
import { resolveChain } from "./resolve-chain";

interface IConnectedUserProviderPrivyProps {
children: ReactNode;
isProd: boolean;
}

export function ConnectedUserProviderPrivy({
isProd,
children,
}: IConnectedUserProviderPrivyProps) {
const { ready, authenticated } = usePrivy();
const { wallets } = useWallets();
const { chainId: configuredChainId, supportedChainIds } = useBreadUIKitContext();

const embeddedWallet = useMemo(() => {
return wallets.find(
Expand All @@ -36,25 +36,29 @@ export function ConnectedUserProviderPrivy({
}

const address = embeddedWallet.address as Hex;
const chainId = embeddedWallet.chainId;
const walletChainId = embeddedWallet.chainId;
const parsedChainId = walletChainId
? parseInt(walletChainId.split(":")[1], 10)
: undefined;
const currentChainId =
parsedChainId !== undefined && Number.isFinite(parsedChainId)
? parsedChainId
: undefined;

const parsedChainId = chainId ? parseInt(chainId.split(":")[1]) : undefined;
const isSupportedChain =
currentChainId !== undefined && supportedChainIds.includes(currentChainId);
const _status: TUserConnected["status"] = isSupportedChain
? "CONNECTED"
: "UNSUPPORTED_CHAIN";

let _status: TUserConnected["status"] = "CONNECTED";
if (isProd) {
_status = parsedChainId === gnosis.id ? "CONNECTED" : "UNSUPPORTED_CHAIN";
} else {
_status = parsedChainId === anvil.id ? "CONNECTED" : "UNSUPPORTED_CHAIN";
}

const chain = isProd ? gnosis : anvil;
const chain = resolveChain(currentChainId ?? configuredChainId);

return {
status: _status,
address,
chain,
};
}, [ready, authenticated, embeddedWallet, isProd]);
}, [ready, authenticated, embeddedWallet, configuredChainId, supportedChainIds]);

// Embedded wallets are never Safe wallets
const isSafe = useMemo(() => {
Expand Down
26 changes: 13 additions & 13 deletions src/components/connected-user/provider-general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import { ReactNode, useMemo } from "react";
import { TConnectedUserState, TUserConnected } from "./interface";
import { useAccount } from "wagmi";
import { useAutoConnect } from "../../hooks/use-auto-connect";
import { anvil, gnosis } from "viem/chains";
import { ConnectedUserContext } from "./context";
import { useBreadUIKitContext } from "../../context/lib";
import { resolveChain } from "./resolve-chain";

interface IConnectedUserProviderGeneralProps {
children: ReactNode;
isProd: boolean;
}

export function ConnectedUserProviderGeneral({ isProd, children }: IConnectedUserProviderGeneralProps) {
export function ConnectedUserProviderGeneral({ children }: IConnectedUserProviderGeneralProps) {
const { isConnected, connector, address, status, chain } = useAccount();
const { isSafe } = useAutoConnect(connector);
const { chainId, supportedChainIds } = useBreadUIKitContext();

const user = useMemo<TConnectedUserState>(() => {
if (status === "connecting" && !address) {
Expand All @@ -25,20 +26,19 @@ export function ConnectedUserProviderGeneral({ isProd, children }: IConnectedUse
return { status: "NOT_CONNECTED" };
}

let _staus: TUserConnected["status"] = "CONNECTED";
if (isProd) {
_staus =
chain?.id === gnosis.id ? "CONNECTED" : "UNSUPPORTED_CHAIN";
} else {
_staus = chain?.id === anvil.id ? "CONNECTED" : "UNSUPPORTED_CHAIN";
}
const currentChainId = chain?.id;
const isSupportedChain =
currentChainId !== undefined && supportedChainIds.includes(currentChainId);
const userStatus: TUserConnected["status"] = isSupportedChain
? "CONNECTED"
: "UNSUPPORTED_CHAIN";

return {
status: _staus,
status: userStatus,
address,
chain: chain || (isProd ? gnosis : anvil),
chain: chain || resolveChain(chainId),
};
}, [isConnected, address, chain, status]);
}, [isConnected, address, chain, status, chainId, supportedChainIds]);

const value = useMemo(() => ({ user, isSafe }), [user, isSafe]);

Expand Down
15 changes: 6 additions & 9 deletions src/components/connected-user/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
"use client";

import { ReactNode } from "react";
import { useAuthProvider } from "../../context/lib";
import { ConnectedUserProviderPrivy } from "./privy-provider";
import { ConnectedUserProviderGeneral } from "./provider-general";

interface IConnectedUserProviderProps {
children: ReactNode;
isProd: boolean;
interface ConnectedUserProviderProps {
children: React.ReactNode;
/** @deprecated Chain behavior is controlled via BreadUIKitProvider config. */
isProd?: boolean;
}

export function ConnectedUserProvider({
isProd,
children,
}: IConnectedUserProviderProps) {
export function ConnectedUserProvider({ children, isProd: _isProd }: ConnectedUserProviderProps) {
const authProvider = useAuthProvider();

const Provider =
authProvider === "privy"
? ConnectedUserProviderPrivy
: ConnectedUserProviderGeneral;

return <Provider isProd={isProd}>{children}</Provider>;
return <Provider>{children}</Provider>;
}
29 changes: 29 additions & 0 deletions src/components/connected-user/resolve-chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { type Chain } from "viem";
import { anvil, gnosis, sepolia } from "viem/chains";

const KNOWN_CHAINS: Record<number, Chain> = {
[gnosis.id]: gnosis,
[anvil.id]: anvil,
[sepolia.id]: sepolia,
};

function createFallbackChain(chainId: number): Chain {
return {
id: chainId,
name: `Chain ${chainId}`,
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
default: {
http: [],
},
},
};
}

export function resolveChain(chainId: number): Chain {
return KNOWN_CHAINS[chainId] || createFallbackChain(chainId);
}
5 changes: 4 additions & 1 deletion src/components/navbar/account-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import * as NavigationMenu from "@radix-ui/react-navigation-menu";
import { type Address } from "viem";
import { type Address, type Chain } from "viem";
import { Body } from "../typography/Typography";
import { truncateAddress } from "../../utils/truncate-address";
import { CaretDownIcon } from "@phosphor-icons/react";
Expand All @@ -12,11 +12,13 @@ import { appsConfig } from "../../utils/app";
export interface AccountMenuProps
extends Pick<NavAccountDetailsProps, "widgetItems" | "ensNameResult" | "actionItems"> {
userAddress: Address;
chain: Chain;
app: App;
}

const AccountMenu = ({
userAddress,
chain,
ensNameResult,
app,
widgetItems,
Expand Down Expand Up @@ -44,6 +46,7 @@ const AccountMenu = ({
<NavAccountDetails
className="border w-full md:w-screen md:max-w-110.75 md:bg-paper-main md:border-paper-2"
userAddress={userAddress}
chain={chain}
ensNameResult={ensNameResult}
app={app}
widgetItems={widgetItems}
Expand Down
1 change: 1 addition & 0 deletions src/components/navbar/account-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const AccountSection = ({ app, widgetItems, actionItems }: AccountSectionProps)
widgetItems={widgetItems}
actionItems={actionItems}
userAddress={address}
chain={user.chain}
ensNameResult={ensNameResult}
app={app}
/>
Expand Down
Loading