Skip to content

Commit 3ffcdf9

Browse files
authored
Merge pull request #535 from Stanley-Owoh/add-integration-test-for-isownwallet-edit-controls-533
feat: add integration tests for isOwnWallet and implement isOwnWallet…
2 parents a48914d + 5ee57e3 commit 3ffcdf9

4 files changed

Lines changed: 193 additions & 8 deletions

File tree

src/components/common/CreatorProfileHeader.tsx

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useEffect } from 'react';
22
import { motion } from 'framer-motion';
3-
import { Copy, Check, Share2 } from 'lucide-react';
3+
import { Copy, Check, Share2, Pencil } from 'lucide-react';
44
import showToast from '@/utils/toast.util';
55
import appendUtmParams from '@/utils/utm.utils';
66
import { copyTextToClipboard } from '@/utils/clipboard.utils';
@@ -12,6 +12,7 @@ import CreatorBio from '@/components/common/CreatorBio';
1212
import { formatCreatorHandle } from '@/utils/handleDisplay.utils';
1313
import { normalizeCreatorDisplayName } from '@/utils/creatorDisplayName.utils';
1414
import { CREATOR_CARD_MEDIA_RADIUS_CLASS } from '@/utils/creatorCardTokens';
15+
import { isOwnWallet } from '@/utils/isOwnWallet';
1516

