From fbdf4517c29d87a2bd0d837fb732f93d255d6f64 Mon Sep 17 00:00:00 2001 From: Hailey Date: Sat, 17 Feb 2024 16:03:47 -0800 Subject: [PATCH] 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 --- package.json | 7 +- src/lib/country-codes.ts | 256 --------------- src/view/com/auth/create/CaptchaWebView.tsx | 86 +++++ .../com/auth/create/CaptchaWebView.web.tsx | 61 ++++ src/view/com/auth/create/CreateAccount.tsx | 71 ++-- src/view/com/auth/create/Step1.tsx | 9 +- src/view/com/auth/create/Step2.tsx | 308 ++---------------- src/view/com/auth/create/Step3.tsx | 120 +++++-- src/view/com/auth/create/StepHeader.tsx | 2 +- src/view/com/auth/create/state.ts | 301 +++++++---------- yarn.lock | 13 +- 11 files changed, 441 insertions(+), 793 deletions(-) delete mode 100644 src/lib/country-codes.ts create mode 100644 src/view/com/auth/create/CaptchaWebView.tsx create mode 100644 src/view/com/auth/create/CaptchaWebView.web.tsx diff --git a/package.json b/package.json index 837a8d0e..4a3a2a7d 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,6 @@ "js-sha256": "^0.9.0", "jwt-decode": "^4.0.0", "lande": "^1.0.10", - "libphonenumber-js": "^1.10.53", "lodash.chunk": "^4.2.0", "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", @@ -137,7 +136,7 @@ "mobx": "^6.6.1", "mobx-react-lite": "^3.4.0", "mobx-utils": "^6.0.6", - "nanoid": "^5.0.2", + "nanoid": "^5.0.5", "normalize-url": "^8.0.0", "patch-package": "^6.5.1", "postinstall-postinstall": "^2.1.0", @@ -164,6 +163,7 @@ "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", "react-native-svg": "14.1.0", + "react-native-ui-text-view": "link:./modules/react-native-ui-text-view", "react-native-url-polyfill": "^1.3.0", "react-native-uuid": "^2.0.1", "react-native-version-number": "^0.3.6", @@ -178,8 +178,7 @@ "tlds": "^1.234.0", "use-deep-compare": "^1.1.0", "zeego": "^1.6.2", - "zod": "^3.20.2", - "react-native-ui-text-view": "link:./modules/react-native-ui-text-view" + "zod": "^3.20.2" }, "devDependencies": { "@atproto/dev-env": "^0.2.28", diff --git a/src/lib/country-codes.ts b/src/lib/country-codes.ts deleted file mode 100644 index 9c9da84c..00000000 --- a/src/lib/country-codes.ts +++ /dev/null @@ -1,256 +0,0 @@ -import {CountryCode} from 'libphonenumber-js' - -// ISO 3166-1 alpha-2 codes - -export interface CountryCodeMap { - code2: CountryCode - name: string -} - -export const COUNTRY_CODES: CountryCodeMap[] = [ - {code2: 'AF', name: 'Afghanistan (+93)'}, - {code2: 'AX', name: 'Åland Islands (+358)'}, - {code2: 'AL', name: 'Albania (+355)'}, - {code2: 'DZ', name: 'Algeria (+213)'}, - {code2: 'AS', name: 'American Samoa (+1)'}, - {code2: 'AD', name: 'Andorra (+376)'}, - {code2: 'AO', name: 'Angola (+244)'}, - {code2: 'AI', name: 'Anguilla (+1)'}, - {code2: 'AG', name: 'Antigua and Barbuda (+1)'}, - {code2: 'AR', name: 'Argentina (+54)'}, - {code2: 'AM', name: 'Armenia (+374)'}, - {code2: 'AW', name: 'Aruba (+297)'}, - {code2: 'AU', name: 'Australia (+61)'}, - {code2: 'AT', name: 'Austria (+43)'}, - {code2: 'AZ', name: 'Azerbaijan (+994)'}, - {code2: 'BS', name: 'Bahamas (+1)'}, - {code2: 'BH', name: 'Bahrain (+973)'}, - {code2: 'BD', name: 'Bangladesh (+880)'}, - {code2: 'BB', name: 'Barbados (+1)'}, - {code2: 'BY', name: 'Belarus (+375)'}, - {code2: 'BE', name: 'Belgium (+32)'}, - {code2: 'BZ', name: 'Belize (+501)'}, - {code2: 'BJ', name: 'Benin (+229)'}, - {code2: 'BM', name: 'Bermuda (+1)'}, - {code2: 'BT', name: 'Bhutan (+975)'}, - {code2: 'BO', name: 'Bolivia (Plurinational State of) (+591)'}, - {code2: 'BQ', name: 'Bonaire, Sint Eustatius and Saba (+599)'}, - {code2: 'BA', name: 'Bosnia and Herzegovina (+387)'}, - {code2: 'BW', name: 'Botswana (+267)'}, - {code2: 'BR', name: 'Brazil (+55)'}, - {code2: 'IO', name: 'British Indian Ocean Territory (+246)'}, - {code2: 'BN', name: 'Brunei Darussalam (+673)'}, - {code2: 'BG', name: 'Bulgaria (+359)'}, - {code2: 'BF', name: 'Burkina Faso (+226)'}, - {code2: 'BI', name: 'Burundi (+257)'}, - {code2: 'CV', name: 'Cabo Verde (+238)'}, - {code2: 'KH', name: 'Cambodia (+855)'}, - {code2: 'CM', name: 'Cameroon (+237)'}, - {code2: 'CA', name: 'Canada (+1)'}, - {code2: 'KY', name: 'Cayman Islands (+1)'}, - {code2: 'CF', name: 'Central African Republic (+236)'}, - {code2: 'TD', name: 'Chad (+235)'}, - {code2: 'CL', name: 'Chile (+56)'}, - {code2: 'CN', name: 'China (+86)'}, - {code2: 'CX', name: 'Christmas Island (+61)'}, - {code2: 'CC', name: 'Cocos (Keeling) Islands (+61)'}, - {code2: 'CO', name: 'Colombia (+57)'}, - {code2: 'KM', name: 'Comoros (+269)'}, - {code2: 'CG', name: 'Congo (+242)'}, - {code2: 'CD', name: 'Congo, Democratic Republic of the (+243)'}, - {code2: 'CK', name: 'Cook Islands (+682)'}, - {code2: 'CR', name: 'Costa Rica (+506)'}, - {code2: 'CI', name: "Côte d'Ivoire (+225)"}, - {code2: 'HR', name: 'Croatia (+385)'}, - {code2: 'CU', name: 'Cuba (+53)'}, - {code2: 'CW', name: 'Curaçao (+599)'}, - {code2: 'CY', name: 'Cyprus (+357)'}, - {code2: 'CZ', name: 'Czechia (+420)'}, - {code2: 'DK', name: 'Denmark (+45)'}, - {code2: 'DJ', name: 'Djibouti (+253)'}, - {code2: 'DM', name: 'Dominica (+1)'}, - {code2: 'DO', name: 'Dominican Republic (+1)'}, - {code2: 'EC', name: 'Ecuador (+593)'}, - {code2: 'EG', name: 'Egypt (+20)'}, - {code2: 'SV', name: 'El Salvador (+503)'}, - {code2: 'GQ', name: 'Equatorial Guinea (+240)'}, - {code2: 'ER', name: 'Eritrea (+291)'}, - {code2: 'EE', name: 'Estonia (+372)'}, - {code2: 'SZ', name: 'Eswatini (+268)'}, - {code2: 'ET', name: 'Ethiopia (+251)'}, - {code2: 'FK', name: 'Falkland Islands (Malvinas) (+500)'}, - {code2: 'FO', name: 'Faroe Islands (+298)'}, - {code2: 'FJ', name: 'Fiji (+679)'}, - {code2: 'FI', name: 'Finland (+358)'}, - {code2: 'FR', name: 'France (+33)'}, - {code2: 'GF', name: 'French Guiana (+594)'}, - {code2: 'PF', name: 'French Polynesia (+689)'}, - {code2: 'GA', name: 'Gabon (+241)'}, - {code2: 'GM', name: 'Gambia (+220)'}, - {code2: 'GE', name: 'Georgia (+995)'}, - {code2: 'DE', name: 'Germany (+49)'}, - {code2: 'GH', name: 'Ghana (+233)'}, - {code2: 'GI', name: 'Gibraltar (+350)'}, - {code2: 'GR', name: 'Greece (+30)'}, - {code2: 'GL', name: 'Greenland (+299)'}, - {code2: 'GD', name: 'Grenada (+1)'}, - {code2: 'GP', name: 'Guadeloupe (+590)'}, - {code2: 'GU', name: 'Guam (+1)'}, - {code2: 'GT', name: 'Guatemala (+502)'}, - {code2: 'GG', name: 'Guernsey (+44)'}, - {code2: 'GN', name: 'Guinea (+224)'}, - {code2: 'GW', name: 'Guinea-Bissau (+245)'}, - {code2: 'GY', name: 'Guyana (+592)'}, - {code2: 'HT', name: 'Haiti (+509)'}, - {code2: 'VA', name: 'Holy See (+39)'}, - {code2: 'HN', name: 'Honduras (+504)'}, - {code2: 'HK', name: 'Hong Kong (+852)'}, - {code2: 'HU', name: 'Hungary (+36)'}, - {code2: 'IS', name: 'Iceland (+354)'}, - {code2: 'IN', name: 'India (+91)'}, - {code2: 'ID', name: 'Indonesia (+62)'}, - {code2: 'IR', name: 'Iran (Islamic Republic of) (+98)'}, - {code2: 'IQ', name: 'Iraq (+964)'}, - {code2: 'IE', name: 'Ireland (+353)'}, - {code2: 'IM', name: 'Isle of Man (+44)'}, - {code2: 'IL', name: 'Israel (+972)'}, - {code2: 'IT', name: 'Italy (+39)'}, - {code2: 'JM', name: 'Jamaica (+1)'}, - {code2: 'JP', name: 'Japan (+81)'}, - {code2: 'JE', name: 'Jersey (+44)'}, - {code2: 'JO', name: 'Jordan (+962)'}, - {code2: 'KZ', name: 'Kazakhstan (+7)'}, - {code2: 'KE', name: 'Kenya (+254)'}, - {code2: 'KI', name: 'Kiribati (+686)'}, - {code2: 'KP', name: "Korea (Democratic People's Republic of) (+850)"}, - {code2: 'KR', name: 'Korea, Republic of (+82)'}, - {code2: 'KW', name: 'Kuwait (+965)'}, - {code2: 'KG', name: 'Kyrgyzstan (+996)'}, - {code2: 'LA', name: "Lao People's Democratic Republic (+856)"}, - {code2: 'LV', name: 'Latvia (+371)'}, - {code2: 'LB', name: 'Lebanon (+961)'}, - {code2: 'LS', name: 'Lesotho (+266)'}, - {code2: 'LR', name: 'Liberia (+231)'}, - {code2: 'LY', name: 'Libya (+218)'}, - {code2: 'LI', name: 'Liechtenstein (+423)'}, - {code2: 'LT', name: 'Lithuania (+370)'}, - {code2: 'LU', name: 'Luxembourg (+352)'}, - {code2: 'MO', name: 'Macao (+853)'}, - {code2: 'MG', name: 'Madagascar (+261)'}, - {code2: 'MW', name: 'Malawi (+265)'}, - {code2: 'MY', name: 'Malaysia (+60)'}, - {code2: 'MV', name: 'Maldives (+960)'}, - {code2: 'ML', name: 'Mali (+223)'}, - {code2: 'MT', name: 'Malta (+356)'}, - {code2: 'MH', name: 'Marshall Islands (+692)'}, - {code2: 'MQ', name: 'Martinique (+596)'}, - {code2: 'MR', name: 'Mauritania (+222)'}, - {code2: 'MU', name: 'Mauritius (+230)'}, - {code2: 'YT', name: 'Mayotte (+262)'}, - {code2: 'MX', name: 'Mexico (+52)'}, - {code2: 'FM', name: 'Micronesia (Federated States of) (+691)'}, - {code2: 'MD', name: 'Moldova, Republic of (+373)'}, - {code2: 'MC', name: 'Monaco (+377)'}, - {code2: 'MN', name: 'Mongolia (+976)'}, - {code2: 'ME', name: 'Montenegro (+382)'}, - {code2: 'MS', name: 'Montserrat (+1)'}, - {code2: 'MA', name: 'Morocco (+212)'}, - {code2: 'MZ', name: 'Mozambique (+258)'}, - {code2: 'MM', name: 'Myanmar (+95)'}, - {code2: 'NA', name: 'Namibia (+264)'}, - {code2: 'NR', name: 'Nauru (+674)'}, - {code2: 'NP', name: 'Nepal (+977)'}, - {code2: 'NL', name: 'Netherlands, Kingdom of the (+31)'}, - {code2: 'NC', name: 'New Caledonia (+687)'}, - {code2: 'NZ', name: 'New Zealand (+64)'}, - {code2: 'NI', name: 'Nicaragua (+505)'}, - {code2: 'NE', name: 'Niger (+227)'}, - {code2: 'NG', name: 'Nigeria (+234)'}, - {code2: 'NU', name: 'Niue (+683)'}, - {code2: 'NF', name: 'Norfolk Island (+672)'}, - {code2: 'MK', name: 'North Macedonia (+389)'}, - {code2: 'MP', name: 'Northern Mariana Islands (+1)'}, - {code2: 'NO', name: 'Norway (+47)'}, - {code2: 'OM', name: 'Oman (+968)'}, - {code2: 'PK', name: 'Pakistan (+92)'}, - {code2: 'PW', name: 'Palau (+680)'}, - {code2: 'PS', name: 'Palestine, State of (+970)'}, - {code2: 'PA', name: 'Panama (+507)'}, - {code2: 'PG', name: 'Papua New Guinea (+675)'}, - {code2: 'PY', name: 'Paraguay (+595)'}, - {code2: 'PE', name: 'Peru (+51)'}, - {code2: 'PH', name: 'Philippines (+63)'}, - {code2: 'PL', name: 'Poland (+48)'}, - {code2: 'PT', name: 'Portugal (+351)'}, - {code2: 'PR', name: 'Puerto Rico (+1)'}, - {code2: 'QA', name: 'Qatar (+974)'}, - {code2: 'RE', name: 'Réunion (+262)'}, - {code2: 'RO', name: 'Romania (+40)'}, - {code2: 'RU', name: 'Russian Federation (+7)'}, - {code2: 'RW', name: 'Rwanda (+250)'}, - {code2: 'BL', name: 'Saint Barthélemy (+590)'}, - {code2: 'SH', name: 'Saint Helena, Ascension and Tristan da Cunha (+290)'}, - {code2: 'KN', name: 'Saint Kitts and Nevis (+1)'}, - {code2: 'LC', name: 'Saint Lucia (+1)'}, - {code2: 'MF', name: 'Saint Martin (French part) (+590)'}, - {code2: 'PM', name: 'Saint Pierre and Miquelon (+508)'}, - {code2: 'VC', name: 'Saint Vincent and the Grenadines (+1)'}, - {code2: 'WS', name: 'Samoa (+685)'}, - {code2: 'SM', name: 'San Marino (+378)'}, - {code2: 'ST', name: 'Sao Tome and Principe (+239)'}, - {code2: 'SA', name: 'Saudi Arabia (+966)'}, - {code2: 'SN', name: 'Senegal (+221)'}, - {code2: 'RS', name: 'Serbia (+381)'}, - {code2: 'SC', name: 'Seychelles (+248)'}, - {code2: 'SL', name: 'Sierra Leone (+232)'}, - {code2: 'SG', name: 'Singapore (+65)'}, - {code2: 'SX', name: 'Sint Maarten (Dutch part) (+1)'}, - {code2: 'SK', name: 'Slovakia (+421)'}, - {code2: 'SI', name: 'Slovenia (+386)'}, - {code2: 'SB', name: 'Solomon Islands (+677)'}, - {code2: 'SO', name: 'Somalia (+252)'}, - {code2: 'ZA', name: 'South Africa (+27)'}, - {code2: 'SS', name: 'South Sudan (+211)'}, - {code2: 'ES', name: 'Spain (+34)'}, - {code2: 'LK', name: 'Sri Lanka (+94)'}, - {code2: 'SD', name: 'Sudan (+249)'}, - {code2: 'SR', name: 'Suriname (+597)'}, - {code2: 'SJ', name: 'Svalbard and Jan Mayen (+47)'}, - {code2: 'SE', name: 'Sweden (+46)'}, - {code2: 'CH', name: 'Switzerland (+41)'}, - {code2: 'SY', name: 'Syrian Arab Republic (+963)'}, - {code2: 'TW', name: 'Taiwan (+886)'}, - {code2: 'TJ', name: 'Tajikistan (+992)'}, - {code2: 'TZ', name: 'Tanzania, United Republic of (+255)'}, - {code2: 'TH', name: 'Thailand (+66)'}, - {code2: 'TL', name: 'Timor-Leste (+670)'}, - {code2: 'TG', name: 'Togo (+228)'}, - {code2: 'TK', name: 'Tokelau (+690)'}, - {code2: 'TO', name: 'Tonga (+676)'}, - {code2: 'TT', name: 'Trinidad and Tobago (+1)'}, - {code2: 'TN', name: 'Tunisia (+216)'}, - {code2: 'TR', name: 'Türkiye (+90)'}, - {code2: 'TM', name: 'Turkmenistan (+993)'}, - {code2: 'TC', name: 'Turks and Caicos Islands (+1)'}, - {code2: 'TV', name: 'Tuvalu (+688)'}, - {code2: 'UG', name: 'Uganda (+256)'}, - {code2: 'UA', name: 'Ukraine (+380)'}, - {code2: 'AE', name: 'United Arab Emirates (+971)'}, - { - code2: 'GB', - name: 'United Kingdom of Great Britain and Northern Ireland (+44)', - }, - {code2: 'US', name: 'United States of America (+1)'}, - {code2: 'UY', name: 'Uruguay (+598)'}, - {code2: 'UZ', name: 'Uzbekistan (+998)'}, - {code2: 'VU', name: 'Vanuatu (+678)'}, - {code2: 'VE', name: 'Venezuela (Bolivarian Republic of) (+58)'}, - {code2: 'VN', name: 'Viet Nam (+84)'}, - {code2: 'VG', name: 'Virgin Islands (British) (+1)'}, - {code2: 'VI', name: 'Virgin Islands (U.S.) (+1)'}, - {code2: 'WF', name: 'Wallis and Futuna (+681)'}, - {code2: 'EH', name: 'Western Sahara (+212)'}, - {code2: 'YE', name: 'Yemen (+967)'}, - {code2: 'ZM', name: 'Zambia (+260)'}, - {code2: 'ZW', name: 'Zimbabwe (+263)'}, -] diff --git a/src/view/com/auth/create/CaptchaWebView.tsx b/src/view/com/auth/create/CaptchaWebView.tsx new file mode 100644 index 00000000..b0de8b4a --- /dev/null +++ b/src/view/com/auth/create/CaptchaWebView.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import {WebView, WebViewNavigation} from 'react-native-webview' +import {ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes' +import {StyleSheet} from 'react-native' +import {CreateAccountState} from 'view/com/auth/create/state' + +const ALLOWED_HOSTS = [ + 'bsky.social', + 'bsky.app', + 'staging.bsky.app', + 'staging.bsky.dev', + 'js.hcaptcha.com', + 'newassets.hcaptcha.com', + 'api2.hcaptcha.com', +] + +export function CaptchaWebView({ + url, + stateParam, + uiState, + onSuccess, + onError, +}: { + url: string + stateParam: string + uiState?: CreateAccountState + onSuccess: (code: string) => void + onError: () => void +}) { + const redirectHost = React.useMemo(() => { + if (!uiState?.serviceUrl) return 'bsky.app' + + return uiState?.serviceUrl && + new URL(uiState?.serviceUrl).host === 'staging.bsky.dev' + ? 'staging.bsky.app' + : 'bsky.app' + }, [uiState?.serviceUrl]) + + const wasSuccessful = React.useRef(false) + + const onShouldStartLoadWithRequest = React.useCallback( + (event: ShouldStartLoadRequest) => { + const urlp = new URL(event.url) + return ALLOWED_HOSTS.includes(urlp.host) + }, + [], + ) + + const onNavigationStateChange = React.useCallback( + (e: WebViewNavigation) => { + if (wasSuccessful.current) return + + const urlp = new URL(e.url) + if (urlp.host !== redirectHost) return + + const code = urlp.searchParams.get('code') + if (urlp.searchParams.get('state') !== stateParam || !code) { + onError() + return + } + + wasSuccessful.current = true + onSuccess(code) + }, + [redirectHost, stateParam, onSuccess, onError], + ) + + return ( + + ) +} + +const styles = StyleSheet.create({ + webview: { + flex: 1, + backgroundColor: 'transparent', + borderRadius: 10, + }, +}) diff --git a/src/view/com/auth/create/CaptchaWebView.web.tsx b/src/view/com/auth/create/CaptchaWebView.web.tsx new file mode 100644 index 00000000..7791a58d --- /dev/null +++ b/src/view/com/auth/create/CaptchaWebView.web.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import {StyleSheet} from 'react-native' + +// @ts-ignore web only, we will always redirect to the app on web (CORS) +const REDIRECT_HOST = new URL(window.location.href).host + +export function CaptchaWebView({ + url, + stateParam, + onSuccess, + onError, +}: { + url: string + stateParam: string + onSuccess: (code: string) => void + onError: () => void +}) { + const onLoad = React.useCallback(() => { + // @ts-ignore web + const frame: HTMLIFrameElement = document.getElementById( + 'captcha-iframe', + ) as HTMLIFrameElement + + try { + // @ts-ignore web + const href = frame?.contentWindow?.location.href + if (!href) return + const urlp = new URL(href) + + // This shouldn't happen with CORS protections, but for good measure + if (urlp.host !== REDIRECT_HOST) return + + const code = urlp.searchParams.get('code') + if (urlp.searchParams.get('state') !== stateParam || !code) { + onError() + return + } + onSuccess(code) + } catch (e) { + // We don't need to handle this + } + }, [stateParam, onSuccess, onError]) + + return ( +