diff --git a/lib/components/farcaster-data-handler.react.js b/lib/components/farcaster-data-handler.react.js index 1d786e63e..dc7a077a4 100644 --- a/lib/components/farcaster-data-handler.react.js +++ b/lib/components/farcaster-data-handler.react.js @@ -1,207 +1,207 @@ // @flow import * as React from 'react'; import { setAuxUserFIDsActionType } from '../actions/aux-user-actions.js'; import { updateRelationships as serverUpdateRelationships, updateRelationshipsActionTypes, } from '../actions/relationship-actions.js'; import { setSyncedMetadataEntryActionType } from '../actions/synced-metadata-actions.js'; import { NeynarClientContext } from '../components/neynar-client-provider.react.js'; +import { useIsLoggedInToIdentityAndAuthoritativeKeyserver } from '../hooks/account-hooks.js'; import { useLegacyAshoatKeyserverCall } from '../keyserver-conn/legacy-keyserver-call.js'; -import { isLoggedInToIdentityAndAuthoritativeKeyserver } from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { relationshipActions } from '../types/relationship-types.js'; import { syncedMetadataNames } from '../types/synced-metadata-types.js'; import { useCurrentUserFID, useUnlinkFID } from '../utils/farcaster-utils.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; function FarcasterDataHandler(): React.Node { const isActive = useSelector(state => state.lifecycleState !== 'background'); const currentUserID = useSelector(state => state.currentUserInfo?.id); - const loggedIn = useSelector(isLoggedInToIdentityAndAuthoritativeKeyserver); + const loggedIn = useIsLoggedInToIdentityAndAuthoritativeKeyserver(); const neynarClient = React.useContext(NeynarClientContext)?.client; const identityServiceClient = React.useContext(IdentityClientContext); const getFarcasterUsers = identityServiceClient?.identityClient.getFarcasterUsers; const findUserIdentities = identityServiceClient?.identityClient.findUserIdentities; const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const updateRelationships = useLegacyAshoatKeyserverCall( serverUpdateRelationships, ); const createThreadsAndRobotextForFarcasterMutuals = React.useCallback( (userIDsToFID: { +[userID: string]: string }) => updateRelationships({ action: relationshipActions.FARCASTER_MUTUAL, userIDsToFID, }), [updateRelationships], ); const userInfos = useSelector(state => state.userStore.userInfos); const fid = useCurrentUserFID(); const unlinkFID = useUnlinkFID(); const prevCanQueryRef = React.useRef(); const handleFarcasterMutuals = React.useCallback(async () => { const canQuery = isActive && !!fid && loggedIn; if (canQuery === prevCanQueryRef.current) { return; } prevCanQueryRef.current = canQuery; if ( !loggedIn || !isActive || !fid || !neynarClient || !getFarcasterUsers || !currentUserID ) { return; } const followerFIDs = await neynarClient.fetchFriendFIDs(fid); const commFCUsers = await getFarcasterUsers(followerFIDs); const newCommUsers = commFCUsers.filter(({ userID }) => !userInfos[userID]); if (newCommUsers.length === 0) { return; } const userIDsToFID: { +[userID: string]: string } = Object.fromEntries( newCommUsers.map(({ userID, farcasterID }) => [userID, farcasterID]), ); const userIDsToFIDIncludingCurrentUser: { +[userID: string]: string } = { ...userIDsToFID, [(currentUserID: string)]: fid, }; void dispatchActionPromise( updateRelationshipsActionTypes, createThreadsAndRobotextForFarcasterMutuals( userIDsToFIDIncludingCurrentUser, ), ); }, [ isActive, fid, loggedIn, neynarClient, getFarcasterUsers, userInfos, dispatchActionPromise, createThreadsAndRobotextForFarcasterMutuals, currentUserID, ]); const handleUserStoreFIDs = React.useCallback(async () => { if (!loggedIn || !isActive || !findUserIdentities) { return; } const userStoreIDs = Object.keys(userInfos); const userIdentities = await findUserIdentities(userStoreIDs); const userStoreFarcasterUsers = Object.entries(userIdentities) .filter(([, identity]) => identity.farcasterID !== null) .map(([userID, identity]) => ({ userID, username: identity.username, farcasterID: identity.farcasterID, })); dispatch({ type: setAuxUserFIDsActionType, payload: { farcasterUsers: userStoreFarcasterUsers, }, }); }, [loggedIn, isActive, findUserIdentities, userInfos, dispatch]); const prevCanQueryHandleCurrentUserFIDRef = React.useRef(); const canQueryHandleCurrentUserFID = isActive && loggedIn; const handleCurrentUserFID = React.useCallback(async () => { if ( canQueryHandleCurrentUserFID === prevCanQueryHandleCurrentUserFIDRef.current ) { return; } prevCanQueryHandleCurrentUserFIDRef.current = canQueryHandleCurrentUserFID; if ( !canQueryHandleCurrentUserFID || !findUserIdentities || !currentUserID || !neynarClient ) { return; } if (fid) { const isCurrentUserFIDValid = await neynarClient.checkIfCurrentUserFIDIsValid(fid); if (!isCurrentUserFIDValid) { await unlinkFID(); return; } return; } const currentUserIdentityObj = await findUserIdentities([currentUserID]); const identityFID = currentUserIdentityObj[currentUserID]?.farcasterID; if (identityFID) { dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.CURRENT_USER_FID, data: identityFID, }, }); } }, [ canQueryHandleCurrentUserFID, findUserIdentities, currentUserID, neynarClient, fid, unlinkFID, dispatch, ]); React.useEffect(() => { if (!usingCommServicesAccessToken) { return; } void handleFarcasterMutuals(); void handleUserStoreFIDs(); void handleCurrentUserFID(); }, [handleCurrentUserFID, handleFarcasterMutuals, handleUserStoreFIDs]); return null; } export { FarcasterDataHandler }; diff --git a/lib/handlers/user-infos-handler.react.js b/lib/handlers/user-infos-handler.react.js index e77944a7c..850d8ac5e 100644 --- a/lib/handlers/user-infos-handler.react.js +++ b/lib/handlers/user-infos-handler.react.js @@ -1,136 +1,132 @@ // @flow import * as React from 'react'; import { updateRelationships, updateRelationshipsActionTypes, } from '../actions/relationship-actions.js'; import { useFindUserIdentities, findUserIdentitiesActionTypes, } from '../actions/user-actions.js'; +import { useIsLoggedInToAuthoritativeKeyserver } from '../hooks/account-hooks.js'; import { useGetAndUpdateDeviceListsForUsers } from '../hooks/peer-list-hooks.js'; import { useLegacyAshoatKeyserverCall } from '../keyserver-conn/legacy-keyserver-call.js'; -import { - usersWithMissingDeviceListSelector, - isLoggedInToAuthoritativeKeyserver, -} from '../selectors/user-selectors.js'; +import { usersWithMissingDeviceListSelector } from '../selectors/user-selectors.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import { relationshipActions } from '../types/relationship-types.js'; import { getMessageForException } from '../utils/errors.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector } from '../utils/redux-utils.js'; import { relyingOnAuthoritativeKeyserver, usingCommServicesAccessToken, } from '../utils/services-utils.js'; function UserInfosHandler(): React.Node { const userInfos = useSelector(state => state.userStore.userInfos); const userInfosWithMissingUsernames = React.useMemo(() => { const entriesWithoutUsernames = Object.entries(userInfos).filter( ([, value]) => !value.username, ); return Object.fromEntries(entriesWithoutUsernames); }, [userInfos]); const dispatchActionPromise = useDispatchActionPromise(); const findUserIdentities = useFindUserIdentities(); const requestedIDsRef = React.useRef(new Set()); const callUpdateRelationships = useLegacyAshoatKeyserverCall(updateRelationships); const currentUserInfo = useSelector(state => state.currentUserInfo); - const loggedInToAuthKeyserver = useSelector( - isLoggedInToAuthoritativeKeyserver, - ); + const loggedInToAuthKeyserver = useIsLoggedInToAuthoritativeKeyserver(); React.useEffect(() => { if (!loggedInToAuthKeyserver) { return; } const newUserIDs = Object.keys(userInfosWithMissingUsernames).filter( id => !requestedIDsRef.current.has(id), ); if (!usingCommServicesAccessToken || newUserIDs.length === 0) { return; } // 1. Fetch usernames from identity const promise = (async () => { newUserIDs.forEach(id => requestedIDsRef.current.add(id)); const identities = await findUserIdentities(newUserIDs); newUserIDs.forEach(id => requestedIDsRef.current.delete(id)); const newUserInfos = []; for (const id in identities) { newUserInfos.push({ id, username: identities[id].username, }); } return { userInfos: newUserInfos }; })(); void dispatchActionPromise(findUserIdentitiesActionTypes, promise); // 2. Fetch avatars from auth keyserver if (relyingOnAuthoritativeKeyserver) { const userIDsWithoutOwnID = newUserIDs.filter( id => id !== currentUserInfo?.id, ); if (userIDsWithoutOwnID.length === 0) { return; } void dispatchActionPromise( updateRelationshipsActionTypes, callUpdateRelationships({ action: relationshipActions.ACKNOWLEDGE, userIDs: userIDsWithoutOwnID, }), ); } }, [ callUpdateRelationships, currentUserInfo?.id, dispatchActionPromise, findUserIdentities, userInfos, userInfosWithMissingUsernames, loggedInToAuthKeyserver, ]); const usersWithMissingDeviceList = useSelector( usersWithMissingDeviceListSelector, ); const getAndUpdateDeviceListsForUsers = useGetAndUpdateDeviceListsForUsers(); const { socketState } = useTunnelbroker(); React.useEffect(() => { if ( !usingCommServicesAccessToken || usersWithMissingDeviceList.length === 0 || !socketState.isAuthorized ) { return; } void (async () => { try { await getAndUpdateDeviceListsForUsers(usersWithMissingDeviceList, true); } catch (e) { console.log( `Error getting and setting peer device list: ${ getMessageForException(e) ?? 'unknown' }`, ); } })(); }, [ socketState.isAuthorized, getAndUpdateDeviceListsForUsers, usersWithMissingDeviceList, ]); } export { UserInfosHandler }; diff --git a/lib/hooks/account-hooks.js b/lib/hooks/account-hooks.js index a6db54a87..860a80e68 100644 --- a/lib/hooks/account-hooks.js +++ b/lib/hooks/account-hooks.js @@ -1,14 +1,35 @@ // @flow +import { + isLoggedIn, + isLoggedInToKeyserver, +} from '../selectors/user-selectors.js'; import type { LoggedInUserInfo } from '../types/user-types.js'; +import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { useSelector } from '../utils/redux-utils.js'; function useLoggedInUserInfo(): ?LoggedInUserInfo { return useSelector(state => state.currentUserInfo && !state.currentUserInfo.anonymous ? state.currentUserInfo : null, ); } -export { useLoggedInUserInfo }; +function useIsLoggedInToAuthoritativeKeyserver(): boolean { + return useSelector(isLoggedInToKeyserver(authoritativeKeyserverID())); +} + +function useIsLoggedInToIdentityAndAuthoritativeKeyserver(): boolean { + return useSelector( + state => + isLoggedInToKeyserver(authoritativeKeyserverID())(state) && + isLoggedIn(state), + ); +} + +export { + useLoggedInUserInfo, + useIsLoggedInToAuthoritativeKeyserver, + useIsLoggedInToIdentityAndAuthoritativeKeyserver, +}; diff --git a/lib/selectors/user-selectors.js b/lib/selectors/user-selectors.js index 900e5b2ef..01d0e5d45 100644 --- a/lib/selectors/user-selectors.js +++ b/lib/selectors/user-selectors.js @@ -1,287 +1,276 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import bots from '../facts/bots.js'; import { getAvatarForUser, getRandomDefaultEmojiAvatar, } from '../shared/avatar-utils.js'; import { getSingleOtherUser } from '../shared/thread-utils.js'; import { type AuxUserInfos, type AuxUserInfo, } from '../types/aux-user-types.js'; import type { ClientEmojiAvatar } from '../types/avatar-types'; import type { RelativeMemberInfo, RawThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import type { BaseAppState } from '../types/redux-types.js'; import { userRelationshipStatus } from '../types/relationship-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; import type { RawThreadInfos } from '../types/thread-types.js'; import type { UserInfos, RelativeUserInfo, AccountUserInfo, CurrentUserInfo, } from '../types/user-types.js'; -import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { entries, values } from '../utils/objects.js'; // Used for specific message payloads that include an array of user IDs, ie. // array of initial users, array of added users function userIDsToRelativeUserInfos( userIDs: $ReadOnlyArray, viewerID: ?string, userInfos: UserInfos, ): RelativeUserInfo[] { const relativeUserInfos: RelativeUserInfo[] = []; for (const userID of userIDs) { const username = userInfos[userID] ? userInfos[userID].username : null; const relativeUserInfo = { id: userID, username, isViewer: userID === viewerID, }; if (userID === viewerID) { relativeUserInfos.unshift(relativeUserInfo); } else { relativeUserInfos.push(relativeUserInfo); } } return relativeUserInfos; } function getRelativeMemberInfos( threadInfo: ?RawThreadInfo, currentUserID: ?string, userInfos: UserInfos, ): $ReadOnlyArray { const relativeMemberInfos: RelativeMemberInfo[] = []; if (!threadInfo) { return relativeMemberInfos; } const memberInfos = threadInfo.members; for (const memberInfo of memberInfos) { if (!memberInfo.role) { continue; } const username = userInfos[memberInfo.id] ? userInfos[memberInfo.id].username : null; const { id, role, isSender, minimallyEncoded } = memberInfo; if (memberInfo.id === currentUserID) { relativeMemberInfos.unshift({ id, role, isSender, minimallyEncoded, username, isViewer: true, }); } else { relativeMemberInfos.push({ id, role, isSender, minimallyEncoded, username, isViewer: false, }); } } return relativeMemberInfos; } const emptyArray: $ReadOnlyArray = []; // Includes current user at the start const baseRelativeMemberInfoSelectorForMembersOfThread: ( threadID: ?string, ) => (state: BaseAppState<>) => $ReadOnlyArray = ( threadID: ?string, ) => { if (!threadID) { return () => emptyArray; } return createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos[threadID], (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, (state: BaseAppState<>) => state.userStore.userInfos, getRelativeMemberInfos, ); }; const relativeMemberInfoSelectorForMembersOfThread: ( threadID: ?string, ) => (state: BaseAppState<>) => $ReadOnlyArray = _memoize( baseRelativeMemberInfoSelectorForMembersOfThread, ); const userInfoSelectorForPotentialMembers: (state: BaseAppState<>) => { [id: string]: AccountUserInfo, } = createSelector( (state: BaseAppState<>) => state.userStore.userInfos, (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, ( userInfos: UserInfos, currentUserID: ?string, ): { [id: string]: AccountUserInfo } => { const availableUsers: { [id: string]: AccountUserInfo } = {}; for (const id in userInfos) { const { username, relationshipStatus } = userInfos[id]; if (id === currentUserID || !username) { continue; } if ( relationshipStatus !== userRelationshipStatus.BLOCKED_VIEWER && relationshipStatus !== userRelationshipStatus.BOTH_BLOCKED ) { availableUsers[id] = { id, username, relationshipStatus }; } } return availableUsers; }, ); const isLoggedIn = (state: BaseAppState<>): boolean => !!( state.currentUserInfo && !state.currentUserInfo.anonymous && state.dataLoaded ); const isLoggedInToKeyserver: ( keyserverID: ?string, ) => (state: BaseAppState<>) => boolean = _memoize( (keyserverID: ?string) => (state: BaseAppState<>) => { if (!keyserverID) { return false; } const cookie = state.keyserverStore.keyserverInfos[keyserverID]?.cookie; return !!cookie && cookie.startsWith('user='); }, ); -const isLoggedInToAuthoritativeKeyserver: (state: BaseAppState<>) => boolean = - isLoggedInToKeyserver(authoritativeKeyserverID()); - -const isLoggedInToIdentityAndAuthoritativeKeyserver: ( - state: BaseAppState<>, -) => boolean = (state: BaseAppState<>) => - isLoggedInToAuthoritativeKeyserver(state) && isLoggedIn(state); - const usersWithPersonalThreadSelector: ( state: BaseAppState<>, ) => $ReadOnlySet = createSelector( (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, (state: BaseAppState<>) => state.threadStore.threadInfos, (viewerID: ?string, threadInfos: RawThreadInfos) => { const personalThreadMembers = new Set(); for (const threadID in threadInfos) { const thread = threadInfos[threadID]; if ( thread.type !== threadTypes.GENESIS_PERSONAL || !thread.members.find(member => member.id === viewerID) ) { continue; } const otherMemberID = getSingleOtherUser(thread, viewerID); if (otherMemberID) { personalThreadMembers.add(otherMemberID); } } return personalThreadMembers; }, ); const savedEmojiAvatarSelectorForCurrentUser: ( state: BaseAppState<>, ) => () => ClientEmojiAvatar = createSelector( (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo, (currentUser: ?CurrentUserInfo) => { return () => { let userAvatar = getAvatarForUser(currentUser); if (userAvatar.type !== 'emoji') { userAvatar = getRandomDefaultEmojiAvatar(); } return userAvatar; }; }, ); const getRelativeUserIDs: (state: BaseAppState<>) => $ReadOnlyArray = createSelector( (state: BaseAppState<>) => state.userStore.userInfos, (userInfos: UserInfos): $ReadOnlyArray => Object.keys(userInfos), ); const usersWithMissingDeviceListSelector: ( state: BaseAppState<>, ) => $ReadOnlyArray = createSelector( getRelativeUserIDs, (state: BaseAppState<>) => state.auxUserStore.auxUserInfos, ( userIDs: $ReadOnlyArray, auxUserInfos: AuxUserInfos, ): $ReadOnlyArray => userIDs.filter( userID => (!auxUserInfos[userID] || !auxUserInfos[userID].deviceList) && userID !== bots.commbot.userID, ), ); // Foreign Peer Devices are all devices of users we are aware of, // but not our own devices. const getForeignPeerDevices: (state: BaseAppState<>) => $ReadOnlyArray = createSelector( (state: BaseAppState<>) => state.auxUserStore.auxUserInfos, (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, ( auxUserInfos: AuxUserInfos, currentUserID: ?string, ): $ReadOnlyArray => entries(auxUserInfos) .map(([userID, auxUserInfo]: [string, AuxUserInfo]) => userID !== currentUserID && auxUserInfo.deviceList?.devices ? auxUserInfo.deviceList.devices : [], ) .flat(), ); const getAllPeerDevices: (state: BaseAppState<>) => $ReadOnlyArray = createSelector( (state: BaseAppState<>) => state.auxUserStore.auxUserInfos, (auxUserInfos: AuxUserInfos): $ReadOnlyArray => values(auxUserInfos) .map( (auxUserInfo: AuxUserInfo) => auxUserInfo.deviceList?.devices ?? [], ) .flat(), ); export { userIDsToRelativeUserInfos, getRelativeMemberInfos, relativeMemberInfoSelectorForMembersOfThread, userInfoSelectorForPotentialMembers, isLoggedIn, isLoggedInToKeyserver, - isLoggedInToAuthoritativeKeyserver, - isLoggedInToIdentityAndAuthoritativeKeyserver, usersWithPersonalThreadSelector, savedEmojiAvatarSelectorForCurrentUser, getRelativeUserIDs, usersWithMissingDeviceListSelector, getForeignPeerDevices, getAllPeerDevices, }; diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js index 12144d3ba..67920f62f 100644 --- a/native/account/logged-out-modal.react.js +++ b/native/account/logged-out-modal.react.js @@ -1,727 +1,723 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome.js'; import * as React from 'react'; import { View, Text, TouchableOpacity, Image, Keyboard, Platform, BackHandler, ActivityIndicator, } from 'react-native'; import { Easing, useSharedValue, withTiming, useAnimatedStyle, runOnJS, } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { useIsLoggedInToAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; import { setActiveSessionRecoveryActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; -import { - isLoggedIn, - isLoggedInToAuthoritativeKeyserver, -} from 'lib/selectors/user-selectors.js'; +import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { recoveryFromReduxActionSources } from 'lib/types/account-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { splashBackgroundURI } from './background-info.js'; import FullscreenSIWEPanel from './fullscreen-siwe-panel.react.js'; import LegacyRegisterPanel from './legacy-register-panel.react.js'; import type { LegacyRegisterState } from './legacy-register-panel.react.js'; import LogInPanel from './log-in-panel.react.js'; import type { LogInState } from './log-in-panel.react.js'; import LoggedOutStaffInfo from './logged-out-staff-info.react.js'; import { enableNewRegistrationMode } from './registration/registration-types.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react.js'; import ConnectedStatusBar from '../connected-status-bar.react.js'; import { useRatchetingKeyboardHeight } from '../keyboard/animated-keyboard.js'; import { createIsForegroundSelector } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import { type NavigationRoute, LoggedOutModalRouteName, RegistrationRouteName, QRCodeSignInNavigatorRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { usePersistedStateLoaded } from '../selectors/app-state-selectors.js'; import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors.js'; import { splashStyleSelector } from '../splash.js'; import { useStyles } from '../themes/colors.js'; import { AnimatedView } from '../types/styles.js'; import EthereumLogo from '../vectors/ethereum-logo.react.js'; let initialAppLoad = true; const safeAreaEdges = ['top', 'bottom']; export type LoggedOutMode = | 'loading' | 'prompt' | 'log-in' | 'register' | 'siwe'; const timingConfig = { duration: 250, easing: Easing.out(Easing.ease), }; // prettier-ignore function getPanelPaddingTop( modeValue /*: string */, keyboardHeightValue /*: number */, contentHeightValue /*: number */, ) /*: number */ { 'worklet'; const headerHeight = Platform.OS === 'ios' ? 62.33 : 58.54; let containerSize = headerHeight; if (modeValue === 'loading' || modeValue === 'prompt') { containerSize += Platform.OS === 'ios' ? 40 : 61; } else if (modeValue === 'log-in') { containerSize += 140; } else if (modeValue === 'register') { containerSize += Platform.OS === 'ios' ? 181 : 180; } else if (modeValue === 'siwe') { containerSize += 250; } const freeSpace = contentHeightValue - keyboardHeightValue - containerSize; const targetPanelPaddingTop = Math.max(freeSpace, 0) / 2; return withTiming(targetPanelPaddingTop, timingConfig); } // prettier-ignore function getPanelOpacity( modeValue /*: string */, finishResettingToPrompt/*: () => void */, ) /*: number */ { 'worklet'; const targetPanelOpacity = modeValue === 'loading' || modeValue === 'prompt' ? 0 : 1; return withTiming( targetPanelOpacity, timingConfig, (succeeded /*?: boolean */) => { if (succeeded && targetPanelOpacity === 0) { runOnJS(finishResettingToPrompt)(); } }, ); } const unboundStyles = { animationContainer: { flex: 1, }, backButton: { position: 'absolute', top: 13, }, button: { borderRadius: 4, marginBottom: 4, marginTop: 4, marginLeft: 4, marginRight: 4, paddingBottom: 14, paddingLeft: 18, paddingRight: 18, paddingTop: 14, flex: 1, }, buttonContainer: { bottom: 0, left: 0, marginLeft: 26, marginRight: 26, paddingBottom: 20, position: 'absolute', right: 0, }, buttonText: { fontFamily: 'OpenSans-Semibold', fontSize: 17, textAlign: 'center', }, classicAuthButton: { backgroundColor: 'purpleButton', }, classicAuthButtonText: { color: 'whiteText', }, registerButtons: { flexDirection: 'row', }, signInButtons: { flexDirection: 'row', }, container: { backgroundColor: 'transparent', flex: 1, }, header: { color: 'white', fontFamily: Platform.OS === 'ios' ? 'IBMPlexSans' : 'IBMPlexSans-Medium', fontSize: 56, fontWeight: '500', lineHeight: 66, textAlign: 'center', }, loadingIndicator: { paddingTop: 15, }, modalBackground: { bottom: 0, left: 0, position: 'absolute', right: 0, top: 0, }, siweButton: { backgroundColor: 'siweButton', flex: 1, flexDirection: 'row', justifyContent: 'center', }, siweButtonText: { color: 'siweButtonText', }, siweOr: { flex: 1, flexDirection: 'row', marginBottom: 18, marginTop: 14, }, siweOrLeftHR: { borderColor: 'logInSpacer', borderTopWidth: 1, flex: 1, marginRight: 18, marginTop: 10, }, siweOrRightHR: { borderColor: 'logInSpacer', borderTopWidth: 1, flex: 1, marginLeft: 18, marginTop: 10, }, siweOrText: { color: 'whiteText', fontSize: 17, textAlign: 'center', }, siweIcon: { paddingRight: 10, }, }; const isForegroundSelector = createIsForegroundSelector( LoggedOutModalRouteName, ); const backgroundSource = { uri: splashBackgroundURI }; const initialLogInState = { usernameInputText: null, passwordInputText: null, }; const initialLegacyRegisterState = { usernameInputText: '', passwordInputText: '', confirmPasswordInputText: '', }; type Mode = { +curMode: LoggedOutMode, +nextMode: LoggedOutMode, }; type Props = { +navigation: RootNavigationProp<'LoggedOutModal'>, +route: NavigationRoute<'LoggedOutModal'>, }; function LoggedOutModal(props: Props) { const mountedRef = React.useRef(false); React.useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const [logInState, baseSetLogInState] = React.useState(initialLogInState); const setLogInState = React.useCallback( (newLogInState: Partial) => { if (!mountedRef.current) { return; } baseSetLogInState(prevLogInState => ({ ...prevLogInState, ...newLogInState, })); }, [], ); const logInStateContainer = React.useMemo( () => ({ state: logInState, setState: setLogInState, }), [logInState, setLogInState], ); const [legacyRegisterState, baseSetLegacyRegisterState] = React.useState(initialLegacyRegisterState); const setLegacyRegisterState = React.useCallback( (newLegacyRegisterState: Partial) => { if (!mountedRef.current) { return; } baseSetLegacyRegisterState(prevLegacyRegisterState => ({ ...prevLegacyRegisterState, ...newLegacyRegisterState, })); }, [], ); const legacyRegisterStateContainer = React.useMemo( () => ({ state: legacyRegisterState, setState: setLegacyRegisterState, }), [legacyRegisterState, setLegacyRegisterState], ); const persistedStateLoaded = usePersistedStateLoaded(); const initialMode = persistedStateLoaded ? 'prompt' : 'loading'; const [mode, baseSetMode] = React.useState(() => ({ curMode: initialMode, nextMode: initialMode, })); const setMode = React.useCallback((newMode: Partial) => { if (!mountedRef.current) { return; } baseSetMode(prevMode => ({ ...prevMode, ...newMode, })); }, []); const nextModeRef = React.useRef(initialMode); const dimensions = useSelector(derivedDimensionsInfoSelector); const contentHeight = useSharedValue(dimensions.safeAreaHeight); const modeValue = useSharedValue(initialMode); const buttonOpacity = useSharedValue(persistedStateLoaded ? 1 : 0); const onPrompt = mode.curMode === 'prompt'; const prevOnPromptRef = React.useRef(onPrompt); React.useEffect(() => { if (onPrompt && !prevOnPromptRef.current) { buttonOpacity.value = withTiming(1, { easing: Easing.out(Easing.ease), }); } prevOnPromptRef.current = onPrompt; }, [onPrompt, buttonOpacity]); const curContentHeight = dimensions.safeAreaHeight; const prevContentHeightRef = React.useRef(curContentHeight); React.useEffect(() => { if (curContentHeight === prevContentHeightRef.current) { return; } prevContentHeightRef.current = curContentHeight; contentHeight.value = curContentHeight; }, [curContentHeight, contentHeight]); const combinedSetMode = React.useCallback( (newMode: LoggedOutMode) => { nextModeRef.current = newMode; setMode({ curMode: newMode, nextMode: newMode }); modeValue.value = newMode; }, [setMode, modeValue], ); const goBackToPrompt = React.useCallback(() => { nextModeRef.current = 'prompt'; setMode({ nextMode: 'prompt' }); modeValue.value = 'prompt'; Keyboard.dismiss(); }, [setMode, modeValue]); const loadingCompleteRef = React.useRef(persistedStateLoaded); React.useEffect(() => { if (!loadingCompleteRef.current && persistedStateLoaded) { combinedSetMode('prompt'); loadingCompleteRef.current = true; } }, [persistedStateLoaded, combinedSetMode]); const [activeAlert, setActiveAlert] = React.useState(false); const navContext = React.useContext(NavContext); const isForeground = isForegroundSelector(navContext); const ratchetingKeyboardHeightInput = React.useMemo( () => ({ ignoreKeyboardDismissal: activeAlert, disabled: !isForeground, }), [activeAlert, isForeground], ); const keyboardHeightValue = useRatchetingKeyboardHeight( ratchetingKeyboardHeightInput, ); // We remove the password from the TextInput on iOS before dismissing it, // because otherwise iOS will prompt the user to save the password if the // iCloud password manager is enabled. We'll put the password back after the // dismissal concludes. const temporarilyHiddenPassword = React.useRef(); const curLogInPassword = logInState.passwordInputText; const resetToPrompt = React.useCallback(() => { if (nextModeRef.current === 'prompt') { return false; } if (Platform.OS === 'ios' && curLogInPassword) { temporarilyHiddenPassword.current = curLogInPassword; setLogInState({ passwordInputText: null }); } goBackToPrompt(); return true; }, [goBackToPrompt, curLogInPassword, setLogInState]); const finishResettingToPrompt = React.useCallback(() => { setMode({ curMode: nextModeRef.current }); if (temporarilyHiddenPassword.current) { setLogInState({ passwordInputText: temporarilyHiddenPassword.current }); temporarilyHiddenPassword.current = null; } }, [setMode, setLogInState]); React.useEffect(() => { if (!isForeground) { return undefined; } BackHandler.addEventListener('hardwareBackPress', resetToPrompt); return () => { BackHandler.removeEventListener('hardwareBackPress', resetToPrompt); }; }, [isForeground, resetToPrompt]); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated && navContext), ); - const isLoggedInToAuthKeyserver = useSelector( - isLoggedInToAuthoritativeKeyserver, - ); + const isLoggedInToAuthKeyserver = useIsLoggedInToAuthoritativeKeyserver(); const loggedIn = useSelector(isLoggedIn); const dispatch = useDispatch(); React.useEffect(() => { // This gets triggered when an app is killed and restarted // Not when it is returned from being backgrounded if (!initialAppLoad || !rehydrateConcluded) { return; } initialAppLoad = false; if (usingCommServicesAccessToken || __DEV__) { return; } if (loggedIn === isLoggedInToAuthKeyserver) { return; } const actionSource = loggedIn ? recoveryFromReduxActionSources.appStartReduxLoggedInButInvalidCookie : recoveryFromReduxActionSources.appStartCookieLoggedInButInvalidRedux; dispatch({ type: setActiveSessionRecoveryActionType, payload: { activeSessionRecovery: actionSource, keyserverID: authoritativeKeyserverID, }, }); }, [rehydrateConcluded, loggedIn, isLoggedInToAuthKeyserver, dispatch]); const onPressSIWE = React.useCallback(() => { combinedSetMode('siwe'); }, [combinedSetMode]); const onPressLogIn = React.useCallback(() => { combinedSetMode('log-in'); }, [combinedSetMode]); const { navigate } = props.navigation; const onPressQRCodeSignIn = React.useCallback(() => { navigate(QRCodeSignInNavigatorRouteName); }, [navigate]); const onPressRegister = React.useCallback(() => { combinedSetMode('register'); }, [combinedSetMode]); const onPressNewRegister = React.useCallback(() => { navigate(RegistrationRouteName); }, [navigate]); const opacityStyle = useAnimatedStyle(() => ({ opacity: getPanelOpacity(modeValue.value, finishResettingToPrompt), })); const styles = useStyles(unboundStyles); const panel = React.useMemo(() => { if (mode.curMode === 'log-in') { return ( ); } else if (mode.curMode === 'register') { return ( ); } else if (mode.curMode === 'loading') { return ( ); } return null; }, [ mode.curMode, setActiveAlert, opacityStyle, logInStateContainer, legacyRegisterStateContainer, styles.loadingIndicator, ]); const classicAuthButtonStyle = React.useMemo( () => [styles.button, styles.classicAuthButton], [styles.button, styles.classicAuthButton], ); const classicAuthButtonTextStyle = React.useMemo( () => [styles.buttonText, styles.classicAuthButtonText], [styles.buttonText, styles.classicAuthButtonText], ); const siweAuthButtonStyle = React.useMemo( () => [styles.button, styles.siweButton], [styles.button, styles.siweButton], ); const siweAuthButtonTextStyle = React.useMemo( () => [styles.buttonText, styles.siweButtonText], [styles.buttonText, styles.siweButtonText], ); const buttonsViewOpacity = useAnimatedStyle(() => ({ opacity: buttonOpacity.value, })); const buttonsViewStyle = React.useMemo( () => [styles.buttonContainer, buttonsViewOpacity], [styles.buttonContainer, buttonsViewOpacity], ); const buttons = React.useMemo(() => { if (mode.curMode !== 'prompt') { return null; } const registerButtons = []; registerButtons.push( Register , ); if (enableNewRegistrationMode) { registerButtons.push( Register (new) , ); } const signInButtons = []; signInButtons.push( Sign in , ); if (__DEV__) { signInButtons.push( Sign in (QR) , ); } return ( Sign in with Ethereum or {signInButtons} {registerButtons} ); }, [ mode.curMode, onPressRegister, onPressNewRegister, onPressLogIn, onPressQRCodeSignIn, onPressSIWE, classicAuthButtonStyle, classicAuthButtonTextStyle, siweAuthButtonStyle, siweAuthButtonTextStyle, buttonsViewStyle, styles.siweIcon, styles.siweOr, styles.siweOrLeftHR, styles.siweOrText, styles.siweOrRightHR, styles.signInButtons, styles.registerButtons, ]); const windowWidth = dimensions.width; const backButtonStyle = React.useMemo( () => [ styles.backButton, opacityStyle, { left: windowWidth < 360 ? 28 : 40 }, ], [styles.backButton, opacityStyle, windowWidth], ); const paddingTopStyle = useAnimatedStyle(() => ({ paddingTop: getPanelPaddingTop( modeValue.value, keyboardHeightValue.value, contentHeight.value, ), })); const animatedContentStyle = React.useMemo( () => [styles.animationContainer, paddingTopStyle], [styles.animationContainer, paddingTopStyle], ); const animatedContent = React.useMemo( () => ( Comm {panel} ), [ animatedContentStyle, styles.header, backButtonStyle, resetToPrompt, panel, ], ); const curModeIsSIWE = mode.curMode === 'siwe'; const nextModeIsPrompt = mode.nextMode === 'prompt'; const siwePanel = React.useMemo(() => { if (!curModeIsSIWE) { return null; } return ( ); }, [curModeIsSIWE, goBackToPrompt, nextModeIsPrompt]); const splashStyle = useSelector(splashStyleSelector); const backgroundStyle = React.useMemo( () => [styles.modalBackground, splashStyle], [styles.modalBackground, splashStyle], ); return React.useMemo( () => ( <> {animatedContent} {buttons} {siwePanel} ), [backgroundStyle, styles.container, animatedContent, buttons, siwePanel], ); } const MemoizedLoggedOutModal: React.ComponentType = React.memo(LoggedOutModal); export default MemoizedLoggedOutModal; diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js index dc8a66832..051840629 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,612 +1,610 @@ // @flow import * as React from 'react'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { setSyncedMetadataEntryActionType } from 'lib/actions/synced-metadata-actions.js'; import { legacyKeyserverRegisterActionTypes, legacyKeyserverRegister, useIdentityPasswordRegister, identityRegisterActionTypes, deleteAccountActionTypes, useDeleteDiscardedIdentityAccount, } from 'lib/actions/user-actions.js'; +import { useIsLoggedInToAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; import { useKeyserverAuthWithRetry } from 'lib/keyserver-conn/keyserver-auth.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { usePreRequestUserState } from 'lib/selectors/account-selectors.js'; -import { isLoggedInToAuthoritativeKeyserver } from 'lib/selectors/user-selectors.js'; import { type LegacyLogInStartingPayload, logInActionSources, type LogOutResult, } from 'lib/types/account-types.js'; import { syncedMetadataNames } from 'lib/types/synced-metadata-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { NO_FID_METADATA } from 'lib/utils/farcaster-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { setURLPrefix } from 'lib/utils/url-utils.js'; import { waitUntilDatabaseDeleted } from 'lib/utils/wait-until-db-deleted.js'; import type { RegistrationServerCallInput, UsernameAccountSelection, EthereumAccountSelection, AvatarData, } from './registration-types.js'; import { authoritativeKeyserverID } from '../../authoritative-keyserver.js'; import { useNativeSetUserAvatar, useUploadSelectedMedia, } from '../../avatars/avatar-hooks.js'; import { commCoreModule } from '../../native-modules.js'; import { persistConfig } from '../../redux/persist.js'; import { useSelector } from '../../redux/redux-utils.js'; import { nativeLegacyLogInExtraInfoSelector } from '../../selectors/account-selectors.js'; import { appOutOfDateAlertDetails, usernameReservedAlertDetails, usernameTakenAlertDetails, unknownErrorAlertDetails, } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { defaultURLPrefix } from '../../utils/url-utils.js'; import { setNativeCredentials } from '../native-credentials.js'; import { useLegacySIWEServerCall, useIdentityWalletRegisterCall, } from '../siwe-hooks.js'; // We can't just do everything in one async callback, since the server calls // would get bound to Redux state from before the registration. The registration // flow has multiple steps where critical Redux state is changed, where // subsequent steps depend on accessing the updated Redux state. // To address this, we break the registration process up into multiple steps. // When each step completes we update the currentStep state, and we have Redux // selectors that trigger useEffects for subsequent steps when relevant data // starts to appear in Redux. type CurrentStep = | { +step: 'inactive' } | { +step: 'identity_registration_dispatched', +clearCachedSelections: () => void, +onAlertAcknowledged: ?() => mixed, +avatarData: ?AvatarData, +credentialsToSave: ?{ +username: string, +password: string }, +resolve: () => void, +reject: Error => void, } | { +step: 'authoritative_keyserver_registration_dispatched', +clearCachedSelections: () => void, +avatarData: ?AvatarData, +credentialsToSave: ?{ +username: string, +password: string }, +resolve: () => void, +reject: Error => void, }; const inactiveStep = { step: 'inactive' }; function useRegistrationServerCall(): RegistrationServerCallInput => Promise { const [currentStep, setCurrentStep] = React.useState(inactiveStep); // STEP 1: ACCOUNT REGISTRATION const legacyLogInExtraInfo = useSelector(nativeLegacyLogInExtraInfoSelector); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyKeyserverRegister = useLegacyAshoatKeyserverCall( legacyKeyserverRegister, ); const callIdentityPasswordRegister = useIdentityPasswordRegister(); const identityRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, farcasterID: ?string, onAlertAcknowledged: ?() => mixed, ) => { const identityRegisterPromise = (async () => { try { return await callIdentityPasswordRegister( accountSelection.username, accountSelection.password, farcasterID, ); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'username_reserved') { Alert.alert( usernameReservedAlertDetails.title, usernameReservedAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else if (messageForException === 'username_already_exists') { Alert.alert( usernameTakenAlertDetails.title, usernameTakenAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else if (messageForException === 'unsupported_version') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } throw e; } })(); void dispatchActionPromise( identityRegisterActionTypes, identityRegisterPromise, ); await identityRegisterPromise; }, [callIdentityPasswordRegister, dispatchActionPromise], ); const legacyKeyserverRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, keyserverURL: string, onAlertAcknowledged: ?() => mixed, ) => { const extraInfo = await legacyLogInExtraInfo(); const legacyKeyserverRegisterPromise = (async () => { try { return await callLegacyKeyserverRegister( { ...extraInfo, username: accountSelection.username, password: accountSelection.password, }, { urlPrefixOverride: keyserverURL, }, ); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'username_reserved') { Alert.alert( usernameReservedAlertDetails.title, usernameReservedAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else if (messageForException === 'username_taken') { Alert.alert( usernameTakenAlertDetails.title, usernameTakenAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else if (messageForException === 'client_version_unsupported') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } throw e; } })(); void dispatchActionPromise( legacyKeyserverRegisterActionTypes, legacyKeyserverRegisterPromise, undefined, ({ calendarQuery: extraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); await legacyKeyserverRegisterPromise; }, [legacyLogInExtraInfo, callLegacyKeyserverRegister, dispatchActionPromise], ); const legacySiweServerCall = useLegacySIWEServerCall(); const legacyKeyserverRegisterEthereumAccount = React.useCallback( async ( accountSelection: EthereumAccountSelection, keyserverURL: string, onAlertAcknowledged: ?() => mixed, ) => { try { await legacySiweServerCall(accountSelection, { urlPrefixOverride: keyserverURL, }); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'client_version_unsupported') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } throw e; } }, [legacySiweServerCall], ); const identityWalletRegisterCall = useIdentityWalletRegisterCall(); const identityRegisterEthereumAccount = React.useCallback( async ( accountSelection: EthereumAccountSelection, farcasterID: ?string, onNonceExpired: () => mixed, onAlertAcknowledged: ?() => mixed, ) => { try { await identityWalletRegisterCall({ address: accountSelection.address, message: accountSelection.message, signature: accountSelection.signature, fid: farcasterID, }); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'nonce_expired') { onNonceExpired(); } else if (messageForException === 'unsupported_version') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } throw e; } }, [identityWalletRegisterCall], ); const dispatch = useDispatch(); const returnedFunc = React.useCallback( (input: RegistrationServerCallInput) => new Promise( // eslint-disable-next-line no-async-promise-executor async (resolve, reject) => { try { if (currentStep.step !== 'inactive') { return; } const { accountSelection, avatarData, keyserverURL: passedKeyserverURL, farcasterID, siweBackupSecrets, clearCachedSelections, onNonceExpired, onAlertAcknowledged, } = input; const keyserverURL = passedKeyserverURL ?? defaultURLPrefix; if ( accountSelection.accountType === 'username' && !usingCommServicesAccessToken ) { await legacyKeyserverRegisterUsernameAccount( accountSelection, keyserverURL, onAlertAcknowledged, ); } else if (accountSelection.accountType === 'username') { await identityRegisterUsernameAccount( accountSelection, farcasterID, onAlertAcknowledged, ); } else if (!usingCommServicesAccessToken) { await legacyKeyserverRegisterEthereumAccount( accountSelection, keyserverURL, onAlertAcknowledged, ); } else { await identityRegisterEthereumAccount( accountSelection, farcasterID, onNonceExpired, onAlertAcknowledged, ); } if (passedKeyserverURL) { dispatch({ type: setURLPrefix, payload: passedKeyserverURL, }); } const fidToSave = farcasterID ?? NO_FID_METADATA; dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.CURRENT_USER_FID, data: fidToSave, }, }); if (siweBackupSecrets) { await commCoreModule.setSIWEBackupSecrets(siweBackupSecrets); } const credentialsToSave = accountSelection.accountType === 'username' ? { username: accountSelection.username, password: accountSelection.password, } : null; if (usingCommServicesAccessToken) { setCurrentStep({ step: 'identity_registration_dispatched', avatarData, clearCachedSelections, onAlertAcknowledged, credentialsToSave, resolve, reject, }); } else { setCurrentStep({ step: 'authoritative_keyserver_registration_dispatched', avatarData, clearCachedSelections, credentialsToSave, resolve, reject, }); } } catch (e) { reject(e); } }, ), [ currentStep, legacyKeyserverRegisterUsernameAccount, identityRegisterUsernameAccount, legacyKeyserverRegisterEthereumAccount, identityRegisterEthereumAccount, dispatch, ], ); // STEP 2: REGISTERING ON AUTHORITATIVE KEYSERVER const keyserverAuth = useKeyserverAuthWithRetry(authoritativeKeyserverID); const isRegisteredOnIdentity = useSelector( state => !!state.commServicesAccessToken && !!state.currentUserInfo && !state.currentUserInfo.anonymous, ); // We call deleteDiscardedIdentityAccount in order to reset state if identity // registration succeeds but authoritative keyserver auth fails const deleteDiscardedIdentityAccount = useDeleteDiscardedIdentityAccount(); const preRequestUserState = usePreRequestUserState(); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); const registeringOnAuthoritativeKeyserverRef = React.useRef(false); React.useEffect(() => { if ( !isRegisteredOnIdentity || currentStep.step !== 'identity_registration_dispatched' || registeringOnAuthoritativeKeyserverRef.current ) { return; } registeringOnAuthoritativeKeyserverRef.current = true; const { avatarData, clearCachedSelections, onAlertAcknowledged, credentialsToSave, resolve, reject, } = currentStep; void (async () => { try { await keyserverAuth({ authActionSource: process.env.BROWSER ? logInActionSources.keyserverAuthFromWeb : logInActionSources.keyserverAuthFromNative, setInProgress: () => {}, hasBeenCancelled: () => false, doNotRegister: false, password: credentialsToSave?.password, }); setCurrentStep({ step: 'authoritative_keyserver_registration_dispatched', avatarData, clearCachedSelections, credentialsToSave, resolve, reject, }); } catch (keyserverAuthException) { const messageForException = getMessageForException( keyserverAuthException, ); const discardIdentityAccountPromise: Promise = (async () => { try { const deletionResult = await deleteDiscardedIdentityAccount( credentialsToSave?.password, ); if (messageForException === 'client_version_unsupported') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } return deletionResult; } catch (deleteException) { // We swallow the exception here because // discardIdentityAccountPromise is used in a scenario where the // user is visibly logged-out, and it's only used to reset state // (eg. Redux, SQLite) to a logged-out state. The state reset // only occurs when a success action is dispatched, so by // swallowing exceptions we ensure that we always dispatch a // success. Alert.alert( 'Account created but login failed', 'We were able to create your account, but were unable to log ' + 'you in. Try going back to the login screen and logging in ' + 'with your new credentials.', [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); return { currentUserInfo: null, preRequestUserState: { ...preRequestUserState, commServicesAccessToken, }, }; } })(); void dispatchActionPromise( deleteAccountActionTypes, discardIdentityAccountPromise, ); await waitUntilDatabaseDeleted(); reject(keyserverAuthException); setCurrentStep(inactiveStep); } finally { registeringOnAuthoritativeKeyserverRef.current = false; } })(); }, [ currentStep, isRegisteredOnIdentity, keyserverAuth, dispatchActionPromise, deleteDiscardedIdentityAccount, preRequestUserState, commServicesAccessToken, ]); // STEP 3: SETTING AVATAR const uploadSelectedMedia = useUploadSelectedMedia(); const nativeSetUserAvatar = useNativeSetUserAvatar(); - const isLoggedInToAuthKeyserver = useSelector( - isLoggedInToAuthoritativeKeyserver, - ); + const isLoggedInToAuthKeyserver = useIsLoggedInToAuthoritativeKeyserver(); const avatarBeingSetRef = React.useRef(false); React.useEffect(() => { if ( !isLoggedInToAuthKeyserver || currentStep.step !== 'authoritative_keyserver_registration_dispatched' || avatarBeingSetRef.current ) { return; } avatarBeingSetRef.current = true; const { avatarData, resolve, clearCachedSelections, credentialsToSave } = currentStep; void (async () => { try { if (!avatarData) { return; } let updateUserAvatarRequest; if (!avatarData.needsUpload) { ({ updateUserAvatarRequest } = avatarData); } else { const { mediaSelection } = avatarData; updateUserAvatarRequest = await uploadSelectedMedia(mediaSelection); if (!updateUserAvatarRequest) { return; } } await nativeSetUserAvatar(updateUserAvatarRequest); } finally { dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.DB_VERSION, data: `${persistConfig.version}`, }, }); dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); clearCachedSelections(); if (credentialsToSave) { void setNativeCredentials(credentialsToSave); } setCurrentStep(inactiveStep); avatarBeingSetRef.current = false; resolve(); } })(); }, [ currentStep, isLoggedInToAuthKeyserver, uploadSelectedMedia, nativeSetUserAvatar, dispatch, ]); return returnedFunc; } export { useRegistrationServerCall }; diff --git a/native/components/auto-join-community-handler.react.js b/native/components/auto-join-community-handler.react.js index 03f97971c..5061bd5d9 100644 --- a/native/components/auto-join-community-handler.react.js +++ b/native/components/auto-join-community-handler.react.js @@ -1,159 +1,159 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { joinThreadActionTypes, useJoinThread, } from 'lib/actions/thread-actions.js'; import { NeynarClientContext } from 'lib/components/neynar-client-provider.react.js'; import blobService from 'lib/facts/blob-service.js'; +import { useIsLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; import { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js'; -import { isLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/selectors/user-selectors.js'; import { farcasterChannelTagBlobHash } from 'lib/shared/community-utils.js'; import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { defaultThreadSubscription } from 'lib/types/subscription-types.js'; import { getBlobFetchableURL } from 'lib/utils/blob-service.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken, createDefaultHTTPRequestHeaders, } from 'lib/utils/services-utils.js'; import { nonThreadCalendarQuery } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; function AutoJoinCommunityHandler(): React.Node { const isActive = useSelector(state => state.lifecycleState !== 'background'); - const loggedIn = useSelector(isLoggedInToIdentityAndAuthoritativeKeyserver); + const loggedIn = useIsLoggedInToIdentityAndAuthoritativeKeyserver(); const fid = useCurrentUserFID(); const neynarClient = React.useContext(NeynarClientContext)?.client; const navContext = React.useContext(NavContext); const identityClientContext = React.useContext(IdentityClientContext); invariant(identityClientContext, 'IdentityClientContext should be set'); const { getAuthMetadata } = identityClientContext; const calendarQuery = useSelector(state => nonThreadCalendarQuery({ redux: state, navContext, }), ); const joinThread = useJoinThread(); const joinThreadActionPromise = React.useCallback( async (communityID: string) => { const query = calendarQuery(); return await joinThread({ threadID: communityID, calendarQuery: { startDate: query.startDate, endDate: query.endDate, filters: [ ...query.filters, { type: 'threads', threadIDs: [communityID] }, ], }, defaultSubscription: defaultThreadSubscription, }); }, [calendarQuery, joinThread], ); const dispatchActionPromise = useDispatchActionPromise(); const threadInfos = useSelector(state => state.threadStore.threadInfos); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); React.useEffect(() => { if (!loggedIn || !isActive || !fid || !neynarClient || !threadInfos) { return; } void (async () => { const authMetadataPromise: Promise = (async () => { if (!usingCommServicesAccessToken) { return undefined; } return await getAuthMetadata(); })(); const followedFarcasterChannelsPromise = neynarClient.fetchFollowedFarcasterChannels(fid); const [authMetadata, followedFarcasterChannels] = await Promise.all([ authMetadataPromise, followedFarcasterChannelsPromise, ]); const headers = authMetadata ? createDefaultHTTPRequestHeaders(authMetadata) : {}; const followedFarcasterChannelIDs = followedFarcasterChannels.map( channel => channel.id, ); const promises = followedFarcasterChannelIDs.map(async channelID => { const blobHash = farcasterChannelTagBlobHash(channelID); const blobURL = getBlobFetchableURL(blobHash); const blobResult = await fetch(blobURL, { method: blobService.httpEndpoints.GET_BLOB.method, headers, }); if (blobResult.status !== 200) { return; } const { commCommunityID } = await blobResult.json(); const keyserverID = extractKeyserverIDFromID(commCommunityID); if (!keyserverInfos[keyserverID]) { return; } // The user is already in the community if (threadInfos[commCommunityID]) { return; } void dispatchActionPromise( joinThreadActionTypes, joinThreadActionPromise(commCommunityID), ); }); await Promise.all(promises); })(); }, [ threadInfos, dispatchActionPromise, fid, isActive, joinThreadActionPromise, loggedIn, neynarClient, getAuthMetadata, keyserverInfos, ]); return null; } export { AutoJoinCommunityHandler }; diff --git a/native/components/background-identity-login-handler.react.js b/native/components/background-identity-login-handler.react.js index 399d9d0e0..9b415153b 100644 --- a/native/components/background-identity-login-handler.react.js +++ b/native/components/background-identity-login-handler.react.js @@ -1,103 +1,103 @@ // @flow import * as React from 'react'; import { identityLogInActionTypes, useIdentityPasswordLogIn, logOutActionTypes, useLogOut, } from 'lib/actions/user-actions.js'; -import { isLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/selectors/user-selectors.js'; +import { useIsLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import { securityUpdateLogoutText } from 'lib/types/alert-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useSelector } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { fetchNativeKeychainCredentials } from '../account/native-credentials.js'; import { useIsAppLoggedIn } from '../navigation/nav-selectors.js'; import Alert from '../utils/alert.js'; function BackgroundIdentityLoginHandler() { const dispatchActionPromise = useDispatchActionPromise(); const callLogOut = useLogOut(); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const hasAccessToken = useSelector(state => !!state.commServicesAccessToken); - const loggedIn = useSelector(isLoggedInToIdentityAndAuthoritativeKeyserver); + const loggedIn = useIsLoggedInToIdentityAndAuthoritativeKeyserver(); const navLoggedIn = useIsAppLoggedIn(); // We don't want to try identity login until both loggedIn and navLoggedIn are // true. The former is to make sure that we will be able to successfully log // in with identity. The latter is to address a race condition in ENG-8785. const readyToTryIdentityLogin = loggedIn && navLoggedIn; const callIdentityPasswordLogIn = useIdentityPasswordLogIn(); const handleLogOutAndAlert = React.useCallback(() => { void dispatchActionPromise(logOutActionTypes, callLogOut()); Alert.alert('Security update', securityUpdateLogoutText, [{ text: 'OK' }]); }, [dispatchActionPromise, callLogOut]); const loginAttemptedRef = React.useRef(false); const logInIfPossibleElseLogOut = React.useCallback(async () => { if ( hasAccessToken || !readyToTryIdentityLogin || !usingCommServicesAccessToken || loginAttemptedRef.current ) { return; } if (!isAccountWithPassword) { handleLogOutAndAlert(); return; } const nativeCredentials = await fetchNativeKeychainCredentials(); if (!nativeCredentials) { console.log( 'Native credentials missing. Cannot log in to identity service', ); handleLogOutAndAlert(); return; } if (loginAttemptedRef.current) { return; } loginAttemptedRef.current = true; const logInPromise = callIdentityPasswordLogIn( nativeCredentials.username, nativeCredentials.password, ); void dispatchActionPromise(identityLogInActionTypes, logInPromise); try { await logInPromise; } catch (e) { console.log('BackgroundIdentityLoginHandler failed identity login', e); handleLogOutAndAlert(); } }, [ callIdentityPasswordLogIn, dispatchActionPromise, readyToTryIdentityLogin, handleLogOutAndAlert, hasAccessToken, isAccountWithPassword, ]); React.useEffect(() => { void logInIfPossibleElseLogOut(); }, [logInIfPossibleElseLogOut]); } export default BackgroundIdentityLoginHandler; diff --git a/native/components/connect-farcaster-alert-handler.react.js b/native/components/connect-farcaster-alert-handler.react.js index fad0f77d9..8313e61e8 100644 --- a/native/components/connect-farcaster-alert-handler.react.js +++ b/native/components/connect-farcaster-alert-handler.react.js @@ -1,64 +1,64 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { recordAlertActionType } from 'lib/actions/alert-actions.js'; -import { isLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/selectors/user-selectors.js'; +import { useIsLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; import { alertTypes, type RecordAlertActionPayload, } from 'lib/types/alert-types.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; import { shouldSkipConnectFarcasterAlert } from 'lib/utils/push-alerts.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import sleep from 'lib/utils/sleep.js'; import { ConnectFarcasterBottomSheetRouteName } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; function ConnectFarcasterAlertHandler(): React.Node { const { navigate } = useNavigation(); const isActive = useSelector(state => state.lifecycleState !== 'background'); - const loggedIn = useSelector(isLoggedInToIdentityAndAuthoritativeKeyserver); + const loggedIn = useIsLoggedInToIdentityAndAuthoritativeKeyserver(); const fid = useCurrentUserFID(); const connectFarcasterAlertInfo = useSelector( state => state.alertStore.alertInfos[alertTypes.CONNECT_FARCASTER], ); const dispatch = useDispatch(); React.useEffect(() => { if ( !loggedIn || !isActive || shouldSkipConnectFarcasterAlert(connectFarcasterAlertInfo, fid) ) { return; } void (async () => { await sleep(1000); navigate(ConnectFarcasterBottomSheetRouteName); const payload: RecordAlertActionPayload = { alertType: alertTypes.CONNECT_FARCASTER, time: Date.now(), }; dispatch({ type: recordAlertActionType, payload, }); })(); }, [connectFarcasterAlertInfo, dispatch, fid, isActive, loggedIn, navigate]); return null; } export default ConnectFarcasterAlertHandler; diff --git a/native/navigation/navigation-handler.react.js b/native/navigation/navigation-handler.react.js index b9f15bca8..083ab5569 100644 --- a/native/navigation/navigation-handler.react.js +++ b/native/navigation/navigation-handler.react.js @@ -1,83 +1,82 @@ // @flow import * as React from 'react'; -import { isLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/selectors/user-selectors.js'; +import { useIsLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; import { logInActionType, logOutActionType } from './action-types.js'; import ModalPruner from './modal-pruner.react.js'; import NavFromReduxHandler from './nav-from-redux-handler.react.js'; import { useIsAppLoggedIn } from './nav-selectors.js'; import { NavContext, type NavAction } from './navigation-context.js'; import PolicyAcknowledgmentHandler from './policy-acknowledgment-handler.react.js'; import ThreadScreenTracker from './thread-screen-tracker.react.js'; import { MissingRegistrationDataHandler } from '../account/registration/missing-registration-data/missing-registration-data-handler.react.js'; import DevTools from '../redux/dev-tools.react.js'; -import { useSelector } from '../redux/redux-utils.js'; import { usePersistedStateLoaded } from '../selectors/app-state-selectors.js'; const NavigationHandler: React.ComponentType<{}> = React.memo<{}>( function NavigationHandler() { const navContext = React.useContext(NavContext); const persistedStateLoaded = usePersistedStateLoaded(); const devTools = __DEV__ ? : null; if (!navContext || !persistedStateLoaded) { if (__DEV__) { return ( <> {devTools} ); } else { return null; } } const { dispatch } = navContext; return ( <> {devTools} ); }, ); NavigationHandler.displayName = 'NavigationHandler'; type LogInHandlerProps = { +dispatch: (action: NavAction) => void, }; const LogInHandler = React.memo(function LogInHandler( props: LogInHandlerProps, ) { const { dispatch } = props; - const loggedIn = useSelector(isLoggedInToIdentityAndAuthoritativeKeyserver); + const loggedIn = useIsLoggedInToIdentityAndAuthoritativeKeyserver(); const navLoggedIn = useIsAppLoggedIn(); const prevLoggedInRef = React.useRef(); React.useEffect(() => { if (loggedIn === prevLoggedInRef.current) { return; } prevLoggedInRef.current = loggedIn; if (loggedIn && !navLoggedIn) { dispatch({ type: (logInActionType: 'LOG_IN') }); } else if (!loggedIn && navLoggedIn) { dispatch({ type: (logOutActionType: 'LOG_OUT') }); } }, [navLoggedIn, loggedIn, dispatch]); return null; }); LogInHandler.displayName = 'LogInHandler'; export default NavigationHandler;