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;
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: