Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ jest.mock('@/components/user/utils/UserCICAndLevel', () => ({
UserCICAndLevelSize: { XLARGE: 'XLARGE' },
}));

jest.mock('@/components/nft-transfer/TransferModalPfp', () => ({
__esModule: true,
default: () => <div data-testid="transfer-pfp" />,
}));

let mockOnProfileSelect: ((profile: any) => void) | null = null;
let mockOnWalletSelect: ((wallet: string | null) => void) | null = null;

Expand Down Expand Up @@ -85,6 +90,42 @@ describe('ManifoldMintingConnect', () => {
expect(onMintFor).toHaveBeenCalledWith(seizeCtx.address);
});

it('lets mint for me switch wallet inside connected profile', async () => {
const { onMintFor } = renderConnected();
const alternateWallet = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';

if (mockOnWalletSelect) {
mockOnWalletSelect(alternateWallet);
}

await waitFor(() =>
expect(onMintFor).toHaveBeenLastCalledWith(alternateWallet)
);
});

it('reselects connected wallet when switching back to mint for me', async () => {
const { onMintFor, seizeCtx } = renderConnected();
const alternateWallet = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
const frenWallet = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';

if (mockOnWalletSelect) {
mockOnWalletSelect(alternateWallet);
}
await waitFor(() =>
expect(onMintFor).toHaveBeenLastCalledWith(alternateWallet)
);

await userEvent.click(screen.getByRole('button', { name: /Mint for fren/i }));
if (mockOnProfileSelect && mockOnWalletSelect) {
mockOnProfileSelect({ handle: 'fren', wallet: frenWallet });
mockOnWalletSelect(frenWallet);
}
await waitFor(() => expect(onMintFor).toHaveBeenLastCalledWith(frenWallet));

await userEvent.click(screen.getByRole('button', { name: /Mint for me/i }));
await waitFor(() => expect(onMintFor).toHaveBeenLastCalledWith(seizeCtx.address));
});

