diff --git a/app/index.js b/app/index.js index 88d8a918b..30f039869 100644 --- a/app/index.js +++ b/app/index.js @@ -63,6 +63,7 @@ require('../assets/css/sitemap/sitemap.scss') require('../assets/css/settings/update-password.scss') require('../assets/css/settings/settings.scss') require('../assets/css/settings/preferences.scss') +require('../assets/css/settings/email.scss') require('../assets/css/settings/edit-profile.scss') require('../assets/css/settings/account-info.scss') require('../assets/css/profile/subtrack.scss') diff --git a/app/services/api.service.js b/app/services/api.service.js index 8881f4d4d..9f7735975 100644 --- a/app/services/api.service.js +++ b/app/services/api.service.js @@ -74,6 +74,8 @@ import _ from 'lodash' case 'SUBMISSIONS': case 'USER': return _getRestangularV3(CONSTANTS.AUTH_API_URL) + case 'MAILCHIMP': + return _getRestangularV3(CONSTANTS.INTERNAL_API_URL) default: return _getRestangularV3() } @@ -93,6 +95,10 @@ import _ from 'lodash' }) .setDefaultHeaders({ 'Content-Type': 'application/json' }) .addRequestInterceptor(function(element, operation, what, url) { + // for mailchimp api, don't add param field in the body + if (url.indexOf('mailchimp') > -1) { + return element + } if (url.indexOf('members') > -1 || (operation.toLowerCase() === 'post' && url.indexOf('profiles') > -1)) { return { param: element diff --git a/app/services/mailchimp.service.js b/app/services/mailchimp.service.js new file mode 100644 index 000000000..5b5cbfdbd --- /dev/null +++ b/app/services/mailchimp.service.js @@ -0,0 +1,78 @@ +import angular from 'angular' + +(function() { + 'use strict' + + angular.module('tc.services').factory('MailchimpService', MailchimpService) + + MailchimpService.$inject = ['$http', 'logger', 'Restangular', 'CONSTANTS', 'ApiService', '$q'] + + function MailchimpService($http, logger, Restangular, CONSTANTS, ApiService, $q) { + var mailchimpApi = ApiService.getApiServiceProvider('MAILCHIMP') + var service = { + getMemberSubscription: getMemberSubscription, + addSubscription: addSubscription + } + return service + + function getMemberSubscription(user) { + return $q(function(resolve, reject) { + mailchimpApi.one('mailchimp/lists', CONSTANTS.MAILCHIMP_LIST_ID) + .one('members', user.userId).get() + .then(function(resp) { + resolve(resp) + }) + .catch(function(err) { + if (err.status === 404) { + logger.debug('Member subscription not found') + resolve() + return + } + logger.error('Error getting member to subscription list', err) + + var errorStatus = 'FATAL_ERROR' + reject({ + status: errorStatus, + msg: err.errorMessage + }) + }) + }) + } + + + function addSubscription(user, preferences) { + var subscription = { + userId: user.userId, + firstName: user.firstName, + lastName: user.lastName, + interests: {} + } + if (!preferences) { + subscription.interests[CONSTANTS.MAILCHIMP_NL_TCO] = true + subscription.interests[CONSTANTS.MAILCHIMP_NL_IOS] = true + subscription.interests[CONSTANTS.MAILCHIMP_NL_DEV] = true + subscription.interests[CONSTANTS.MAILCHIMP_NL_DESIGN] = true + subscription.interests[CONSTANTS.MAILCHIMP_NL_DATA] = true + } else { + subscription.interests = preferences + } + return $q(function(resolve, reject) { + mailchimpApi.one('mailchimp/lists', CONSTANTS.MAILCHIMP_LIST_ID) + .customPUT(subscription, 'members') + .then(function(resp) { + resolve(resp) + }) + .catch(function(err) { + logger.error('Error adding member to subscription list', err) + + var errorStatus = 'FATAL_ERROR' + + reject({ + status: errorStatus, + msg: err.errorMessage + }) + }) + }) + } + } +})() diff --git a/app/settings/email/email.controller.js b/app/settings/email/email.controller.js new file mode 100644 index 000000000..dbee69659 --- /dev/null +++ b/app/settings/email/email.controller.js @@ -0,0 +1,104 @@ +import angular from 'angular' + +(function () { + 'use strict' + + angular.module('tc.settings').controller('EmailSettingsController', EmailSettingsController) + + EmailSettingsController.$inject = ['$rootScope', 'userProfile', 'ProfileService', 'MailchimpService', 'logger', 'CONSTANTS', 'toaster', '$q', '$scope'] + + function EmailSettingsController($rootScope, userProfile, ProfileService, MailchimpService, logger, CONSTANTS, toaster, $q, $scope) { + var vm = this + vm.loading = false + vm.saving = false + vm.isDirty = isDirty + vm.save = save + + activate() + + function activate() { + vm.newsletters = [ + { + id: CONSTANTS.MAILCHIMP_NL_DEV, + name: 'Developer Newsletter', + desc: 'Software architecture, component assembly, application development and bug hunting', + enabled: false, + dirty: false + }, + { + id: CONSTANTS.MAILCHIMP_NL_DESIGN, + name: 'Design Newsletter', + desc: 'Website, mobile, and product design; UI and UX', + enabled: false, + dirty: false + }, + { + id: CONSTANTS.MAILCHIMP_NL_DATA, + name: 'Data Science Newsletter', + desc: 'Algorithm and data structures, statistical analysis', + enabled: false, + dirty: false + }, + { + id: CONSTANTS.MAILCHIMP_NL_TCO, + name: 'TCO Newsletter', + desc: 'Software architecture, component assembly, application development and bug hunting', + enabled: false, + dirty: false + }, + { + id: CONSTANTS.MAILCHIMP_NL_IOS, + name: 'iOS Community Newsletter', + desc: 'Software architecture, component assembly, application development and bug hunting', + enabled: false, + dirty: false + } + ] + + vm.loading = true + MailchimpService.getMemberSubscription(userProfile).then(function(resp) { + vm.loading = false + if (resp.interests) { + vm.newsletters.forEach(function(newsletter) { + if (resp.interests[newsletter.id]) { + newsletter.enabled = true + } + }) + } + }) + } + + function isDirty() { + var dirty = false + vm.newsletters.forEach(function(newsletter) { + if (newsletter.dirty){ + dirty = true + } + }) + return dirty + } + + function save() { + vm.saving = true + var preferences = {} + vm.newsletters.forEach(function(newsletter) { + preferences[newsletter.id] = newsletter.enabled + }) + MailchimpService.addSubscription(userProfile, preferences).then(function(resp) { + vm.loading = false + vm.saving = false + // reset dirty state for all newsletter options + vm.newsletters.forEach(function(newsletter) { + newsletter.dirty = false + }) + toaster.pop('success', 'Success!', 'Preferences updated.') + }).catch(function(err) { + logger.error('Could not update email preferences', err) + vm.loading = false + vm.saving = false + + toaster.pop('error', 'Whoops!', 'Something went wrong. Please try again later.') + }) + } + } +})() diff --git a/app/settings/email/email.jade b/app/settings/email/email.jade new file mode 100644 index 000000000..37977606f --- /dev/null +++ b/app/settings/email/email.jade @@ -0,0 +1,30 @@ +.email-preferences-container + .settings-section.newsletters + .section-info + h2 Always be up to date + .description Select the email notifications that you'd like to receive. We send them once every week, so you can be up to date with latest news, events and callenges. + + .section-fields + .processing(ng-show="vm.loading") + i.fa.fa-spinner.fa-spin + .newsletters(ng-hide="vm.loading") + .newsletter(ng-repeat="newsletter in vm.newsletters") + + .content(ng-class="{ disabled: !newsletter.enabled }") + .newsletter-details + .text + span.title {{newsletter.name}} + .description + span(ng-bind="newsletter.desc") + + .onoffswitch + input.onoffswitch-checkbox(type='checkbox', name='onoffswitch', checked='', ng-model="newsletter.enabled", id="{{newsletter.name}}-onoffswitch", ng-change="newsletter.dirty = !newsletter.dirty") + label.onoffswitch-label(for='{{newsletter.name}}-onoffswitch') + span.onoffswitch-inner + span.onoffswitch-switch + .save-section + button.tc-btn.tc-btn-l.done-button( + type="button", + tc-busy-button, tc-busy-when="vm.saving", + ng-click="vm.save()", + ng-disabled="!vm.isDirty()") Save diff --git a/app/settings/preferences/preferences.jade b/app/settings/preferences/preferences.jade index 7e9c5f3b2..b836ce4dd 100644 --- a/app/settings/preferences/preferences.jade +++ b/app/settings/preferences/preferences.jade @@ -1,12 +1,5 @@ .preferences-container ul - li - a(href="http://thecloud.appirio.com/email_prefs_request.html", target="_blank") - .icon - i.fa.fa-envelope - span Email Preferences - .description Specify what kind of email you would like to receive from us - li a(href="https://apps.{{DOMAIN}}/forums/?module=Settings", target="_blank") .icon diff --git a/app/settings/settings.jade b/app/settings/settings.jade index 8c3cd2d0a..8923a13f5 100644 --- a/app/settings/settings.jade +++ b/app/settings/settings.jade @@ -10,6 +10,9 @@ li a(ui-sref="settings.account", ui-sref-active="active-tab") Account + li + a(ui-sref="settings.email", ui-sref-active="active-tab") Email + li a(ui-sref="settings.preferences", ui-sref-active="active-tab") Preferences diff --git a/app/settings/settings.routes.js b/app/settings/settings.routes.js index 0dce4be57..031faed38 100644 --- a/app/settings/settings.routes.js +++ b/app/settings/settings.routes.js @@ -47,6 +47,23 @@ import angular from 'angular' title: 'Account Info' } }, + 'settings.email': { + url: 'email/', + template: require('./email/email')(), + controller: 'EmailSettingsController', + controllerAs: 'vm', + data: { + title: 'Email Preferences' + }, + resolve: { + userIdentity: ['UserService', function(UserService) { + return UserService.getUserIdentity() + }], + userProfile: ['userIdentity', 'ProfileService', function(userIdentity, ProfileService) { + return ProfileService.getUserProfile(userIdentity.handle.toLowerCase()) + }] + } + }, 'settings.preferences': { url: 'preferences/', template: require('./preferences/preferences')(), diff --git a/app/skill-picker/skill-picker.controller.js b/app/skill-picker/skill-picker.controller.js index 75b0668d0..a564c876c 100644 --- a/app/skill-picker/skill-picker.controller.js +++ b/app/skill-picker/skill-picker.controller.js @@ -6,9 +6,9 @@ import _ from 'lodash' angular.module('tc.skill-picker').controller('SkillPickerController', SkillPickerController) - SkillPickerController.$inject = ['$scope', 'CONSTANTS', 'ProfileService', '$state', 'userProfile', 'featuredSkills', 'logger', 'toaster', 'MemberCertService', '$q'] + SkillPickerController.$inject = ['$scope', 'CONSTANTS', 'ProfileService', '$state', 'userProfile', 'featuredSkills', 'logger', 'toaster', 'MemberCertService', '$q', 'MailchimpService'] - function SkillPickerController($scope, CONSTANTS, ProfileService, $state, userProfile, featuredSkills, logger, toaster, MemberCertService, $q) { + function SkillPickerController($scope, CONSTANTS, ProfileService, $state, userProfile, featuredSkills, logger, toaster, MemberCertService, $q, MailchimpService) { var vm = this vm.ASSET_PREFIX = CONSTANTS.ASSET_PREFIX vm.IOS_PROGRAM_ID = CONSTANTS.SWIFT_PROGRAM_ID @@ -32,6 +32,7 @@ import _ from 'lodash' * Activates the controller. */ function activate() { + addToMailingList() initCommunities() checkCommunityStatus() } @@ -137,6 +138,25 @@ import _ from 'lodash' } } + function addToMailingList() { + return MailchimpService.getMemberSubscription(userProfile).then(function(subscription) { + logger.debug(subscription) + if (!subscription) { + return MailchimpService.addSubscription(userProfile).then(function(resp) { + logger.debug(resp) + }).catch(function(err) { + // no error to user + //TODO some error alert to community admin + logger.debug('error in adding user to member list') + }) + } + }).catch(function(err) { + // no error to user + //TODO some error alert to community admin + logger.debug('error in adding user to member list') + }) + } + /** * Persists the user's altered information. */ diff --git a/app/skill-picker/skill-picker.spec.js b/app/skill-picker/skill-picker.spec.js index 7a2100fc7..70dd236c1 100644 --- a/app/skill-picker/skill-picker.spec.js +++ b/app/skill-picker/skill-picker.spec.js @@ -3,12 +3,12 @@ const mockData = require('../../tests/test-helpers/mock-data') describe('Skill Picker Controller', function() { var vm - var toasterSvc, memberCertService, profileService, state + var toasterSvc, memberCertService, profileService, mailchimpService, state var mockProfile = mockData.getMockProfile() beforeEach(function() { bard.appModule('tc.skill-picker') - bard.inject(this, '$controller', '$rootScope', '$q', 'MemberCertService', 'ProfileService', 'toaster', 'CONSTANTS') + bard.inject(this, '$controller', '$rootScope', '$q', 'MemberCertService', 'ProfileService', 'MailchimpService', 'toaster', 'CONSTANTS') memberCertService = MemberCertService profileService = ProfileService @@ -52,6 +52,29 @@ describe('Skill Picker Controller', function() { return deferred.promise }) + mailchimpService = MailchimpService + sinon.stub(mailchimpService, 'getMemberSubscription', function(user) { + var deferred = $q.defer() + if (user.userId === 10336829) { + deferred.resolve() + } else if (user.userId === 12345) { + var resp = { id: 'sdku34i5kdk', email_address: user.email} + deferred.resolve(resp) + } else { + deferred.reject() + } + return deferred.promise + }) + sinon.stub(mailchimpService, 'addSubscription', function(user) { + var deferred = $q.defer() + if (user.userId === 10336829) { + deferred.resolve() + } else { + deferred.reject() + } + return deferred.promise + }) + // mocks the toaster service toasterSvc = toaster bard.mockService(toaster, { @@ -115,6 +138,39 @@ describe('Skill Picker Controller', function() { CONSTANTS.SWIFT_PROGRAM_ID = origSwiftProgId }) + it('should call mailchimp service to add subscription', function() { + expect(vm).to.exist + // getMemberSubscription should always be called + expect(mailchimpService.getMemberSubscription).to.be.calledOnce + // addSubscription should be called once if not subscribed + // getMemberSubscription service mock returns null for mockProfile.userId + expect(mailchimpService.addSubscription).to.be.calledOnce + }) + + it('should not call mailchimp service to add subscription', function() { + // reset getMemberSubscription, addSubscription spy's called count + mailchimpService.getMemberSubscription.reset() + mailchimpService.addSubscription.reset() + var scope = $rootScope.$new() + + var profile = angular.copy(mockProfile) + // update userId to return valid object in service mock + profile.userId = 12345 + vm = $controller('SkillPickerController', { + $scope: scope, + userProfile: profile, + featuredSkills: [], + $state: state + }) + $rootScope.$digest() + expect(vm).to.exist + // getMemberSubscription should always be called + expect(mailchimpService.getMemberSubscription).to.be.calledOnce + // addSubscription should not be called if already subscribed + // getMemberSubscription service mock returns valid object for userId 12345 + expect(mailchimpService.addSubscription).not.to.be.called + }) + it('should add skill ', function() { vm.toggleSkill(409) expect(vm.mySkills).to.exist.have.length(1) diff --git a/app/topcoder.constants.js b/app/topcoder.constants.js index cab991847..bf5f73ea4 100644 --- a/app/topcoder.constants.js +++ b/app/topcoder.constants.js @@ -4,6 +4,7 @@ angular.module('CONSTANTS', []).constant('CONSTANTS', { 'API_URL' : process.env.API_URL, 'AUTH_API_URL' : process.env.AUTH_API_URL, 'API_URL_V2' : process.env.API_URL_V2, + 'INTERNAL_API_URL' : process.env.INTERNAL_API_URL, 'ASSET_PREFIX' : process.env.ASSET_PREFIX || '', 'auth0Callback' : process.env.auth0Callback, 'auth0Domain' : process.env.auth0Domain, @@ -19,6 +20,13 @@ angular.module('CONSTANTS', []).constant('CONSTANTS', { 'PHOTO_LINK_LOCATION' : process.env.PHOTO_LINK_LOCATION, 'SWIFT_PROGRAM_URL' : process.env.SWIFT_PROGRAM_URL, 'TCO16_URL' : process.env.TCO16_URL, + 'MAILCHIMP_LIST_ID' : process.env.MAILCHIMP_LIST_ID, + 'MAILCHIMP_NL_CATEGORY_ID': process.env.MAILCHIMP_NL_CATEGORY_ID, + 'MAILCHIMP_NL_TCO' : process.env.MAILCHIMP_NL_TCO, + 'MAILCHIMP_NL_IOS' : process.env.MAILCHIMP_NL_IOS, + 'MAILCHIMP_NL_DEV' : process.env.MAILCHIMP_NL_DEV, + 'MAILCHIMP_NL_DESIGN' : process.env.MAILCHIMP_NL_DESIGN, + 'MAILCHIMP_NL_DATA' : process.env.MAILCHIMP_NL_DATA, 'NEW_CHALLENGES_URL' : 'https://www.topcoder.com/challenges/develop/upcoming/', 'SWIFT_PROGRAM_ID' : 3445, diff --git a/assets/css/settings/email.scss b/assets/css/settings/email.scss new file mode 100644 index 000000000..2dab3612b --- /dev/null +++ b/assets/css/settings/email.scss @@ -0,0 +1,92 @@ +@import 'topcoder/tc-includes'; + +.email-preferences-container { + width: 100%; + padding: 0 60px 30px; + + .processing { + display: flex; + justify-content: center; + align-items: center; + } + + .newsletters { + .newsletter { + width: 100%; + display: flex; + flex-direction: column; + align-items: left; + + .content { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 15px 0; + border-bottom: 1px solid $gray-light; + width: 100%; + transition: background-color .15s; + + &.disabled { + background-color: $gray-lightest; + } + + .newsletter-details { + display: flex; + flex-direction: row; + align-items: center; + padding-left: 10px; + + .icon { + &.disabled { + color: #b7b7b7; + } + img { + height: 32px; + width: 32px; + } + span { + @include font-with-weight('Sofia Pro', 500); + font-size: 16px; + } + } + } + } + .text { + margin-left: 15px; + .title { + font-size: 16px; + line-height: 28px; + @include sofia-pro-medium; + text-transform: uppercase; + transition: .1s color; + &.disabled { + color: #b7b7b7; + } + } + .description { + @include merriweather-sans-regular; + font-size: 13px; + margin-top: 4px; + color: $accent-gray; + } + } + } + } + + .save-section { + width: 100%; + margin: 0 auto; + background-color: #fcfcfc; + border-top: 1px solid #f0f0f0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + button.save { + margin-bottom: 15px; + margin-top: 15px; + width: 100px; + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 97f576d82..8f93e9e96 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "angular-mocks": "^1.4.9", - "appirio-tech-webpack-config": "^0.2.0", + "appirio-tech-webpack-config": "^0.3.0", "babel-polyfill": "^6.7.2", "bardjs": "^0.1.8", "bower": "^1.6.8",