diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index 4405d3ee1..0c2896fb3 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,694 +1,686 @@ // @flow import * as React from 'react'; import { extractKeyserverIDFromID, sortThreadIDsPerKeyserver, sortCalendarQueryPerKeyserver, } from '../keyserver-conn/keyserver-call-utils.js'; import { preRequestUserStateSelector } from '../selectors/account-selectors.js'; +import { IdentityClientContext } from '../shared/identity-client-context.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { LogOutResult, LogInInfo, LogInResult, RegisterResult, RegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, LogInResponse, LogInRequest, KeyserverAuthResult, KeyserverAuthInfo, KeyserverAuthRequest, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { RawEntryInfo, CalendarQuery } from '../types/entry-types.js'; -import type { IdentityServiceClient } from '../types/identity-service-types'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../types/message-types.js'; import type { GetSessionPublicKeysArgs, GetOlmSessionInitializationDataResponse, } from '../types/request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from '../types/search-types.js'; import type { SessionPublicKeys, PreRequestUserState, } from '../types/session-types.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; import type { RawThreadInfos } from '../types/thread-types'; import type { UserInfo, PasswordUpdate, LoggedOutUserInfo, } from '../types/user-types.js'; import type { CallServerEndpoint, CallServerEndpointOptions, } from '../utils/call-server-endpoint.js'; import { getConfig } from '../utils/config.js'; import type { CallKeyserverEndpoint } from '../utils/keyserver-call'; import { useKeyserverCall } from '../utils/keyserver-call.js'; import { useSelector } from '../utils/redux-utils.js'; import sleep from '../utils/sleep.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; const loggedOutUserInfo: LoggedOutUserInfo = { anonymous: true, }; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const logOut = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: PreRequestUserState) => Promise) => async preRequestUserState => { const requests: { [string]: {} } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = {}; } let response = null; try { response = await Promise.race([ callKeyserverEndpoint('log_out', requests), (async () => { await sleep(500); throw new Error('log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? loggedOutUserInfo : null; return { currentUserInfo, preRequestUserState }; }; function useLogOut(): () => Promise { const preRequestUserState = useSelector(preRequestUserStateSelector); const callKeyserverLogOut = useKeyserverCall(logOut); return React.useCallback( () => callKeyserverLogOut(preRequestUserState), [callKeyserverLogOut, preRequestUserState], ); } const claimUsernameActionTypes = Object.freeze({ started: 'CLAIM_USERNAME_STARTED', success: 'CLAIM_USERNAME_SUCCESS', failed: 'CLAIM_USERNAME_FAILED', }); const claimUsernameCallServerEndpointOptions = { timeout: 500 }; const claimUsername = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (() => Promise) => async () => { const requests = { [ashoatKeyserverID]: {} }; const responses = await callKeyserverEndpoint('claim_username', requests, { ...claimUsernameCallServerEndpointOptions, }); const response = responses[ashoatKeyserverID]; return { message: response.message, signature: response.signature, }; }; function useClaimUsername(): () => Promise { return useKeyserverCall(claimUsername); } const deleteKeyserverAccountActionTypes = Object.freeze({ started: 'DELETE_KEYSERVER_ACCOUNT_STARTED', success: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', failed: 'DELETE_KEYSERVER_ACCOUNT_FAILED', }); const deleteKeyserverAccount = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: PreRequestUserState) => Promise) => async preRequestUserState => { const requests: { [string]: {} } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = {}; } await callKeyserverEndpoint('delete_account', requests); return { currentUserInfo: loggedOutUserInfo, preRequestUserState }; }; function useDeleteKeyserverAccount(): ( input: PreRequestUserState, ) => Promise { return useKeyserverCall(deleteKeyserverAccount); } const deleteIdentityAccountActionTypes = Object.freeze({ started: 'DELETE_IDENTITY_ACCOUNT_STARTED', success: 'DELETE_IDENTITY_ACCOUNT_SUCCESS', failed: 'DELETE_IDENTITY_ACCOUNT_FAILED', }); -function useDeleteIdentityAccount(): ( - client: IdentityServiceClient, - deviceID: ?string, -) => Promise { - const userID = useSelector(state => state.currentUserInfo?.id); - const accessToken = useSelector(state => state.commServicesAccessToken); - const deleteIdentityAccount = React.useCallback( - async (client: IdentityServiceClient, deviceID: ?string) => { - if (!userID || !accessToken || !deviceID) { - throw new Error('missing identity service auth metadata'); - } - await client.deleteUser(userID, deviceID, accessToken); - }, - [userID, accessToken], - ); - - return deleteIdentityAccount; +function useDeleteIdentityAccount(): () => Promise { + const client = React.useContext(IdentityClientContext); + const identityClient = client?.identityClient; + return React.useCallback(() => { + if (!identityClient) { + throw new Error('Identity service client is not initialized'); + } + return identityClient.deleteUser(); + }, [identityClient]); } const registerActionTypes = Object.freeze({ started: 'REGISTER_STARTED', success: 'REGISTER_SUCCESS', failed: 'REGISTER_FAILED', }); const registerCallServerEndpointOptions = { timeout: 60000 }; const register = ( callServerEndpoint: CallServerEndpoint, ): (( registerInfo: RegisterInfo, options?: CallServerEndpointOptions, ) => Promise) => async (registerInfo, options) => { const deviceTokenUpdateRequest = registerInfo.deviceTokenUpdateRequest[ashoatKeyserverID]; const response = await callServerEndpoint( 'create_account', { ...registerInfo, deviceTokenUpdateRequest, platformDetails: getConfig().platformDetails, }, { ...registerCallServerEndpointOptions, ...options, }, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; const keyserverAuthActionTypes = Object.freeze({ started: 'KEYSERVER_AUTH_STARTED', success: 'KEYSERVER_AUTH_SUCCESS', failed: 'KEYSERVER_AUTH_FAILED', }); const keyserverAuthCallServerEndpointOptions = { timeout: 60000 }; const keyserverAuth = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: KeyserverAuthInfo) => Promise) => async keyserverAuthInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { logInActionSource, calendarQuery, keyserverData, deviceTokenUpdateInput, ...restLogInInfo } = keyserverAuthInfo; const keyserverIDs = Object.keys(keyserverData); const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: KeyserverAuthRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: deviceTokenUpdateInput[keyserverID], watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, initialContentEncryptedMessage: keyserverData[keyserverID].initialContentEncryptedMessage, initialNotificationsEncryptedMessage: keyserverData[keyserverID].initialNotificationsEncryptedMessage, source: logInActionSource, }; } const responses: { +[string]: LogInResponse } = await callKeyserverEndpoint( 'keyserver_auth', requests, keyserverAuthCallServerEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: keyserverAuthInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[ashoatKeyserverID].currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, logInActionSource: keyserverAuthInfo.logInActionSource, notAcknowledgedPolicies: responses[ashoatKeyserverID].notAcknowledgedPolicies, }; }; function useKeyserverAuth(): ( input: KeyserverAuthInfo, ) => Promise { return useKeyserverCall(keyserverAuth); } function mergeUserInfos( ...userInfoArrays: Array<$ReadOnlyArray> ): UserInfo[] { const merged: { [string]: UserInfo } = {}; for (const userInfoArray of userInfoArrays) { for (const userInfo of userInfoArray) { merged[userInfo.id] = userInfo; } } const flattened = []; for (const id in merged) { flattened.push(merged[id]); } return flattened; } type WritableGenericMessagesResult = { messageInfos: RawMessageInfo[], truncationStatus: MessageTruncationStatuses, watchedIDsAtRequestTime: string[], currentAsOf: { [keyserverID: string]: number }, }; type WritableCalendarResult = { rawEntryInfos: RawEntryInfo[], calendarQuery: CalendarQuery, }; const tempIdentityLoginActionTypes = Object.freeze({ started: 'TEMP_IDENTITY_LOG_IN_STARTED', success: 'TEMP_IDENTITY_LOG_IN_SUCCESS', failed: 'TEMP_IDENTITY_LOG_IN_FAILED', }); const logInActionTypes = Object.freeze({ started: 'LOG_IN_STARTED', success: 'LOG_IN_SUCCESS', failed: 'LOG_IN_FAILED', }); const logInCallServerEndpointOptions = { timeout: 60000 }; const logIn = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: LogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { logInActionSource, calendarQuery, keyserverIDs: inputKeyserverIDs, ...restLogInInfo } = logInInfo; // Eventually the list of keyservers will be fetched from the // identity service const keyserverIDs = inputKeyserverIDs ?? [ashoatKeyserverID]; const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: LogInRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: logInInfo.deviceTokenUpdateRequest[keyserverID], source: logInActionSource, watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, }; } const responses: { +[string]: LogInResponse } = await callKeyserverEndpoint( 'log_in', requests, logInCallServerEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[ashoatKeyserverID].currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, logInActionSource: logInInfo.logInActionSource, notAcknowledgedPolicies: responses[ashoatKeyserverID].notAcknowledgedPolicies, }; }; function useLogIn(): (input: LogInInfo) => Promise { return useKeyserverCall(logIn); } const changeUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_USER_PASSWORD_STARTED', success: 'CHANGE_USER_PASSWORD_SUCCESS', failed: 'CHANGE_USER_PASSWORD_FAILED', }); const changeUserPassword = ( callServerEndpoint: CallServerEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callServerEndpoint('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callServerEndpoint: CallServerEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callServerEndpoint('search_users', { prefix: usernamePrefix, }); return { userInfos: response.userInfos, }; }; const exactSearchUserActionTypes = Object.freeze({ started: 'EXACT_SEARCH_USER_STARTED', success: 'EXACT_SEARCH_USER_SUCCESS', failed: 'EXACT_SEARCH_USER_FAILED', }); const exactSearchUser = ( callServerEndpoint: CallServerEndpoint, ): ((username: string) => Promise) => async username => { const response = await callServerEndpoint('exact_search_user', { username, }); return { userInfo: response.userInfo, }; }; const updateSubscriptionActionTypes = Object.freeze({ started: 'UPDATE_SUBSCRIPTION_STARTED', success: 'UPDATE_SUBSCRIPTION_SUCCESS', failed: 'UPDATE_SUBSCRIPTION_FAILED', }); const updateSubscription = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: SubscriptionUpdateRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'update_user_subscription', requests, ); const response = responses[keyserverID]; return { threadID: input.threadID, subscription: response.threadSubscription, }; }; function useUpdateSubscription(): ( input: SubscriptionUpdateRequest, ) => Promise { return useKeyserverCall(updateSubscription); } const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: UpdateUserSettingsRequest) => Promise) => async input => { const requests: { [string]: UpdateUserSettingsRequest } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = input; } await callKeyserverEndpoint('update_user_settings', requests); }; function useSetUserSettings(): ( input: UpdateUserSettingsRequest, ) => Promise { return useKeyserverCall(setUserSettings); } const getSessionPublicKeys = ( callServerEndpoint: CallServerEndpoint, ): ((data: GetSessionPublicKeysArgs) => Promise) => async data => { return await callServerEndpoint('get_session_public_keys', data); }; const getOlmSessionInitializationDataActionTypes = Object.freeze({ started: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', success: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', failed: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', }); const getOlmSessionInitializationData = ( callServerEndpoint: CallServerEndpoint, ): (( options?: ?CallServerEndpointOptions, ) => Promise) => async options => { return await callServerEndpoint( 'get_olm_session_initialization_data', {}, options, ); }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callServerEndpoint: CallServerEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callServerEndpoint('policy_acknowledgment', policyRequest); }; const updateUserAvatarActionTypes = Object.freeze({ started: 'UPDATE_USER_AVATAR_STARTED', success: 'UPDATE_USER_AVATAR_SUCCESS', failed: 'UPDATE_USER_AVATAR_FAILED', }); const updateUserAvatar = ( callServerEndpoint: CallServerEndpoint, ): (( avatarDBContent: UpdateUserAvatarRequest, ) => Promise) => async avatarDBContent => { const { updates }: UpdateUserAvatarResponse = await callServerEndpoint( 'update_user_avatar', avatarDBContent, ); return { updates }; }; const resetUserStateActionType = 'RESET_USER_STATE'; const setAccessTokenActionType = 'SET_ACCESS_TOKEN'; export { changeUserPasswordActionTypes, changeUserPassword, claimUsernameActionTypes, useClaimUsername, useDeleteKeyserverAccount, deleteKeyserverAccountActionTypes, getSessionPublicKeys, getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, logIn as logInRawAction, tempIdentityLoginActionTypes, useLogIn, logInActionTypes, useLogOut, logOutActionTypes, register, registerActionTypes, searchUsers, searchUsersActionTypes, exactSearchUser, exactSearchUserActionTypes, useSetUserSettings, setUserSettingsActionTypes, useUpdateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, updateUserAvatarActionTypes, updateUserAvatar, resetUserStateActionType, setAccessTokenActionType, deleteIdentityAccountActionTypes, useDeleteIdentityAccount, keyserverAuthActionTypes, useKeyserverAuth, }; diff --git a/lib/shared/identity-client-context.js b/lib/shared/identity-client-context.js new file mode 100644 index 000000000..49bdab848 --- /dev/null +++ b/lib/shared/identity-client-context.js @@ -0,0 +1,14 @@ +// @flow + +import * as React from 'react'; + +import type { IdentityServiceClient } from '../types/identity-service-types.js'; + +export type IdentityClientContextType = { + +identityClient: ?IdentityServiceClient, +}; + +const IdentityClientContext: React.Context = + React.createContext(); + +export { IdentityClientContext }; diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index 2631c0197..cc442a2c4 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,48 +1,45 @@ // @flow export type UserLoginResponse = { +userId: string, +accessToken: string, }; // This type should not be altered without also updating // OutboundKeyInfoResponse in native/native_rust_library/src/lib.rs export type OutboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +socialProof: ?string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +oneTimeContentPrekey: ?string, +oneTimeNotifPrekey: ?string, }; export interface IdentityServiceClient { - +deleteUser: ( - userID: string, - deviceID: string, - accessToken: string, - ) => Promise; + +deleteUser: () => Promise; + +getKeyserverKeys: string => Promise; } export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, +commServicesAccessToken: string, }; // This type should not be altered without also updating // InboundKeyInfoResponse in native/native_rust_library/src/lib.rs export type InboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +socialProof?: ?string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +username?: ?string, +walletAddress?: ?string, }; diff --git a/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js new file mode 100644 index 000000000..249de9267 --- /dev/null +++ b/native/identity-service/identity-service-context-provider.react.js @@ -0,0 +1,107 @@ +// @flow + +import * as React from 'react'; + +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import type { + IdentityServiceClient, + OutboundKeyInfoResponse, + UserLoginResponse, +} from 'lib/types/identity-service-types.js'; + +import { getCommServicesAuthMetadataEmitter } from '../event-emitters/csa-auth-metadata-emitter.js'; +import { commCoreModule, commRustModule } from '../native-modules.js'; +import { getContentSigningKey } from '../utils/crypto-utils.js'; + +type Props = { + +children: React.Node, +}; +function IdentityServiceContextProvider(props: Props): React.Node { + const { children } = props; + + const authMetadataPromiseRef = + React.useRef>(); + if (!authMetadataPromiseRef.current) { + authMetadataPromiseRef.current = (async () => { + const { userID, accessToken } = + await commCoreModule.getCommServicesAuthMetadata(); + return { userID, accessToken }; + })(); + } + + React.useEffect(() => { + const metadataEmitter = getCommServicesAuthMetadataEmitter(); + const subscription = metadataEmitter.addListener( + 'commServicesAuthMetadata', + (authMetadata: UserLoginResponse) => { + authMetadataPromiseRef.current = Promise.resolve({ + userID: authMetadata.userId, + accessToken: authMetadata.accessToken, + }); + }, + ); + return () => subscription.remove(); + }, []); + + const getAuthMetadata = React.useCallback< + () => Promise<{ + +deviceID: string, + +userID: string, + +accessToken: string, + }>, + >(async () => { + const deviceID = await getContentSigningKey(); + const authMetadata = await authMetadataPromiseRef.current; + const userID = authMetadata?.userID; + const accessToken = authMetadata?.accessToken; + if (!deviceID || !userID || !accessToken) { + throw new Error('Identity service client is not initialized'); + } + return { deviceID, userID, accessToken }; + }, []); + + const client = React.useMemo(() => { + return { + deleteUser: async () => { + const { deviceID, userID, accessToken } = await getAuthMetadata(); + return commRustModule.deleteUser(userID, deviceID, accessToken); + }, + getKeyserverKeys: async (keyserverID: string) => { + const { deviceID, userID, accessToken } = await getAuthMetadata(); + const result = await commRustModule.getKeyserverKeys( + userID, + deviceID, + accessToken, + keyserverID, + ); + const resultObject: OutboundKeyInfoResponse = JSON.parse(result); + if ( + !resultObject.payload || + !resultObject.payloadSignature || + !resultObject.contentPrekey || + !resultObject.contentPrekeySignature || + !resultObject.notifPrekey || + !resultObject.notifPrekeySignature + ) { + throw new Error('Invalid response from Identity service'); + } + return resultObject; + }, + }; + }, [getAuthMetadata]); + + const value = React.useMemo( + () => ({ + identityClient: client, + }), + [client], + ); + + return ( + + {children} + + ); +} + +export default IdentityServiceContextProvider; diff --git a/native/profile/delete-account.react.js b/native/profile/delete-account.react.js index 663b2a500..ca782672d 100644 --- a/native/profile/delete-account.react.js +++ b/native/profile/delete-account.react.js @@ -1,176 +1,173 @@ // @flow import * as React from 'react'; import { Text, View, ActivityIndicator } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { deleteIdentityAccountActionTypes, deleteKeyserverAccountActionTypes, useDeleteIdentityAccount, useDeleteKeyserverAccount, } from 'lib/actions/user-actions.js'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { deleteNativeCredentialsFor } from '../account/native-credentials.js'; import Button from '../components/button.react.js'; -import { commRustModule } from '../native-modules.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; -import { getContentSigningKey } from '../utils/crypto-utils.js'; const keyserverLoadingStatusSelector = createLoadingStatusSelector( deleteKeyserverAccountActionTypes, ); const identityLoadingStatusSelector = createLoadingStatusSelector( deleteIdentityAccountActionTypes, ); type Props = { +navigation: ProfileNavigationProp<'DeleteAccount'>, +route: NavigationRoute<'DeleteAccount'>, }; const DeleteAccount: React.ComponentType = React.memo( function DeleteAccount() { const keyserverLoadingStatus = useSelector(keyserverLoadingStatusSelector); const identityLoadingStatus = useSelector(identityLoadingStatusSelector); const combinedLoadingStatuses = combineLoadingStatuses( keyserverLoadingStatus, identityLoadingStatus, ); const preRequestUserState = useSelector(preRequestUserStateSelector); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const callDeleteKeyserverAccount = useDeleteKeyserverAccount(); const callDeleteIdentityAccount = useDeleteIdentityAccount(); const isButtonDisabled = combinedLoadingStatuses === 'loading'; const buttonContent = isButtonDisabled ? ( ) : ( Delete account ); const noWayToReverseThisStyles = React.useMemo( () => [styles.warningText, styles.lastWarningText], [styles.warningText, styles.lastWarningText], ); const deleteKeyserverAction = React.useCallback(async () => { try { await deleteNativeCredentialsFor(); return await callDeleteKeyserverAccount(preRequestUserState); } catch (e) { Alert.alert( 'Unknown error deleting keyserver account', 'Uhh... try again?', [{ text: 'OK' }], { cancelable: false, }, ); throw e; } }, [callDeleteKeyserverAccount, preRequestUserState]); const deleteIdentityAction = React.useCallback(async () => { try { - const deviceID = await getContentSigningKey(); - return await callDeleteIdentityAccount(commRustModule, deviceID); + return await callDeleteIdentityAccount(); } catch (e) { Alert.alert( 'Unknown error deleting account', 'Uhh... try again?', [{ text: 'OK' }], { cancelable: false, }, ); throw e; } }, [callDeleteIdentityAccount]); const onDelete = React.useCallback(() => { void dispatchActionPromise( deleteKeyserverAccountActionTypes, deleteKeyserverAction(), ); if (usingCommServicesAccessToken) { void dispatchActionPromise( deleteIdentityAccountActionTypes, deleteIdentityAction(), ); } }, [dispatchActionPromise, deleteKeyserverAction, deleteIdentityAction]); return ( Your account will be permanently deleted. There is no way to reverse this. ); }, ); const unboundStyles = { deleteButton: { backgroundColor: 'vibrantRedButton', borderRadius: 5, flex: 1, marginHorizontal: 24, marginVertical: 12, padding: 12, }, lastWarningText: { marginBottom: 24, }, saveText: { color: 'white', fontSize: 18, textAlign: 'center', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, warningText: { color: 'panelForegroundLabel', fontSize: 16, marginHorizontal: 24, textAlign: 'center', }, }; export default DeleteAccount; diff --git a/native/root.react.js b/native/root.react.js index bcd96fb88..814acb958 100644 --- a/native/root.react.js +++ b/native/root.react.js @@ -1,374 +1,377 @@ // @flow import { ActionSheetProvider } from '@expo/react-native-action-sheet'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import AsyncStorage from '@react-native-async-storage/async-storage'; import type { PossiblyStaleNavigationState, UnsafeContainerActionEvent, GenericNavigationAction, } from '@react-navigation/core'; import { useReduxDevToolsExtension } from '@react-navigation/devtools'; import { NavigationContainer } from '@react-navigation/native'; import * as SplashScreen from 'expo-splash-screen'; import invariant from 'invariant'; import * as React from 'react'; import { Platform, UIManager, StyleSheet } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import { SafeAreaProvider, initialWindowMetrics, } from 'react-native-safe-area-context'; import { Provider } from 'react-redux'; import { PersistGate as ReduxPersistGate } from 'redux-persist/es/integration/react.js'; import { ChatMentionContextProvider } from 'lib/components/chat-mention-provider.react.js'; import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js'; import { ENSCacheProvider } from 'lib/components/ens-cache-provider.react.js'; import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import KeyserverConnectionsHandler from 'lib/components/keyserver-connections-handler.js'; import { MediaCacheProvider } from 'lib/components/media-cache-provider.react.js'; import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js'; import { actionLogger } from 'lib/utils/action-logger.js'; import { RegistrationContextProvider } from './account/registration/registration-context-provider.react.js'; import NativeEditThreadAvatarProvider from './avatars/native-edit-thread-avatar-provider.react.js'; import BackupHandler from './backup/backup-handler.js'; import { BottomSheetProvider } from './bottom-sheet/bottom-sheet-provider.react.js'; import ChatContextProvider from './chat/chat-context-provider.react.js'; import MessageEditingContextProvider from './chat/message-editing-context-provider.react.js'; import { FeatureFlagsProvider } from './components/feature-flags-provider.react.js'; import PersistedStateGate from './components/persisted-state-gate.js'; import VersionSupportedChecker from './components/version-supported.react.js'; import ConnectedStatusBar from './connected-status-bar.react.js'; import { SQLiteDataHandler } from './data/sqlite-data-handler.js'; import ErrorBoundary from './error-boundary.react.js'; import { peerToPeerMessageHandler } from './handlers/peer-to-peer-message-handler.js'; +import IdentityServiceContextProvider from './identity-service/identity-service-context-provider.react.js'; import InputStateContainer from './input/input-state-container.react.js'; import LifecycleHandler from './lifecycle/lifecycle-handler.react.js'; import MarkdownContextProvider from './markdown/markdown-context-provider.react.js'; import { filesystemMediaCache } from './media/media-cache.js'; import { DeepLinksContextProvider } from './navigation/deep-links-context-provider.react.js'; import { defaultNavigationState } from './navigation/default-state.js'; import DisconnectedBarVisibilityHandler from './navigation/disconnected-bar-visibility-handler.react.js'; import { setGlobalNavContext } from './navigation/icky-global.js'; import { NavContext, type NavContextType, } from './navigation/navigation-context.js'; import NavigationHandler from './navigation/navigation-handler.react.js'; import { validNavState } from './navigation/navigation-utils.js'; import OrientationHandler from './navigation/orientation-handler.react.js'; import { navStateAsyncStorageKey } from './navigation/persistance.js'; import RootNavigator from './navigation/root-navigator.react.js'; import ConnectivityUpdater from './redux/connectivity-updater.react.js'; import { DimensionsUpdater } from './redux/dimensions-updater.react.js'; import { getPersistor } from './redux/persist.js'; import { store } from './redux/redux-setup.js'; import { useSelector } from './redux/redux-utils.js'; import { RootContext } from './root-context.js'; import { MessageSearchProvider } from './search/search-provider.react.js'; import Socket from './socket.react.js'; import { StaffContextProvider } from './staff/staff-context.provider.react.js'; import { useLoadCommFonts } from './themes/fonts.js'; import { DarkTheme, LightTheme } from './themes/navigation.js'; import ThemeHandler from './themes/theme-handler.react.js'; import { provider } from './utils/ethers-utils.js'; import { useTunnelbrokerInitMessage } from './utils/tunnelbroker-utils.js'; // Add custom items to expo-dev-menu import './dev-menu.js'; import './types/message-types-validator.js'; if (Platform.OS === 'android') { UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); } const navInitAction = Object.freeze({ type: 'NAV/@@INIT' }); const navUnknownAction = Object.freeze({ type: 'NAV/@@UNKNOWN' }); SplashScreen.preventAutoHideAsync().catch(console.log); function Root() { const navStateRef = React.useRef(); const navDispatchRef = React.useRef GenericNavigationAction), ) => void>(); const navStateInitializedRef = React.useRef(false); // We call this here to start the loading process // We gate the UI on the fonts loading in AppNavigator useLoadCommFonts(); const [navContext, setNavContext] = React.useState(null); const updateNavContext = React.useCallback(() => { if ( !navStateRef.current || !navDispatchRef.current || !navStateInitializedRef.current ) { return; } const updatedNavContext = { state: navStateRef.current, dispatch: navDispatchRef.current, }; setNavContext(updatedNavContext); setGlobalNavContext(updatedNavContext); }, []); const [initialState, setInitialState] = React.useState( __DEV__ ? undefined : defaultNavigationState, ); React.useEffect(() => { Orientation.lockToPortrait(); void (async () => { let loadedState = initialState; if (__DEV__) { try { const navStateString = await AsyncStorage.getItem( navStateAsyncStorageKey, ); if (navStateString) { const savedState = JSON.parse(navStateString); if (validNavState(savedState)) { loadedState = savedState; } } } catch {} } if (!loadedState) { loadedState = defaultNavigationState; } if (loadedState !== initialState) { setInitialState(loadedState); } navStateRef.current = loadedState; updateNavContext(); actionLogger.addOtherAction('navState', navInitAction, null, loadedState); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [updateNavContext]); const setNavStateInitialized = React.useCallback(() => { navStateInitializedRef.current = true; updateNavContext(); }, [updateNavContext]); const [rootContext, setRootContext] = React.useState(() => ({ setNavStateInitialized, })); const detectUnsupervisedBackgroundRef = React.useCallback( (detectUnsupervisedBackground: ?(alreadyClosed: boolean) => boolean) => { setRootContext(prevRootContext => ({ ...prevRootContext, detectUnsupervisedBackground, })); }, [], ); const frozen = useSelector(state => state.frozen); const queuedActionsRef = React.useRef>([]); const onNavigationStateChange = React.useCallback( (state: ?PossiblyStaleNavigationState) => { invariant(state, 'nav state should be non-null'); const prevState = navStateRef.current; navStateRef.current = state; updateNavContext(); const queuedActions = queuedActionsRef.current; queuedActionsRef.current = []; if (queuedActions.length === 0) { queuedActions.push(navUnknownAction); } for (const action of queuedActions) { actionLogger.addOtherAction('navState', action, prevState, state); } if (!__DEV__ || frozen) { return; } void (async () => { try { await AsyncStorage.setItem( navStateAsyncStorageKey, JSON.stringify(state), ); } catch (e) { console.log('AsyncStorage threw while trying to persist navState', e); } })(); }, [updateNavContext, frozen], ); const navContainerRef = React.useRef>(); const containerRef = React.useCallback( (navContainer: ?React.ElementRef) => { navContainerRef.current = navContainer; if (navContainer && !navDispatchRef.current) { navDispatchRef.current = navContainer.dispatch; updateNavContext(); } }, [updateNavContext], ); useReduxDevToolsExtension(navContainerRef); const navContainer = navContainerRef.current; React.useEffect(() => { if (!navContainer) { return undefined; } return navContainer.addListener( '__unsafe_action__', (event: { +data: UnsafeContainerActionEvent, ... }) => { const { action, noop } = event.data; const navState = navStateRef.current; if (noop) { actionLogger.addOtherAction('navState', action, navState, navState); return; } queuedActionsRef.current.push({ ...action, type: `NAV/${action.type}`, }); }, ); }, [navContainer]); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const theme = (() => { if (activeTheme === 'light') { return LightTheme; } else if (activeTheme === 'dark') { return DarkTheme; } return undefined; })(); const tunnelbrokerInitMessage = useTunnelbrokerInitMessage(); const gated: React.Node = ( <> ); let navigation; if (initialState) { navigation = ( ); } return ( - - - - - - - - - - - - - - - - - - - - {gated} - - - - - - {navigation} - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + {gated} + + + + + + {navigation} + + + + + + + + + + + + + + + + + ); } const styles = StyleSheet.create({ app: { flex: 1, }, }); function AppRoot(): React.Node { return ( ); } export default AppRoot; diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js index e27c17e02..ecc6025b5 100644 --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,143 +1,119 @@ // @flow import identityServiceConfig from 'lib/facts/identity-service.js'; import type { IdentityServiceAuthLayer, + IdentityServiceClient, OutboundKeyInfoResponse, } from 'lib/types/identity-service-types.js'; import { VersionInterceptor, AuthInterceptor } from './interceptor.js'; import * as IdentityAuthClient from '../protobufs/identity-auth-client.cjs'; import * as IdentityAuthStructs from '../protobufs/identity-auth-structs.cjs'; import { Empty } from '../protobufs/identity-unauth-structs.cjs'; -import * as IdentityClient from '../protobufs/identity-unauth.cjs'; +import * as IdentityUnauthClient from '../protobufs/identity-unauth.cjs'; -class IdentityServiceClientWrapper { +class IdentityServiceClientWrapper implements IdentityServiceClient { authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient; - unauthorizedClient: ?IdentityClient.IdentityClientServicePromiseClient; + unauthClient: IdentityUnauthClient.IdentityClientServicePromiseClient; - constructor() { - this.authClient = null; - this.unauthorizedClient = null; + constructor(authLayer: ?IdentityServiceAuthLayer) { + if (authLayer) { + this.authClient = + IdentityServiceClientWrapper.createAuthClient(authLayer); + } + this.unauthClient = IdentityServiceClientWrapper.createUnauthClient(); } - determineSocketAddr(): string { + static determineSocketAddr(): string { return process.env.IDENTITY_SOCKET_ADDR ?? identityServiceConfig.defaultURL; } - async initAuthClient(authLayer: IdentityServiceAuthLayer): Promise { + static createAuthClient( + authLayer: IdentityServiceAuthLayer, + ): IdentityAuthClient.IdentityClientServicePromiseClient { const { userID, deviceID, commServicesAccessToken } = authLayer; - const identitySocketAddr = this.determineSocketAddr(); + const identitySocketAddr = + IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); const authInterceptor = new AuthInterceptor( userID, deviceID, commServicesAccessToken, ); const authClientOpts = { unaryInterceptors: [versionInterceptor, authInterceptor], }; - this.authClient = new IdentityAuthClient.IdentityClientServicePromiseClient( + return new IdentityAuthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, authClientOpts, ); } - async initUnauthorizedClient(): Promise { - const identitySocketAddr = this.determineSocketAddr(); + static createUnauthClient(): IdentityUnauthClient.IdentityClientServicePromiseClient { + const identitySocketAddr = + IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor(); - const unauthorizedClientOpts = { + const unauthClientOpts = { unaryInterceptors: [versionInterceptor], }; - this.unauthorizedClient = - new IdentityClient.IdentityClientServicePromiseClient( - identitySocketAddr, - null, - unauthorizedClientOpts, - ); + return new IdentityUnauthClient.IdentityClientServicePromiseClient( + identitySocketAddr, + null, + unauthClientOpts, + ); } - deleteUser: ( - userID: string, - deviceID: string, - accessToken: string, - ) => Promise = async ( - userID: string, - deviceID: string, - accessToken: string, - ): Promise => { + deleteUser: () => Promise = async () => { if (!this.authClient) { - const authLayer: IdentityServiceAuthLayer = { - userID, - deviceID, - commServicesAccessToken: accessToken, - }; - await this.initAuthClient(authLayer); - } - - if (this.authClient) { - await this.authClient.deleteUser(new Empty()); - } else { throw new Error('Identity service client is not initialized'); } + await this.authClient.deleteUser(new Empty()); }; - async getKeyserverKeys( - userID: string, - deviceID: string, - accessToken: string, - keyserverID: string, - ): Promise { - if (!this.authClient) { - const authLayer: IdentityServiceAuthLayer = { - userID, - deviceID, - commServicesAccessToken: accessToken, + getKeyserverKeys: (keyserverID: string) => Promise = + async (keyserverID: string) => { + const client = this.authClient; + if (!client) { + throw new Error('Identity service client is not initialized'); + } + + const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); + request.setUserId(keyserverID); + const response = await client.getKeyserverKeys(request); + const keyserverInfo = response.getKeyserverInfo(); + if (!response.hasKeyserverInfo() || !keyserverInfo) { + return null; + } + + const identityInfo = keyserverInfo.getIdentityInfo(); + const contentPreKey = keyserverInfo.getContentPrekey(); + const notifPreKey = keyserverInfo.getNotifPrekey(); + + if (!identityInfo || !contentPreKey || !notifPreKey) { + return null; + } + + return { + payload: identityInfo.getPayload(), + payloadSignature: identityInfo.getPayloadSignature(), + socialProof: identityInfo.getSocialProof(), + contentPrekey: contentPreKey.getPrekey(), + contentPrekeySignature: contentPreKey.getPrekeySignature(), + notifPrekey: notifPreKey.getPrekey(), + notifPrekeySignature: notifPreKey.getPrekeySignature(), + oneTimeContentPrekey: keyserverInfo.getOneTimeContentPrekey(), + oneTimeNotifPrekey: keyserverInfo.getOneTimeNotifPrekey(), }; - await this.initAuthClient(authLayer); - } - - const client = this.authClient; - if (!client) { - throw new Error('Identity service client is not initialized'); - } - - const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); - request.setUserId(keyserverID); - const response = await client.getKeyserverKeys(request); - const keyserverInfo = response.getKeyserverInfo(); - if (!response.hasKeyserverInfo() || !keyserverInfo) { - return null; - } - - const identityInfo = keyserverInfo.getIdentityInfo(); - const contentPreKey = keyserverInfo.getContentPrekey(); - const notifPreKey = keyserverInfo.getNotifPrekey(); - - if (!identityInfo || !contentPreKey || !notifPreKey) { - return null; - } - - return { - payload: identityInfo.getPayload(), - payloadSignature: identityInfo.getPayloadSignature(), - socialProof: identityInfo.getSocialProof(), - contentPrekey: contentPreKey.getPrekey(), - contentPrekeySignature: contentPreKey.getPrekeySignature(), - notifPrekey: notifPreKey.getPrekey(), - notifPrekeySignature: notifPreKey.getPrekeySignature(), - oneTimeContentPrekey: keyserverInfo.getOneTimeContentPrekey(), - oneTimeNotifPrekey: keyserverInfo.getOneTimeNotifPrekey(), }; - } } export { IdentityServiceClientWrapper }; diff --git a/web/grpc/identity-service-context-provider.react.js b/web/grpc/identity-service-context-provider.react.js new file mode 100644 index 000000000..3db503c7e --- /dev/null +++ b/web/grpc/identity-service-context-provider.react.js @@ -0,0 +1,50 @@ +// @flow + +import * as React from 'react'; + +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import type { IdentityServiceClient } from 'lib/types/identity-service-types.js'; + +import { IdentityServiceClientWrapper } from './identity-service-client-wrapper.js'; +import { useSelector } from '../redux/redux-utils.js'; + +type Props = { + +children: React.Node, +}; +function IdentityServiceContextProvider(props: Props): React.Node { + const { children } = props; + const [client, setClient] = React.useState(); + + const userID = useSelector(state => state.currentUserInfo?.id); + const accessToken = useSelector(state => state.commServicesAccessToken); + const deviceID = useSelector( + state => state.cryptoStore?.primaryIdentityKeys.ed25519, + ); + + React.useEffect(() => { + let authLayer = null; + if (userID && deviceID && accessToken) { + authLayer = { + userID, + deviceID, + commServicesAccessToken: accessToken, + }; + } + setClient(new IdentityServiceClientWrapper(authLayer)); + }, [accessToken, deviceID, userID]); + + const value = React.useMemo( + () => ({ + identityClient: client, + }), + [client], + ); + + return ( + + {children} + + ); +} + +export default IdentityServiceContextProvider; diff --git a/web/root.js b/web/root.js index 56e7b49af..a7b049105 100644 --- a/web/root.js +++ b/web/root.js @@ -1,60 +1,63 @@ // @flow import localforage from 'localforage'; import * as React from 'react'; import { Provider } from 'react-redux'; import { Router, Route } from 'react-router'; import { createStore, applyMiddleware, type Store } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction.js'; import { persistReducer, persistStore } from 'redux-persist'; import thunk from 'redux-thunk'; import IntegrityHandler from 'lib/components/integrity-handler.react.js'; import KeyserverConnectionsHandler from 'lib/components/keyserver-connections-handler.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { GetOrCreateCryptoStoreProvider, WebNotificationsSessionCreatorProvider, } from './account/account-hooks.js'; import App from './app.react.js'; import { SQLiteDataHandler } from './database/sqlite-data-handler.js'; import { localforageConfig } from './database/utils/constants.js'; import ErrorBoundary from './error-boundary.react.js'; +import IdentityServiceContextProvider from './grpc/identity-service-context-provider.react.js'; import { defaultWebState } from './redux/default-state.js'; import InitialReduxStateGate from './redux/initial-state-gate.js'; import { persistConfig } from './redux/persist.js'; import { type AppState, type Action, reducer } from './redux/redux-setup.js'; import history from './router-history.js'; import Socket from './socket.react.js'; localforage.config(localforageConfig); const persistedReducer = persistReducer(persistConfig, reducer); const store: Store = createStore( persistedReducer, defaultWebState, composeWithDevTools({})(applyMiddleware(thunk, reduxLoggerMiddleware)), ); const persistor = persistStore(store); const RootProvider = (): React.Node => ( - - - - - - - - + + + + + + + + + + ); export default RootProvider; diff --git a/web/settings/account-delete-modal.react.js b/web/settings/account-delete-modal.react.js index 8ac0fc6e6..804d61f7b 100644 --- a/web/settings/account-delete-modal.react.js +++ b/web/settings/account-delete-modal.react.js @@ -1,160 +1,145 @@ // @flow import * as React from 'react'; import { useDeleteKeyserverAccount, deleteKeyserverAccountActionTypes, useDeleteIdentityAccount, deleteIdentityAccountActionTypes, } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import css from './account-delete-modal.css'; import Button, { buttonThemes } from '../components/button.react.js'; -import { IdentityServiceClientWrapper } from '../grpc/identity-service-client-wrapper.js'; import Modal from '../modals/modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; const deleteKeyserverAccountLoadingStatusSelector = createLoadingStatusSelector( deleteKeyserverAccountActionTypes, ); const deleteIdentityAccountLoadingStatusSelector = createLoadingStatusSelector( deleteIdentityAccountActionTypes, ); const AccountDeleteModal: React.ComponentType<{}> = React.memo<{}>( function AccountDeleteModal(): React.Node { const preRequestUserState = useSelector(preRequestUserStateSelector); const isDeleteKeyserverAccountLoading = useSelector( state => deleteKeyserverAccountLoadingStatusSelector(state) === 'loading', ); const isDeleteIdentityAccountLoading = useSelector( state => deleteIdentityAccountLoadingStatusSelector(state) === 'loading', ); const inputDisabled = isDeleteKeyserverAccountLoading || isDeleteIdentityAccountLoading; - const deviceID = useSelector( - state => state.cryptoStore?.primaryIdentityKeys.ed25519, - ); - - const identityServiceClientWrapperRef = - React.useRef(null); - if (!identityServiceClientWrapperRef.current) { - identityServiceClientWrapperRef.current = - new IdentityServiceClientWrapper(); - } - const identityServiceClient = identityServiceClientWrapperRef.current; const callDeleteIdentityAccount = useDeleteIdentityAccount(); const callDeleteKeyserverAccount = useDeleteKeyserverAccount(); const dispatchActionPromise = useDispatchActionPromise(); const { popModal } = useModalContext(); const [keyserverErrorMessage, setKeyserverErrorMessage] = React.useState(''); const [identityErrorMessage, setIdentityErrorMessage] = React.useState(''); const keyserverError = keyserverErrorMessage ? (

