diff --git a/assets/icons/circle_stroke2_corner0_rounded.svg b/assets/icons/circle_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..98fe755b09 --- /dev/null +++ b/assets/icons/circle_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/icons/Circle.tsx b/src/components/icons/Circle.tsx new file mode 100644 index 0000000000..93d837119e --- /dev/null +++ b/src/components/icons/Circle.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Circle_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z', +}) diff --git a/src/lib/strings/handles.ts b/src/lib/strings/handles.ts index 78a2e1a09a..7666bc7084 100644 --- a/src/lib/strings/handles.ts +++ b/src/lib/strings/handles.ts @@ -2,6 +2,8 @@ // https://github.com/bluesky-social/indigo/blob/main/atproto/syntax/handle.go#L10 import {forceLTR} from '#/lib/strings/bidi' +export * from './reserved' + const VALIDATE_REGEX = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/ @@ -34,7 +36,8 @@ export function sanitizeHandle(handle: string, prefix = ''): string { export interface IsValidHandle { handleChars: boolean hyphenStartOrEnd: boolean - frontLength: boolean + frontLengthLongEnough: boolean + frontLengthNotTooLong: boolean totalLength: boolean overall: boolean } @@ -50,7 +53,8 @@ export function validateServiceHandle( handleChars: !str || (VALIDATE_REGEX.test(fullHandle) && !str.includes('.')), hyphenStartOrEnd: !str.startsWith('-') && !str.endsWith('-'), - frontLength: str.length >= 3 && str.length <= MAX_SERVICE_HANDLE_LENGTH, + frontLengthLongEnough: str.length >= 3, + frontLengthNotTooLong: str.length <= MAX_SERVICE_HANDLE_LENGTH, totalLength: fullHandle.length <= 253, } diff --git a/src/lib/strings/reserved.ts b/src/lib/strings/reserved.ts new file mode 100644 index 0000000000..a0954ed35f --- /dev/null +++ b/src/lib/strings/reserved.ts @@ -0,0 +1,1066 @@ +// reserved handles for Bluesky PBC's PDS implementation +// copied from: https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/handle/reserved.ts + +const atpSpecific = [ + 'at', + 'atp', + 'plc', + 'pds', + 'did', + 'repo', + 'tid', + 'nsid', + 'xrpc', + 'lex', + 'lexicon', + 'bsky', + 'bluesky', + 'handle', +] + +// naively pulled from: https://radwebhosting.com/client_area/knowledgebase/196/List-of-Banned-Subdomain-Prefixes.html +const commonlyReserved = [ + 'about', + 'abuse', + 'access', + 'account', + 'accounts', + 'acme', + 'activate', + 'activities', + 'activity', + 'ad', + 'add', + 'address', + 'adm', + 'admanager', + 'admin', + 'administration', + 'administrator', + 'administrators', + 'admins', + 'ads', + 'adsense', + 'adult', + 'advertising', + 'adwords', + 'affiliate', + 'affiliatepage', + 'affiliates', + 'afp', + 'ajax', + 'all', + 'alpha', + 'analysis', + 'analytics', + 'android', + 'anon', + 'anonymous', + 'answer', + 'answers', + 'ap', + 'api', + 'apis', + 'app', + 'appengine', + 'appnews', + 'apps', + 'archive', + 'archives', + 'article', + 'asdf', + 'asset', + 'assets', + 'auth', + 'authentication', + 'avatar', + 'backup', + 'bank', + 'banner', + 'banners', + 'base', + 'beginners', + 'beta', + 'billing', + 'bin', + 'binaries', + 'binary', + 'blackberry', + 'blog', + 'blogs', + 'blogsearch', + 'board', + 'book', + 'bookmark', + 'bookmarks', + 'books', + 'bot', + 'bots', + 'bug', + 'bugs', + 'business', + 'buy', + 'buzz', + 'cache', + 'calendar', + 'call', + 'campaign', + 'cancel', + 'captcha', + 'career', + 'careers', + 'cart', + 'catalog', + 'catalogs', + 'categories', + 'category', + 'cdn', + 'cgi', + 'cgi-bin', + 'changelog', + 'chart', + 'charts', + 'chat', + 'check', + 'checked', + 'checking', + 'checkout', + 'client', + 'cliente', + 'clients', + 'clients1', + 'cnarne', + 'code', + 'comercial', + 'comment', + 'comments', + 'communities', + 'community', + 'company', + 'compare', + 'compras', + 'config', + 'configuration', + 'confirm', + 'confirmation', + 'connect', + 'contact', + 'contacts', + 'contactus', + 'contact-us', + 'contact_us', + 'content', + 'contest', + 'contribute', + 'contributor', + 'contributors', + 'coppa', + 'copyright', + 'copyrights', + 'core', + 'corp', + 'countries', + 'country', + 'cpanel', + 'create', + 'css', + 'cssproxy', + 'customise', + 'customize', + 'dashboard', + 'data', + 'db', + 'default', + 'delete', + 'demo', + 'design', + 'designer', + 'desktop', + 'destroy', + 'dev', + 'devel', + 'developer', + 'developers', + 'devs', + 'diagram', + 'diary', + 'dict', + 'dictionary', + 'die', + 'dir', + 'directory', + 'direct_messages', + 'direct-messages', + 'dist', + 'diversity', + 'dl', + 'dmca', + 'doc', + 'docs', + 'documentation', + 'documentations', + 'documents', + 'domain', + 'domains', + 'donate', + 'download', + 'downloads', + 'e', + 'e-mail', + 'earth', + 'ecommerce', + 'edit', + 'edits', + 'editor', + 'edu', + 'education', + 'email', + 'embed', + 'embedded', + 'employment', + 'employments', + 'empty', + 'enable', + 'encrypted', + 'end', + 'engine', + 'enterprise', + 'enterprises', + 'entries', + 'entry', + 'error', + 'errorlog', + 'errors', + 'eval', + 'event', + 'example', + 'examplecommunity', + 'exampleopenid', + 'examplesyn', + 'examplesyndicated', + 'exampleusername', + 'exchange', + 'exit', + 'explore', + 'faq', + 'faqs', + 'favorite', + 'favorites', + 'favourite', + 'favourites', + 'feature', + 'features', + 'feed', + 'feedback', + 'feedburner', + 'feedproxy', + 'feeds', + 'file', + 'files', + 'finance', + 'folder', + 'folders', + 'first', + 'following', + 'forgot', + 'form', + 'forms', + 'forum', + 'forums', + 'founder', + 'free', + 'friend', + 'friends', + 'ftp', + 'fuck', + 'fun', + 'fusion', + 'gadget', + 'gadgets', + 'game', + 'games', + 'gears', + 'general', + 'geographic', + 'get', + 'gettingstarted', + 'gift', + 'gifts', + 'gist', + 'git', + 'github', + 'gmail', + 'go', + 'golang', + 'goto', + 'gov', + 'graph', + 'graphs', + 'group', + 'groups', + 'guest', + 'guests', + 'guide', + 'guides', + 'hack', + 'hacks', + 'head', + 'help', + 'home', + 'homepage', + 'host', + 'hosting', + 'hostmaster', + 'hostname', + 'howto', + 'how-to', + 'how_to', + 'html', + 'htrnl', + 'http', + 'httpd', + 'https', + 'i', + 'iamges', + 'icon', + 'icons', + 'id', + 'idea', + 'ideas', + 'im', + 'image', + 'images', + 'img', + 'imap', + 'inbox', + 'inboxes', + 'index', + 'indexes', + 'info', + 'information', + 'inquiry', + 'intranet', + 'investor', + 'investors', + 'invitation', + 'invitations', + 'invite', + 'invoice', + 'invoices', + 'imac', + 'ios', + 'ipad', + 'iphone', + 'irc', + 'irnages', + 'irng', + 'is', + 'issue', + 'issues', + 'it', + 'item', + 'items', + 'java', + 'javascript', + 'job', + 'jobs', + 'join', + 'js', + 'json', + 'jump', + 'kb', + 'knowledge-base', + 'knowledgebase', + 'lab', + 'labs', + 'language', + 'languages', + 'last', + 'ldap_status', + 'ldap-status', + 'ldapstatus', + 'legal', + 'license', + 'licenses', + 'link', + 'links', + 'linux', + 'list', + 'lists', + 'livejournal', + 'lj', + 'local', + 'locale', + 'location', + 'log', + 'log-in', + 'log-out', + 'login', + 'logout', + 'logs', + 'log_in', + 'log_out', + 'm', + 'mac', + 'macos', + 'macosx', + 'mac-os', + 'mac-os-x', + 'mac_os_x', + 'mail', + 'mailer', + 'mailing', + 'main', + 'maintenance', + 'manage', + 'manager', + 'manual', + 'map', + 'maps', + 'marketing', + 'master', + 'me', + 'media', + 'member', + 'members', + 'memories', + 'memory', + 'merchandise', + 'message', + 'messages', + 'messenger', + 'mg', + 'microblog', + 'microblogs', + 'mine', + 'mis', + 'misc', + 'mms', + 'mob', + 'mobile', + 'model', + 'models', + 'money', + 'movie', + 'movies', + 'mp3', + 'mp4', + 'msg', + 'msn', + 'music', + 'mx', + 'my', + 'mymme', + 'mysql', + 'name', + 'named', + 'nan', + 'navi', + 'navigation', + 'net', + 'network', + 'networks', + 'new', + 'news', + 'newsletter', + 'nick', + 'nickname', + 'nil', + 'none', + 'notes', + 'noticias', + 'notification', + 'notifications', + 'notify', + 'ns', + 'ns1', + 'ns2', + 'ns3', + 'ns4', + 'ns5', + 'null', + 'oauth', + 'oauth-clients', + 'oauth_clients', + 'ocsp', + 'offer', + 'offers', + 'official', + 'old', + 'online', + 'openid', + 'operator', + 'option', + 'options', + 'order', + 'orders', + 'org', + 'organization', + 'organizations', + 'other', + 'overview', + 'owner', + 'owners', + 'p0rn', + 'pack', + 'page', + 'pager', + 'pages', + 'paid', + 'panel', + 'partner', + 'partnerpage', + 'partners', + 'password', + 'patch', + 'pay', + 'payment', + 'people', + 'perl', + 'person', + 'phone', + 'photo', + 'photoalbum', + 'photos', + 'php', + 'phpmyadmin', + 'phppgadmin', + 'phpredisadmin', + 'pic', + 'pics', + 'picture', + 'pictures', + 'ping', + 'pixel', + 'places', + 'plan', + 'plans', + 'plugin', + 'plugins', + 'podcasts', + 'policies', + 'policy', + 'pop', + 'pop3', + 'popular', + 'porn', + 'portal', + 'portals', + 'post', + 'postfix', + 'postmaster', + 'posts', + 'pr', + 'pr0n', + 'premium', + 'press', + 'price', + 'pricing', + 'principles', + 'print', + 'privacy', + 'privacy-policy', + 'privacypolicy', + 'privacy_policy', + 'private', + 'prod', + 'product', + 'production', + 'products', + 'profile', + 'profiles', + 'project', + 'projects', + 'promo', + 'promotions', + 'proxies', + 'proxy', + 'pub', + 'public', + 'purchase', + 'purpose', + 'put', + 'python', + 'queries', + 'query', + 'radio', + 'random', + 'ranking', + 'read', + 'reader', + 'readme', + 'recent', + 'recruit', + 'recruitment', + 'redirect', + 'register', + 'registration', + 'release', + 'remove', + 'replies', + 'report', + 'reports', + 'repositories', + 'repository', + 'req', + 'request', + 'requests', + 'research', + 'reset', + 'resolve', + 'resolver', + 'review', + 'rnail', + 'rnicrosoft', + 'roc', + 'root', + 'rss', + 'ruby', + 'rule', + 'sag', + 'sale', + 'sales', + 'sample', + 'samples', + 'sandbox', + 'save', + 'scholar', + 'school', + 'schools', + 'script', + 'scripts', + 'search', + 'secure', + 'security', + 'self', + 'seminars', + 'send', + 'server', + 'server-info', + 'server_info', + 'server-status', + 'server_status', + 'servers', + 'service', + 'services', + 'session', + 'sessions', + 'setting', + 'settings', + 'setup', + 'share', + 'shop', + 'shopping', + 'shortcut', + 'shortcuts', + 'show', + 'sign-in', + 'sign-up', + 'signin', + 'signout', + 'signup', + 'sign_in', + 'sign_up', + 'site', + 'sitemap', + 'sitemaps', + 'sitenews', + 'sites', + 'sketchup', + 'sky', + 'slash', + 'slashinvoice', + 'slut', + 'smartphone', + 'sms', + 'smtp', + 'soap', + 'software', + 'sorry', + 'source', + 'spec', + 'special', + 'spreadsheet', + 'spreadsheets', + 'sql', + 'src', + 'srntp', + 'ssh', + 'ssl', + 'ssladmin', + 'ssladministrator', + 'sslwebmaster', + 'ssytem', + 'staff', + 'stage', + 'staging', + 'start', + 'stat', + 'state', + 'static', + 'statistics', + 'stats', + 'status', + 'store', + 'stores', + 'stories', + 'style', + 'styleguide', + 'styles', + 'stylesheet', + 'stylesheets', + 'subdomain', + 'subscribe', + 'subscription', + 'subscriptions', + 'suggest', + 'suggestqueries', + 'support', + 'survey', + 'surveys', + 'surveytool', + 'svn', + 'swf', + 'syn', + 'sync', + 'syndicated', + 'sys', + 'sysadmin', + 'sysadministrator', + 'sysadmins', + 'system', + 'tablet', + 'tablets', + 'tag', + 'tags', + 'talk', + 'talkgadget', + 'task', + 'tasks', + 'team', + 'teams', + 'tech', + 'telnet', + 'term', + 'terms', + 'terms-of-service', + 'termsofservice', + 'terms_of_service', + 'test', + 'testing', + 'tests', + 'text', + 'theme', + 'themes', + 'thread', + 'threads', + 'ticket', + 'tickets', + 'tmp', + 'todo', + 'to-do', + 'to_do', + 'toml', + 'tool', + 'toolbar', + 'toolbars', + 'tools', + 'top', + 'topic', + 'topics', + 'tos', + 'tour', + 'trac', + 'translate', + 'trace', + 'translation', + 'translations', + 'translator', + 'trends', + 'tutorial', + 'tux', + 'tv', + 'twitter', + 'txt', + 'ul', + 'undef', + 'unfollow', + 'unsubscribe', + 'update', + 'updates', + 'upgrade', + 'upgrades', + 'upi', + 'upload', + 'uploads', + 'url', + 'usage', + 'user', + 'username', + 'usernames', + 'users', + 'uuid', + 'validation', + 'validations', + 'ver', + 'version', + 'video', + 'videos', + 'video-stats', + 'visitor', + 'visitors', + 'voice', + 'volunteer', + 'volunteers', + 'w', + 'watch', + 'wave', + 'weather', + 'web', + 'webdisk', + 'webhook', + 'webhooks', + 'webmail', + 'webmaster', + 'webmasters', + 'webrnail', + 'website', + 'websites', + 'welcome', + 'whm', + 'whois', + 'widget', + 'widgets', + 'wifi', + 'wiki', + 'wikis', + 'win', + 'windows', + 'word', + 'work', + 'works', + 'workshop', + 'wpad', + 'ww', + 'wws', + 'www', + 'wwws', + 'wwww', + 'xfn', + 'xhtml', + 'xhtrnl', + 'xml', + 'xmpp', + 'xpg', + 'xxx', + 'yaml', + 'year', + 'yml', + 'you', + 'yourdomain', + 'yourname', + 'yoursite', + 'yourusername', +] + +const famousAccounts = [ + // reserving some large twitter accounts (top 100 by followers according to wikidata dump) + '10ronaldinho', + '3gerardpique', + 'aclu', + 'adele', + 'akshaykumar', + 'aliaa08', + 'aliciakeys', + 'amitshah', + 'andresiniesta8', + 'anushkasharma', + 'arianagrande', + 'arrahman', + 'arvindkejriwal', + 'avrillavigne', + 'barackobama', + 'bbcbreaking', + 'bbcworld', + 'beingsalmankhan', + 'billgates', + 'britneyspears', + 'brunomars', + 'bts_bighit', + 'bts_twt', + 'championsleague', + 'chrisbrown', + 'cnnbrk', + 'coldplay', + 'conanobrien', + 'cristiano', + 'danieltosh', + 'davidguetta', + 'ddlovato', + 'deepikapadukone', + 'drake', + 'elisapie', + 'ellendegeneres', + 'elonmusk', + 'eminem', + 'emmawatson', + 'fcbarcelona', + 'foxnews', + 'harry_styles', + 'hillaryclinton', + 'iamsrk', + 'ihrithik', + 'imvkohli', + 'instagram', + 'jimmyfallon', + 'jlo', + 'joebiden', + 'jtimberlake', + 'justinbieber', + 'kaka', + 'kanyewest', + 'katyperry', + 'kendalljenner', + 'kevinhart4real', + 'khloekardashian', + 'kimkardashian', + 'kingjames', + 'kourtneykardash', + 'kyliejenner', + 'ladygaga', + 'liampayne', + 'liltunechi', + 'manutd', + 'mariahcarey', + 'mileycyrus', + 'mohamadalarefe', + 'narendramodi', + 'nasa', + 'nba', + 'neymarjr', + 'nfl', + 'niallofficial', + 'nickiminaj', + 'npr', + 'nytimes', + 'onedirection', + 'oprah', + 'pink', + 'pitbull', + 'playstation', + 'pmoindia', + 'premierleague', + 'priyankachopra', + 'realdonaldtrump', + 'ricky_martin', + 'rihanna', + 'sachin_rt', + 'selenagomez', + 'shakira', + 'shawnmendes', + 'sportscenter', + 'srbachchan', + 'subhisharma100', + 'taylorswift13', + 'theeconomist', + 'twitter', + 'virendersehwag', + 'whitehouse45', + 'wizkhalifa', + 'youtube', + 'zaynmalik', + + // some top instagram (https://en.wikipedia.org/wiki/List_of_most-followed_Instagram_accounts) + 'beyonce', + 'billieeilish', + 'leomessi', + 'natgeo', + 'nike', + 'snoopdogg', + 'taylorswift', + 'therock', + + // ... and a couple more prominent accounts, subjectively + '10downingstreet', + 'aoc', + 'carterjwm', + 'dril', + 'gretathunberg', + 'kamalaharris', + 'kremlinrussia_e', + 'potus', + 'rondesantisfl', + 'ukraine', + 'washingtonpost', + 'yousuck2020', + 'zelenskyyua', + + // top japan-specific accounts + 'akiko_lawson', + 'ariyoshihiroiki', + 'asahi', + 'dozle_official', + 'famima_now', + 'ff_xiv_jp', + 'fujitv', + 'gigazine', + 'hajimesyacho', + 'hikakin', + 'jocx', + 'jotx', + 'kiyo_saiore', + 'mainichi', + 'matsu_bouzu', + 'naomiosaka', + 'nhk', + 'nikkei', + 'nintendo', + 'ntv', + 'oowareware1945', + 'pamyurin', + 'poke_times', + 'rolaworld', + 'seikintv', + 'starbucksjapan', + 'tbs', + 'tbs_pr', + 'tvasahi', + 'tvtokyo', + 'yokoono', + 'yomiuri_online', + + // top brazil-specific accounts + 'brasildefato', + 'claudialeitte', + 'correio', + 'em_com', + 'estadao', + 'folha', + 'gazetadopovo', + 'ivetesangalo', + 'jairbolsonaro', + 'jornaldobrasil', + 'jornaloglobo', + 'lucianohuck', + 'lulaoficial', + 'marcosmion', + 'paulocoelho', + 'portalr7', + 'rede_globo', + 'zerohora', +] + +/** + * Check if a handle is will be rejected by Bluesky PBC's PDS implementation + * Note: other PDSes may have different restrictions, so we should only apply this check + * if signing up to `bsky.social` + * + * See: https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/handle/reserved.ts + */ +export function isHandleReserved(prefix: string): boolean { + return ( + atpSpecific.includes(prefix) || + commonlyReserved.includes(prefix) || + famousAccounts.includes(prefix) + ) +} diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index dfca1f7d8e..3c892bdb57 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -68,6 +68,7 @@ export type MetricEvents = { backgroundCount: number } 'signup:handleTaken': {} + 'signup:handleReserved': {} 'signin:hostingProviderPressed': { hostingProviderDidChange: boolean } diff --git a/src/screens/Signup/BackNextButtons.tsx b/src/screens/Signup/BackNextButtons.tsx index 888b9071e1..5a85a85d1f 100644 --- a/src/screens/Signup/BackNextButtons.tsx +++ b/src/screens/Signup/BackNextButtons.tsx @@ -9,7 +9,7 @@ import {Loader} from '#/components/Loader' export interface BackNextButtonsProps { hideNext?: boolean showRetry?: boolean - isLoading: boolean + isLoading?: boolean isNextDisabled?: boolean onBackPress: () => void onNextPress?: () => void diff --git a/src/screens/Signup/StepCaptcha/index.tsx b/src/screens/Signup/StepCaptcha/index.tsx index 388deecaff..e900b96c09 100644 --- a/src/screens/Signup/StepCaptcha/index.tsx +++ b/src/screens/Signup/StepCaptcha/index.tsx @@ -75,7 +75,7 @@ export function StepCaptcha() { return ( - + (state.handle) - const [draftValue, setDraftValue] = React.useState(state.handle) - const isLoading = useThrottledValue(state.isLoading, 500) + const [draftValue, setDraftValue] = useState(state.handle) + const isNextLoading = useThrottledValue(state.isLoading, 500) - const onNextPress = React.useCallback(async () => { - const handle = handleValueRef.current.trim() + const validCheck = validateServiceHandle(draftValue, state.userDomain) + + const {data: isHandleAvailable, isLoading} = useHandleAvailabilityQuery( + draftValue, + state.userDomain, + validCheck.overall, + ) + + const onNextPress = async () => { + if (!isHandleAvailable?.available) return + + const handle = draftValue.trim() dispatch({ type: 'setHandle', value: handle, }) - const newValidCheck = validateServiceHandle(handle, state.userDomain) - if (!newValidCheck.overall) { + if (!validCheck.overall) { + return + } + + if (isHandleReserved(handle)) { + dispatch({ + type: 'setError', + value: _(msg`That username is not available`), + field: 'handle', + }) + logger.metric('signup:handleReserved', {}, {statsig: true}) return } @@ -52,7 +80,7 @@ export function StepHandle() { if (res.data.did) { dispatch({ type: 'setError', - value: _(msg`That handle is already taken.`), + value: _(msg`That username is already taken`), field: 'handle', }) logger.metric('signup:handleTaken', {}, {statsig: true}) @@ -82,17 +110,10 @@ export function StepHandle() { return } dispatch({type: 'next'}) - }, [ - _, - dispatch, - state.activeStep, - state.serviceDescription?.phoneVerificationRequired, - state.userDomain, - agent, - ]) + } - const onBackPress = React.useCallback(() => { - const handle = handleValueRef.current.trim() + const onBackPress = () => { + const handle = draftValue.trim() dispatch({ type: 'setHandle', value: handle, @@ -103,13 +124,18 @@ export function StepHandle() { {activeStep: state.activeStep}, {statsig: true}, ) - }, [dispatch, state.activeStep]) + } - const validCheck = validateServiceHandle(draftValue, state.userDomain) return ( + + This can be changed at any time. + + + Username + + + {draftValue.length > 0 && ( + {draftValue} + )} - {draftValue !== '' && ( - - - Your full username will be{' '} - - @{createFullHandle(draftValue, state.userDomain)} - - - - )} - - {draftValue !== '' && ( - - {state.error ? ( - - - {state.error} - - ) : undefined} - {validCheck.hyphenStartOrEnd ? ( - - - - Only contains letters, numbers, and hyphens - - - ) : ( - - - - Doesn't begin or end with a hyphen - - + + + {state.error && ( + + + {state.error} + )} - - + + + {isLoading ? ( + Checking... + ) : isHandleAvailable?.available ? ( + Available! + ) : isHandleAvailable?.reason === 'taken' ? ( + Taken + ) : ( + Invalid + )} + + + )} + + - {!validCheck.totalLength || - draftValue.length > MAX_SERVICE_HANDLE_LENGTH ? ( - + + {!validCheck.hyphenStartOrEnd ? ( + + Doesn't begin or end with a hyphen + + ) : ( + + Only contains letters, numbers, and hyphens + + )} + + + + {!validCheck.frontLengthNotTooLong || !validCheck.totalLength ? ( + No longer than{' '} - + ) : ( - + At least 3 characters - + )} - + - )} + @@ -208,10 +252,105 @@ export function StepHandle() { ) } -function IsValidIcon({valid}: {valid: boolean}) { - const t = useTheme() - if (!valid) { - return - } - return +function Requirement({ + children, + fade, +}: { + children: React.ReactNode + fade?: boolean +}) { + return ( + + {children} + + ) +} + +function RequirementText({children}: {children: React.ReactNode}) { + const t = useTheme() + return ( + + {children} + + ) +} + +function RequirementIcon({ + state, +}: { + state: 'initial' | 'valid' | 'invalid' | 'loading' +}) { + const t = useTheme() + + switch (state) { + case 'initial': + return + case 'valid': + return ( + + ) + case 'invalid': + return + case 'loading': + return + } +} + +function GhostText({children, value}: {children: string; value: string}) { + const t = useTheme() + // eslint-disable-next-line bsky-internal/avoid-unwrapped-text + return ( + + + {children} + + {value} + + + + ) } diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx index f24cd0e45d..cf4a9297e2 100644 --- a/src/screens/Signup/StepInfo/index.tsx +++ b/src/screens/Signup/StepInfo/index.tsx @@ -144,7 +144,7 @@ export function StepInfo({ return ( - + void}) { a.pt_2xl, !gtMobile && {paddingBottom: 100}, ]}> - - + + Step {state.activeStep + 1} of{' '} {state.serviceDescription && @@ -153,7 +154,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { : '3'} - + {state.activeStep === SignupStep.INFO ? ( Your account ) : state.activeStep === SignupStep.HANDLE ? ( diff --git a/src/state/queries/handle-availablility.ts b/src/state/queries/handle-availablility.ts new file mode 100644 index 0000000000..6ec2c043ea --- /dev/null +++ b/src/state/queries/handle-availablility.ts @@ -0,0 +1,44 @@ +import {useQuery} from '@tanstack/react-query' + +import {createFullHandle, isHandleReserved} from '#/lib/strings/handles' +import {useAgent} from '#/state/session' +import {useDebouncedValue} from '#/components/live/utils' + +export const RQKEY_handleAvailability = (handle: string) => [ + 'handle-availability', + handle, +] + +export function useHandleAvailabilityQuery( + name: string, + domain: string, + enabled: boolean, + debounceDelayMs = 500, +) { + name = name.trim() + const handle = createFullHandle(name, domain) + const debouncedHandle = useDebouncedValue(handle, debounceDelayMs) + const agent = useAgent() + + return useQuery({ + enabled: enabled && handle === debouncedHandle, + queryKey: RQKEY_handleAvailability(debouncedHandle), + queryFn: async () => { + const frontSegment = debouncedHandle.split('.')[0] + if (isHandleReserved(frontSegment)) { + return {available: false, reason: 'reserved'} as const + } + try { + const res = await agent.resolveHandle({ + handle: debouncedHandle, + }) + + if (res.data.did) { + return {available: false, reason: 'taken'} as const + } + } catch {} + + return {available: true} as const + }, + }) +}