diff --git a/.gitignore b/.gitignore
index 2af11b2e..983ad299 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ __coverage__
dist
node_modules
_auto_doc_
+.vscode
\ No newline at end of file
diff --git a/__tests__/__snapshots__/index.js.snap b/__tests__/__snapshots__/index.js.snap
index 67d964da..69a9d410 100644
--- a/__tests__/__snapshots__/index.js.snap
+++ b/__tests__/__snapshots__/index.js.snap
@@ -48,6 +48,9 @@ Object {
"getGroupsDone": [Function],
"getGroupsInit": [Function],
},
+ "lookup": Object {
+ "getApprovedSkills": [Function],
+ },
"memberTasks": Object {
"dropAll": [Function],
"getDone": [Function],
@@ -64,19 +67,49 @@ Object {
"getStatsInit": [Function],
},
"profile": Object {
+ "addSkillDone": [Function],
+ "addSkillInit": [Function],
+ "addWebLinkDone": [Function],
+ "addWebLinkInit": [Function],
+ "deletePhotoDone": [Function],
+ "deletePhotoInit": [Function],
+ "deleteWebLinkDone": [Function],
+ "deleteWebLinkInit": [Function],
"getAchievementsDone": [Function],
"getAchievementsInit": [Function],
+ "getActiveChallengesCountDone": [Function],
+ "getActiveChallengesCountInit": [Function],
+ "getCredentialDone": [Function],
+ "getCredentialInit": [Function],
+ "getEmailPreferencesDone": [Function],
+ "getEmailPreferencesInit": [Function],
"getExternalAccountsDone": [Function],
"getExternalAccountsInit": [Function],
"getExternalLinksDone": [Function],
"getExternalLinksInit": [Function],
"getInfoDone": [Function],
"getInfoInit": [Function],
+ "getLinkedAccountsDone": [Function],
+ "getLinkedAccountsInit": [Function],
"getSkillsDone": [Function],
"getSkillsInit": [Function],
"getStatsDone": [Function],
"getStatsInit": [Function],
+ "hideSkillDone": [Function],
+ "hideSkillInit": [Function],
+ "linkExternalAccountDone": [Function],
+ "linkExternalAccountInit": [Function],
"loadProfile": [Function],
+ "saveEmailPreferencesDone": [Function],
+ "saveEmailPreferencesInit": [Function],
+ "unlinkExternalAccountDone": [Function],
+ "unlinkExternalAccountInit": [Function],
+ "updatePasswordDone": [Function],
+ "updatePasswordInit": [Function],
+ "updateProfileDone": [Function],
+ "updateProfileInit": [Function],
+ "uploadPhotoDone": [Function],
+ "uploadPhotoInit": [Function],
},
"reviewOpportunity": Object {
"cancelApplicationsDone": [Function],
@@ -159,6 +192,21 @@ Object {
"default": undefined,
"mockAction": [Function],
},
+ "reducerFactories": Object {
+ "authFactory": [Function],
+ "challengeFactory": [Function],
+ "directFactory": [Function],
+ "errorsFactory": [Function],
+ "groupsFactory": [Function],
+ "lookupFactory": [Function],
+ "memberTasksFactory": [Function],
+ "membersFactory": [Function],
+ "mySubmissionsManagementFactory": [Function],
+ "profileFactory": [Function],
+ "reviewOpportunityFactory": [Function],
+ "statsFactory": [Function],
+ "termsFactory": [Function],
+ },
"reducerFactory": [Function],
"reducers": Object {
"auth": [Function],
@@ -166,6 +214,7 @@ Object {
"direct": [Function],
"errors": [Function],
"groups": [Function],
+ "lookup": [Function],
"memberTasks": [Function],
"members": [Function],
"mySubmissionsManagement": [Function],
@@ -209,6 +258,10 @@ Object {
"default": undefined,
"getService": [Function],
},
+ "lookup": Object {
+ "default": undefined,
+ "getService": [Function],
+ },
"members": Object {
"default": undefined,
"getService": [Function],
@@ -242,6 +295,7 @@ Object {
"Spec Review": "Specification Review",
},
"getApiResponsePayloadV3": [Function],
+ "looseEqual": [Function],
},
"time": Object {
"default": undefined,
diff --git a/__tests__/actions/lookup.js b/__tests__/actions/lookup.js
new file mode 100644
index 00000000..f33f198f
--- /dev/null
+++ b/__tests__/actions/lookup.js
@@ -0,0 +1,30 @@
+import * as LookupService from 'services/lookup';
+import actions from 'actions/lookup';
+
+const tag = {
+ domain: 'SKILLS',
+ id: 251,
+ name: 'Jekyll',
+ status: 'APPROVED',
+};
+
+// Mock services
+const mockLookupService = {
+ getTags: jest.fn().mockReturnValue(Promise.resolve([tag])),
+};
+LookupService.getService = jest.fn().mockReturnValue(mockLookupService);
+
+
+describe('lookup.getApprovedSkills', () => {
+ const a = actions.lookup.getApprovedSkills();
+
+ test('has expected type', () => {
+ expect(a.type).toEqual('LOOKUP/GET_APPROVED_SKILLS');
+ });
+
+ test('Approved skills should be returned', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual([tag]);
+ expect(mockLookupService.getTags).toBeCalled();
+ }));
+});
diff --git a/__tests__/actions/profile.js b/__tests__/actions/profile.js
new file mode 100644
index 00000000..3efdd708
--- /dev/null
+++ b/__tests__/actions/profile.js
@@ -0,0 +1,252 @@
+import * as ChallengesService from 'services/challenges';
+import * as MembersService from 'services/members';
+import * as UserService from 'services/user';
+
+import actions from 'actions/profile';
+
+const handle = 'tcscoder';
+const tokenV3 = 'tokenV3';
+const profile = { userId: 12345, handle };
+const skill = { tagId: 123, tagName: 'Node.js' };
+const weblink = 'https://www.google.com';
+const linkedAccounts = [{
+ providerType: 'github',
+ social: true,
+ userId: '623633',
+}];
+
+// Mock services
+const mockChanllengesService = {
+ getUserChallenges: jest.fn().mockReturnValue(Promise.resolve({ totalCount: 3 })),
+ getUserMarathonMatches: jest.fn().mockReturnValue(Promise.resolve({ totalCount: 5 })),
+};
+ChallengesService.getService = jest.fn().mockReturnValue(mockChanllengesService);
+
+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] })),
+ hideSkill: jest.fn().mockReturnValue(Promise.resolve({ skills: [] })),
+ addWebLink: jest.fn().mockReturnValue(Promise.resolve(weblink)),
+ deleteWebLink: jest.fn().mockReturnValue(Promise.resolve(weblink)),
+};
+MembersService.getService = jest.fn().mockReturnValue(mockMembersService);
+
+const mockUserService = {
+ linkExternalAccount: jest.fn().mockReturnValue(Promise.resolve(linkedAccounts[0])),
+ unlinkExternalAccount: jest.fn().mockReturnValue(Promise.resolve('unlinked')),
+ getLinkedAccounts: jest.fn().mockReturnValue(Promise.resolve({ profiles: linkedAccounts })),
+ getCredential: jest.fn().mockReturnValue(Promise.resolve({ credential: { hasPassword: true } })),
+ getEmailPreferences:
+ jest.fn().mockReturnValue(Promise.resolve({ subscriptions: { TOPCODER_NL_DATA: true } })),
+ saveEmailPreferences:
+ jest.fn().mockReturnValue(Promise.resolve({ subscriptions: { TOPCODER_NL_DATA: true } })),
+ updatePassword: jest.fn().mockReturnValue(Promise.resolve({ update: true })),
+};
+UserService.getService = jest.fn().mockReturnValue(mockUserService);
+
+
+describe('profile.getActiveChallengesCountDone', () => {
+ const a = actions.profile.getActiveChallengesCountDone(handle, tokenV3);
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/GET_ACTIVE_CHALLENGES_COUNT_DONE');
+ });
+
+ test('Sum of challenges and marathon matches should be returned', () =>
+ a.payload.then((res) => {
+ expect(res).toBe(8);
+ expect(mockChanllengesService.getUserChallenges).toBeCalled();
+ expect(mockChanllengesService.getUserMarathonMatches).toBeCalled();
+ }));
+});
+
+describe('profile.uploadPhotoDone', () => {
+ const a = actions.profile.uploadPhotoDone(handle, tokenV3);
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/UPLOAD_PHOTO_DONE');
+ });
+
+ test('Photo URL should be returned', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({
+ handle,
+ photoURL: 'url-of-photo',
+ });
+ expect(mockMembersService.getPresignedUrl).toBeCalled();
+ expect(mockMembersService.uploadFileToS3).toBeCalled();
+ expect(mockMembersService.updateMemberPhoto).toBeCalled();
+ }));
+});
+
+describe('profile.updateProfileDone', () => {
+ const a = actions.profile.updateProfileDone(profile, tokenV3);
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/UPDATE_PROFILE_DONE');
+ });
+
+ test('Profile should be updated', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual(profile);
+ expect(mockMembersService.updateMemberProfile).toBeCalled();
+ }));
+});
+
+describe('profile.addSkillDone', () => {
+ const a = actions.profile.addSkillDone(handle, tokenV3, skill);
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/ADD_SKILL_DONE');
+ });
+
+ test('Skill should be added', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ skills: [skill], handle, skill });
+ expect(mockMembersService.addSkill).toBeCalled();
+ }));
+});
+
+describe('profile.hideSkillDone', () => {
+ const a = actions.profile.hideSkillDone(handle, tokenV3, skill);
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/HIDE_SKILL_DONE');
+ });
+
+ test('Skill should be removed', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ skills: [], handle, skill });
+ expect(mockMembersService.hideSkill).toBeCalled();
+ }));
+});
+
+describe('profile.addWebLinkDone', () => {
+ const a = actions.profile.addWebLinkDone(handle, tokenV3, weblink);
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/ADD_WEB_LINK_DONE');
+ });
+
+ test('Web link should be added', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ data: weblink, handle });
+ expect(mockMembersService.addWebLink).toBeCalled();
+ }));
+});
+
+describe('profile.deleteWebLinkDone', () => {
+ const a = actions.profile.deleteWebLinkDone(handle, tokenV3, weblink);
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/DELETE_WEB_LINK_DONE');
+ });
+
+ test('Web link should be deleted', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ data: weblink, handle });
+ expect(mockMembersService.deleteWebLink).toBeCalled();
+ }));
+});
+
+describe('profile.linkExternalAccountDone', () => {
+ const a = actions.profile.linkExternalAccountDone(profile, tokenV3, 'github');
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/LINK_EXTERNAL_ACCOUNT_DONE');
+ });
+
+ test('External account should be linked', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ data: linkedAccounts[0], handle });
+ expect(mockUserService.linkExternalAccount).toBeCalled();
+ }));
+});
+
+describe('profile.unlinkExternalAccountDone', () => {
+ const a = actions.profile.unlinkExternalAccountDone(profile, tokenV3, 'github');
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/UNLINK_EXTERNAL_ACCOUNT_DONE');
+ });
+
+ test('External account should be unlinked', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ handle, providerType: 'github' });
+ expect(mockUserService.unlinkExternalAccount).toBeCalled();
+ }));
+});
+
+describe('profile.getLinkedAccountsDone', () => {
+ const a = actions.profile.getLinkedAccountsDone(profile, tokenV3);
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/GET_LINKED_ACCOUNTS_DONE');
+ });
+
+ test('Linked account should be returned', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ profiles: linkedAccounts });
+ expect(mockUserService.getLinkedAccounts).toBeCalled();
+ }));
+});
+
+describe('profile.getCredentialDone', () => {
+ const a = actions.profile.getCredentialDone(profile, tokenV3);
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/GET_CREDENTIAL_DONE');
+ });
+
+ test('Credential should be returned', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ credential: { hasPassword: true } });
+ expect(mockUserService.getCredential).toBeCalled();
+ }));
+});
+
+describe('profile.getEmailPreferencesDone', () => {
+ const a = actions.profile.getEmailPreferencesDone(profile, tokenV3);
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/GET_EMAIL_PREFERENCES_DONE');
+ });
+
+ test('Email preferences should be returned', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ subscriptions: { TOPCODER_NL_DATA: true } });
+ expect(mockUserService.getEmailPreferences).toBeCalled();
+ }));
+});
+
+describe('profile.saveEmailPreferencesDone', () => {
+ const a = actions.profile.saveEmailPreferencesDone(profile, tokenV3, {});
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/SAVE_EMAIL_PREFERENCES_DONE');
+ });
+
+ test('Email preferences should be updated', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ handle, data: { subscriptions: { TOPCODER_NL_DATA: true } } });
+ expect(mockUserService.saveEmailPreferences).toBeCalled();
+ }));
+});
+
+describe('profile.updatePasswordDone', () => {
+ const a = actions.profile.updatePasswordDone(profile, tokenV3, 'newPassword', 'oldPassword');
+
+ test('has expected type', () => {
+ expect(a.type).toBe('PROFILE/UPDATE_PASSWORD_DONE');
+ });
+
+ test('User password should be updated', () =>
+ a.payload.then((res) => {
+ expect(res).toEqual({ handle, data: { update: true } });
+ expect(mockUserService.updatePassword).toBeCalled();
+ }));
+});
+
diff --git a/__tests__/reducers/auth.js b/__tests__/reducers/auth.js
index a86137f7..f89ea226 100644
--- a/__tests__/reducers/auth.js
+++ b/__tests__/reducers/auth.js
@@ -2,6 +2,8 @@ import { mockAction } from 'utils/mock';
import { redux } from 'topcoder-react-utils';
const dummy = 'DUMMY';
+const handle = 'tcscoder';
+const photoURL = 'http://url';
const mockActions = {
auth: {
@@ -9,8 +11,14 @@ const mockActions = {
setTcTokenV2: mockAction('SET_TC_TOKEN_V2', 'Token V2'),
setTcTokenV3: mockAction('SET_TC_TOKEN_V3', 'Token V3'),
},
+ profile: {
+ uploadPhotoDone: mockAction('UPLOAD_PHOTO_DONE', Promise.resolve({ handle, photoURL })),
+ deletePhotoDone: mockAction('DELETE_PHOTO_DONE', Promise.resolve({ handle })),
+ updateProfileDone: mockAction('UPDATE_PROFILE_DONE', Promise.resolve({ handle, photoURL: 'http://newurl' })),
+ },
};
jest.setMock(require.resolve('actions/auth'), mockActions);
+jest.setMock(require.resolve('actions/profile'), mockActions);
jest.setMock('tc-accounts', {
decodeToken: () => 'User object',
@@ -19,7 +27,9 @@ jest.setMock('tc-accounts', {
const reducers = require('reducers/auth');
-function testReducer(reducer, istate) {
+let reducer;
+
+function testReducer(istate) {
test('Initial state', () => {
const state = reducer(undefined, {});
expect(state).toEqual(istate);
@@ -62,10 +72,61 @@ function testReducer(reducer, istate) {
});
mockActions.auth.setTcTokenV3 = mockAction('SET_TC_TOKEN_V3', 'Token V3');
});
+
+ test('Upload photo', () =>
+ redux.resolveAction(mockActions.profile.uploadPhotoDone()).then((action) => {
+ const state = reducer({ profile: { handle } }, action);
+ expect(state).toEqual({
+ profile: {
+ handle,
+ photoURL,
+ },
+ });
+ }));
+
+ test('Delete photo', () =>
+ redux.resolveAction(mockActions.profile.deletePhotoDone()).then((action) => {
+ const state = reducer({ profile: { handle, photoURL } }, action);
+ expect(state).toEqual({
+ profile: {
+ handle,
+ photoURL: null,
+ },
+ });
+ }));
+
+ test('Update profile', () =>
+ redux.resolveAction(mockActions.profile.updateProfileDone()).then((action) => {
+ const state = reducer({ profile: { handle, photoURL } }, action);
+ expect(state).toEqual({
+ profile: {
+ handle,
+ photoURL: 'http://newurl',
+ },
+ });
+ }));
}
describe('Default reducer', () => {
- testReducer(reducers.default, {
+ reducer = reducers.default;
+ testReducer({
+ authenticating: true,
+ profile: null,
+ tokenV2: '',
+ tokenV3: '',
+ user: null,
+ });
+});
+
+describe('Factory without server side rendering', () => {
+ beforeAll((done) => {
+ reducers.factory().then((res) => {
+ reducer = res;
+ done();
+ });
+ });
+
+ testReducer({
authenticating: true,
profile: null,
tokenV2: '',
@@ -74,15 +135,20 @@ describe('Default reducer', () => {
});
});
-describe('Factory without server side rendering', () =>
- reducers.factory().then(res =>
- testReducer(res, {})));
-
-describe('Factory with server side rendering', () =>
- reducers.factory({
- cookies: {
- tcjwt: 'Token V2',
- v3jwt: 'Token V3',
- },
- }).then(res =>
- testReducer(res, {})));
+describe('Factory with server side rendering', () => {
+ beforeAll((done) => {
+ reducers.factory({
+ auth: {
+ tokenV2: 'Token V2',
+ tokenV3: 'Token V3',
+ },
+ }).then((res) => {
+ reducer = res;
+ done();
+ });
+ });
+
+ testReducer({
+ authenticating: false, user: 'User object', profile: 'Profile', tokenV2: 'Token V2', tokenV3: 'Token V3',
+ });
+});
diff --git a/__tests__/reducers/lookup.js b/__tests__/reducers/lookup.js
new file mode 100644
index 00000000..6e30d571
--- /dev/null
+++ b/__tests__/reducers/lookup.js
@@ -0,0 +1,59 @@
+import { mockAction } from 'utils/mock';
+import { redux } from 'topcoder-react-utils';
+
+const tag = {
+ domain: 'SKILLS',
+ id: 251,
+ name: 'Jekyll',
+ status: 'APPROVED',
+};
+
+const mockActions = {
+ lookup: {
+ getApprovedSkills: mockAction('LOOKUP/GET_APPROVED_SKILLS', Promise.resolve([tag])),
+ getApprovedSkillsError: mockAction('LOOKUP/GET_APPROVED_SKILLS', null, 'Unknown error'),
+ },
+};
+jest.setMock(require.resolve('actions/lookup'), mockActions);
+
+const reducers = require('reducers/lookup');
+
+let reducer;
+
+function testReducer(istate) {
+ test('Initial state', () => {
+ const state = reducer(undefined, {});
+ expect(state).toEqual(istate);
+ });
+
+ test('Load approved skills', () =>
+ redux.resolveAction(mockActions.lookup.getApprovedSkills()).then((action) => {
+ const state = reducer({}, action);
+ expect(state).toEqual({
+ approvedSkills: [tag],
+ loadingApprovedSkillsError: false,
+ });
+ }));
+
+ test('Load approved skills error', () => {
+ const state = reducer({}, mockActions.lookup.getApprovedSkillsError());
+ expect(state).toEqual({
+ loadingApprovedSkillsError: true,
+ });
+ });
+}
+
+describe('Default reducer', () => {
+ reducer = reducers.default;
+ testReducer({ approvedSkills: [] });
+});
+
+describe('Factory without server side rendering', () => {
+ beforeAll((done) => {
+ reducers.factory().then((res) => {
+ reducer = res;
+ done();
+ });
+ });
+ testReducer({ approvedSkills: [] });
+});
diff --git a/__tests__/reducers/profile.js b/__tests__/reducers/profile.js
new file mode 100644
index 00000000..f805ed11
--- /dev/null
+++ b/__tests__/reducers/profile.js
@@ -0,0 +1,274 @@
+import { mockAction } from 'utils/mock';
+
+const handle = 'tcscoder';
+const photoURL = 'http://url';
+const skill = { tagId: 123, tagName: 'Node.js' };
+const externalLink = { providerType: 'weblink', key: '1111', URL: 'http://www.github.com' };
+const webLink = { providerType: 'weblink', key: '2222', URL: 'http://www.google.com' };
+const linkedAccount = { providerType: 'github', social: true, userId: '623633' };
+const linkedAccount2 = { providerType: 'stackoverlow', social: true, userId: '343523' };
+
+const mockActions = {
+ profile: {
+ loadProfile: mockAction('LOAD_PROFILE', handle),
+ getInfoDone: mockAction('GET_INFO_DONE', { handle }),
+ getExternalLinksDone: mockAction('GET_EXTERNAL_LINKS_DONE', [externalLink]),
+ getActiveChallengesCountDone: mockAction('GET_ACTIVE_CHALLENGES_COUNT_DONE', 5),
+ getLinkedAccountsDone: mockAction('GET_LINKED_ACCOUNTS_DONE', { profiles: [linkedAccount] }),
+ getCredentialDone: mockAction('GET_CREDENTIAL_DONE', { credential: { hasPassword: true } }),
+ getEmailPreferencesDone: mockAction('GET_EMAIL_PREFERENCES_DONE', { subscriptions: { TOPCODER_NL_DATA: true } }),
+ uploadPhotoInit: mockAction('UPLOAD_PHOTO_INIT'),
+ uploadPhotoDone: mockAction('UPLOAD_PHOTO_DONE', { handle, photoURL }),
+ deletePhotoInit: mockAction('DELETE_PHOTO_INIT'),
+ deletePhotoDone: mockAction('DELETE_PHOTO_DONE', { handle }),
+ updatePasswordInit: mockAction('UPDATE_PASSWORD_INIT'),
+ updatePasswordDone: mockAction('UPDATE_PASSWORD_DONE'),
+ updateProfileInit: mockAction('UPDATE_PROFILE_INIT'),
+ updateProfileDone: mockAction('UPDATE_PROFILE_DONE', { handle, description: 'bio desc' }),
+ addSkillInit: mockAction('ADD_SKILL_INIT'),
+ addSkillDone: mockAction('ADD_SKILL_DONE', { handle, skills: [skill] }),
+ hideSkillInit: mockAction('HIDE_SKILL_INIT'),
+ hideSkillDone: mockAction('HIDE_SKILL_DONE', { handle, skills: [] }),
+ addWebLinkInit: mockAction('ADD_WEB_LINK_INIT'),
+ addWebLinkDone: mockAction('ADD_WEB_LINK_DONE', { handle, data: webLink }),
+ deleteWebLinkInit: mockAction('DELETE_WEB_LINK_INIT'),
+ deleteWebLinkDone: mockAction('DELETE_WEB_LINK_DONE', { handle, data: webLink }),
+ saveEmailPreferencesInit: mockAction('SAVE_EMAIL_PREFERENCES_INIT'),
+ saveEmailPreferencesDone: mockAction('SAVE_EMAIL_PREFERENCES_DONE', { handle, data: { subscriptions: { TOPCODER_NL_DATA: true } } }),
+ linkExternalAccountInit: mockAction('LINK_EXTERNAL_ACCOUNT_INIT'),
+ linkExternalAccountDone: mockAction('LINK_EXTERNAL_ACCOUNT_DONE', { handle, data: linkedAccount2 }),
+ unlinkExternalAccountInit: mockAction('UNLINK_EXTERNAL_ACCOUNT_INIT'),
+ unlinkExternalAccountDone: mockAction('UNLINK_EXTERNAL_ACCOUNT_DONE', { handle, providerType: linkedAccount2.providerType }),
+ },
+};
+jest.setMock(require.resolve('actions/profile'), mockActions);
+
+const reducers = require('reducers/profile');
+
+let reducer;
+
+function testReducer(istate) {
+ let state;
+
+ test('Initial state', () => {
+ state = reducer(undefined, {});
+ expect(state).toEqual(istate);
+ });
+
+ test('Load profile', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.loadProfile());
+ expect(state).toEqual({ ...prevState, profileForHandle: handle });
+ });
+
+ test('Get member info', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.getInfoDone());
+ expect(state).toEqual({ ...prevState, info: { handle } });
+ });
+
+ test('Get external links', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.getExternalLinksDone());
+ expect(state).toEqual({ ...prevState, externalLinks: [externalLink] });
+ });
+
+ test('Get active challenges count', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.getActiveChallengesCountDone());
+ expect(state).toEqual({ ...prevState, activeChallengesCount: 5 });
+ });
+
+ test('Get linked account', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.getLinkedAccountsDone());
+ expect(state).toEqual({ ...prevState, linkedAccounts: [linkedAccount] });
+ });
+
+ test('Get credential', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.getCredentialDone());
+ expect(state).toEqual({ ...prevState, credential: { hasPassword: true } });
+ });
+
+ test('Get email preferences', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.getEmailPreferencesDone());
+ expect(state).toEqual({ ...prevState, emailPreferences: { TOPCODER_NL_DATA: true } });
+ });
+
+ test('Upload photo init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.uploadPhotoInit());
+ expect(state).toEqual({ ...prevState, uploadingPhoto: true });
+ });
+
+ test('Upload photo done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.uploadPhotoDone());
+ expect(state).toEqual({ ...prevState, info: { handle, photoURL }, uploadingPhoto: false });
+ });
+
+ test('Delete photo init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.deletePhotoInit());
+ expect(state).toEqual({ ...prevState, deletingPhoto: true });
+ });
+
+ test('Delete photo done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.deletePhotoDone());
+ expect(state).toEqual({ ...prevState, info: { handle, photoURL: null }, deletingPhoto: false });
+ });
+
+ test('Update profile init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.updateProfileInit());
+ expect(state).toEqual({ ...prevState, updatingProfile: true });
+ });
+
+ test('Update profile done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.updateProfileDone());
+ expect(state).toEqual({ ...prevState, info: { handle, photoURL: null, description: 'bio desc' }, updatingProfile: false });
+ });
+
+ test('Add skill init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.addSkillInit());
+ expect(state).toEqual({ ...prevState, addingSkill: true });
+ });
+
+ test('Add skill done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.addSkillDone());
+ expect(state).toEqual({ ...prevState, skills: [skill], addingSkill: false });
+ });
+
+ test('Hide skill init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.hideSkillInit());
+ expect(state).toEqual({ ...prevState, hidingSkill: true });
+ });
+
+ test('Hide skill done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.hideSkillDone());
+ expect(state).toEqual({ ...prevState, skills: [], hidingSkill: false });
+ });
+
+ test('Add web link init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.addWebLinkInit());
+ expect(state).toEqual({ ...prevState, addingWebLink: true });
+ });
+
+ test('Add web link done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.addWebLinkDone());
+ expect(state).toEqual({
+ ...prevState,
+ externalLinks: [externalLink, webLink],
+ addingWebLink: false,
+ });
+ });
+
+ test('Delete web link init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.deleteWebLinkInit());
+ expect(state).toEqual({ ...prevState, deletingWebLink: true });
+ });
+
+ test('Delete web link done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.deleteWebLinkDone());
+ expect(state).toEqual({ ...prevState, externalLinks: [externalLink], deletingWebLink: false });
+ });
+
+ test('Link external account init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.linkExternalAccountInit());
+ expect(state).toEqual({ ...prevState, linkingExternalAccount: true });
+ });
+
+ test('Link external account done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.linkExternalAccountDone());
+ expect(state).toEqual({
+ ...prevState,
+ linkedAccounts: [linkedAccount, linkedAccount2],
+ linkingExternalAccount: false,
+ });
+ });
+
+ test('Unlink external account init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.unlinkExternalAccountInit());
+ expect(state).toEqual({ ...prevState, unlinkingExternalAccount: true });
+ });
+
+ test('Unlink external account done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.unlinkExternalAccountDone());
+ expect(state).toEqual({
+ ...prevState,
+ linkedAccounts: [linkedAccount],
+ unlinkingExternalAccount: false,
+ });
+ });
+
+ test('Save email preferences init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.saveEmailPreferencesInit());
+ expect(state).toEqual({ ...prevState, savingEmailPreferences: true });
+ });
+
+ test('Save email preferences done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.saveEmailPreferencesDone());
+ expect(state).toEqual({
+ ...prevState,
+ emailPreferences: { TOPCODER_NL_DATA: true },
+ savingEmailPreferences: false,
+ });
+ });
+
+ test('Update password init', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.updatePasswordInit());
+ expect(state).toEqual({ ...prevState, updatingPassword: true });
+ });
+
+ test('Update password done', () => {
+ const prevState = state;
+ state = reducer(state, mockActions.profile.updatePasswordDone());
+ expect(state).toEqual({ ...prevState, updatingPassword: false });
+ });
+}
+
+const defaultState = {
+ achievements: null,
+ copilot: false,
+ country: '',
+ info: null,
+ loadingError: false,
+ skills: null,
+ stats: null,
+};
+
+describe('Default reducer', () => {
+ reducer = reducers.default;
+ testReducer(defaultState);
+});
+
+describe('Factory without server side rendering', () => {
+ beforeAll((done) => {
+ reducers.factory().then((res) => {
+ reducer = res;
+ done();
+ });
+ });
+
+ testReducer(defaultState);
+});
+
diff --git a/docs/actions.lookup.md b/docs/actions.lookup.md
new file mode 100644
index 00000000..00193737
--- /dev/null
+++ b/docs/actions.lookup.md
@@ -0,0 +1,11 @@
+
+
+## actions.lookup
+Actions related to lookup data.
+
+
+
+### actions.lookup.getApprovedSkills() ⇒ Action
+Gets approved skill tags.
+
+**Kind**: static method of [actions.lookup
](#module_actions.lookup)
diff --git a/docs/actions.profile.md b/docs/actions.profile.md
index 14e5b1fd..9f0899b0 100644
--- a/docs/actions.profile.md
+++ b/docs/actions.profile.md
@@ -23,6 +23,35 @@ Actions for interactions with profile details API.
* [.getSkillsDone(handle)](#module_actions.profile.getSkillsDone) ⇒ Action
* [.getStatsInit()](#module_actions.profile.getStatsInit) ⇒ Action
* [.getStatsDone(handle)](#module_actions.profile.getStatsDone) ⇒ Action
+ * [.getActiveChallengesCountInit()](#module_actions.profile.getActiveChallengesCountInit) ⇒ Action
+ * [.getActiveChallengesCountDone(handle, tokenV3)](#module_actions.profile.getActiveChallengesCountDone) ⇒ Action
+ * [.getLinkedAccountsInit()](#module_actions.profile.getLinkedAccountsInit) ⇒ Action
+ * [.getLinkedAccountsDone(profile, tokenV3)](#module_actions.profile.getLinkedAccountsDone) ⇒ Action
+ * [.getCredentialInit()](#module_actions.profile.getCredentialInit) ⇒ Action
+ * [.getCredentialDone(profile, tokenV3)](#module_actions.profile.getCredentialDone) ⇒ Action
+ * [.getEmailPreferencesInit()](#module_actions.profile.getEmailPreferencesInit) ⇒ Action
+ * [.getEmailPreferencesDone(profile, tokenV3)](#module_actions.profile.getEmailPreferencesDone) ⇒ Action
+ * [.uploadPhotoInit()](#module_actions.profile.uploadPhotoInit) ⇒ Action
+ * [.uploadPhotoDone(handle, tokenV3, file)](#module_actions.profile.uploadPhotoDone) ⇒ Action
+ * [.deletePhotoInit()](#module_actions.profile.deletePhotoInit) ⇒ Action
+ * [.updateProfileInit()](#module_actions.profile.updateProfileInit) ⇒ Action
+ * [.updateProfileDone(profile, tokenV3)](#module_actions.profile.updateProfileDone) ⇒ Action
+ * [.addSkillInit()](#module_actions.profile.addSkillInit) ⇒ Action
+ * [.addSkillDone(handle, tokenV3, skill)](#module_actions.profile.addSkillDone) ⇒ Action
+ * [.hideSkillInit()](#module_actions.profile.hideSkillInit) ⇒ Action
+ * [.hideSkillDone(handle, tokenV3, skill)](#module_actions.profile.hideSkillDone) ⇒ Action
+ * [.addWebLinkInit()](#module_actions.profile.addWebLinkInit) ⇒ Action
+ * [.addWebLinkDone(handle, tokenV3, webLink)](#module_actions.profile.addWebLinkDone) ⇒ Action
+ * [.deleteWebLinkInit(key)](#module_actions.profile.deleteWebLinkInit) ⇒ Action
+ * [.deleteWebLinkDone(handle, tokenV3, webLink)](#module_actions.profile.deleteWebLinkDone) ⇒ Action
+ * [.linkExternalAccountInit()](#module_actions.profile.linkExternalAccountInit) ⇒ Action
+ * [.linkExternalAccountDone(profile, tokenV3, providerType, callbackUrl)](#module_actions.profile.linkExternalAccountDone) ⇒ Action
+ * [.unlinkExternalAccountInit(providerType)](#module_actions.profile.unlinkExternalAccountInit) ⇒ Action
+ * [.unlinkExternalAccountDone(profile, tokenV3, providerType)](#module_actions.profile.unlinkExternalAccountDone) ⇒ Action
+ * [.saveEmailPreferencesInit()](#module_actions.profile.saveEmailPreferencesInit) ⇒ Action
+ * [.saveEmailPreferencesDone(profile, tokenV3, preferences)](#module_actions.profile.saveEmailPreferencesDone) ⇒ Action
+ * [.updatePasswordInit()](#module_actions.profile.updatePasswordInit) ⇒ Action
+ * [.updatePasswordDone(profile, tokenV3, newPassword, oldPassword)](#module_actions.profile.updatePasswordDone) ⇒ Action
@@ -167,3 +196,282 @@ Creates an action that loads member's stats.
| --- | --- | --- |
| handle | String
| Member handle. |
+
+
+### actions.profile.getActiveChallengesCountInit() ⇒ Action
+Creates an action that signals beginning of getting count of user's active challenges.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.getActiveChallengesCountDone(handle, tokenV3) ⇒ Action
+Creates an action that gets count of user's active challenges from the backend.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| handle | String
| Topcoder user handle. |
+| tokenV3 | String
| Optional. Topcoder auth token v3. Without token only public challenges will be counted. With the token provided, the action will also count private challenges related to this user. |
+
+
+
+### actions.profile.getLinkedAccountsInit() ⇒ Action
+Creates an action that signals beginning of getting linked accounts.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.getLinkedAccountsDone(profile, tokenV3) ⇒ Action
+Creates an action that gets linked accounts.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| profile | Object
| Topcoder member profile. |
+| tokenV3 | String
| Topcoder auth token v3. |
+
+
+
+### actions.profile.getCredentialInit() ⇒ Action
+Creates an action that signals beginning of getting credential.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.getCredentialDone(profile, tokenV3) ⇒ Action
+Creates an action that gets credential.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| profile | Object
| Topcoder member profile. |
+| tokenV3 | String
| Topcoder auth token v3. |
+
+
+
+### actions.profile.getEmailPreferencesInit() ⇒ Action
+Creates an action that signals beginning of getting email preferences.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.getEmailPreferencesDone(profile, tokenV3) ⇒ Action
+Creates an action that gets email preferences.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| profile | Object
| Topcoder member profile. |
+| tokenV3 | String
| Topcoder auth token v3. |
+
+
+
+### actions.profile.uploadPhotoInit() ⇒ Action
+Creates an action that signals beginning of uploading user's photo.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.uploadPhotoDone(handle, tokenV3, file) ⇒ Action
+Creates an action that uploads user's photo.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| handle | String
| Topcoder user handle. |
+| tokenV3 | String
| Topcoder auth token v3. |
+| file | String
| The photo file. |
+
+
+
+### actions.profile.deletePhotoInit() ⇒ Action
+Creates an action that signals beginning of deleting user's photo.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.updateProfileInit() ⇒ Action
+Creates an action that signals beginning of updating user's profile.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.updateProfileDone(profile, tokenV3) ⇒ Action
+Creates an action that updates user's profile.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| profile | String
| Topcoder user profile. |
+| tokenV3 | String
| Topcoder auth token v3. |
+
+
+
+### actions.profile.addSkillInit() ⇒ Action
+Creates an action that signals beginning of adding user's skill.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.addSkillDone(handle, tokenV3, skill) ⇒ Action
+Creates an action that adds user's skill.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| handle | String
| Topcoder user handle. |
+| tokenV3 | String
| Topcoder auth token v3. |
+| skill | Object
| Skill to add. |
+
+
+
+### actions.profile.hideSkillInit() ⇒ Action
+Creates an action that signals beginning of hiding user's skill.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.hideSkillDone(handle, tokenV3, skill) ⇒ Action
+Creates an action that hides user's skill.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| handle | String
| Topcoder user handle. |
+| tokenV3 | String
| Topcoder auth token v3. |
+| skill | Object
| Skill to hide. |
+
+
+
+### actions.profile.addWebLinkInit() ⇒ Action
+Creates an action that signals beginning of adding user's web link.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.addWebLinkDone(handle, tokenV3, webLink) ⇒ Action
+Creates an action that adds user's web link.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| handle | String
| Topcoder user handle. |
+| tokenV3 | String
| Topcoder auth token v3. |
+| webLink | String
| Web link to add. |
+
+
+
+### actions.profile.deleteWebLinkInit(key) ⇒ Action
+Creates an action that signals beginning of deleting user's web link.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| key | Object
| Web link key to delete. |
+
+
+
+### actions.profile.deleteWebLinkDone(handle, tokenV3, webLink) ⇒ Action
+Creates an action that deletes user's web link.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| handle | String
| Topcoder user handle. |
+| tokenV3 | String
| Topcoder auth token v3. |
+| webLink | String
| Web link to delete. |
+
+
+
+### actions.profile.linkExternalAccountInit() ⇒ Action
+Creates an action that signals beginning of linking external account.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.linkExternalAccountDone(profile, tokenV3, providerType, callbackUrl) ⇒ Action
+Creates an action that links external account.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| profile | Object
| Topcoder member handle. |
+| tokenV3 | String
| Topcoder auth token v3. |
+| providerType | String
| The external account service provider |
+| callbackUrl | String
| Optional. The callback url |
+
+
+
+### actions.profile.unlinkExternalAccountInit(providerType) ⇒ Action
+Creates an action that signals beginning of unlinking external account.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| providerType | Object
| External account provider type to delete. |
+
+
+
+### actions.profile.unlinkExternalAccountDone(profile, tokenV3, providerType) ⇒ Action
+Creates an action that unlinks external account.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| profile | Object
| Topcoder member profile. |
+| tokenV3 | String
| Topcoder auth token v3. |
+| providerType | String
| The external account service provider |
+
+
+
+### actions.profile.saveEmailPreferencesInit() ⇒ Action
+Creates an action that signals beginning of saving email preferences.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.saveEmailPreferencesDone(profile, tokenV3, preferences) ⇒ Action
+Creates an action that saves email preferences.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| profile | Object
| Topcoder member profile. |
+| tokenV3 | String
| Topcoder auth token v3. |
+| preferences | Object
| The email preferences |
+
+
+
+### actions.profile.updatePasswordInit() ⇒ Action
+Creates an action that signals beginning of updating user password.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+
+### actions.profile.updatePasswordDone(profile, tokenV3, newPassword, oldPassword) ⇒ Action
+Creates an action that updates user password.
+
+**Kind**: static method of [actions.profile
](#module_actions.profile)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| profile | Object
| Topcoder member profile. |
+| tokenV3 | String
| Topcoder auth token v3. |
+| newPassword | String
| The new password |
+| oldPassword | String
| The old password |
+
diff --git a/docs/index.md b/docs/index.md
index 70f1bb72..ec065107 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -25,6 +25,10 @@ messaging.
Actions related to user groups.
Actions related to lookup data.
+Actions for management of member tasks and payments. Under the hood it is very similar to the challenge listing management, as these tasks are in @@ -94,6 +98,11 @@ not look really necessary at the moment, thus we do not provide an action to really cancel group loading.
Reducer for actions.lookup actions.
+State segment managed by this reducer has the following structure:
+Member tasks reducer.
This module provides a service to get lookup data from Topcoder +via API V3.
+This module provides a service for searching for Topcoder members via API V3.
diff --git a/docs/reducers.lookup.md b/docs/reducers.lookup.md new file mode 100644 index 00000000..24c01523 --- /dev/null +++ b/docs/reducers.lookup.md @@ -0,0 +1,59 @@ + + +## reducers.lookup +Reducer for [actions.lookup](#module_actions.lookup) actions. + +State segment managed by this reducer has the following structure: + + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| approvedSkills |Array
| ''
| approved skill tags. |
+
+
+* [reducers.lookup](#module_reducers.lookup)
+ * _static_
+ * [.default](#module_reducers.lookup.default)
+ * [.factory()](#module_reducers.lookup.factory) ⇒ Promise
+ * _inner_
+ * [~onGetApprovedSkills(state, action)](#module_reducers.lookup..onGetApprovedSkills) ⇒ Object
+ * [~create(initialState)](#module_reducers.lookup..create) ⇒ function
+
+
+
+### reducers.lookup.default
+Reducer with default initial state.
+
+**Kind**: static property of [reducers.lookup
](#module_reducers.lookup)
+
+
+### reducers.lookup.factory() ⇒ Promise
+Factory which creates a new reducer.
+
+**Kind**: static method of [reducers.lookup
](#module_reducers.lookup)
+**Resolves**: Function(state, action): state
New reducer.
+
+
+### reducers.lookup~onGetApprovedSkills(state, action) ⇒ Object
+Handles LOOKUP/GET_APPROVED_SKILLS action.
+
+**Kind**: inner method of [reducers.lookup
](#module_reducers.lookup)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.lookup~create(initialState) ⇒ function
+Creates a new Lookup reducer with the specified initial state.
+
+**Kind**: inner method of [reducers.lookup
](#module_reducers.lookup)
+**Returns**: function
- Lookup reducer.
+
+| Param | Type | Description |
+| --- | --- | --- |
+| initialState | Object
| Optional. Initial state. |
+
diff --git a/docs/reducers.profile.md b/docs/reducers.profile.md
index 5492081a..c81c34d5 100644
--- a/docs/reducers.profile.md
+++ b/docs/reducers.profile.md
@@ -18,6 +18,21 @@ Reducer for Profile API data
* [~onGetInfoDone(state, action)](#module_reducers.profile..onGetInfoDone) ⇒ Object
* [~onGetSkillsDone(state, action)](#module_reducers.profile..onGetSkillsDone) ⇒ Object
* [~onGetStatsDone(state, action)](#module_reducers.profile..onGetStatsDone) ⇒ Object
+ * [~onGetActiveChallengesCountDone(state, action)](#module_reducers.profile..onGetActiveChallengesCountDone) ⇒ Object
+ * [~onGetLinkedAccountsDone(state, action)](#module_reducers.profile..onGetLinkedAccountsDone) ⇒ Object
+ * [~onGetCredentialDone(state, action)](#module_reducers.profile..onGetCredentialDone) ⇒ Object
+ * [~onGetEmailPreferencesDone(state, action)](#module_reducers.profile..onGetEmailPreferencesDone) ⇒ Object
+ * [~onUploadPhotoDone(state, action)](#module_reducers.profile..onUploadPhotoDone) ⇒ Object
+ * [~onDeletePhotoDone(state, action)](#module_reducers.profile..onDeletePhotoDone) ⇒ Object
+ * [~onUpdateProfileDone(state, action)](#module_reducers.profile..onUpdateProfileDone) ⇒ Object
+ * [~onAddSkillDone(state, action)](#module_reducers.profile..onAddSkillDone) ⇒ Object
+ * [~onHideSkillDone(state, action)](#module_reducers.profile..onHideSkillDone) ⇒ Object
+ * [~onAddWebLinkDone(state, action)](#module_reducers.profile..onAddWebLinkDone) ⇒ Object
+ * [~onDeleteWebLinkDone(state, action)](#module_reducers.profile..onDeleteWebLinkDone) ⇒ Object
+ * [~onLinkExternalAccountDone(state, action)](#module_reducers.profile..onLinkExternalAccountDone) ⇒ Object
+ * [~onUnlinkExternalAccountDone(state, action)](#module_reducers.profile..onUnlinkExternalAccountDone) ⇒ Object
+ * [~onSaveEmailPreferencesDone(state, action)](#module_reducers.profile..onSaveEmailPreferencesDone) ⇒ Object
+ * [~onUpdatePasswordDone(state, action)](#module_reducers.profile..onUpdatePasswordDone) ⇒ Object
* [~create(initialState)](#module_reducers.profile..create) ⇒ function
@@ -107,6 +122,201 @@ Handles PROFILE/GET_STATS_DONE action.
| state | Object
| |
| action | Object
| Payload will be JSON from api call |
+
+
+### reducers.profile~onGetActiveChallengesCountDone(state, action) ⇒ Object
+Handles PROFILE/GET_ACTIVE_CHALLENGES_COUNT_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onGetLinkedAccountsDone(state, action) ⇒ Object
+Handles PROFILE/GET_LINKED_ACCOUNTS_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onGetCredentialDone(state, action) ⇒ Object
+Handles PROFILE/GET_CREDENTIAL_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onGetEmailPreferencesDone(state, action) ⇒ Object
+Handles PROFILE/GET_EMAIL_PREFERENCES_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onUploadPhotoDone(state, action) ⇒ Object
+Handles PROFILE/UPLOAD_PHOTO_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onDeletePhotoDone(state, action) ⇒ Object
+Handles PROFILE/DELETE_PHOTO_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onUpdateProfileDone(state, action) ⇒ Object
+Handles PROFILE/UPDATE_PROFILE_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onAddSkillDone(state, action) ⇒ Object
+Handles PROFILE/ADD_SKILL_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onHideSkillDone(state, action) ⇒ Object
+Handles PROFILE/HIDE_SKILL_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onAddWebLinkDone(state, action) ⇒ Object
+Handles PROFILE/ADD_WEB_LINK_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onDeleteWebLinkDone(state, action) ⇒ Object
+Handles PROFILE/DELETE_WEB_LINK_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onLinkExternalAccountDone(state, action) ⇒ Object
+Handles PROFILE/LINK_EXTERNAL_ACCOUNT_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onUnlinkExternalAccountDone(state, action) ⇒ Object
+Handles PROFILE/UNLINK_EXTERNAL_ACCOUNT_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onSaveEmailPreferencesDone(state, action) ⇒ Object
+Handles PROFILE/SAVE_EMAIL_PREFERENCES_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
+
+
+### reducers.profile~onUpdatePasswordDone(state, action) ⇒ Object
+Handles PROFILE/UPDATE_PASSWORD_DONE action.
+
+**Kind**: inner method of [reducers.profile
](#module_reducers.profile)
+**Returns**: Object
- New state
+
+| Param | Type | Description |
+| --- | --- | --- |
+| state | Object
| |
+| action | Object
| Payload will be JSON from api call |
+
### reducers.profile~create(initialState) ⇒ function
diff --git a/docs/services.api.md b/docs/services.api.md
index 015b5986..eb787a8e 100644
--- a/docs/services.api.md
+++ b/docs/services.api.md
@@ -19,6 +19,8 @@ This module provides a service for conventient access to Topcoder APIs.
* [.postJson(endpoint, json)](#module_services.api..Api+postJson) ⇒ Promise
* [.put(endpoint, body)](#module_services.api..Api+put) ⇒ Promise
* [.putJson(endpoint, json)](#module_services.api..Api+putJson) ⇒ Promise
+ * [.patch(endpoint, body)](#module_services.api..Api+patch) ⇒ Promise
+ * [.patchJson(endpoint, json)](#module_services.api..Api+patchJson) ⇒ Promise
* [.upload(endpoint, body, callback)](#module_services.api..Api+upload) ⇒ Promise
@@ -70,6 +72,8 @@ thing we need to be different is the base URL and auth token to use.
* [.postJson(endpoint, json)](#module_services.api..Api+postJson) ⇒ Promise
* [.put(endpoint, body)](#module_services.api..Api+put) ⇒ Promise
* [.putJson(endpoint, json)](#module_services.api..Api+putJson) ⇒ Promise
+ * [.patch(endpoint, body)](#module_services.api..Api+patch) ⇒ Promise
+ * [.patchJson(endpoint, json)](#module_services.api..Api+patchJson) ⇒ Promise
* [.upload(endpoint, body, callback)](#module_services.api..Api+upload) ⇒ Promise
@@ -182,6 +186,30 @@ Sends PUT request to the specified endpoint.
| endpoint | String
|
| json | JSON
|
+
+
+#### api.patch(endpoint, body) ⇒ Promise
+Sends PATCH request to the specified endpoint.
+
+**Kind**: instance method of [Api
](#module_services.api..Api)
+
+| Param | Type |
+| --- | --- |
+| endpoint | String
|
+| body | Blob
\| BufferSource
\| FormData
\| String
|
+
+
+
+#### api.patchJson(endpoint, json) ⇒ Promise
+Sends PATCH request to the specified endpoint.
+
+**Kind**: instance method of [Api
](#module_services.api..Api)
+
+| Param | Type |
+| --- | --- |
+| endpoint | String
|
+| json | JSON
|
+
#### api.upload(endpoint, body, callback) ⇒ Promise
diff --git a/docs/services.lookup.md b/docs/services.lookup.md
new file mode 100644
index 00000000..a0088232
--- /dev/null
+++ b/docs/services.lookup.md
@@ -0,0 +1,56 @@
+
+
+## services.lookup
+This module provides a service to get lookup data from Topcoder
+via API V3.
+
+
+* [services.lookup](#module_services.lookup)
+ * _static_
+ * [.getService(tokenV3)](#module_services.lookup.getService) ⇒ LookupService
+ * _inner_
+ * [~LookupService](#module_services.lookup..LookupService)
+ * [new LookupService(tokenV3)](#new_module_services.lookup..LookupService_new)
+ * [.getTags(params)](#module_services.lookup..LookupService+getTags) ⇒ Promise
+
+
+
+### services.lookup.getService(tokenV3) ⇒ LookupService
+Returns a new or existing lookup service.
+
+**Kind**: static method of [services.lookup
](#module_services.lookup)
+**Returns**: LookupService
- Lookup service object
+
+| Param | Type | Description |
+| --- | --- | --- |
+| tokenV3 | String
| Optional. Auth token for Topcoder API v3. |
+
+
+
+### services.lookup~LookupService
+**Kind**: inner class of [services.lookup
](#module_services.lookup)
+
+* [~LookupService](#module_services.lookup..LookupService)
+ * [new LookupService(tokenV3)](#new_module_services.lookup..LookupService_new)
+ * [.getTags(params)](#module_services.lookup..LookupService+getTags) ⇒ Promise
+
+
+
+#### new LookupService(tokenV3)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| tokenV3 | String
| Optional. Auth token for Topcoder API v3. |
+
+
+
+#### lookupService.getTags(params) ⇒ Promise
+Gets tags.
+
+**Kind**: instance method of [LookupService
](#module_services.lookup..LookupService)
+**Returns**: Promise
- Resolves to the tags.
+
+| Param | Type | Description |
+| --- | --- | --- |
+| params | Object
| Parameters |
+
diff --git a/docs/services.members.md b/docs/services.members.md
index 2af2f272..87dade98 100644
--- a/docs/services.members.md
+++ b/docs/services.members.md
@@ -18,6 +18,14 @@ members via API V3.
* [.getSkills(handle)](#module_services.members..MembersService+getSkills) ⇒ Promise
* [.getStats(handle)](#module_services.members..MembersService+getStats) ⇒ Promise
* [.getMemberSuggestions(keyword)](#module_services.members..MembersService+getMemberSuggestions) ⇒ Promise
+ * [.addWebLink(userHandle, webLink)](#module_services.members..MembersService+addWebLink) ⇒ Promise
+ * [.deleteWebLink(userHandle, webLinkHandle)](#module_services.members..MembersService+deleteWebLink) ⇒ Promise
+ * [.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
@@ -47,6 +55,14 @@ Service class.
* [.getSkills(handle)](#module_services.members..MembersService+getSkills) ⇒ Promise
* [.getStats(handle)](#module_services.members..MembersService+getStats) ⇒ Promise
* [.getMemberSuggestions(keyword)](#module_services.members..MembersService+getMemberSuggestions) ⇒ Promise
+ * [.addWebLink(userHandle, webLink)](#module_services.members..MembersService+addWebLink) ⇒ Promise
+ * [.deleteWebLink(userHandle, webLinkHandle)](#module_services.members..MembersService+deleteWebLink) ⇒ Promise
+ * [.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
@@ -144,3 +160,104 @@ WARNING: This method requires v3 authorization.
| --- | --- | --- |
| keyword | String
| Partial string to find suggestions for |
+
+
+#### membersService.addWebLink(userHandle, webLink) ⇒ Promise
+Adds external web link for member.
+
+**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 |
+| webLink | String
| The external web link |
+
+
+
+#### membersService.deleteWebLink(userHandle, webLinkHandle) ⇒ Promise
+Deletes external web link for member.
+
+**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 |
+| webLinkHandle | String
| The external web link handle |
+
+
+
+#### membersService.addSkill(handle, skillTagId) ⇒ Promise
+Adds user skill.
+
+**Kind**: instance method of [MembersService
](#module_services.members..MembersService)
+**Returns**: Promise
- Resolves to operation result
+
+| Param | Type | Description |
+| --- | --- | --- |
+| handle | String
| Topcoder user handle |
+| skillTagId | Number
| Skill tag id |
+
+
+
+#### membersService.hideSkill(handle, skillTagId) ⇒ Promise
+Hides user skill.
+
+**Kind**: instance method of [MembersService
](#module_services.members..MembersService)
+**Returns**: Promise
- Resolves to operation result
+
+| Param | Type | Description |
+| --- | --- | --- |
+| handle | String
| Topcoder user handle |
+| skillTagId | Number
| Skill tag id |
+
+
+
+#### membersService.updateMemberProfile(profile) ⇒ Promise
+Updates member profile.
+
+**Kind**: instance method of [MembersService
](#module_services.members..MembersService)
+**Returns**: Promise
- Resolves to the api response content
+
+| Param | Type | Description |
+| --- | --- | --- |
+| 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.
+
+**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. |
+
diff --git a/docs/services.user.md b/docs/services.user.md
index 0daaf23a..de282529 100644
--- a/docs/services.user.md
+++ b/docs/services.user.md
@@ -15,6 +15,14 @@ The User service provides functionality related to Topcoder user
* [.getAchievements(username)](#module_services.user..User+getAchievements) ⇒ Object
* [.getUserPublic(username)](#module_services.user..User+getUserPublic) ⇒ Object
* [.getUser(username)](#module_services.user..User+getUser) ⇒ Promise
+ * [.getEmailPreferences(userId)](#module_services.user..User+getEmailPreferences) ⇒ Promise
+ * [.saveEmailPreferences(user, preferences)](#module_services.user..User+saveEmailPreferences) ⇒ Promise
+ * [.getCredential(userId)](#module_services.user..User+getCredential) ⇒ Promise
+ * [.updatePassword(userId, newPassword, oldPassword)](#module_services.user..User+updatePassword) ⇒ Promise
+ * [.getLinkedAccounts(userId)](#module_services.user..User+getLinkedAccounts) ⇒ Promise
+ * [.unlinkExternalAccount(userId, provider)](#module_services.user..User+unlinkExternalAccount) ⇒ Promise
+ * [.linkExternalAccount(userId, provider, callbackUrl)](#module_services.user..User+linkExternalAccount) ⇒ Promise
+ * [~getSocialUserData(profile, accessToken)](#module_services.user..getSocialUserData) ⇒ Object
@@ -47,6 +55,13 @@ Service class.
* [.getAchievements(username)](#module_services.user..User+getAchievements) ⇒ Object
* [.getUserPublic(username)](#module_services.user..User+getUserPublic) ⇒ Object
* [.getUser(username)](#module_services.user..User+getUser) ⇒ Promise
+ * [.getEmailPreferences(userId)](#module_services.user..User+getEmailPreferences) ⇒ Promise
+ * [.saveEmailPreferences(user, preferences)](#module_services.user..User+saveEmailPreferences) ⇒ Promise
+ * [.getCredential(userId)](#module_services.user..User+getCredential) ⇒ Promise
+ * [.updatePassword(userId, newPassword, oldPassword)](#module_services.user..User+updatePassword) ⇒ Promise
+ * [.getLinkedAccounts(userId)](#module_services.user..User+getLinkedAccounts) ⇒ Promise
+ * [.unlinkExternalAccount(userId, provider)](#module_services.user..User+unlinkExternalAccount) ⇒ Promise
+ * [.linkExternalAccount(userId, provider, callbackUrl)](#module_services.user..User+linkExternalAccount) ⇒ Promise
@@ -95,3 +110,116 @@ NOTE: Only admins are authorized to use the underlying endpoint.
| --- | --- |
| username | String
|
+
+
+#### user.getEmailPreferences(userId) ⇒ Promise
+Gets email preferences.
+
+NOTE: Only admins are authorized to use the underlying endpoint.
+
+**Kind**: instance method of [User
](#module_services.user..User)
+**Returns**: Promise
- Resolves to the email preferences result
+
+| Param | Type | Description |
+| --- | --- | --- |
+| userId | Number
| The TopCoder user id |
+
+
+
+#### user.saveEmailPreferences(user, preferences) ⇒ Promise
+Saves email preferences.
+
+NOTE: Only admins are authorized to use the underlying endpoint.
+
+**Kind**: instance method of [User
](#module_services.user..User)
+**Returns**: Promise
- Resolves to the email preferences result
+
+| Param | Type | Description |
+| --- | --- | --- |
+| user | Object
| The TopCoder user |
+| preferences | Object
| The email preferences |
+
+
+
+#### user.getCredential(userId) ⇒ Promise
+Gets credential for the specified user id.
+
+NOTE: Only admins are authorized to use the underlying endpoint.
+
+**Kind**: instance method of [User
](#module_services.user..User)
+**Returns**: Promise
- Resolves to the linked accounts array.
+
+| Param | Type | Description |
+| --- | --- | --- |
+| userId | Number
| The user id |
+
+
+
+#### user.updatePassword(userId, newPassword, oldPassword) ⇒ Promise
+Updates user password.
+
+NOTE: Only admins are authorized to use the underlying endpoint.
+
+**Kind**: instance method of [User
](#module_services.user..User)
+**Returns**: Promise
- Resolves to the update result.
+
+| Param | Type | Description |
+| --- | --- | --- |
+| userId | Number
| The user id |
+| newPassword | String
| The new password |
+| oldPassword | String
| The old password |
+
+
+
+#### user.getLinkedAccounts(userId) ⇒ Promise
+Gets linked accounts for the specified user id.
+
+NOTE: Only admins are authorized to use the underlying endpoint.
+
+**Kind**: instance method of [User
](#module_services.user..User)
+**Returns**: Promise
- Resolves to the linked accounts array.
+
+| Param | Type | Description |
+| --- | --- | --- |
+| userId | Number
| The user id |
+
+
+
+#### user.unlinkExternalAccount(userId, provider) ⇒ Promise
+Unlinks external account.
+
+**Kind**: instance method of [User
](#module_services.user..User)
+**Returns**: Promise
- Resolves to the unlink result
+
+| Param | Type | Description |
+| --- | --- | --- |
+| userId | Number
| The TopCoder user id |
+| provider | String
| The external account service provider |
+
+
+
+#### user.linkExternalAccount(userId, provider, callbackUrl) ⇒ Promise
+Links external account.
+
+**Kind**: instance method of [User
](#module_services.user..User)
+**Returns**: Promise
- Resolves to the linked account result
+
+| Param | Type | Description |
+| --- | --- | --- |
+| userId | Number
| The TopCoder user id |
+| provider | String
| The external account service provider |
+| callbackUrl | String
| Optional. The callback url |
+
+
+
+### services.user~getSocialUserData(profile, accessToken) ⇒ Object
+Gets social user data.
+
+**Kind**: inner method of [services.user
](#module_services.user)
+**Returns**: Object
- Social user data
+
+| Param | Type | Description |
+| --- | --- | --- |
+| profile | Object
| The user social profile |
+| accessToken | \*
| The access token |
+
diff --git a/package-lock.json b/package-lock.json
index ebabdc18..2792f6b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -160,6 +160,11 @@
}
}
},
+ "Base64": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.1.4.tgz",
+ "integrity": "sha1-6fbGvvVn/WNepBYqsU3TKedKpt4="
+ },
"abab": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
@@ -543,6 +548,39 @@
"integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=",
"dev": true
},
+ "auth0-js": {
+ "version": "6.8.4",
+ "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-6.8.4.tgz",
+ "integrity": "sha1-Qw3Uystk2NFdabHmIRhPmipkCmE=",
+ "requires": {
+ "Base64": "0.1.4",
+ "json-fallback": "0.0.1",
+ "jsonp": "0.0.4",
+ "qs": "git+https://github.com/jfromaniello/node-querystring.git#5d96513991635e3e22d7aa54a8584d6ce97cace8",
+ "reqwest": "1.1.6",
+ "trim": "0.0.1",
+ "winchan": "0.1.4",
+ "xtend": "2.1.2"
+ },
+ "dependencies": {
+ "object-keys": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz",
+ "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY="
+ },
+ "qs": {
+ "version": "git+https://github.com/jfromaniello/node-querystring.git#5d96513991635e3e22d7aa54a8584d6ce97cace8"
+ },
+ "xtend": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz",
+ "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=",
+ "requires": {
+ "object-keys": "0.4.0"
+ }
+ }
+ }
+ },
"autoprefixer": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-8.4.1.tgz",
@@ -7238,6 +7276,11 @@
"integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
"dev": true
},
+ "json-fallback": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/json-fallback/-/json-fallback-0.0.1.tgz",
+ "integrity": "sha1-6OMIPD/drQ+bXwnTMSB0RCWA14E="
+ },
"json-loader": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz",
@@ -7287,6 +7330,14 @@
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
"dev": true
},
+ "jsonp": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.0.4.tgz",
+ "integrity": "sha1-lGZaS3caq+y4qshBNbmVlHVpGL0=",
+ "requires": {
+ "debug": "2.6.9"
+ }
+ },
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -9655,6 +9706,11 @@
}
}
},
+ "reqwest": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/reqwest/-/reqwest-1.1.6.tgz",
+ "integrity": "sha1-S2iU0pWWv46CSiXzSXXfFVYu6BM="
+ },
"reselect": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
@@ -18937,6 +18993,11 @@
"punycode": "2.1.0"
}
},
+ "trim": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
+ "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0="
+ },
"trim-right": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
@@ -20014,6 +20075,11 @@
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
},
+ "winchan": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.1.4.tgz",
+ "integrity": "sha1-iPoSQRzVQutiYBjDihlry7F5k7s="
+ },
"window-size": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
diff --git a/package.json b/package.json
index e3569660..7ccbb3ae 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
},
"version": "0.0.6",
"dependencies": {
+ "auth0-js": "^6.8.4",
"isomorphic-fetch": "^2.2.1",
"le_node": "^1.7.0",
"lodash": "^4.17.5",
diff --git a/src/actions/index.js b/src/actions/index.js
index dff22096..333494c1 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -10,6 +10,7 @@ import profileActions from './profile';
import memberActions from './members';
import memberTaskActions from './member-tasks';
import reviewOpportunityActions from './reviewOpportunity';
+import lookupActions from './lookup';
export const actions = {
auth: authActions.auth,
@@ -24,6 +25,7 @@ export const actions = {
members: memberActions.members,
memberTasks: memberTaskActions.memberTasks,
reviewOpportunity: reviewOpportunityActions.reviewOpportunity,
+ lookup: lookupActions.lookup,
};
export default undefined;
diff --git a/src/actions/lookup.js b/src/actions/lookup.js
new file mode 100644
index 00000000..4b80311e
--- /dev/null
+++ b/src/actions/lookup.js
@@ -0,0 +1,27 @@
+/**
+ * @module "actions.lookup"
+ * @desc Actions related to lookup data.
+ */
+
+import { createActions } from 'redux-actions';
+import { getService } from '../services/lookup';
+
+/**
+ * @static
+ * @desc Gets approved skill tags.
+ * @return {Action}
+ */
+function getApprovedSkills() {
+ const service = getService();
+ const params = {
+ domain: 'SKILLS',
+ status: 'APPROVED',
+ };
+ return service.getTags(params);
+}
+
+export default createActions({
+ LOOKUP: {
+ GET_APPROVED_SKILLS: getApprovedSkills,
+ },
+});
diff --git a/src/actions/profile.js b/src/actions/profile.js
index 9a5d8b65..69540709 100644
--- a/src/actions/profile.js
+++ b/src/actions/profile.js
@@ -8,6 +8,7 @@ import { createActions } from 'redux-actions';
import { getService as getUserService } from '../services/user';
import { getService as getMembersService } from '../services/members';
+import { getService as getChallengesService } from '../services/challenges';
/**
* @static
@@ -127,6 +128,319 @@ function getStatsDone(handle) {
return getMembersService().getStats(handle);
}
+/**
+ * @static
+ * @desc Creates an action that signals beginning of getting count of user's active challenges.
+ * @return {Action}
+ */
+function getActiveChallengesCountInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that gets count of user's active challenges from the backend.
+ * @param {String} handle Topcoder user handle.
+ * @param {String} tokenV3 Optional. Topcoder auth token v3. Without token only
+ * public challenges will be counted. With the token provided, the action will
+ * also count private challenges related to this user.
+ * @return {Action}
+ */
+function getActiveChallengesCountDone(handle, tokenV3) {
+ const service = getChallengesService(tokenV3);
+ const filter = { status: 'ACTIVE' };
+ const params = { limit: 1, offset: 0 };
+
+ const calls = [];
+ calls.push(service.getUserChallenges(handle, filter, params));
+ calls.push(service.getUserMarathonMatches(handle, filter, params));
+
+ return Promise.all(calls).then(([uch, umm]) => uch.totalCount + umm.totalCount);
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of getting linked accounts.
+ * @return {Action}
+ */
+function getLinkedAccountsInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that gets linked accounts.
+ *
+ * @param {Object} profile Topcoder member profile.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @return {Action}
+ */
+function getLinkedAccountsDone(profile, tokenV3) {
+ const service = getUserService(tokenV3);
+ return service.getLinkedAccounts(profile.userId);
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of getting credential.
+ * @return {Action}
+ */
+function getCredentialInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that gets credential.
+ *
+ * @param {Object} profile Topcoder member profile.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @return {Action}
+ */
+function getCredentialDone(profile, tokenV3) {
+ const service = getUserService(tokenV3);
+ return service.getCredential(profile.userId);
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of getting email preferences.
+ * @return {Action}
+ */
+function getEmailPreferencesInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that gets email preferences.
+ *
+ * @param {Object} profile Topcoder member profile.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @return {Action}
+ */
+function getEmailPreferencesDone(profile, tokenV3) {
+ const service = getUserService(tokenV3);
+ return service.getEmailPreferences(profile.userId);
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of uploading user's photo.
+ * @return {Action}
+ */
+function uploadPhotoInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that uploads user's photo.
+ * @param {String} handle Topcoder user handle.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @param {String} file The photo file.
+ * @return {Action}
+ */
+function uploadPhotoDone(handle, tokenV3, file) {
+ const service = getMembersService(tokenV3);
+ return service.getPresignedUrl(handle, file)
+ .then(res => service.uploadFileToS3(res))
+ .then(res => service.updateMemberPhoto(res))
+ .then(photoURL => ({ handle, photoURL }));
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of deleting user's photo.
+ * @return {Action}
+ */
+function deletePhotoInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of updating user's profile.
+ * @return {Action}
+ */
+function updateProfileInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that updates user's profile.
+ * @param {String} profile Topcoder user profile.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @return {Action}
+ */
+function updateProfileDone(profile, tokenV3) {
+ const service = getMembersService(tokenV3);
+ return service.updateMemberProfile(profile);
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of adding user's skill.
+ * @return {Action}
+ */
+function addSkillInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that adds user's skill.
+ * @param {String} handle Topcoder user handle.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @param {Object} skill Skill to add.
+ * @return {Action}
+ */
+function addSkillDone(handle, tokenV3, skill) {
+ const service = getMembersService(tokenV3);
+ return service.addSkill(handle, skill.tagId)
+ .then(res => ({ skills: res.skills, handle, skill }));
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of hiding user's skill.
+ * @return {Action}
+ */
+function hideSkillInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that hides user's skill.
+ * @param {String} handle Topcoder user handle.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @param {Object} skill Skill to hide.
+ * @return {Action}
+ */
+function hideSkillDone(handle, tokenV3, skill) {
+ const service = getMembersService(tokenV3);
+ return service.hideSkill(handle, skill.tagId)
+ .then(res => ({ skills: res.skills, handle, skill }));
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of adding user's web link.
+ * @return {Action}
+ */
+function addWebLinkInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that adds user's web link.
+ * @param {String} handle Topcoder user handle.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @param {String} webLink Web link to add.
+ * @return {Action}
+ */
+function addWebLinkDone(handle, tokenV3, webLink) {
+ const service = getMembersService(tokenV3);
+ return service.addWebLink(handle, webLink).then(res => ({ data: res, handle }));
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of deleting user's web link.
+ * @param {Object} key Web link key to delete.
+ * @return {Action}
+ */
+function deleteWebLinkInit({ key }) {
+ return { key };
+}
+
+/**
+ * @static
+ * @desc Creates an action that deletes user's web link.
+ * @param {String} handle Topcoder user handle.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @param {String} webLink Web link to delete.
+ * @return {Action}
+ */
+function deleteWebLinkDone(handle, tokenV3, webLink) {
+ const service = getMembersService(tokenV3);
+ return service.deleteWebLink(handle, webLink.key).then(res => ({ data: res, handle }));
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of linking external account.
+ * @return {Action}
+ */
+function linkExternalAccountInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that links external account.
+ * @param {Object} profile Topcoder member handle.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @param {String} providerType The external account service provider
+ * @param {String} callbackUrl Optional. The callback url
+ * @return {Action}
+ */
+function linkExternalAccountDone(profile, tokenV3, providerType, callbackUrl) {
+ const service = getUserService(tokenV3);
+ return service.linkExternalAccount(profile.userId, providerType, callbackUrl)
+ .then(res => ({ data: res, handle: profile.handle }));
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of unlinking external account.
+ * @param {Object} providerType External account provider type to delete.
+ * @return {Action}
+ */
+function unlinkExternalAccountInit({ providerType }) {
+ return { providerType };
+}
+
+/**
+ * @static
+ * @desc Creates an action that unlinks external account.
+ * @param {Object} profile Topcoder member profile.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @param {String} providerType The external account service provider
+ * @return {Action}
+ */
+function unlinkExternalAccountDone(profile, tokenV3, providerType) {
+ const service = getUserService(tokenV3);
+ return service.unlinkExternalAccount(profile.userId, providerType)
+ .then(() => ({ providerType, handle: profile.handle }));
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of saving email preferences.
+ * @return {Action}
+ */
+function saveEmailPreferencesInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that saves email preferences.
+ *
+ * @param {Object} profile Topcoder member profile.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @param {Object} preferences The email preferences
+ * @return {Action}
+ */
+function saveEmailPreferencesDone(profile, tokenV3, preferences) {
+ const service = getUserService(tokenV3);
+ return service.saveEmailPreferences(profile, preferences)
+ .then(res => ({ data: res, handle: profile.handle }));
+}
+
+/**
+ * @static
+ * @desc Creates an action that signals beginning of updating user password.
+ * @return {Action}
+ */
+function updatePasswordInit() {}
+
+/**
+ * @static
+ * @desc Creates an action that updates user password.
+ *
+ * @param {Object} profile Topcoder member profile.
+ * @param {String} tokenV3 Topcoder auth token v3.
+ * @param {String} newPassword The new password
+ * @param {String} oldPassword The old password
+ * @return {Action}
+ */
+function updatePasswordDone(profile, tokenV3, newPassword, oldPassword) {
+ const service = getUserService(tokenV3);
+ return service.updatePassword(profile.userId, newPassword, oldPassword)
+ .then(res => ({ data: res, handle: profile.handle }));
+}
+
export default createActions({
PROFILE: {
LOAD_PROFILE: loadProfile,
@@ -142,5 +456,35 @@ export default createActions({
GET_SKILLS_DONE: getSkillsDone,
GET_STATS_INIT: getStatsInit,
GET_STATS_DONE: getStatsDone,
+ GET_ACTIVE_CHALLENGES_COUNT_INIT: getActiveChallengesCountInit,
+ GET_ACTIVE_CHALLENGES_COUNT_DONE: getActiveChallengesCountDone,
+ GET_LINKED_ACCOUNTS_INIT: getLinkedAccountsInit,
+ GET_LINKED_ACCOUNTS_DONE: getLinkedAccountsDone,
+ GET_EMAIL_PREFERENCES_INIT: getEmailPreferencesInit,
+ GET_EMAIL_PREFERENCES_DONE: getEmailPreferencesDone,
+ GET_CREDENTIAL_INIT: getCredentialInit,
+ GET_CREDENTIAL_DONE: getCredentialDone,
+ UPLOAD_PHOTO_INIT: uploadPhotoInit,
+ UPLOAD_PHOTO_DONE: uploadPhotoDone,
+ DELETE_PHOTO_INIT: deletePhotoInit,
+ DELETE_PHOTO_DONE: updateProfileDone,
+ UPDATE_PROFILE_INIT: updateProfileInit,
+ UPDATE_PROFILE_DONE: updateProfileDone,
+ ADD_SKILL_INIT: addSkillInit,
+ ADD_SKILL_DONE: addSkillDone,
+ HIDE_SKILL_INIT: hideSkillInit,
+ HIDE_SKILL_DONE: hideSkillDone,
+ ADD_WEB_LINK_INIT: addWebLinkInit,
+ ADD_WEB_LINK_DONE: addWebLinkDone,
+ DELETE_WEB_LINK_INIT: deleteWebLinkInit,
+ DELETE_WEB_LINK_DONE: deleteWebLinkDone,
+ LINK_EXTERNAL_ACCOUNT_INIT: linkExternalAccountInit,
+ LINK_EXTERNAL_ACCOUNT_DONE: linkExternalAccountDone,
+ UNLINK_EXTERNAL_ACCOUNT_INIT: unlinkExternalAccountInit,
+ UNLINK_EXTERNAL_ACCOUNT_DONE: unlinkExternalAccountDone,
+ SAVE_EMAIL_PREFERENCES_INIT: saveEmailPreferencesInit,
+ SAVE_EMAIL_PREFERENCES_DONE: saveEmailPreferencesDone,
+ UPDATE_PASSWORD_INIT: updatePasswordInit,
+ UPDATE_PASSWORD_DONE: updatePasswordDone,
},
});
diff --git a/src/index.js b/src/index.js
index f892cdd8..ac7ff9e0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,9 +1,9 @@
/**
* Export the lib.
*/
-import reducers, { factory as reducerFactory } from './reducers';
+import reducers, { factories as reducerFactories, factory as reducerFactory } from './reducers';
-export { reducerFactory };
+export { reducerFactories, reducerFactory };
export { reducers };
diff --git a/src/reducers/auth.js b/src/reducers/auth.js
index 5afb468b..6957133e 100644
--- a/src/reducers/auth.js
+++ b/src/reducers/auth.js
@@ -16,6 +16,7 @@ import _ from 'lodash';
import { decodeToken } from 'tc-accounts';
import { redux } from 'topcoder-react-utils';
import actions from '../actions/auth';
+import profileActions from '../actions/profile';
/**
* Handles actions.auth.loadProfile action.
@@ -55,6 +56,51 @@ function create(initialState) {
groups: state.profile.groups.concat({ id: payload.groupId.toString() }),
},
}),
+ [profileActions.profile.uploadPhotoDone]: (state, { payload, error }) => {
+ if (error) {
+ return state;
+ }
+ if (!state.profile || state.profile.handle !== payload.handle) {
+ return state;
+ }
+ return {
+ ...state,
+ profile: {
+ ...state.profile,
+ photoURL: payload.photoURL,
+ },
+ };
+ },
+ [profileActions.profile.deletePhotoDone]: (state, { payload, error }) => {
+ if (error) {
+ return state;
+ }
+ if (!state.profile || state.profile.handle !== payload.handle) {
+ return state;
+ }
+ return {
+ ...state,
+ profile: {
+ ...state.profile,
+ photoURL: null,
+ },
+ };
+ },
+ [profileActions.profile.updateProfileDone]: (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/index.js b/src/reducers/index.js
index ed0d4829..708b46c5 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -12,6 +12,7 @@ import errors, { factory as errorsFactory } from './errors';
import challenge, { factory as challengeFactory } from './challenge';
import profile, { factory as profileFactory } from './profile';
import members, { factory as membersFactory } from './members';
+import lookup, { factory as lookupFactory } from './lookup';
import memberTasks, { factory as memberTasksFactory } from './member-tasks';
import reviewOpportunity, { factory as reviewOpportunityFactory }
from './reviewOpportunity';
@@ -29,6 +30,7 @@ export function factory(options) {
errors: errorsFactory(options),
challenge: challengeFactory(options),
profile: profileFactory(options),
+ lookup: lookupFactory(options),
members: membersFactory(options),
memberTasks: memberTasksFactory(options),
reviewOpportunity: reviewOpportunityFactory(options),
@@ -36,6 +38,22 @@ export function factory(options) {
});
}
+export const factories = {
+ authFactory,
+ statsFactory,
+ termsFactory,
+ directFactory,
+ groupsFactory,
+ errorsFactory,
+ challengeFactory,
+ profileFactory,
+ lookupFactory,
+ membersFactory,
+ memberTasksFactory,
+ reviewOpportunityFactory,
+ mySubmissionsManagementFactory,
+};
+
export default ({
auth,
stats,
@@ -45,6 +63,7 @@ export default ({
errors,
challenge,
profile,
+ lookup,
members,
memberTasks,
reviewOpportunity,
diff --git a/src/reducers/lookup.js b/src/reducers/lookup.js
new file mode 100644
index 00000000..bf217a85
--- /dev/null
+++ b/src/reducers/lookup.js
@@ -0,0 +1,60 @@
+/**
+ * @module "reducers.lookup"
+ * @desc Reducer for {@link module:actions.lookup} actions.
+ *
+ * State segment managed by this reducer has the following structure:
+ * @param {Array} approvedSkills='' approved skill tags.
+ */
+import _ from 'lodash';
+import { handleActions } from 'redux-actions';
+import logger from '../utils/logger';
+import actions from '../actions/lookup';
+
+/**
+ * Handles LOOKUP/GET_APPROVED_SKILLS action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onGetApprovedSkills(state, { payload, error }) {
+ if (error) {
+ logger.error('Failed to get approved skill tags', payload);
+ return { ...state, loadingApprovedSkillsError: true };
+ }
+
+ return ({
+ ...state,
+ loadingApprovedSkillsError: false,
+ approvedSkills: payload,
+ });
+}
+
+/**
+ * Creates a new Lookup reducer with the specified initial state.
+ * @param {Object} initialState Optional. Initial state.
+ * @return {Function} Lookup reducer.
+ */
+function create(initialState = {}) {
+ const a = actions.lookup;
+ return handleActions({
+ [a.getApprovedSkills]: onGetApprovedSkills,
+ }, _.defaults(initialState, {
+ approvedSkills: [],
+ }));
+}
+
+/**
+ * Factory which creates a new reducer.
+ * @return {Promise}
+ * @resolves {Function(state, action): state} New reducer.
+ */
+export function factory() {
+ return Promise.resolve(create());
+}
+
+/**
+ * @static
+ * @member default
+ * @desc Reducer with default initial state.
+ */
+export default create();
diff --git a/src/reducers/profile.js b/src/reducers/profile.js
index 51970fa3..2f773db4 100644
--- a/src/reducers/profile.js
+++ b/src/reducers/profile.js
@@ -6,6 +6,8 @@
import _ from 'lodash';
import { handleActions } from 'redux-actions';
import actions from '../actions/profile';
+import logger from '../utils/logger';
+import { fireErrorMessage } from '../utils/errors';
/**
* Handles PROFILE/GET_ACHIEVEMENTS_DONE action.
@@ -103,6 +105,339 @@ function onGetStatsDone(state, { payload, error }) {
return ({ ...state, stats: payload, loadingError: false });
}
+/**
+ * Handles PROFILE/GET_ACTIVE_CHALLENGES_COUNT_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onGetActiveChallengesCountDone(state, { payload, error }) {
+ if (error) {
+ return { ...state, loadingError: true };
+ }
+
+ return ({ ...state, activeChallengesCount: payload, loadingError: false });
+}
+
+/**
+ * Handles PROFILE/GET_LINKED_ACCOUNTS_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onGetLinkedAccountsDone(state, { payload, error }) {
+ if (error) {
+ return { ...state, loadingError: true };
+ }
+
+ return { ...state, linkedAccounts: payload.profiles, loadingError: false };
+}
+
+/**
+ * Handles PROFILE/GET_CREDENTIAL_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onGetCredentialDone(state, { payload, error }) {
+ if (error) {
+ return { ...state, loadingError: true };
+ }
+
+ return { ...state, credential: payload.credential, loadingError: false };
+}
+
+/**
+ * Handles PROFILE/GET_EMAIL_PREFERENCES_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onGetEmailPreferencesDone(state, { payload, error }) {
+ if (error) {
+ return { ...state, loadingError: true };
+ }
+
+ return { ...state, emailPreferences: payload.subscriptions, loadingError: false };
+}
+
+/**
+ * Handles PROFILE/UPLOAD_PHOTO_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onUploadPhotoDone(state, { payload, error }) {
+ const newState = { ...state, uploadingPhoto: false };
+
+ if (error) {
+ logger.error('Failed to upload user photo', payload);
+ fireErrorMessage('ERROR: Failed to upload photo!');
+ return newState;
+ }
+
+ if (!newState.info || newState.info.handle !== payload.handle) {
+ return newState;
+ }
+
+ return {
+ ...newState,
+ info: {
+ ...newState.info,
+ photoURL: payload.photoURL,
+ },
+ };
+}
+
+/**
+ * Handles PROFILE/DELETE_PHOTO_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onDeletePhotoDone(state, { payload, error }) {
+ const newState = { ...state, deletingPhoto: false };
+
+ if (error) {
+ logger.error('Failed to delete user photo', payload);
+ fireErrorMessage('ERROR: Failed to delete photo!');
+ return newState;
+ }
+
+ if (!newState.info || newState.info.handle !== payload.handle) {
+ return newState;
+ }
+
+ return {
+ ...newState,
+ info: {
+ ...newState.info,
+ photoURL: null,
+ },
+ };
+}
+
+/**
+ * Handles PROFILE/UPDATE_PROFILE_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onUpdateProfileDone(state, { payload, error }) {
+ const newState = { ...state, updatingProfile: false };
+
+ if (error) {
+ logger.error('Failed to update user profile', payload);
+ fireErrorMessage('ERROR: Failed to update user profile!');
+ return newState;
+ }
+
+ if (!newState.info || newState.info.handle !== payload.handle) {
+ return newState;
+ }
+
+ return {
+ ...newState,
+ info: {
+ ...newState.info,
+ ...payload,
+ },
+ };
+}
+
+/**
+ * Handles PROFILE/ADD_SKILL_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onAddSkillDone(state, { payload, error }) {
+ const newState = { ...state, addingSkill: false };
+
+ if (error) {
+ logger.error('Failed to add user skill', payload);
+ fireErrorMessage('ERROR: Failed to add user skill!');
+ return newState;
+ }
+
+ if (newState.profileForHandle !== payload.handle) {
+ return newState;
+ }
+
+ return {
+ ...newState,
+ skills: payload.skills,
+ };
+}
+
+/**
+ * Handles PROFILE/HIDE_SKILL_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onHideSkillDone(state, { payload, error }) {
+ const newState = { ...state, hidingSkill: false };
+
+ if (error) {
+ logger.error('Failed to remove user skill', payload);
+ fireErrorMessage('ERROR: Failed to remove user skill!');
+ return newState;
+ }
+
+ if (newState.profileForHandle !== payload.handle) {
+ return newState;
+ }
+
+ return {
+ ...newState,
+ skills: payload.skills,
+ };
+}
+
+/**
+ * Handles PROFILE/ADD_WEB_LINK_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onAddWebLinkDone(state, { payload, error }) {
+ const newState = { ...state, addingWebLink: false };
+
+ if (error) {
+ logger.error('Failed to add web link', payload);
+ fireErrorMessage('ERROR: Failed to add web link!');
+ return newState;
+ }
+
+ if (newState.profileForHandle !== payload.handle || !payload.data) {
+ return newState;
+ }
+
+ return {
+ ...newState,
+ externalLinks: [...newState.externalLinks, payload.data],
+ };
+}
+
+/**
+ * Handles PROFILE/DELETE_WEB_LINK_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onDeleteWebLinkDone(state, { payload, error }) {
+ const newState = { ...state, deletingWebLink: false };
+
+ if (error) {
+ logger.error('Failed to delete web link', payload);
+ fireErrorMessage('ERROR: Failed to delete web link!');
+ return newState;
+ }
+
+ if (newState.profileForHandle !== payload.handle || !payload.data) {
+ return newState;
+ }
+
+ return {
+ ...newState,
+ externalLinks: _.filter(newState.externalLinks, el => el.key !== payload.data.key),
+ };
+}
+
+/**
+ * Handles PROFILE/LINK_EXTERNAL_ACCOUNT_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onLinkExternalAccountDone(state, { payload, error }) {
+ const newState = { ...state, linkingExternalAccount: false };
+
+ if (error) {
+ logger.error('Failed to link external account', payload);
+ fireErrorMessage('ERROR: Failed to link external account!');
+ return newState;
+ }
+
+ if (newState.profileForHandle !== payload.handle || !payload.data) {
+ return newState;
+ }
+
+ return {
+ ...newState,
+ linkedAccounts: [...newState.linkedAccounts, payload.data],
+ };
+}
+
+/**
+ * Handles PROFILE/UNLINK_EXTERNAL_ACCOUNT_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onUnlinkExternalAccountDone(state, { payload, error }) {
+ const newState = { ...state, unlinkingExternalAccount: false };
+
+ if (error) {
+ logger.error('Failed to unlink external account', payload);
+ fireErrorMessage('ERROR: Failed to unlink external account!');
+ return newState;
+ }
+
+ if (newState.profileForHandle !== payload.handle) {
+ return newState;
+ }
+
+ return {
+ ...newState,
+ linkedAccounts: _.filter(
+ newState.linkedAccounts,
+ el => el.providerType !== payload.providerType,
+ ),
+ };
+}
+
+/**
+ * Handles PROFILE/SAVE_EMAIL_PREFERENCES_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onSaveEmailPreferencesDone(state, { payload, error }) {
+ const newState = { ...state, savingEmailPreferences: false };
+
+ if (error) {
+ logger.error('Failed to save email preferences', payload);
+ fireErrorMessage('ERROR: Failed to save email preferences!');
+ return newState;
+ }
+
+ if (newState.profileForHandle !== payload.handle || !payload.data) {
+ return newState;
+ }
+
+ return {
+ ...newState,
+ emailPreferences: payload.data.subscriptions,
+ };
+}
+
+/**
+ * Handles PROFILE/UPDATE_PASSWORD_DONE action.
+ * @param {Object} state
+ * @param {Object} action Payload will be JSON from api call
+ * @return {Object} New state
+ */
+function onUpdatePasswordDone(state, { payload, error }) {
+ const newState = { ...state, updatingPassword: false };
+
+ if (error) {
+ logger.error('Failed to update password', payload);
+ }
+ return newState;
+}
+
/**
* Creates a new Profile reducer with the specified initial state.
* @param {Object} initialState Optional. Initial state.
@@ -124,6 +459,36 @@ function create(initialState) {
[a.getSkillsDone]: onGetSkillsDone,
[a.getStatsInit]: state => state,
[a.getStatsDone]: onGetStatsDone,
+ [a.getLinkedAccountsInit]: state => state,
+ [a.getLinkedAccountsDone]: onGetLinkedAccountsDone,
+ [a.getActiveChallengesCountInit]: state => state,
+ [a.getActiveChallengesCountDone]: onGetActiveChallengesCountDone,
+ [a.uploadPhotoInit]: state => ({ ...state, uploadingPhoto: true }),
+ [a.uploadPhotoDone]: onUploadPhotoDone,
+ [a.deletePhotoInit]: state => ({ ...state, deletingPhoto: true }),
+ [a.deletePhotoDone]: onDeletePhotoDone,
+ [a.updateProfileInit]: state => ({ ...state, updatingProfile: true }),
+ [a.updateProfileDone]: onUpdateProfileDone,
+ [a.addSkillInit]: state => ({ ...state, addingSkill: true }),
+ [a.addSkillDone]: onAddSkillDone,
+ [a.hideSkillInit]: state => ({ ...state, hidingSkill: true }),
+ [a.hideSkillDone]: onHideSkillDone,
+ [a.addWebLinkInit]: state => ({ ...state, addingWebLink: true }),
+ [a.addWebLinkDone]: onAddWebLinkDone,
+ [a.deleteWebLinkInit]: state => ({ ...state, deletingWebLink: true }),
+ [a.deleteWebLinkDone]: onDeleteWebLinkDone,
+ [a.linkExternalAccountInit]: state => ({ ...state, linkingExternalAccount: true }),
+ [a.linkExternalAccountDone]: onLinkExternalAccountDone,
+ [a.unlinkExternalAccountInit]: state => ({ ...state, unlinkingExternalAccount: true }),
+ [a.unlinkExternalAccountDone]: onUnlinkExternalAccountDone,
+ [a.getCredentialInit]: state => state,
+ [a.getCredentialDone]: onGetCredentialDone,
+ [a.getEmailPreferencesInit]: state => state,
+ [a.getEmailPreferencesDone]: onGetEmailPreferencesDone,
+ [a.saveEmailPreferencesInit]: state => ({ ...state, savingEmailPreferences: true }),
+ [a.saveEmailPreferencesDone]: onSaveEmailPreferencesDone,
+ [a.updatePasswordInit]: state => ({ ...state, updatingPassword: true }),
+ [a.updatePasswordDone]: onUpdatePasswordDone,
}, _.defaults(initialState, {
achievements: null,
copilot: false,
diff --git a/src/services/api.js b/src/services/api.js
index df363b7d..3d98c3dd 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -170,6 +170,29 @@ class Api {
return this.put(endpoint, JSON.stringify(json));
}
+ /**
+ * Sends PATCH request to the specified endpoint.
+ * @param {String} endpoint
+ * @param {Blob|BufferSource|FormData|String} body
+ * @return {Promise}
+ */
+ patch(endpoint, body) {
+ return this.fetch(endpoint, {
+ body,
+ method: 'PATCH',
+ });
+ }
+
+ /**
+ * Sends PATCH request to the specified endpoint.
+ * @param {String} endpoint
+ * @param {JSON} json
+ * @return {Promise}
+ */
+ patchJson(endpoint, json) {
+ return this.patch(endpoint, JSON.stringify(json));
+ }
+
/**
* Upload with progress
* @param {String} endpoint
diff --git a/src/services/index.js b/src/services/index.js
index af222abd..a71a3306 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -12,6 +12,7 @@ import * as communities from './communities';
import * as reviewOpportunities from './reviewOpportunities';
import * as userSetting from './user-settings';
import * as user from './user';
+import * as lookup from './lookup';
export const services = {
api,
@@ -25,6 +26,7 @@ export const services = {
user,
userSetting,
reviewOpportunities,
+ lookup,
};
export default undefined;
diff --git a/src/services/lookup.js b/src/services/lookup.js
new file mode 100644
index 00000000..60a7e480
--- /dev/null
+++ b/src/services/lookup.js
@@ -0,0 +1,47 @@
+/**
+ * @module "services.lookup"
+ * @desc This module provides a service to get lookup data from Topcoder
+ * via API V3.
+ */
+import qs from 'qs';
+import { getApiResponsePayloadV3 } from '../utils/tc';
+import { getApiV3 } from './api';
+
+class LookupService {
+ /**
+ * @param {String} tokenV3 Optional. Auth token for Topcoder API v3.
+ */
+ constructor(tokenV3) {
+ this.private = {
+ api: getApiV3(tokenV3),
+ tokenV3,
+ };
+ }
+
+ /**
+ * Gets tags.
+ * @param {Object} params Parameters
+ * @return {Promise} Resolves to the tags.
+ */
+ async getTags(params) {
+ const res = await this.private.api.get(`/tags/?${qs.stringify(params)}`);
+ return getApiResponsePayloadV3(res);
+ }
+}
+
+let lastInstance = null;
+/**
+ * Returns a new or existing lookup service.
+ * @param {String} tokenV3 Optional. Auth token for Topcoder API v3.
+ * @return {LookupService} Lookup service object
+ */
+export function getService(tokenV3) {
+ if (!lastInstance || tokenV3 !== lastInstance.private.tokenV3) {
+ lastInstance = new LookupService(tokenV3);
+ }
+ return lastInstance;
+}
+
+/* Using default export would be confusing in this case. */
+export default undefined;
+
diff --git a/src/services/members.js b/src/services/members.js
index ba75e5c7..19948eff 100644
--- a/src/services/members.js
+++ b/src/services/members.js
@@ -4,6 +4,9 @@
* members via API V3.
*/
+/* global XMLHttpRequest */
+import _ from 'lodash';
+import logger from '../utils/logger';
import { getApiResponsePayloadV3 } from '../utils/tc';
import { getApiV3 } from './api';
@@ -96,6 +99,155 @@ class MembersService {
const res = await this.private.api.get(`/members/_suggest/${keyword}`);
return getApiResponsePayloadV3(res);
}
+
+ /**
+ * Adds external web link for member.
+ * @param {String} userHandle The user handle
+ * @param {String} webLink The external web link
+ * @return {Promise} Resolves to the api response content
+ */
+ async addWebLink(userHandle, webLink) {
+ const res = await this.private.api.postJson(`/members/${userHandle}/externalLinks`, { param: { url: webLink } });
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Deletes external web link for member.
+ * @param {String} userHandle The user handle
+ * @param {String} webLinkHandle The external web link handle
+ * @return {Promise} Resolves to the api response content
+ */
+ async deleteWebLink(userHandle, webLinkHandle) {
+ const body = {
+ param: {
+ handle: webLinkHandle,
+ },
+ };
+ const res = await this.private.api.delete(`/members/${userHandle}/externalLinks/${webLinkHandle}`, JSON.stringify(body));
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Adds user skill.
+ * @param {String} handle Topcoder user handle
+ * @param {Number} skillTagId Skill tag id
+ * @return {Promise} Resolves to operation result
+ */
+ async addSkill(handle, skillTagId) {
+ const body = {
+ param: {
+ skills: {
+ [skillTagId]: {
+ hidden: false,
+ },
+ },
+ },
+ };
+ const res = await this.private.api.patchJson(`/members/${handle}/skills`, body);
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Hides user skill.
+ * @param {String} handle Topcoder user handle
+ * @param {Number} skillTagId Skill tag id
+ * @return {Promise} Resolves to operation result
+ */
+ async hideSkill(handle, skillTagId) {
+ const body = {
+ param: {
+ skills: {
+ [skillTagId]: {
+ hidden: true,
+ },
+ },
+ },
+ };
+ const res = await this.private.api.fetch(`/members/${handle}/skills`, {
+ body: JSON.stringify(body),
+ method: 'PATCH',
+ });
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Updates member profile.
+ * @param {Object} profile The profile to update.
+ * @return {Promise} Resolves to the api response content
+ */
+ async updateMemberProfile(profile) {
+ const res = await this.private.api.putJson(`/members/${profile.handle}`, { param: profile });
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Gets presigned url for member photo file.
+ * @param {String} userHandle The user handle
+ * @param {File} file The file to get its presigned url
+ * @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 getApiResponsePayloadV3(res);
+
+ return {
+ preSignedURL: payload.preSignedURL,
+ token: payload.token,
+ file,
+ userHandle,
+ };
+ }
+
+ /**
+ * 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 getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Uploads file to S3.
+ * @param {Object} presignedUrlResponse The presigned url response from
+ * getPresignedUrl() function.
+ * @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);
+ });
+ }
}
let lastInstance = null;
diff --git a/src/services/user.js b/src/services/user.js
index 88718a3a..3eddb2c5 100644
--- a/src/services/user.js
+++ b/src/services/user.js
@@ -3,10 +3,96 @@
* @desc The User service provides functionality related to Topcoder user
* accounts.
*/
+import { config, isomorphy } from 'topcoder-react-utils';
+import logger from '../utils/logger';
import { getApiResponsePayloadV3 } from '../utils/tc';
import { getApiV2, getApiV3 } from './api';
+let auth0;
+if (isomorphy.isClientSide()) {
+ const Auth0 = require('auth0-js'); /* eslint-disable-line global-require */
+ auth0 = new Auth0({
+ domain: config.AUTH0.DOMAIN,
+ clientID: config.AUTH0.CLIENT_ID,
+ callbackOnLocationHash: true,
+ sso: false,
+ });
+}
+
+/**
+ * Gets social user data.
+ * @param {Object} profile The user social profile
+ * @param {*} accessToken The access token
+ * @returns {Object} Social user data
+ */
+function getSocialUserData(profile, accessToken) {
+ const socialProvider = profile.identities[0].connection;
+ let firstName = '';
+ let lastName = '';
+ let handle = '';
+ const email = profile.email || '';
+
+ const socialUserId = profile.user_id.substring(profile.user_id.lastIndexOf('|') + 1);
+ let splitName;
+
+ if (socialProvider === 'google-oauth2') {
+ firstName = profile.given_name;
+ lastName = profile.family_name;
+ handle = profile.nickname;
+ } else if (socialProvider === 'facebook') {
+ firstName = profile.given_name;
+ lastName = profile.family_name;
+ handle = `${firstName}.${lastName}`;
+ } else if (socialProvider === 'twitter') {
+ splitName = profile.name.split(' ');
+ [firstName] = splitName;
+ if (splitName.length > 1) {
+ [, lastName] = splitName;
+ }
+ handle = profile.screen_name;
+ } else if (socialProvider === 'github') {
+ splitName = profile.name.split(' ');
+ [firstName] = splitName;
+ if (splitName.length > 1) {
+ [, lastName] = splitName;
+ }
+ handle = profile.nickname;
+ } else if (socialProvider === 'bitbucket') {
+ firstName = profile.first_name;
+ lastName = profile.last_name;
+ handle = profile.username;
+ } else if (socialProvider === 'stackoverflow') {
+ firstName = profile.first_name;
+ lastName = profile.last_name;
+ handle = socialUserId;
+ } else if (socialProvider === 'dribbble') {
+ firstName = profile.first_name;
+ lastName = profile.last_name;
+ handle = socialUserId;
+ }
+
+ let token = accessToken;
+ let tokenSecret = null;
+ if (profile.identities[0].access_token) {
+ token = profile.identities[0].access_token;
+ }
+ if (profile.identities[0].access_token_secret) {
+ tokenSecret = profile.identities[0].access_token_secret;
+ }
+ return {
+ socialUserId,
+ username: handle,
+ firstname: firstName,
+ lastname: lastName,
+ email,
+ socialProfile: profile,
+ socialProvider,
+ accessToken: token,
+ accessTokenSecret: tokenSecret,
+ };
+}
+
/**
* Service class.
*/
@@ -60,6 +146,163 @@ class User {
const res = await this.private.api.get(url);
return (await getApiResponsePayloadV3(res))[0];
}
+
+ /**
+ * Gets email preferences.
+ *
+ * NOTE: Only admins are authorized to use the underlying endpoint.
+ *
+ * @param {Number} userId The TopCoder user id
+ * @returns {Promise} Resolves to the email preferences result
+ */
+ async getEmailPreferences(userId) {
+ const url = `/users/${userId}/preferences/email`;
+ const res = await this.private.api.get(url);
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Saves email preferences.
+ *
+ * NOTE: Only admins are authorized to use the underlying endpoint.
+ *
+ * @param {Object} user The TopCoder user
+ * @param {Object} preferences The email preferences
+ * @returns {Promise} Resolves to the email preferences result
+ */
+ async saveEmailPreferences({ firstName, lastName, userId }, preferences) {
+ const settings = {
+ firstName,
+ lastName,
+ subscriptions: {},
+ };
+
+ if (!preferences) {
+ settings.subscriptions.TOPCODER_NL_GEN = true;
+ } else {
+ settings.subscriptions = preferences;
+ }
+ const url = `/users/${userId}/preferences/email`;
+
+ const res = await this.private.api.putJson(url, { param: settings });
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Gets credential for the specified user id.
+ *
+ * NOTE: Only admins are authorized to use the underlying endpoint.
+ *
+ * @param {Number} userId The user id
+ * @return {Promise} Resolves to the linked accounts array.
+ */
+ async getCredential(userId) {
+ const url = `/users/${userId}?fields=credential`;
+ const res = await this.private.api.get(url);
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Updates user password.
+ *
+ * NOTE: Only admins are authorized to use the underlying endpoint.
+ *
+ * @param {Number} userId The user id
+ * @param {String} newPassword The new password
+ * @param {String} oldPassword The old password
+ * @return {Promise} Resolves to the update result.
+ */
+ async updatePassword(userId, newPassword, oldPassword) {
+ const credential = {
+ password: newPassword,
+ currentPassword: oldPassword,
+ };
+
+ const url = `/users/${userId}`;
+ const res = await this.private.api.patchJson(url, { param: { credential } });
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Gets linked accounts for the specified user id.
+ *
+ * NOTE: Only admins are authorized to use the underlying endpoint.
+ *
+ * @param {Number} userId The user id
+ * @return {Promise} Resolves to the linked accounts array.
+ */
+ async getLinkedAccounts(userId) {
+ const url = `/users/${userId}?fields=profiles`;
+ const res = await this.private.api.get(url);
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Unlinks external account.
+ * @param {Number} userId The TopCoder user id
+ * @param {String} provider The external account service provider
+ * @returns {Promise} Resolves to the unlink result
+ */
+ async unlinkExternalAccount(userId, provider) {
+ const url = `/users/${userId}/profiles/${provider}`;
+ const res = await this.private.api.delete(url);
+ return getApiResponsePayloadV3(res);
+ }
+
+ /**
+ * Links external account.
+ * @param {Number} userId The TopCoder user id
+ * @param {String} provider The external account service provider
+ * @param {String} callbackUrl Optional. The callback url
+ * @returns {Promise} Resolves to the linked account result
+ */
+ async linkExternalAccount(userId, provider, callbackUrl) {
+ return new Promise((resolve, reject) => {
+ auth0.signin(
+ {
+ popup: true,
+ connection: provider,
+ scope: 'openid profile offline_access',
+ state: callbackUrl,
+ },
+ (authError, profile, idToken, accessToken) => {
+ if (authError) {
+ logger.error('Error signing in - onSocialLoginFailure', authError);
+ reject(authError);
+ return;
+ }
+
+ const socialData = getSocialUserData(profile, accessToken);
+
+ const postData = {
+ userId: socialData.socialUserId,
+ name: socialData.username,
+ email: socialData.email,
+ emailVerified: false,
+ providerType: socialData.socialProvider,
+ context: {
+ handle: socialData.username,
+ accessToken: socialData.accessToken,
+ auth0UserId: profile.user_id,
+ },
+ };
+ if (socialData.accessTokenSecret) {
+ postData.context.accessTokenSecret = socialData.accessTokenSecret;
+ }
+ logger.debug(`link API postdata: ${JSON.stringify(postData)}`);
+ this.private.api.postJson(`/users/${userId}/profiles`, { param: postData })
+ .then(resp => getApiResponsePayloadV3(resp).then((result) => {
+ logger.debug(`Succesfully linked account: ${JSON.stringify(result)}`);
+ resolve(postData);
+ }))
+ .catch((err) => {
+ logger.error('Error linking account', err);
+ reject(err);
+ });
+ },
+ );
+ });
+ }
}
let lastInstance = null;