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
5 changes: 5 additions & 0 deletions src/components/wallet/L3/hooks/useWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export const useWallet = () => {
enabled: !!identityQuery.data?.address,
});

const checkNametagAvailability = async(nametag: string): Promise<boolean> => {
Comment thread
MastaP marked this conversation as resolved.
return await nametagService.isNametagAvailable(nametag);
}

// Ensure registry is loaded before aggregating assets
const registryQuery = useQuery({
queryKey: KEYS.REGISTRY,
Expand Down Expand Up @@ -581,5 +585,6 @@ export const useWallet = () => {
getSeedPhrase,
getL1Address,
getUnifiedKeyManager,
checkNametagAvailability,
};
};
18 changes: 15 additions & 3 deletions src/components/wallet/L3/onboarding/CreateWalletFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand Down Expand Up @@ -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) {
Comment thread
MastaP marked this conversation as resolved.
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
Expand Down Expand Up @@ -1103,7 +1110,12 @@ export function CreateWalletFlow() {
<input
type="text"
value={nametagInput}
onChange={(e) => setNametagInput(e.target.value)}
onChange={(e) => {
const value = e.target.value.toLowerCase();
if (/^[a-z0-9_\-+.]*$/.test(value)) {
Comment thread
MastaP marked this conversation as resolved.
setNametagInput(value);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" && nametagInput && !isBusy) handleMintNametag();
}}
Expand Down
9 changes: 9 additions & 0 deletions src/components/wallet/L3/services/NametagService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ export class NametagService {
return NametagService.instance;
}

async isNametagAvailable(nametag: string): Promise<boolean> {
const nametagTokenId = await TokenId.fromNameTag(nametag);
const isAlreadyMinted = await ServiceProvider.stateTransitionClient.isMinted(
ServiceProvider.getRootTrustBase(),
nametagTokenId
);
return !isAlreadyMinted;
}

async mintNametagAndPublish(nametag: string): Promise<MintResult> {
try {
const cleanTag = nametag.replace("@unicity", "").replace("@", "").trim();
Expand Down
87 changes: 70 additions & 17 deletions src/utils/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
/(?<!\\)((?:\\\\)*)\\\((.+?)\\\)/g,
(match, backslashes, latex) => {
if (backslashes && backslashes.length % 2 === 1) {
Expand All @@ -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*])?)\*|_([^_]+?)_|`([^`]+?)`|<br\s*\/?>|<b>(.+?)<\/b>|<strong>(.+?)<\/strong>|<i>(.+?)<\/i>|<em>(.+?)<\/em>|<code>(.+?)<\/code>|<a\s+href=["']([^"']+)["']>(.+?)<\/a>|\[([^\]]+)\]\(([^)]+)\)|!\[([^\]]*)\]\(([^)]+)\)|(https?:\/\/[^\s<>[\]()]+[^\s<>[\]().,;:!?'"]))/gi;
// THIRD PASS: Process markdown - math placeholders won't be captured by markdown patterns
const regex = /(\*\*(.+?)\*\*|\*([^\s*](?:[^*]*[^\s*])?)\*|_([^_]+?)_|`([^`]+?)`|<br\s*\/?>|<b>(.+?)<\/b>|<strong>(.+?)<\/strong>|<i>(.+?)<\/i>|<em>(.+?)<\/em>|<code>(.+?)<\/code>|<a\s+href=["']([^"']+)["']>(.+?)<\/a>|\[([^\]]+)\]\(([^\s)]+)(?:\s+"([^"]+)")?\)|!\[([^\]]*)\]\(([^)]+)\)|(https?:\/\/[^\s<>[\]()]+[^\s<>[\]().,;:!?'"]))/gi;
let lastIndex = 0;
let match;

Expand Down Expand Up @@ -242,17 +246,25 @@ function parseInline(text: string, keyPrefix: string): React.ReactNode[] {
</a>
);
} 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(
<a key={`${keyPrefix}-link-${key++}`} href={match[14]} target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 underline">
<a
key={`${keyPrefix}-link-${key++}`}
href={match[14]}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 underline"
title={tooltip || undefined}
>
{content}
</a>
);
} 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(
<img
key={`${keyPrefix}-img-${key++}`}
Expand All @@ -262,9 +274,9 @@ function parseInline(text: string, keyPrefix: string): React.ReactNode[] {
loading="lazy"
/>
);
} else if (match[17]) {
} else if (match[18]) {
// Plain URL (https://... or http://...)
const url = match[17];
const url = match[18];
parts.push(
<a
key={`${keyPrefix}-url-${key++}`}
Expand All @@ -290,16 +302,57 @@ function parseInline(text: string, keyPrefix: string): React.ReactNode[] {
return parts.length > 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;

Expand Down
Loading