diff --git a/src/components/wallet/L3/hooks/useWallet.ts b/src/components/wallet/L3/hooks/useWallet.ts index a1b811c79..0c378bbee 100644 --- a/src/components/wallet/L3/hooks/useWallet.ts +++ b/src/components/wallet/L3/hooks/useWallet.ts @@ -85,6 +85,10 @@ export const useWallet = () => { enabled: !!identityQuery.data?.address, }); + const checkNametagAvailability = async(nametag: string): Promise => { + return await nametagService.isNametagAvailable(nametag); + } + // Ensure registry is loaded before aggregating assets const registryQuery = useQuery({ queryKey: KEYS.REGISTRY, @@ -581,5 +585,6 @@ export const useWallet = () => { getSeedPhrase, getL1Address, getUnifiedKeyManager, + checkNametagAvailability, }; }; diff --git a/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx b/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx index e2ce5673a..804273944 100644 --- a/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx +++ b/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx @@ -32,7 +32,7 @@ const SESSION_KEY = "user-pin-1234"; const identityManager = IdentityManager.getInstance(SESSION_KEY); export function CreateWalletFlow() { - const { identity, createWallet, restoreWallet, mintNametag, nametag, getUnifiedKeyManager } = useWallet(); + const { identity, createWallet, restoreWallet, mintNametag, nametag, getUnifiedKeyManager, checkNametagAvailability } = useWallet(); const [step, setStep] = useState<'start' | 'restoreMethod' | 'restore' | 'importFile' | 'addressSelection' | 'nametag' | 'processing'>('start'); const [nametagInput, setNametagInput] = useState(''); @@ -226,10 +226,17 @@ export function CreateWalletFlow() { setIsBusy(true); setError(null); - setStep('processing'); try { const cleanTag = nametagInput.trim().replace('@', ''); + + const isNametagAvailable = await checkNametagAvailability(cleanTag); + if(!isNametagAvailable) { + setError(`${cleanTag} already exists.`); + return; + } + + setStep('processing'); await mintNametag(cleanTag); // Successfully minted nametag - reload to reinitialize with new nametag // This ensures React Query refreshes and the app transitions to main wallet view @@ -1103,7 +1110,12 @@ export function CreateWalletFlow() { setNametagInput(e.target.value)} + onChange={(e) => { + const value = e.target.value.toLowerCase(); + if (/^[a-z0-9_\-+.]*$/.test(value)) { + setNametagInput(value); + } + }} onKeyDown={(e) => { if (e.key === "Enter" && nametagInput && !isBusy) handleMintNametag(); }} diff --git a/src/components/wallet/L3/services/NametagService.ts b/src/components/wallet/L3/services/NametagService.ts index 6443462d0..247a3ef4e 100644 --- a/src/components/wallet/L3/services/NametagService.ts +++ b/src/components/wallet/L3/services/NametagService.ts @@ -40,6 +40,15 @@ export class NametagService { return NametagService.instance; } + async isNametagAvailable(nametag: string): Promise { + const nametagTokenId = await TokenId.fromNameTag(nametag); + const isAlreadyMinted = await ServiceProvider.stateTransitionClient.isMinted( + ServiceProvider.getRootTrustBase(), + nametagTokenId + ); + return !isAlreadyMinted; + } + async mintNametagAndPublish(nametag: string): Promise { try { const cleanTag = nametag.replace("@unicity", "").replace("@", "").trim(); diff --git a/src/utils/markdown.tsx b/src/utils/markdown.tsx index e0041e08d..d30a571ea 100644 --- a/src/utils/markdown.tsx +++ b/src/utils/markdown.tsx @@ -155,11 +155,15 @@ function replaceMathPlaceholders( // Parse inline markdown and HTML (bold, italic, code, br, links, images, plain URLs) function parseInline(text: string, keyPrefix: string): React.ReactNode[] { - // FIRST PASS: Extract inline math and replace with safe tokens that won't be matched by markdown regex + // FIRST PASS: Handle escape sequences (e.g., \* should become just *) + // Process common escaped markdown characters + const unescapedText = text.replace(/\\([*_`[\]()#+-.|!\\])/g, '$1'); + + // SECOND PASS: Extract inline math and replace with safe tokens that won't be matched by markdown regex const mathBlocks: string[] = []; const mathPlaceholder = '\u0000MATH'; // Unique placeholder that markdown won't match - const processedText = text.replace( + const processedText = unescapedText.replace( /(? { if (backslashes && backslashes.length % 2 === 1) { @@ -175,8 +179,8 @@ function parseInline(text: string, keyPrefix: string): React.ReactNode[] { const parts: React.ReactNode[] = []; let key = 0; - // SECOND PASS: Process markdown - math placeholders won't be captured by markdown patterns - const regex = /(\*\*(.+?)\*\*|\*([^\s*](?:[^*]*[^\s*])?)\*|_([^_]+?)_|`([^`]+?)`||(.+?)<\/b>|(.+?)<\/strong>|(.+?)<\/i>|(.+?)<\/em>|(.+?)<\/code>|(.+?)<\/a>|\[([^\]]+)\]\(([^)]+)\)|!\[([^\]]*)\]\(([^)]+)\)|(https?:\/\/[^\s<>[\]()]+[^\s<>[\]().,;:!?'"]))/gi; + // THIRD PASS: Process markdown - math placeholders won't be captured by markdown patterns + const regex = /(\*\*(.+?)\*\*|\*([^\s*](?:[^*]*[^\s*])?)\*|_([^_]+?)_|`([^`]+?)`||(.+?)<\/b>|(.+?)<\/strong>|(.+?)<\/i>|(.+?)<\/em>|(.+?)<\/code>|(.+?)<\/a>|\[([^\]]+)\]\(([^\s)]+)(?:\s+"([^"]+)")?\)|!\[([^\]]*)\]\(([^)]+)\)|(https?:\/\/[^\s<>[\]()]+[^\s<>[\]().,;:!?'"]))/gi; let lastIndex = 0; let match; @@ -242,17 +246,25 @@ function parseInline(text: string, keyPrefix: string): React.ReactNode[] { ); } else if (match[13] && match[14]) { - // [text](url) markdown link + // [text](url) or [text](url "tooltip") markdown link const content = replaceMathPlaceholders(match[13], mathBlocks, `${keyPrefix}-link`, key); + const tooltip = match[15]; // Optional tooltip parts.push( - + {content} ); - } else if (match[16]) { + } else if (match[17]) { // ![alt](url) markdown image (supports base64 data URLs) - const alt = match[15] || 'image'; - const src = match[16]; + const alt = match[16] || 'image'; + const src = match[17]; parts.push( ); - } else if (match[17]) { + } else if (match[18]) { // Plain URL (https://... or http://...) - const url = match[17]; + const url = match[18]; parts.push( 0 ? parts : [text]; } +// Helper function to split table row by | while respecting quoted strings (for tooltips) +function splitTableRow(line: string): string[] { + const cells: string[] = []; + let currentCell = ''; + let inQuotes = false; + let escaped = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (escaped) { + currentCell += char; + escaped = false; + continue; + } + + if (char === '\\') { + escaped = true; + currentCell += char; + continue; + } + + if (char === '"') { + inQuotes = !inQuotes; + currentCell += char; + continue; + } + + if (char === '|' && !inQuotes) { + cells.push(currentCell); + currentCell = ''; + continue; + } + + currentCell += char; + } + + // Push the last cell + if (currentCell || line.endsWith('|')) { + cells.push(currentCell); + } + + // Remove empty first/last from split (table format is |cell1|cell2|) + return cells.slice(1, -1).map(cell => cell.trim()); +} + // Parse markdown table function parseTable(lines: string[], keyPrefix: string): React.ReactNode { const rows = lines .filter(line => !line.match(/^\|[\s-:|]+\|$/)) // Skip separator rows - .map(line => - line - .split('|') - .slice(1, -1) // Remove empty first/last from split - .map(cell => cell.trim()) - ); + .map(line => splitTableRow(line)); if (rows.length === 0) return null;