diff --git a/__tests__/__snapshots__/index.js.snap b/__tests__/__snapshots__/index.js.snap
index 283e8d01..351b4e3c 100644
--- a/__tests__/__snapshots__/index.js.snap
+++ b/__tests__/__snapshots__/index.js.snap
@@ -174,7 +174,9 @@ Object {
"updatePasswordDone": [Function],
"updatePasswordInit": [Function],
"updateProfileDone": [Function],
+ "updateProfileDoneV5": [Function],
"updateProfileInit": [Function],
+ "updateProfileInitV5": [Function],
"uploadPhotoDone": [Function],
"uploadPhotoInit": [Function],
"verifyMemberNewEmailDone": [Function],
diff --git a/__tests__/actions/__snapshots__/profile.js.snap b/__tests__/actions/__snapshots__/profile.js.snap
index b784f238..0649581b 100644
--- a/__tests__/actions/__snapshots__/profile.js.snap
+++ b/__tests__/actions/__snapshots__/profile.js.snap
@@ -44,7 +44,9 @@ Object {
"updatePasswordDone": [Function],
"updatePasswordInit": [Function],
"updateProfileDone": [Function],
+ "updateProfileDoneV5": [Function],
"updateProfileInit": [Function],
+ "updateProfileInitV5": [Function],
"uploadPhotoDone": [Function],
"uploadPhotoInit": [Function],
"verifyMemberNewEmailDone": [Function],
diff --git a/__tests__/actions/auth.js b/__tests__/actions/auth.js
index a36ef7e3..19cc26d5 100644
--- a/__tests__/actions/auth.js
+++ b/__tests__/actions/auth.js
@@ -1,5 +1,5 @@
const MOCK_GROUPS_REQ_URL = 'https://api.topcoder-dev.com/v5/groups?memberId=12345&membershipType=user';
-const MOCK_PROFILE_REQ_URL = 'https://api.topcoder-dev.com/v3/members/username12345';
+const MOCK_PROFILE_REQ_URL = 'https://api.topcoder-dev.com/v5/members/username12345';
jest.mock('isomorphic-fetch', () => jest.fn(url => Promise.resolve({
ok: true,
@@ -10,7 +10,7 @@ jest.mock('isomorphic-fetch', () => jest.fn(url => Promise.resolve({
content = ['Group1', 'Group2'];
break;
case MOCK_PROFILE_REQ_URL:
- content = { result: { content: { userId: 12345 }, status: 200 } };
+ content = Promise.resolve({ userId: 12345 });
break;
default: throw new Error('Unexpected URL!');
}
diff --git a/__tests__/actions/profile.js b/__tests__/actions/profile.js
index b4d1848b..4ea366a3 100644
--- a/__tests__/actions/profile.js
+++ b/__tests__/actions/profile.js
@@ -18,8 +18,6 @@ const linkedAccounts = [{
// Mock services
const mockMembersService = {
- getPresignedUrl: jest.fn().mockReturnValue(Promise.resolve()),
- uploadFileToS3: jest.fn().mockReturnValue(Promise.resolve()),
updateMemberPhoto: jest.fn().mockReturnValue(Promise.resolve('url-of-photo')),
updateMemberProfile: jest.fn().mockReturnValue(Promise.resolve(profile)),
addSkill: jest.fn().mockReturnValue(Promise.resolve({ skills: [skill] })),
@@ -47,8 +45,6 @@ test('Module exports', () => expect(actions).toMatchSnapshot());
test('profile.uploadPhotoDone', async () => {
const actionResult = await redux.resolveAction(actions.profile.uploadPhotoDone(handle, tokenV3));
expect(actionResult).toMatchSnapshot();
- expect(mockMembersService.getPresignedUrl).toBeCalled();
- expect(mockMembersService.uploadFileToS3).toBeCalled();
expect(mockMembersService.updateMemberPhoto).toBeCalled();
});
diff --git a/docs/services.members.md b/docs/services.members.md
index 856b90ce..8fe477f2 100644
--- a/docs/services.members.md
+++ b/docs/services.members.md
@@ -25,9 +25,7 @@ members via API V3.
* [.addSkill(handle, skillTagId)](#module_services.members..MembersService+addSkill) ⇒ Promise
* [.hideSkill(handle, skillTagId)](#module_services.members..MembersService+hideSkill) ⇒ Promise
* [.updateMemberProfile(profile)](#module_services.members..MembersService+updateMemberProfile) ⇒ Promise
- * [.getPresignedUrl(userHandle, file)](#module_services.members..MembersService+getPresignedUrl) ⇒ Promise
- * [.updateMemberPhoto(S3Response)](#module_services.members..MembersService+updateMemberPhoto) ⇒ Promise
- * [.uploadFileToS3(presignedUrlResponse)](#module_services.members..MembersService+uploadFileToS3) ⇒ Promise
+ * [.updateMemberPhoto(userHandle, file)](#module_services.members..MembersService+updateMemberPhoto) ⇒ Promise
* [.verifyMemberNewEmail(handle, emailVerifyToken)](#module_services.members..MembersService+verifyMemberNewEmail) ⇒ Promise
@@ -65,9 +63,7 @@ Service class.
* [.addSkill(handle, skillTagId)](#module_services.members..MembersService+addSkill) ⇒ Promise
* [.hideSkill(handle, skillTagId)](#module_services.members..MembersService+hideSkill) ⇒ Promise
* [.updateMemberProfile(profile)](#module_services.members..MembersService+updateMemberProfile) ⇒ Promise
- * [.getPresignedUrl(userHandle, file)](#module_services.members..MembersService+getPresignedUrl) ⇒ Promise
- * [.updateMemberPhoto(S3Response)](#module_services.members..MembersService+updateMemberPhoto) ⇒ Promise
- * [.uploadFileToS3(presignedUrlResponse)](#module_services.members..MembersService+uploadFileToS3) ⇒ Promise
+ * [.updateMemberPhoto(userHandle, file)](#module_services.members..MembersService+updateMemberPhoto) ⇒ Promise
* [.verifyMemberNewEmail(handle, emailVerifyToken)](#module_services.members..MembersService+verifyMemberNewEmail) ⇒ Promise
@@ -256,42 +252,18 @@ Updates member profile.
| --- | --- | --- |
| profile | Object
| The profile to update. |
-
-
-#### membersService.getPresignedUrl(userHandle, file) ⇒ Promise
-Gets presigned url for member photo file.
-
-**Kind**: instance method of [MembersService
](#module_services.members..MembersService)
-**Returns**: Promise
- Resolves to the api response content
-
-| Param | Type | Description |
-| --- | --- | --- |
-| userHandle | String
| The user handle |
-| file | File
| The file to get its presigned url |
-
-#### membersService.updateMemberPhoto(S3Response) ⇒ Promise
-Updates member photo.
+#### membersService.updateMemberPhoto(userHandle, file) ⇒ Promise
+Uploads and updates member photo.
**Kind**: instance method of [MembersService
](#module_services.members..MembersService)
**Returns**: Promise
- Resolves to the api response content
| Param | Type | Description |
| --- | --- | --- |
-| S3Response | Object
| The response from uploadFileToS3() function. |
-
-
-
-#### membersService.uploadFileToS3(presignedUrlResponse) ⇒ Promise
-Uploads file to S3.
-
-**Kind**: instance method of [MembersService
](#module_services.members..MembersService)
-**Returns**: Promise
- Resolves to the api response content
-
-| Param | Type | Description |
-| --- | --- | --- |
-| presignedUrlResponse | Object
| The presigned url response from getPresignedUrl() function. |
+| userHandle | String
| The user handle |
+| file | File
| The file to be uploaded |
diff --git a/package.json b/package.json
index 442f898e..30e2676e 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
"lint:js": "./node_modules/.bin/eslint --ext .js,.jsx .",
"test": "npm run lint && npm run jest"
},
- "version": "1.2.4",
+ "version": "1.2.5",
"dependencies": {
"auth0-js": "^6.8.4",
"config": "^3.2.0",
diff --git a/src/actions/auth.js b/src/actions/auth.js
index 3cec762a..c8a760b0 100644
--- a/src/actions/auth.js
+++ b/src/actions/auth.js
@@ -5,9 +5,10 @@
import { createActions } from 'redux-actions';
import { decodeToken } from '@topcoder-platform/tc-auth-lib';
-import { getApiV3, getApiV5 } from '../services/api';
+import { getApiV5 } from '../services/api';
import { setErrorIcon, ERROR_ICON_TYPES } from '../utils/errors';
import { getService } from '../services/groups';
+import { handleApiResponse } from '../utils/tc';
/**
* Helper method that checks for HTTP error response v5 and throws Error in this case.
@@ -41,11 +42,10 @@ async function checkErrorV5(res) {
function loadProfileDone(userTokenV3) {
if (!userTokenV3) return Promise.resolve(null);
const user = decodeToken(userTokenV3);
- const apiV3 = getApiV3(userTokenV3);
const apiV5 = getApiV5(userTokenV3);
return Promise.all([
- apiV3.get(`/members/${user.handle}`)
- .then(res => res.json()).then(res => (res.result.status === 200 ? res.result.content : {})),
+ apiV5.get(`/members/${user.handle}`)
+ .then(handleApiResponse),
apiV5.get(`/groups?memberId=${user.userId}&membershipType=user`)
.then(checkErrorV5).then(res => res.result || []),
]).then(([profile, groups]) => ({ ...profile, groups }));
diff --git a/src/actions/profile.js b/src/actions/profile.js
index bcd668cf..e9368d0a 100644
--- a/src/actions/profile.js
+++ b/src/actions/profile.js
@@ -216,9 +216,7 @@ function uploadPhotoInit() {}
*/
function uploadPhotoDone(handle, tokenV3, file) {
const service = getMembersService(tokenV3);
- return service.getPresignedUrl(handle, file)
- .then(res => service.uploadFileToS3(res))
- .then(res => service.updateMemberPhoto(res))
+ return service.updateMemberPhoto(handle, file)
.then(photoURL => ({ handle, photoURL }));
}
@@ -248,6 +246,25 @@ function updateProfileDone(profile, tokenV3) {
return service.updateMemberProfile(profile);
}
+/**
+ * @static
+ * @desc Creates an action that signals beginning of updating user's profile.
+ * @return {Action}
+ */
+function updateProfileInitV5() {}
+
+/**
+ * @static
+ * @desc Creates an action that updates user's profile.
+ * @param {String} profile Topcoder user profile.
+ * @param {String} tokenV5 Topcoder auth token v5.
+ * @return {Action}
+ */
+function updateProfileDoneV5(profile, handle, tokenV3) {
+ const service = getMembersService(tokenV3);
+ return service.updateMemberProfileV5(profile, handle);
+}
+
/**
* @static
* @desc Creates an action that signals beginning of adding user's skill.
@@ -485,6 +502,8 @@ export default createActions({
DELETE_PHOTO_DONE: updateProfileDone,
UPDATE_PROFILE_INIT: updateProfileInit,
UPDATE_PROFILE_DONE: updateProfileDone,
+ UPDATE_PROFILE_INIT_V5: updateProfileInitV5,
+ UPDATE_PROFILE_DONE_V5: updateProfileDoneV5,
ADD_SKILL_INIT: addSkillInit,
ADD_SKILL_DONE: addSkillDone,
HIDE_SKILL_INIT: hideSkillInit,
diff --git a/src/reducers/auth.js b/src/reducers/auth.js
index b20e7851..a957d6d8 100644
--- a/src/reducers/auth.js
+++ b/src/reducers/auth.js
@@ -106,6 +106,21 @@ function create(initialState) {
},
};
},
+ [profileActions.profile.updateProfileDoneV5]: (state, { payload, error }) => {
+ if (error) {
+ return state;
+ }
+ if (!state.profile || state.profile.handle !== payload.handle) {
+ return state;
+ }
+ return {
+ ...state,
+ profile: {
+ ...state.profile,
+ ...payload,
+ },
+ };
+ },
}, _.defaults(initialState, {
authenticating: true,
profile: null,
diff --git a/src/reducers/profile.js b/src/reducers/profile.js
index 8ca03fdf..e060f7d4 100644
--- a/src/reducers/profile.js
+++ b/src/reducers/profile.js
@@ -266,6 +266,49 @@ function onUpdateProfileDone(state, { payload, error }) {
};
}
+/**
+ * Handles PROFILE/UPDATE_PROFILE_DONE_V5 action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onUpdateProfileDoneV5(state, { payload, error }) {
+ const newState = { ...state, updatingProfile: false };
+
+ if (payload.isEmailConflict) {
+ return {
+ ...newState,
+ isEmailConflict: true,
+ updateProfileSuccess: false,
+ };
+ }
+
+ if (error) {
+ logger.error('Failed to update user profile', payload);
+ fireErrorMessage('ERROR: Failed to update user profile!');
+ return {
+ ...newState,
+ updateProfileSuccess: false,
+ };
+ }
+
+ if (!newState.info || newState.info.handle !== payload.handle) {
+ return {
+ ...newState,
+ updateProfileSuccess: true,
+ };
+ }
+
+ return {
+ ...newState,
+ info: {
+ ...newState.info,
+ ...payload,
+ },
+ updateProfileSuccess: true,
+ };
+}
+
/**
* Handles PROFILE/ADD_SKILL_DONE action.
* @param {Object} state
@@ -530,6 +573,8 @@ function create(initialState) {
[a.deletePhotoDone]: onDeletePhotoDone,
[a.updateProfileInit]: state => ({ ...state, updatingProfile: true }),
[a.updateProfileDone]: onUpdateProfileDone,
+ [a.updateProfileInitV5]: state => ({ ...state, updatingProfile: true }),
+ [a.updateProfileDoneV5]: onUpdateProfileDoneV5,
[a.addSkillInit]: state => ({ ...state, addingSkill: true }),
[a.addSkillDone]: onAddSkillDone,
[a.hideSkillInit]: state => ({ ...state, hidingSkill: true }),
diff --git a/src/services/members.js b/src/services/members.js
index 7b6648b0..7779d6b3 100644
--- a/src/services/members.js
+++ b/src/services/members.js
@@ -4,11 +4,10 @@
* members via API V3.
*/
-/* global XMLHttpRequest */
+/* global FormData */
import _ from 'lodash';
import qs from 'qs';
import { decodeToken } from '@topcoder-platform/tc-auth-lib';
-import logger from '../utils/logger';
import { getApiResponsePayload, handleApiResponse } from '../utils/tc';
import { getApi } from './api';
@@ -46,8 +45,8 @@ class MembersService {
* @return {Promise} Resolves to the data object.
*/
async getMemberInfo(handle) {
- const res = await this.private.api.get(`/members/${handle}`);
- return getApiResponsePayload(res);
+ const res = await this.private.apiV5.get(`/members/${handle}`);
+ return handleApiResponse(res);
}
/**
@@ -76,8 +75,8 @@ class MembersService {
* @return {Promise} Resolves to the stats object.
*/
async getSkills(handle) {
- const res = await this.private.api.get(`/members/${handle}/skills`);
- return getApiResponsePayload(res);
+ const res = await this.private.apiV5.get(`/members/${handle}/skills`);
+ return handleApiResponse(res);
}
/**
@@ -188,17 +187,23 @@ class MembersService {
* @return {Promise} Resolves to operation result
*/
async addSkill(handle, skillTagId) {
+ let res = {};
+ const url = `/members/${handle}/skills`;
+ const skills = await this.getSkills(handle);
+
const body = {
- param: {
- skills: {
- [skillTagId]: {
- hidden: false,
- },
- },
+ [skillTagId]: {
+ hidden: false,
},
};
- const res = await this.private.api.patchJson(`/members/${handle}/skills`, body);
- return getApiResponsePayload(res);
+
+ if (skills && skills.createdAt) {
+ res = await this.private.apiV5.patchJson(url, body);
+ } else {
+ res = await this.private.apiV5.postJson(url, body);
+ }
+
+ return handleApiResponse(res);
}
/**
@@ -209,19 +214,15 @@ class MembersService {
*/
async hideSkill(handle, skillTagId) {
const body = {
- param: {
- skills: {
- [skillTagId]: {
- hidden: true,
- },
- },
+ [skillTagId]: {
+ hidden: true,
},
};
- const res = await this.private.api.fetch(`/members/${handle}/skills`, {
+ const res = await this.private.apiV5.fetch(`/members/${handle}/skills`, {
body: JSON.stringify(body),
method: 'PATCH',
});
- return getApiResponsePayload(res);
+ return handleApiResponse(res);
}
/**
@@ -239,72 +240,37 @@ class MembersService {
}
/**
- * Gets presigned url for member photo file.
- * @param {String} userHandle The user handle
- * @param {File} file The file to get its presigned url
+ * Updates member profile.
+ * @param {Object} profile The profile to update.
* @return {Promise} Resolves to the api response content
*/
- async getPresignedUrl(userHandle, file) {
- const res = await this.private.api.postJson(`/members/${userHandle}/photoUploadUrl`, { param: { contentType: file.type } });
- const payload = await getApiResponsePayload(res);
-
- return {
- preSignedURL: payload.preSignedURL,
- token: payload.token,
- file,
- userHandle,
- };
+ async updateMemberProfileV5(profile, handle) {
+ const url = profile.verifyUrl ? `/members/${handle}?verifyUrl=${profile.verifyUrl}` : `/members/${handle}`;
+ const res = await this.private.apiV5.putJson(url, profile.verifyUrl ? _.omit(profile, ['verifyUrl']) : profile);
+ if (profile.verifyUrl && res.status === 409) {
+ return Promise.resolve(Object.assign({}, profile, { isEmailConflict: true }));
+ }
+ return handleApiResponse(res);
}
/**
* Updates member photo.
- * @param {Object} S3Response The response from uploadFileToS3() function.
- * @return {Promise} Resolves to the api response content
- */
- async updateMemberPhoto(S3Response) {
- const res = await this.private.api.putJson(`/members/${S3Response.userHandle}/photo`, { param: S3Response.body });
- return getApiResponsePayload(res);
- }
-
- /**
- * Uploads file to S3.
- * @param {Object} presignedUrlResponse The presigned url response from
- * getPresignedUrl() function.
+ * @param {String} userHandle The user handle
+ * @param {File} file The photo to upload
* @return {Promise} Resolves to the api response content
*/
- uploadFileToS3(presignedUrlResponse) {
- _.noop(this);
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
-
- xhr.open('PUT', presignedUrlResponse.preSignedURL, true);
- xhr.setRequestHeader('Content-Type', presignedUrlResponse.file.type);
-
- xhr.onreadystatechange = () => {
- const { status } = xhr;
- if (((status >= 200 && status < 300) || status === 304) && xhr.readyState === 4) {
- resolve({
- userHandle: presignedUrlResponse.userHandle,
- body: {
- token: presignedUrlResponse.token,
- contentType: presignedUrlResponse.file.type,
- },
- });
- } else if (status >= 400) {
- const err = new Error('Could not upload image to S3');
- err.status = status;
- reject(err);
- }
- };
-
- xhr.onerror = (err) => {
- logger.error('Could not upload image to S3', err);
-
- reject(err);
- };
-
- xhr.send(presignedUrlResponse.file);
+ async updateMemberPhoto(userHandle, file) {
+ const formData = new FormData();
+ formData.append('photo', file);
+ const res = await this.private.apiV5.fetch(`/members/${userHandle}/photo`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': null,
+ },
+ body: formData,
});
+ return handleApiResponse(res)
+ .then(({ photoURL }) => photoURL);
}
/**
diff --git a/src/services/user-traits.js b/src/services/user-traits.js
index c1d71b71..34d25a26 100644
--- a/src/services/user-traits.js
+++ b/src/services/user-traits.js
@@ -4,7 +4,7 @@
* via API V3.
*/
import toCapitalCase from 'to-capital-case';
-import { getApiResponsePayload } from '../utils/tc';
+import { handleApiResponse } from '../utils/tc';
import { getApi } from './api';
/**
@@ -16,7 +16,7 @@ class UserTraitsService {
*/
constructor(tokenV3) {
this.private = {
- api: getApi('V3', tokenV3),
+ api: getApi('V5', tokenV3),
tokenV3,
};
}
@@ -29,7 +29,7 @@ class UserTraitsService {
async getAllUserTraits(handle) {
// FIXME: Remove the .toLowerCase() when the API is fixed to ignore the case in the route params
const res = await this.private.api.get(`/members/${handle.toLowerCase()}/traits`);
- return getApiResponsePayload(res);
+ return handleApiResponse(res);
}
/**
@@ -40,18 +40,16 @@ class UserTraitsService {
* @return {Promise} Resolves to the member traits.
*/
async addUserTrait(handle, traitId, data) {
- const body = {
- param: [{
- traitId,
- categoryName: toCapitalCase(traitId),
- traits: {
- data,
- },
- }],
- };
+ const body = [{
+ traitId,
+ categoryName: toCapitalCase(traitId),
+ traits: {
+ data,
+ },
+ }];
const res = await this.private.api.postJson(`/members/${handle}/traits`, body);
- return getApiResponsePayload(res);
+ return handleApiResponse(res);
}
/**
@@ -62,18 +60,16 @@ class UserTraitsService {
* @return {Promise} Resolves to the member traits.
*/
async updateUserTrait(handle, traitId, data) {
- const body = {
- param: [{
- traitId,
- categoryName: toCapitalCase(traitId),
- traits: {
- data,
- },
- }],
- };
+ const body = [{
+ traitId,
+ categoryName: toCapitalCase(traitId),
+ traits: {
+ data,
+ },
+ }];
const res = await this.private.api.putJson(`/members/${handle}/traits`, body);
- return getApiResponsePayload(res);
+ return handleApiResponse(res);
}
/**
@@ -84,7 +80,7 @@ class UserTraitsService {
*/
async deleteUserTrait(handle, traitId) {
const res = await this.private.api.delete(`/members/${handle}/traits?traitIds=${traitId}`);
- return getApiResponsePayload(res);
+ return handleApiResponse(res);
}
}
diff --git a/src/utils/tc.js b/src/utils/tc.js
index 2941de37..ff6d46f1 100644
--- a/src/utils/tc.js
+++ b/src/utils/tc.js
@@ -87,7 +87,8 @@ export async function getApiResponsePayload(res, shouldThrowError = true) {
*/
export function handleApiResponse(response) {
if (!response.ok) throw new Error(response.statusText);
- return response.json();
+ return response.json()
+ .catch(() => null);
}
/**