Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement captcha (#2882)

* web height adjustment

border radius incase of dark/dim mismatch

rm country codes

adjust height

general form refactor

more form refactor

refactor form submission

activity indicator after finished

remove remaining phone stuff

adjust captcha height

adjust state to reflect switch

move handle to the second step

pass color scheme param

ts

ts

update state when captcha is complete

web views and callbacks

remove old state

allow specified hosts

replace phone verification with a webview

* remove log

* height adjustment

* few changes

* use the correct url

* remove some debug

* validate handle before continuing

* explicitly check if there is a did, dont rely on error

* rm throw

* update allowed hosts

* update redirect host for webview

* fix handle

* fix handle check

* adjust height for full challenge

authored by

Hailey and committed by
GitHub
fbdf4517 dc143d6a

+441 -793
+3 -4
package.json
··· 123 123 "js-sha256": "^0.9.0", 124 124 "jwt-decode": "^4.0.0", 125 125 "lande": "^1.0.10", 126 - "libphonenumber-js": "^1.10.53", 127 126 "lodash.chunk": "^4.2.0", 128 127 "lodash.debounce": "^4.0.8", 129 128 "lodash.isequal": "^4.5.0", ··· 137 136 "mobx": "^6.6.1", 138 137 "mobx-react-lite": "^3.4.0", 139 138 "mobx-utils": "^6.0.6", 140 - "nanoid": "^5.0.2", 139 + "nanoid": "^5.0.5", 141 140 "normalize-url": "^8.0.0", 142 141 "patch-package": "^6.5.1", 143 142 "postinstall-postinstall": "^2.1.0", ··· 164 163 "react-native-safe-area-context": "4.8.2", 165 164 "react-native-screens": "~3.29.0", 166 165 "react-native-svg": "14.1.0", 166 + "react-native-ui-text-view": "link:./modules/react-native-ui-text-view", 167 167 "react-native-url-polyfill": "^1.3.0", 168 168 "react-native-uuid": "^2.0.1", 169 169 "react-native-version-number": "^0.3.6", ··· 178 178 "tlds": "^1.234.0", 179 179 "use-deep-compare": "^1.1.0", 180 180 "zeego": "^1.6.2", 181 - "zod": "^3.20.2", 182 - "react-native-ui-text-view": "link:./modules/react-native-ui-text-view" 181 + "zod": "^3.20.2" 183 182 }, 184 183 "devDependencies": { 185 184 "@atproto/dev-env": "^0.2.28",
-256
src/lib/country-codes.ts
··· 1 - import {CountryCode} from 'libphonenumber-js' 2 - 3 - // ISO 3166-1 alpha-2 codes 4 - 5 - export interface CountryCodeMap { 6 - code2: CountryCode 7 - name: string 8 - } 9 - 10 - export const COUNTRY_CODES: CountryCodeMap[] = [ 11 - {code2: 'AF', name: 'Afghanistan (+93)'}, 12 - {code2: 'AX', name: 'Åland Islands (+358)'}, 13 - {code2: 'AL', name: 'Albania (+355)'}, 14 - {code2: 'DZ', name: 'Algeria (+213)'}, 15 - {code2: 'AS', name: 'American Samoa (+1)'}, 16 - {code2: 'AD', name: 'Andorra (+376)'}, 17 - {code2: 'AO', name: 'Angola (+244)'}, 18 - {code2: 'AI', name: 'Anguilla (+1)'}, 19 - {code2: 'AG', name: 'Antigua and Barbuda (+1)'}, 20 - {code2: 'AR', name: 'Argentina (+54)'}, 21 - {code2: 'AM', name: 'Armenia (+374)'}, 22 - {code2: 'AW', name: 'Aruba (+297)'}, 23 - {code2: 'AU', name: 'Australia (+61)'}, 24 - {code2: 'AT', name: 'Austria (+43)'}, 25 - {code2: 'AZ', name: 'Azerbaijan (+994)'}, 26 - {code2: 'BS', name: 'Bahamas (+1)'}, 27 - {code2: 'BH', name: 'Bahrain (+973)'}, 28 - {code2: 'BD', name: 'Bangladesh (+880)'}, 29 - {code2: 'BB', name: 'Barbados (+1)'}, 30 - {code2: 'BY', name: 'Belarus (+375)'}, 31 - {code2: 'BE', name: 'Belgium (+32)'}, 32 - {code2: 'BZ', name: 'Belize (+501)'}, 33 - {code2: 'BJ', name: 'Benin (+229)'}, 34 - {code2: 'BM', name: 'Bermuda (+1)'}, 35 - {code2: 'BT', name: 'Bhutan (+975)'}, 36 - {code2: 'BO', name: 'Bolivia (Plurinational State of) (+591)'}, 37 - {code2: 'BQ', name: 'Bonaire, Sint Eustatius and Saba (+599)'}, 38 - {code2: 'BA', name: 'Bosnia and Herzegovina (+387)'}, 39 - {code2: 'BW', name: 'Botswana (+267)'}, 40 - {code2: 'BR', name: 'Brazil (+55)'}, 41 - {code2: 'IO', name: 'British Indian Ocean Territory (+246)'}, 42 - {code2: 'BN', name: 'Brunei Darussalam (+673)'}, 43 - {code2: 'BG', name: 'Bulgaria (+359)'}, 44 - {code2: 'BF', name: 'Burkina Faso (+226)'}, 45 - {code2: 'BI', name: 'Burundi (+257)'}, 46 - {code2: 'CV', name: 'Cabo Verde (+238)'}, 47 - {code2: 'KH', name: 'Cambodia (+855)'}, 48 - {code2: 'CM', name: 'Cameroon (+237)'}, 49 - {code2: 'CA', name: 'Canada (+1)'}, 50 - {code2: 'KY', name: 'Cayman Islands (+1)'}, 51 - {code2: 'CF', name: 'Central African Republic (+236)'}, 52 - {code2: 'TD', name: 'Chad (+235)'}, 53 - {code2: 'CL', name: 'Chile (+56)'}, 54 - {code2: 'CN', name: 'China (+86)'}, 55 - {code2: 'CX', name: 'Christmas Island (+61)'}, 56 - {code2: 'CC', name: 'Cocos (Keeling) Islands (+61)'}, 57 - {code2: 'CO', name: 'Colombia (+57)'}, 58 - {code2: 'KM', name: 'Comoros (+269)'}, 59 - {code2: 'CG', name: 'Congo (+242)'}, 60 - {code2: 'CD', name: 'Congo, Democratic Republic of the (+243)'}, 61 - {code2: 'CK', name: 'Cook Islands (+682)'}, 62 - {code2: 'CR', name: 'Costa Rica (+506)'}, 63 - {code2: 'CI', name: "Côte d'Ivoire (+225)"}, 64 - {code2: 'HR', name: 'Croatia (+385)'}, 65 - {code2: 'CU', name: 'Cuba (+53)'}, 66 - {code2: 'CW', name: 'Curaçao (+599)'}, 67 - {code2: 'CY', name: 'Cyprus (+357)'}, 68 - {code2: 'CZ', name: 'Czechia (+420)'}, 69 - {code2: 'DK', name: 'Denmark (+45)'}, 70 - {code2: 'DJ', name: 'Djibouti (+253)'}, 71 - {code2: 'DM', name: 'Dominica (+1)'}, 72 - {code2: 'DO', name: 'Dominican Republic (+1)'}, 73 - {code2: 'EC', name: 'Ecuador (+593)'}, 74 - {code2: 'EG', name: 'Egypt (+20)'}, 75 - {code2: 'SV', name: 'El Salvador (+503)'}, 76 - {code2: 'GQ', name: 'Equatorial Guinea (+240)'}, 77 - {code2: 'ER', name: 'Eritrea (+291)'}, 78 - {code2: 'EE', name: 'Estonia (+372)'}, 79 - {code2: 'SZ', name: 'Eswatini (+268)'}, 80 - {code2: 'ET', name: 'Ethiopia (+251)'}, 81 - {code2: 'FK', name: 'Falkland Islands (Malvinas) (+500)'}, 82 - {code2: 'FO', name: 'Faroe Islands (+298)'}, 83 - {code2: 'FJ', name: 'Fiji (+679)'}, 84 - {code2: 'FI', name: 'Finland (+358)'}, 85 - {code2: 'FR', name: 'France (+33)'}, 86 - {code2: 'GF', name: 'French Guiana (+594)'}, 87 - {code2: 'PF', name: 'French Polynesia (+689)'}, 88 - {code2: 'GA', name: 'Gabon (+241)'}, 89 - {code2: 'GM', name: 'Gambia (+220)'}, 90 - {code2: 'GE', name: 'Georgia (+995)'}, 91 - {code2: 'DE', name: 'Germany (+49)'}, 92 - {code2: 'GH', name: 'Ghana (+233)'}, 93 - {code2: 'GI', name: 'Gibraltar (+350)'}, 94 - {code2: 'GR', name: 'Greece (+30)'}, 95 - {code2: 'GL', name: 'Greenland (+299)'}, 96 - {code2: 'GD', name: 'Grenada (+1)'}, 97 - {code2: 'GP', name: 'Guadeloupe (+590)'}, 98 - {code2: 'GU', name: 'Guam (+1)'}, 99 - {code2: 'GT', name: 'Guatemala (+502)'}, 100 - {code2: 'GG', name: 'Guernsey (+44)'}, 101 - {code2: 'GN', name: 'Guinea (+224)'}, 102 - {code2: 'GW', name: 'Guinea-Bissau (+245)'}, 103 - {code2: 'GY', name: 'Guyana (+592)'}, 104 - {code2: 'HT', name: 'Haiti (+509)'}, 105 - {code2: 'VA', name: 'Holy See (+39)'}, 106 - {code2: 'HN', name: 'Honduras (+504)'}, 107 - {code2: 'HK', name: 'Hong Kong (+852)'}, 108 - {code2: 'HU', name: 'Hungary (+36)'}, 109 - {code2: 'IS', name: 'Iceland (+354)'}, 110 - {code2: 'IN', name: 'India (+91)'}, 111 - {code2: 'ID', name: 'Indonesia (+62)'}, 112 - {code2: 'IR', name: 'Iran (Islamic Republic of) (+98)'}, 113 - {code2: 'IQ', name: 'Iraq (+964)'}, 114 - {code2: 'IE', name: 'Ireland (+353)'}, 115 - {code2: 'IM', name: 'Isle of Man (+44)'}, 116 - {code2: 'IL', name: 'Israel (+972)'}, 117 - {code2: 'IT', name: 'Italy (+39)'}, 118 - {code2: 'JM', name: 'Jamaica (+1)'}, 119 - {code2: 'JP', name: 'Japan (+81)'}, 120 - {code2: 'JE', name: 'Jersey (+44)'}, 121 - {code2: 'JO', name: 'Jordan (+962)'}, 122 - {code2: 'KZ', name: 'Kazakhstan (+7)'}, 123 - {code2: 'KE', name: 'Kenya (+254)'}, 124 - {code2: 'KI', name: 'Kiribati (+686)'}, 125 - {code2: 'KP', name: "Korea (Democratic People's Republic of) (+850)"}, 126 - {code2: 'KR', name: 'Korea, Republic of (+82)'}, 127 - {code2: 'KW', name: 'Kuwait (+965)'}, 128 - {code2: 'KG', name: 'Kyrgyzstan (+996)'}, 129 - {code2: 'LA', name: "Lao People's Democratic Republic (+856)"}, 130 - {code2: 'LV', name: 'Latvia (+371)'}, 131 - {code2: 'LB', name: 'Lebanon (+961)'}, 132 - {code2: 'LS', name: 'Lesotho (+266)'}, 133 - {code2: 'LR', name: 'Liberia (+231)'}, 134 - {code2: 'LY', name: 'Libya (+218)'}, 135 - {code2: 'LI', name: 'Liechtenstein (+423)'}, 136 - {code2: 'LT', name: 'Lithuania (+370)'}, 137 - {code2: 'LU', name: 'Luxembourg (+352)'}, 138 - {code2: 'MO', name: 'Macao (+853)'}, 139 - {code2: 'MG', name: 'Madagascar (+261)'}, 140 - {code2: 'MW', name: 'Malawi (+265)'}, 141 - {code2: 'MY', name: 'Malaysia (+60)'}, 142 - {code2: 'MV', name: 'Maldives (+960)'}, 143 - {code2: 'ML', name: 'Mali (+223)'}, 144 - {code2: 'MT', name: 'Malta (+356)'}, 145 - {code2: 'MH', name: 'Marshall Islands (+692)'}, 146 - {code2: 'MQ', name: 'Martinique (+596)'}, 147 - {code2: 'MR', name: 'Mauritania (+222)'}, 148 - {code2: 'MU', name: 'Mauritius (+230)'}, 149 - {code2: 'YT', name: 'Mayotte (+262)'}, 150 - {code2: 'MX', name: 'Mexico (+52)'}, 151 - {code2: 'FM', name: 'Micronesia (Federated States of) (+691)'}, 152 - {code2: 'MD', name: 'Moldova, Republic of (+373)'}, 153 - {code2: 'MC', name: 'Monaco (+377)'}, 154 - {code2: 'MN', name: 'Mongolia (+976)'}, 155 - {code2: 'ME', name: 'Montenegro (+382)'}, 156 - {code2: 'MS', name: 'Montserrat (+1)'}, 157 - {code2: 'MA', name: 'Morocco (+212)'}, 158 - {code2: 'MZ', name: 'Mozambique (+258)'}, 159 - {code2: 'MM', name: 'Myanmar (+95)'}, 160 - {code2: 'NA', name: 'Namibia (+264)'}, 161 - {code2: 'NR', name: 'Nauru (+674)'}, 162 - {code2: 'NP', name: 'Nepal (+977)'}, 163 - {code2: 'NL', name: 'Netherlands, Kingdom of the (+31)'}, 164 - {code2: 'NC', name: 'New Caledonia (+687)'}, 165 - {code2: 'NZ', name: 'New Zealand (+64)'}, 166 - {code2: 'NI', name: 'Nicaragua (+505)'}, 167 - {code2: 'NE', name: 'Niger (+227)'}, 168 - {code2: 'NG', name: 'Nigeria (+234)'}, 169 - {code2: 'NU', name: 'Niue (+683)'}, 170 - {code2: 'NF', name: 'Norfolk Island (+672)'}, 171 - {code2: 'MK', name: 'North Macedonia (+389)'}, 172 - {code2: 'MP', name: 'Northern Mariana Islands (+1)'}, 173 - {code2: 'NO', name: 'Norway (+47)'}, 174 - {code2: 'OM', name: 'Oman (+968)'}, 175 - {code2: 'PK', name: 'Pakistan (+92)'}, 176 - {code2: 'PW', name: 'Palau (+680)'}, 177 - {code2: 'PS', name: 'Palestine, State of (+970)'}, 178 - {code2: 'PA', name: 'Panama (+507)'}, 179 - {code2: 'PG', name: 'Papua New Guinea (+675)'}, 180 - {code2: 'PY', name: 'Paraguay (+595)'}, 181 - {code2: 'PE', name: 'Peru (+51)'}, 182 - {code2: 'PH', name: 'Philippines (+63)'}, 183 - {code2: 'PL', name: 'Poland (+48)'}, 184 - {code2: 'PT', name: 'Portugal (+351)'}, 185 - {code2: 'PR', name: 'Puerto Rico (+1)'}, 186 - {code2: 'QA', name: 'Qatar (+974)'}, 187 - {code2: 'RE', name: 'Réunion (+262)'}, 188 - {code2: 'RO', name: 'Romania (+40)'}, 189 - {code2: 'RU', name: 'Russian Federation (+7)'}, 190 - {code2: 'RW', name: 'Rwanda (+250)'}, 191 - {code2: 'BL', name: 'Saint Barthélemy (+590)'}, 192 - {code2: 'SH', name: 'Saint Helena, Ascension and Tristan da Cunha (+290)'}, 193 - {code2: 'KN', name: 'Saint Kitts and Nevis (+1)'}, 194 - {code2: 'LC', name: 'Saint Lucia (+1)'}, 195 - {code2: 'MF', name: 'Saint Martin (French part) (+590)'}, 196 - {code2: 'PM', name: 'Saint Pierre and Miquelon (+508)'}, 197 - {code2: 'VC', name: 'Saint Vincent and the Grenadines (+1)'}, 198 - {code2: 'WS', name: 'Samoa (+685)'}, 199 - {code2: 'SM', name: 'San Marino (+378)'}, 200 - {code2: 'ST', name: 'Sao Tome and Principe (+239)'}, 201 - {code2: 'SA', name: 'Saudi Arabia (+966)'}, 202 - {code2: 'SN', name: 'Senegal (+221)'}, 203 - {code2: 'RS', name: 'Serbia (+381)'}, 204 - {code2: 'SC', name: 'Seychelles (+248)'}, 205 - {code2: 'SL', name: 'Sierra Leone (+232)'}, 206 - {code2: 'SG', name: 'Singapore (+65)'}, 207 - {code2: 'SX', name: 'Sint Maarten (Dutch part) (+1)'}, 208 - {code2: 'SK', name: 'Slovakia (+421)'}, 209 - {code2: 'SI', name: 'Slovenia (+386)'}, 210 - {code2: 'SB', name: 'Solomon Islands (+677)'}, 211 - {code2: 'SO', name: 'Somalia (+252)'}, 212 - {code2: 'ZA', name: 'South Africa (+27)'}, 213 - {code2: 'SS', name: 'South Sudan (+211)'}, 214 - {code2: 'ES', name: 'Spain (+34)'}, 215 - {code2: 'LK', name: 'Sri Lanka (+94)'}, 216 - {code2: 'SD', name: 'Sudan (+249)'}, 217 - {code2: 'SR', name: 'Suriname (+597)'}, 218 - {code2: 'SJ', name: 'Svalbard and Jan Mayen (+47)'}, 219 - {code2: 'SE', name: 'Sweden (+46)'}, 220 - {code2: 'CH', name: 'Switzerland (+41)'}, 221 - {code2: 'SY', name: 'Syrian Arab Republic (+963)'}, 222 - {code2: 'TW', name: 'Taiwan (+886)'}, 223 - {code2: 'TJ', name: 'Tajikistan (+992)'}, 224 - {code2: 'TZ', name: 'Tanzania, United Republic of (+255)'}, 225 - {code2: 'TH', name: 'Thailand (+66)'}, 226 - {code2: 'TL', name: 'Timor-Leste (+670)'}, 227 - {code2: 'TG', name: 'Togo (+228)'}, 228 - {code2: 'TK', name: 'Tokelau (+690)'}, 229 - {code2: 'TO', name: 'Tonga (+676)'}, 230 - {code2: 'TT', name: 'Trinidad and Tobago (+1)'}, 231 - {code2: 'TN', name: 'Tunisia (+216)'}, 232 - {code2: 'TR', name: 'Türkiye (+90)'}, 233 - {code2: 'TM', name: 'Turkmenistan (+993)'}, 234 - {code2: 'TC', name: 'Turks and Caicos Islands (+1)'}, 235 - {code2: 'TV', name: 'Tuvalu (+688)'}, 236 - {code2: 'UG', name: 'Uganda (+256)'}, 237 - {code2: 'UA', name: 'Ukraine (+380)'}, 238 - {code2: 'AE', name: 'United Arab Emirates (+971)'}, 239 - { 240 - code2: 'GB', 241 - name: 'United Kingdom of Great Britain and Northern Ireland (+44)', 242 - }, 243 - {code2: 'US', name: 'United States of America (+1)'}, 244 - {code2: 'UY', name: 'Uruguay (+598)'}, 245 - {code2: 'UZ', name: 'Uzbekistan (+998)'}, 246 - {code2: 'VU', name: 'Vanuatu (+678)'}, 247 - {code2: 'VE', name: 'Venezuela (Bolivarian Republic of) (+58)'}, 248 - {code2: 'VN', name: 'Viet Nam (+84)'}, 249 - {code2: 'VG', name: 'Virgin Islands (British) (+1)'}, 250 - {code2: 'VI', name: 'Virgin Islands (U.S.) (+1)'}, 251 - {code2: 'WF', name: 'Wallis and Futuna (+681)'}, 252 - {code2: 'EH', name: 'Western Sahara (+212)'}, 253 - {code2: 'YE', name: 'Yemen (+967)'}, 254 - {code2: 'ZM', name: 'Zambia (+260)'}, 255 - {code2: 'ZW', name: 'Zimbabwe (+263)'}, 256 - ]
+86
src/view/com/auth/create/CaptchaWebView.tsx
··· 1 + import React from 'react' 2 + import {WebView, WebViewNavigation} from 'react-native-webview' 3 + import {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes' 4 + import {StyleSheet} from 'react-native' 5 + import {CreateAccountState} from 'view/com/auth/create/state' 6 + 7 + const ALLOWED_HOSTS = [ 8 + 'bsky.social', 9 + 'bsky.app', 10 + 'staging.bsky.app', 11 + 'staging.bsky.dev', 12 + 'js.hcaptcha.com', 13 + 'newassets.hcaptcha.com', 14 + 'api2.hcaptcha.com', 15 + ] 16 + 17 + export function CaptchaWebView({ 18 + url, 19 + stateParam, 20 + uiState, 21 + onSuccess, 22 + onError, 23 + }: { 24 + url: string 25 + stateParam: string 26 + uiState?: CreateAccountState 27 + onSuccess: (code: string) => void 28 + onError: () => void 29 + }) { 30 + const redirectHost = React.useMemo(() => { 31 + if (!uiState?.serviceUrl) return 'bsky.app' 32 + 33 + return uiState?.serviceUrl && 34 + new URL(uiState?.serviceUrl).host === 'staging.bsky.dev' 35 + ? 'staging.bsky.app' 36 + : 'bsky.app' 37 + }, [uiState?.serviceUrl]) 38 + 39 + const wasSuccessful = React.useRef(false) 40 + 41 + const onShouldStartLoadWithRequest = React.useCallback( 42 + (event: ShouldStartLoadRequest) => { 43 + const urlp = new URL(event.url) 44 + return ALLOWED_HOSTS.includes(urlp.host) 45 + }, 46 + [], 47 + ) 48 + 49 + const onNavigationStateChange = React.useCallback( 50 + (e: WebViewNavigation) => { 51 + if (wasSuccessful.current) return 52 + 53 + const urlp = new URL(e.url) 54 + if (urlp.host !== redirectHost) return 55 + 56 + const code = urlp.searchParams.get('code') 57 + if (urlp.searchParams.get('state') !== stateParam || !code) { 58 + onError() 59 + return 60 + } 61 + 62 + wasSuccessful.current = true 63 + onSuccess(code) 64 + }, 65 + [redirectHost, stateParam, onSuccess, onError], 66 + ) 67 + 68 + return ( 69 + <WebView 70 + source={{uri: url}} 71 + javaScriptEnabled 72 + style={styles.webview} 73 + onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} 74 + onNavigationStateChange={onNavigationStateChange} 75 + scrollEnabled={false} 76 + /> 77 + ) 78 + } 79 + 80 + const styles = StyleSheet.create({ 81 + webview: { 82 + flex: 1, 83 + backgroundColor: 'transparent', 84 + borderRadius: 10, 85 + }, 86 + })
+61
src/view/com/auth/create/CaptchaWebView.web.tsx
··· 1 + import React from 'react' 2 + import {StyleSheet} from 'react-native' 3 + 4 + // @ts-ignore web only, we will always redirect to the app on web (CORS) 5 + const REDIRECT_HOST = new URL(window.location.href).host 6 + 7 + export function CaptchaWebView({ 8 + url, 9 + stateParam, 10 + onSuccess, 11 + onError, 12 + }: { 13 + url: string 14 + stateParam: string 15 + onSuccess: (code: string) => void 16 + onError: () => void 17 + }) { 18 + const onLoad = React.useCallback(() => { 19 + // @ts-ignore web 20 + const frame: HTMLIFrameElement = document.getElementById( 21 + 'captcha-iframe', 22 + ) as HTMLIFrameElement 23 + 24 + try { 25 + // @ts-ignore web 26 + const href = frame?.contentWindow?.location.href 27 + if (!href) return 28 + const urlp = new URL(href) 29 + 30 + // This shouldn't happen with CORS protections, but for good measure 31 + if (urlp.host !== REDIRECT_HOST) return 32 + 33 + const code = urlp.searchParams.get('code') 34 + if (urlp.searchParams.get('state') !== stateParam || !code) { 35 + onError() 36 + return 37 + } 38 + onSuccess(code) 39 + } catch (e) { 40 + // We don't need to handle this 41 + } 42 + }, [stateParam, onSuccess, onError]) 43 + 44 + return ( 45 + <iframe 46 + src={url} 47 + style={styles.iframe} 48 + id="captcha-iframe" 49 + onLoad={onLoad} 50 + /> 51 + ) 52 + } 53 + 54 + const styles = StyleSheet.create({ 55 + iframe: { 56 + flex: 1, 57 + borderWidth: 0, 58 + borderRadius: 10, 59 + backgroundColor: 'transparent', 60 + }, 61 + })
+39 -32
src/view/com/auth/create/CreateAccount.tsx
··· 13 13 import {usePalette} from 'lib/hooks/usePalette' 14 14 import {msg, Trans} from '@lingui/macro' 15 15 import {useLingui} from '@lingui/react' 16 - import {useOnboardingDispatch} from '#/state/shell' 17 - import {useSessionApi} from '#/state/session' 18 - import {useCreateAccount, submit} from './state' 16 + import {useCreateAccount, useSubmitCreateAccount} from './state' 19 17 import {useServiceQuery} from '#/state/queries/service' 20 - import { 21 - usePreferencesSetBirthDateMutation, 22 - useSetSaveFeedsMutation, 23 - DEFAULT_PROD_FEEDS, 24 - } from '#/state/queries/preferences' 25 - import {FEEDBACK_FORM_URL, HITSLOP_10, IS_PROD} from '#/lib/constants' 18 + import {FEEDBACK_FORM_URL, HITSLOP_10} from '#/lib/constants' 26 19 27 20 import {Step1} from './Step1' 28 21 import {Step2} from './Step2' 29 22 import {Step3} from './Step3' 30 23 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 31 24 import {TextLink} from '../../util/Link' 25 + import {getAgent} from 'state/session' 26 + import {createFullHandle} from 'lib/strings/handles' 32 27 33 28 export function CreateAccount({onPressBack}: {onPressBack: () => void}) { 34 29 const {screen} = useAnalytics() 35 30 const pal = usePalette('default') 36 31 const {_} = useLingui() 37 32 const [uiState, uiDispatch] = useCreateAccount() 38 - const onboardingDispatch = useOnboardingDispatch() 39 - const {createAccount} = useSessionApi() 40 - const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation() 41 - const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() 42 33 const {isTabletOrDesktop} = useWebMediaQueries() 34 + const submit = useSubmitCreateAccount(uiState, uiDispatch) 43 35 44 36 React.useEffect(() => { 45 37 screen('CreateAccount') ··· 84 76 if (!uiState.canNext) { 85 77 return 86 78 } 87 - if (uiState.step < 3) { 88 - uiDispatch({type: 'next'}) 89 - } else { 79 + 80 + if (uiState.step === 2) { 81 + uiDispatch({type: 'set-processing', value: true}) 90 82 try { 91 - await submit({ 92 - onboardingDispatch, 93 - createAccount, 94 - uiState, 95 - uiDispatch, 96 - _, 83 + const res = await getAgent().resolveHandle({ 84 + handle: createFullHandle(uiState.handle, uiState.userDomain), 97 85 }) 98 - setBirthDate({birthDate: uiState.birthDate}) 99 - if (IS_PROD(uiState.serviceUrl)) { 100 - setSavedFeeds(DEFAULT_PROD_FEEDS) 86 + 87 + if (res.data.did) { 88 + uiDispatch({ 89 + type: 'set-error', 90 + value: _(msg`That handle is already taken.`), 91 + }) 92 + return 93 + } 94 + } catch (e) { 95 + // Don't need to handle 96 + } finally { 97 + uiDispatch({type: 'set-processing', value: false}) 98 + } 99 + 100 + if (!uiState.isCaptchaRequired) { 101 + try { 102 + await submit() 103 + } catch { 104 + // dont need to handle here 101 105 } 102 - } catch { 103 - // dont need to handle here 106 + // We don't need to go to the next page if there wasn't a captcha required 107 + return 104 108 } 105 109 } 110 + 111 + uiDispatch({type: 'next'}) 106 112 }, [ 107 - uiState, 113 + uiState.canNext, 114 + uiState.step, 115 + uiState.isCaptchaRequired, 116 + uiState.handle, 117 + uiState.userDomain, 108 118 uiDispatch, 109 - onboardingDispatch, 110 - createAccount, 111 - setBirthDate, 112 - setSavedFeeds, 113 119 _, 120 + submit, 114 121 ]) 115 122 116 123 // rendering
+5 -4
src/view/com/auth/create/Step1.tsx
··· 73 73 /> 74 74 <StepHeader uiState={uiState} title={_(msg`Your account`)} /> 75 75 76 + {uiState.error ? ( 77 + <ErrorMessage message={uiState.error} style={styles.error} /> 78 + ) : undefined} 79 + 76 80 <View style={s.pb20}> 77 81 <Text type="md-medium" style={[pal.text, s.mb2]}> 78 82 <Trans>Hosting provider</Trans> ··· 259 263 )} 260 264 </> 261 265 )} 262 - {uiState.error ? ( 263 - <ErrorMessage message={uiState.error} style={styles.error} /> 264 - ) : undefined} 265 266 </View> 266 267 ) 267 268 } ··· 269 270 const styles = StyleSheet.create({ 270 271 error: { 271 272 borderRadius: 6, 272 - marginTop: 10, 273 + marginBottom: 10, 273 274 }, 274 275 dateInputButton: { 275 276 borderWidth: 1,
+32 -276
src/view/com/auth/create/Step2.tsx
··· 1 1 import React from 'react' 2 - import { 3 - ActivityIndicator, 4 - StyleSheet, 5 - TouchableWithoutFeedback, 6 - View, 7 - } from 'react-native' 8 - import RNPickerSelect from 'react-native-picker-select' 9 - import { 10 - CreateAccountState, 11 - CreateAccountDispatch, 12 - requestVerificationCode, 13 - } from './state' 2 + import {StyleSheet, View} from 'react-native' 3 + import {CreateAccountState, CreateAccountDispatch} from './state' 14 4 import {Text} from 'view/com/util/text/Text' 15 5 import {StepHeader} from './StepHeader' 16 6 import {s} from 'lib/styles' 17 - import {usePalette} from 'lib/hooks/usePalette' 18 7 import {TextInput} from '../util/TextInput' 19 - import {Button} from '../../util/forms/Button' 8 + import {createFullHandle} from 'lib/strings/handles' 9 + import {usePalette} from 'lib/hooks/usePalette' 20 10 import {ErrorMessage} from 'view/com/util/error/ErrorMessage' 21 - import {isAndroid, isWeb} from 'platform/detection' 22 - import {Trans, msg} from '@lingui/macro' 11 + import {msg, Trans} from '@lingui/macro' 23 12 import {useLingui} from '@lingui/react' 24 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 25 - import parsePhoneNumber from 'libphonenumber-js' 26 - import {COUNTRY_CODES} from '#/lib/country-codes' 27 - import { 28 - FontAwesomeIcon, 29 - FontAwesomeIconStyle, 30 - } from '@fortawesome/react-native-fontawesome' 31 - import {HITSLOP_10} from '#/lib/constants' 32 13 14 + /** STEP 3: Your user handle 15 + * @field User handle 16 + */ 33 17 export function Step2({ 34 18 uiState, 35 19 uiDispatch, ··· 39 23 }) { 40 24 const pal = usePalette('default') 41 25 const {_} = useLingui() 42 - const {isMobile} = useWebMediaQueries() 43 - 44 - const onPressRequest = React.useCallback(() => { 45 - const phoneNumber = parsePhoneNumber( 46 - uiState.verificationPhone, 47 - uiState.phoneCountry, 48 - ) 49 - if (phoneNumber && phoneNumber.isValid()) { 50 - requestVerificationCode({uiState, uiDispatch, _}) 51 - } else { 52 - uiDispatch({ 53 - type: 'set-error', 54 - value: _( 55 - msg`There's something wrong with this number. Please choose your country and enter your full phone number!`, 56 - ), 57 - }) 58 - } 59 - }, [uiState, uiDispatch, _]) 60 - 61 - const onPressRetry = React.useCallback(() => { 62 - uiDispatch({type: 'set-has-requested-verification-code', value: false}) 63 - }, [uiDispatch]) 64 - 65 - const phoneNumberFormatted = React.useMemo( 66 - () => 67 - uiState.hasRequestedVerificationCode 68 - ? parsePhoneNumber( 69 - uiState.verificationPhone, 70 - uiState.phoneCountry, 71 - )?.formatInternational() 72 - : '', 73 - [ 74 - uiState.hasRequestedVerificationCode, 75 - uiState.verificationPhone, 76 - uiState.phoneCountry, 77 - ], 78 - ) 79 - 80 26 return ( 81 27 <View> 82 - <StepHeader uiState={uiState} title={_(msg`SMS verification`)} /> 83 - 84 - {!uiState.hasRequestedVerificationCode ? ( 85 - <> 86 - <View style={s.pb10}> 87 - <Text 88 - type="md-medium" 89 - style={[pal.text, s.mb2]} 90 - nativeID="phoneCountry"> 91 - <Trans>Country</Trans> 92 - </Text> 93 - <View 94 - style={[ 95 - {position: 'relative'}, 96 - isAndroid && { 97 - borderWidth: 1, 98 - borderColor: pal.border.borderColor, 99 - borderRadius: 4, 100 - }, 101 - ]}> 102 - <RNPickerSelect 103 - placeholder={{}} 104 - value={uiState.phoneCountry} 105 - onValueChange={value => 106 - uiDispatch({type: 'set-phone-country', value}) 107 - } 108 - items={COUNTRY_CODES.filter(l => Boolean(l.code2)).map(l => ({ 109 - label: l.name, 110 - value: l.code2, 111 - key: l.code2, 112 - }))} 113 - style={{ 114 - inputAndroid: { 115 - backgroundColor: pal.view.backgroundColor, 116 - color: pal.text.color, 117 - fontSize: 21, 118 - letterSpacing: 0.5, 119 - fontWeight: '500', 120 - paddingHorizontal: 14, 121 - paddingVertical: 8, 122 - borderRadius: 4, 123 - }, 124 - inputIOS: { 125 - backgroundColor: pal.view.backgroundColor, 126 - color: pal.text.color, 127 - fontSize: 14, 128 - letterSpacing: 0.5, 129 - fontWeight: '500', 130 - paddingHorizontal: 14, 131 - paddingVertical: 8, 132 - borderWidth: 1, 133 - borderColor: pal.border.borderColor, 134 - borderRadius: 4, 135 - }, 136 - inputWeb: { 137 - // @ts-ignore web only 138 - cursor: 'pointer', 139 - '-moz-appearance': 'none', 140 - '-webkit-appearance': 'none', 141 - appearance: 'none', 142 - outline: 0, 143 - borderWidth: 1, 144 - borderColor: pal.border.borderColor, 145 - backgroundColor: pal.view.backgroundColor, 146 - color: pal.text.color, 147 - fontSize: 14, 148 - letterSpacing: 0.5, 149 - fontWeight: '500', 150 - paddingHorizontal: 14, 151 - paddingVertical: 8, 152 - borderRadius: 4, 153 - }, 154 - }} 155 - accessibilityLabel={_(msg`Select your phone's country`)} 156 - accessibilityHint="" 157 - accessibilityLabelledBy="phoneCountry" 158 - /> 159 - <View 160 - style={{ 161 - position: 'absolute', 162 - top: 1, 163 - right: 1, 164 - bottom: 1, 165 - width: 40, 166 - pointerEvents: 'none', 167 - alignItems: 'center', 168 - justifyContent: 'center', 169 - }}> 170 - <FontAwesomeIcon 171 - icon="chevron-down" 172 - style={pal.text as FontAwesomeIconStyle} 173 - /> 174 - </View> 175 - </View> 176 - </View> 177 - 178 - <View style={s.pb20}> 179 - <Text 180 - type="md-medium" 181 - style={[pal.text, s.mb2]} 182 - nativeID="phoneNumber"> 183 - <Trans>Phone number</Trans> 184 - </Text> 185 - <TextInput 186 - testID="phoneInput" 187 - icon="phone" 188 - placeholder={_(msg`Enter your phone number`)} 189 - value={uiState.verificationPhone} 190 - editable 191 - onChange={value => 192 - uiDispatch({type: 'set-verification-phone', value}) 193 - } 194 - accessibilityLabel={_(msg`Email`)} 195 - accessibilityHint={_( 196 - msg`Input phone number for SMS verification`, 197 - )} 198 - accessibilityLabelledBy="phoneNumber" 199 - keyboardType="phone-pad" 200 - autoCapitalize="none" 201 - autoComplete="tel" 202 - autoCorrect={false} 203 - autoFocus={true} 204 - /> 205 - <Text type="sm" style={[pal.textLight, s.mt5]}> 206 - <Trans> 207 - Please enter a phone number that can receive SMS text messages. 208 - </Trans> 209 - </Text> 210 - </View> 211 - 212 - <View style={isMobile ? {} : {flexDirection: 'row'}}> 213 - {uiState.isProcessing ? ( 214 - <ActivityIndicator /> 215 - ) : ( 216 - <Button 217 - testID="requestCodeBtn" 218 - type="primary" 219 - label={_(msg`Request code`)} 220 - labelStyle={isMobile ? [s.flex1, s.textCenter, s.f17] : []} 221 - style={ 222 - isMobile ? {paddingVertical: 12, paddingHorizontal: 20} : {} 223 - } 224 - onPress={onPressRequest} 225 - /> 226 - )} 227 - </View> 228 - </> 229 - ) : ( 230 - <> 231 - <View style={s.pb20}> 232 - <View 233 - style={[ 234 - s.flexRow, 235 - s.mb5, 236 - s.alignCenter, 237 - {justifyContent: 'space-between'}, 238 - ]}> 239 - <Text 240 - type="md-medium" 241 - style={pal.text} 242 - nativeID="verificationCode"> 243 - <Trans>Verification code</Trans>{' '} 244 - </Text> 245 - <TouchableWithoutFeedback 246 - onPress={onPressRetry} 247 - accessibilityLabel={_(msg`Retry.`)} 248 - accessibilityHint="" 249 - hitSlop={HITSLOP_10}> 250 - <View style={styles.touchable}> 251 - <Text 252 - type="md-medium" 253 - style={pal.link} 254 - nativeID="verificationCode"> 255 - <Trans>Retry</Trans> 256 - </Text> 257 - </View> 258 - </TouchableWithoutFeedback> 259 - </View> 260 - <TextInput 261 - testID="codeInput" 262 - icon="hashtag" 263 - placeholder={_(msg`XXXXXX`)} 264 - value={uiState.verificationCode} 265 - editable 266 - onChange={value => 267 - uiDispatch({type: 'set-verification-code', value}) 268 - } 269 - accessibilityLabel={_(msg`Email`)} 270 - accessibilityHint={_( 271 - msg`Input the verification code we have texted to you`, 272 - )} 273 - accessibilityLabelledBy="verificationCode" 274 - keyboardType="phone-pad" 275 - autoCapitalize="none" 276 - autoComplete="one-time-code" 277 - textContentType="oneTimeCode" 278 - autoCorrect={false} 279 - autoFocus={true} 280 - /> 281 - <Text type="sm" style={[pal.textLight, s.mt5]}> 282 - <Trans> 283 - Please enter the verification code sent to{' '} 284 - {phoneNumberFormatted}. 285 - </Trans> 286 - </Text> 287 - </View> 288 - </> 289 - )} 290 - 28 + <StepHeader uiState={uiState} title={_(msg`Your user handle`)} /> 291 29 {uiState.error ? ( 292 30 <ErrorMessage message={uiState.error} style={styles.error} /> 293 31 ) : undefined} 32 + <View style={s.pb10}> 33 + <TextInput 34 + testID="handleInput" 35 + icon="at" 36 + placeholder="e.g. alice" 37 + value={uiState.handle} 38 + editable 39 + autoFocus 40 + autoComplete="off" 41 + autoCorrect={false} 42 + onChange={value => uiDispatch({type: 'set-handle', value})} 43 + // TODO: Add explicit text label 44 + accessibilityLabel={_(msg`User handle`)} 45 + accessibilityHint={_(msg`Input your user handle`)} 46 + /> 47 + <Text type="lg" style={[pal.text, s.pl5, s.pt10]}> 48 + <Trans>Your full handle will be</Trans>{' '} 49 + <Text type="lg-bold" style={pal.text}> 50 + @{createFullHandle(uiState.handle, uiState.userDomain)} 51 + </Text> 52 + </Text> 53 + </View> 294 54 </View> 295 55 ) 296 56 } ··· 298 58 const styles = StyleSheet.create({ 299 59 error: { 300 60 borderRadius: 6, 301 - marginTop: 10, 302 - }, 303 - // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. 304 - touchable: { 305 - ...(isWeb && {cursor: 'pointer'}), 61 + marginBottom: 10, 306 62 }, 307 63 })
+86 -34
src/view/com/auth/create/Step3.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 - import {CreateAccountState, CreateAccountDispatch} from './state' 4 - import {Text} from 'view/com/util/text/Text' 2 + import {ActivityIndicator, StyleSheet, View} from 'react-native' 3 + import { 4 + CreateAccountState, 5 + CreateAccountDispatch, 6 + useSubmitCreateAccount, 7 + } from './state' 5 8 import {StepHeader} from './StepHeader' 6 - import {s} from 'lib/styles' 7 - import {TextInput} from '../util/TextInput' 8 - import {createFullHandle} from 'lib/strings/handles' 9 - import {usePalette} from 'lib/hooks/usePalette' 10 9 import {ErrorMessage} from 'view/com/util/error/ErrorMessage' 11 - import {msg, Trans} from '@lingui/macro' 10 + import {isWeb} from 'platform/detection' 11 + import {msg} from '@lingui/macro' 12 12 import {useLingui} from '@lingui/react' 13 13 14 - /** STEP 3: Your user handle 15 - * @field User handle 16 - */ 14 + import {nanoid} from 'nanoid/non-secure' 15 + import {CaptchaWebView} from 'view/com/auth/create/CaptchaWebView' 16 + import {useTheme} from 'lib/ThemeContext' 17 + import {createFullHandle} from 'lib/strings/handles' 18 + 19 + const CAPTCHA_PATH = '/gate/signup' 20 + 17 21 export function Step3({ 18 22 uiState, 19 23 uiDispatch, ··· 21 25 uiState: CreateAccountState 22 26 uiDispatch: CreateAccountDispatch 23 27 }) { 24 - const pal = usePalette('default') 25 28 const {_} = useLingui() 29 + const theme = useTheme() 30 + const submit = useSubmitCreateAccount(uiState, uiDispatch) 31 + 32 + const [completed, setCompleted] = React.useState(false) 33 + 34 + const stateParam = React.useMemo(() => nanoid(15), []) 35 + const url = React.useMemo(() => { 36 + const newUrl = new URL(uiState.serviceUrl) 37 + newUrl.pathname = CAPTCHA_PATH 38 + newUrl.searchParams.set( 39 + 'handle', 40 + createFullHandle(uiState.handle, uiState.userDomain), 41 + ) 42 + newUrl.searchParams.set('state', stateParam) 43 + newUrl.searchParams.set('colorScheme', theme.colorScheme) 44 + 45 + console.log(newUrl) 46 + 47 + return newUrl.href 48 + }, [ 49 + uiState.serviceUrl, 50 + uiState.handle, 51 + uiState.userDomain, 52 + stateParam, 53 + theme.colorScheme, 54 + ]) 55 + 56 + const onSuccess = React.useCallback( 57 + (code: string) => { 58 + setCompleted(true) 59 + submit(code) 60 + }, 61 + [submit], 62 + ) 63 + 64 + const onError = React.useCallback(() => { 65 + uiDispatch({ 66 + type: 'set-error', 67 + value: _(msg`Error receiving captcha response.`), 68 + }) 69 + }, [_, uiDispatch]) 70 + 26 71 return ( 27 72 <View> 28 - <StepHeader uiState={uiState} title={_(msg`Your user handle`)} /> 29 - <View style={s.pb10}> 30 - <TextInput 31 - testID="handleInput" 32 - icon="at" 33 - placeholder="e.g. alice" 34 - value={uiState.handle} 35 - editable 36 - autoFocus 37 - autoComplete="off" 38 - autoCorrect={false} 39 - onChange={value => uiDispatch({type: 'set-handle', value})} 40 - // TODO: Add explicit text label 41 - accessibilityLabel={_(msg`User handle`)} 42 - accessibilityHint={_(msg`Input your user handle`)} 43 - /> 44 - <Text type="lg" style={[pal.text, s.pl5, s.pt10]}> 45 - <Trans>Your full handle will be</Trans>{' '} 46 - <Text type="lg-bold" style={pal.text}> 47 - @{createFullHandle(uiState.handle, uiState.userDomain)} 48 - </Text> 49 - </Text> 73 + <StepHeader uiState={uiState} title={_(msg`Complete the challenge`)} /> 74 + <View style={[styles.container, completed && styles.center]}> 75 + {!completed ? ( 76 + <CaptchaWebView 77 + url={url} 78 + stateParam={stateParam} 79 + uiState={uiState} 80 + onSuccess={onSuccess} 81 + onError={onError} 82 + /> 83 + ) : ( 84 + <ActivityIndicator size="large" /> 85 + )} 50 86 </View> 87 + 51 88 {uiState.error ? ( 52 89 <ErrorMessage message={uiState.error} style={styles.error} /> 53 90 ) : undefined} ··· 58 95 const styles = StyleSheet.create({ 59 96 error: { 60 97 borderRadius: 6, 98 + marginTop: 10, 99 + }, 100 + // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. 101 + touchable: { 102 + ...(isWeb && {cursor: 'pointer'}), 103 + }, 104 + container: { 105 + minHeight: 500, 106 + width: '100%', 107 + paddingBottom: 20, 108 + overflow: 'hidden', 109 + }, 110 + center: { 111 + alignItems: 'center', 112 + justifyContent: 'center', 61 113 }, 62 114 })
+1 -1
src/view/com/auth/create/StepHeader.tsx
··· 11 11 children, 12 12 }: React.PropsWithChildren<{uiState: CreateAccountState; title: string}>) { 13 13 const pal = usePalette('default') 14 - const numSteps = uiState.isPhoneVerificationRequired ? 3 : 2 14 + const numSteps = 3 15 15 return ( 16 16 <View style={styles.container}> 17 17 <View>
+124 -177
src/view/com/auth/create/state.ts
··· 1 - import {useReducer} from 'react' 1 + import {useCallback, useReducer} from 'react' 2 2 import { 3 3 ComAtprotoServerDescribeServer, 4 4 ComAtprotoServerCreateAccount, 5 - BskyAgent, 6 5 } from '@atproto/api' 7 6 import {I18nContext, useLingui} from '@lingui/react' 8 7 import {msg} from '@lingui/macro' ··· 11 10 import {logger} from '#/logger' 12 11 import {createFullHandle} from '#/lib/strings/handles' 13 12 import {cleanError} from '#/lib/strings/errors' 14 - import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding' 15 - import {ApiContext as SessionApiContext} from '#/state/session' 16 - import {DEFAULT_SERVICE} from '#/lib/constants' 17 - import parsePhoneNumber, {CountryCode} from 'libphonenumber-js' 13 + import {useOnboardingDispatch} from '#/state/shell/onboarding' 14 + import {useSessionApi} from '#/state/session' 15 + import {DEFAULT_SERVICE, IS_PROD} from '#/lib/constants' 16 + import { 17 + DEFAULT_PROD_FEEDS, 18 + usePreferencesSetBirthDateMutation, 19 + useSetSaveFeedsMutation, 20 + } from 'state/queries/preferences' 18 21 19 22 export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema 20 23 const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago ··· 29 32 | {type: 'set-invite-code'; value: string} 30 33 | {type: 'set-email'; value: string} 31 34 | {type: 'set-password'; value: string} 32 - | {type: 'set-phone-country'; value: CountryCode} 33 - | {type: 'set-verification-phone'; value: string} 34 - | {type: 'set-verification-code'; value: string} 35 - | {type: 'set-has-requested-verification-code'; value: boolean} 36 35 | {type: 'set-handle'; value: string} 37 36 | {type: 'set-birth-date'; value: Date} 38 37 | {type: 'next'} ··· 49 48 inviteCode: string 50 49 email: string 51 50 password: string 52 - phoneCountry: CountryCode 53 - verificationPhone: string 54 - verificationCode: string 55 - hasRequestedVerificationCode: boolean 56 51 handle: string 57 52 birthDate: Date 58 53 ··· 60 55 canBack: boolean 61 56 canNext: boolean 62 57 isInviteCodeRequired: boolean 63 - isPhoneVerificationRequired: boolean 58 + isCaptchaRequired: boolean 64 59 } 65 60 66 61 export type CreateAccountDispatch = (action: CreateAccountAction) => void 67 62 68 63 export function useCreateAccount() { 69 64 const {_} = useLingui() 65 + 70 66 return useReducer(createReducer({_}), { 71 67 step: 1, 72 68 error: undefined, ··· 77 73 inviteCode: '', 78 74 email: '', 79 75 password: '', 80 - phoneCountry: 'US', 81 - verificationPhone: '', 82 - verificationCode: '', 83 - hasRequestedVerificationCode: false, 84 76 handle: '', 85 77 birthDate: DEFAULT_DATE, 86 78 87 79 canBack: false, 88 80 canNext: false, 89 81 isInviteCodeRequired: false, 90 - isPhoneVerificationRequired: false, 82 + isCaptchaRequired: false, 91 83 }) 92 84 } 93 85 94 - export async function requestVerificationCode({ 95 - uiState, 96 - uiDispatch, 97 - _, 98 - }: { 99 - uiState: CreateAccountState 100 - uiDispatch: CreateAccountDispatch 101 - _: I18nContext['_'] 102 - }) { 103 - const phoneNumber = parsePhoneNumber( 104 - uiState.verificationPhone, 105 - uiState.phoneCountry, 106 - )?.number 107 - if (!phoneNumber) { 108 - return 109 - } 110 - uiDispatch({type: 'set-error', value: ''}) 111 - uiDispatch({type: 'set-processing', value: true}) 112 - uiDispatch({type: 'set-verification-phone', value: phoneNumber}) 113 - try { 114 - const agent = new BskyAgent({service: uiState.serviceUrl}) 115 - await agent.com.atproto.temp.requestPhoneVerification({ 116 - phoneNumber, 117 - }) 118 - uiDispatch({type: 'set-has-requested-verification-code', value: true}) 119 - } catch (e: any) { 120 - logger.error( 121 - `Failed to request sms verification code (${e.status} status)`, 122 - {message: e}, 123 - ) 124 - uiDispatch({type: 'set-error', value: cleanError(e.toString())}) 125 - } 126 - uiDispatch({type: 'set-processing', value: false}) 127 - } 86 + export function useSubmitCreateAccount( 87 + uiState: CreateAccountState, 88 + uiDispatch: CreateAccountDispatch, 89 + ) { 90 + const {_} = useLingui() 91 + const {createAccount} = useSessionApi() 92 + const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation() 93 + const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() 94 + const onboardingDispatch = useOnboardingDispatch() 95 + 96 + return useCallback( 97 + async (verificationCode?: string) => { 98 + if (!uiState.email) { 99 + uiDispatch({type: 'set-step', value: 1}) 100 + console.log('no email?') 101 + return uiDispatch({ 102 + type: 'set-error', 103 + value: _(msg`Please enter your email.`), 104 + }) 105 + } 106 + if (!EmailValidator.validate(uiState.email)) { 107 + uiDispatch({type: 'set-step', value: 1}) 108 + return uiDispatch({ 109 + type: 'set-error', 110 + value: _(msg`Your email appears to be invalid.`), 111 + }) 112 + } 113 + if (!uiState.password) { 114 + uiDispatch({type: 'set-step', value: 1}) 115 + return uiDispatch({ 116 + type: 'set-error', 117 + value: _(msg`Please choose your password.`), 118 + }) 119 + } 120 + if (!uiState.handle) { 121 + uiDispatch({type: 'set-step', value: 2}) 122 + return uiDispatch({ 123 + type: 'set-error', 124 + value: _(msg`Please choose your handle.`), 125 + }) 126 + } 127 + if (uiState.isCaptchaRequired && !verificationCode) { 128 + uiDispatch({type: 'set-step', value: 3}) 129 + return uiDispatch({ 130 + type: 'set-error', 131 + value: _(msg`Please complete the verification captcha.`), 132 + }) 133 + } 134 + uiDispatch({type: 'set-error', value: ''}) 135 + uiDispatch({type: 'set-processing', value: true}) 128 136 129 - export async function submit({ 130 - createAccount, 131 - onboardingDispatch, 132 - uiState, 133 - uiDispatch, 134 - _, 135 - }: { 136 - createAccount: SessionApiContext['createAccount'] 137 - onboardingDispatch: OnboardingDispatchContext 138 - uiState: CreateAccountState 139 - uiDispatch: CreateAccountDispatch 140 - _: I18nContext['_'] 141 - }) { 142 - if (!uiState.email) { 143 - uiDispatch({type: 'set-step', value: 1}) 144 - return uiDispatch({ 145 - type: 'set-error', 146 - value: _(msg`Please enter your email.`), 147 - }) 148 - } 149 - if (!EmailValidator.validate(uiState.email)) { 150 - uiDispatch({type: 'set-step', value: 1}) 151 - return uiDispatch({ 152 - type: 'set-error', 153 - value: _(msg`Your email appears to be invalid.`), 154 - }) 155 - } 156 - if (!uiState.password) { 157 - uiDispatch({type: 'set-step', value: 1}) 158 - return uiDispatch({ 159 - type: 'set-error', 160 - value: _(msg`Please choose your password.`), 161 - }) 162 - } 163 - if ( 164 - uiState.isPhoneVerificationRequired && 165 - (!uiState.verificationPhone || !uiState.verificationCode) 166 - ) { 167 - uiDispatch({type: 'set-step', value: 2}) 168 - return uiDispatch({ 169 - type: 'set-error', 170 - value: _(msg`Please enter the code you received by SMS.`), 171 - }) 172 - } 173 - if (!uiState.handle) { 174 - uiDispatch({type: 'set-step', value: 3}) 175 - return uiDispatch({ 176 - type: 'set-error', 177 - value: _(msg`Please choose your handle.`), 178 - }) 179 - } 180 - uiDispatch({type: 'set-error', value: ''}) 181 - uiDispatch({type: 'set-processing', value: true}) 137 + try { 138 + onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view 139 + await createAccount({ 140 + service: uiState.serviceUrl, 141 + email: uiState.email, 142 + handle: createFullHandle(uiState.handle, uiState.userDomain), 143 + password: uiState.password, 144 + inviteCode: uiState.inviteCode.trim(), 145 + verificationCode: uiState.isCaptchaRequired 146 + ? verificationCode 147 + : undefined, 148 + }) 149 + setBirthDate({birthDate: uiState.birthDate}) 150 + if (IS_PROD(uiState.serviceUrl)) { 151 + setSavedFeeds(DEFAULT_PROD_FEEDS) 152 + } 153 + } catch (e: any) { 154 + onboardingDispatch({type: 'skip'}) // undo starting the onboard 155 + let errMsg = e.toString() 156 + if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { 157 + errMsg = _( 158 + msg`Invite code not accepted. Check that you input it correctly and try again.`, 159 + ) 160 + uiDispatch({type: 'set-step', value: 1}) 161 + } 182 162 183 - try { 184 - onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view 185 - await createAccount({ 186 - service: uiState.serviceUrl, 187 - email: uiState.email, 188 - handle: createFullHandle(uiState.handle, uiState.userDomain), 189 - password: uiState.password, 190 - inviteCode: uiState.inviteCode.trim(), 191 - verificationPhone: uiState.verificationPhone.trim(), 192 - verificationCode: uiState.verificationCode.trim(), 193 - }) 194 - } catch (e: any) { 195 - onboardingDispatch({type: 'skip'}) // undo starting the onboard 196 - let errMsg = e.toString() 197 - if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { 198 - errMsg = _( 199 - msg`Invite code not accepted. Check that you input it correctly and try again.`, 200 - ) 201 - uiDispatch({type: 'set-step', value: 1}) 202 - } else if (e.error === 'InvalidPhoneVerification') { 203 - uiDispatch({type: 'set-step', value: 2}) 204 - } 163 + if ([400, 429].includes(e.status)) { 164 + logger.warn('Failed to create account', {message: e}) 165 + } else { 166 + logger.error(`Failed to create account (${e.status} status)`, { 167 + message: e, 168 + }) 169 + } 205 170 206 - if ([400, 429].includes(e.status)) { 207 - logger.warn('Failed to create account', {message: e}) 208 - } else { 209 - logger.error(`Failed to create account (${e.status} status)`, { 210 - message: e, 211 - }) 212 - } 171 + const error = cleanError(errMsg) 172 + const isHandleError = error.toLowerCase().includes('handle') 213 173 214 - uiDispatch({type: 'set-processing', value: false}) 215 - uiDispatch({type: 'set-error', value: cleanError(errMsg)}) 216 - throw e 217 - } 174 + uiDispatch({type: 'set-processing', value: false}) 175 + uiDispatch({type: 'set-error', value: cleanError(errMsg)}) 176 + uiDispatch({type: 'set-step', value: isHandleError ? 2 : 1}) 177 + } 178 + }, 179 + [ 180 + uiState.email, 181 + uiState.password, 182 + uiState.handle, 183 + uiState.isCaptchaRequired, 184 + uiState.serviceUrl, 185 + uiState.userDomain, 186 + uiState.inviteCode, 187 + uiState.birthDate, 188 + uiDispatch, 189 + _, 190 + onboardingDispatch, 191 + createAccount, 192 + setBirthDate, 193 + setSavedFeeds, 194 + ], 195 + ) 218 196 } 219 197 220 198 export function is13(state: CreateAccountState) { ··· 269 247 case 'set-password': { 270 248 return compute({...state, password: action.value}) 271 249 } 272 - case 'set-phone-country': { 273 - return compute({...state, phoneCountry: action.value}) 274 - } 275 - case 'set-verification-phone': { 276 - return compute({ 277 - ...state, 278 - verificationPhone: action.value, 279 - hasRequestedVerificationCode: false, 280 - }) 281 - } 282 - case 'set-verification-code': { 283 - return compute({...state, verificationCode: action.value.trim()}) 284 - } 285 - case 'set-has-requested-verification-code': { 286 - return compute({...state, hasRequestedVerificationCode: action.value}) 287 - } 288 250 case 'set-handle': { 289 251 return compute({...state, handle: action.value}) 290 252 } ··· 302 264 }) 303 265 } 304 266 } 305 - let increment = 1 306 - if (state.step === 1 && !state.isPhoneVerificationRequired) { 307 - increment = 2 308 - } 309 - return compute({...state, error: '', step: state.step + increment}) 267 + return compute({...state, error: '', step: state.step + 1}) 310 268 } 311 269 case 'back': { 312 - let decrement = 1 313 - if (state.step === 3 && !state.isPhoneVerificationRequired) { 314 - decrement = 2 315 - } 316 - return compute({...state, error: '', step: state.step - decrement}) 270 + return compute({...state, error: '', step: state.step - 1}) 317 271 } 318 272 } 319 273 } ··· 328 282 !!state.email && 329 283 !!state.password 330 284 } else if (state.step === 2) { 331 - canNext = 332 - !state.isPhoneVerificationRequired || 333 - (!!state.verificationPhone && 334 - isValidVerificationCode(state.verificationCode)) 285 + canNext = !!state.handle 335 286 } else if (state.step === 3) { 336 - canNext = !!state.handle 287 + // Step 3 will automatically redirect as soon as the captcha completes 288 + canNext = false 337 289 } 338 290 return { 339 291 ...state, 340 292 canBack: state.step > 1, 341 293 canNext, 342 294 isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired, 343 - isPhoneVerificationRequired: 344 - !!state.serviceDescription?.phoneVerificationRequired, 295 + isCaptchaRequired: !!state.serviceDescription?.phoneVerificationRequired, 345 296 } 346 297 } 347 - 348 - function isValidVerificationCode(str: string): boolean { 349 - return /[0-9]{6}/.test(str) 350 - }
+4 -9
yarn.lock
··· 15158 15158 prelude-ls "^1.2.1" 15159 15159 type-check "~0.4.0" 15160 15160 15161 - libphonenumber-js@^1.10.53: 15162 - version "1.10.53" 15163 - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.53.tgz#8dbfe1355ef1a3d8e13b8d92849f7db7ebddc98f" 15164 - integrity sha512-sDTnnqlWK4vH4AlDQuswz3n4Hx7bIQWTpIcScJX+Sp7St3LXHmfiax/ZFfyYxHmkdCvydOLSuvtAO/XpXiSySw== 15165 - 15166 15161 lie@3.1.1: 15167 15162 version "3.1.1" 15168 15163 resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" ··· 16157 16152 resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" 16158 16153 integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== 16159 16154 16160 - nanoid@^5.0.2: 16161 - version "5.0.2" 16162 - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.2.tgz#97588ebc70166d0feaf73ccd2799bb4ceaebf692" 16163 - integrity sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg== 16155 + nanoid@^5.0.5: 16156 + version "5.0.5" 16157 + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.5.tgz#5112efb5c0caf4fc80680d66d303c65233a79fdd" 16158 + integrity sha512-/Veqm+QKsyMY3kqi4faWplnY1u+VuKO3dD2binyPIybP31DRO29bPF+1mszgLnrR2KqSLceFLBNw0zmvDzN1QQ== 16164 16159 16165 16160 napi-build-utils@^1.0.1: 16166 16161 version "1.0.2"