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
51 changes: 43 additions & 8 deletions src/components/common/CreatorProfileHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Copy, Check, Share2 } from 'lucide-react';
import { Copy, Check, Share2, Pencil } from 'lucide-react';
import showToast from '@/utils/toast.util';
import appendUtmParams from '@/utils/utm.utils';
import { copyTextToClipboard } from '@/utils/clipboard.utils';
Expand All @@ -12,6 +12,7 @@ import CreatorBio from '@/components/common/CreatorBio';
import { formatCreatorHandle } from '@/utils/handleDisplay.utils';
import { normalizeCreatorDisplayName } from '@/utils/creatorDisplayName.utils';
import { CREATOR_CARD_MEDIA_RADIUS_CLASS } from '@/utils/creatorCardTokens';
import { isOwnWallet } from '@/utils/isOwnWallet';

interface CreatorProfileHeaderProps {
name: string;
Expand All @@ -21,6 +22,7 @@ interface CreatorProfileHeaderProps {
isVerified?: boolean;
bio?: string | null;
className?: string;
connectedWalletAddress?: string | null;
}

const CREATOR_PROFILE_SUBTITLE_WRAP_CLASS_NAME =
Expand All @@ -34,6 +36,7 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
isVerified,
bio,
className,
connectedWalletAddress,
}) => {
const [copied, setCopied] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
Expand All @@ -50,6 +53,10 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
// URL construction the caller might do via the prop.
const displayHandle = formatCreatorHandle(handle);
const displayName = normalizeCreatorDisplayName(name) || 'Unnamed creator';
const normalizedCreatorId =
creatorId == null ? creatorId : String(creatorId);

const own = isOwnWallet(connectedWalletAddress, normalizedCreatorId);

const handleShare = async () => {
let url = window.location.href;
Expand Down Expand Up @@ -163,13 +170,41 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
</div>
</div>

<div
className={cn(
'flex items-center gap-3 transition-transform duration-300',
isScrolled ? 'scale-90' : 'scale-100'
)}
>
<Button
<div
className={cn(
'flex items-center gap-3 transition-transform duration-300',
isScrolled ? 'scale-90' : 'scale-100'
)}
>
{own && (
<>
<Button
aria-label="Edit bio"
variant="outline"
className={cn(
'rounded-xl border-white/10 bg-white/5 font-bold text-white transition-all hover:border-amber-500/30 hover:bg-amber-500/10 active:scale-95',
isScrolled ? 'h-9 px-3 text-xs' : 'h-11 px-4 text-sm'
)}
>
<Pencil className="mr-2 size-4 text-amber-500" />
<span className="hidden sm:inline">Edit Bio</span>
<span className="sm:hidden">Edit</span>
</Button>
<Button
aria-label="Change avatar"
variant="outline"
className={cn(
'rounded-xl border-white/10 bg-white/5 font-bold text-white transition-all hover:border-amber-500/30 hover:bg-amber-500/10 active:scale-95',
isScrolled ? 'h-9 px-3 text-xs' : 'h-11 px-4 text-sm'
)}
>
<Pencil className="mr-2 size-4 text-amber-500" />
<span className="hidden sm:inline">Change Avatar</span>
<span className="sm:hidden">Avatar</span>
</Button>
</>
)}
<Button
onClick={handleShare}
variant="outline"
className={cn(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';

import CreatorProfileHeader from '@/components/common/CreatorProfileHeader';

const CREATOR_ADDRESS = '0xCreator1111111111111111111111111111111111';
const OTHER_ADDRESS = '0xOther22222222222222222222222222222222222';

const BASE_PROPS = {
name: 'Alex Rivers',
handle: 'arivers',
creatorId: CREATOR_ADDRESS,
avatarUrl: 'https://example.com/avatar.png',
};

describe('CreatorProfileHeader – isOwnWallet edit-controls visibility', () => {
it('hides edit controls when the connected wallet does not match the creator', () => {
const { container } = render(
<CreatorProfileHeader
{...BASE_PROPS}
connectedWalletAddress={OTHER_ADDRESS}
/>
);

expect(screen.queryByRole('button', { name: /edit bio/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /change avatar/i })).not.toBeInTheDocument();

// Confirm the controls are truly absent from the DOM, not just hidden
expect(container.querySelector('[aria-label="Edit bio"]')).toBeNull();
expect(container.querySelector('[aria-label="Change avatar"]')).toBeNull();
});

it('shows edit controls when the connected wallet matches the creator', () => {
render(
<CreatorProfileHeader
{...BASE_PROPS}
connectedWalletAddress={CREATOR_ADDRESS}
/>
);

expect(screen.getByRole('button', { name: /edit bio/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /change avatar/i })).toBeInTheDocument();
});

it('matches case-insensitively so mixed-case addresses are treated as the same wallet', () => {
render(
<CreatorProfileHeader
{...BASE_PROPS}
connectedWalletAddress={CREATOR_ADDRESS.toLowerCase()}
/>
);

expect(screen.getByRole('button', { name: /edit bio/i })).toBeInTheDocument();
});

it('toggles edit controls when the connected wallet changes', () => {
const { rerender } = render(
<CreatorProfileHeader
{...BASE_PROPS}
connectedWalletAddress={OTHER_ADDRESS}
/>
);

expect(screen.queryByRole('button', { name: /edit bio/i })).not.toBeInTheDocument();

rerender(
<CreatorProfileHeader
{...BASE_PROPS}
connectedWalletAddress={CREATOR_ADDRESS}
/>
);

expect(screen.getByRole('button', { name: /edit bio/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /change avatar/i })).toBeInTheDocument();
});

it('hides edit controls when no wallet is connected', () => {
render(
<CreatorProfileHeader
{...BASE_PROPS}
connectedWalletAddress={null}
/>
);

expect(screen.queryByRole('button', { name: /edit bio/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /change avatar/i })).not.toBeInTheDocument();
});
});
47 changes: 47 additions & 0 deletions src/utils/__tests__/isOwnWallet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, it, expect } from 'vitest';
import { isOwnWallet } from '../isOwnWallet';

describe('isOwnWallet', () => {
it('returns true when addresses match exactly', () => {
expect(isOwnWallet('0xABC', '0xABC')).toBe(true);
});

it('returns true when addresses match case-insensitively', () => {
expect(isOwnWallet('0xABC', '0xabc')).toBe(true);
expect(isOwnWallet('0xabc', '0xABC')).toBe(true);
});

it('returns false when addresses differ', () => {
expect(isOwnWallet('0xABC', '0xDEF')).toBe(false);
});

it('returns false when connected address is null', () => {
expect(isOwnWallet(null, '0xABC')).toBe(false);
});

it('returns false when creator address is null', () => {
expect(isOwnWallet('0xABC', null)).toBe(false);
});

it('returns false when both addresses are null', () => {
expect(isOwnWallet(null, null)).toBe(false);
});

it('returns false when connected address is undefined', () => {
expect(isOwnWallet(undefined, '0xABC')).toBe(false);
});

it('returns false when creator address is undefined', () => {
expect(isOwnWallet('0xABC', undefined)).toBe(false);
});

it('returns false when both addresses are undefined', () => {
expect(isOwnWallet(undefined, undefined)).toBe(false);
});

it('returns false for empty strings', () => {
expect(isOwnWallet('', '0xABC')).toBe(false);
expect(isOwnWallet('0xABC', '')).toBe(false);
expect(isOwnWallet('', '')).toBe(false);
});
});
15 changes: 15 additions & 0 deletions src/utils/isOwnWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Returns true when the connected wallet address matches the creator's
* address. Both arguments are compared case-insensitively so that
* mixed-case Stellar / EVM addresses are handled correctly.
*
* Returns false when either address is missing — callers should never
* see an "own wallet" state unless both sides are present.
*/
export function isOwnWallet(
connectedAddress: string | null | undefined,
creatorAddress: string | null | undefined
): boolean {
if (!connectedAddress || !creatorAddress) return false;
return connectedAddress.toLowerCase() === creatorAddress.toLowerCase();
}
Loading