diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index 982145486..6ea2031ee 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,55 +1,57 @@ // @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: () => Promise; +getKeyserverKeys: string => Promise; +registerUser?: ( username: string, password: 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, }; export type IdentityRegisterResult = { +userID: string, +accessToken: string, +username: string, }; + +export const ONE_TIME_KEYS_NUMBER = 10; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js index 37c41bda5..b77aafed1 100644 --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1,1312 +1,1328 @@ // @flow import type { LogOutResult, KeyserverLogOutResult, LogInStartingPayload, LogInResult, RegisterResult, DefaultNotificationPayload, ClaimUsernameResponse, KeyserverAuthResult, } from './account-types.js'; import type { ActivityUpdateSuccessPayload, QueueActivityUpdatesPayload, SetThreadUnreadStatusPayload, } from './activity-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from './avatar-types.js'; import type { CryptoStore } from './crypto-types.js'; import type { GetVersionActionPayload, LastCommunicatedPlatformDetails, } from './device-types.js'; import type { DraftStore } from './draft-types.js'; import type { EnabledApps, SupportedApps } from './enabled-apps.js'; import type { RawEntryInfo, EntryStore, SaveEntryPayload, CreateEntryPayload, DeleteEntryResult, RestoreEntryPayload, FetchEntryInfosResult, CalendarQueryUpdateResult, CalendarQueryUpdateStartingPayload, CalendarQuery, FetchRevisionsForEntryPayload, } from './entry-types.js'; import type { CalendarFilter, CalendarThreadFilter, SetCalendarDeletedFilterPayload, } from './filter-types.js'; +import type { IdentityRegisterResult } from './identity-service-types.js'; import type { IntegrityStore } from './integrity-types.js'; import type { KeyserverStore, AddKeyserverPayload, RemoveKeyserverPayload, } from './keyserver-types.js'; import type { LifecycleState } from './lifecycle-state-types.js'; import type { FetchInviteLinksResponse, InviteLink, InviteLinksStore, InviteLinkVerificationResponse, DisableInviteLinkPayload, } from './link-types.js'; import type { LoadingStatus, LoadingInfo } from './loading-types.js'; import type { UpdateMultimediaMessageMediaPayload } from './media-types.js'; import type { MessageReportCreationResult } from './message-report-types.js'; import type { MessageStore, RawMultimediaMessageInfo, FetchMessageInfosPayload, SendMessagePayload, EditMessagePayload, SaveMessagesPayload, NewMessagesPayload, MessageStorePrunePayload, LocallyComposedMessageInfo, SimpleMessagesPayload, FetchPinnedMessagesResult, SearchMessagesResponse, } from './message-types.js'; import type { RawReactionMessageInfo } from './messages/reaction.js'; import type { RawTextMessageInfo } from './messages/text.js'; import type { BaseNavInfo } from './nav-types.js'; import { type ForcePolicyAcknowledgmentPayload, type PolicyAcknowledgmentPayload, type UserPolicies, } from './policy-types.js'; import type { RelationshipErrors } from './relationship-types.js'; import type { EnabledReports, ClearDeliveredReportsPayload, QueueReportsPayload, ReportStore, } from './report-types.js'; import type { ProcessServerRequestAction, GetOlmSessionInitializationDataResponse, } from './request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from './search-types.js'; import type { SetSessionPayload } from './session-types.js'; import type { ConnectionIssue, StateSyncFullActionPayload, StateSyncIncrementalActionPayload, UpdateConnectionStatusPayload, SetLateResponsePayload, UpdateDisconnectedBarPayload, } from './socket-types.js'; import { type ClientStore } from './store-ops-types.js'; import type { SubscriptionUpdateResult } from './subscription-types.js'; import type { GlobalThemeInfo } from './theme-types.js'; import type { ThreadActivityStore } from './thread-activity-types.js'; import type { ThreadStore, ChangeThreadSettingsPayload, LeaveThreadPayload, NewThreadResult, ThreadJoinPayload, ToggleMessagePinResult, RoleModificationPayload, RoleDeletionPayload, } from './thread-types.js'; import type { ClientUpdatesResultWithUserInfos } from './update-types.js'; import type { CurrentUserInfo, UserStore } from './user-types.js'; import type { SetDeviceTokenActionPayload } from '../actions/device-actions.js'; import type { NotifPermissionAlertInfo } from '../utils/push-alerts.js'; export type BaseAppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +notifPermissionAlertInfo: NotifPermissionAlertInfo, +actualizedCalendarQuery: CalendarQuery, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +nextLocalID: number, +dataLoaded: boolean, +userPolicies: UserPolicies, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +integrityStore: IntegrityStore, +globalThemeInfo: GlobalThemeInfo, +customServer: ?string, ... }; export type NativeAppState = BaseAppState<>; export type WebAppState = BaseAppState<> & { +cryptoStore: ?CryptoStore, +pushApiPublicKey: ?string, ... }; export type AppState = NativeAppState | WebAppState; export type BaseAction = | { +type: '@@redux/INIT', +payload?: void, } | { +type: 'FETCH_ENTRIES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ENTRIES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ENTRIES_SUCCESS', +payload: FetchEntryInfosResult, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_SUCCESS', +payload: KeyserverLogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_SUCCESS', +payload: ClaimUsernameResponse, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', +payload: KeyserverLogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_IDENTITY_ACCOUNT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_IDENTITY_ACCOUNT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_IDENTITY_ACCOUNT_SUCCESS', +payload: LogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_LOCAL_ENTRY', +payload: RawEntryInfo, } | { +type: 'CREATE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_ENTRY_SUCCESS', +payload: CreateEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_SUCCESS', +payload: SaveEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'CONCURRENT_MODIFICATION_RESET', +payload: { +id: string, +dbText: string, }, } | { +type: 'DELETE_ENTRY_STARTED', +loadingInfo: LoadingInfo, +payload: { +localID: ?string, +serverID: ?string, }, } | { +type: 'DELETE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ENTRY_SUCCESS', +payload: ?DeleteEntryResult, +loadingInfo: LoadingInfo, } | { +type: 'TEMP_IDENTITY_LOG_IN_STARTED', +loadingInfo: LoadingInfo, +payload?: void, } | { +type: 'TEMP_IDENTITY_LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'TEMP_IDENTITY_LOG_IN_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_AUTH_STARTED', +loadingInfo: LoadingInfo, +payload: LogInStartingPayload, } | { +type: 'KEYSERVER_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_AUTH_SUCCESS', +payload: KeyserverAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'LOG_IN_STARTED', +loadingInfo: LoadingInfo, +payload: LogInStartingPayload, } | { +type: 'LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LOG_IN_SUCCESS', +payload: LogInResult, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_REGISTER_STARTED', +loadingInfo: LoadingInfo, +payload: LogInStartingPayload, } | { +type: 'KEYSERVER_REGISTER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_REGISTER_SUCCESS', +payload: RegisterResult, +loadingInfo: LoadingInfo, } + | { + +type: 'IDENTITY_REGISTER_STARTED', + +payload?: void, + +loadingInfo: LoadingInfo, + } + | { + +type: 'IDENTITY_REGISTER_FAILED', + +payload: Error, + +loadingInfo: LoadingInfo, + } + | { + +type: 'IDENTITY_REGISTER_SUCCESS', + +payload: IdentityRegisterResult, + +loadingInfo: LoadingInfo, + } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_SUCCESS', +payload: LeaveThreadPayload, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_SUCCESS', +payload: NewThreadResult, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_SUCCESS', +payload: FetchRevisionsForEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_SUCCESS', +payload: RestoreEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_SUCCESS', +payload: ThreadJoinPayload, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_SUCCESS', +payload: LeaveThreadPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_NEW_SESSION', +payload: SetSessionPayload, } | { +type: 'persist/REHYDRATE', +payload: ?BaseAppState<>, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_SUCCESS', +payload: FetchMessageInfosPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_SUCCESS', +payload: FetchMessageInfosPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_SUCCESS', +payload: SimpleMessagesPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawTextMessageInfo, } | { +type: 'SEND_TEXT_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawMultimediaMessageInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawReactionMessageInfo, } | { +type: 'SEND_REACTION_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, +targetMessageID: string, +reaction: string, +action: string, }, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_SUCCESS', +payload: UserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_SUCCESS', +payload: ExactUserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_DRAFT', +payload: { +key: string, +text: string, }, } | { +type: 'MOVE_DRAFT', +payload: { +oldKey: string, +newKey: string, }, } | { +type: 'SET_CLIENT_DB_STORE', +payload: ClientStore, } | { +type: 'UPDATE_ACTIVITY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_SUCCESS', +payload: ActivityUpdateSuccessPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_SUCCESS', +payload: SetDeviceTokenActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'QUEUE_REPORTS', +payload: QueueReportsPayload, } | { +type: 'SET_URL_PREFIX', +payload: string, } | { +type: 'SAVE_MESSAGES', +payload: SaveMessagesPayload, } | { +type: 'UPDATE_CALENDAR_THREAD_FILTER', +payload: CalendarThreadFilter, } | { +type: 'CLEAR_CALENDAR_THREAD_FILTER', +payload?: void, } | { +type: 'SET_CALENDAR_DELETED_FILTER', +payload: SetCalendarDeletedFilterPayload, } | { +type: 'UPDATE_SUBSCRIPTION_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_SUCCESS', +payload: SubscriptionUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_STARTED', +loadingInfo: LoadingInfo, +payload?: CalendarQueryUpdateStartingPayload, } | { +type: 'UPDATE_CALENDAR_QUERY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_SUCCESS', +payload: CalendarQueryUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'FULL_STATE_SYNC', +payload: StateSyncFullActionPayload, } | { +type: 'INCREMENTAL_STATE_SYNC', +payload: StateSyncIncrementalActionPayload, } | ProcessServerRequestAction | { +type: 'UPDATE_CONNECTION_STATUS', +payload: UpdateConnectionStatusPayload, } | { +type: 'QUEUE_ACTIVITY_UPDATES', +payload: QueueActivityUpdatesPayload, } | { +type: 'UNSUPERVISED_BACKGROUND', +payload: { +keyserverID: string }, } | { +type: 'UPDATE_LIFECYCLE_STATE', +payload: LifecycleState, } | { +type: 'ENABLE_APP', +payload: SupportedApps, } | { +type: 'DISABLE_APP', +payload: SupportedApps, } | { +type: 'UPDATE_REPORTS_ENABLED', +payload: Partial, } | { +type: 'PROCESS_UPDATES', +payload: ClientUpdatesResultWithUserInfos, } | { +type: 'PROCESS_MESSAGES', +payload: NewMessagesPayload, } | { +type: 'MESSAGE_STORE_PRUNE', +payload: MessageStorePrunePayload, } | { +type: 'SET_LATE_RESPONSE', +payload: SetLateResponsePayload, } | { +type: 'UPDATE_DISCONNECTED_BAR', +payload: UpdateDisconnectedBarPayload, } | { +type: 'REQUEST_ACCESS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA', +payload: UpdateMultimediaMessageMediaPayload, } | { +type: 'CREATE_LOCAL_MESSAGE', +payload: LocallyComposedMessageInfo, } | { +type: 'UPDATE_RELATIONSHIPS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_SUCCESS', +payload: RelationshipErrors, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_STARTED', +payload: { +threadID: string, +unread: boolean, }, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_SUCCESS', +payload: SetThreadUnreadStatusPayload, } | { +type: 'SET_USER_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SET_USER_SETTINGS_SUCCESS', +payload: DefaultNotificationPayload, } | { +type: 'SET_USER_SETTINGS_FAILED', +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_SUCCESS', +payload: MessageReportCreationResult, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FORCE_POLICY_ACKNOWLEDGMENT', +payload: ForcePolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_SUCCESS', +payload: PolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SIWE_AUTH_STARTED', +payload: LogInStartingPayload, +loadingInfo: LoadingInfo, } | { +type: 'SIWE_AUTH_SUCCESS', +payload: LogInResult, +loadingInfo: LoadingInfo, } | { +type: 'SIWE_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RECORD_NOTIF_PERMISSION_ALERT', +payload: { +time: number }, } | { +type: 'UPDATE_USER_AVATAR_STARTED', +payload: UpdateUserAvatarRequest, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_SUCCESS', +payload: UpdateUserAvatarResponse, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'SEND_EDIT_MESSAGE_SUCCESS', +payload: EditMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'TOGGLE_MESSAGE_PIN_SUCCESS', +payload: ToggleMessagePinResult, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PINNED_MESSAGES_SUCCESS', +payload: FetchPinnedMessagesResult, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'VERIFY_INVITE_LINK_SUCCESS', +payload: InviteLinkVerificationResponse, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', +payload: FetchInviteLinksResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CALENDAR_COMMUNITY_FILTER', +payload: void, } | { +type: 'UPDATE_CHAT_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CHAT_COMMUNITY_FILTER', +payload: void, } | { +type: 'SEARCH_MESSAGES_STARTED', +payload: void, +loadingInfo?: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_SUCCESS', +payload: SearchMessagesResponse, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', +payload: InviteLink, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DISABLE_INVITE_LINK_SUCCESS', +payload: DisableInviteLinkPayload, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', +payload: GetOlmSessionInitializationDataResponse, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DATA_LOADED', +payload: { +dataLoaded: boolean, }, } | { +type: 'GET_VERSION_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_VERSION_SUCCESS', +payload: GetVersionActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'GET_VERSION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_LAST_COMMUNICATED_PLATFORM_DETAILS', +payload: LastCommunicatedPlatformDetails, } | { +type: 'RESET_USER_STATE', +payload?: void } | { +type: 'MODIFY_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'MODIFY_COMMUNITY_ROLE_SUCCESS', +payload: RoleModificationPayload, +loadingInfo: LoadingInfo, } | { +type: 'MODIFY_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DELETE_COMMUNITY_ROLE_SUCCESS', +payload: RoleDeletionPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_ACCESS_TOKEN', +payload: string, } | { +type: 'UPDATE_THREAD_LAST_NAVIGATED', +payload: { +threadID: string, +time: number }, } | { +type: 'UPDATE_INTEGRITY_STORE', +payload: { +threadIDsToHash?: $ReadOnlyArray, +threadHashingStatus?: 'starting' | 'running' | 'completed', }, } | { +type: 'UPDATE_THEME_INFO', +payload: Partial, } | { +type: 'ADD_KEYSERVER', +payload: AddKeyserverPayload, } | { +type: 'REMOVE_KEYSERVER', +payload: RemoveKeyserverPayload, } | { +type: 'SET_CUSTOM_SERVER', +payload: string, } | { +type: 'SET_CONNECTION_ISSUE', +payload: { +connectionIssue: ?ConnectionIssue, +keyserverID: string }, }; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); export type SuperAction = { type: string, payload?: ActionPayload, loadingInfo?: LoadingInfo, error?: boolean, }; type ThunkedAction = (dispatch: Dispatch) => void; export type PromisedAction = (dispatch: Dispatch) => Promise; export type Dispatch = ((promisedAction: PromisedAction) => Promise) & ((thunkedAction: ThunkedAction) => void) & ((action: SuperAction) => boolean); // This is lifted from redux-persist/lib/constants.js // I don't want to add redux-persist to the web/server bundles... // import { REHYDRATE } from 'redux-persist'; export const rehydrateActionType = 'persist/REHYDRATE'; diff --git a/native/account/register-panel.react.js b/native/account/register-panel.react.js index 3e3b4d25d..396c92e7a 100644 --- a/native/account/register-panel.react.js +++ b/native/account/register-panel.react.js @@ -1,505 +1,508 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, StyleSheet, Platform, Keyboard, Linking, } from 'react-native'; import Animated from 'react-native-reanimated'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { keyserverRegisterActionTypes, keyserverRegister, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validUsernameRegex } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import type { RegisterInfo, LogInExtraInfo, RegisterResult, LogInStartingPayload, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { useServerCall } from 'lib/utils/action-utils.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { TextInput } from './modal-components.react.js'; import { setNativeCredentials } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; -import { AppOutOfDateAlertDetails } from '../utils/alert-messages.js'; +import { + AppOutOfDateAlertDetails, + UsernameReservedAlertDetails, + UsernameTakenAlertDetails, +} from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import { nativeNotificationsSessionCreator } from '../utils/crypto-utils.js'; import { type StateContainer } from '../utils/state-container.js'; type WritableRegisterState = { usernameInputText: string, passwordInputText: string, confirmPasswordInputText: string, }; export type RegisterState = $ReadOnly; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityValue: Animated.Node, +registerState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +logInExtraInfo: () => Promise, +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, +register: (registerInfo: RegisterInfo) => Promise, +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +confirmPasswordFocused: boolean, }; class RegisterPanel extends React.PureComponent { state: State = { confirmPasswordFocused: false, }; usernameInput: ?TextInput; passwordInput: ?TextInput; confirmPasswordInput: ?TextInput; passwordBeingAutoFilled = false; render(): React.Node { let confirmPasswordTextInputExtraProps; if ( Platform.OS !== 'ios' || this.state.confirmPasswordFocused || this.props.registerState.state.confirmPasswordInputText.length > 0 ) { confirmPasswordTextInputExtraProps = { secureTextEntry: true, textContentType: 'password', }; } let onPasswordKeyPress; if (Platform.OS === 'ios') { onPasswordKeyPress = this.onPasswordKeyPress; } const privatePolicyNotice = ( By signing up, you agree to our{' '} Terms {' & '} Privacy Policy . ); return ( {privatePolicyNotice} ); } usernameInputRef = (usernameInput: ?TextInput) => { this.usernameInput = usernameInput; }; passwordInputRef = (passwordInput: ?TextInput) => { this.passwordInput = passwordInput; }; confirmPasswordInputRef = (confirmPasswordInput: ?TextInput) => { this.confirmPasswordInput = confirmPasswordInput; }; focusUsernameInput = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; focusPasswordInput = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; focusConfirmPasswordInput = () => { invariant(this.confirmPasswordInput, 'ref should be set'); this.confirmPasswordInput.focus(); }; onTermsOfUsePressed = () => { void Linking.openURL('https://comm.app/terms'); }; onPrivacyPolicyPressed = () => { void Linking.openURL('https://comm.app/privacy'); }; onChangeUsernameInputText = (text: string) => { this.props.registerState.setState({ usernameInputText: text }); }; onChangePasswordInputText = (text: string) => { const stateUpdate: Partial = {}; stateUpdate.passwordInputText = text; if (this.passwordBeingAutoFilled) { this.passwordBeingAutoFilled = false; stateUpdate.confirmPasswordInputText = text; } this.props.registerState.setState(stateUpdate); }; onPasswordKeyPress = (event: KeyPressEvent) => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.props.registerState.state.confirmPasswordInputText.length === 0 ) { this.passwordBeingAutoFilled = true; } }; onChangeConfirmPasswordInputText = (text: string) => { this.props.registerState.setState({ confirmPasswordInputText: text }); }; onConfirmPasswordFocus = () => { this.setState({ confirmPasswordFocused: true }); }; onSubmit = async () => { this.props.setActiveAlert(true); if (this.props.registerState.state.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.registerState.state.passwordInputText !== this.props.registerState.state.confirmPasswordInputText ) { Alert.alert( 'Passwords don’t match', 'Password fields must contain the same password', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.registerState.state.usernameInputText.search( validUsernameRegex, ) === -1 ) { Alert.alert( 'Invalid username', 'Usernames must be at least six characters long, start with either a ' + 'letter or a number, and may contain only letters, numbers, or the ' + 'characters “-” and “_”', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else { Keyboard.dismiss(); const extraInfo = await this.props.logInExtraInfo(); const initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( keyserverRegisterActionTypes, this.registerAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); } }; onPasswordAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.passwordInput, 'ref should exist'); this.passwordInput.focus(); }, ); }; onUsernameAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { usernameInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; async registerAction(extraInfo: LogInExtraInfo): Promise { try { const result = await this.props.register({ ...extraInfo, username: this.props.registerState.state.usernameInputText, password: this.props.registerState.state.passwordInputText, }); this.props.setActiveAlert(false); this.props.dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.props.registerState.state.passwordInputText, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( - 'Username reserved', - 'This username is currently reserved. Please contact support@' + - 'comm.app if you would like to claim this account.', + UsernameReservedAlertDetails.title, + UsernameReservedAlertDetails.message, [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'username_taken') { Alert.alert( - 'Username taken', - 'An account with that username already exists', + UsernameTakenAlertDetails.title, + UsernameTakenAlertDetails.message, [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } onUnknownErrorAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { usernameInputText: '', passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; onAppOutOfDateAlertAcknowledged = () => { this.props.setActiveAlert(false); }; } const styles = StyleSheet.create({ container: { zIndex: 2, }, footer: { alignItems: 'stretch', flexDirection: 'row', flexShrink: 1, justifyContent: 'space-between', paddingLeft: 24, }, hyperlinkText: { color: '#036AFF', fontWeight: 'bold', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, notice: { alignSelf: 'center', display: 'flex', flexShrink: 1, maxWidth: 190, paddingBottom: 18, paddingRight: 8, paddingTop: 12, }, noticeText: { color: '#444', fontSize: 13, lineHeight: 20, textAlign: 'center', }, row: { marginHorizontal: 24, }, }); const registerLoadingStatusSelector = createLoadingStatusSelector( keyserverRegisterActionTypes, ); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedRegisterPanel: React.ComponentType = React.memo(function ConnectedRegisterPanel(props: BaseProps) { const registerLoadingStatus = useSelector(registerLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( registerLoadingStatus, olmSessionInitializationDataLoadingStatus, ); const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callRegister = useServerCall(keyserverRegister); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage( nativeNotificationsSessionCreator, ); return ( ); }); export default ConnectedRegisterPanel; diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js index f6cd9075b..da7f7f1d9 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,222 +1,287 @@ // @flow import * as React from 'react'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { keyserverRegisterActionTypes, keyserverRegister, + useIdentityRegister, + identityRegisterActionTypes, } from 'lib/actions/user-actions.js'; import type { LogInStartingPayload } from 'lib/types/account-types.js'; import { useServerCall } from 'lib/utils/action-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 type { RegistrationServerCallInput, UsernameAccountSelection, AvatarData, } from './registration-types.js'; import { useNativeSetUserAvatar, useUploadSelectedMedia, } from '../../avatars/avatar-hooks.js'; import { useSelector } from '../../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../../selectors/account-selectors.js'; -import { AppOutOfDateAlertDetails } from '../../utils/alert-messages.js'; +import { + AppOutOfDateAlertDetails, + UsernameReservedAlertDetails, + UsernameTakenAlertDetails, +} from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { setNativeCredentials } from '../native-credentials.js'; import { useSIWEServerCall } 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: 'waiting_for_registration_call', +avatarData: ?AvatarData, +resolve: () => void, +reject: Error => void, }; const inactiveStep = { step: 'inactive' }; function useRegistrationServerCall(): RegistrationServerCallInput => Promise { const [currentStep, setCurrentStep] = React.useState(inactiveStep); // STEP 1: ACCOUNT REGISTRATION const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const dispatchActionPromise = useDispatchActionPromise(); - const callRegister = useServerCall(keyserverRegister); + const callKeyserverRegister = useServerCall(keyserverRegister); + const callIdentityRegister = useIdentityRegister(); + + const identityRegisterUsernameAccount = React.useCallback( + async (accountSelection: UsernameAccountSelection) => { + const identityRegisterPromise = (async () => { + try { + const result = await callIdentityRegister( + accountSelection.username, + accountSelection.password, + ); + await setNativeCredentials({ + username: accountSelection.username, + password: accountSelection.password, + }); + return result; + } catch (e) { + if (e.message === 'username reserved') { + Alert.alert( + UsernameReservedAlertDetails.title, + UsernameReservedAlertDetails.message, + ); + } else if (e.message === 'username already exists') { + Alert.alert( + UsernameTakenAlertDetails.title, + UsernameTakenAlertDetails.message, + ); + } else if (e.message === 'Unsupported version') { + Alert.alert( + AppOutOfDateAlertDetails.title, + AppOutOfDateAlertDetails.message, + ); + } else { + Alert.alert('Unknown error', 'Uhh... try again?'); + } + throw e; + } + })(); + void dispatchActionPromise( + identityRegisterActionTypes, + identityRegisterPromise, + ); + await identityRegisterPromise; + }, + [callIdentityRegister, dispatchActionPromise], + ); - const registerUsernameAccount = React.useCallback( + const keyserverRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, keyserverURL: string, ) => { const extraInfo = await logInExtraInfo(); - const registerPromise = (async () => { + const keyserverRegisterPromise = (async () => { try { - const result = await callRegister( + const result = await callKeyserverRegister( { ...extraInfo, username: accountSelection.username, password: accountSelection.password, }, { urlPrefixOverride: keyserverURL, }, ); await setNativeCredentials({ username: result.currentUserInfo.username, password: accountSelection.password, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( - 'Username reserved', - 'This username is currently reserved. Please contact support@' + - 'comm.app if you would like to claim this account.', + UsernameReservedAlertDetails.title, + UsernameReservedAlertDetails.message, ); } else if (e.message === 'username_taken') { Alert.alert( - 'Username taken', - 'An account with that username already exists', + UsernameTakenAlertDetails.title, + UsernameTakenAlertDetails.message, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, ); } else { Alert.alert('Unknown error', 'Uhh... try again?'); } throw e; } })(); void dispatchActionPromise( keyserverRegisterActionTypes, - registerPromise, + keyserverRegisterPromise, undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); - await registerPromise; + await keyserverRegisterPromise; }, - [logInExtraInfo, callRegister, dispatchActionPromise], + [logInExtraInfo, callKeyserverRegister, dispatchActionPromise], ); const siweServerCall = useSIWEServerCall(); 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 } = input; - if (accountSelection.accountType === 'username') { - await registerUsernameAccount(accountSelection, keyserverURL); + if ( + accountSelection.accountType === 'username' && + !usingCommServicesAccessToken + ) { + await keyserverRegisterUsernameAccount( + accountSelection, + keyserverURL, + ); + } else if (accountSelection.accountType === 'username') { + await identityRegisterUsernameAccount(accountSelection); } else { try { await siweServerCall(accountSelection, { urlPrefixOverride: keyserverURL, }); } catch (e) { Alert.alert('Unknown error', 'Uhh... try again?'); throw e; } } dispatch({ type: setURLPrefix, payload: keyserverURL, }); setCurrentStep({ step: 'waiting_for_registration_call', avatarData, resolve, reject, }); } catch (e) { reject(e); } }, ), - [currentStep, registerUsernameAccount, siweServerCall, dispatch], + [ + currentStep, + keyserverRegisterUsernameAccount, + identityRegisterUsernameAccount, + siweServerCall, + dispatch, + ], ); // STEP 2: SETTING AVATAR const uploadSelectedMedia = useUploadSelectedMedia(); const nativeSetUserAvatar = useNativeSetUserAvatar(); const hasCurrentUserInfo = useSelector( state => !!state.currentUserInfo && !state.currentUserInfo.anonymous, ); const avatarBeingSetRef = React.useRef(false); React.useEffect(() => { if ( !hasCurrentUserInfo || currentStep.step !== 'waiting_for_registration_call' || avatarBeingSetRef.current ) { return; } avatarBeingSetRef.current = true; const { avatarData, resolve } = 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: setDataLoadedActionType, payload: { dataLoaded: true, }, }); setCurrentStep(inactiveStep); avatarBeingSetRef.current = false; resolve(); } })(); }, [ currentStep, hasCurrentUserInfo, uploadSelectedMedia, nativeSetUserAvatar, dispatch, ]); return returnedFunc; } export { useRegistrationServerCall }; diff --git a/native/identity-service/identity-service-context-provider.react.js b/native/identity-service/identity-service-context-provider.react.js index 249de9267..2eff6650d 100644 --- a/native/identity-service/identity-service-context-provider.react.js +++ b/native/identity-service/identity-service-context-provider.react.js @@ -1,107 +1,137 @@ // @flow import * as React from 'react'; +import { getOneTimeKeyArray } from 'lib/shared/crypto-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import type { IdentityServiceClient, OutboundKeyInfoResponse, UserLoginResponse, } from 'lib/types/identity-service-types.js'; +import { ONE_TIME_KEYS_NUMBER } 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; }, + registerUser: async (username: string, password: string) => { + await commCoreModule.initializeCryptoAccount(); + const [ + { blobPayload, signature }, + notificationsOneTimeKeys, + primaryOneTimeKeys, + prekeys, + ] = await Promise.all([ + commCoreModule.getUserPublicKey(), + commCoreModule.getNotificationsOneTimeKeys(ONE_TIME_KEYS_NUMBER), + commCoreModule.getPrimaryOneTimeKeys(ONE_TIME_KEYS_NUMBER), + commCoreModule.generateAndGetPrekeys(), + ]); + const registrationResult = await commRustModule.registerUser( + username, + password, + blobPayload, + signature, + prekeys.contentPrekey, + prekeys.contentPrekeySignature, + prekeys.notifPrekey, + prekeys.notifPrekeySignature, + getOneTimeKeyArray(primaryOneTimeKeys), + getOneTimeKeyArray(notificationsOneTimeKeys), + ); + const { userID, accessToken } = JSON.parse(registrationResult); + return { accessToken, userID, username }; + }, }; }, [getAuthMetadata]); const value = React.useMemo( () => ({ identityClient: client, }), [client], ); return ( {children} ); } export default IdentityServiceContextProvider; diff --git a/native/utils/alert-messages.js b/native/utils/alert-messages.js index 655f42e96..c78484557 100644 --- a/native/utils/alert-messages.js +++ b/native/utils/alert-messages.js @@ -1,20 +1,32 @@ // @flow import { Platform } from 'react-native'; type AlertDetails = { +title: string, +message: string, }; const platformStore: string = Platform.select({ ios: 'App Store', android: 'Play Store', }); export const AppOutOfDateAlertDetails: AlertDetails = { title: 'App out of date', message: 'Your app version is pretty old, and the server doesn’t know how ' + `to speak to it anymore. Please use the ${platformStore} to update!`, }; + +export const UsernameReservedAlertDetails: AlertDetails = { + title: 'Username reserved', + message: + 'This username is currently reserved. Please contact support@' + + 'comm.app if you would like to claim this account.', +}; + +export const UsernameTakenAlertDetails: AlertDetails = { + title: 'Username taken', + message: 'An account with that username already exists', +};