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 challengezio/stable
parent
dc143d6a6e
commit
fbdf4517c2
|
@ -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",
|
||||
|
|
|
@ -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)'},
|
||||
]
|
|
@ -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 (
|
||||
<WebView
|
||||
source={{uri: url}}
|
||||
javaScriptEnabled
|
||||
style={styles.webview}
|
||||
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
|
||||
onNavigationStateChange={onNavigationStateChange}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
webview: {
|
||||
flex: 1,
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 10,
|
||||
},
|
||||
})
|
|
@ -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 (
|
||||
<iframe
|
||||
src={url}
|
||||
style={styles.iframe}
|
||||
id="captcha-iframe"
|
||||
onLoad={onLoad}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
iframe: {
|
||||
flex: 1,
|
||||
borderWidth: 0,
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
})
|
|
@ -13,33 +13,25 @@ import {s} from 'lib/styles'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {useSessionApi} from '#/state/session'
|
||||
import {useCreateAccount, submit} from './state'
|
||||
import {useCreateAccount, useSubmitCreateAccount} from './state'
|
||||
import {useServiceQuery} from '#/state/queries/service'
|
||||
import {
|
||||
usePreferencesSetBirthDateMutation,
|
||||
useSetSaveFeedsMutation,
|
||||
DEFAULT_PROD_FEEDS,
|
||||
} from '#/state/queries/preferences'
|
||||
import {FEEDBACK_FORM_URL, HITSLOP_10, IS_PROD} from '#/lib/constants'
|
||||
import {FEEDBACK_FORM_URL, HITSLOP_10} from '#/lib/constants'
|
||||
|
||||
import {Step1} from './Step1'
|
||||
import {Step2} from './Step2'
|
||||
import {Step3} from './Step3'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {TextLink} from '../../util/Link'
|
||||
import {getAgent} from 'state/session'
|
||||
import {createFullHandle} from 'lib/strings/handles'
|
||||
|
||||
export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
|
||||
const {screen} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const [uiState, uiDispatch] = useCreateAccount()
|
||||
const onboardingDispatch = useOnboardingDispatch()
|
||||
const {createAccount} = useSessionApi()
|
||||
const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
|
||||
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const submit = useSubmitCreateAccount(uiState, uiDispatch)
|
||||
|
||||
React.useEffect(() => {
|
||||
screen('CreateAccount')
|
||||
|
@ -84,33 +76,48 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
|
|||
if (!uiState.canNext) {
|
||||
return
|
||||
}
|
||||
if (uiState.step < 3) {
|
||||
uiDispatch({type: 'next'})
|
||||
} else {
|
||||
|
||||
if (uiState.step === 2) {
|
||||
uiDispatch({type: 'set-processing', value: true})
|
||||
try {
|
||||
await submit({
|
||||
onboardingDispatch,
|
||||
createAccount,
|
||||
uiState,
|
||||
uiDispatch,
|
||||
_,
|
||||
const res = await getAgent().resolveHandle({
|
||||
handle: createFullHandle(uiState.handle, uiState.userDomain),
|
||||
})
|
||||
setBirthDate({birthDate: uiState.birthDate})
|
||||
if (IS_PROD(uiState.serviceUrl)) {
|
||||
setSavedFeeds(DEFAULT_PROD_FEEDS)
|
||||
|
||||
if (res.data.did) {
|
||||
uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`That handle is already taken.`),
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// dont need to handle here
|
||||
} catch (e) {
|
||||
// Don't need to handle
|
||||
} finally {
|
||||
uiDispatch({type: 'set-processing', value: false})
|
||||
}
|
||||
|
||||
if (!uiState.isCaptchaRequired) {
|
||||
try {
|
||||
await submit()
|
||||
} catch {
|
||||
// dont need to handle here
|
||||
}
|
||||
// We don't need to go to the next page if there wasn't a captcha required
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
uiDispatch({type: 'next'})
|
||||
}, [
|
||||
uiState,
|
||||
uiState.canNext,
|
||||
uiState.step,
|
||||
uiState.isCaptchaRequired,
|
||||
uiState.handle,
|
||||
uiState.userDomain,
|
||||
uiDispatch,
|
||||
onboardingDispatch,
|
||||
createAccount,
|
||||
setBirthDate,
|
||||
setSavedFeeds,
|
||||
_,
|
||||
submit,
|
||||
])
|
||||
|
||||
// rendering
|
||||
|
|
|
@ -73,6 +73,10 @@ export function Step1({
|
|||
/>
|
||||
<StepHeader uiState={uiState} title={_(msg`Your account`)} />
|
||||
|
||||
{uiState.error ? (
|
||||
<ErrorMessage message={uiState.error} style={styles.error} />
|
||||
) : undefined}
|
||||
|
||||
<View style={s.pb20}>
|
||||
<Text type="md-medium" style={[pal.text, s.mb2]}>
|
||||
<Trans>Hosting provider</Trans>
|
||||
|
@ -259,9 +263,6 @@ export function Step1({
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
{uiState.error ? (
|
||||
<ErrorMessage message={uiState.error} style={styles.error} />
|
||||
) : undefined}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -269,7 +270,7 @@ export function Step1({
|
|||
const styles = StyleSheet.create({
|
||||
error: {
|
||||
borderRadius: 6,
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
dateInputButton: {
|
||||
borderWidth: 1,
|
||||
|
|
|
@ -1,35 +1,19 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import RNPickerSelect from 'react-native-picker-select'
|
||||
import {
|
||||
CreateAccountState,
|
||||
CreateAccountDispatch,
|
||||
requestVerificationCode,
|
||||
} from './state'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {CreateAccountState, CreateAccountDispatch} from './state'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {StepHeader} from './StepHeader'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {TextInput} from '../util/TextInput'
|
||||
import {Button} from '../../util/forms/Button'
|
||||
import {createFullHandle} from 'lib/strings/handles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||
import {isAndroid, isWeb} from 'platform/detection'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import parsePhoneNumber from 'libphonenumber-js'
|
||||
import {COUNTRY_CODES} from '#/lib/country-codes'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
|
||||
/** STEP 3: Your user handle
|
||||
* @field User handle
|
||||
*/
|
||||
export function Step2({
|
||||
uiState,
|
||||
uiDispatch,
|
||||
|
@ -39,258 +23,34 @@ export function Step2({
|
|||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
||||
const onPressRequest = React.useCallback(() => {
|
||||
const phoneNumber = parsePhoneNumber(
|
||||
uiState.verificationPhone,
|
||||
uiState.phoneCountry,
|
||||
)
|
||||
if (phoneNumber && phoneNumber.isValid()) {
|
||||
requestVerificationCode({uiState, uiDispatch, _})
|
||||
} else {
|
||||
uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(
|
||||
msg`There's something wrong with this number. Please choose your country and enter your full phone number!`,
|
||||
),
|
||||
})
|
||||
}
|
||||
}, [uiState, uiDispatch, _])
|
||||
|
||||
const onPressRetry = React.useCallback(() => {
|
||||
uiDispatch({type: 'set-has-requested-verification-code', value: false})
|
||||
}, [uiDispatch])
|
||||
|
||||
const phoneNumberFormatted = React.useMemo(
|
||||
() =>
|
||||
uiState.hasRequestedVerificationCode
|
||||
? parsePhoneNumber(
|
||||
uiState.verificationPhone,
|
||||
uiState.phoneCountry,
|
||||
)?.formatInternational()
|
||||
: '',
|
||||
[
|
||||
uiState.hasRequestedVerificationCode,
|
||||
uiState.verificationPhone,
|
||||
uiState.phoneCountry,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
<View>
|
||||
<StepHeader uiState={uiState} title={_(msg`SMS verification`)} />
|
||||
|
||||
{!uiState.hasRequestedVerificationCode ? (
|
||||
<>
|
||||
<View style={s.pb10}>
|
||||
<Text
|
||||
type="md-medium"
|
||||
style={[pal.text, s.mb2]}
|
||||
nativeID="phoneCountry">
|
||||
<Trans>Country</Trans>
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
{position: 'relative'},
|
||||
isAndroid && {
|
||||
borderWidth: 1,
|
||||
borderColor: pal.border.borderColor,
|
||||
borderRadius: 4,
|
||||
},
|
||||
]}>
|
||||
<RNPickerSelect
|
||||
placeholder={{}}
|
||||
value={uiState.phoneCountry}
|
||||
onValueChange={value =>
|
||||
uiDispatch({type: 'set-phone-country', value})
|
||||
}
|
||||
items={COUNTRY_CODES.filter(l => Boolean(l.code2)).map(l => ({
|
||||
label: l.name,
|
||||
value: l.code2,
|
||||
key: l.code2,
|
||||
}))}
|
||||
style={{
|
||||
inputAndroid: {
|
||||
backgroundColor: pal.view.backgroundColor,
|
||||
color: pal.text.color,
|
||||
fontSize: 21,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: '500',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
inputIOS: {
|
||||
backgroundColor: pal.view.backgroundColor,
|
||||
color: pal.text.color,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: '500',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: pal.border.borderColor,
|
||||
borderRadius: 4,
|
||||
},
|
||||
inputWeb: {
|
||||
// @ts-ignore web only
|
||||
cursor: 'pointer',
|
||||
'-moz-appearance': 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
appearance: 'none',
|
||||
outline: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: pal.border.borderColor,
|
||||
backgroundColor: pal.view.backgroundColor,
|
||||
color: pal.text.color,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: '500',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
accessibilityLabel={_(msg`Select your phone's country`)}
|
||||
accessibilityHint=""
|
||||
accessibilityLabelledBy="phoneCountry"
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 1,
|
||||
right: 1,
|
||||
bottom: 1,
|
||||
width: 40,
|
||||
pointerEvents: 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<FontAwesomeIcon
|
||||
icon="chevron-down"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={s.pb20}>
|
||||
<Text
|
||||
type="md-medium"
|
||||
style={[pal.text, s.mb2]}
|
||||
nativeID="phoneNumber">
|
||||
<Trans>Phone number</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="phoneInput"
|
||||
icon="phone"
|
||||
placeholder={_(msg`Enter your phone number`)}
|
||||
value={uiState.verificationPhone}
|
||||
editable
|
||||
onChange={value =>
|
||||
uiDispatch({type: 'set-verification-phone', value})
|
||||
}
|
||||
accessibilityLabel={_(msg`Email`)}
|
||||
accessibilityHint={_(
|
||||
msg`Input phone number for SMS verification`,
|
||||
)}
|
||||
accessibilityLabelledBy="phoneNumber"
|
||||
keyboardType="phone-pad"
|
||||
autoCapitalize="none"
|
||||
autoComplete="tel"
|
||||
autoCorrect={false}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<Text type="sm" style={[pal.textLight, s.mt5]}>
|
||||
<Trans>
|
||||
Please enter a phone number that can receive SMS text messages.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={isMobile ? {} : {flexDirection: 'row'}}>
|
||||
{uiState.isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<Button
|
||||
testID="requestCodeBtn"
|
||||
type="primary"
|
||||
label={_(msg`Request code`)}
|
||||
labelStyle={isMobile ? [s.flex1, s.textCenter, s.f17] : []}
|
||||
style={
|
||||
isMobile ? {paddingVertical: 12, paddingHorizontal: 20} : {}
|
||||
}
|
||||
onPress={onPressRequest}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View style={s.pb20}>
|
||||
<View
|
||||
style={[
|
||||
s.flexRow,
|
||||
s.mb5,
|
||||
s.alignCenter,
|
||||
{justifyContent: 'space-between'},
|
||||
]}>
|
||||
<Text
|
||||
type="md-medium"
|
||||
style={pal.text}
|
||||
nativeID="verificationCode">
|
||||
<Trans>Verification code</Trans>{' '}
|
||||
</Text>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPressRetry}
|
||||
accessibilityLabel={_(msg`Retry.`)}
|
||||
accessibilityHint=""
|
||||
hitSlop={HITSLOP_10}>
|
||||
<View style={styles.touchable}>
|
||||
<Text
|
||||
type="md-medium"
|
||||
style={pal.link}
|
||||
nativeID="verificationCode">
|
||||
<Trans>Retry</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
<TextInput
|
||||
testID="codeInput"
|
||||
icon="hashtag"
|
||||
placeholder={_(msg`XXXXXX`)}
|
||||
value={uiState.verificationCode}
|
||||
editable
|
||||
onChange={value =>
|
||||
uiDispatch({type: 'set-verification-code', value})
|
||||
}
|
||||
accessibilityLabel={_(msg`Email`)}
|
||||
accessibilityHint={_(
|
||||
msg`Input the verification code we have texted to you`,
|
||||
)}
|
||||
accessibilityLabelledBy="verificationCode"
|
||||
keyboardType="phone-pad"
|
||||
autoCapitalize="none"
|
||||
autoComplete="one-time-code"
|
||||
textContentType="oneTimeCode"
|
||||
autoCorrect={false}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<Text type="sm" style={[pal.textLight, s.mt5]}>
|
||||
<Trans>
|
||||
Please enter the verification code sent to{' '}
|
||||
{phoneNumberFormatted}.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
<StepHeader uiState={uiState} title={_(msg`Your user handle`)} />
|
||||
{uiState.error ? (
|
||||
<ErrorMessage message={uiState.error} style={styles.error} />
|
||||
) : undefined}
|
||||
<View style={s.pb10}>
|
||||
<TextInput
|
||||
testID="handleInput"
|
||||
icon="at"
|
||||
placeholder="e.g. alice"
|
||||
value={uiState.handle}
|
||||
editable
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
autoCorrect={false}
|
||||
onChange={value => uiDispatch({type: 'set-handle', value})}
|
||||
// TODO: Add explicit text label
|
||||
accessibilityLabel={_(msg`User handle`)}
|
||||
accessibilityHint={_(msg`Input your user handle`)}
|
||||
/>
|
||||
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
|
||||
<Trans>Your full handle will be</Trans>{' '}
|
||||
<Text type="lg-bold" style={pal.text}>
|
||||
@{createFullHandle(uiState.handle, uiState.userDomain)}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -298,10 +58,6 @@ export function Step2({
|
|||
const styles = StyleSheet.create({
|
||||
error: {
|
||||
borderRadius: 6,
|
||||
marginTop: 10,
|
||||
},
|
||||
// @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.
|
||||
touchable: {
|
||||
...(isWeb && {cursor: 'pointer'}),
|
||||
marginBottom: 10,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {CreateAccountState, CreateAccountDispatch} from './state'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
||||
import {
|
||||
CreateAccountState,
|
||||
CreateAccountDispatch,
|
||||
useSubmitCreateAccount,
|
||||
} from './state'
|
||||
import {StepHeader} from './StepHeader'
|
||||
import {s} from 'lib/styles'
|
||||
import {TextInput} from '../util/TextInput'
|
||||
import {createFullHandle} from 'lib/strings/handles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
/** STEP 3: Your user handle
|
||||
* @field User handle
|
||||
*/
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
import {CaptchaWebView} from 'view/com/auth/create/CaptchaWebView'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {createFullHandle} from 'lib/strings/handles'
|
||||
|
||||
const CAPTCHA_PATH = '/gate/signup'
|
||||
|
||||
export function Step3({
|
||||
uiState,
|
||||
uiDispatch,
|
||||
|
@ -21,33 +25,66 @@ export function Step3({
|
|||
uiState: CreateAccountState
|
||||
uiDispatch: CreateAccountDispatch
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const theme = useTheme()
|
||||
const submit = useSubmitCreateAccount(uiState, uiDispatch)
|
||||
|
||||
const [completed, setCompleted] = React.useState(false)
|
||||
|
||||
const stateParam = React.useMemo(() => nanoid(15), [])
|
||||
const url = React.useMemo(() => {
|
||||
const newUrl = new URL(uiState.serviceUrl)
|
||||
newUrl.pathname = CAPTCHA_PATH
|
||||
newUrl.searchParams.set(
|
||||
'handle',
|
||||
createFullHandle(uiState.handle, uiState.userDomain),
|
||||
)
|
||||
newUrl.searchParams.set('state', stateParam)
|
||||
newUrl.searchParams.set('colorScheme', theme.colorScheme)
|
||||
|
||||
console.log(newUrl)
|
||||
|
||||
return newUrl.href
|
||||
}, [
|
||||
uiState.serviceUrl,
|
||||
uiState.handle,
|
||||
uiState.userDomain,
|
||||
stateParam,
|
||||
theme.colorScheme,
|
||||
])
|
||||
|
||||
const onSuccess = React.useCallback(
|
||||
(code: string) => {
|
||||
setCompleted(true)
|
||||
submit(code)
|
||||
},
|
||||
[submit],
|
||||
)
|
||||
|
||||
const onError = React.useCallback(() => {
|
||||
uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Error receiving captcha response.`),
|
||||
})
|
||||
}, [_, uiDispatch])
|
||||
|
||||
return (
|
||||
<View>
|
||||
<StepHeader uiState={uiState} title={_(msg`Your user handle`)} />
|
||||
<View style={s.pb10}>
|
||||
<TextInput
|
||||
testID="handleInput"
|
||||
icon="at"
|
||||
placeholder="e.g. alice"
|
||||
value={uiState.handle}
|
||||
editable
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
autoCorrect={false}
|
||||
onChange={value => uiDispatch({type: 'set-handle', value})}
|
||||
// TODO: Add explicit text label
|
||||
accessibilityLabel={_(msg`User handle`)}
|
||||
accessibilityHint={_(msg`Input your user handle`)}
|
||||
/>
|
||||
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
|
||||
<Trans>Your full handle will be</Trans>{' '}
|
||||
<Text type="lg-bold" style={pal.text}>
|
||||
@{createFullHandle(uiState.handle, uiState.userDomain)}
|
||||
</Text>
|
||||
</Text>
|
||||
<StepHeader uiState={uiState} title={_(msg`Complete the challenge`)} />
|
||||
<View style={[styles.container, completed && styles.center]}>
|
||||
{!completed ? (
|
||||
<CaptchaWebView
|
||||
url={url}
|
||||
stateParam={stateParam}
|
||||
uiState={uiState}
|
||||
onSuccess={onSuccess}
|
||||
onError={onError}
|
||||
/>
|
||||
) : (
|
||||
<ActivityIndicator size="large" />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{uiState.error ? (
|
||||
<ErrorMessage message={uiState.error} style={styles.error} />
|
||||
) : undefined}
|
||||
|
@ -58,5 +95,20 @@ export function Step3({
|
|||
const styles = StyleSheet.create({
|
||||
error: {
|
||||
borderRadius: 6,
|
||||
marginTop: 10,
|
||||
},
|
||||
// @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.
|
||||
touchable: {
|
||||
...(isWeb && {cursor: 'pointer'}),
|
||||
},
|
||||
container: {
|
||||
minHeight: 500,
|
||||
width: '100%',
|
||||
paddingBottom: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
center: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -11,7 +11,7 @@ export function StepHeader({
|
|||
children,
|
||||
}: React.PropsWithChildren<{uiState: CreateAccountState; title: string}>) {
|
||||
const pal = usePalette('default')
|
||||
const numSteps = uiState.isPhoneVerificationRequired ? 3 : 2
|
||||
const numSteps = 3
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import {useReducer} from 'react'
|
||||
import {useCallback, useReducer} from 'react'
|
||||
import {
|
||||
ComAtprotoServerDescribeServer,
|
||||
ComAtprotoServerCreateAccount,
|
||||
BskyAgent,
|
||||
} from '@atproto/api'
|
||||
import {I18nContext, useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
@ -11,10 +10,14 @@ import {getAge} from 'lib/strings/time'
|
|||
import {logger} from '#/logger'
|
||||
import {createFullHandle} from '#/lib/strings/handles'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding'
|
||||
import {ApiContext as SessionApiContext} from '#/state/session'
|
||||
import {DEFAULT_SERVICE} from '#/lib/constants'
|
||||
import parsePhoneNumber, {CountryCode} from 'libphonenumber-js'
|
||||
import {useOnboardingDispatch} from '#/state/shell/onboarding'
|
||||
import {useSessionApi} from '#/state/session'
|
||||
import {DEFAULT_SERVICE, IS_PROD} from '#/lib/constants'
|
||||
import {
|
||||
DEFAULT_PROD_FEEDS,
|
||||
usePreferencesSetBirthDateMutation,
|
||||
useSetSaveFeedsMutation,
|
||||
} from 'state/queries/preferences'
|
||||
|
||||
export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
|
||||
const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
|
||||
|
@ -29,10 +32,6 @@ export type CreateAccountAction =
|
|||
| {type: 'set-invite-code'; value: string}
|
||||
| {type: 'set-email'; value: string}
|
||||
| {type: 'set-password'; value: string}
|
||||
| {type: 'set-phone-country'; value: CountryCode}
|
||||
| {type: 'set-verification-phone'; value: string}
|
||||
| {type: 'set-verification-code'; value: string}
|
||||
| {type: 'set-has-requested-verification-code'; value: boolean}
|
||||
| {type: 'set-handle'; value: string}
|
||||
| {type: 'set-birth-date'; value: Date}
|
||||
| {type: 'next'}
|
||||
|
@ -49,10 +48,6 @@ export interface CreateAccountState {
|
|||
inviteCode: string
|
||||
email: string
|
||||
password: string
|
||||
phoneCountry: CountryCode
|
||||
verificationPhone: string
|
||||
verificationCode: string
|
||||
hasRequestedVerificationCode: boolean
|
||||
handle: string
|
||||
birthDate: Date
|
||||
|
||||
|
@ -60,13 +55,14 @@ export interface CreateAccountState {
|
|||
canBack: boolean
|
||||
canNext: boolean
|
||||
isInviteCodeRequired: boolean
|
||||
isPhoneVerificationRequired: boolean
|
||||
isCaptchaRequired: boolean
|
||||
}
|
||||
|
||||
export type CreateAccountDispatch = (action: CreateAccountAction) => void
|
||||
|
||||
export function useCreateAccount() {
|
||||
const {_} = useLingui()
|
||||
|
||||
return useReducer(createReducer({_}), {
|
||||
step: 1,
|
||||
error: undefined,
|
||||
|
@ -77,144 +73,126 @@ export function useCreateAccount() {
|
|||
inviteCode: '',
|
||||
email: '',
|
||||
password: '',
|
||||
phoneCountry: 'US',
|
||||
verificationPhone: '',
|
||||
verificationCode: '',
|
||||
hasRequestedVerificationCode: false,
|
||||
handle: '',
|
||||
birthDate: DEFAULT_DATE,
|
||||
|
||||
canBack: false,
|
||||
canNext: false,
|
||||
isInviteCodeRequired: false,
|
||||
isPhoneVerificationRequired: false,
|
||||
isCaptchaRequired: false,
|
||||
})
|
||||
}
|
||||
|
||||
export async function requestVerificationCode({
|
||||
uiState,
|
||||
uiDispatch,
|
||||
_,
|
||||
}: {
|
||||
uiState: CreateAccountState
|
||||
uiDispatch: CreateAccountDispatch
|
||||
_: I18nContext['_']
|
||||
}) {
|
||||
const phoneNumber = parsePhoneNumber(
|
||||
uiState.verificationPhone,
|
||||
uiState.phoneCountry,
|
||||
)?.number
|
||||
if (!phoneNumber) {
|
||||
return
|
||||
}
|
||||
uiDispatch({type: 'set-error', value: ''})
|
||||
uiDispatch({type: 'set-processing', value: true})
|
||||
uiDispatch({type: 'set-verification-phone', value: phoneNumber})
|
||||
try {
|
||||
const agent = new BskyAgent({service: uiState.serviceUrl})
|
||||
await agent.com.atproto.temp.requestPhoneVerification({
|
||||
phoneNumber,
|
||||
})
|
||||
uiDispatch({type: 'set-has-requested-verification-code', value: true})
|
||||
} catch (e: any) {
|
||||
logger.error(
|
||||
`Failed to request sms verification code (${e.status} status)`,
|
||||
{message: e},
|
||||
)
|
||||
uiDispatch({type: 'set-error', value: cleanError(e.toString())})
|
||||
}
|
||||
uiDispatch({type: 'set-processing', value: false})
|
||||
}
|
||||
export function useSubmitCreateAccount(
|
||||
uiState: CreateAccountState,
|
||||
uiDispatch: CreateAccountDispatch,
|
||||
) {
|
||||
const {_} = useLingui()
|
||||
const {createAccount} = useSessionApi()
|
||||
const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
|
||||
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
|
||||
const onboardingDispatch = useOnboardingDispatch()
|
||||
|
||||
export async function submit({
|
||||
createAccount,
|
||||
onboardingDispatch,
|
||||
uiState,
|
||||
uiDispatch,
|
||||
_,
|
||||
}: {
|
||||
createAccount: SessionApiContext['createAccount']
|
||||
onboardingDispatch: OnboardingDispatchContext
|
||||
uiState: CreateAccountState
|
||||
uiDispatch: CreateAccountDispatch
|
||||
_: I18nContext['_']
|
||||
}) {
|
||||
if (!uiState.email) {
|
||||
uiDispatch({type: 'set-step', value: 1})
|
||||
return uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Please enter your email.`),
|
||||
})
|
||||
}
|
||||
if (!EmailValidator.validate(uiState.email)) {
|
||||
uiDispatch({type: 'set-step', value: 1})
|
||||
return uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Your email appears to be invalid.`),
|
||||
})
|
||||
}
|
||||
if (!uiState.password) {
|
||||
uiDispatch({type: 'set-step', value: 1})
|
||||
return uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Please choose your password.`),
|
||||
})
|
||||
}
|
||||
if (
|
||||
uiState.isPhoneVerificationRequired &&
|
||||
(!uiState.verificationPhone || !uiState.verificationCode)
|
||||
) {
|
||||
uiDispatch({type: 'set-step', value: 2})
|
||||
return uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Please enter the code you received by SMS.`),
|
||||
})
|
||||
}
|
||||
if (!uiState.handle) {
|
||||
uiDispatch({type: 'set-step', value: 3})
|
||||
return uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Please choose your handle.`),
|
||||
})
|
||||
}
|
||||
uiDispatch({type: 'set-error', value: ''})
|
||||
uiDispatch({type: 'set-processing', value: true})
|
||||
return useCallback(
|
||||
async (verificationCode?: string) => {
|
||||
if (!uiState.email) {
|
||||
uiDispatch({type: 'set-step', value: 1})
|
||||
console.log('no email?')
|
||||
return uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Please enter your email.`),
|
||||
})
|
||||
}
|
||||
if (!EmailValidator.validate(uiState.email)) {
|
||||
uiDispatch({type: 'set-step', value: 1})
|
||||
return uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Your email appears to be invalid.`),
|
||||
})
|
||||
}
|
||||
if (!uiState.password) {
|
||||
uiDispatch({type: 'set-step', value: 1})
|
||||
return uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Please choose your password.`),
|
||||
})
|
||||
}
|
||||
if (!uiState.handle) {
|
||||
uiDispatch({type: 'set-step', value: 2})
|
||||
return uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Please choose your handle.`),
|
||||
})
|
||||
}
|
||||
if (uiState.isCaptchaRequired && !verificationCode) {
|
||||
uiDispatch({type: 'set-step', value: 3})
|
||||
return uiDispatch({
|
||||
type: 'set-error',
|
||||
value: _(msg`Please complete the verification captcha.`),
|
||||
})
|
||||
}
|
||||
uiDispatch({type: 'set-error', value: ''})
|
||||
uiDispatch({type: 'set-processing', value: true})
|
||||
|
||||
try {
|
||||
onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
|
||||
await createAccount({
|
||||
service: uiState.serviceUrl,
|
||||
email: uiState.email,
|
||||
handle: createFullHandle(uiState.handle, uiState.userDomain),
|
||||
password: uiState.password,
|
||||
inviteCode: uiState.inviteCode.trim(),
|
||||
verificationPhone: uiState.verificationPhone.trim(),
|
||||
verificationCode: uiState.verificationCode.trim(),
|
||||
})
|
||||
} catch (e: any) {
|
||||
onboardingDispatch({type: 'skip'}) // undo starting the onboard
|
||||
let errMsg = e.toString()
|
||||
if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
|
||||
errMsg = _(
|
||||
msg`Invite code not accepted. Check that you input it correctly and try again.`,
|
||||
)
|
||||
uiDispatch({type: 'set-step', value: 1})
|
||||
} else if (e.error === 'InvalidPhoneVerification') {
|
||||
uiDispatch({type: 'set-step', value: 2})
|
||||
}
|
||||
try {
|
||||
onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
|
||||
await createAccount({
|
||||
service: uiState.serviceUrl,
|
||||
email: uiState.email,
|
||||
handle: createFullHandle(uiState.handle, uiState.userDomain),
|
||||
password: uiState.password,
|
||||
inviteCode: uiState.inviteCode.trim(),
|
||||
verificationCode: uiState.isCaptchaRequired
|
||||
? verificationCode
|
||||
: undefined,
|
||||
})
|
||||
setBirthDate({birthDate: uiState.birthDate})
|
||||
if (IS_PROD(uiState.serviceUrl)) {
|
||||
setSavedFeeds(DEFAULT_PROD_FEEDS)
|
||||
}
|
||||
} catch (e: any) {
|
||||
onboardingDispatch({type: 'skip'}) // undo starting the onboard
|
||||
let errMsg = e.toString()
|
||||
if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
|
||||
errMsg = _(
|
||||
msg`Invite code not accepted. Check that you input it correctly and try again.`,
|
||||
)
|
||||
uiDispatch({type: 'set-step', value: 1})
|
||||
}
|
||||
|
||||
if ([400, 429].includes(e.status)) {
|
||||
logger.warn('Failed to create account', {message: e})
|
||||
} else {
|
||||
logger.error(`Failed to create account (${e.status} status)`, {
|
||||
message: e,
|
||||
})
|
||||
}
|
||||
if ([400, 429].includes(e.status)) {
|
||||
logger.warn('Failed to create account', {message: e})
|
||||
} else {
|
||||
logger.error(`Failed to create account (${e.status} status)`, {
|
||||
message: e,
|
||||
})
|
||||
}
|
||||
|
||||
uiDispatch({type: 'set-processing', value: false})
|
||||
uiDispatch({type: 'set-error', value: cleanError(errMsg)})
|
||||
throw e
|
||||
}
|
||||
const error = cleanError(errMsg)
|
||||
const isHandleError = error.toLowerCase().includes('handle')
|
||||
|
||||
uiDispatch({type: 'set-processing', value: false})
|
||||
uiDispatch({type: 'set-error', value: cleanError(errMsg)})
|
||||
uiDispatch({type: 'set-step', value: isHandleError ? 2 : 1})
|
||||
}
|
||||
},
|
||||
[
|
||||
uiState.email,
|
||||
uiState.password,
|
||||
uiState.handle,
|
||||
uiState.isCaptchaRequired,
|
||||
uiState.serviceUrl,
|
||||
uiState.userDomain,
|
||||
uiState.inviteCode,
|
||||
uiState.birthDate,
|
||||
uiDispatch,
|
||||
_,
|
||||
onboardingDispatch,
|
||||
createAccount,
|
||||
setBirthDate,
|
||||
setSavedFeeds,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
export function is13(state: CreateAccountState) {
|
||||
|
@ -269,22 +247,6 @@ function createReducer({_}: {_: I18nContext['_']}) {
|
|||
case 'set-password': {
|
||||
return compute({...state, password: action.value})
|
||||
}
|
||||
case 'set-phone-country': {
|
||||
return compute({...state, phoneCountry: action.value})
|
||||
}
|
||||
case 'set-verification-phone': {
|
||||
return compute({
|
||||
...state,
|
||||
verificationPhone: action.value,
|
||||
hasRequestedVerificationCode: false,
|
||||
})
|
||||
}
|
||||
case 'set-verification-code': {
|
||||
return compute({...state, verificationCode: action.value.trim()})
|
||||
}
|
||||
case 'set-has-requested-verification-code': {
|
||||
return compute({...state, hasRequestedVerificationCode: action.value})
|
||||
}
|
||||
case 'set-handle': {
|
||||
return compute({...state, handle: action.value})
|
||||
}
|
||||
|
@ -302,18 +264,10 @@ function createReducer({_}: {_: I18nContext['_']}) {
|
|||
})
|
||||
}
|
||||
}
|
||||
let increment = 1
|
||||
if (state.step === 1 && !state.isPhoneVerificationRequired) {
|
||||
increment = 2
|
||||
}
|
||||
return compute({...state, error: '', step: state.step + increment})
|
||||
return compute({...state, error: '', step: state.step + 1})
|
||||
}
|
||||
case 'back': {
|
||||
let decrement = 1
|
||||
if (state.step === 3 && !state.isPhoneVerificationRequired) {
|
||||
decrement = 2
|
||||
}
|
||||
return compute({...state, error: '', step: state.step - decrement})
|
||||
return compute({...state, error: '', step: state.step - 1})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -328,23 +282,16 @@ function compute(state: CreateAccountState): CreateAccountState {
|
|||
!!state.email &&
|
||||
!!state.password
|
||||
} else if (state.step === 2) {
|
||||
canNext =
|
||||
!state.isPhoneVerificationRequired ||
|
||||
(!!state.verificationPhone &&
|
||||
isValidVerificationCode(state.verificationCode))
|
||||
} else if (state.step === 3) {
|
||||
canNext = !!state.handle
|
||||
} else if (state.step === 3) {
|
||||
// Step 3 will automatically redirect as soon as the captcha completes
|
||||
canNext = false
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
canBack: state.step > 1,
|
||||
canNext,
|
||||
isInviteCodeRequired: !!state.serviceDescription?.inviteCodeRequired,
|
||||
isPhoneVerificationRequired:
|
||||
!!state.serviceDescription?.phoneVerificationRequired,
|
||||
isCaptchaRequired: !!state.serviceDescription?.phoneVerificationRequired,
|
||||
}
|
||||
}
|
||||
|
||||
function isValidVerificationCode(str: string): boolean {
|
||||
return /[0-9]{6}/.test(str)
|
||||
}
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -15158,11 +15158,6 @@ levn@^0.4.1:
|
|||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
libphonenumber-js@^1.10.53:
|
||||
version "1.10.53"
|
||||
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.53.tgz#8dbfe1355ef1a3d8e13b8d92849f7db7ebddc98f"
|
||||
integrity sha512-sDTnnqlWK4vH4AlDQuswz3n4Hx7bIQWTpIcScJX+Sp7St3LXHmfiax/ZFfyYxHmkdCvydOLSuvtAO/XpXiSySw==
|
||||
|
||||
lie@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||
|
@ -16157,10 +16152,10 @@ nanoid@^3.1.23, nanoid@^3.3.1, nanoid@^3.3.6:
|
|||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
||||
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
||||
|
||||
nanoid@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.2.tgz#97588ebc70166d0feaf73ccd2799bb4ceaebf692"
|
||||
integrity sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==
|
||||
nanoid@^5.0.5:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.5.tgz#5112efb5c0caf4fc80680d66d303c65233a79fdd"
|
||||
integrity sha512-/Veqm+QKsyMY3kqi4faWplnY1u+VuKO3dD2binyPIybP31DRO29bPF+1mszgLnrR2KqSLceFLBNw0zmvDzN1QQ==
|
||||
|
||||
napi-build-utils@^1.0.1:
|
||||
version "1.0.2"
|
||||
|
|
Loading…
Reference in New Issue