diff --git a/e2e/specs/stateless/profileEditor.spec.ts b/e2e/specs/stateless/profileEditor.spec.ts index f5d1d70f0..b84999320 100644 --- a/e2e/specs/stateless/profileEditor.spec.ts +++ b/e2e/specs/stateless/profileEditor.spec.ts @@ -725,7 +725,7 @@ test.describe('unwrapped', () => { await profilePage.profileEditorInput('eth').fill('') await expect(await profilePage.profileEditorInput('eth')).toHaveAttribute( 'placeholder', - '0xD9hbQK...', + '0xb8c2C2...', ) await profilePage.profileEditorClearButton('eth').click() await profilePage.profileEditorInput('btc').fill('') @@ -854,7 +854,7 @@ test.describe('wrapped', () => { await profilePage.profileEditorInput('eth').fill('') await expect(await profilePage.profileEditorInput('eth')).toHaveAttribute( 'placeholder', - '0xD9hbQK...', + '0xb8c2C2...', ) await profilePage.profileEditorClearButton('eth').click() await profilePage.profileEditorInput('btc').fill('') diff --git a/public/locales/en/register.json b/public/locales/en/register.json index db62dc151..25df468fd 100644 --- a/public/locales/en/register.json +++ b/public/locales/en/register.json @@ -58,18 +58,20 @@ "social": { "label": "Social", "items": { - "com.twitter": "Twitter", - "com.github": "GitHub", + "email": "Email", "com.discord": "Discord", - "org.telegram": "Telegram", - "email": "Email" + "com.github": "GitHub", + "com.twitter": "Twitter", + "eth.farcaster": "Farcaster", + "org.telegram": "Telegram" }, "placeholder": { - "com.twitter": "e.g. ensdomains", - "com.github": "e.g. ensdomains", + "email": "e.g. hello@example.com", "com.discord": "e.g. nickjohnson", - "org.telegram": "e.g. nicksdjohnson", - "email": "e.g. hello@example.com" + "com.github": "e.g. ensdomains", + "com.twitter": "e.g. ensdomains", + "eth.farcaster": "e.g. ensdomains", + "org.telegram": "e.g. nicksdjohnson" } }, "address": { @@ -77,7 +79,7 @@ "itemLabel": "{{coin}} address", "placeholder": { "default": "Add address here", - "eth": "0xD9hbQK...", + "eth": "0xb8c2C2...", "bnb": "bnb1grpf...", "btc": "3FZbgi29...", "ltc": "ltc1qdp7p...", diff --git a/public/locales/en/transactionFlow.json b/public/locales/en/transactionFlow.json index c7795973b..7a11c71c9 100644 --- a/public/locales/en/transactionFlow.json +++ b/public/locales/en/transactionFlow.json @@ -54,11 +54,12 @@ "noOptions": "No account options available", "placeholder": { "default": "Add username here", - "com.twitter": "e.g. nicksdjohnson", - "com.github": "e.g. arachnid", + "email": "e.g. hello@example.com", "com.discord": "e.g. nickjohnson", - "org.telegram": "e.g. nicksdjohnson", - "email": "e.g. hello@example.com" + "com.github": "e.g. arachnid", + "com.twitter": "e.g. nicksdjohnson", + "eth.farcaster": "e.g. nick.eth", + "org.telegram": "e.g. nicksdjohnson" } }, "address": { @@ -67,10 +68,10 @@ "noOptions": "No address options available", "placeholder": { "default": "Add address here", - "eth": "0xD9hbQK...", - "bnb": "0xD9hbQK...", + "eth": "0xb8c2C2...", + "bnb": "bnb1grpf...", "btc": "3FZbgi29...", - "ltc": "3FZbgi29...", + "ltc": "ltc1qdp7p...", "sol": "D4kA7VzHnmV...", "dot": "1D4kA7VxH...", "doge": "DFabcd12..." @@ -219,7 +220,8 @@ "title_one": "You do not own this name", "title_other": "You do not own all these names", "description_one": "Extending this name will extend the current owner's registration length. This will not give you ownership of it.", - "description_other": "Extending these names will extend the current owner's registration length. This will not give you ownership if you are not already the owner" }, + "description_other": "Extending these names will extend the current owner's registration length. This will not give you ownership if you are not already the owner" + }, "invoice": { "extension": "{{count}} year extension", "transaction": "Transaction fee", diff --git a/public/locales/nl/transactionFlow.json b/public/locales/nl/transactionFlow.json index aae480c3f..b14891ebf 100644 --- a/public/locales/nl/transactionFlow.json +++ b/public/locales/nl/transactionFlow.json @@ -63,13 +63,13 @@ "noOptions": "Geen adres opties beschikbaar", "placeholder": { "default": "Voeg adres hier toe", - "ETH": "0xD9hbQK...", - "BNB": "0xD9hbQK...", - "BTC": "3FZbgi29...", - "LTC": "3FZbgi29...", - "SOL": "D4kA7VzHnmV...", - "DOT": "1D4kA7VxH...", - "DOGE": "DFabcd12..." + "eth": "0xb8c2C2...", + "bnb": "bnb1grpf...", + "btc": "3FZbgi29...", + "ltc": "ltc1qdp7p...", + "sol": "D4kA7VzHnmV...", + "dot": "1D4kA7VxH...", + "doge": "DFabcd12..." } }, "contentHash": { diff --git a/public/locales/zh/register.json b/public/locales/zh/register.json index 29744f59e..ff12a7ad6 100644 --- a/public/locales/zh/register.json +++ b/public/locales/zh/register.json @@ -77,7 +77,7 @@ "itemLabel": "{{coin}} 地址", "placeholder": { "default": "在此添加地址", - "eth": "0xD9hbQK...", + "eth": "0xb8c2C2...", "bnb": "bnb1grpf...", "btc": "3FZbgi29...", "ltc": "ltc1qdp7p...", @@ -161,10 +161,7 @@ "等待计时器完成 60 秒计时", "完成第二笔交易来获得该名称" ], - "moonpayItems": [ - "创建或登录现有的 Moonpay 账户", - "使用信用卡或借记卡完成单笔交易" - ], + "moonpayItems": ["创建或登录现有的 Moonpay 账户", "使用信用卡或借记卡完成单笔交易"], "setupProfile": "我希望先创建我的个人资料", "paymentMethod": "支付方式", "notEnoughEth": "钱包中的 ETH 不足", diff --git a/public/locales/zh/transactionFlow.json b/public/locales/zh/transactionFlow.json index ac08c39fd..bddd3d898 100644 --- a/public/locales/zh/transactionFlow.json +++ b/public/locales/zh/transactionFlow.json @@ -66,13 +66,13 @@ "noOptions": "没有可用的地址", "placeholder": { "default": "在此添加地址", - "ETH": "0xD9hbQK...", - "BNB": "0xD9hbQK...", - "BTC": "3FZbgi29...", - "LTC": "3FZbgi29...", - "SOL": "D4kA7VzHnmV...", - "DOT": "1D4kA7VxH...", - "DOGE": "DFabcd12..." + "eth": "0xb8c2C2...", + "bnb": "bnb1grpf...", + "btc": "3FZbgi29...", + "ltc": "ltc1qdp7p...", + "sol": "D4kA7VzHnmV...", + "dot": "1D4kA7VxH...", + "doge": "DFabcd12..." } }, "contentHash": { @@ -344,7 +344,7 @@ "invalidResolver": { "title": "未经授权的解析器", "description": "要使用此名称作为您的主名称,您需要先设置一个有效的解析器并更新该名称的 ETH 地址记录。" - } + } }, "multiStepSubnameDelete": { "title": "删除子名称", diff --git a/src/assets/social/DynamicSocialIcon.tsx b/src/assets/social/DynamicSocialIcon.tsx index cfc9f9ece..192b27a1c 100644 --- a/src/assets/social/DynamicSocialIcon.tsx +++ b/src/assets/social/DynamicSocialIcon.tsx @@ -4,27 +4,29 @@ import dynamic from 'next/dynamic' import { QuestionCircleSVG } from '@ensdomains/thorin' export const socialIconTypes = { + email: dynamic(() => import('@ensdomains/thorin').then((m) => m.EnvelopeSVG)), 'com.discord': dynamic(() => import('./SocialDiscord.svg')), 'com.discourse': dynamic(() => import('./SocialDiscourseColour.svg')), 'com.github': dynamic(() => import('./SocialGithub.svg')), 'com.medium': dynamic(() => import('./SocialMedium.svg')), 'com.twitter': dynamic(() => import('./SocialX.svg')), 'com.youtube': dynamic(() => import('./SocialYoutube.svg')), + 'eth.farcaster': dynamic(() => import('./SocialFarcaster.svg')), 'org.telegram': dynamic(() => import('./SocialTelegram.svg')), 'xyz.mirror': dynamic(() => import('./SocialMirrorColour.svg')), - email: dynamic(() => import('@ensdomains/thorin').then((m) => m.EnvelopeSVG)), } export const socialIconColors = { + email: '#000000', 'com.discord': '#5A57DD', 'com.discourse': undefined, 'com.github': '#000000', 'com.medium': '#000000', 'com.twitter': '#000000', 'com.youtube': '#FF0000', + 'eth.farcaster': '#8A63D2', 'org.telegram': '#2BABEE', 'xyz.mirror': undefined, - email: '#000000', } export const DynamicSocialIcon = ({ diff --git a/src/assets/social/SocialFarcaster.svg b/src/assets/social/SocialFarcaster.svg new file mode 100644 index 000000000..e5fe62100 --- /dev/null +++ b/src/assets/social/SocialFarcaster.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/src/constants/supportedSocialRecordKeys.ts b/src/constants/supportedSocialRecordKeys.ts index 34e8f4652..9e9d1410b 100644 --- a/src/constants/supportedSocialRecordKeys.ts +++ b/src/constants/supportedSocialRecordKeys.ts @@ -1,7 +1,8 @@ export const supportedSocialRecordKeys = [ - 'com.twitter', - 'com.github', + 'email', 'com.discord', + 'com.github', + 'com.twitter', + 'eth.farcaster', 'org.telegram', - 'email', ] as const diff --git a/src/constants/textRecords.ts b/src/constants/textRecords.ts index b8a58336f..781ba7e04 100644 --- a/src/constants/textRecords.ts +++ b/src/constants/textRecords.ts @@ -1,14 +1,15 @@ export const textRecords = [ - 'email', - 'url', 'avatar', 'description', - 'notice', + 'email', 'keywords', + 'notice', + 'url', 'com.discord', 'com.github', 'com.reddit', 'com.twitter', - 'org.telegram', + 'eth.farcaster', 'eth.ens.delegate', + 'org.telegram', ] as const diff --git a/src/hooks/useProfileEditorForm.test.ts b/src/hooks/useProfileEditorForm.test.ts index 16698a1b5..8c509053a 100644 --- a/src/hooks/useProfileEditorForm.test.ts +++ b/src/hooks/useProfileEditorForm.test.ts @@ -266,6 +266,18 @@ describe('useProfileEditorForm', () => { username: 'test@example', isValid: 'steps.profile.errors.invalidValue', }, + { + platform: 'Farcaster', + key: 'eth.farcaster', + username: 'valid-fname', + isValid: true, + }, + { + platform: 'Farcaster', + key: 'eth.farcaster', + username: 'invalid_fname', + isValid: 'steps.profile.errors.invalidValue', + }, ] testCases.forEach(({ platform, key, username, isValid }) => { diff --git a/src/utils/getSocialData.ts b/src/utils/getSocialData.ts index b09f272c2..6066fe97f 100644 --- a/src/utils/getSocialData.ts +++ b/src/utils/getSocialData.ts @@ -1,14 +1,21 @@ export const getSocialData = (iconKey: string, value: string) => { switch (iconKey) { - case 'twitter': - case 'com.twitter': + case 'email': return { - icon: 'com.twitter', + icon: 'email', color: '#000000', - label: 'X', - value: `@${value.replace(/^@/, '')}`, - type: 'link', - urlFormatter: `https://x.com/${value.replace(/^@/, '')}`, + label: 'Email', + value, + type: 'copy', + } + case 'discord': + case 'com.discord': + return { + icon: 'com.discord', + color: '#5A57DD', + label: 'Discord', + value, + type: 'copy', } case 'github': case 'com.github': @@ -20,15 +27,30 @@ export const getSocialData = (iconKey: string, value: string) => { type: 'link', urlFormatter: `https://github.com/${value}`, } - case 'discord': - case 'com.discord': + case 'twitter': + case 'com.x': + case 'com.twitter': { + const formattedValue = value.replace(/^@/, '') return { - icon: 'com.discord', - color: '#5A57DD', - label: 'Discord', - value, - type: 'copy', + icon: 'com.twitter', + color: '#000000', + label: 'X', + value: `@${formattedValue}`, + type: 'link', + urlFormatter: `https://x.com/${formattedValue}`, + } + } + case 'eth.farcaster': { + const formattedValue = value.toLowerCase() + return { + icon: 'eth.farcaster', + color: '#8A63D2', + label: 'Farcaster', + value: formattedValue, + type: 'link', + urlFormatter: `https://warpcast.com/${formattedValue}`, } + } case 'telegram': case 'org.telegram': return { @@ -39,14 +61,6 @@ export const getSocialData = (iconKey: string, value: string) => { type: 'link', urlFormatter: `https://t.me/${value}`, } - case 'email': - return { - icon: 'email', - color: '#000000', - label: 'Email', - value, - type: 'copy', - } default: return null } diff --git a/src/validators/validateAccount.test.ts b/src/validators/validateAccount.test.ts index ae6952563..1e3a9ed4f 100644 --- a/src/validators/validateAccount.test.ts +++ b/src/validators/validateAccount.test.ts @@ -216,6 +216,86 @@ describe('validateAccount', () => { expected: true, }, + // farcaster tests + { + description: 'farcaster fname: min length', + key: 'eth.farcaster', + value: 'v', + expected: true, + }, + { + description: 'farcaster fname: max length', + key: 'eth.farcaster', + value: 'valid-fname12345', + expected: true, + }, + { + description: 'farcaster fname: starts with hyphen', + key: 'eth.farcaster', + value: '-invalid', + expected: false, + }, + { + description: 'farcaster fname: too long', + key: 'eth.farcaster', + value: 'a'.repeat(17), + expected: false, + }, + { + description: 'farcaster fname: not lowercased', + key: 'eth.farcaster', + value: 'aBc', + expected: false, + }, + { + description: 'farcaster fname: non alphanumeric', + key: 'eth.farcaster', + value: 'cõõl', + expected: false, + }, + { + description: 'farcaster ens: min length', + key: 'eth.farcaster', + value: 'abc.eth', + expected: true, + }, + { + description: 'farcaster ens: max length', + key: 'eth.farcaster', + value: 'valid-ensname123.eth', + expected: true, + }, + { + description: 'farcaster ens: starts with hyphen', + key: 'eth.farcaster', + value: '-invalid.eth', + expected: false, + }, + { + description: 'farcaster ens: too long', + key: 'eth.farcaster', + value: 'a'.repeat(17) + '.eth', + expected: false, + }, + { + description: 'farcaster ens: too short', + key: 'eth.farcaster', + value: 'ab.eth', + expected: false, + }, + { + description: 'farcaster ens: not lowercased', + key: 'eth.farcaster', + value: 'aBc.eth', + expected: false, + }, + { + description: 'farcaster ens: not alphanumeric', + key: 'eth.farcaster', + value: 'cõõl.eth', + expected: false, + }, + // Test for unknown key { description: 'True for unknown key', key: 'unknown', value: 'value', expected: true }, ] diff --git a/src/validators/validateAccount.ts b/src/validators/validateAccount.ts index 8b9391cea..91b1d5e18 100644 --- a/src/validators/validateAccount.ts +++ b/src/validators/validateAccount.ts @@ -32,18 +32,42 @@ const telegramRegex = /^[A-Za-z0-9_]{5,32}$/ */ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ +/* farcaster: + * - 1-16 chars + * - case insensitive alphanumeric + hyphen + * - insensitive but we don't have a way to normalise case in the profile editor so we enforce lowercase + * - cannot start with hyphen + */ +const farcasterRegex = /^[a-z0-9][a-z0-9-]{0,15}$/ + +const validateFarcaster = (value: string): boolean => { + // farcaster only supports 2LD .eth currently! + if (value.endsWith('.eth')) { + const label = value.slice(0, -4) + // 2LD .eth only can only currently be registered with 3+ chars + if (label.length < 3) return false + // label doesn't need to be normalised or anything + // because the farcaster regex fits into ens name normalisation + // the regex also ensures that subdomains aren't being used + return farcasterRegex.test(label) + } + return farcasterRegex.test(value) +} + export const validateAccount = ({ key, value }: { key: string; value: string }): boolean => { switch (key) { - case 'com.twitter': - return twitterRegex.test(value) - case 'com.github': - return githubRegex.test(value) + case 'email': + return emailRegex.test(value) case 'com.discord': return discordRegex.test(value) + case 'com.github': + return githubRegex.test(value) + case 'com.twitter': + return twitterRegex.test(value) + case 'eth.farcaster': + return validateFarcaster(value) case 'org.telegram': return telegramRegex.test(value) - case 'email': - return emailRegex.test(value) default: return true } diff --git a/src/validators/validateAddress.ts b/src/validators/validateAddress.ts index 59cd13504..11b14c4c5 100644 --- a/src/validators/validateAddress.ts +++ b/src/validators/validateAddress.ts @@ -15,11 +15,13 @@ export const validateCryptoAddress = ({ const coinTypeInstance = getCoderByCoinName(coin) coinTypeInstance.decode(_address) return true - } catch (e: any) { + } catch (e: unknown) { if (typeof e === 'string') return e - if (e.reason) return e.reason - if (e.message) return e.message - if (e.toString) return e.toString() + if (typeof e === 'object' && e) { + if ('reason' in e && e.reason) return e.reason as string + if ('message' in e && e.message) return e.message as string + if ('toString' in e && typeof e.toString === 'function') return e.toString() + } return 'Invalid address' } }