From 9ac8349e2f61ddfc603294fbfb67dd82d63447a5 Mon Sep 17 00:00:00 2001 From: Risto Laanoja Date: Wed, 10 Dec 2025 12:01:25 +0200 Subject: [PATCH 1/4] markdown: escaped formatting chars, link with tooltip, no table break in link --- src/utils/markdown.tsx | 87 +++++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 17 deletions(-) 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; From e2dcb42941383355cbfe91c6147354c666d0e983 Mon Sep 17 00:00:00 2001 From: igmahl Date: Wed, 10 Dec 2025 16:02:58 +0200 Subject: [PATCH 2/4] Add nametag availability check and input validation - Add isNametagAvailable method to NametagService - Implement checkNametagAvailability in useWallet hook - Validate nametag availability before minting in CreateWalletFlow - Add input validation to allow only Latin letters, numbers, and special chars (_, -, +, .) - Show error message if nametag already exists --- src/components/wallet/L3/hooks/useWallet.ts | 5 +++++ .../wallet/L3/onboarding/CreateWalletFlow.tsx | 20 +++++++++++++++++-- .../wallet/L3/services/NametagService.ts | 8 ++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) 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..3ea1894a9 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(''); @@ -230,6 +230,16 @@ export function CreateWalletFlow() { try { const cleanTag = nametagInput.trim().replace('@', ''); + + const isNametagAvailable = await checkNametagAvailability(cleanTag); + console.log(isNametagAvailable) + if(!isNametagAvailable) { + console.log("Setting error") + setError(`${cleanTag} already exists.`); + setStep('nametag') + return; + } + 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 +1113,13 @@ export function CreateWalletFlow() { setNametagInput(e.target.value)} + onChange={(e) => { + // Allow only Latin letters, numbers, hyphen, underscore, plus, dot + const value = e.target.value; + 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..2a040d38d 100644 --- a/src/components/wallet/L3/services/NametagService.ts +++ b/src/components/wallet/L3/services/NametagService.ts @@ -40,6 +40,14 @@ export class NametagService { return NametagService.instance; } + async isNametagAvailable(nametag: string): Promise { + const client = ServiceProvider.stateTransitionClient; + const rootTrustBase = ServiceProvider.getRootTrustBase(); + const nametagTokenId = await TokenId.fromNameTag(nametag); + + return await !client.isMinted(rootTrustBase, nametagTokenId); + } + async mintNametagAndPublish(nametag: string): Promise { try { const cleanTag = nametag.replace("@unicity", "").replace("@", "").trim(); From b95c759b6f724654ceeb0b11566f05aae34f073d Mon Sep 17 00:00:00 2001 From: igmahl Date: Wed, 10 Dec 2025 23:57:18 +0200 Subject: [PATCH 3/4] improve nametag availability check and fix UX flow --- src/components/wallet/L3/onboarding/CreateWalletFlow.tsx | 5 +---- src/components/wallet/L3/services/NametagService.ts | 9 +++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx b/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx index 3ea1894a9..dcb635554 100644 --- a/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx +++ b/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx @@ -226,20 +226,17 @@ export function CreateWalletFlow() { setIsBusy(true); setError(null); - setStep('processing'); try { const cleanTag = nametagInput.trim().replace('@', ''); const isNametagAvailable = await checkNametagAvailability(cleanTag); - console.log(isNametagAvailable) if(!isNametagAvailable) { - console.log("Setting error") setError(`${cleanTag} already exists.`); - setStep('nametag') 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 diff --git a/src/components/wallet/L3/services/NametagService.ts b/src/components/wallet/L3/services/NametagService.ts index 2a040d38d..247a3ef4e 100644 --- a/src/components/wallet/L3/services/NametagService.ts +++ b/src/components/wallet/L3/services/NametagService.ts @@ -41,11 +41,12 @@ export class NametagService { } async isNametagAvailable(nametag: string): Promise { - const client = ServiceProvider.stateTransitionClient; - const rootTrustBase = ServiceProvider.getRootTrustBase(); const nametagTokenId = await TokenId.fromNameTag(nametag); - - return await !client.isMinted(rootTrustBase, nametagTokenId); + const isAlreadyMinted = await ServiceProvider.stateTransitionClient.isMinted( + ServiceProvider.getRootTrustBase(), + nametagTokenId + ); + return !isAlreadyMinted; } async mintNametagAndPublish(nametag: string): Promise { From 02c5dd4917a790c484229589187ce8dfc368784a Mon Sep 17 00:00:00 2001 From: igmahl Date: Tue, 16 Dec 2025 15:42:29 +0200 Subject: [PATCH 4/4] - Automatically convert uppercase letters to lowercase in nametag input field --- src/components/wallet/L3/onboarding/CreateWalletFlow.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx b/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx index 3ea1894a9..82d897ddf 100644 --- a/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx +++ b/src/components/wallet/L3/onboarding/CreateWalletFlow.tsx @@ -1114,8 +1114,7 @@ export function CreateWalletFlow() { type="text" value={nametagInput} onChange={(e) => { - // Allow only Latin letters, numbers, hyphen, underscore, plus, dot - const value = e.target.value; + const value = e.target.value.toLowerCase(); if (/^[a-z0-9_\-+.]*$/.test(value)) { setNametagInput(value); }