From 11a4c1c95dc4a35e2d5502cdebe890543c8c003f Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Thu, 20 Jul 2017 02:39:39 +0100 Subject: [PATCH] Implemented new Achievement and Badge: Invited a Friend (Fixes #8615) (#8819) * Added text to locale * Added achievement to content and libs * Added achievement modal * Added achievement to notification model and controller * Added achievement to user schema * Grant achievement to inviter when user registers using emailed link * Fix icon name * Added integration test * Fix linting * Added sprite --- .../user/auth/POST-register_local.test.js | 22 ++++++++++++++++++ .../achievements/achievement-friends2x.png | Bin 0 -> 3570 bytes .../js/controllers/notificationCtrl.js | 4 ++++ website/common/locales/en/groups.json | 2 ++ website/common/script/content/achievements.js | 5 ++++ website/common/script/libs/achievements.js | 1 + website/server/controllers/api-v3/auth.js | 8 +++++++ website/server/models/user/schema.js | 1 + website/server/models/userNotification.js | 1 + website/views/shared/modals/achievements.jade | 11 +++++++++ 10 files changed, 55 insertions(+) create mode 100644 website/assets/sprites/spritesmith/achievements/achievement-friends2x.png diff --git a/test/api/v3/integration/user/auth/POST-register_local.test.js b/test/api/v3/integration/user/auth/POST-register_local.test.js index 39683ce5b4..18eedd7c0f 100644 --- a/test/api/v3/integration/user/auth/POST-register_local.test.js +++ b/test/api/v3/integration/user/auth/POST-register_local.test.js @@ -516,6 +516,28 @@ describe('POST /user/auth/local/register', () => { }); }); + it('awards achievement to inviter', async () => { + let { group, groupLeader } = await createAndPopulateGroup({ + groupDetails: { type: 'party', privacy: 'private' }, + }); + + let invite = encrypt(JSON.stringify({ + id: group._id, + inviter: groupLeader._id, + sentAt: Date.now(), + })); + + await api.post(`/user/auth/local/register?groupInvite=${invite}`, { + username, + email, + password, + confirmPassword: password, + }); + + await groupLeader.sync(); + expect(groupLeader.achievements.invitedFriend).to.be.true; + }); + it('user not added to a party on expired invite', async () => { let { group, groupLeader } = await createAndPopulateGroup({ groupDetails: { type: 'party', privacy: 'private' }, diff --git a/website/assets/sprites/spritesmith/achievements/achievement-friends2x.png b/website/assets/sprites/spritesmith/achievements/achievement-friends2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cf70f7c5ca5168ac2fd62a621c70dc02ba999e2a GIT binary patch literal 3570 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 literal 0 HcmV?d00001 diff --git a/website/client-old/js/controllers/notificationCtrl.js b/website/client-old/js/controllers/notificationCtrl.js index 0653b88e10..2caf517039 100644 --- a/website/client-old/js/controllers/notificationCtrl.js +++ b/website/client-old/js/controllers/notificationCtrl.js @@ -228,6 +228,10 @@ habitrpg.controller('NotificationCtrl', $rootScope.playSound('Achievement_Unlocked'); Achievement.displayAchievement('joinedChallenge', {size: 'md'}); break; + case 'INVITED_FRIEND_ACHIEVEMENT': + $rootScope.playSound('Achievement_Unlocked'); + Achievement.displayAchievement('invitedFriend', {size: 'md'}); + break; case 'NEW_CONTRIBUTOR_LEVEL': $rootScope.playSound('Achievement_Unlocked'); Achievement.displayAchievement('contributor', {size: 'md'}); diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 2d6ee1c816..99338176d7 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -149,6 +149,8 @@ "sendInvitations": "Send Invitations", "invitationsSent": "Invitations sent!", "invitationSent": "Invitation sent!", + "invitedFriend": "Invited a Friend", + "invitedFriendText": "This user invited a friend (or friends) who joined them on their adventure!", "inviteAlertInfo2": "Or share this link (copy/paste):", "inviteLimitReached": "You have already sent the maximum number of email invitations. We have a limit to prevent spamming, however if you would like more, please contact us at <%= techAssistanceEmail %> and we'll be happy to discuss it!", "sendGiftHeading": "Send Gift to <%= name %>", diff --git a/website/common/script/content/achievements.js b/website/common/script/content/achievements.js index ca2a7f9089..ad024101ed 100644 --- a/website/common/script/content/achievements.js +++ b/website/common/script/content/achievements.js @@ -112,6 +112,11 @@ let basicAchievs = { titleKey: 'joinedChallenge', textKey: 'joinedChallengeText', }, + invitedFriend: { + icon: 'achievement-friends', + titleKey: 'invitedFriend', + textKey: 'invitedFriendText', + }, }; Object.assign(achievementsData, basicAchievs); diff --git a/website/common/script/libs/achievements.js b/website/common/script/libs/achievements.js index 1d801d7d76..aa742b7960 100644 --- a/website/common/script/libs/achievements.js +++ b/website/common/script/libs/achievements.js @@ -182,6 +182,7 @@ function _getBasicAchievements (user, language) { _addSimple(result, user, {path: 'joinedGuild', language}); _addSimple(result, user, {path: 'royallyLoyal', language}); _addSimple(result, user, {path: 'joinedChallenge', language}); + _addSimple(result, user, {path: 'invitedFriend', language}); _addSimpleWithMasterCount(result, user, {path: 'beastMaster', language}); _addSimpleWithMasterCount(result, user, {path: 'mountMaster', language}); diff --git a/website/server/controllers/api-v3/auth.js b/website/server/controllers/api-v3/auth.js index 53a3548884..8990a40c98 100644 --- a/website/server/controllers/api-v3/auth.js +++ b/website/server/controllers/api-v3/auth.js @@ -49,6 +49,14 @@ async function _handleGroupInvitation (user, invite) { } else { user.invitations.guilds.push({id: group._id, name: group.name, inviter}); } + + // award the inviter with 'Invited a Friend' achievement + inviter = await User.findById(inviter); + if (!inviter.achievements.invitedFriend) { + inviter.achievements.invitedFriend = true; + inviter.addNotification('INVITED_FRIEND_ACHIEVEMENT'); + await inviter.save(); + } } catch (err) { logger.error(err); } diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index 915ed65c05..454d697b14 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -117,6 +117,7 @@ let schema = new Schema({ royallyLoyal: Boolean, joinedGuild: Boolean, joinedChallenge: Boolean, + invitedFriend: Boolean, }, backer: { diff --git a/website/server/models/userNotification.js b/website/server/models/userNotification.js index 72f790a56a..f0e445e524 100644 --- a/website/server/models/userNotification.js +++ b/website/server/models/userNotification.js @@ -21,6 +21,7 @@ const NOTIFICATION_TYPES = [ 'GUILD_PROMPT', 'GUILD_JOINED_ACHIEVEMENT', 'CHALLENGE_JOINED_ACHIEVEMENT', + 'INVITED_FRIEND_ACHIEVEMENT', ]; const Schema = mongoose.Schema; diff --git a/website/views/shared/modals/achievements.jade b/website/views/shared/modals/achievements.jade index 179c59a9b3..16f93961be 100644 --- a/website/views/shared/modals/achievements.jade +++ b/website/views/shared/modals/achievements.jade @@ -174,3 +174,14 @@ script(id='modals/achievements/joinedChallenge.html', type='text/ng-template') br button.btn.btn-primary(ng-click='$close()')=env.t('huzzah') +achievementFooter + +// Invited Friend +script(id='modals/achievements/invitedFriend.html', type='text/ng-template') + .modal-content(style='min-width:28em') + .modal-body.text-center + h3(style='margin-bottom:0')=env.t('modalAchievement') + +achievementAvatar('friends',0) + p=env.t('invitedFriendText') + br + button.btn.btn-primary(ng-click='$close()')=env.t('huzzah') + +achievementFooter