it('shows RecipientSelector when mint for fren is clicked', async () => {
renderConnected();
await userEvent.click(screen.getByRole('button', { name: /Mint for fren/i }));
Expand Down
36 changes: 33 additions & 3 deletions components/auth/SeizeConnectContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, {
useState,
} from "react";

import { getNodeEnv, publicEnv } from "@/config/env";
import { getWalletAddress, removeAuthJwt } from "@/services/auth/auth.utils";
import { WalletInitializationError } from "@/src/errors/wallet";
import { SecurityEventType } from "@/src/types/security";
Expand Down Expand Up @@ -364,6 +365,23 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({
isInitialized,
} = useConsolidatedWalletState();
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const nodeEnv = getNodeEnv();
const isDevLikeEnv =
nodeEnv === "development" || nodeEnv === "test" || nodeEnv === "local";
const isLocalHost =
globalThis.window !== undefined &&
(globalThis.window.location.hostname === "localhost" ||
globalThis.window.location.hostname === "127.0.0.1" ||
globalThis.window.location.hostname === "::1" ||
globalThis.window.location.hostname.endsWith(".local"));
const impersonatedAddress =
isDevLikeEnv &&
isLocalHost &&
publicEnv.USE_DEV_AUTH === "true" &&
publicEnv.DEV_MODE_WALLET_ADDRESS &&
isAddress(publicEnv.DEV_MODE_WALLET_ADDRESS)
? getAddress(publicEnv.DEV_MODE_WALLET_ADDRESS)
: undefined;
Comment thread
prxt6529 marked this conversation as resolved.

useEffect(() => {
// Wait for initialization to complete before processing account changes
Expand All @@ -378,6 +396,16 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({

// Use debounced state update to prevent race conditions
debounceTimeoutRef.current = setTimeout(() => {
if (impersonatedAddress) {
const isAlreadyConnected =
walletState.status === "connected" &&
walletState.address === impersonatedAddress;
if (!isAlreadyConnected) {
setConnected(impersonatedAddress);
}
return;
}

if (account.address && account.isConnected) {
// Validate and normalize address to checksummed format
if (isAddress(account.address)) {
Expand Down Expand Up @@ -442,6 +470,7 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({
setConnected,
setDisconnected,
setConnecting,
impersonatedAddress,
]);

const seizeConnect = useCallback((): void => {
Expand Down Expand Up @@ -584,7 +613,7 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({

const contextValue = useMemo(
(): SeizeConnectContextType => ({
address: connectedAddress,
address: impersonatedAddress ?? connectedAddress,
walletName: walletInfo?.name,
walletIcon: walletInfo?.icon,
isSafeWallet: isSafeWalletInfo(walletInfo),
Expand All @@ -593,15 +622,16 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({
seizeDisconnectAndLogout,
seizeAcceptConnection,
seizeConnectOpen: state.open,
isConnected: account.isConnected,
isAuthenticated: !!connectedAddress,
isConnected: impersonatedAddress ? true : account.isConnected,
isAuthenticated: !!(impersonatedAddress ?? connectedAddress),
connectionState: walletState.status, // Unified state machine
walletState, // Expose unified state for advanced consumers
hasInitializationError,
initializationError,
}),
[
connectedAddress,
impersonatedAddress,
walletInfo?.name,
walletInfo?.icon,
walletInfo?.type,
Expand Down
110 changes: 75 additions & 35 deletions components/common/RecipientSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,27 @@ function RecipientSelectedDisplay({
profile,
isIdentityLoading,
onClear,
allowProfileChange,
showSelectedProfileCard,
walletsListRef,
walletsHasOverflow,
walletsAtEnd,
selectedWallet,
onWalletSelect,
disableSingleWalletSelection,
}: {
readonly selectedProfile: CommunityMemberMinimal;
readonly profile: ApiIdentity | null;
readonly isIdentityLoading: boolean;
readonly onClear: () => void;
readonly allowProfileChange: boolean;
readonly showSelectedProfileCard: boolean;
readonly walletsListRef: React.RefObject<HTMLDivElement | null>;
readonly walletsHasOverflow: boolean;
readonly walletsAtEnd: boolean;
readonly selectedWallet: string | null;
readonly onWalletSelect: (wallet: string) => void;
readonly disableSingleWalletSelection: boolean;
}) {
const getWallets = () => {
if (profile?.wallets && profile.wallets.length > 0) {
Expand Down Expand Up @@ -89,23 +95,42 @@ function RecipientSelectedDisplay({
</div>
);
} else {
const hasSingleWallet = wallets.length === 1;
const isWalletSelectionDisabled =
disableSingleWalletSelection && hasSingleWallet;

walletsContent = wallets.map(
(w: { wallet: string; display: string | null; tdh: number }) => {
const isSel = selectedWallet?.toLowerCase() === w.wallet.toLowerCase();
const hasDisplay =
w.display && w.display.toLowerCase() !== w.wallet.toLowerCase();
const classes = [
"tw-flex tw-min-h-[58px] tw-w-full tw-flex-col tw-rounded-lg tw-border tw-border-white/10 tw-bg-white/10 tw-p-2",
hasDisplay
? "tw-items-start tw-justify-between"
: "tw-items-start tw-justify-center",
isSel ? "tw-border-2 tw-border-solid !tw-border-emerald-400" : "",
].join(" ");

if (isWalletSelectionDisabled) {
return (
<div key={w.wallet} className={classes}>
<div className="tw-text-sm tw-font-medium">
{w.display || w.wallet}
</div>
{hasDisplay && (
<div className="tw-text-[11px] tw-opacity-60">{w.wallet}</div>
)}
</div>
);
}

return (
<button
key={w.wallet}
type="button"
onClick={() => onWalletSelect(w.wallet)}
className={[
"tw-flex tw-min-h-[58px] tw-w-full tw-flex-col tw-rounded-lg tw-border tw-border-white/10 tw-bg-white/10 tw-p-2 hover:tw-bg-white/15",
hasDisplay
? "tw-items-start tw-justify-between"
: "tw-items-start tw-justify-center",
isSel ? "tw-border-2 tw-border-solid !tw-border-emerald-400" : "",
].join(" ")}
className={[classes, "hover:tw-bg-white/15"].join(" ")}
>
<div className="tw-text-sm tw-font-medium">
{w.display || w.wallet}
Expand All @@ -121,38 +146,44 @@ function RecipientSelectedDisplay({

return (
<>
<div className="tw-flex tw-items-center tw-justify-between tw-rounded-lg tw-bg-white/10 tw-px-3 tw-py-2">
<div className="tw-flex tw-min-w-0 tw-items-center tw-gap-3">
<TransferModalPfp
src={selectedProfile.pfp}
alt={
selectedProfile.display ||
selectedProfile.handle ||
selectedProfile.wallet
}
level={selectedProfile.level}
/>
<div className="tw-min-w-0">
<div className="tw-truncate tw-text-sm tw-font-medium">
{selectedProfile.handle || selectedProfile.display}
</div>
<div className="tw-truncate tw-text-[11px] tw-opacity-60">
TDH: {selectedProfile.tdh.toLocaleString()} - Level:{" "}
{selectedProfile.level}
{showSelectedProfileCard && (
<div className="tw-flex tw-items-center tw-justify-between tw-rounded-lg tw-bg-white/10 tw-px-3 tw-py-2">
<div className="tw-flex tw-min-w-0 tw-items-center tw-gap-3">
<TransferModalPfp
src={selectedProfile.pfp}
alt={
selectedProfile.display ||
selectedProfile.handle ||
selectedProfile.wallet
}
level={selectedProfile.level}
/>
<div className="tw-min-w-0">
<div className="tw-truncate tw-text-sm tw-font-medium">
{selectedProfile.handle || selectedProfile.display}
</div>
<div className="tw-truncate tw-text-[11px] tw-opacity-60">
TDH: {selectedProfile.tdh.toLocaleString()} - Level:{" "}
{selectedProfile.level}
</div>
</div>
</div>
{allowProfileChange && (
<button
type="button"
className="tw-rounded-md tw-border-2 tw-border-solid tw-border-[#444] tw-bg-white/10 tw-px-2 tw-py-1 !tw-text-xs tw-font-medium hover:tw-bg-white/15"
onClick={onClear}
>
Change
</button>
)}
</div>
<button
type="button"
className="tw-rounded-md tw-border-2 tw-border-solid tw-border-[#444] tw-bg-white/10 tw-px-2 tw-py-1 !tw-text-xs tw-font-medium hover:tw-bg-white/15"
onClick={onClear}
>
Change
</button>
</div>
)}

<div className="tw-flex tw-min-h-0 tw-flex-col tw-space-y-2 tw-pt-4">
{wallets.length > 1 && (
<div
className={`tw-flex tw-min-h-0 tw-flex-col tw-space-y-2 ${showSelectedProfileCard ? "tw-pt-4" : "tw-pt-0"}`}
>
{(showSelectedProfileCard ? wallets.length > 1 : wallets.length > 0) && (
<div className="tw-text-sm">Choose destination wallet</div>
)}
<div
Expand Down Expand Up @@ -253,6 +284,9 @@ interface RecipientSelectorProps {
readonly placeholder?: string;
readonly showLabel?: boolean;
readonly label?: string;
readonly allowProfileChange?: boolean;
readonly disableSingleWalletSelection?: boolean;
readonly showSelectedProfileCard?: boolean;
}

export default function RecipientSelector({
Expand All @@ -264,6 +298,9 @@ export default function RecipientSelector({
placeholder,
showLabel = true,
label = "Recipient",
allowProfileChange = true,
disableSingleWalletSelection = false,
showSelectedProfileCard = true,
}: RecipientSelectorProps) {
const [query, setQuery] = useState("");
const [isSearching, setIsSearching] = useState(false);
Expand Down Expand Up @@ -525,11 +562,14 @@ export default function RecipientSelector({
profile={profile}
isIdentityLoading={isIdentityLoading}
onClear={handleClear}
allowProfileChange={allowProfileChange}
showSelectedProfileCard={showSelectedProfileCard}
walletsListRef={walletsListRef}
walletsHasOverflow={walletsHasOverflow}
walletsAtEnd={walletsAtEnd}
selectedWallet={selectedWallet}
onWalletSelect={onWalletSelect}
disableSingleWalletSelection={disableSingleWalletSelection}
/>
) : (
<RecipientSearchDisplay
Expand Down
Loading