Skip to content
Draft
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
8 changes: 5 additions & 3 deletions packages/example/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,11 @@ const Example = ({ authEnabled }: AppContextProps) => {
gap: '20px',
}}
>
{['rainbow', 'metamask', 'baseAccount'].map((connector) => {
return <WalletButton key={connector} wallet={connector} />;
})}
{(['rainbow', 'metaMask', 'baseAccount'] as const).map(
(connector) => {
return <WalletButton key={connector} wallet={connector} />;
},
)}
</div>
</div>

Expand Down
15 changes: 14 additions & 1 deletion packages/rainbowkit/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,23 +133,36 @@ const walletsBuildOptions = {
outdir: 'dist/wallets/walletConnectors',
};

const componentsBuildOptions = {
...baseBuildConfig((result) => {
if (result.errors.length) {
console.error('❌ components build failed', result.errors);
} else console.log('✅ components build succeeded');
}),
entryPoints: await getAllEntryPoints('src/components'),
outdir: 'dist/components',
};

const build = async () => {
// Build and watch for new changes if --watch flag is passed
if (isWatching) {
const [mainContext, walletsContext] = await Promise.all([
const [mainContext, walletsContext, componentsContext] = await Promise.all([
esbuild.context(mainBuildOptions),
esbuild.context(walletsBuildOptions),
esbuild.context(componentsBuildOptions),
]);

await mainContext.watch();
await walletsContext.watch();
await componentsContext.watch();
return;
}

// Only build once
await Promise.all([
esbuild.build(mainBuildOptions),
esbuild.build(walletsBuildOptions),
esbuild.build(componentsBuildOptions),
]);
};

Expand Down
3 changes: 3 additions & 0 deletions packages/rainbowkit/components/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"main": "../dist/components/index.js"
}
6 changes: 4 additions & 2 deletions packages/rainbowkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
"files": [
"dist",
"styles.css",
"wallets"
"wallets",
"components"
],
"type": "module",
"exports": {
".": "./dist/index.js",
"./styles.css": "./dist/index.css",
"./wallets": "./dist/wallets/walletConnectors/index.js"
"./wallets": "./dist/wallets/walletConnectors/index.js",
"./components": "./dist/components/index.js"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
4 changes: 2 additions & 2 deletions packages/rainbowkit/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Box } from '../Box/Box';
import { SpinnerIcon } from '../Icons/Spinner';
import { AvatarContext } from '../RainbowKitProvider/AvatarContext';

interface AvatarProps {
export type AvatarProps = {
address: string;
loading?: boolean;
imageUrl?: string | null;
size: number;
}
};

export function Avatar({ address, imageUrl, loading, size }: AvatarProps) {
const AvatarComponent = useContext(AvatarContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import { Box } from '../Box/Box';
import { SpinnerIcon } from '../Icons/Spinner';
import { I18nContext } from '../RainbowKitProvider/I18nContext';
import * as styles from './WalletButton.css';
import { WalletButtonRenderer } from './WalletButtonRenderer';
import { WalletButtonRenderer, type WalletId } from './WalletButtonRenderer';

export const WalletButton = ({ wallet }: { wallet?: string }) => {
export interface WalletButtonProps {
wallet?: WalletId;
}

export const WalletButton = ({ wallet }: WalletButtonProps) => {
return (
<WalletButtonRenderer wallet={wallet}>
{({ ready, connect, connected, mounted, connector, loading }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ import {
useModalState,
} from '../RainbowKitProvider/ModalContext';
import { WalletButtonContext } from '../RainbowKitProvider/WalletButtonContext';
import type * as walletConnectors from '../../wallets/walletConnectors';

type WalletFactories = typeof walletConnectors;
export type WalletId = ReturnType<WalletFactories[keyof WalletFactories]>['id'];

export interface WalletButtonRendererProps {
wallet?: string;
wallet?: WalletId;
children: (renderProps: {
error: boolean;
loading: boolean;
Expand Down
28 changes: 27 additions & 1 deletion packages/rainbowkit/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
// ============================ Button Components ============================
// Main connect button with wallet connection flow
export { ConnectButton } from './ConnectButton/ConnectButton';
export type { ConnectButtonProps } from './ConnectButton/ConnectButton';

// Individual wallet button for specific wallet connections
export { WalletButton } from './WalletButton/WalletButton';
export { RainbowKitProvider } from './RainbowKitProvider/RainbowKitProvider';
export type { WalletButtonProps } from './WalletButton/WalletButton';

// ============================ Avatar Components ============================
// Main Avatar component that displays user avatars with optional loading states
export { Avatar } from './Avatar/Avatar';
export type { AvatarProps } from './Avatar/Avatar';

// Default emoji-based avatar implementation
export { EmojiAvatar } from './Avatar/EmojiAvatar';
export type { AvatarComponentProps as EmojiAvatarProps } from './RainbowKitProvider/AvatarContext';

// Utility function to generate emoji avatars based on wallet addresses
export { emojiAvatarForAddress } from './Avatar/emojiAvatarForAddress';

// ============================ Modal Components ============================
// Modal for displaying account details, balance, and disconnect functionality
export { AccountModal } from './AccountModal/AccountModal';
export type { AccountModalProps } from './AccountModal/AccountModal';

// Modal for switching between different chains
export { ChainModal } from './ChainModal/ChainModal';
export type { ChainModalProps } from './ChainModal/ChainModal';
25 changes: 20 additions & 5 deletions packages/rainbowkit/src/wallets/Wallet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Connector, CreateConnectorFn } from 'wagmi';
import type { WalletConnectParameters } from 'wagmi/connectors';
import type { CoinbaseWalletOptions } from './walletConnectors/coinbaseWallet/coinbaseWallet';
import type { WalletConnectWalletOptions } from './walletConnectors/walletConnectWallet/walletConnectWallet';

export type InstructionStepName =
Expand Down Expand Up @@ -77,17 +76,33 @@ export type Wallet = {
createConnector: (walletDetails: WalletDetailsParams) => CreateConnectorFn;
} & RainbowKitConnector;

/**
* Generic helper type for wallet factory functions that preserves literal ID types.
*
* @remarks
* - The `as const` assertion on the `id` field is REQUIRED for literal type preservation
* - Without it, TypeScript infers `id: string` instead of `id: 'literal'`
* - The `satisfies Wallet` validates the implementation conforms to the Wallet interface
*/
export type WalletFactory<Opts, ID extends string> = Opts extends void
? () => Wallet & { id: ID }
: (options: Opts) => Wallet & { id: ID };

export interface DefaultWalletOptions {
projectId: string;
walletConnectParameters?: RainbowKitWalletConnectParameters;
}

export type CreateWalletFn = (
// These parameters will be used when creating a wallet. If injected
// wallet doesn't have parameters it will just ignore these passed in parameters
createWalletParams: CoinbaseWalletOptions &
Omit<WalletConnectWalletOptions, 'projectId'> &
DefaultWalletOptions,
// wallet doesn't have parameters it will just ignore these passed in parameters.
// Note: appName and appIcon are required by certain wallets (baseAccount,
// coinbaseWallet, geminiWallet) that need app metadata for their connectors.
createWalletParams: Omit<WalletConnectWalletOptions, 'projectId'> &
DefaultWalletOptions & {
appName: string;
appIcon?: string;
},
) => Wallet;

export type WalletList = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ export type ZilPayWalletOptions = DefaultWalletOptions;
export const zilPayWallet = ({
projectId,
walletConnectParameters,
}: ZilPayWalletOptions): Wallet => {
}: ZilPayWalletOptions) => {
const isZilPayInjected = hasInjectedProvider({ flag: 'isZilPay' });
const shouldUseWalletConnect = !isZilPayInjected;

return {
id: 'zilpay',
id: 'zilpay' as const,
name: 'ZilPay',
rdns: 'io.zilpay',
iconUrl: async () => (await import('./zilPayWallet.svg')).default,
Expand Down Expand Up @@ -69,5 +69,5 @@ export const zilPayWallet = ({
: getInjectedConnector({
flag: 'isZilPay',
}),
};
} satisfies Wallet;
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ export type ArgentWalletOptions = ReadyWalletOptions;
/**
* @deprecated Use {@link readyWallet} instead.
*/
export const argentWallet = (options: ArgentWalletOptions): Wallet => {
export const argentWallet = (options: ArgentWalletOptions) => {
const wallet = readyWallet(options);

return {
...wallet,
id: 'argent',
};
id: 'argent' as const,
} satisfies Wallet;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
} from '../../getInjectedConnector';
import type { Wallet } from '../../Wallet';

export const backpackWallet = (): Wallet => {
export const backpackWallet = () => {
return {
id: 'backpack',
id: 'backpack' as const,
name: 'Backpack',
rdns: 'app.backpack.mobile',
iconUrl: async () => (await import('./backpackWallet.svg')).default,
Expand Down Expand Up @@ -48,5 +48,5 @@ export const backpackWallet = (): Wallet => {
},
},
createConnector: getInjectedConnector({ namespace: 'backpack.ethereum' }),
};
} satisfies Wallet;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,22 @@ import {
type BaseAccountParameters,
baseAccount as baseAccountConnector,
} from 'wagmi/connectors';
import type { Wallet, WalletDetailsParams } from '../../Wallet';
import type { Wallet, WalletDetailsParams, WalletFactory } from '../../Wallet';

export interface BaseAccountOptions {
// supports preference, paymasterUrls, subAccounts
export interface BaseAccountOptions
extends Omit<BaseAccountParameters, 'appName' | 'appLogoUrl'> {
appName: string;
appIcon?: string;
}

// supports preference, paymasterUrls, subAccounts
type AcceptedBaseAccountParameters = Omit<
BaseAccountParameters,
'appName' | 'appLogoUrl'
>;

interface BaseAccount extends AcceptedBaseAccountParameters {
(params: BaseAccountOptions): Wallet;
}

export const baseAccount: BaseAccount = ({ appName, appIcon }) => {
export const baseAccount: WalletFactory<BaseAccountOptions, 'baseAccount'> = ({
appName,
appIcon,
...optionalConfig
}) => {
return {
id: 'baseAccount',
id: 'baseAccount' as const,
name: 'Base Account',
shortName: 'Base Account',
rdns: 'app.base.account',
Expand All @@ -32,11 +28,6 @@ export const baseAccount: BaseAccount = ({ appName, appIcon }) => {
// a popup will appear prompting the user to connect or create a wallet via passkey.
installed: true,
createConnector: (walletDetails: WalletDetailsParams) => {
// Extract all AcceptedBaseAccountParameters from baseAccount
// This approach avoids type errors for properties not yet in upstream connector
const { ...optionalConfig }: AcceptedBaseAccountParameters =
baseAccount as any;

const connector: CreateConnectorFn = baseAccountConnector({
appName,
appLogoUrl: appIcon,
Expand All @@ -48,5 +39,5 @@ export const baseAccount: BaseAccount = ({ appName, appIcon }) => {
...walletDetails,
}));
},
};
} satisfies Wallet;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ export type BerasigWalletOptions = DefaultWalletOptions;
export const berasigWallet = ({
projectId,
walletConnectParameters,
}: BerasigWalletOptions): Wallet => {
}: BerasigWalletOptions) => {
const isBerasigWalletInjected = hasInjectedProvider({
namespace: 'berasig.ethereum',
});

const shouldUseWalletConnect = !isBerasigWalletInjected;
return {
id: 'berasig',
id: 'berasig' as const,
name: 'BeraSig',
rdns: 'app.berasig',
iconUrl: async () => (await import('./berasigWallet.svg')).default,
Expand Down Expand Up @@ -64,5 +64,5 @@ export const berasigWallet = ({
: getInjectedConnector({
namespace: 'berasig.ethereum',
}),
};
} satisfies Wallet;
};
Loading