diff --git a/native/more/more-screen.react.js b/native/more/more-screen.react.js index a42be5897..fa34e197a 100644 --- a/native/more/more-screen.react.js +++ b/native/more/more-screen.react.js @@ -1,558 +1,552 @@ // @flow import invariant from 'invariant'; -import PropTypes from 'prop-types'; import * as React from 'react'; import { View, Text, Alert, Platform, ScrollView, ActivityIndicator, } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; import { logOutActionTypes, logOut, resendVerificationEmailActionTypes, resendVerificationEmail, } from 'lib/actions/user-actions'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import type { LogOutResult } from 'lib/types/account-types'; +import { type PreRequestUserState } from 'lib/types/session-types'; +import { type CurrentUserInfo } from 'lib/types/user-types'; import { - type PreRequestUserState, - preRequestUserStatePropType, -} from 'lib/types/session-types'; -import { - type CurrentUserInfo, - currentUserPropType, -} from 'lib/types/user-types'; -import type { DispatchActionPromise } from 'lib/utils/action-utils'; -import { connect } from 'lib/utils/redux-utils'; + type DispatchActionPromise, + useDispatchActionPromise, + useServerCall, +} from 'lib/utils/action-utils'; import { firstLine } from 'lib/utils/string-utils'; import { getNativeSharedWebCredentials, deleteNativeCredentialsFor, } from '../account/native-credentials'; import Button from '../components/button.react'; import EditSettingButton from '../components/edit-setting-button.react'; import { SingleLine } from '../components/single-line.react'; +import type { NavigationRoute } from '../navigation/route-names'; import { EditEmailRouteName, EditPasswordRouteName, DeleteAccountRouteName, BuildInfoRouteName, DevToolsRouteName, AppearancePreferencesRouteName, FriendListRouteName, BlockListRouteName, } from '../navigation/route-names'; -import type { AppState } from '../redux/redux-setup'; -import { - type Colors, - colorsPropType, - colorsSelector, - styleSelector, -} from '../themes/colors'; +import { useSelector } from '../redux/redux-utils'; +import { type Colors, useColors, useStyles } from '../themes/colors'; import type { MoreNavigationProp } from './more.react'; -type Props = { - navigation: MoreNavigationProp<'MoreScreen'>, - // Redux state - currentUserInfo: ?CurrentUserInfo, - preRequestUserState: PreRequestUserState, - resendVerificationLoading: boolean, - logOutLoading: boolean, - colors: Colors, - styles: typeof styles, - // Redux dispatch functions - dispatchActionPromise: DispatchActionPromise, - // async functions that hit server APIs - logOut: (preRequestUserState: PreRequestUserState) => Promise, - resendVerificationEmail: () => Promise, -}; +type BaseProps = {| + +navigation: MoreNavigationProp<'MoreScreen'>, + +route: NavigationRoute<'MoreScreen'>, +|}; +type Props = {| + ...BaseProps, + +currentUserInfo: ?CurrentUserInfo, + +preRequestUserState: PreRequestUserState, + +resendVerificationLoading: boolean, + +logOutLoading: boolean, + +colors: Colors, + +styles: typeof unboundStyles, + +dispatchActionPromise: DispatchActionPromise, + +logOut: (preRequestUserState: PreRequestUserState) => Promise, + +resendVerificationEmail: () => Promise, +|}; class MoreScreen extends React.PureComponent { - static propTypes = { - navigation: PropTypes.shape({ - navigate: PropTypes.func.isRequired, - }).isRequired, - currentUserInfo: currentUserPropType, - preRequestUserState: preRequestUserStatePropType.isRequired, - resendVerificationLoading: PropTypes.bool.isRequired, - logOutLoading: PropTypes.bool.isRequired, - colors: colorsPropType.isRequired, - styles: PropTypes.objectOf(PropTypes.object).isRequired, - dispatchActionPromise: PropTypes.func.isRequired, - logOut: PropTypes.func.isRequired, - resendVerificationEmail: PropTypes.func.isRequired, - }; - get username() { return this.props.currentUserInfo && !this.props.currentUserInfo.anonymous ? this.props.currentUserInfo.username : undefined; } get email() { return this.props.currentUserInfo && !this.props.currentUserInfo.anonymous ? this.props.currentUserInfo.email : undefined; } get emailVerified() { return this.props.currentUserInfo && !this.props.currentUserInfo.anonymous ? this.props.currentUserInfo.emailVerified : undefined; } get loggedOutOrLoggingOut() { return ( !this.props.currentUserInfo || this.props.currentUserInfo.anonymous || this.props.logOutLoading ); } render() { const { emailVerified } = this; let emailVerifiedNode = null; if (emailVerified === true) { emailVerifiedNode = ( Verified ); } else if (emailVerified === false) { let resendVerificationEmailSpinner; if (this.props.resendVerificationLoading) { resendVerificationEmailSpinner = ( ); } emailVerifiedNode = ( Not verified {' - '} ); } const { panelIosHighlightUnderlay: underlay, link: linkColor, } = this.props.colors; return ( {'Logged in as '} {firstLine(this.username)} ACCOUNT Email {this.email} {emailVerifiedNode} Password •••••••••••••••• PREFERENCES ); } onPressLogOut = () => { if (this.loggedOutOrLoggingOut) { return; } const alertTitle = Platform.OS === 'ios' ? 'Keep Login Info in Keychain' : 'Keep Login Info'; const sharedWebCredentials = getNativeSharedWebCredentials(); const alertDescription = sharedWebCredentials ? 'We will automatically fill out log-in forms with your credentials ' + 'in the app and keep them available on squadcal.org in Safari.' : 'We will automatically fill out log-in forms with your credentials ' + 'in the app.'; Alert.alert( alertTitle, alertDescription, [ { text: 'Cancel', style: 'cancel' }, { text: 'Keep', onPress: this.logOutButKeepNativeCredentialsWrapper }, { text: 'Remove', onPress: this.logOutAndDeleteNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); }; logOutButKeepNativeCredentialsWrapper = () => { if (this.loggedOutOrLoggingOut) { return; } this.props.dispatchActionPromise(logOutActionTypes, this.logOut()); }; logOutAndDeleteNativeCredentialsWrapper = () => { if (this.loggedOutOrLoggingOut) { return; } this.props.dispatchActionPromise( logOutActionTypes, this.logOutAndDeleteNativeCredentials(), ); }; logOut() { return this.props.logOut(this.props.preRequestUserState); } async logOutAndDeleteNativeCredentials() { const { username } = this; invariant(username, "can't log out if not logged in"); await deleteNativeCredentialsFor(username); return await this.logOut(); } onPressResendVerificationEmail = () => { this.props.dispatchActionPromise( resendVerificationEmailActionTypes, this.resendVerificationEmailAction(), ); }; async resendVerificationEmailAction() { await this.props.resendVerificationEmail(); Alert.alert( 'Verify email', "We've sent you an email to verify your email address. Just click on " + 'the link in the email to complete the verification process.', undefined, { cancelable: true }, ); } navigateIfActive(name) { this.props.navigation.navigate({ name }); } onPressEditEmail = () => { this.navigateIfActive(EditEmailRouteName); }; onPressEditPassword = () => { this.navigateIfActive(EditPasswordRouteName); }; onPressDeleteAccount = () => { this.navigateIfActive(DeleteAccountRouteName); }; onPressBuildInfo = () => { this.navigateIfActive(BuildInfoRouteName); }; onPressDevTools = () => { this.navigateIfActive(DevToolsRouteName); }; onPressAppearance = () => { this.navigateIfActive(AppearancePreferencesRouteName); }; onPressFriendList = () => { this.navigateIfActive(FriendListRouteName); }; onPressBlockList = () => { this.navigateIfActive(BlockListRouteName); }; } -const styles = { +const unboundStyles = { container: { flex: 1, }, content: { flex: 1, }, deleteAccountButton: { paddingHorizontal: 24, paddingVertical: 12, }, deleteAccountText: { color: 'redText', flex: 1, fontSize: 16, }, editEmailButton: { paddingTop: Platform.OS === 'android' ? 9 : 7, }, editPasswordButton: { paddingTop: Platform.OS === 'android' ? 3 : 2, }, emailNotVerified: { color: 'redText', }, emailVerified: { color: 'greenText', }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, label: { color: 'panelForegroundTertiaryLabel', fontSize: 16, paddingRight: 12, }, logOutText: { color: 'link', fontSize: 16, paddingLeft: 6, }, resendVerificationEmailButton: { flexDirection: 'row', paddingRight: 1, }, resendVerificationEmailSpinner: { marginTop: Platform.OS === 'ios' ? -4 : 0, paddingHorizontal: 4, }, resendVerificationEmailText: { color: 'link', fontStyle: 'italic', }, row: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, paddingHorizontal: 24, paddingVertical: 12, }, slightlyPaddedSection: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, paddingVertical: 2, }, submenuButton: { flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 10, }, submenuText: { color: 'panelForegroundLabel', flex: 1, fontSize: 16, }, unpaddedSection: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, }, username: { color: 'panelForegroundLabel', }, value: { color: 'panelForegroundLabel', fontSize: 16, textAlign: 'right', }, verification: { alignSelf: 'flex-end', flexDirection: 'row', height: 20, }, verificationSection: { alignSelf: 'flex-end', flexDirection: 'row', height: 20, }, verificationText: { color: 'panelForegroundLabel', fontSize: 13, fontStyle: 'italic', }, }; -const stylesSelector = styleSelector(styles); const logOutLoadingStatusSelector = createLoadingStatusSelector( logOutActionTypes, ); const resendVerificationLoadingStatusSelector = createLoadingStatusSelector( resendVerificationEmailActionTypes, ); -export default connect( - (state: AppState) => ({ - currentUserInfo: state.currentUserInfo, - preRequestUserState: preRequestUserStateSelector(state), - resendVerificationLoading: - resendVerificationLoadingStatusSelector(state) === 'loading', - logOutLoading: logOutLoadingStatusSelector(state) === 'loading', - colors: colorsSelector(state), - styles: stylesSelector(state), - }), - { logOut, resendVerificationEmail }, -)(MoreScreen); +export default React.memo(function ConnectedMoreScreen( + props: BaseProps, +) { + const currentUserInfo = useSelector((state) => state.currentUserInfo); + const preRequestUserState = useSelector(preRequestUserStateSelector); + const resendVerificationLoading = + useSelector(resendVerificationLoadingStatusSelector) === 'loading'; + const logOutLoading = useSelector(logOutLoadingStatusSelector) === 'loading'; + const colors = useColors(); + const styles = useStyles(unboundStyles); + const callLogOut = useServerCall(logOut); + const callResendVerificationEmail = useServerCall(resendVerificationEmail); + const dispatchActionPromise = useDispatchActionPromise(); + + return ( + + ); +});