#
{tokenLabel}
diff --git a/src/features/transaction-notification/TransactionNotificationBanner.tsx b/src/features/transaction-notification/TransactionNotificationBanner.tsx
index b359894b0..5fd70143a 100644
--- a/src/features/transaction-notification/TransactionNotificationBanner.tsx
+++ b/src/features/transaction-notification/TransactionNotificationBanner.tsx
@@ -30,6 +30,11 @@ function getSubmittedMeta(payload: TxPayload): { title: string; subtitle: string
return { title: 'Publishing post', subtitle: 'Sign in your wallet to continue' };
case TxPayloadType.CreateComment:
return { title: 'Publishing reply', subtitle: 'Sign in your wallet to continue' };
+ case TxPayloadType.ClaimChainName:
+ return {
+ title: 'Confirm in your wallet',
+ subtitle: `Sign the claim request for ${payload.name}.chain`,
+ };
case TxPayloadType.SwapToken:
return {
title: 'Confirm in your wallet',
@@ -68,6 +73,39 @@ function getPendingMeta(payload: TxPayload): { title: string; subtitle: string }
return { title: 'Publishing post', subtitle: 'Confirming on blockchain…' };
case TxPayloadType.CreateComment:
return { title: 'Publishing reply', subtitle: 'Confirming on blockchain…' };
+ case TxPayloadType.ClaimChainName:
+ switch (payload.step) {
+ case 'preclaim':
+ return {
+ title: `Claiming ${payload.name}.chain`,
+ subtitle: 'Step 1 of 4 - reserving the name on-chain',
+ };
+ case 'claim':
+ return {
+ title: `Claiming ${payload.name}.chain`,
+ subtitle: 'Step 2 of 4 - submitting the sponsored claim',
+ };
+ case 'update':
+ return {
+ title: `Claiming ${payload.name}.chain`,
+ subtitle: 'Step 3 of 4 - updating the name pointers',
+ };
+ case 'transfer':
+ return {
+ title: `Claiming ${payload.name}.chain`,
+ subtitle: 'Step 4 of 4 - transferring the name to your wallet',
+ };
+ case 'queued':
+ return {
+ title: `Claiming ${payload.name}.chain`,
+ subtitle: 'Preparing the sponsored transactions. This can take a couple of minutes.',
+ };
+ default:
+ return {
+ title: `Claiming ${payload.name}.chain`,
+ subtitle: 'Processing in the background. You can continue using the app.',
+ };
+ }
case TxPayloadType.SwapToken:
return { title: 'Swap in progress', subtitle: 'Usually confirms in a few seconds' };
case TxPayloadType.WrapToken:
@@ -155,6 +193,11 @@ function getConfirmedMeta(payload: TxPayload): {
return { title: 'Post published', line: null };
case TxPayloadType.CreateComment:
return { title: 'Reply published', line: null };
+ case TxPayloadType.ClaimChainName:
+ return {
+ title: 'Name claimed',
+ line: { leftLabel: `${payload.name}.chain`, leftColor: '#4ade80' },
+ };
case TxPayloadType.AddLiquidity:
return {
title: 'Liquidity added',
diff --git a/src/features/transaction-notification/transaction-notification.context.tsx b/src/features/transaction-notification/transaction-notification.context.tsx
index 488abf5c6..dfca19082 100644
--- a/src/features/transaction-notification/transaction-notification.context.tsx
+++ b/src/features/transaction-notification/transaction-notification.context.tsx
@@ -12,6 +12,7 @@ export const TxPayloadType = {
CreateToken: 'create_token',
CreatePost: 'create_post',
CreateComment: 'create_comment',
+ ClaimChainName: 'claim_chain_name',
SwapToken: 'swap_token',
WrapToken: 'wrap_ae',
UnwrapToken: 'unwrap_wae',
@@ -26,6 +27,7 @@ export type TxPayload =
| { type: typeof TxPayloadType.CreateToken; tokenName: string }
| { type: typeof TxPayloadType.CreatePost; content: string }
| { type: typeof TxPayloadType.CreateComment; postId: string }
+ | { type: typeof TxPayloadType.ClaimChainName; name: string; step?: 'wallet' | 'queued' | 'preclaim' | 'claim' | 'update' | 'transfer' }
| { type: typeof TxPayloadType.SwapToken; tokenInSymbol: string; tokenOutSymbol: string; amountIn: string; amountOut: string }
| { type: typeof TxPayloadType.WrapToken; amount: string }
| { type: typeof TxPayloadType.UnwrapToken; amount: string }
@@ -44,6 +46,7 @@ export type NotificationState =
type TransactionNotificationContextValue = {
notificationState: NotificationState;
notifySubmitted: (payload: TxPayload) => void;
+ notifyPending: (payload: TxPayload) => void;
notifyPendingTx: (payload: TxPayload, txHash: string) => void;
notifyConfirmed: (payload: TxPayload) => void;
notifyError: (message: string) => void;
@@ -121,6 +124,12 @@ export const TransactionNotificationProvider: React.FC<{
setNotificationState({ status: 'submitted', payload });
}, []);
+ const notifyPending = useCallback((payload: TxPayload) => {
+ clearDismissTimer();
+ clearPollInterval();
+ setNotificationState({ status: 'pending', payload, txHash: '' });
+ }, []);
+
const notifyConfirmed = useCallback((payload: TxPayload) => {
clearDismissTimer();
clearPollInterval();
@@ -167,6 +176,7 @@ export const TransactionNotificationProvider: React.FC<{
const contextValue = useMemo(() => ({
notificationState,
notifySubmitted,
+ notifyPending,
notifyPendingTx,
notifyConfirmed,
notifyError,
@@ -174,6 +184,7 @@ export const TransactionNotificationProvider: React.FC<{
}), [
notificationState,
notifySubmitted,
+ notifyPending,
notifyPendingTx,
notifyConfirmed,
notifyError,
diff --git a/src/features/trending/views/TokenList.tsx b/src/features/trending/views/TokenList.tsx
index aa8c548a7..c4479c6d3 100644
--- a/src/features/trending/views/TokenList.tsx
+++ b/src/features/trending/views/TokenList.tsx
@@ -64,14 +64,6 @@ const TAB_LABELS: Record
= {
type OrderByOption = typeof SORT[keyof typeof SORT];
-const NO_GRADIENT_STYLE: React.CSSProperties = {
- color: 'var(--standard-font-color)',
- WebkitTextFillColor: 'var(--standard-font-color)',
- background: 'none',
- WebkitBackgroundClip: 'initial',
- backgroundClip: 'initial',
-};
-
const ORDER_BY_OPTIONS: SelectOptions = [
{ title: 'Market Cap', value: SORT.marketCap },
{ title: 'Trending', value: SORT.trendingScore },
@@ -114,8 +106,7 @@ const SearchSectionShell = ({
{title}
@@ -183,8 +174,7 @@ const UserResultsList = ({ items }: { items: Array
@@ -579,10 +569,9 @@ const TokenList = () => {
key={tab}
type="button"
onClick={() => setActiveTab(tab)}
- className={`no-gradient-text normal-case tracking-normal relative pb-3 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1161FE] focus-visible:ring-offset-2 focus-visible:ring-offset-transparent rounded-sm ${
+ className={`normal-case tracking-normal relative pb-3 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1161FE] focus-visible:ring-offset-2 focus-visible:ring-offset-transparent rounded-sm ${
isActive ? 'text-white' : 'text-white/55 hover:text-white/80'
}`}
- style={NO_GRADIENT_STYLE}
>
{TAB_LABELS[tab]}
{isActive ? (
@@ -698,8 +687,7 @@ const TokenList = () => {
Tokenize Trend
diff --git a/src/hooks/__tests__/useClaimChainName.test.ts b/src/hooks/__tests__/useClaimChainName.test.ts
new file mode 100644
index 000000000..02d044152
--- /dev/null
+++ b/src/hooks/__tests__/useClaimChainName.test.ts
@@ -0,0 +1,367 @@
+import { act, renderHook } from '@testing-library/react';
+import {
+ afterEach, beforeEach, describe, expect, it, vi,
+} from 'vitest';
+import { useClaimChainName } from '@/hooks/useClaimChainName';
+
+const mockCreateChainNameChallenge = vi.fn();
+const mockClaimChainName = vi.fn();
+const mockGetChainNameClaimStatus = vi.fn();
+const mockSignMessage = vi.fn();
+const mockAeSdkSignMessage = vi.fn();
+const mockSelectAccount = vi.fn();
+const mockGetName = vi.fn();
+const mockGetNameEntryByName = vi.fn();
+const mockResolveAccount = vi.fn();
+const mockConnectWallet = vi.fn();
+let mockWalletConnected = true;
+let mockWalletInfo: Record | undefined = { id: 'wallet' };
+let mockConnectingWallet = false;
+
+let mockActiveAccount = 'ak_test_active';
+const mockFetch = vi.fn();
+let mockAeSdkState: Record | undefined;
+
+vi.mock('@/api/backend', () => ({
+ SuperheroApi: {
+ createChainNameChallenge: (...args: any[]) => mockCreateChainNameChallenge(...args),
+ claimChainName: (...args: any[]) => mockClaimChainName(...args),
+ getChainNameClaimStatus: (...args: any[]) => mockGetChainNameClaimStatus(...args),
+ },
+}));
+
+vi.mock('@/hooks/useAeSdk', () => ({
+ useAeSdk: () => ({
+ activeAccount: mockActiveAccount,
+ sdk: {
+ getName: (...args: any[]) => mockGetName(...args),
+ api: {
+ getNameEntryByName: (...args: any[]) => mockGetNameEntryByName(...args),
+ },
+ },
+ staticAeSdk: null,
+ aeSdk: mockAeSdkState,
+ }),
+}));
+
+vi.mock('@/hooks/useWalletConnect', () => ({
+ useWalletConnect: () => ({
+ connectWallet: (...args: any[]) => mockConnectWallet(...args),
+ connectingWallet: mockConnectingWallet,
+ walletConnected: mockWalletConnected,
+ walletInfo: mockWalletInfo,
+ }),
+}));
+
+vi.mock('@/config', () => ({
+ CONFIG: {
+ NODE_URL: 'https://node.example',
+ MIDDLEWARE_URL: 'https://mdw.example',
+ },
+}));
+
+describe('useClaimChainName', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal('fetch', mockFetch);
+ mockActiveAccount = 'ak_test_active';
+ mockAeSdkState = {
+ signMessage: (...args: any[]) => mockAeSdkSignMessage(...args),
+ selectAccount: (...args: any[]) => mockSelectAccount(...args),
+ addresses: () => [mockActiveAccount],
+ _resolveAccount: (...args: any[]) => mockResolveAccount(...args),
+ };
+ mockSignMessage.mockResolvedValue(new Uint8Array([0xab, 0xcd]));
+ mockAeSdkSignMessage.mockResolvedValue(new Uint8Array([0xab, 0xcd]));
+ mockResolveAccount.mockReturnValue({
+ signMessage: (...args: any[]) => mockSignMessage(...args),
+ });
+ mockConnectWallet.mockResolvedValue(undefined);
+ mockWalletConnected = true;
+ mockWalletInfo = { id: 'wallet' };
+ mockConnectingWallet = false;
+ mockCreateChainNameChallenge.mockResolvedValue({
+ nonce: 'nonce-1',
+ expires_at: '123456',
+ message: 'profile_chain_name_claim:ak_test_active:nonce-1:123456',
+ });
+ mockGetName.mockRejectedValue(new Error('Name not found'));
+ mockGetNameEntryByName.mockRejectedValue(new Error('Name not found'));
+ mockClaimChainName.mockResolvedValue({ status: 'ok' });
+ mockFetch.mockImplementation(async (input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url.includes('/v3/transactions/th_transfer')) {
+ return {
+ ok: true,
+ json: async () => ({ block_height: 123 }),
+ };
+ }
+ if (url.includes('/v3/names/averylongchain.chain')) {
+ return {
+ ok: true,
+ json: async () => ({
+ ownership: { current: 'ak_test_active' },
+ pointers: { account_pubkey: 'ak_test_active' },
+ }),
+ };
+ }
+ return {
+ ok: false,
+ json: async () => ({}),
+ };
+ });
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ vi.unstubAllGlobals();
+ });
+
+ it('waits for transfer confirmation and final middleware ownership before completing', async () => {
+ const onStatusChange = vi.fn();
+ const onSubmitted = vi.fn();
+ mockGetChainNameClaimStatus
+ .mockResolvedValueOnce({
+ status: 'completed',
+ name: 'averylongchain.chain',
+ transfer_tx_hash: 'th_transfer',
+ expires_at: 999999,
+ })
+ .mockResolvedValueOnce({
+ status: 'completed',
+ name: 'averylongchain.chain',
+ transfer_tx_hash: 'th_transfer',
+ expires_at: 999999,
+ });
+ mockFetch
+ .mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ block_height: -1 }),
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ block_height: 123 }),
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ ownership: { current: 'ak_test_active' },
+ pointers: [{
+ key: 'account_pubkey',
+ id: 'ak_test_active',
+ }],
+ }),
+ });
+ const { result } = renderHook(() => useClaimChainName('ak_test_active'));
+
+ let response: any;
+ await act(async () => {
+ response = await result.current.claimSponsoredChainName({
+ name: 'averylongchain',
+ onSubmitted,
+ onStatusChange,
+ pollIntervalMs: 0,
+ });
+ });
+
+ expect(mockCreateChainNameChallenge).toHaveBeenCalledWith('ak_test_active');
+ expect(mockSelectAccount).toHaveBeenCalledWith('ak_test_active');
+ expect(mockAeSdkSignMessage).toHaveBeenCalledWith(
+ 'profile_chain_name_claim:ak_test_active:nonce-1:123456',
+ { onAccount: 'ak_test_active' },
+ );
+ expect(mockClaimChainName).toHaveBeenCalledWith({
+ address: 'ak_test_active',
+ name: 'averylongchain',
+ challenge_nonce: 'nonce-1',
+ challenge_expires_at: '123456',
+ signature_hex: 'abcd',
+ });
+ expect(onSubmitted).toHaveBeenCalledWith(expect.objectContaining({ status: 'ok' }));
+ expect(onStatusChange).toHaveBeenCalledWith(expect.objectContaining({ status: 'ok' }));
+ expect(onStatusChange).toHaveBeenCalledWith(expect.objectContaining({ status: 'queued' }));
+ expect(onStatusChange).toHaveBeenCalledWith(expect.objectContaining({ status: 'transfer_pending' }));
+ expect(onStatusChange).toHaveBeenCalledWith(expect.objectContaining({ status: 'completed' }));
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.stringContaining('/v3/transactions/th_transfer'),
+ expect.any(Object),
+ );
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.stringContaining('/v3/names/averylongchain.chain'),
+ expect.any(Object),
+ );
+ expect(response).toMatchObject({
+ status: 'completed',
+ name: 'averylongchain.chain',
+ expiresAt: 999999,
+ });
+ });
+
+ it('surfaces backend chain name claim failures', async () => {
+ mockFetch.mockReset();
+ mockGetChainNameClaimStatus.mockResolvedValueOnce({
+ status: 'failed',
+ error: 'Name is already taken',
+ });
+ const { result } = renderHook(() => useClaimChainName('ak_test_active'));
+
+ await act(async () => {
+ await expect(result.current.claimSponsoredChainName({
+ name: 'averylongchain',
+ pollIntervalMs: 0,
+ })).rejects.toThrow('Name is already taken');
+ });
+ });
+
+ it('checks whether a name is already present on chain', async () => {
+ mockGetName.mockResolvedValueOnce({ status: 'claimed' });
+ const { result } = renderHook(() => useClaimChainName('ak_test_active'));
+
+ await expect(result.current.checkNameAvailability('taken-name')).resolves.toBe(false);
+ await expect(result.current.checkNameAvailability('available-name')).resolves.toBe(true);
+ });
+
+ it('falls back to the node name lookup when sdk.getName is unavailable', async () => {
+ mockGetName.mockImplementation(() => {
+ throw new Error('getName unavailable');
+ });
+ mockGetNameEntryByName
+ .mockResolvedValueOnce({ id: 'nm_taken' })
+ .mockRejectedValueOnce(new Error('404 not found'));
+ const { result } = renderHook(() => useClaimChainName('ak_test_active'));
+
+ await expect(result.current.checkNameAvailability('taken-name')).resolves.toBe(false);
+ await expect(result.current.checkNameAvailability('available-name')).resolves.toBe(true);
+ });
+
+ it('rejects claims when the wallet signer account is unavailable', async () => {
+ mockAeSdkSignMessage.mockRejectedValueOnce(new Error('Wallet message signing is not available'));
+ mockResolveAccount.mockReturnValueOnce(null);
+ const { result } = renderHook(() => useClaimChainName('ak_test_active'));
+
+ await act(async () => {
+ await expect(result.current.claimSponsoredChainName({
+ name: 'averylongchain',
+ pollIntervalMs: 0,
+ })).rejects.toThrow('Wallet message signing is not available');
+ });
+ });
+
+ it('does not retry signing through the fallback signer after user rejection', async () => {
+ mockAeSdkSignMessage.mockRejectedValueOnce(new Error('Rejected by user'));
+ const { result } = renderHook(() => useClaimChainName('ak_test_active'));
+
+ await act(async () => {
+ await expect(result.current.claimSponsoredChainName({
+ name: 'averylongchain',
+ pollIntervalMs: 0,
+ })).rejects.toThrow('Rejected by user');
+ });
+
+ expect(mockResolveAccount).not.toHaveBeenCalled();
+ expect(mockSignMessage).not.toHaveBeenCalled();
+ });
+
+ it('falls back to an authorized wallet signer when direct sdk signing is unavailable', async () => {
+ mockAeSdkSignMessage.mockRejectedValueOnce(new Error('sdk sign failed'));
+ mockGetChainNameClaimStatus.mockResolvedValueOnce({
+ status: 'completed',
+ name: 'averylongchain.chain',
+ transfer_tx_hash: 'th_transfer',
+ expires_at: 999999,
+ });
+ const { result } = renderHook(() => useClaimChainName('ak_test_active'));
+
+ await act(async () => {
+ await expect(result.current.claimSponsoredChainName({
+ name: 'averylongchain',
+ pollIntervalMs: 0,
+ })).resolves.toMatchObject({
+ status: 'completed',
+ name: 'averylongchain.chain',
+ });
+ });
+
+ expect(mockResolveAccount).toHaveBeenCalledWith('ak_test_active');
+ expect(mockSignMessage).toHaveBeenCalledWith('profile_chain_name_claim:ak_test_active:nonce-1:123456');
+ });
+
+ it('keeps claiming enabled for the connected profile address', () => {
+ const { result } = renderHook(() => useClaimChainName('ak_test_active'));
+ expect(result.current.canClaim).toBe(true);
+ });
+
+ it('does not crash when aeSdk.address throws before wallet reconnect', () => {
+ mockActiveAccount = undefined as any;
+ mockAeSdkState = {
+ signMessage: (...args: any[]) => mockAeSdkSignMessage(...args),
+ selectAccount: (...args: any[]) => mockSelectAccount(...args),
+ addresses: () => [],
+ _resolveAccount: (...args: any[]) => mockResolveAccount(...args),
+ };
+ Object.defineProperty(mockAeSdkState, 'address', {
+ get() {
+ throw new Error('You are not connected to Wallet');
+ },
+ });
+
+ expect(() => renderHook(() => useClaimChainName('ak_test_active'))).not.toThrow();
+ });
+
+ it('reconnects the extension before signing when wallet session is stale', async () => {
+ mockWalletConnected = false;
+ mockConnectWallet.mockImplementation(async () => {
+ mockWalletConnected = true;
+ });
+ mockGetChainNameClaimStatus.mockResolvedValueOnce({
+ status: 'completed',
+ name: 'averylongchain.chain',
+ transfer_tx_hash: 'th_transfer',
+ expires_at: 999999,
+ });
+ const { result } = renderHook(() => useClaimChainName('ak_test_active'));
+
+ await act(async () => {
+ await expect(result.current.claimSponsoredChainName({
+ name: 'averylongchain',
+ pollIntervalMs: 0,
+ })).resolves.toMatchObject({
+ status: 'completed',
+ name: 'averylongchain.chain',
+ });
+ });
+
+ expect(mockConnectWallet).toHaveBeenCalled();
+ });
+
+ it('accepts final middleware ownership even when pointers are omitted', async () => {
+ mockGetChainNameClaimStatus.mockResolvedValueOnce({
+ status: 'completed',
+ name: 'averylongchain.chain',
+ transfer_tx_hash: 'th_transfer',
+ expires_at: 999999,
+ });
+ mockFetch
+ .mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ block_height: 123 }),
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ ownership: { current: 'ak_test_active' },
+ }),
+ });
+ const { result } = renderHook(() => useClaimChainName('ak_test_active'));
+
+ await act(async () => {
+ await expect(result.current.claimSponsoredChainName({
+ name: 'averylongchain',
+ pollIntervalMs: 0,
+ })).resolves.toMatchObject({
+ status: 'completed',
+ name: 'averylongchain.chain',
+ });
+ });
+ });
+});
diff --git a/src/hooks/useClaimChainName.ts b/src/hooks/useClaimChainName.ts
new file mode 100644
index 000000000..535c85f92
--- /dev/null
+++ b/src/hooks/useClaimChainName.ts
@@ -0,0 +1,534 @@
+import {
+ useCallback,
+ useEffect,
+ useRef,
+} from 'react';
+import {
+ type ChainNameClaimStatusResponse,
+ SuperheroApi,
+} from '@/api/backend';
+import { decode } from '@aeternity/aepp-sdk';
+import { CONFIG } from '@/config';
+import { useAeSdk } from './useAeSdk';
+import { useWalletConnect } from './useWalletConnect';
+
+const normalizeName = (value: string) => value.trim().toLowerCase();
+const normalizeAddress = (value?: string | null) => (value || '').trim().toLowerCase();
+const readSdkString = (sdkRecord: Record, key: string) => {
+ try {
+ const value = sdkRecord[key];
+ return typeof value === 'string' ? value : '';
+ } catch {
+ return '';
+ }
+};
+const getSdkAddresses = (candidate: any): string[] => {
+ // eslint-disable-next-line no-underscore-dangle, dot-notation
+ const current = candidate?.['_accounts']?.current;
+ if (current && typeof current === 'object') return Object.keys(current);
+ if (typeof candidate?.addresses === 'function') return candidate.addresses();
+ return [];
+};
+const sdkHasAccount = (candidate: any, expectedAddress?: string): boolean => {
+ const addresses = getSdkAddresses(candidate);
+ if (!addresses.length) return false;
+ if (!expectedAddress) return true;
+ const target = normalizeAddress(expectedAddress);
+ return addresses.some((address) => normalizeAddress(address) === target);
+};
+const isNameNotFoundError = (error: unknown) => {
+ const message = error instanceof Error ? error.message : String(error || '');
+ return /404|not found|name not found|Name revoked/i.test(message);
+};
+const isUserRejectedSigningError = (error: unknown) => {
+ const message = error instanceof Error ? error.message : String(error || '');
+ const lower = message.toLowerCase();
+ const code = (error as any)?.code;
+ return Boolean(
+ code === 'ACTION_REJECTED'
+ || code === 4001
+ || lower.includes('rejected by user')
+ || lower.includes('user rejected')
+ || lower.includes('user denied')
+ || lower.includes('denied by user')
+ || lower.includes('cancelled by user')
+ || lower.includes('canceled by user')
+ || lower.includes('operation cancelled')
+ || lower.includes('operation canceled'),
+ );
+};
+const getSdkAddress = (sdk: unknown) => {
+ if (sdk && typeof sdk === 'object') {
+ const sdkRecord = sdk as Record;
+ const directAddress = readSdkString(sdkRecord, 'address');
+ const selectedAddress = readSdkString(sdkRecord, 'selectedAddress');
+ if (typeof directAddress === 'string' && directAddress) return directAddress;
+
+ if (typeof selectedAddress === 'string' && selectedAddress) return selectedAddress;
+
+ // eslint-disable-next-line no-underscore-dangle, dot-notation
+ const accounts = sdkRecord['_accounts'];
+ const currentAccounts = accounts && typeof accounts === 'object'
+ ? (accounts as Record).current
+ : null;
+ if (currentAccounts && typeof currentAccounts === 'object') {
+ const firstAddress = Object.keys(currentAccounts as Record)[0];
+ if (firstAddress) return firstAddress;
+ }
+ }
+ return '';
+};
+const wait = (ms: number) => new Promise((resolve) => {
+ window.setTimeout(resolve, ms);
+});
+const stripTrailingSlash = (value: string) => value.replace(/\/$/, '');
+
+const bytesToHex = (value: Uint8Array): string => Array.from(value)
+ .map((byte) => byte.toString(16).padStart(2, '0'))
+ .join('');
+
+const normalizeSignatureHex = (signature: unknown): string => {
+ if (typeof signature === 'string') {
+ if (signature.startsWith('sg_')) return bytesToHex(decode(signature));
+ const clean = signature.startsWith('0x') ? signature.slice(2) : signature;
+ if (/^[0-9a-f]+$/iu.test(clean) && clean.length % 2 === 0) return clean.toLowerCase();
+ }
+ if (signature instanceof Uint8Array) return bytesToHex(signature);
+ if (signature instanceof ArrayBuffer) return bytesToHex(new Uint8Array(signature));
+ if (ArrayBuffer.isView(signature)) {
+ return bytesToHex(
+ new Uint8Array(signature.buffer, signature.byteOffset, signature.byteLength),
+ );
+ }
+ if (Array.isArray(signature)) return bytesToHex(Uint8Array.from(signature));
+ if (signature && typeof signature === 'object') {
+ const nested = (signature as Record).signature
+ ?? (signature as Record).raw
+ ?? (signature as Record).value;
+ if (nested != null) return normalizeSignatureHex(nested);
+ }
+ throw new Error('Wallet did not return a valid signature');
+};
+
+const extractChainNameExpiry = (status?: ChainNameClaimStatusResponse | null): number | null => {
+ if (!status) return null;
+ return [
+ status.expires_at,
+ status.approximate_expire_time,
+ status.approximateExpireTime,
+ status.expire_time,
+ status.expireTime,
+ ].reduce((resolved, value) => {
+ if (resolved) return resolved;
+ const numeric = Number(value);
+ if (Number.isFinite(numeric) && numeric > 0) return Math.floor(numeric);
+ if (typeof value === 'string') {
+ const parsed = Date.parse(value);
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
+ }
+ return null;
+ }, null);
+};
+
+const readJson = async (url: string) => {
+ try {
+ const response = await fetch(url, { cache: 'no-cache' });
+ if (!response.ok) return null;
+ return await response.json();
+ } catch {
+ return null;
+ }
+};
+
+const getApiBases = () => Array.from(
+ new Set(
+ [
+ CONFIG.NODE_URL,
+ CONFIG.MIDDLEWARE_URL,
+ ].filter(Boolean).map((value) => stripTrailingSlash(String(value))),
+ ),
+);
+
+const fetchTransactionRecord = async (txHash: string) => {
+ const bases = getApiBases();
+ const encodedHash = encodeURIComponent(txHash);
+ const tryRead = async (index: number): Promise => {
+ if (index >= bases.length) return null;
+ const data = await readJson(`${bases[index]}/v3/transactions/${encodedHash}?int-as-string=false`);
+ if (data) return data;
+ return tryRead(index + 1);
+ };
+ return tryRead(0);
+};
+
+const isTransactionMined = async (txHash?: string | null) => {
+ if (!txHash) return false;
+ const record = await fetchTransactionRecord(txHash);
+ const blockHeight = Number(record?.block_height ?? record?.blockHeight ?? -1);
+ return Number.isFinite(blockHeight) && blockHeight > 0;
+};
+
+const fetchNameRecord = async (name: string) => {
+ const bases = getApiBases();
+ const encodedName = encodeURIComponent(name);
+ const tryRead = async (index: number): Promise => {
+ if (index >= bases.length) return null;
+ const data = await readJson(`${bases[index]}/v3/names/${encodedName}`);
+ if (data) return data;
+ return tryRead(index + 1);
+ };
+ return tryRead(0);
+};
+
+const getAuthorizedWalletSigner = (
+ walletSdk: unknown,
+ targetAddress: string,
+) => {
+ if (!walletSdk || typeof walletSdk !== 'object') return null;
+ const walletRecord = walletSdk as Record;
+ // eslint-disable-next-line no-underscore-dangle, dot-notation
+ const resolver = walletRecord['_resolveAccount'];
+ if (typeof resolver === 'function') {
+ try {
+ return resolver.call(walletSdk, targetAddress) as {
+ signMessage?: (message: string) => Promise;
+ };
+ } catch {
+ return null;
+ }
+ }
+ return null;
+};
+
+const signMessageWithSdk = async (
+ signerSdk: unknown,
+ address: string,
+ message: string,
+) => {
+ if (!signerSdk || typeof signerSdk !== 'object') {
+ throw new Error('Wallet message signing is not available');
+ }
+ if (typeof (signerSdk as any).selectAccount === 'function') {
+ try {
+ (signerSdk as any).selectAccount(address);
+ } catch {
+ // Continue; some sdk variants may not support explicit selection.
+ }
+ }
+ if (typeof (signerSdk as any).signMessage === 'function') {
+ try {
+ return await (signerSdk as any).signMessage(message, { onAccount: address });
+ } catch (error) {
+ if (isUserRejectedSigningError(error)) throw error;
+ // Fall through to account-level signer resolution below.
+ }
+ }
+ const walletSigner = getAuthorizedWalletSigner(signerSdk, address);
+ if (walletSigner && typeof walletSigner.signMessage === 'function') {
+ return walletSigner.signMessage(message);
+ }
+ throw new Error('Wallet message signing is not available');
+};
+
+const extractAccountPointer = (pointers: unknown) => {
+ if (Array.isArray(pointers)) {
+ const accountPointer = pointers.find((pointer) => (
+ normalizeName(String((pointer as Record)?.key || '')) === 'account_pubkey'
+ ));
+ return normalizeAddress(
+ (accountPointer as Record | undefined)?.id as string | undefined,
+ );
+ }
+ if (pointers && typeof pointers === 'object') {
+ return normalizeAddress(
+ (pointers as Record).account_pubkey as string | undefined
+ ?? (pointers as Record).accountPubkey as string | undefined,
+ );
+ }
+ return '';
+};
+
+const isNameClaimFinalized = async (name: string, address: string) => {
+ const record = await fetchNameRecord(name);
+ const ownerAddress = normalizeAddress(
+ record?.ownership?.current ?? record?.owner_id ?? record?.owner ?? record?.ownerId,
+ );
+ const targetAddress = normalizeAddress(address);
+ if (ownerAddress !== targetAddress) return false;
+
+ const accountPointer = extractAccountPointer(record?.pointers);
+ return !accountPointer || accountPointer === targetAddress;
+};
+
+const buildVerifiedStatus = (
+ name: string,
+ baseStatus: ChainNameClaimStatusResponse,
+ overrides: Partial = {},
+): ChainNameClaimStatusResponse => ({
+ ...baseStatus,
+ name,
+ ...overrides,
+});
+export function useClaimChainName(targetAddress?: string) {
+ const {
+ activeAccount,
+ aeSdk,
+ sdk,
+ staticAeSdk,
+ } = useAeSdk();
+ const {
+ connectWallet,
+ connectingWallet,
+ walletConnected,
+ walletInfo,
+ } = useWalletConnect();
+ const activeAccountRef = useRef(activeAccount);
+ const walletConnectedRef = useRef(walletConnected);
+ const walletInfoRef = useRef(walletInfo);
+ const connectingWalletRef = useRef(connectingWallet);
+
+ useEffect(() => {
+ activeAccountRef.current = activeAccount;
+ }, [activeAccount]);
+
+ useEffect(() => {
+ walletConnectedRef.current = walletConnected;
+ }, [walletConnected]);
+
+ useEffect(() => {
+ walletInfoRef.current = walletInfo;
+ }, [walletInfo]);
+
+ useEffect(() => {
+ connectingWalletRef.current = connectingWallet;
+ }, [connectingWallet]);
+
+ const connectedAddress = activeAccount || getSdkAddress(aeSdk) || getSdkAddress(sdk);
+
+ const waitForWalletReconnect = useCallback(async (
+ expectedAddress?: string,
+ timeoutMs = 20_000,
+ ): Promise => {
+ const knownAddress = expectedAddress || targetAddress;
+ const normalizedKnownAddress = knownAddress ? normalizeAddress(knownAddress) : '';
+ const matchesExpectedAddress = (account?: string) => {
+ if (!account) return false;
+ if (!knownAddress) return true;
+ return normalizeAddress(account) === normalizedKnownAddress;
+ };
+ const hasKnownSignerReady = () => (
+ Boolean(knownAddress)
+ && walletConnectedRef.current
+ && sdkHasAccount(aeSdk, knownAddress)
+ );
+ const getReconnectAddress = (): string | null => {
+ const { current } = activeAccountRef;
+ if (knownAddress) {
+ if (hasKnownSignerReady()) return knownAddress as string;
+ if (walletConnectedRef.current && matchesExpectedAddress(current)) return current as string;
+ } else if (walletConnectedRef.current && matchesExpectedAddress(current)) {
+ return current as string;
+ }
+ return null;
+ };
+
+ const immediate = getReconnectAddress();
+ if (immediate) return immediate;
+
+ if (!walletConnectedRef.current && !walletInfoRef.current) {
+ throw new Error('You are not connected to Wallet');
+ }
+
+ if (!walletConnectedRef.current && walletInfoRef.current && !connectingWalletRef.current) {
+ try {
+ await connectWallet();
+ } catch {
+ // Continue waiting below in case wallet state is still propagating.
+ }
+ }
+
+ return new Promise((resolve, reject) => {
+ const startedAt = Date.now();
+ let reconnectAttempted = walletConnectedRef.current
+ || !walletInfoRef.current
+ || connectingWalletRef.current;
+ const interval = window.setInterval(() => {
+ const resolvedAddress = getReconnectAddress();
+ if (resolvedAddress) {
+ window.clearInterval(interval);
+ resolve(resolvedAddress);
+ return;
+ }
+ if (!reconnectAttempted && Date.now() - startedAt > 3_000) {
+ reconnectAttempted = true;
+ Promise.resolve(connectWallet() as any).catch(() => {
+ // Keep waiting until timeout.
+ });
+ }
+ if (Date.now() - startedAt >= timeoutMs) {
+ window.clearInterval(interval);
+ reject(new Error('You are not connected to Wallet'));
+ }
+ }, 300);
+ });
+ }, [aeSdk, connectWallet, targetAddress]);
+
+ const checkNameAvailability = useCallback(async (name: string) => {
+ const normalizedName = normalizeName(name).replace(/\.chain$/u, '');
+ const fullName = `${normalizedName}.chain` as `${string}.chain`;
+ const readSdk = staticAeSdk || sdk;
+ const lookups = [
+ async () => {
+ if (typeof (readSdk as any)?.getName !== 'function') return null;
+ return (readSdk as any).getName(fullName);
+ },
+ async () => {
+ if (typeof (readSdk as any)?.api?.getNameEntryByName !== 'function') return null;
+ return (readSdk as any).api.getNameEntryByName(fullName);
+ },
+ async () => fetchNameRecord(fullName),
+ ];
+ const runLookup = async (index: number): Promise => {
+ if (index >= lookups.length) return true;
+ try {
+ const result = await lookups[index]();
+ if (result == null) return runLookup(index + 1);
+ return false;
+ } catch (error) {
+ if (isNameNotFoundError(error)) return true;
+ if (index === lookups.length - 1) {
+ throw new Error('Unable to verify chain name availability right now');
+ }
+ return runLookup(index + 1);
+ }
+ };
+
+ return runLookup(0);
+ }, [sdk, staticAeSdk]);
+
+ const claimSponsoredChainName = useCallback(async (params: {
+ name: string;
+ onSubmitted?: (status: ChainNameClaimStatusResponse) => void;
+ onStatusChange?: (status: ChainNameClaimStatusResponse) => void;
+ pollIntervalMs?: number;
+ maxAttempts?: number;
+ }) => {
+ let reconnectedAddress: string | undefined;
+ try {
+ reconnectedAddress = await waitForWalletReconnect(targetAddress || connectedAddress);
+ } catch {
+ // Keep original error path below if extension reconnect fails.
+ }
+ const target = targetAddress || reconnectedAddress || connectedAddress;
+ if (!target) throw new Error('Connect your wallet to claim a .chain name');
+ const signerAddress = reconnectedAddress || connectedAddress;
+ if (!signerAddress || normalizeAddress(signerAddress) !== normalizeAddress(target)) {
+ throw new Error('Connect the wallet for this profile to claim a .chain name');
+ }
+
+ const normalizedName = normalizeName(params.name).replace(/\.chain$/u, '');
+ const challenge = await SuperheroApi.createChainNameChallenge(target);
+ const signature = await signMessageWithSdk(aeSdk, target, challenge.message).catch((error) => {
+ throw error instanceof Error
+ ? error
+ : new Error('Wallet message signing is not available');
+ });
+ const signatureHex = normalizeSignatureHex(signature);
+
+ const claimResponse = await SuperheroApi.claimChainName({
+ address: target,
+ name: normalizedName,
+ challenge_nonce: challenge.nonce,
+ challenge_expires_at: String(challenge.expires_at),
+ signature_hex: signatureHex,
+ });
+
+ const initialStatus: ChainNameClaimStatusResponse = {
+ status: claimResponse.status || 'pending',
+ name: `${normalizedName}.chain`,
+ };
+ params.onSubmitted?.(initialStatus);
+ params.onStatusChange?.(initialStatus);
+
+ const fullName = `${normalizedName}.chain`;
+ const pollIntervalMs = params.pollIntervalMs ?? 5_000;
+ const maxAttempts = params.maxAttempts ?? 120;
+ const verifyClaimProgress = async (
+ status: ChainNameClaimStatusResponse,
+ ): Promise => {
+ if (String(status.status || '').toLowerCase() === 'failed') return buildVerifiedStatus(fullName, status);
+
+ const hasPreclaim = Boolean(status.preclaim_tx_hash);
+ const hasClaim = Boolean(status.claim_tx_hash);
+ const hasUpdate = Boolean(status.update_tx_hash);
+ const hasTransfer = Boolean(status.transfer_tx_hash);
+
+ if (hasPreclaim && !(await isTransactionMined(status.preclaim_tx_hash))) {
+ return buildVerifiedStatus(fullName, status, { status: 'preclaim_pending' });
+ }
+ if (hasClaim && !(await isTransactionMined(status.claim_tx_hash))) {
+ return buildVerifiedStatus(fullName, status, { status: 'claim_pending' });
+ }
+ if (hasUpdate && !(await isTransactionMined(status.update_tx_hash))) {
+ return buildVerifiedStatus(fullName, status, { status: 'update_pending' });
+ }
+ if (hasTransfer && !(await isTransactionMined(status.transfer_tx_hash))) {
+ return buildVerifiedStatus(fullName, status, { status: 'transfer_pending' });
+ }
+
+ const statusValue = String(status.status || '').toLowerCase();
+ if (hasTransfer || statusValue === 'completed') {
+ const finalized = await isNameClaimFinalized(fullName, target);
+ if (!finalized) {
+ return buildVerifiedStatus(fullName, status, { status: 'transfer_pending' });
+ }
+ return buildVerifiedStatus(fullName, status, { status: 'completed' });
+ }
+
+ return buildVerifiedStatus(fullName, status, { status: 'queued' });
+ };
+
+ const pollStatus = async (
+ attempt: number,
+ latestStatus: ChainNameClaimStatusResponse,
+ ): Promise => {
+ const verifiedStatus = await verifyClaimProgress(latestStatus);
+ params.onStatusChange?.(verifiedStatus);
+ const latestStatusName = String(verifiedStatus.status || '').toLowerCase();
+ if (['completed', 'failed'].includes(latestStatusName)) return verifiedStatus;
+ if (attempt >= maxAttempts) return verifiedStatus;
+
+ await wait(pollIntervalMs);
+ const nextStatus = await SuperheroApi.getChainNameClaimStatus(target);
+ if (!nextStatus.name) nextStatus.name = fullName;
+ return pollStatus(attempt + 1, nextStatus);
+ };
+
+ const latestStatus = await pollStatus(0, initialStatus);
+ const finalStatusName = String(latestStatus.status || '').toLowerCase();
+ if (!['completed', 'failed'].includes(finalStatusName)) {
+ throw new Error('Timed out while waiting for .chain name claim to finish');
+ }
+ if (finalStatusName === 'failed') {
+ throw new Error(latestStatus.error || 'Failed to claim .chain name');
+ }
+
+ return {
+ ...latestStatus,
+ name: latestStatus.name || `${normalizedName}.chain`,
+ expiresAt: extractChainNameExpiry(latestStatus),
+ };
+ }, [aeSdk, connectedAddress, targetAddress, waitForWalletReconnect]);
+
+ return {
+ canClaim: Boolean(
+ connectedAddress
+ && (
+ !targetAddress
+ || normalizeAddress(targetAddress) === normalizeAddress(connectedAddress)
+ ),
+ ),
+ checkNameAvailability,
+ claimSponsoredChainName,
+ };
+}
diff --git a/src/hooks/useProfile.ts b/src/hooks/useProfile.ts
index 71b7d71a7..24eeab5c1 100644
--- a/src/hooks/useProfile.ts
+++ b/src/hooks/useProfile.ts
@@ -70,7 +70,11 @@ type SetProfileInput = {
type ProfileRegistryContractApi = ContractMethodsBase & {
_calldata: {
- encode: (contractName: string, functionName: string, args: unknown[]) => Encoded.ContractBytearray;
+ encode: (
+ contractName: string,
+ functionName: string,
+ args: unknown[],
+ ) => Encoded.ContractBytearray;
};
};
diff --git a/src/locales/en.json b/src/locales/en.json
index 5aa091df2..11cba151e 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -24,7 +24,9 @@
"saveProfile": "Save profile",
"sendFeedback": "Send Feedback",
"retry": "Retry",
- "connectX": "Connect X"
+ "connectX": "Connect X",
+ "getChainName": "+ Get .chain name",
+ "claimChainName": "Claim"
},
"labels": {
"numberOfInvites": "Number of invites",
@@ -50,7 +52,8 @@
"none": "None",
"xAccessToken": "X access token (optional)",
"connectX": "Link X account",
- "xAccount": "X account"
+ "xAccount": "X account",
+ "claimChainName": "Claim a new .chain name"
},
"actions": {
"copy": "Copy",
@@ -115,7 +118,8 @@
"username": "your_username",
"selectChainName": "Select one of your chain names",
"solName": "Your Solana name",
- "xAccessToken": "Paste your X OAuth access token"
+ "xAccessToken": "Paste your X OAuth access token",
+ "claimChainName": "myuniquename123"
},
"messages": {
"notEnoughBalance": "Not enough balance. You need {{amount}} AE.",
@@ -142,6 +146,27 @@
"failedToRefreshProfile": "Profile transaction succeeded but refresh failed. Please reload the page.",
"tooManyRequests": "Too many requests. Please wait and try again.",
"noChainNamesFound": "No owned chain names found for this account.",
+ "connectWalletToClaimChainName": "Connect your wallet to claim a .chain name",
+ "chainNameClaimRequired": "Enter the name you want to claim.",
+ "chainNameClaimHint": "Use letters, numbers, or hyphens. More than 12 characters name is for free.",
+ "chainNameClaimInvalidChars": "Chain names can use letters, numbers, and hyphens, and can't start or end with a hyphen.",
+ "chainNameClaimChecking": "Checking…",
+ "chainNameClaimTooShort": "Sponsored claims require more than 12 characters before .chain.",
+ "chainNameClaimLoading": "Claiming…",
+ "chainNameClaimStarted": "Claim submitted. Waiting for backend sponsorship and transfer…",
+ "chainNameClaimPending": "Claim status: {{status}}",
+ "chainNameClaimCompleted": ".chain name claimed successfully.",
+ "chainNameClaimWalletUnavailable": "Wallet message signing is not available right now. Please try again.",
+ "chainNameClaimNameTaken": "That .chain name is already taken. Try another one.",
+ "chainNameClaimNameInProgress": "That .chain name is currently being claimed. Try another one.",
+ "chainNameClaimAddressInProgress": "You already have a sponsored .chain claim in progress.",
+ "chainNameClaimAddressClaimed": "This wallet already has a sponsored .chain name.",
+ "chainNameClaimChallengeExpired": "Your claim session expired. Please try again.",
+ "chainNameClaimVerificationFailed": "We could not verify your wallet signature. Please try again.",
+ "chainNameClaimUnavailable": "Sponsored .chain claiming is temporarily unavailable. Please try again later.",
+ "chainNameClaimRetry": "We could not start the .chain claim. Please try again.",
+ "chainNameClaimFailed": "Failed to claim .chain name.",
+ "chainNameClaimTimedOut": "The claim is still processing. Please check back in a moment.",
"selectChainNameForDisplaySource": "Choose a chain name before using Chain as display source.",
"oops": "Oops",
"loading": "Loading…",
diff --git a/src/styles/base.scss b/src/styles/base.scss
index ac94bef5a..0c7a75e34 100644
--- a/src/styles/base.scss
+++ b/src/styles/base.scss
@@ -245,56 +245,17 @@ html.safari.safari-zooming [style*='backdrop-filter'] {
-webkit-backdrop-filter: none !important;
}
-a {
- color: $custom_links_color;
+a {
+ color: inherit;
text-decoration: none;
- background: $accent_gradient;
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- transition: filter 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- position: relative;
-}
-
-a:hover {
- filter: brightness(1.1);
- transform: none;
- text-shadow: none;
-}
-
-a::after {
- content: '';
- position: absolute;
- width: 0;
- height: 2px;
- bottom: -2px;
- left: 0;
- background: $accent_gradient;
- transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- border-radius: 1px;
+ transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
-a:hover::after {
- width: 100%;
-}
-
-/* Utility: disable gradient text for specific links */
-a.no-gradient-text {
- background: none !important;
- -webkit-background-clip: initial !important;
- -webkit-text-fill-color: currentColor !important;
- background-clip: initial !important;
- color: inherit !important;
-}
-a.no-gradient-text:hover {
- color: inherit !important;
- filter: none;
+a:hover {
+ color: inherit;
transform: none;
text-shadow: none;
}
-a.no-gradient-text::after {
- display: none;
-}
/* Reduce gap only between consecutive token activity items on desktop, not between activity and posts */
@media (min-width: 768px) {
@@ -303,31 +264,12 @@ a.no-gradient-text::after {
}
}
-/* Specific override for white-with-purple CTA on banner */
-.banner-explore-btn {
- background: #ffffff !important;
- color: #6d28d9 !important; /* violet-700 */
- -webkit-text-fill-color: #6d28d9 !important;
- -webkit-background-clip: initial !important;
- background-clip: initial !important;
- border: 1px solid rgba(255,255,255,0.25) !important;
-}
-
/* Banner CTA shared hover: gentle nudge up, subtle scale, no underline bar */
.banner-cta { position: relative; transform: translateY(0); transition: transform 180ms ease, box-shadow 180ms ease, background-color 180ms ease, color 180ms ease; }
.banner-cta::after { display: none !important; }
.banner-cta:hover { transform: translateY(-2px) scale(1.015); box-shadow: 0 10px 24px rgba(0,0,0,0.25); }
.banner-cta:active { transform: translateY(0) scale(0.995); }
-/* Force 'Learn more' to white text, disable gradient on anchor while keeping bg-color */
-.banner-learn-btn {
- color: rgba(255,255,255,0.8) !important;
- -webkit-text-fill-color: rgba(255,255,255,0.8) !important;
- background-image: none !important;
- -webkit-background-clip: initial !important;
- background-clip: initial !important;
-}
-
img, video { max-width: 100%; height: auto; display: block; }
/* Prevent content overflow */
@@ -509,32 +451,26 @@ textarea:disabled {
}
/* Gen Z Ultra Modern Typography */
- h1 {
- font-size: 2rem;
+ h1 {
+ font-size: 2rem;
font-weight: 800;
- // background: $primary_gradient;
- // -webkit-background-clip: text;
- // -webkit-text-fill-color: transparent;
- background-clip: text;
+ color: var(--standard-font-color);
letter-spacing: -1px;
line-height: 1.1;
}
- h2 {
- font-size: 1.75rem;
+ h2 {
+ font-size: 1.75rem;
font-weight: 700;
- background: $primary_gradient;
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
+ color: var(--standard-font-color);
letter-spacing: -0.5px;
}
- h3 {
- font-size: 1.5rem;
+ h3 {
+ font-size: 1.5rem;
font-weight: 600;
- color: var(--neon-teal);
+ color: var(--standard-font-color);
}
- h4 {
- font-size: 1.25rem;
+ h4 {
+ font-size: 1.25rem;
font-weight: 600;
color: var(--neon-blue);
}
@@ -542,8 +478,8 @@ textarea:disabled {
font-size: 1.125rem;
font-weight: 500;
}
- h6 {
- font-size: 1rem;
+ h6 {
+ font-size: 1rem;
font-weight: 500;
color: var(--neon-yellow);
}
diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css
index 5dd3d155c..9892da65e 100644
--- a/src/styles/tailwind.css
+++ b/src/styles/tailwind.css
@@ -83,14 +83,6 @@
@apply font-bold text-white;
}
- .sh-dex-title {
- @apply font-bold;
- color: #ffffff;
- -webkit-text-fill-color: #ffffff;
- background: none;
- -webkit-background-clip: initial;
- background-clip: initial;
- }
}
@keyframes truncate-scroll {
diff --git a/src/utils/linkify.tsx b/src/utils/linkify.tsx
index 6dfb13692..cb165c3c4 100644
--- a/src/utils/linkify.tsx
+++ b/src/utils/linkify.tsx
@@ -80,12 +80,6 @@ export function linkify(
href={`/users/${name}`}
key={`aens-${name}-${offset}`}
className="text-[var(--neon-teal)] underline-offset-2 hover:underline break-words"
- style={{
- WebkitTextFillColor: 'currentColor',
- WebkitBackgroundClip: 'initial',
- backgroundClip: 'initial',
- background: 'none',
- }}
>
{match}
,
@@ -118,12 +112,6 @@ export function linkify(
href={`/users/${address}`}
key={`acc-${address}-${idx}-${off}`}
className="text-[var(--neon-teal)] underline-offset-2 hover:underline break-words"
- style={{
- WebkitTextFillColor: 'currentColor',
- WebkitBackgroundClip: 'initial',
- backgroundClip: 'initial',
- background: 'none',
- }}
>
{display}
,
@@ -165,10 +153,6 @@ export function linkify(
lineHeight: 'inherit',
margin: 0,
padding: 0,
- WebkitTextFillColor: 'currentColor',
- WebkitBackgroundClip: 'initial',
- backgroundClip: 'initial',
- background: 'none',
verticalAlign: 'baseline',
}}
title={m}
@@ -209,12 +193,6 @@ export function linkify(
to={target}
key={`hashtag-${tag}-${idx}-${off}`}
className="text-[var(--neon-teal)] underline-offset-2 hover:underline break-words"
- style={{
- WebkitTextFillColor: 'currentColor',
- WebkitBackgroundClip: 'initial',
- backgroundClip: 'initial',
- background: 'none',
- }}
onClick={(e) => e.stopPropagation()}
>
{m}
diff --git a/src/views/UserProfile.tsx b/src/views/UserProfile.tsx
index 476292291..39c36d4c0 100644
--- a/src/views/UserProfile.tsx
+++ b/src/views/UserProfile.tsx
@@ -38,6 +38,7 @@ import { useAddressByChainName, useChainName } from '../hooks/useChainName';
import { SuperheroApi } from '../api/backend';
import AccountPortfolio from '@/components/Account/AccountPortfolio';
+import ClaimChainNameModal from '@/components/modals/ClaimChainNameModal';
import ProfileEditModal from '../components/modals/ProfileEditModal';
import { CONFIG } from '../config';
import { useModal } from '../hooks';
@@ -97,6 +98,7 @@ export default function UserProfile({
});
const profileDisplayName = (profileInfo?.public_name || chainName || '').trim();
const hasProfileDisplayName = Boolean(profileDisplayName);
+ const showClaimChainNameCta = Boolean(canEdit && !chainName);
const profileHeading = profileDisplayName || formatAddress(effectiveAddress, 6, true);
const { data: onChainProfile, refetch: refetchOnChainProfile } = useQuery({
@@ -108,6 +110,7 @@ export default function UserProfile({
const [profile, setProfile] = useState(null);
const [editOpen, setEditOpen] = useState(false);
+ const [claimChainNameOpen, setClaimChainNameOpen] = useState(false);
const [editInitialSection, setEditInitialSection] = useState<'profile' | 'x'>('profile');
// Get tab from URL search params, default to "feed"
@@ -379,17 +382,34 @@ export default function UserProfile({
/>
-
- {profileHeading}
-
+
+
+ {profileHeading}
+
+ {showClaimChainNameCta && (
+
+ )}
+
{effectiveAddress}
@@ -403,19 +423,6 @@ export default function UserProfile({
{/* Action buttons */}
- {(canEdit && false) ? (
-
{
- setEditInitialSection('profile');
- setEditOpen(true);
- }}
- >
- {t('buttons.editProfile')}
-
- ) : null}
{!canEdit ? (
openModal({ name: 'tip', props: { toAddress: effectiveAddress } })}
@@ -445,19 +452,6 @@ export default function UserProfile({
- {(canEdit && !isXVerified && false) && (
-
- )}
-
{/* Portfolio Chart and Stats - Side by side on md+ */}
{/* Portfolio Chart - Smaller on md+ */}
@@ -600,6 +594,11 @@ export default function UserProfile({
initialBio={bioText}
initialSection={editInitialSection}
/>
+ setClaimChainNameOpen(false)}
+ address={effectiveAddress}
+ />
) : (
<>
@@ -630,6 +629,11 @@ export default function UserProfile({
initialBio={bioText}
initialSection={editInitialSection}
/>
+ setClaimChainNameOpen(false)}
+ address={effectiveAddress}
+ />
>
);
}