1617
interface CreatorProfileHeaderProps {
1718
name: string;
@@ -21,6 +22,7 @@ interface CreatorProfileHeaderProps {
2122
isVerified?: boolean;
2223
bio?: string | null;
2324
className?: string;
25+
connectedWalletAddress?: string | null;
2426
}
2527

2628
const CREATOR_PROFILE_SUBTITLE_WRAP_CLASS_NAME =
@@ -34,6 +36,7 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
3436
isVerified,
3537
bio,
3638
className,
39+
connectedWalletAddress,
3740
}) => {
3841
const [copied, setCopied] = useState(false);
3942
const [isScrolled, setIsScrolled] = useState(false);
@@ -50,6 +53,10 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
5053
// URL construction the caller might do via the prop.
5154
const displayHandle = formatCreatorHandle(handle);
5255
const displayName = normalizeCreatorDisplayName(name) || 'Unnamed creator';
56+
const normalizedCreatorId =
57+
creatorId == null ? creatorId : String(creatorId);
58+
59+
const own = isOwnWallet(connectedWalletAddress, normalizedCreatorId);
5360

5461
const handleShare = async () => {
5562
let url = window.location.href;
@@ -163,13 +170,41 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
163170
</div>
164171
</div>
165172

166-
<div
167-
className={cn(
168-
'flex items-center gap-3 transition-transform duration-300',
169-
isScrolled ? 'scale-90' : 'scale-100'
170-
)}
171-
>
172-
<Button
173+
<div
174+
className={cn(
175+
'flex items-center gap-3 transition-transform duration-300',
176+
isScrolled ? 'scale-90' : 'scale-100'
177+
)}
178+
>
179+
{own && (
180+
<>
181+
<Button
182+
aria-label="Edit bio"
183+
variant="outline"
184+
className={cn(
185+
'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',
186+
isScrolled ? 'h-9 px-3 text-xs' : 'h-11 px-4 text-sm'
187+
)}
188+
>
189+
<Pencil className="mr-2 size-4 text-amber-500" />
190+
<span className="hidden sm:inline">Edit Bio</span>
191+
<span className="sm:hidden">Edit</span>
192+
</Button>
193+
<Button
194+
aria-label="Change avatar"
195+
variant="outline"
196+
className={cn(
197+
'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',
198+
isScrolled ? 'h-9 px-3 text-xs' : 'h-11 px-4 text-sm'
199+
)}
200+
>
201+
<Pencil className="mr-2 size-4 text-amber-500" />
202+
<span className="hidden sm:inline">Change Avatar</span>
203+
<span className="sm:hidden">Avatar</span>
204+
</Button>
205+
</>
206+
)}
207+
<Button
173208
onClick={handleShare}
174209
variant="outline"
175210
className={cn(
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
4+
import CreatorProfileHeader from '@/components/common/CreatorProfileHeader';
5+
6+
const CREATOR_ADDRESS = '0xCreator1111111111111111111111111111111111';
7+
const OTHER_ADDRESS = '0xOther22222222222222222222222222222222222';
8+
9+
const BASE_PROPS = {
10+
name: 'Alex Rivers',
11+
handle: 'arivers',
12+
creatorId: CREATOR_ADDRESS,
13+
avatarUrl: 'https://example.com/avatar.png',
14+
};
15+
16+
describe('CreatorProfileHeader – isOwnWallet edit-controls visibility', () => {
17+
it('hides edit controls when the connected wallet does not match the creator', () => {
18+
const { container } = render(
19+
<CreatorProfileHeader
20+
{...BASE_PROPS}
21+
connectedWalletAddress={OTHER_ADDRESS}
22+
/>
23+
);
24+
25+
expect(screen.queryByRole('button', { name: /edit bio/i })).not.toBeInTheDocument();
26+
expect(screen.queryByRole('button', { name: /change avatar/i })).not.toBeInTheDocument();
27+
28+
// Confirm the controls are truly absent from the DOM, not just hidden
29+
expect(container.querySelector('[aria-label="Edit bio"]')).toBeNull();
30+
expect(container.querySelector('[aria-label="Change avatar"]')).toBeNull();
31+
});
32+
33+
it('shows edit controls when the connected wallet matches the creator', () => {
34+
render(
35+
<CreatorProfileHeader
36+
{...BASE_PROPS}
37+
connectedWalletAddress={CREATOR_ADDRESS}
38+
/>
39+
);
40+
41+
expect(screen.getByRole('button', { name: /edit bio/i })).toBeInTheDocument();
42+
expect(screen.getByRole('button', { name: /change avatar/i })).toBeInTheDocument();
43+
});
44+
45+
it('matches case-insensitively so mixed-case addresses are treated as the same wallet', () => {
46+
render(
47+
<CreatorProfileHeader
48+
{...BASE_PROPS}
49+
connectedWalletAddress={CREATOR_ADDRESS.toLowerCase()}
50+
/>
51+
);
52+
53+
expect(screen.getByRole('button', { name: /edit bio/i })).toBeInTheDocument();
54+
});
55+
56+
it('toggles edit controls when the connected wallet changes', () => {
57+
const { rerender } = render(
58+
<CreatorProfileHeader
59+
{...BASE_PROPS}
60+
connectedWalletAddress={OTHER_ADDRESS}
61+
/>
62+
);
63+
64+
expect(screen.queryByRole('button', { name: /edit bio/i })).not.toBeInTheDocument();
65+
66+
rerender(
67+
<CreatorProfileHeader
68+
{...BASE_PROPS}
69+
connectedWalletAddress={CREATOR_ADDRESS}
70+
/>
71+
);
72+
73+
expect(screen.getByRole('button', { name: /edit bio/i })).toBeInTheDocument();
74+
expect(screen.getByRole('button', { name: /change avatar/i })).toBeInTheDocument();
75+
});
76+
77+
it('hides edit controls when no wallet is connected', () => {
78+
render(
79+
<CreatorProfileHeader
80+
{...BASE_PROPS}
81+
connectedWalletAddress={null}
82+
/>
83+
);
84+
85+
expect(screen.queryByRole('button', { name: /edit bio/i })).not.toBeInTheDocument();
86+
expect(screen.queryByRole('button', { name: /change avatar/i })).not.toBeInTheDocument();
87+
});
88+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { isOwnWallet } from '../isOwnWallet';
3+
4+
describe('isOwnWallet', () => {
5+
it('returns true when addresses match exactly', () => {
6+
expect(isOwnWallet('0xABC', '0xABC')).toBe(true);
7+
});
8+
9+
it('returns true when addresses match case-insensitively', () => {
10+
expect(isOwnWallet('0xABC', '0xabc')).toBe(true);
11+
expect(isOwnWallet('0xabc', '0xABC')).toBe(true);
12+
});
13+
14+
it('returns false when addresses differ', () => {
15+
expect(isOwnWallet('0xABC', '0xDEF')).toBe(false);
16+
});
17+
18+
it('returns false when connected address is null', () => {
19+
expect(isOwnWallet(null, '0xABC')).toBe(false);
20+
});
21+
22+
it('returns false when creator address is null', () => {
23+
expect(isOwnWallet('0xABC', null)).toBe(false);
24+
});
25+
26+
it('returns false when both addresses are null', () => {
27+
expect(isOwnWallet(null, null)).toBe(false);
28+
});
29+
30+
it('returns false when connected address is undefined', () => {
31+
expect(isOwnWallet(undefined, '0xABC')).toBe(false);
32+
});
33+
34+
it('returns false when creator address is undefined', () => {
35+
expect(isOwnWallet('0xABC', undefined)).toBe(false);
36+
});
37+
38+
it('returns false when both addresses are undefined', () => {
39+
expect(isOwnWallet(undefined, undefined)).toBe(false);
40+
});
41+
42+
it('returns false for empty strings', () => {
43+
expect(isOwnWallet('', '0xABC')).toBe(false);
44+
expect(isOwnWallet('0xABC', '')).toBe(false);
45+
expect(isOwnWallet('', '')).toBe(false);
46+
});
47+
});

src/utils/isOwnWallet.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Returns true when the connected wallet address matches the creator's
3+
* address. Both arguments are compared case-insensitively so that
4+
* mixed-case Stellar / EVM addresses are handled correctly.
5+
*
6+
* Returns false when either address is missing — callers should never
7+
* see an "own wallet" state unless both sides are present.
8+
*/
9+
export function isOwnWallet(
10+
connectedAddress: string | null | undefined,
11+
creatorAddress: string | null | undefined
12+
): boolean {
13+
if (!connectedAddress || !creatorAddress) return false;
14+
return connectedAddress.toLowerCase() === creatorAddress.toLowerCase();
15+
}

0 commit comments

Comments
 (0)