{keyserverErrorMessage}

) : null; const identityError = identityErrorMessage ? (

{identityErrorMessage}

) : null; let combinedErrorMessages; if (keyserverError || identityError) { combinedErrorMessages = (
{keyserverError} {identityError}
); } const deleteKeyserverAction = React.useCallback(async () => { try { setKeyserverErrorMessage(''); const response = await callDeleteKeyserverAccount(preRequestUserState); // This check ensures that we don't call `popModal()` twice if (!usingCommServicesAccessToken) { popModal(); } return response; } catch (e) { setKeyserverErrorMessage( 'unknown error deleting account from keyserver', ); throw e; } }, [callDeleteKeyserverAccount, preRequestUserState, popModal]); const deleteIdentityAction = React.useCallback(async () => { try { setIdentityErrorMessage(''); - const response = await callDeleteIdentityAccount( - identityServiceClient, - deviceID, - ); + const response = await callDeleteIdentityAccount(); popModal(); return response; } catch (e) { setIdentityErrorMessage( 'unknown error deleting account from identity service', ); throw e; } - }, [callDeleteIdentityAccount, deviceID, identityServiceClient, popModal]); + }, [callDeleteIdentityAccount, popModal]); const onDelete = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); void dispatchActionPromise( deleteKeyserverAccountActionTypes, deleteKeyserverAction(), ); if (usingCommServicesAccessToken) { void dispatchActionPromise( deleteIdentityAccountActionTypes, deleteIdentityAction(), ); } }, [dispatchActionPromise, deleteKeyserverAction, deleteIdentityAction], ); return (

Your account will be permanently deleted. There is no way to reverse this.

{combinedErrorMessages}
); }, ); export default AccountDeleteModal;