From 8f88fd5b2f45613adca2958c189843e74486f8fa Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 1 Aug 2025 20:36:58 +0200 Subject: [PATCH] add suggestions --- package.json | 4 +- src/lib/constants.ts | 2 +- src/lib/strings/handles.ts | 2 - src/lib/strings/reserved.ts | 1066 --------------------- src/screens/Signup/StepHandle.tsx | 126 ++- src/state/queries/handle-availablility.ts | 136 ++- yarn.lock | 73 +- 7 files changed, 236 insertions(+), 1173 deletions(-) delete mode 100644 src/lib/strings/reserved.ts diff --git a/package.json b/package.json index 77908a1615..a936f2ee54 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "icons:optimize": "svgo -f ./assets/icons" }, "dependencies": { - "@atproto/api": "^0.15.26", + "@atproto/api": "^0.16.0", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", @@ -219,7 +219,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/dev-env": "^0.3.155", + "@atproto/dev-env": "^0.3.158", "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/runtime": "^7.26.0", diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b77c884fe3..15785d2610 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -5,7 +5,7 @@ export const LOCAL_DEV_SERVICE = Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' export const STAGING_SERVICE = 'https://staging.bsky.dev' export const BSKY_SERVICE = 'https://bsky.social' -export const BSKY_SERVICE_HANDLE_ENDSWITH = '.bsky.social' +export const BSKY_SERVICE_DID = 'did:web:bsky.social' export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app' export const DEFAULT_SERVICE = BSKY_SERVICE const HELP_DESK_LANG = 'en-us' diff --git a/src/lib/strings/handles.ts b/src/lib/strings/handles.ts index 5b1a6f9474..02b9943d31 100644 --- a/src/lib/strings/handles.ts +++ b/src/lib/strings/handles.ts @@ -2,8 +2,6 @@ // 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])?$/ diff --git a/src/lib/strings/reserved.ts b/src/lib/strings/reserved.ts deleted file mode 100644 index a0954ed35f..0000000000 --- a/src/lib/strings/reserved.ts +++ /dev/null @@ -1,1066 +0,0 @@ -// 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/screens/Signup/StepHandle.tsx b/src/screens/Signup/StepHandle.tsx index f892bc7fad..980389450c 100644 --- a/src/screens/Signup/StepHandle.tsx +++ b/src/screens/Signup/StepHandle.tsx @@ -2,17 +2,18 @@ import {useState} from 'react' import {View} from 'react-native' import Animated, { FadeIn, + FadeInDown, FadeOut, + FadeOutDown, LayoutAnimationConfig, LinearTransition, } from 'react-native-reanimated' +import {type ComAtprotoTempCheckHandleAvailability} from '@atproto/api' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {BSKY_SERVICE_HANDLE_ENDSWITH} from '#/lib/constants' import { createFullHandle, - isHandleReserved, MAX_SERVICE_HANDLE_LENGTH, validateServiceHandle, } from '#/lib/strings/handles' @@ -22,14 +23,18 @@ import {useAgent} from '#/state/session' import {ScreenTransition} from '#/screens/Login/ScreenTransition' import {useSignupContext} from '#/screens/Signup/state' import {atoms as a, native, useTheme} from '#/alf' +import {borderRadius} from '#/alf/tokens' +import {Button} from '#/components/Button' import * as TextField from '#/components/forms/TextField' import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {At_Stroke2_Corner0_Rounded as AtIcon} from '#/components/icons/At' +import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' import {Text} from '#/components/Typography' import {BackNextButtons} from './BackNextButtons' export function StepHandle() { const {_} = useLingui() + const t = useTheme() const {state, dispatch} = useSignupContext() const agent = useAgent() const [draftValue, setDraftValue] = useState(state.handle) @@ -37,11 +42,14 @@ export function StepHandle() { const validCheck = validateServiceHandle(draftValue, state.userDomain) - const {data: isHandleAvailable, isLoading} = useHandleAvailabilityQuery( - draftValue, - state.userDomain, - validCheck.overall, - ) + const {data: isHandleAvailable, isLoading} = useHandleAvailabilityQuery({ + username: draftValue, + serviceDid: state.serviceDescription?.did ?? 'UNKNOWN', + serviceDomain: state.userDomain, + birthDate: state.dateOfBirth.toISOString(), + email: state.email, + enabled: validCheck.overall, + }) const onNextPress = async () => { if (!isHandleAvailable?.available) return @@ -56,19 +64,6 @@ export function StepHandle() { return } - if ( - state.userDomain === BSKY_SERVICE_HANDLE_ENDSWITH && - isHandleReserved(handle) - ) { - dispatch({ - type: 'setError', - value: _(msg`That username is not available`), - field: 'handle', - }) - logger.metric('signup:handleReserved', {}, {statsig: true}) - return - } - try { dispatch({type: 'setIsLoading', value: true}) @@ -159,6 +154,9 @@ export function StepHandle() { {draftValue} )} + {isHandleAvailable?.available && ( + + )} @@ -171,21 +169,31 @@ export function StepHandle() { {isHandleAvailable && !isHandleAvailable.available && validCheck.overall && ( - - - {isHandleAvailable?.reason === 'taken' ? ( + <> + + - {createFullHandle(draftValue, state.userDomain)} is - taken - - ) : ( - - {createFullHandle(draftValue, state.userDomain)} is - invalid + {createFullHandle(draftValue, state.userDomain)} is not + available + + + {isHandleAvailable.suggestions && + isHandleAvailable.suggestions.length > 0 && ( + { + setDraftValue( + suggestion.handle.slice( + 0, + state.userDomain.length * -1, + ), + ) + // TODO: add logging + }} + /> )} - - + )} {(!validCheck.handleChars || !validCheck.hyphenStartOrEnd) && ( @@ -247,3 +255,57 @@ function RequirementText({children}: {children: React.ReactNode}) { ) } + +function HandleSuggestions({ + suggestions, + onSelect, +}: { + suggestions: ComAtprotoTempCheckHandleAvailability.Suggestion[] + onSelect: ( + suggestions: ComAtprotoTempCheckHandleAvailability.Suggestion, + ) => void +}) { + const t = useTheme() + const {_} = useLingui() + + return ( + + {suggestions.map((suggestion, index) => ( + + ))} + + ) +} diff --git a/src/state/queries/handle-availablility.ts b/src/state/queries/handle-availablility.ts index e1056d45be..e5d9f208f1 100644 --- a/src/state/queries/handle-availablility.ts +++ b/src/state/queries/handle-availablility.ts @@ -1,63 +1,113 @@ +import {useMemo} from 'react' +import {Agent, ComAtprotoTempCheckHandleAvailability} from '@atproto/api' import {useQuery} from '@tanstack/react-query' -import {BSKY_SERVICE_HANDLE_ENDSWITH} from '#/lib/constants' -import {createFullHandle, isHandleReserved} from '#/lib/strings/handles' +import { + BSKY_SERVICE, + BSKY_SERVICE_DID, + PUBLIC_BSKY_SERVICE, +} from '#/lib/constants' +import {createFullHandle} from '#/lib/strings/handles' import {logger} from '#/logger' -import {useAgent} from '#/state/session' import {useDebouncedValue} from '#/components/live/utils' +import * as bsky from '#/types/bsky' -export const RQKEY_handleAvailability = (handle: string) => [ - 'handle-availability', - handle, -] +export const RQKEY_handleAvailability = ( + handle: string, + domain: string, + serviceDid: string, +) => ['handle-availability', handle, domain, serviceDid] export function useHandleAvailabilityQuery( - name: string, - domain: string, - enabled: boolean, + { + username, + serviceDomain, + serviceDid, + enabled, + birthDate, + email, + }: { + username: string + serviceDomain: string + serviceDid: string + enabled: boolean + birthDate?: string + email?: string + }, debounceDelayMs = 500, ) { - name = name.trim() - const handle = createFullHandle(name, domain) - const debouncedHandle = useDebouncedValue(handle, debounceDelayMs) - const agent = useAgent() + const name = username.trim() + const debouncedHandle = useDebouncedValue(name, debounceDelayMs) + const agent = useMemo(() => { + if (serviceDid === BSKY_SERVICE_DID) { + return new Agent({service: BSKY_SERVICE}) + } else { + return new Agent({service: PUBLIC_BSKY_SERVICE}) + } + }, [serviceDid]) return useQuery({ - enabled: enabled && handle === debouncedHandle, - queryKey: RQKEY_handleAvailability(debouncedHandle), + enabled: enabled && name === debouncedHandle, + queryKey: RQKEY_handleAvailability( + debouncedHandle, + serviceDomain, + serviceDid, + ), queryFn: async () => { - const frontSegment = debouncedHandle.split('.')[0] - if ( - domain === BSKY_SERVICE_HANDLE_ENDSWITH && - isHandleReserved(frontSegment) - ) { + const handle = createFullHandle(name, serviceDomain) + if (serviceDid === BSKY_SERVICE_DID) { + // entryway has a special API for handle availability + const {data} = await agent.com.atproto.temp.checkHandleAvailability({ + handle, + birthDate, + email, + }) + + if ( + bsky.dangerousIsType( + data.result, + ComAtprotoTempCheckHandleAvailability.isResultAvailable, + ) + ) { + return {available: true} as const + } else if ( + bsky.dangerousIsType( + data.result, + ComAtprotoTempCheckHandleAvailability.isResultUnavailable, + ) + ) { + return { + available: false, + suggestions: data.result.suggestions, + } as const + } else { + throw new Error( + `Unexpected result of \`checkHandleAvailability\`: ${JSON.stringify(data.result)}`, + ) + } + } else { + // 3rd party services won't have this API so just try and resolve the handle + try { + const res = await agent.resolveHandle({ + handle, + }) + + if (res.data.did) { + logger.metric( + 'signup:handleReserved', + {typeahead: true}, + {statsig: true}, + ) + return {available: false} as const + } + } catch {} logger.metric( - 'signup:handleReserved', + 'signup:handleAvailable', {typeahead: true}, {statsig: true}, ) - return {available: false, reason: 'reserved'} as const + return {available: true} as const } - try { - const res = await agent.resolveHandle({ - handle: debouncedHandle, - }) - - if (res.data.did) { - logger.metric( - 'signup:handleReserved', - {typeahead: true}, - {statsig: true}, - ) - return {available: false, reason: 'taken'} as const - } - } catch {} - logger.metric( - 'signup:handleAvailable', - {typeahead: true}, - {statsig: true}, - ) - return {available: true} as const }, }) } diff --git a/yarn.lock b/yarn.lock index 8079c90bfc..3595ee8fed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,10 +63,10 @@ "@atproto/xrpc" "^0.7.1" "@atproto/xrpc-server" "^0.9.0" -"@atproto/api@^0.15.26": - version "0.15.26" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.26.tgz#452019d6d0753d4caa0f7941e8e87e9f8bfbee52" - integrity sha512-AdXGjeCpLZiP9YMGi4YOdK1ayqkBhklmGfSG8UefqR6tTHth59PZvYs5KiwLnFhedt2Xljt3eUlhkn14Y48wEA== +"@atproto/api@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.16.0.tgz#55ea5200131dfe18ce2944042e16c6923ff9b606" + integrity sha512-PQHeae6mz/L1YirUslfci7bknfg3RrSZjXpYwzLICxIOvqGKIkOi0+qukC2Py238RhXRo8YZ9dCuole9HQBXDw== dependencies: "@atproto/common-web" "^0.4.2" "@atproto/lexicon" "^0.4.12" @@ -94,14 +94,14 @@ multiformats "^9.9.0" uint8arrays "3.0.0" -"@atproto/bsky@^0.0.172": - version "0.0.172" - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.172.tgz#963e3e2bc661e05c03fcb58788b2621d1afa6a3e" - integrity sha512-P/oDJ4i4TuRBV8pQg8hht147jl+OruDYAXdVmUaHRMWPnry+godnfp6vzph9W9Y03DN+G320Z63dCfvFZquSUQ== +"@atproto/bsky@^0.0.175": + version "0.0.175" + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.175.tgz#e37671cba8014e9d6b4b41f229568fca8f3f6adf" + integrity sha512-i5UEwdaff3clAuL/+xBVFB1RuFDD/NviDUBCl35c5L1D2YcvjzwQPEn73fs+UKHMgH5J+TeYwnpOs8uckJpDQg== dependencies: "@atproto-labs/fetch-node" "0.1.9" "@atproto-labs/xrpc-utils" "0.0.17" - "@atproto/api" "^0.15.26" + "@atproto/api" "^0.16.0" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" "@atproto/did" "^0.1.5" @@ -129,6 +129,7 @@ jose "^5.0.1" key-encoder "^2.0.3" kysely "^0.22.0" + leo-profanity "^1.8.0" multiformats "^9.9.0" murmurhash "^2.0.1" p-queue "^6.6.2" @@ -221,20 +222,20 @@ "@noble/hashes" "^1.6.1" uint8arrays "3.0.0" -"@atproto/dev-env@^0.3.155": - version "0.3.155" - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.155.tgz#26f865b92e9241d3e39c1c81e38c8422f2df26e2" - integrity sha512-KNrArdTAfrZQsRsFnwLC1Vgqh27brD5G2ZI0E3qF9BXQw2iiCH1r46IAhhggugL6xo3VXYucmM6/HGX+bKyavw== +"@atproto/dev-env@^0.3.158": + version "0.3.158" + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.158.tgz#26666e462ea4a0bd6461b634a544041cedb70032" + integrity sha512-JllbsaEYH+abs5bTXMt2PcTu7TbHnZeGQRAXguUQZji3s3bdbNfCWsceCiKbR7O3w9X1JVFg1nz4OOl7CachSg== dependencies: - "@atproto/api" "^0.15.26" - "@atproto/bsky" "^0.0.172" + "@atproto/api" "^0.16.0" + "@atproto/bsky" "^0.0.175" "@atproto/bsync" "^0.0.20" "@atproto/common-web" "^0.4.2" "@atproto/crypto" "^0.4.4" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.12" - "@atproto/ozone" "^0.1.131" - "@atproto/pds" "^0.4.161" + "@atproto/ozone" "^0.1.133" + "@atproto/pds" "^0.4.163" "@atproto/sync" "^0.1.29" "@atproto/syntax" "^0.4.0" "@atproto/xrpc-server" "^0.9.0" @@ -350,12 +351,12 @@ "@atproto/jwk" "0.4.0" zod "^3.23.8" -"@atproto/ozone@^0.1.131": - version "0.1.131" - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.131.tgz#b097c0274f424afd6af4e891cf78c563395b65b3" - integrity sha512-xGp0KPK89SXwtXHYza5kYNkwizCdXvn1sq0+5gwR6U8qNrvw0ibxBVMgtHTmqo9633tGD0c0woza1j+r5adm1w== +"@atproto/ozone@^0.1.133": + version "0.1.133" + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.133.tgz#3c8c6a846b1ea1fced11b9ad799d795ad52a9c90" + integrity sha512-Ta9U7y8VCN7M9abo2W3SFtJpnEiid6ZP4UC4FVOXkgUSXHCU3kE/UdfIf9SyoZwpDJOjrElSj9GYXN/U8DWuIQ== dependencies: - "@atproto/api" "^0.15.26" + "@atproto/api" "^0.16.0" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" "@atproto/identity" "^0.4.8" @@ -380,14 +381,14 @@ undici "^6.14.1" ws "^8.12.0" -"@atproto/pds@^0.4.161": - version "0.4.161" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.161.tgz#1f74675d5d5e4ca56361c89926571646b4c641ee" - integrity sha512-az1PGUCwIEx/b1neWBh9lgv27nbtczinGazI/ccG5A0AOMps1MXpTIJgEz5yJ1c6mSrhZlP0sL1EO6uC89irNA== +"@atproto/pds@^0.4.163": + version "0.4.163" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.163.tgz#ec52e8194843ccdb08f7c28f16ce294a09d748b1" + integrity sha512-k3qgBCuVb1twc7D61qH9Z9j93uV1Mg7aq7qEOZ9I+BjiiIyyXECfwIAhw/U3EHL+lDVDmkAyir8F7H4t3j/QNQ== dependencies: "@atproto-labs/fetch-node" "0.1.9" "@atproto-labs/xrpc-utils" "0.0.17" - "@atproto/api" "^0.15.26" + "@atproto/api" "^0.16.0" "@atproto/aws" "^0.2.25" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" @@ -11877,6 +11878,11 @@ freeport-async@^2.0.0: resolved "https://registry.yarnpkg.com/freeport-async/-/freeport-async-2.0.0.tgz#6adf2ec0c629d11abff92836acd04b399135bab4" integrity sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ== +french-badwords-list@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/french-badwords-list/-/french-badwords-list-1.0.7.tgz#8ed2442cb20fa2781c38b5a75c944f8e91a73c08" + integrity sha512-H1ziKs2PJh2+UXZ9oCGJ/rRQpsI9NBykGf2Sc7WaKaj1OnWFuBXfsvANTdRcfVmOghGQaUmRyZ1hJOPbDpy04Q== + fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -14088,6 +14094,14 @@ launch-editor@^2.6.0: picocolors "^1.0.0" shell-quote "^1.7.3" +leo-profanity@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/leo-profanity/-/leo-profanity-1.8.0.tgz#2ce6522e27c093d02253cdb9755b74cc7ffccf90" + integrity sha512-TATupbZhT3oFCtsiEXzgXcLvW6cDPhDtN+0yQAibjwBdSw3WJFLKM0DZ18eEW8KeFTT10x8gCP+DDZz0cI4Bfg== + optionalDependencies: + french-badwords-list "^1.0.7" + russian-bad-words "^0.5.0" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -17575,6 +17589,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +russian-bad-words@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/russian-bad-words/-/russian-bad-words-0.5.0.tgz#d4abcdc55f7af37606cdbac44bcb9e0bde6c3855" + integrity sha512-euNvEYki6iYYpkNbeudW+lEMMYGEmN7EBwVF8ezlbv0bZoQpVYB7W10cCeUIGV7Ed50sJynLQ0c559q5iI0ejQ== + rxjs@^6.6.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"