diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -38,7 +38,10 @@ reportMultiCreationResponder, errorReportFetchInfosResponder, } from './responders/report-responders.js'; -import { userSearchResponder } from './responders/search-responders.js'; +import { + userSearchResponder, + exactUserSearchResponder, +} from './responders/search-responders.js'; import { siweNonceResponder } from './responders/siwe-nonce-responders.js'; import { threadDeletionResponder, @@ -133,6 +136,10 @@ responder: uploadDeletionResponder, requiredPolicies: baseLegalPolicies, }, + exact_search_user: { + responder: exactUserSearchResponder, + requiredPolicies: [], + }, fetch_entries: { responder: entryFetchResponder, requiredPolicies: baseLegalPolicies, diff --git a/keyserver/src/responders/search-responders.js b/keyserver/src/responders/search-responders.js --- a/keyserver/src/responders/search-responders.js +++ b/keyserver/src/responders/search-responders.js @@ -5,11 +5,13 @@ import type { UserSearchRequest, UserSearchResult, + ExactUserSearchRequest, + ExactUserSearchResult, } from 'lib/types/search-types.js'; import { globalAccountUserInfoValidator } from 'lib/types/user-types.js'; import { tShape } from 'lib/utils/validation-utils.js'; -import { searchForUsers } from '../search/users.js'; +import { searchForUsers, searchForUser } from '../search/users.js'; import type { Viewer } from '../session/viewer.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; @@ -40,4 +42,30 @@ ); } -export { userSearchResponder }; +const exactUserSearchRequestInputValidator = tShape({ + username: t.String, +}); + +const exactUserSearchResultValidator = tShape({ + userInfo: t.maybe(globalAccountUserInfoValidator), +}); + +async function exactUserSearchResponder( + viewer: Viewer, + input: mixed, +): Promise { + const request = await validateInput( + viewer, + exactUserSearchRequestInputValidator, + input, + ); + const searchResult = await searchForUser(request.username); + const result = { userInfo: searchResult }; + return validateOutput( + viewer.platformDetails, + exactUserSearchResultValidator, + result, + ); +} + +export { userSearchResponder, exactUserSearchResponder }; diff --git a/keyserver/src/search/users.js b/keyserver/src/search/users.js --- a/keyserver/src/search/users.js +++ b/keyserver/src/search/users.js @@ -27,4 +27,21 @@ return userInfos; } -export { searchForUsers }; +async function searchForUser( + usernameQuery: string, +): Promise { + const query = SQL` + SELECT id, username + FROM users + WHERE LOWER(username) = LOWER(${usernameQuery}) + `; + const [result] = await dbQuery(query); + + if (result.length === 0) { + return null; + } + const { id, username } = result[0]; + return { id, username }; +} + +export { searchForUsers, searchForUser }; diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -15,7 +15,10 @@ UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { GetSessionPublicKeysArgs } from '../types/request-types.js'; -import type { UserSearchResult } from '../types/search-types.js'; +import type { + UserSearchResult, + ExactUserSearchResult, +} from '../types/search-types.js'; import type { SessionPublicKeys, PreRequestUserState, @@ -191,6 +194,24 @@ }; }; +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', @@ -282,6 +303,8 @@ registerActionTypes, searchUsers, searchUsersActionTypes, + exactSearchUser, + exactSearchUserActionTypes, setUserSettings, setUserSettingsActionTypes, updateSubscription, diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -58,6 +58,7 @@ DELETE_ENTRY: 'delete_entry', DELETE_THREAD: 'delete_thread', DELETE_UPLOAD: 'delete_upload', + EXACT_SEARCH_USER: 'exact_search_user', FETCH_ENTRIES: 'fetch_entries', FETCH_ENTRY_REVISIONS: 'fetch_entry_revisions', FETCH_ERROR_REPORT_INFOS: 'fetch_error_report_infos', diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -74,7 +74,10 @@ ReportStore, } from './report-types.js'; import type { ProcessServerRequestsPayload } from './request-types.js'; -import type { UserSearchResult } from './search-types.js'; +import type { + UserSearchResult, + ExactUserSearchResult, +} from './search-types.js'; import type { SetSessionPayload } from './session-types.js'; import type { ConnectionInfo, @@ -583,6 +586,22 @@ +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: { diff --git a/lib/types/search-types.js b/lib/types/search-types.js --- a/lib/types/search-types.js +++ b/lib/types/search-types.js @@ -3,8 +3,15 @@ import type { GlobalAccountUserInfo } from './user-types.js'; export type UserSearchRequest = { - prefix?: string, + +prefix?: string, }; export type UserSearchResult = { - userInfos: $ReadOnlyArray, + +userInfos: $ReadOnlyArray, +}; + +export type ExactUserSearchRequest = { + +username: string, +}; +export type ExactUserSearchResult = { + +userInfo: ?GlobalAccountUserInfo, }; diff --git a/native/account/registration/connect-ethereum.react.js b/native/account/registration/connect-ethereum.react.js --- a/native/account/registration/connect-ethereum.react.js +++ b/native/account/registration/connect-ethereum.react.js @@ -3,6 +3,16 @@ import * as React from 'react'; import { Text, View } from 'react-native'; +import { + exactSearchUser, + exactSearchUserActionTypes, +} from 'lib/actions/user-actions.js'; +import type { SIWEResult } from 'lib/types/siwe-types.js'; +import { + useServerCall, + useDispatchActionPromise, +} from 'lib/utils/action-utils.js'; + import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationButton from './registration-button.react.js'; import RegistrationContainer from './registration-container.react.js'; @@ -94,8 +104,27 @@ [panelState], ); - const onSkip = React.useCallback(() => {}, []); - const onSuccessfulWalletSignature = React.useCallback(() => {}, []); + const onSkip = React.useCallback(() => { + // show username selection screen + }, []); + + const exactSearchUserCall = useServerCall(exactSearchUser); + const dispatchActionPromise = useDispatchActionPromise(); + + const onSuccessfulWalletSignature = React.useCallback( + async (result: SIWEResult) => { + const searchPromise = exactSearchUserCall(result.address); + dispatchActionPromise(exactSearchUserActionTypes, searchPromise); + const { userInfo } = await searchPromise; + + if (userInfo) { + // show duplicate account screen + } else { + // show avatar selection screen + } + }, + [exactSearchUserCall, dispatchActionPromise], + ); let siwePanel; if (panelState !== 'closed') {