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
5 changes: 5 additions & 0 deletions .changeset/tidy-wallet-visibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/mosaic': patch
---

Hide the Web3 wallet section when Web3 wallets are disabled in the instance settings.
2 changes: 2 additions & 0 deletions .changeset/web3-wallets-wire-up.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
3 changes: 2 additions & 1 deletion packages/mosaic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
},
"dependencies": {
"@clerk/shared": "workspace:^",
"@floating-ui/react": "catalog:repo"
"@floating-ui/react": "catalog:repo",
"@wallet-standard/core": "catalog:module-manager"
},
"devDependencies": {
"@stylexjs/eslint-plugin": "0.19.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { ClerkRuntimeError } from '@clerk/shared/error';
import type * as SharedReact from '@clerk/shared/react';
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { MosaicProvider } from '../../../MosaicProvider';
import { UserProfileWeb3WalletsSection } from '../user-profile-web3-wallets-section/user-profile-web3-wallets-section';

const request = vi.fn();
const attributes: { web3_wallet?: { enabled: boolean } } = {};
vi.mock('@clerk/shared/internal/clerk-js/web3', () => ({
createWeb3: () => ({ getWeb3Identifier: () => Promise.resolve('0x1234') }),
}));
vi.mock('@clerk/shared/react', async importOriginal => {
const actual = await importOriginal<typeof SharedReact>();
return {
...actual,
useUser: () => ({
isLoaded: true,
user: {
id: 'user_1',
enterpriseAccounts: [],
primaryWeb3WalletId: null,
web3Wallets: [
{
id: 'wallet_1',
web3Wallet: '0xabcdef',
verification: { strategy: 'admin', status: 'verified' },
destroy: request,
},
],
createWeb3Wallet: request,
update: request,
},
}),
useClerk: () => ({
__internal_getOption: () => undefined,
__internal_moduleManager: {},
__internal_environment: {
userSettings: {
attributes,
web3FirstFactors: ['web3_metamask_signature'],
enterpriseSSO: { enabled: false },
},
displayConfig: { supportEmail: 'support@example.com' },
},
}),
useSession: () => ({ session: { id: 'session_1' } }),
useReverification: <F,>(fetcher: F) => fetcher,
};
});

describe('Web3 wallets', () => {
beforeEach(() => {
attributes.web3_wallet = { enabled: true };
request.mockReset().mockRejectedValue(new ClerkRuntimeError('Cancelled', { code: 'reverification_cancelled' }));
});

it.each([false, undefined])('hides existing wallets and providers when Web3 enabled is %s', enabled => {
attributes.web3_wallet = enabled === undefined ? undefined : { enabled };

render(
<MosaicProvider>
<UserProfileWeb3WalletsSection />
</MosaicProvider>,
);

expect(screen.queryByRole('button', { name: 'Manage 0xabcdef' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Connect MetaMask' })).not.toBeInTheDocument();
expect(request).not.toHaveBeenCalled();
});

it.each(['connect', 'primary', 'remove'] as const)(
'keeps %s cancellation out of the visible errors',
async action => {
const user = userEvent.setup();
render(
<MosaicProvider>
<UserProfileWeb3WalletsSection />
</MosaicProvider>,
);
if (action === 'connect') {
await user.click(screen.getByRole('button', { name: 'Connect MetaMask' }));
} else {
await user.click(screen.getByRole('button', { name: 'Manage 0xabcdef' }));
await user.click(
screen.getByRole('menuitem', { name: action === 'primary' ? 'Set as primary' : 'Remove wallet' }),
);
if (action === 'remove') {
await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove' }));
}
}
await waitFor(() => expect(request).toHaveBeenCalledOnce());
if (action === 'remove') {
await waitFor(() =>
expect(
within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove', exact: true }),
).not.toHaveAttribute('aria-busy', 'true'),
);
} else {
await waitFor(() => expect(screen.getByRole('button', { name: 'Connect MetaMask' })).not.toBeDisabled());
}
expect(screen.queryByText(/Cancelled/)).not.toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
if (action === 'remove') {
expect(screen.getByRole('alertdialog')).toBeInTheDocument();
}
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface UserProfileProfilePanelViewProps extends UserProfileAccountSect
onRemoveConnectedAccount?: (id: string) => void | Promise<void>;
/** Connected accounts section. Replaces the one built from `connectedAccounts` and its callbacks. */
connectedAccountsSlot?: ReactNode;
web3WalletsSlot?: ReactNode;
onConnectWeb3Wallet?: (id: string) => void;
onSetPrimaryWeb3Wallet?: (id: string) => void;
onRemoveWeb3Wallet?: (id: string) => void | Promise<void>;
Expand Down Expand Up @@ -80,6 +81,7 @@ export function UserProfileProfilePanelView({
onConnectAccount,
onRemoveConnectedAccount,
connectedAccountsSlot,
web3WalletsSlot,
onConnectWeb3Wallet,
onSetPrimaryWeb3Wallet,
onRemoveWeb3Wallet,
Expand Down Expand Up @@ -136,14 +138,16 @@ export function UserProfileProfilePanelView({
onRemove={onRemoveConnectedAccount}
/>
)}
<UserProfileWeb3WalletsSectionView
fallbackFocus={() => pageTitle.current}
wallets={web3Wallets}
availableProviders={availableWeb3Providers}
onConnect={onConnectWeb3Wallet}
onRemove={onRemoveWeb3Wallet}
onSetPrimary={onSetPrimaryWeb3Wallet}
/>
{web3WalletsSlot ?? (
<UserProfileWeb3WalletsSectionView
fallbackFocus={() => pageTitle.current}
wallets={web3Wallets}
availableProviders={availableWeb3Providers}
onConnect={onConnectWeb3Wallet}
onRemove={onRemoveWeb3Wallet}
onSetPrimary={onSetPrimaryWeb3Wallet}
/>
)}
{deleteAccountSlot}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Ref } from 'react';
import type { ActionMenuAction } from '../../components/action-menu';
import { ActionMenu } from '../../components/action-menu';
import { Badge } from '../../components/badge';
import { Button } from '../../components/button';
import { SubmitButton } from '../../components/button';
import { Icon, IconFrame } from '../../components/icon';
import { Section } from '../../components/section';
import { fill, useMessages } from '../../localization';
Expand All @@ -14,12 +14,16 @@ import type { UserProfileWeb3Provider, UserProfileWeb3Wallet } from './user-prof
export function UserProfileWeb3WalletRowView({
wallet,
triggerRef,
isPending = false,
isDisabled = false,
onConnect,
onSetPrimary,
onRemove,
}: {
wallet: UserProfileWeb3Wallet | UserProfileWeb3Provider;
triggerRef?: Ref<HTMLButtonElement>;
isPending?: boolean;
isDisabled?: boolean;
onConnect?: (id: string) => void;
onSetPrimary?: (id: string) => void;
onRemove?: (wallet: UserProfileWeb3Wallet) => void;
Expand All @@ -31,7 +35,7 @@ export function UserProfileWeb3WalletRowView({
const shortAddress = address && (address.length <= 10 ? address : `${address.slice(0, 6)}...${address.slice(-4)}`);
const actions: ActionMenuAction[] = [];

if (linkedWallet && !linkedWallet.isPrimary && linkedWallet.isVerified && onSetPrimary) {
if (linkedWallet && !linkedWallet.isPrimary && linkedWallet.isVerified && onSetPrimary && !isDisabled) {
actions.push({ label: m.setPrimary, onClick: () => onSetPrimary(wallet.id) });
}
if (linkedWallet && onRemove && linkedWallet.canRemove !== false) {
Expand Down Expand Up @@ -79,10 +83,12 @@ export function UserProfileWeb3WalletRowView({
</Section.Content>
{onConnect ? (
<Section.Actions>
<Button
<SubmitButton
color='neutral'
size='sm'
variant='outline'
isPending={isPending}
disabled={isDisabled && !isPending}
aria-label={fill(m.connectLabel, { provider: wallet.provider ?? '' })}
onClick={() => onConnect(wallet.id)}
>
Expand All @@ -92,7 +98,7 @@ export function UserProfileWeb3WalletRowView({
placement='inline-end'
size='sm'
/>
</Button>
</SubmitButton>
</Section.Actions>
) : actions.length > 0 ? (
<Section.Actions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Section } from '../../components/section';
import { useListRemovalFocus } from '../../hooks/useListRemovalFocus';
import { fill, useMessages } from '../../localization';
import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible';
import type { ReverificationController } from '../reverification';
import { UserProfileWeb3WalletRowView } from './user-profile-web3-wallet-row.view';

export interface UserProfileWeb3Provider {
Expand All @@ -29,6 +30,8 @@ export interface UserProfileWeb3WalletsSectionViewProps {
fallbackFocus?: () => HTMLElement | null;
wallets: UserProfileWeb3Wallet[];
availableProviders?: UserProfileWeb3Provider[];
pendingId?: string;
removeReverification?: ReverificationController;
onConnect?: (id: string) => void;
onSetPrimary?: (id: string) => void;
onRemove?: (id: string) => void | Promise<void>;
Expand All @@ -38,9 +41,11 @@ export function UserProfileWeb3WalletsSectionView({
wallets,
fallbackFocus,
availableProviders = [],
pendingId,
onConnect,
onSetPrimary,
onRemove,
removeReverification,
}: UserProfileWeb3WalletsSectionViewProps) {
const m = useMessages('userProfileWeb3Wallets');
const section = useRef<HTMLElement>(null);
Expand All @@ -55,6 +60,7 @@ export function UserProfileWeb3WalletsSectionView({
});
const removeWallet = useMemo(() => Confirmation.createHandle<UserProfileWeb3Wallet>(), []);
const hasRows = wallets.length > 0 || (availableProviders.length > 0 && Boolean(onConnect));
const isBusy = pendingId !== undefined;

return (
<>
Expand All @@ -70,6 +76,7 @@ export function UserProfileWeb3WalletsSectionView({
key={wallet.id}
wallet={wallet}
triggerRef={removalFocus.registerTrigger(wallet.id)}
isDisabled={isBusy}
onSetPrimary={onSetPrimary}
onRemove={onRemove ? wallet => removeWallet.open(wallet) : undefined}
/>
Expand All @@ -79,6 +86,8 @@ export function UserProfileWeb3WalletsSectionView({
<UserProfileWeb3WalletRowView
key={provider.id}
wallet={provider}
isPending={pendingId === provider.id}
isDisabled={isBusy}
onConnect={onConnect}
/>
))
Expand All @@ -89,6 +98,7 @@ export function UserProfileWeb3WalletsSectionView({
{onRemove ? (
<Confirmation
handle={removeWallet}
reverification={removeReverification}
title={m.removeDialog.title}
description={wallet =>
fill(wallet.isVerified ? m.removeDialog.verifiedDescription : m.removeDialog.description, {
Expand Down
Loading
Loading